Introduzione ai combinatori
I combinatori sono funzioni di livello superiore che consentono di combinare funzioni, variabili o altri combinatori. Di solito, non contengono dichiarazioni delle proprie variabili o della logica aziendale. Sono gli unici che permettono di controllare il controllo in un programma di funzioni.
In questo articolo cercherò di trattarne alcuni:
- Rubinetto
- Arricciatura
- Tubo/Comporre
- Forcella
- Alternanza
- Sequenza
Rubinetto
Un combinatore è molto utile per le funzioni che non restituiscono nulla. Prende la funzione a cui va il parametro e poi la restituisce.
Dichiarazione
const tap = (fn) => (valore) => {
fn(valore);
restituisce il valore;
};
Esempio di imperativo
const [items, setItems] = useState(() => [])
axios
.get('http://localhost')
.then({dati } => {
setItems(dati)
console.log(dati)
onLoadData(dati)
}).then(...) // restituisce undefined - i dati nella promessa sono stati modificati
Esempio dichiarativo
const [items, setItems] = useState(() => [])
axios
.get('http://localhost')
.then(({dati }) => dati)
.then(tap(setItems)) // (data) => { setItems(data); return data }
.then(tap(console.log)) // un then = una funzione = una responsabilità
.then(tap(onLoadData))
.then(...) // ancora accesso ai dati originali
// facile mantenere il principio di apertura/chiusura
Arricciatura
Divide gli argomenti di una funzione e permette di chiamarli in sequenza.
Dichiarazione
const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args);
};
restituire curried
};
Esempio
const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args);
};
restituire curried
};
const somma = (a, b, c) => a + b + c
const currySum = curry(sum)
/*
possibili chiamate
currySum(a)(b)(c),
currySum(a)(b,c),
currySum(a,b)(c),
currySum(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
currySum(5)(10)(20) // 35
currySum(5, 10)(20) // 35
currySum(5)(10, 20) // 35
const divideBy = curry((a, b) => b / a)
const multiplyBy = curry((a, b) => a * b)
const divideByTwo = divideBy(2)
divideByTwo(10) // restituisce 5
const multiplyByFive = multiplyBy(5)
multiplyByFive(10) // restituisce 50
Tubo/Comporre
Attraverso la composizione, è possibile passare i dati da una funzione all'altra. È importante che le funzioni accettino lo stesso numero di argomenti. La differenza tra pipe e compose è che la prima esegue le funzioni dalla prima all'ultima, mentre compose le chiama dalla fine.
Dichiarazione
const pipe = (...fns) => (value, ...args) =>
fns.reduce((v, f, i) =>
i === 0
f(v, ...args)
: f(v),
valore);
const compose = (...fns) => (valore, ...args) =>
fns.reduceRight((v, f, i) =>
i === (fns.length - 1)
? f(v, ...args)
: f(v),
valore);
const pipe = (...fns) => (value, ...args) =>
fns.reduce((v, f, i) =>
i === 0
f(v, ...args)
: f(v),
valore);
const compose = (...fns) => (valore, ...args) =>
fns.reduceRight((v, f, i) =>
i === (fns.length - 1)
? f(v, ...args)
: f(v),
valore);
Esempio di imperativo
const seno = (val) => Math.sin(val * Math.PI / 180) // non leggibile
sine(90) // restituisce 1
Esempio dichiarativo
const sine = pipe(
multiplyBy(Math.PI) // ↓ val * Math.PI
divideBy(180), // ↓ val * Math.PI / 180
Math.sin, // ↓ Math.sin(val * Math.PI / 180)
)
const sine = compose(
Math.sin, // ↑ Math.sin(val * Math.PI / 180)
divideBy(180), // ↑ val * Math.PI / 180
multiplyBy(Math.PI) // ↑ val * Math.PI
)
seno(90) // restituisce 1
Forcella
Il combinatore a forcella è utile nelle situazioni in cui si desidera elaborare un valore in due modi e combinare il risultato.
Dichiarazione
const fork = (join, fn1, fn2) => (valore) => join(fn1(valore), fn2(valore));
Esempio
const length = (array) => array.length
const sum = (array) => array.reduce((a, b) => a + b, 0)
const divide = (a, b) => a / b
const arithmeticAverage = fork(divide, sum, length)
arithmeticAverage([5, 3, 2, 8, 4, 2]) // restituisce 4
Alternanza
Questo combinatore accetta due funzioni e restituisce il risultato della prima se è vero. Altrimenti, restituisce il risultato della seconda funzione.
Dichiarazione
const alt = (fn, orFn) => (valore) => fn(valore) || orFn(valore)
Esempio
const utenti = [{
uuid: '123e4567-e89b-12d3-a456-426655440000',
nome: 'William'
}]
const findUser = ({ uuid: searchesUuid }) =>
users.find(({ uuid }) => uuid === searchesUuid)
const newUser = data => ({ ...data, uuid: uuid() // crea un nuovo uuid })
const findOrCreate = alt(findUser, newUser)
findOrCreate({ uuid: '123e4567-e89b-12d3-a456-426655440000' }) // restituisce l'oggetto William
findOrCreate({nome: 'John' }) // restituisce l'oggetto John con il nuovo uuid
Sequenza
Accetta molte funzioni indipendenti e passa lo stesso parametro a ciascuna di esse. In genere, queste funzioni non restituiscono alcun valore.
Dichiarazione
const seq = (...fns) => (val) => fns.forEach(fn => fn(val))
Esempio
const appendUser = (id) => ({ name }) => {
document.getElementById(id).innerHTML = nome
}
const printUserContact = pipe(
findOrCreate, // restituisce l'utente
seq(
appendUser('#contact'), // utente => void
console.log, // utente => void
onContactUpdate // utente => void
)
)
printUserContact(userData)
