¿Cómo puedo leer el código JavaScript funcional?

9

Creo que he aprendido algunos / muchos / la mayoría de los conceptos básicos que subyacen a la programación funcional en JavaScript. Sin embargo, tengo problemas para leer específicamente el código funcional, incluso el código que he escrito, y me pregunto si alguien puede darme consejos, consejos, mejores prácticas, terminología, etc. que puedan ayudar.

Toma el código a continuación. Escribí este código. Su objetivo es asignar un porcentaje de similitud entre dos objetos, entre say {a:1, b:2, c:3, d:3}y {a:1, b:1, e:2, f:2, g:3, h:5}. Produje el código en respuesta a esta pregunta sobre Stack Overflow . Como no estaba seguro exactamente de qué tipo de similitud porcentual preguntaba el afiche, proporcioné cuatro tipos diferentes:

  • el porcentaje de las claves en el primer objeto que se puede encontrar en el segundo,
  • el porcentaje de los valores en el primer objeto que se puede encontrar en el segundo, incluidos los duplicados,
  • el porcentaje de los valores en el primer objeto que se puede encontrar en el segundo, sin duplicados permitidos, y
  • el porcentaje de pares {clave: valor} en el primer objeto que se puede encontrar en el segundo objeto.

Comencé con un código razonablemente imperativo, pero rápidamente me di cuenta de que este era un problema muy adecuado para la programación funcional. En particular, me di cuenta de que si podía extraer una función o tres para cada una de las cuatro estrategias anteriores que definían el tipo de característica que buscaba comparar (por ejemplo, las claves o los valores, etc.), entonces podría ser capaz de reducir (perdonar el juego de palabras) el resto del código en unidades repetibles. Ya sabes, manteniéndolo SECO. Entonces cambié a programación funcional. Estoy bastante orgulloso del resultado, creo que es razonablemente elegante, y creo que entiendo lo que hice bastante bien.

Sin embargo, incluso después de haber escrito el código yo mismo y comprender cada parte de él durante la construcción, cuando ahora lo recuerdo, sigo estando un poco desconcertado tanto sobre cómo leer cualquier media línea particular como sobre cómo leerlo. "grok" lo que realmente está haciendo cualquier media línea de código en particular. Me encuentro haciendo flechas mentales para conectar diferentes partes que se degradan rápidamente en un lío de espagueti.

Entonces, ¿alguien puede decirme cómo "leer" algunos de los bits de código más intrincados de una manera concisa y que contribuya a mi comprensión de lo que estoy leyendo? Supongo que las partes que más me atraen son aquellas que tienen varias flechas gruesas en una fila y / o partes que tienen varios paréntesis en una fila. Nuevamente, en esencia, eventualmente puedo descubrir la lógica, pero (espero) que haya una mejor manera de hacerlo rápida y clara y directamente "tomando" una línea de programación funcional de JavaScript.

Siéntase libre de usar cualquier línea de código de abajo, o incluso otros ejemplos. Sin embargo, si quieres algunas sugerencias iniciales de mí, aquí hay algunas. Comience con una razonablemente simple. De cerca de la final del código, hay una que se pasa como parámetro a una función: obj => key => obj[key]. ¿Cómo se lee y comprende eso? Un ejemplo más es una función completa de cerca del inicio: const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));. La última mapparte me atrapa en particular.

Tenga en cuenta, en este punto en el tiempo que estoy no en busca de referencias a Haskell o notación simbólica abstracta o los fundamentos de currificación, etc. Lo que estoy buscando es frases en inglés que en silencio puede boca mientras mira a una línea de código. Si tiene referencias que aborden específicamente exactamente eso, genial, pero tampoco estoy buscando respuestas que digan que debería ir a leer algunos libros de texto básicos. Lo he hecho y obtengo (al menos una cantidad significativa de) la lógica. También tenga en cuenta que no necesito respuestas exhaustivas (aunque tales intentos serían bienvenidos): incluso las respuestas cortas que proporcionan una forma elegante de leer una sola línea particular de código problemático serían apreciadas.

