Jerga de la Programación Funcional
La programación funcional ofrece muchas ventajas, de ahí el auge de su popularidad. Sin embargo, cada paradigma de programación trae consigo una jerga única, y la programación funcional no es la excepción. Al proveer un glosario, creemos que podemos ayudar a aprender programación funcional de una manera más fácil.
Los ejemplos presentados están escritos en JavaScript (ES2015). Por qué JavaScript?
Este es un trabajo en progreso; siéntase libre en enviar PR.
Traducciones
Tabla de Contenido
- Aridad
- Funciones de Orden Superior (FOS)
- Aplicación Parcial
- Currying
- Auto Currying
- Composición de Funciones
- Pureza
- Efectos secundarios
- Idempotente
- Estilo Point-Free
- Predicado
- Contratos
- Guarded Functions
- Categorías
- Valor
- Constante
- Funtor
- Transparencia referencial
- Razonamiento Ecuacional
- Lambda
- Cálculo lambda
- Monoide
- Mónada
- Co-mónada
- Funtor aplicativo
- Semigrupo
- Tipo de unión
- Opción
- Librerías en JavaScript de programación funcional
Aridad
El número de argumentos que acepta una función. Derivada de unaria, binaria, ternaria, etc… Esta palabra tiene la distinción de estar compuesta de dos sufijos, “-aria” y “-idad”. Por ejemplo, la suma, acepta dos argumentos, y por lo tanto se define como una función binaria o una función de aridad dos. De igual manera, una función que acepta un número variable de argumentos es llamada de aridad variable.
const suma = (a, b) => a + b
const aridad = suma.length
console.log(aridad) // 2
// La aridad de la suma es 2
Funciones de Orden Superior (FOS)
Función que acepta una función como argumento y/o retorna otra función.
const filtro = (predicado, xs) => {
const resultado = []
for (let idx = 0; idx < xs.length; idx++) {
if (predicado(xs[idx])) {
resultado.push(xs[idx])
}
}
return resultado
}
const es = (tipo) => (x) => Object(x) instanceof tipo
filtro(es(Number), [0, '1', 2, null]) // [0, 2]
Aplicación Parcial
Aplicar parcialmente una función significa crear una nueva función al llamar la función original fijando algunos de sus argumentos.
// Helper para crear funciones aplicadas parcialmente
// Acepta una función y algunos argumentos
const parcial = (f, ...args) =>
// retorna una función que acepta el resto de los argumentos
(...masArgs) =>
// y llama la función original con todos ellos
f(...args, ...masArgs)
// Algo para aplicar
const suma3 = (a, b, c) => a + b + c
// Aplicar parcialmente `2` y `3` a `sumar3` retorna una función que acepta un argumento
const cincoMas = parcial(suma3, 2, 3) // (c) => 2 + 3 + c
cincoMas(4) // 9
También podemos utilizar Function.prototype.bind
para aplicar parcialmente una función en JS:
const suma1mas = suma3.bind(null, 2, 3) // (c) => 2 + 3 + c
La aplicación parcial permite crear funciones simples a partir de funciones complejas a medida que uno va obteniendo datos. La funciones “currying” son parcialmente aplicada de manera automática.
Currying
Es el proceso de convertir una función que acepta múltiples argumentos a una función que acepta dichos argumentos uno a la vez.
Cada vez que la función es invocada solo acepta un solo argumento y retorna otra función que a su vez acepta un solo argumento hasta que todos los argumentos hayan sido pasados.
const suma = (a, b) => a + b
const sumaCurried = (a) => (b) => a + b
sumaCurried(40)(2) // 42.
const suma2 = sumaCurried(2) // (b) => 2 + b
suma2(10) // 12
Auto Currying
Transformar una función que acepta múltiples argumentos en una que al recibir menos argumentos que el número total que acepta, retorna una función que acepta el resto de los argumentos. Cuando la función recibe el número correcto de argumentos, entonces es evaluada.
Underscore, lodash, y ramda tienen una función curry
que funciona de esta manera.
const suma = (x, y) => x + y
const sumaCurried = _.curry(suma)
sumaCurried(1, 2) // 3
sumaCurried(1) // (y) => 1 + y
sumaCurried(1)(2) // 3
Lecturas Adicionales (en inglés)
Composición de Funciones
El acto de unir dos funciones para formar una tercera función donde la salida de una función es la entrada de la otra.
const componer = (f, g) => (a) => f(g(a)) // Definición
const redondearYtoString = componer((val) => val.toString(), Math.floor) // Uso
redondearYtoString(121.212121) // '121'
Pureza
Una función es pura si el valor de retorno está solamente determinado por sus valores de entrada, y no produce efectos secundarios.
const saludar = (nombre) => 'Hola ' + nombre
saludar('Amelie') // 'Hola Amelie'
Contrario a:
let saludo
const saludar = () => {
saludo = 'Hola, ' + window.name
}
saludar() // "Hola Amelie"
Efectos secundarios
Se dice que una función o expresión tiene efectos secundarios si aparte de retornar un valor, esta interactúa (lee o escribe) con algún estado mutable externo.
const diferenteCadaVez = new Date()
console.log('E/S (entrada y salida) es un efecto secundario!')
Idempotente
Una función es idempotente cuando al ser invocada con su resultado no produce un resultado diferente.
f(f(x)) ≍ f(x)
Math.abs(Math.abs(10))
sort(sort(sort([2, 1])))
Estilo Point-Free
Escribir funciones donde su definición no identifica explícitamente los argumentos usados. Este estilo usualmente requiere currying u otras Funciones de Orden Superior (FOS). También conocido como Programación Tácita.
// Dado
const map = (fn) => (lista) => lista.map(fn)
const suma = (a) => (b) => a + b
// Entonces
// No es points-free - `números` es un argumento explicito
const incrementarTodos = (números) => map(suma(1))(números)
// Es Points-free - la lista de números es un argumento implícito
const incrementarTodos2 = map(suma(1))
incrementarTodos
identifica y usa el parámetro números
, por lo tanto no es point-free. incrementarTodos2
esta escrita mediante la simple combinación de funciones y valores, sin hacer mención de sus argumentos. Por lo tanto es points-free.
Las definiciones de funciones escritas utilizando el estilo Point-free se ven justo como las asignaciones normales sin la utilización de function
o =>
.
Predicado
Un predicado es una función que retorna verdadero o falso para un valor dado. Un uso común de un predicado es en el callback del método filter de un arreglo.
const predicado = (a) => a > 2
;[1, 2, 3, 4].filter(predicado) // [3, 4]
Contratos
Pendiente
Guarded Functions
Pendiente
Categorías
Objetos con funciones asociadas que siguen ciertas reglas.
Valor
Todo lo que puede ser asignado a una función.
5
Object.freeze({nombre: 'John', edad: 30}) // La función `freeze` hace que el objeto sea inmutable.
;(a) => a
;[1]
undefined
Constante
Una variable que no puede ser reasignada una vez definida.
const cinco = 5
const juan = {nombre: 'Juan', edad: 30}
Las constantes poseen transparencia referencial. Esto quiere decir que pueden ser reemplazadas por el valor que representan sin afectar el resultado.
Con las dos constantes de arriba la siguiente expresión siempre retornara true
.
juan.edad + cinco === ({name: 'Juan', edad: 30}).edad + (5)
Funtor
Un objeto que implementa una función map
la cual, mientras se ejecuta sobre cada valor del objeto para producir un nuevo objeto, se adhiere a dos reglas:
// preserva la identidad
object.map(x => x) === object
y
// composición
object.map(x => f(g(x))) === object.map(g).map(f)
(Sea f
, g
funciones arbritarias)
Un funtor muy común en JavaScript es Array
ya que cumple las dos reglas de funtor:
[1, 2, 3].map(x => x) // = [1, 2, 3]
y
const f = x => x + 1
const g = x => x * 2
;[1, 2, 3].map(x => f(g(x))) // = [3, 5, 7]
;[1, 2, 3].map(g).map(f) // = [3, 5, 7]
Transparencia referencial
Una expresión que puede ser reemplazada por su valor sin alterar el comportamiento del programa se dice que es referencialmente transparente.
Digamos que tenemos la función saludar:
const saludar = () => 'Hola Mundo!'
Cualquier invocación de saludar()
puede ser reemplazada por Hola Mundo!
, por lo tanto saludar
es referencialmente transparente.
Razonamiento Ecuacional
Cuando una aplicación esta compuesta de expresiones y es libre de efectos secundarios, verdades acerca del sistema pueden ser derivadas a partir de sus partes.
Lambda
Una función anónima que puede tratarse como un valor.
;(function (a) {
return a + 1
})
;(a) => a + 1
Lambdas a menudo se pasan como argumentos de funciones de Orden Superior.
[1, 2].map((a) => a + 1) // [2, 3]
Se puede asignar un lambda a una variables.
const add1 = (a) => a + 1
Cálculo lambda
Una rama de las matemáticas que utiliza funciones para crear un modelo universal de computación.
Monoide
Un objeto con una función que "combina" ese objeto con otro del mismo tipo.
Un monoide simple es la suma de números:
1 + 1 // 2
En este caso el número es el objeto y +
es la función.
Un valor "identidad" también debe existir, el cual al combinarse con un valor no lo cambia.
El valor identidad de la suma es 0
.
1 + 0 // 1
También se requiere que el agrupamiento de operaciones no afecte el resultado (asociatividad):
1 + (2 + 3) === (1 + 2) + 3 // verdadero
La concatenación de arreglos también forma un monoide.
;[1, 2].concat([3, 4]) // [1, 2, 3, 4]
El valor identidad es un arreglo vacío []
;[1, 2].concat([]) // [1, 2]
Si las funciones de identidad y composición son provistas, las funciones en si forman un monoide:
const identidad = (a) => a
const componer = (f, g) => (x) => f(g(x))
foo
es cualquier función que acepte un argumento.
componer(foo, identidad) ≍ componer(identidad, foo) ≍ foo
Mónada
Una mónada es un objeto con funciones of
y chain
. chain
es como map
excepto que desanida el objeto anidado resultante.
// Implementación
Array.prototype.chain = function (f) {
return this.reduce((acc, it) => acc.concat(f(it)), [])
}
// Uso
;['cat,dog', 'fish,bird'].chain((a) => a.split(',')) // ['cat', 'dog', 'fish', 'bird']
// Contraste de map
;['cat,dog', 'fish,bird'].map((a) => a.split(',')) // [['cat', 'dog'], ['fish', 'bird']]
of
también se conoce como return
en otros lenguajes funcional. chain
también se conoce como flatmap
y bind
en otros lenguajes funcional.
Co-mónada
Un objeto que tiene funciones extract
y extend
.
const CoIdentity = (v) => ({
val: v,
extract () {
return this.val
},
extend (f) {
return CoIdentity(f(this))
}
})
extract
toma un valor de un funtor.
CoIdentity(1).extract() // 1
extend
ejecuta una función en la co-mónada. La función debe devolver el mismo tipo que la co-mónada.
CoIdentity(1).extend((co) => co.extract() + 1) // CoIdentity(2)
Funtor aplicativo
Un funtor aplicativo es un objeto con una función ap
. ap
aplica una función en el objeto a un valor en otro objeto del mismo tipo.
// Implementación
Array.prototype.ap = function (xs) {
return this.reduce((acc, f) => acc.concat(xs.map(f)), [])
}
// Ejemplo de uso
;[(a) => a + 1].ap([1]) // [2]
Esto es útil si se tiene dos objetos y se quiere aplicar una función binaria a sus contenidos.
// Arreglos que se quieren combinar
const arg1 = [1, 3]
const arg2 = [4, 5]
// función de combinación - debe ser currificada para que funcione
const suma = (x) => (y) => x + y
const sumasParcialmenteAplicadas = [suma].ap(arg1) // [(y) => 1 + y, (y) => 3 + y]
Esto nos da un arreglo de funciones sobre el cual se puede llamar ap
para obtener el resultado:
sumasParcialmenteAplicadas.ap(arg2) // [5, 6, 7, 8]
Semigrupo
Un objeto que posee una función concat
que lo combina con otro objeto del mismo tipo.
;[1].concat([2]) // [1, 2]
Tipo de unión
Un tipo de unión es la combinación de dos tipos juntos en otro.
JS no tiene tipos estáticos, pero digamos que inventamos un tipo NumOrString
que es una suma de String
y Number
.
El operador +
en JS funciona en cadenas y números de modo que podemos utilizar este nuevo tipo para describir sus entradas y salidas:
// add :: (NumOrString, NumOrString) -> NumOrString
const add = (a, b) => a + b
add(1, 2) // Devuelve numero 3
add('Foo', 2) // Devuelve cadena "Foo2"
add('Foo', 'Bar') // Devuelve cadena "FooBar"
Tipos de unión también se conoce como tipos algebraicos, uniones etiquetadas, o tipos de suma.
Hay un par de bibliotecas en JS que ayuda con definir y uso de tipos de unión.
Opción
Opción es un tipo de unión con dos casos llamados Alguno
y Ninguno
.
Opción es útil para funciones compuestas que pueden no devolver un valor.
// Definición nativa
const Alguno = (v) => ({
val: v,
map (f) {
return Alguno(f(this.val))
},
chain (f) {
return f(this.val)
}
})
const Ninguno = () => ({
map (f) {
return this
},
chain (f) {
return this
}
})
// maybeProp :: (String, {a}) -> Opción a
const maybeProp = (key, obj) => typeof obj[key] === 'undefined' ? Ninguno() : Alguno(obj[key])
Use chain
para secuenciar las funciones que devuelven Opciones
// getItem :: Cart -> Opción CartItem
const getItem = (cart) => maybeProp('item', cart)
// getPrice :: Item -> Opción Number
const getPrice = (item) => maybeProp('price', item)
// getNestedPrice :: cart -> Opción a
const getNestedPrice = (cart) => getItem(obj).chain(getPrice)
getNestedPrice({}) // Ninguno()
getNestedPrice({item: {foo: 1}}) // Ninguno()
getNestedPrice({item: {price: 9.99}}) // Alguno(9.99)
Opción
es también conocido como Talvez
. Alguno
es también llamado Sólo
. Ninguno
a veces es llamado 'Nada'.
Librerías en JavaScript de programación funcional
Nota: Este repositorio ha sido un éxito gracias a las contribuciones de los colaboradores!