0votos

Refactorizar soluciones en Haskell

por josejuan hace 6 años

Realmente aquí refactorizo una función de test, pero sirve igualmente. He cambiado una función de test "matemática" por una función de test "constructiva".

Mi idea no es crear un desafío nuevo, sino refactorizar la solucion de un desafío antiguo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
-- (primero la función de test original, luego la función de test final) 
-- De la solución http://www.solveet.com/exercises/Aberigua-en-que-piso-esta-cada-numero-/167/solution-1207 
 
 
{-- 
  Función original "matemática". 
 
  Dado el nº, piso y lado del cuadrado en el que se encuentra su celda inf-der, 
  genera la infinita lista de nºs y el piso en el que están. 
--} 
generarPisos :: Int -> Int -> Int -> [(Int, Int)] 
generarPisos n0 p l0 = s1 ++ s2 ++ s3 ++ s4 ++ generarPisos nid pi l 
  where s1 = map     (\n -> (n, pi)) $ take (l - 1) [n, n + 1..] 
        s3 = map     (\n -> (n, ps)) $ take (l - 1) [nsi, nsi + 1..] 
        s2 = zipWith (\i n -> (n, pi + i)) [1..l - 1] [nii, nii + 1..] 
        s4 = zipWith (\i n -> (n, ps - i)) [1..l - 1] [nsd, nsd + 1..] 
 
        n = n0 + 1              -- Menor nº de nuestro cuadrado 
        l = l0 + 2              -- Lado de nuestro cuadrado 
 
        nii = n   + l - 1       -- El nº siguiente de la esquina inf-izq 
        nsi = nii + l - 1       -- El nº siguiente de la esquina sup-izq 
        nsd = nsi + l - 1       -- El nº siguiente de la esquina sup-der 
        nid = nsd + l - 2       -- El nº de la esquina inf-der 
 
        pi = p - 1              -- Planta inferior 
        ps = p + l - 2          -- Planta superior 
 
todosPisos :: [(Int, Int)] 
todosPisos = generarPisos 1 0 1 
 
 
 
 
{-- 
   Función final "constructiva". 
 
   La estrategia es como en Logo, se sigue la ruta de una tortuga. 
--} 
 
todosPisos :: [( Int -- Nº 
               , Int -- Piso en que está 
               )] 
todosPisos = (1, 0): 
               (logo' 1 0 $ zip direcciones saltos)       -- Lista infinita haciendo logo. 
 
  where 
 
        -- 0 - Sur, 1 - Oeste, 2 - Norte, 3 - Este 
        direcciones     = cycle [0, 1, 2, 3]              -- 0, 1, 2, 3, 0, 1, 2, 3, ... 
 
        -- Los saltos son el nº de nºs consecutivos en que el logo va en una dirección 
        saltos          = concatMap (replicate 2) [1..]   -- 1, 1, 2, 2, 3, 3, ..., n, n, ... 
 
        -- Hacemos el logo del salto y dirección actuales y añadimos los siguientes 
        logo' n p ((d, s):xs) = xs' ++ logo' n' p' xs 
          where (xs', (n', p')) = logo d n p s 
 
-- Un lodo es, dada una dirección (d), un nº (n), un piso (p) y un nº de saltos (s), 
-- los nºs y piso generados en esa dirección, devolviendo el último generado. 
logo :: Int -> Int -> Int -> Int -> ([(Int, Int)], (Int, Int)) 
logo d n p s = (map f [1..s], f s)          -- Un salto de (s) elementos y el último elemento. 
  where f i = ( n + i                       -- El nº i-ésimo después de (n) 
              , p + [-i, 0, i, 0]!!d)       -- El peso i-ésimo después de (n) 
18 comentarios
0votos

Escrito por Enric Jean hace 6 años

Muchas gracias Jose Juan por participar en este desafío. Aun sin yo conocer este lenguaje se nota un cambio a mejor en lo referente a los nombre de variables y comentarios. Pero si me dijeran que hace el codigo aun no sabría que decir. A mi forma de verlo me gusta hacer una explicación en pseudocódigo de lo que hace. Buscaré un ejemplo para hacerlo. Un cordial saludo.
0votos

Escrito por josejuan hace 6 años

"A mi forma de verlo me gusta hacer una explicación en pseudocódigo de lo que hace"

Lógicamente para entender el sentido de la función debe leerse el problema que resuelve. Una vez sabido ésto, es obvio lo que hace. No he puesto cual es el problema porque queda enlazado al principio.
0votos

Escrito por Enric Jean hace 6 años

Había visto el enunciado del problema, pero siento decir que ni así. Cierto que dominando el lenguage se hará mas obvio. Pero me refiero a este nivel de refactorizacion: si quito el código (dejando los comentarios), con los comentarios restantes sabría entender lo que hace? No me refiero a comentar cada línea de codigo (eso es anti-legible) sino explicar la intencionalidad, el pseudocodigo.
0votos

Escrito por josejuan hace 6 años

Pero es que he hecho lo que pides:

   Función final "constructiva". 
   La estrategia es como en Logo, se sigue la ruta de una tortuga.


No es mucho, pero no hace falta más, lo demás se lee directamente del código.

No pretendo escribir código legible para cualquier programador.

Data.List es una de las librerías más usadas en Haskell, es de las más comentadas que he visto y las funciones son mucho más obvias que "todosPisos" (ej. "length", "take", "drop", "splitAt", ...). Mira tú mismo si te parece un código legible para cualquier programador.

Data.List
0votos

Escrito por Enric Jean hace 6 años

Disculpa que siga con este tema, si no quieres seguir y contestar con esto no sigas, pero a mi que fascina mucho el tema de la legibilidad del código y aprovecho cualquier ocasión :)

Enfatizas cualquier programador. Mi pregunta es: el código que escribimos a quien va dirigido, a parte de a un compilador que lo interpreta?

A gente que conoce el lenguaje, claro, pero creo que sobretodo va dirigido a gente que nunca ha leido antes ese código.

En algoritmos como este caso donde el codigo es escaso, poner ejemplos va muy bien como bien haces con 'direcciones' y 'saltos'. Pero en cambio en las otras lineas que no pones ejemplos cuesta mucho mas entenderlo.

Data.List si trae comentarios pero no trae ejemplos, y eso dificulta su lectura.


Cuando el código es procedural (funciones con decenas o centenares de lineas) descomponer en funciones va muy bien. Para poner un caso simple, he cogido el primero que he visto (que casualmente es tuyo :) )