Supongo que una parte de esta pregunta es: ¿ puedo incluso leer código funcional linealmente, ya sabes, de izquierda a derecha y de arriba a abajo? ¿O uno se ve obligado a crear una imagen mental de cableado tipo espagueti en la página de código que definitivamente no es lineal? Y si uno debe hacer eso, todavía tenemos que leer el código, entonces, ¿cómo tomamos texto lineal y conectamos los espaguetis?

Cualquier consejo sería apreciado.

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25
Andrew Willems
fuente

Respuestas:

18

En su mayoría tiene dificultades para leerlo porque este ejemplo en particular no es muy legible. Sin ánimo de ofender, una proporción desalentadoramente grande de muestras que encuentra en Internet tampoco lo es. Mucha gente solo juega con la programación funcional los fines de semana y nunca tiene que lidiar con el mantenimiento del código funcional de producción a largo plazo. Lo escribiría más así:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

Por alguna razón, muchas personas tienen la idea en mente de que el código funcional debe tener un cierto "aspecto" estético de una gran expresión anidada. Tenga en cuenta que aunque mi versión se parece un poco al código imperativo con todos los puntos y comas, todo es inmutable, por lo que podría sustituir todas las variables y obtener una gran expresión si lo desea. De hecho, es tan "funcional" como la versión de espagueti, pero con más legibilidad.

Aquí las expresiones se dividen en partes muy pequeñas y se les da nombres que son significativos para el dominio. La anidación se evita mediante la extracción de la funcionalidad común como mapObjen una función con nombre. Las lambdas están reservadas para funciones muy cortas con un propósito claro en contexto.

Si encuentra un código que es difícil de leer, refactorícelo hasta que sea más fácil. Se necesita algo de práctica, pero vale la pena. El código funcional puede ser tan legible como imperativo. De hecho, a menudo más, porque suele ser más conciso.

Karl Bielefeldt
fuente
¡Definitivamente no se ofende! Si bien aún mantendré que sé algunas cosas sobre la programación funcional, tal vez mis afirmaciones en la pregunta sobre cuánto sé que fueron un poco exageradas. Soy realmente un principiante relativo. Entonces, ver cómo este intento mío en particular puede reescribirse de una manera tan concisa, clara pero funcional, parece oro ... gracias. Estudiaré tu reescritura cuidadosamente.
Andrew Willems
1
He oído decir que tener cadenas largas y / o anidar métodos elimina variables intermedias innecesarias. Por el contrario, su respuesta divide mis cadenas / anidamiento en declaraciones independientes intermedias utilizando variables intermedias bien nombradas. Encuentro tu código más legible en este caso, pero me pregunto qué tan general estás tratando de ser. ¿Está diciendo que las cadenas de métodos largos y / o el anidamiento profundo son a menudo o incluso siempre un antipatrón que debe evitarse, o hay momentos en que brindan un beneficio significativo? ¿Y la respuesta a esa pregunta es diferente para la codificación funcional frente a la imperativa?
Andrew Willems
3
Hay ciertas situaciones en las que eliminar las variables intermedias puede agregar claridad. Por ejemplo, en FP casi nunca quieres un índice en una matriz. También a veces no hay un gran nombre para el resultado intermedio. Sin embargo, en mi experiencia, la mayoría de las personas tienden a equivocarse demasiado en sentido contrario.
Karl Bielefeldt
6

No he realizado mucho trabajo altamente funcional en Javascript (lo cual diría que es esto: la mayoría de las personas que hablan sobre Javascript funcional pueden estar usando mapas, filtros y reducciones, pero su código define sus propias funciones de nivel superior , que es algo más avanzado que eso), pero lo he hecho en Haskell, y creo que al menos parte de la experiencia se traduce. Te daré algunos consejos sobre las cosas que he aprendido:

