Programación funcional desde el punto de vista de EcmaScript. Composición, Curry, Aplicación Parcial

Hola Habr!

Hoy continuamos nuestra investigación sobre programación funcional en el contexto de EcmaScript, cuya especificación se basa en JavaScript. En el artículo anterior, examinamos los conceptos básicos: funciones puras, lambdas, el concepto de inmunidad. Hoy hablaremos de técnicas de FP un poco más complejas: composición, curry y funciones puras. El artículo está escrito en el estilo de "pseudo codreview", es decir resolveremos un problema práctico, mientras estudiamos los conceptos de transiciones de fase y código de refactorización para aproximar este último a los ideales de las transiciones de fase.

¡Entonces comencemos!

Supongamos que tenemos una tarea: crear un conjunto de herramientas para trabajar con palíndromos.
Palindrome
Género masculino
Una palabra o frase que se lee de la misma manera de izquierda a derecha y de derecha a izquierda.
"P. "Voy con la espada del juez" "
Una de las posibles implementaciones de esta tarea podría verse así:

function getPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase().split('').reverse().join(''); // -       ,       return str; } function isPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase(); return str === str.split('').reverse().join(''); } 

Por supuesto, esta implementación funciona. Podemos esperar que getPalindrom funcione correctamente si la API devuelve los datos correctos. Una llamada a isPalindrom ('Voy con un juez de espada') devolverá verdadero, y una llamada a isPalindrom ('no es un palíndromo') devolverá falso. ¿Esta implementación es buena en términos de ideales de programación funcional? Definitivamente no es bueno!

De acuerdo con la definición de Funciones Puras de este artículo :
Funciones puras (PF): siempre devuelve un resultado previsto.
Propiedades de PF:

El resultado de la ejecución de PF depende solo de los argumentos pasados ​​y del algoritmo que implementa PF
No use valores globales
No modifique valores externos o argumentos pasados
No escriba datos en archivos, bases de datos ni en ningún otro lugar.
¿Y qué vemos en nuestro ejemplo con palíndromos?

En primer lugar, hay duplicación de código, es decir Se viola el principio de SECO . En segundo lugar, la función getPalindrom accede a la base de datos. Tercero, las funciones modifican sus argumentos. Total, nuestras funciones no están limpias.

Recordemos la definición: la programación funcional es una forma de escribir código mediante la compilación de un conjunto de funciones.

Componemos un conjunto de funciones para esta tarea:

 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;//(1) const replace = (regexp, replacement, str) => str.replace(regexp, replacement);//(2) const toLowerCase = str => str.toLowerCase();//(3) const stringReverse = str => str.split('').reverse().join('');//(4) const isStringsEqual = (strA, strB) => strA === strB;//(5) 

En la línea 1, declaramos la expresión regular constante en forma funcional. Este método de describir constantes se usa a menudo en FP. En la línea 2, encapsulamos el método String.prototype.replace en una abstracción de reemplazo funcional para que (la llamada de reemplazo) coincida con el contrato de programación funcional. En la línea 3, se creó una abstracción para String.prototype.toLowerCase de la misma manera. En el cuarto, implementaron una función que crea una nueva cadena expandida a partir de la pasada. 5to cheques para la igualdad de la secuencia.

¡Tenga en cuenta que nuestras características son extremadamente limpias! Hablamos sobre los beneficios de las funciones puras en un artículo anterior.

Ahora necesitamos implementar una verificación para ver si la cadena es un palíndromo. Una composición de funciones vendrá en nuestra ayuda.

La composición de funciones es la unión de dos o más funciones en una determinada función resultante que implementa el comportamiento de aquellas combinadas en la secuencia algorítmica deseada.

La definición puede parecer complicada, pero desde un punto de vista práctico es justa.

Podemos hacer esto:

 isStringsEqual(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')), stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')))); 

o así:

 const strA = toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')); const strB = stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    '))); console.log(isStringsEqual(strA, strB)); 

