Este artículo presentará conceptos de programación funcional que todo programador debería conocer. Comencemos por definir qué es la programación funcional (FP de ahora en adelante). FP es un paradigma de programación donde el software se escribe aplicando y componiendo funciones. Un es un "marco filosófico o teórico de cualquier tipo". En otras palabras, FP es una forma de pensar en los problemas como una cuestión de interconexión de funciones. paradigma Aquí, daré una comprensión básica de los conceptos fundamentales en FP y algunos de los problemas que ayuda a resolver. Nota: Por motivos prácticos, omitiré las propiedades matemáticas específicas que definen estos conceptos. Esto no es necesario para que use estos conceptos y los aplique en sus programas. 1. Inmutabilidad Una mutación es una modificación del valor o estructura de un objeto. Inmutabilidad significa que algo no se puede modificar. Considere el siguiente ejemplo: cartProducts = [ { : , : , : }, { : , : , : } ] cartProducts.forEach( { currencySign = product.currency === ? : product.price = }) total = cartProducts.forEach( { total += product.price }) .log(total) const "name" "Nintendo Switch" "price" 320.0 "currency" "EUR" "name" "Play station 4" "price" 350.0 "currency" "USD" // Let's format the price field so it includes the currency eg 320 € ( ) => product const 'EUR' '€' '$' // Alert! We're mutating the original object ` ` ${product.price} ${currencyName} // Calculate total let 0 ( ) => product // Now let's print the total console // Prints '0320 €350 $' 😟 ¿Qué sucedió? Ya que estamos mutando el objeto, de . cartProducts perdemos el valor original price . No desea llamar a una función en una biblioteca de terceros y no saber si modificará el objeto que está pasando. La mutación puede ser problemática porque dificulta o incluso imposibilita el seguimiento de los cambios de estado en nuestra aplicación Veamos una mejor opción: cartProducts = [...] productsWithCurrencySign = cartProducts.map( { currencyName = product.currency === ? : { ...product, : } }) total = cartProducts.forEach( { total += product.price }) .log(total) const const ( ) => product const 'EUR' 'euros' 'dollars' // Copy the original data and then add priceWithCurrency return priceWithCurrency ` ` ${product.price} ${currencyName} let 0 ( ) => product console // Prints 670 as expected 😎 Ahora, en lugar de modificar el objeto original, clonamos los datos en el original mediante el uso del operador de propagación cartProducts { ...product, : } return priceWithCurrency ` ` ${product.price} ${currencyName} Con esta segunda opción evitamos mutar el objeto original creando uno nuevo que tenga la propiedad. priceWithCurrency La inmutabilidad en realidad puede ser ordenada por el idioma. JavaScript tiene la utilidad, pero también hay bibliotecas maduras como puedes usar en su lugar. Sin embargo, antes de imponer la inmutabilidad en todas partes, evalúe la compensación de agregar una nueva biblioteca + la sintaxis adicional; tal vez sería mejor crear un acuerdo en su equipo para no mutar objetos si es posible. Object.freeze Immutable.js 2. Composición de funciones Es la aplicación de una función a la salida de otra función. He aquí un pequeño ejemplo: deductTaxes = grossSalary * addBonus = grossSalary + netSalary = addBonus(deductTaxes( )) const ( ) => grossSalary 0.8 const ( ) => grossSalary 500 const 2000 En la práctica, esto significa que podemos dividir los algoritmos en partes más pequeñas, reutilizarlos en toda nuestra aplicación y probar cada parte por separado. 3. Funciones deterministas Una función es determinista si, dada la misma entrada, devuelve la misma salida. Por ejemplo: joinWithComma = names.join( ) .log(joinWithComma([ , ])) .log(joinWithComma([ , ])) const ( ) => names ', ' console "Shrek" "Donkey" // Prints Shrek, Donkey console "Shrek" "Donkey" // Prints Shrek, Donkey again! Una función común no determinista es : Math.random .log( .random()) .log( .random()) console Math // Maybe we get 0.6924493472043922 console Math // Maybe we get 0.4146573369082662 Las funciones deterministas ayudan a que el comportamiento de su software sea más predecible y reducen la posibilidad de errores. Vale la pena señalar que no siempre queremos funciones deterministas. Por ejemplo, cuando queremos generar una nueva ID para una fila de la base de datos u obtener la fecha actual en milisegundos, necesitamos que se devuelva un nuevo valor en cada llamada. 4. Funciones puras Una función pura es una función que es y . Ya vimos lo que significa determinista. Un efecto secundario es una modificación de estado fuera del entorno local de una función. determinista no tiene efectos secundarios Veamos una función con un efecto secundario desagradable: sessionState = sessionIsActive = { (lastLogin > expirationDate) { sessionState = } } expirationDate = ( , , ) currentDate = () isActive = sessionIsActive(currentDate, expirationDate) (!isActive && sessionState === ) { logout() } let 'ACTIVE' const ( ) => lastLogin, expirationDate if // Modify state outside of this function 😟 'EXPIRED' return false return true const new Date 2020 10 01 const new Date const // This condition will always evaluate to false 🐛 if 'ACTIVE' Como puedes ver, modifica una variable fuera de su alcance, lo que causa problemas para la persona que llama a la función. sessionIsActive Ahora aquí hay una alternativa sin efectos secundarios: sessionState = { (lastLogin > expirationDate) { } } { (currentState === && !isActive) { } currentState } expirationDate = ( , , ) currentDate = () isActive = sessionIsActive(currentDate, expirationDate) newState = getSessionState(sessionState, isActive) (!isActive && sessionState === ) { logout() } let 'ACTIVE' ( ) function sessionIsActive lastLogin, expirationDate if return false return true ( ) function getSessionState currentState, isActive if 'ACTIVE' return 'EXPIRED' return const new Date 2020 10 01 const new Date const const // Now, this function will only logout when necessary 😎 if 'ACTIVE' Es importante comprender que no queremos eliminar todos los efectos secundarios, ya que todos los programas deben tener algún tipo de efecto secundario, como llamar a las API o imprimir en alguna salida estándar. Lo que queremos es minimizar los efectos secundarios, para que el comportamiento de nuestro programa sea más fácil de predecir y probar. 5. Funciones de orden superior A pesar del nombre intimidante, las funciones de orden superior son solo funciones que: toman una o más funciones como argumentos, o devuelven una función como salida. Aquí hay un ejemplo que toma una función como parámetro y también devuelve una función: simpleProfile = { { .log( ) longRunningTask() .log( ) } } calculateBigSum = { total = ( counter = ; counter < ; counter += ) { total += counter } total } runCalculationWithProfile = simpleProfile(calculateBigSum) runCalculationWithProfile() const ( ) => longRunningTask return => () console `Started running at: ` ${ ().getTime()} new Date console `Finished running at: ` ${ ().getTime()} new Date const => () let 0 for let 0 100000000 1 return const Como puede ver, podemos hacer cosas geniales, como agregar funcionalidad en torno a la ejecución de la función original. Veremos otros usos de orden superior en funciones curry. 6. Aridad La aridad es el número de argumentos que toma una función. stringify = sum => x + y // This function has an arity of 1. Also called unary const => x `Current number is ` ${x} // This function has an arity of 2. Also called binary const ( ) => x, y Es por eso que en programación, a veces escuchas operadores como o unarios ++ ! 7. Funciones al curry Las funciones curry son funciones que toman múltiples parámetros, solo uno a la vez (tienen una aridad de uno). Se pueden crear en JavaScript a través de funciones de orden superior. Aquí hay una función curry con la sintaxis de la función de flecha ES6: generateGreeting = (relationship) => { .log( ) } greeter = generateGreeting( ) greeterCousin = greeter( ) cousins = [ , , ] cousins.forEach( { greeterCousin(cousin) }) greeterFriend = greeter( ) friends = [ , , ] friends.forEach( { greeterFriend(friend) }) const ( ) => ocassion ( ) => name console `My dear . Hope you have a great ` ${relationship} ${name} ${ocassion} const 'birthday' // Specialized greeter for cousin birthday const 'cousin' const 'Jamie' 'Tyrion' 'Cersei' ( ) => cousin /* Prints: My dear cousin Jamie. Hope you have a great birthday My dear cousin Tyrion. Hope you have a great birthday My dear cousin Cersei. Hope you have a great birthday */ // Specialized greeter for friends birthday const 'friend' const 'Ned' 'John' 'Rob' ( ) => friend /* Prints: My dear friend Ned. Hope you have a great birthday My dear friend John. Hope you have a great birthday My dear friend Rob. Hope you have a great birthday */ Genial, ¿verdad? Pudimos personalizar la funcionalidad de nuestra función pasando un argumento a la vez. De manera más general, las funciones curry son excelentes para dar a las funciones un comportamiento polimórfico y para simplificar su composición. 8. Funtores No se deje intimidar por el nombre. Los funtores son solo abstracciones que envuelven un valor en un contexto y permiten mapear este valor. Mapear significa aplicar una función a un valor para obtener otro valor. Así es como se ve un Functor muy simple: Identity = ({ : Identity(fn(value)), : value }) const => value map => fn valueOf => () ¿Por qué se tomaría la molestia de crear un Funtor en lugar de simplemente aplicar una función? Para facilitar la composición de funciones. Los funtores son independientes del tipo dentro de ellos, por lo que puede aplicar funciones de transformación secuencialmente. Veamos un ejemplo: double = { x * } plusTen = { x + } num = doubledPlus10 = Identity(num) .map(double) .map(plusTen) .log(doubledPlus10.valueOf()) const ( ) => x return 2 const ( ) => x return 10 const 10 const console // Prints 30 Esta técnica es muy poderosa porque puede descomponer sus programas en piezas reutilizables más pequeñas y probar cada una por separado sin ningún problema. En caso de que te lo estés preguntando, JavaScript object también es un Funtor. Array 9. Mónadas Una mónada es un functor que también proporciona un operación. Esta estructura ayuda a componer funciones tipo elevación. Ahora explicaremos cada parte de esta definición paso a paso y por qué podríamos querer usarla. flatMap ¿Qué son las funciones de elevación de tipo? Las funciones de elevación de tipo son funciones que envuelven un valor dentro de algún contexto. Veamos algunos ejemplos: repeatTwice = [x, x] setWithSquared = (x ** ) // Here we lift x into an Array data structure and also repeat the value twice. const => x // Here we lift x into a Set data structure and also square it. const => x new Set 2 Las funciones de elevación de tipos pueden ser bastante comunes, por lo que tiene sentido que queramos componerlas. ¿Qué es una función plana? los La función (también llamada unión) es una función que extrae el valor de algún contexto. Puede comprender fácilmente esta operación con la ayuda de JavaScript función. flat Array.prototype.flat favouriteNumbers = [ , [ , ], ] .log(favouriteNumbers.flat()) // Notice the [2, 3] inside the following array. 2 and 3 are inside the context of an Array const 1 2 3 4 // JavaScript's Array.prototype.flat method will go over each of its element, and if the value is itself an array, its values will be extracted and concatenated with the outermost array. console // Will print [1, 2, 3, 4] ¿Qué es una función flatMap? Es una función que primero aplica una función de mapeo (mapa), luego elimina el contexto a su alrededor (plano). Sí, sé que es confuso que las operaciones no se apliquen en el mismo orden que implica el nombre del método. ¿Cómo son útiles las mónadas? Imagina que queremos componer dos funciones de tipo elevación que cuadran y dividen por dos dentro de un contexto. Primero intentemos usar map y un funtor muy simple llamado Identity. Identity = ({ map: Identity.of(f(value)), : value }) Identity.of = Identity(value) squareIdentity = Identity.of(x ** ) divideByTwoIdentity = Identity.of(x / ) result = Identity( ) .map(squareIdentity) .map(divideByTwoIdentity) .valueOf() const => value // flatMap: f => f(value), => f valueOf => () // The `of` method is a common type lifting functions to create a Monad object. => value const => x 2 const => x 2 const 3 // 💣 This will fail because will receive an Identity.of(9) which cannot be divided by 2 No podemos simplemente usar la función de mapa y primero debemos extraer los valores dentro de la Identidad. Aquí es donde entra en juego la función flatMap. Identity = ({ : f(value), : value }) ... const result = Identity( ) .flatMap(squareIdentity) .flatMap(divideByTwoIdentity) .valueOf() .log(result); const => value flatMap => f valueOf => () 3 console // Logs out 4.5 Finalmente somos capaces de componer funciones de elevación de tipo, gracias a las mónadas. Conclusión Espero que este artículo le brinde una comprensión básica de algunos conceptos fundamentales en la programación funcional y lo aliente a profundizar en este paradigma para que pueda escribir software más reutilizable, fácil de mantener y fácil de probar. Publicado anteriormente en https://www.victorandcode.com/9-concepts-you-should-know-from-funcional-programming