Cuánto tiempo se tarda en entender este ejemplo:

a) original

Array.prototype.Shuffle = function () { 
    var L = this.length - 1; 
    for(var i = 0; i < L; i++) {
    var j = i + 1 + ~~(Math.random() * (L - i)); 
    var w = this[i]; 
    this[i] = this[j]; 
    this[j] = w; 
  } 
  return this; 
} 





b) refactorizado

Array.prototype.Shuffle = function () { 

  //para cada posicion del Array 
  var L = this.length - 1; 
  for(var i = 0; i < L; i++) {

     var j = escogerUnaPosicionAleatoria(L,i);
     intercambiarPosiciones(i,j);
  }
}




Un saludo :)
0votos

Escrito por josejuan hace 6 años

Utilizas un argumento para replicar al mío ¡ignorando mi argumento!.

He dicho que yo no escribo código para que lo entienda CUALQUIER programador.

Si tu insistes ("A gente que conoce el lenguaje, claro, pero creo que sobretodo va dirigido a gente que nunca ha leido antes ese código. ") en que TODOS los programadores no escribimos código para el compilador sino para que lo entienda CUALQUIER programador, entonces no estamos dialogando y no vamos a llegar a ningún sitio.

Ten en cuenta que tu visión es completamente subjetiva. Por ejemplo, supón que todos en mi departamento somos físicos (no soy físico), ¿para que carajo tengo que comentar qué significan las siglas "fft"?, si argumentas que porque puede que venga alguien que no lo sea, ese argumento no sirve, porque quien quiera que trabaje en mi departamento, deberá ser físico.

Así, en mi departamento escribimos código para que lo entienda un físico, no cualquier programador.

Fíjate, que lo siguiente es completamente cierto:

A NADIE LE INTERESA COMENTAR NI ESCRIBIR CÓDIGO LEGIBLE

¡Es cierto!, nadie quiere tomarse el tiempo de comentar ni de escribir buen código, ¡pero resulta que DEBEMOS!, querer, desear es diferente a deber, necesitar.

Así, igual que un albañil no dedica 10 minutos para poner cada ladrillo (de tal forma que quede milimétricamente recto y con la cantidad justa de cemento), creo que no deberías llevar al extremo la premisa "escribe un código claro", sólo en la medida del contexto en el que te encuentres.

Por cierto, a mi me gusta mucho más la "A" (a ti te puede gustar más la "B", claro), porque salvo que dispongas en tu lenguaje de las funciones que aplicas (en cuyo caso yo las habría utilizado), vas a tener SEGURO múltiples versiones de esas funciones y además, no vas a saber que hacen realmente (salvo que vayas y las leas), posteriormente, las refactorizaciones te van a resultar más costosas (porque tienes demasiado partido el código) y tendrás código inútil (ej. eliminas la llamada a "intercambiarPosiciones"), etc...

En el caso como el que comentas, cuando por claridad es preferible partir el código (sigamos con shuffle), yo escribo lambdas o macros dentro de la misma función (aunque sea C, hay por aquí uno de Solveet con lambdas en C), por ejemplo así:

Array.prototype.Shuffle = function () {

    // rand entre i-1 y L
    var rand = function (i, L) {
                   var r = Math.random() * (L-i);
                   return i + 1 + ~~r;
               };

    var swap = function (a, b) {
                    var w = this[i];
                    this[i] = this[j];
                    this[j] = w;
                };

    var L = this.length - 1;
    for(var i = 0; i < L; i++)
        swap(rand(i, L), j);

    return this;

}
0votos

Escrito por Enric Jean hace 6 años

