Introducción a los combinadores
Los combinadores son funciones de nivel superior que permiten combinar funciones, variables u otros combinadores. Normalmente, no contienen declaraciones de variables propias ni lógica de negocio. Son los únicos que permiten el control en un programa de funciones.
En este artículo intentaré tratar algunos de ellos:
- Toque
- Curry
- Tubería/Composición
- Horquilla
- Alternancia
- Secuencia
Toque
Un combinador es muy útil para funciones que no devuelven nada. Toma la función a la que va el parámetro y luego lo devuelve.
Declaración
const tap = (fn) => (valor) => {
fn(valor);
devolver valor;
};
Ejemplo imperativo
const [items, setItems] = useState(() => [])
axios
.get('http://localhost')
.then({ datos } => {
setItems(datos)
console.log(datos)
onLoadData(datos)
}).then(...) // devuelve undefined - los datos de la promesa han sido modificados
Ejemplo declarativo
const [items, setItems] = useState(() => [])
axios
.get('http://localhost')
.then(({ datos }) => datos)
.then(tap(setItems)) // (datos) => { setItems(datos); return datos }
.then(tap(console.log)) // un then = una función = una responsabilidad
.then(tap(onLoadData))
.then(...) // todavía acceso a los datos originales
// fácil de mantener el principio de abrir/cerrar
Curry
Divide los argumentos de una función y permite llamarlos secuencialmente.
Declaración
const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args);
};
return curried
};
Ejemplo
const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args);
};
return curried
};
const suma = (a, b, c) => a + b + c
const currySuma = curry(suma)
/*
posibles llamadas
currySuma(a)(b)(c),
currySuma(a)(b,c),
currySuma(a,b)(c),
currySuma(a,b,c)
*/
currySum(1) // (b, c) => 1 + a + b o (b) => (c) => 1 + a + b
currySum(5)(10) // (c) => 5 + 10 + b
currySum(5, 10) // (c) => 5 + 10 + b
currySuma(5)(10)(20) // 35
currySuma(5, 10)(20) // 35
currySuma(5)(10, 20) // 35
const dividirPor = curry((a, b) => b / a)
const multiplicarPor = curry((a, b) => a * b)
const dividePorDos = dividePor(2)
divideByTwo(10) // devuelve 5
const multiplicarPorCinco = multiplicarPor(5)
multiplyByFive(10) // devuelve 50
Tubería/Composición
Mediante la composición, es posible pasar datos de una función a otra. Es importante que las funciones tomen el mismo número de argumentos. La diferencia entre pipe y compose es que la primera ejecuta la función de la primera a la última, y compose las llama desde el final.
Declaración
const pipe = (...fns) => (valor, ...args) =>
fns.reduce((v, f, i) =>
i === 0
? f(v, ...args)
: f(v),
valor);
const compose = (...fns) => (valor, ...args) =>
fns.reduceRight((v, f, i) =>
i === (fns.length - 1)
? f(v, ...args)
: f(v),
valor);
const pipe = (...fns) => (valor, ...args) =>
fns.reduce((v, f, i) =>
i === 0
? f(v, ...args)
: f(v),
valor);
const compose = (...fns) => (valor, ...args) =>
fns.reduceRight((v, f, i) =>
i === (fns.length - 1)
? f(v, ...args)
: f(v),
valor);
Ejemplo imperativo
const seno = (val) => Math.sin(val * Math.PI / 180) // no legible
sine(90) // devuelve 1
Ejemplo declarativo
const seno = pipe(
multiplyBy(Math.PI) // ↓ val * Math.PI
dividePor(180), // ↓ val * Math.PI / 180
Math.sin, // ↓ Math.sin(val * Math.PI / 180)
)
const seno = componer(
Math.sin, // ↑ Math.sin(val * Math.PI / 180)
dividePor(180), // ↑ val * Math.PI / 180
multiplyBy(Math.PI) // ↑ val * Math.PI
)
seno(90) // devuelve 1
Horquilla
El combinador de horquilla es útil en situaciones en las que se desea procesar un valor de dos formas y combinar el resultado.
Declaración
const fork = (join, fn1, fn2) => (valor) => join(fn1(valor), fn2(valor));
Ejemplo
const longitud = (array) => array.longitud
const suma = (array) => array.reduce((a, b) => a + b, 0)
const divide = (a, b) => a / b
const media aritmética = fork(divide, suma, longitud)
arithmeticAverage([5, 3, 2, 8, 4, 2]) // devuelve 4
Alternancia
Este combinador toma dos funciones y devuelve el resultado de la primera si es verdadero. En caso contrario, devuelve el resultado de la segunda función.
Declaración
const alt = (fn, orFn) => (valor) => fn(valor) || orFn(valor)
Ejemplo
const usuarios = [{
uuid: '123e4567-e89b-12d3-a456-426655440000',
name: 'William'
}]
const findUser = ({ uuid: searchesUuid }) =>
users.find(({ uuid }) => uuid === searchesUuid)
const newUser = datos => ({ ...datos, uuid: uuid() // crear nuevo uuid })
const findOrCreate = alt(findUser, newUser)
findOrCreate({ uuid: '123e4567-e89b-12d3-a456-426655440000' }) // devuelve el objeto William
findOrCreate({ name: 'John' }) // devuelve el objeto John con el nuevo uuid
Secuencia
Acepta muchas funciones independientes y pasa el mismo parámetro a cada una de ellas. Normalmente, estas funciones no devuelven ningún valor.
Declaración
const seq = (...fns) => (val) => fns.forEach(fn => fn(val))
Ejemplo
const appendUser = (id) => ({ name }) => {
document.getElementById(id).innerHTML = nombre
}
const printUserContact = pipe(
findOrCreate, // devuelve el usuario
seq(
appendUser('#contact'), // usuario => void
console.log, // usuario => void
onContactUpdate // usuario => void
)
)
printUserContact(userData)