o ingrese otro grupo de variables explicativas para cada paso del algoritmo implementado. Tal código a menudo se puede ver en proyectos, y este es un ejemplo típico de composición: pasar una llamada a una función como argumento a otra. Sin embargo, como vemos, en una situación en la que hay muchas funciones, este enfoque es malo, porque ¡Este código no es legible! ¿Y ahora qué? Bueno, su programación funcional, ¿estamos en desacuerdo?

De hecho, como suele ser el caso en la programación funcional, solo necesitamos escribir otra función.

 const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); 

La función componer toma una lista de funciones ejecutables como argumentos, las convierte en una matriz, las almacena en un cierre y devuelve una función que espera un valor inicial. Después de pasar el valor inicial, comienza la ejecución secuencial de todas las funciones de la matriz fns. El argumento de la primera función será el valor inicial x pasado, y los argumentos de todas las posteriores serán el resultado de la anterior. Entonces podemos crear composiciones de cualquier número de funciones.

Al crear composiciones funcionales, es muy importante monitorear los tipos de parámetros de entrada y los valores de retorno de cada función para que no haya errores inesperados, porque Pasamos el resultado de la función anterior a la siguiente.

Sin embargo, ya vemos problemas al aplicar la técnica de composición a nuestro código, porque la función:

 const replace = (regexp, replacement, str) => str.replace(regexp, replacement); 

espera aceptar 3 parámetros de entrada, y solo enviamos uno para componer. Otra técnica de FP, Curry, nos ayudará a resolver este problema.

Curry es la conversión de una función de muchos argumentos a una función de un argumento.

¿Recuerdas nuestra función de agregar del primer artículo?

 const add = (x,y) => x+y; 

Se puede curry así:

 const add = x => y => x+y; 

La función toma x y devuelve una lambda que espera y y realiza la acción.

Beneficios del curry:

  • el código se ve mejor;
  • Las funciones curry siempre están limpias.

Ahora transformamos nuestra función de reemplazo para que solo tome un argumento. Dado que necesitamos la función para reemplazar los caracteres en la cadena con una expresión regular previamente conocida, podemos crear una función parcialmente aplicada.

 const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); 

Como puede ver, arreglamos uno de los argumentos con una constante. Esto se debe al hecho de que curry es en realidad un caso especial de uso parcial.

Una aplicación parcial está envolviendo una función con un contenedor que acepta menos argumentos que la función en sí; el contenedor debe devolver una función que tome el resto de los argumentos.

En nuestro caso, creamos la función replaceAllNotWordSymbolsGlobal, que es una opción de reemplazo parcialmente aplicada. Acepta el reemplazo, lo almacena en un cierre y espera una línea de entrada para la que llamará reemplazar, y regexp con una constante.

De vuelta a los palíndromos. Cree una composición de funciones para la sincronización del palíndromo:

 const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); 

y la composición de funciones para la línea con la que compararemos el palíndromo potencial:

 const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); 

Ahora recuerda lo que dijimos anteriormente:
Un ejemplo típico de composición es pasar una llamada a una función como argumento a otra
y escribe:

 const testString = '    ';//          , .. ,    ,  ,   -   ,    const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

Aquí tenemos una solución funcional y atractiva:

 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); const testString = '    '; const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

Sin embargo, no queremos hacer curry cada vez o crear funciones parcialmente aplicadas con nuestras manos. Por supuesto que no queremos, los programadores son gente perezosa. Por lo tanto, como suele suceder en FP, escribiremos un par de funciones más:

 const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } 

La función de curry toma una función para curry, la almacena en un cierre y devuelve una lambda. La lambda espera el resto de los argumentos de la función. Cada vez que se recibe un argumento, verifica si se aceptan todos los argumentos declarados. Si se acepta, se llama a la función y se devuelve su resultado. Si no, la función se vuelve a curry.

También podemos crear una función parcialmente aplicada para reemplazar la expresión regular que necesitamos con una cadena vacía:

 const replaceAllNotWordSymbolsToEmpltyGlobal = curry(replace)(allNotWordSymbolsRegexpGlobal(), ''); 