Siento que no se entendiera mi comentario. Si tenia claro que no escribes codigo para cualquier programador, así lo había entendido.

Yo opino que si, que se escribe para cualquier programador que nunca lo ha visto antes, independientemente que sea fisico o no.

Aunque yo sea fisico, si tengo que leer un libro o un articulo de otro fisico primero leeré su indice (o los titulos de los parrafos). Con las funciones largas creo que pasa igual. Pienso que hay que descomponerla en sus partes importantes para de un vistazo tener una idea rapida de lo que hace (notese que esto no es escribir un comentario muy largo antes de la funcion)

Sobre descomponer o no funciones, si el nombre de la funcion es bueno debería ser suficiente para no tener que leer su codigo.

Desconozco si las lambdas se pueden reutilizar en otras funciones. Si no se puede se pierde mantenibilidad, pues es muy posible que se acabe repitiendo codigo.

Sigue el debate..
0votos

Escrito por josejuan hace 6 años

"Yo opino que si, que se escribe para cualquier programador que nunca lo ha visto antes, independientemente que sea físico o no. "

Tú estás describiendo unas prácticas que pretendes aplicar en cualquier contexto.

No sólo eso, pretendes (y ahí está tu error) que yo esté dispuesto a aplicarlas también, argumentando que si no lo hago, ¡no lo hago bien!.

¿En que te basas para hacer semejante afirmación? (de que incluso para los físicos deba decir que fft significa "fast fourier transform")

¿Cual es tu criterio OBJETIVO que aportas a la discusión?

(El ejemplo de que has mostrado el código a 100 programadores y sólo 10 lo han entendido ya he explicado [objetivamente] porqué no vale ¡pues precisamente son esos 10 los únicos que yo necesito que lo entiendan).

(Si empezamos a difuminar todavía más la conversación, por favor, fija claramente cual es la postura que estás defendiendo).
0votos

Escrito por Enric Jean hace 6 años

Un criterio objetivo es el tiempo que se pierde tratando de leer el codigo.


Con una funcion aislada no tiene mas, pero esta metodologia de escribir todo en una funcion trasladada a un proyecto mas grande, se crea una tendencia de añadir y añadir y copy-pastes que al final no hay quien lo lea ni mantenga.

De esos 10 que lo han entendido a mi lo que me preocupa es:

¿cuanto han tardado?

y cuando tengan que leerlo pasado 1 mes y no se acuerden, cuanto tardaran en entenderlo otra vez?

y cuando alguien lo cambie, cuanto tardaran en entenderlo de nuevo?
0votos

Escrito por josejuan hace 6 años

Por supuesto, cuando se requiere el comentario es mucho más verbose, como en mis soluciones de Emulando a Dik T Winter.

:)
0votos

Escrito por Enric Jean hace 6 años

esta muy bien la explicacion inicial.

pero una funcion de 400 lineas seguro que se puede descomponer.
0votos

Escrito por josejuan hace 6 años

Seguro, pero no de forma eficiente (que es de lo que trata el reto), en un entorno "real", yo habría encerrado los unrolling en regiones, pero dejaría todo igual, creando regiones como

  // acc += src + pwr   <<<<<<<<<<<<<<<< UNROLLING REGION


de todos modos, no puedes usar un ejemplo de programación recreativa con la complejidad de los números narcisistas para dar peso a tu argumento, al menos, si quieres que yo te tome en serio XD XD XD.

Es decir, si eludes constantemente el contexto para el cual se escribe un código e insistes monótonamente que tal o cual código (repito, ignorando el contexto) podría escribirse mejor, pues vale, pero comprenderás que nadie (y yo en particular) estamos obligados a cumplir tan arbitrario criterio.

Recuerda que mientras que todos podemos evaluar objetivamente la eficiencia de un algoritmo (aunque sea empíricamente), el criterio de claridad puede variar arbitrariamente de forma subjetiva.

Si tu me dices, "sí, pero se lo he enseñado a 100 programadores y sólo 10 lo han entendido" ¡pues entonces me das la razón!, porque has fijado un contexto y evaluado la legibilidad en ese contexto.

Por eso, yo no escribo código para CUALQUIER programador.

:)
0votos

Escrito por Enric Jean hace 6 años

si el contexto ayuda, pero 4 lineas se leen antes que 400..

Existe un debate de si toda funcion se debe descomponer en 4 - 5 lineas como mucho (como comenta Robert C. Martin), o mantener una complejidad ciclomatica de 10.

[1] http://programmers.stackexchange.com/questions/94429/refactoring-into-lots-of-methods-is-this-considered-clean-or-not
0votos

Escrito por josejuan hace 6 años

"si el contexto ayuda, pero 4 lineas se leen antes que 400"

¿Me tomas el pelo no?
0votos

Escrito por Enric Jean hace 6 años

en absoluto, puedes leer su libro Clean Code.
0votos

Escrito por Enric Jean hace 6 años

(esto de mantener 2 lineas de debate esta confundiendo las cosas :)

Comenta la solución

Tienes que identificarte para poder publicar tu comentario.