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 paradigma 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.
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.
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:
const cartProducts = [ { "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 € cartProducts.forEach( ( product ) => { const currencySign = product.currency === 'EUR' ? '€' : '$' // Alert! We're mutating the original object product.price = ` ${product.price} ${currencyName} ` }) // Calculate total let total = 0 cartProducts.forEach( ( product ) => { total += product.price }) // Now let's print the total console .log(total) // Prints '0320 €350 $' 😟
¿Qué sucedió? Ya que estamos mutando el
cartProducts
objeto, perdemos el valor original de price
.La mutación puede ser problemática porque dificulta o incluso imposibilita el seguimiento de los cambios de estado en nuestra aplicación . No desea llamar a una función en una biblioteca de terceros y no saber si modificará el objeto que está pasando.
Veamos una mejor opción:
const cartProducts = [...] const productsWithCurrencySign = cartProducts.map( ( product ) => { const currencyName = product.currency === 'EUR' ? 'euros' : 'dollars' // Copy the original data and then add priceWithCurrency return { ...product, priceWithCurrency : ` ${product.price} ${currencyName} ` } }) let total = 0 cartProducts.forEach( ( product ) => { total += product.price }) console .log(total) // Prints 670 as expected 😎
Ahora, en lugar de modificar el objeto original, clonamos los datos en el original
cartProducts
mediante el uso del operador de propagación return { ...product, priceWithCurrency : ` ${product.price} ${currencyName} ` }
Con esta segunda opción evitamos mutar el objeto original creando uno nuevo que tenga la
priceWithCurrency
propiedad.La inmutabilidad en realidad puede ser ordenada por el idioma. JavaScript tiene la
Object.freeze
utilidad, pero también hay bibliotecas maduras como Immutable.js
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.Es la aplicación de una función a la salida de otra función. He aquí un pequeño ejemplo:
const deductTaxes = ( grossSalary ) => grossSalary * 0.8 const addBonus = ( grossSalary ) => grossSalary + 500 const netSalary = addBonus(deductTaxes( 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.
Una función es determinista si, dada la misma entrada, devuelve la misma salida. Por ejemplo:
const joinWithComma = ( names ) => names.join( ', ' ) console .log(joinWithComma([ "Shrek" , "Donkey" ])) // Prints Shrek, Donkey console .log(joinWithComma([ "Shrek" , "Donkey" ])) // Prints Shrek, Donkey again!
Una función común no determinista es
Math.random
: console .log( Math .random()) // Maybe we get 0.6924493472043922 console .log( Math .random()) // 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.
Una función pura es una función que es determinista y no tiene efectos secundarios . Ya vimos lo que significa determinista. Un efecto secundario es una modificación de estado fuera del entorno local de una función.
Veamos una función con un efecto secundario desagradable:
let sessionState = 'ACTIVE' const sessionIsActive = ( lastLogin, expirationDate ) => { if (lastLogin > expirationDate) { // Modify state outside of this function 😟 sessionState = 'EXPIRED' return false } return true } const expirationDate = new Date ( 2020 , 10 , 01 ) const currentDate = new Date () const isActive = sessionIsActive(currentDate, expirationDate) // This condition will always evaluate to false 🐛 if (!isActive && sessionState === 'ACTIVE' ) { logout() }
Como puedes ver,
sessionIsActive
modifica una variable fuera de su alcance, lo que causa problemas para la persona que llama a la función.Ahora aquí hay una alternativa sin efectos secundarios:
let sessionState = 'ACTIVE' function sessionIsActive ( lastLogin, expirationDate ) { if (lastLogin > expirationDate) { return false } return true } function getSessionState ( currentState, isActive ) { if (currentState === 'ACTIVE' && !isActive) { return 'EXPIRED' } return currentState } const expirationDate = new Date ( 2020 , 10 , 01 ) const currentDate = new Date () const isActive = sessionIsActive(currentDate, expirationDate) const newState = getSessionState(sessionState, isActive) // Now, this function will only logout when necessary 😎 if (!isActive && sessionState === 'ACTIVE' ) { logout() }
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.
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:
const simpleProfile = ( longRunningTask ) => { return () => { console .log( `Started running at: ${ new Date ().getTime()} ` ) longRunningTask() console .log( `Finished running at: ${ new Date ().getTime()} ` ) } } const calculateBigSum = () => { let total = 0 for ( let counter = 0 ; counter < 100000000 ; counter += 1 ) { total += counter } return total } const runCalculationWithProfile = simpleProfile(calculateBigSum) runCalculationWithProfile()
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.
La aridad es el número de argumentos que toma una función.
// This function has an arity of 1. Also called unary const stringify = x => `Current number is ${x} ` // This function has an arity of 2. Also called binary const sum => ( x, y ) => x + y
Es por eso que en programación, a veces escuchas operadores unarios como ++ o !
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:
const generateGreeting = ( ocassion ) => (relationship) => ( name ) => { console .log( `My dear ${relationship} ${name} . Hope you have a great ${ocassion} ` ) } const greeter = generateGreeting( 'birthday' ) // Specialized greeter for cousin birthday const greeterCousin = greeter( 'cousin' ) const cousins = [ 'Jamie' , 'Tyrion' , 'Cersei' ] cousins.forEach( ( cousin ) => { greeterCousin(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 greeterFriend = greeter( 'friend' ) const friends = [ 'Ned' , 'John' , 'Rob' ] friends.forEach( ( friend ) => { greeterFriend(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.
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:
const Identity = value => ({ map : fn => Identity(fn(value)), valueOf : () => value })
¿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:
const double = ( x ) => { return x * 2 } const plusTen = ( x ) => { return x + 10 } const num = 10 const doubledPlus10 = Identity(num) .map(double) .map(plusTen) console .log(doubledPlus10.valueOf()) // 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
Array
object también es un Funtor.Una mónada es un functor que también proporciona un
flatMap
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.¿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:
// Here we lift x into an Array data structure and also repeat the value twice. const repeatTwice = x => [x, x] // Here we lift x into a Set data structure and also square it. const setWithSquared = x => new Set (x ** 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
flat
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 Array.prototype.flat
función. // Notice the [2, 3] inside the following array. 2 and 3 are inside the context of an Array const favouriteNumbers = [ 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 .log(favouriteNumbers.flat()) // 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.
const Identity = value => ({ // flatMap: f => f(value), map: f => Identity.of(f(value)), valueOf : () => value }) // The `of` method is a common type lifting functions to create a Monad object. Identity.of = value => Identity(value) const squareIdentity = x => Identity.of(x ** 2 ) const divideByTwoIdentity = x => Identity.of(x / 2 ) const result = Identity( 3 ) .map(squareIdentity) .map(divideByTwoIdentity) // 💣 This will fail because will receive an Identity.of(9) which cannot be divided by 2 .valueOf()
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.
const Identity = value => ({ flatMap : f => f(value), valueOf : () => value }) ... const result = Identity( 3 ) .flatMap(squareIdentity) .flatMap(divideByTwoIdentity) .valueOf() console .log(result); // Logs out 4.5
Finalmente somos capaces de componer funciones de elevación de tipo, gracias a las mónadas.
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