Especificar los tipos de funciones es realmente importante. Haskell no requiere que especifique cuál es el tipo de función, pero incluir el tipo en la definición hace que sea mucho más fácil de leer. Si bien Javascript no admite la escritura explícita de la misma manera, no hay razón para no incluir la definición de tipo en un comentario, por ejemplo:

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

Con un poco de práctica trabajando con definiciones de tipo como esta, hacen que el significado de una función sea mucho más claro.

La denominación es importante, quizás incluso más que en la programación de procedimientos. Muchos programas funcionales están escritos en un estilo muy conciso que es pesado en la convención (por ejemplo, la convención de que 'xs' es una lista / matriz y que 'x' es un elemento en ella es muy generalizado), pero a menos que comprenda ese estilo fácilmente sugeriría nombres más detallados. Mirando los nombres específicos que ha usado, "getX" es algo opaco y, por lo tanto, "getXs" tampoco ayuda mucho. Llamaría a "getXs" algo así como "applyToProperties", y "getX" probablemente sería "propertyMapper". "getPctSameXs" sería entonces "percentPropertiesSameWith" ("con")

Otra cosa importante es escribir código idiomático . Noté que estás usando una sintaxis a => b => some-expression-involving-a-and-bpara producir funciones curry. Esto es interesante y podría ser útil en algunas situaciones, pero no está haciendo nada aquí que se beneficie de las funciones currificadas y sería más idiomático Javascript usar funciones tradicionales de argumentos múltiples en su lugar. Hacerlo puede hacer que sea más fácil ver lo que está sucediendo de un vistazo. También lo está utilizando const name = lambda-expressionpara definir funciones, donde sería más idiomático utilizarlo function name (args) { ... }. Sé que son semánticamente ligeramente diferentes, pero a menos que confíes en esas diferencias, te sugiero que uses la variante más común cuando sea posible.

Jules
fuente
55
+1 para tipos! El hecho de que el idioma no los tenga no significa que no tenga que pensar en ellos . Varios sistemas de documentación para ECMAScript tienen un lenguaje de tipos para registrar los tipos de funciones. Varios IDE de ECMAScript también tienen un lenguaje de tipo (y, por lo general, también entienden los lenguajes de tipo para los principales sistemas de documentación), e incluso pueden realizar verificaciones de tipo rudimentarias y sugerencias heurísticas utilizando esas anotaciones de tipo .
Jörg W Mittag
Me has dado mucho para masticar: definiciones de tipos, nombres significativos, uso de modismos ... ¡gracias! Solo algunos de los muchos comentarios posibles: no tenía la intención necesariamente de escribir ciertas partes como funciones curry; simplemente evolucionaron de esa manera mientras refactorizaba mi código durante la escritura. Ahora puedo ver cómo eso no era necesario, e incluso fusionar los parámetros de esas dos funciones en dos parámetros para una sola función no solo tiene más sentido, sino que instantáneamente hace que ese bit corto sea al menos más legible.
Andrew Willems
@ JörgWMittag, gracias por sus comentarios sobre la importancia de los tipos y por el enlace a esa otra respuesta que escribió. Utilizo WebStorm y no me di cuenta de que, de acuerdo con la forma en que leí esa otra respuesta tuya, WebStorm sabe cómo interpretar anotaciones tipo jsdoc. Supongo por su comentario que jsdoc y WebStorm se pueden usar juntos para anotar código funcional, no solo imperativo, sino que tendría que profundizar más para saberlo realmente. He jugado con jsdoc antes y ahora que sé que WebStorm y yo podemos cooperar allí, espero usar esa característica / enfoque más.
Andrew Willems
@Jules, solo para aclarar a qué función curry me refería en mi comentario anterior: Como usted implica, cada instancia de obj => key => ...se puede simplificar (obj, key) => ...porque más tarde getX(obj)(key)también se puede simplificar get(obj, key). Por el contrario, otra función currificada (getX, filter = vals => vals) => (objA, objB) => ...no puede simplificarse fácilmente, al menos en el contexto del resto del código tal como está escrito.
Andrew Willems