Todo parece estar bien, pero somos perfeccionistas y no nos gustan demasiados corchetes, nos gustaría aún mejor, así que escribiremos otra función o tal vez dos:

 const party = (fn, x) => (...args) => fn(x, ...args); 

Esta es una implementación de abstracción para crear funciones aplicadas parciales. Toma una función y el primer argumento, devuelve una lambda que espera el resto y ejecuta la función.

Ahora reescribimos party para que podamos crear una función parcialmente aplicada de varios argumentos:

 const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); 

Vale la pena señalar por separado que las funciones currificadas de esta manera se pueden invocar con cualquier número de argumentos menos que declarado (fn.length).

 const sum = (a,b,c,d) => a+b+c+d; const fn = curry(sum); const r1 = fn(1,2,3,4);//,   const r2 = fn(1, 2, 3)(4);//       const r3 = fn(1, 2)(3)(4); const r4 = fn(1)(2)(3)(4); const r5 = fn(1)(2, 3, 4); const r6 = fn(1)(2)(3, 4); const r7 = fn(1, 2)(3, 4); 

Volvamos a nuestros palíndromos. Podemos reescribir nuestro replaceAllNotWordSymbolsToEmpltyGlobal sin corchetes adicionales:

 const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); 

Veamos todo el código:

 //    -       const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; //       const testString = '    '; //           -    rambda.js const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); //       const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); const processFormPalindrom = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, ); const checkPalindrom = testString => isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

Se ve muy bien, pero ¿qué pasa si no es una cadena para nosotros, pero vendrá una matriz? Por lo tanto, agregamos una función más:

 const map = fn => (...args) => args.map(fn); 

Ahora, si tenemos una matriz para detectar palíndromos, entonces:

 const palindroms = ['    ','   ','   '. ' '] map(checkPalindrom )(...palindroms ); // [true, true, true, false]   

Así es como resolvimos la tarea escribiendo conjuntos de características. Presta atención al estilo inútil de escribir código: esta es una prueba decisiva de pureza funcional.

Ahora un poco más de teoría. Al usar el curry no olvides que cada vez que curries una función creas una nueva, es decir seleccione una celda de memoria para ello. Es importante controlar esto para evitar fugas.

Las bibliotecas funcionales como ramda.js tienen funciones componer y pipe. compose implementa el algoritmo de composición de derecha a izquierda y la tubería de izquierda a derecha. Nuestra función de componer es un análogo de tubería de ramda. Hay dos funciones de composición diferentes en la biblioteca desde La composición de derecha a izquierda y de izquierda a derecha son dos contratos diferentes de programación funcional. Si uno de los lectores encuentra un artículo que describe todos los contratos existentes de la FP, luego compártelo en los comentarios, ¡lo leeré con gusto y pondré un plus en el comentario!

El número de parámetros formales de una función se llama aridad . Esta es también una definición importante desde el punto de vista de la teoría de las transiciones de fase.

Conclusión


En el marco de este artículo, examinamos técnicas de programación funcional como composición, currículum y aplicación parcial. Por supuesto, en proyectos reales usará bibliotecas listas para usar con estas herramientas, pero como parte del artículo, implementé todo en JS nativo para que los lectores con quizás poca experiencia en FP puedan entender cómo funcionan estas técnicas.

También elegí deliberadamente el método de narración: pseudo codreview, para ilustrar mi lógica de lograr la pureza funcional en el código.

Por cierto, puede continuar el desarrollo de este módulo de trabajo con palíndromos y desarrollar sus ideas, por ejemplo, descargar líneas por api, convertirlas en conjuntos de letras y enviarlas al servidor donde la línea será generada por el palíndromo y mucho más ... A su discreción.

También sería bueno deshacerse de la duplicación en los procesos de estas líneas:

  replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, 

En general, ¡es posible y necesario mejorar el código constantemente!

Hasta futuros artículos.

Source: https://habr.com/ru/post/475324/


All Articles