Wprowadzenie do kombinatorów
Kombinatory to funkcje wyższego poziomu, które umożliwiają łączenie funkcji, zmiennych lub innych kombinatorów. Zazwyczaj nie zawierają deklaracji własnych zmiennych lub logiki biznesowej. Są jedynymi, które mogą spłukiwać kontrolę w programie funkcyjnym.
W tym artykule postaram się omówić kilka z nich:
- Kran
- Currying
- Pipe/Compose
- Widelec
- Alternacja
- Sekwencja
Kran
Kombinator jest bardzo przydatny w przypadku funkcji, które nic nie zwracają. Pobiera on funkcję, do której trafia parametr, a następnie jest on zwracany.
Deklaracja
const tap = (fn) => (value) => {
fn(value);
return value;
};
Przykład imperatywu
const [items, setItems] = useState(() => [])
axios
.get('http://localhost')
.then({ data } => {
setItems(data)
console.log(data)
onLoadData(data)
}).then(...) // zwraca undefined - dane w obietnicy zostały zmodyfikowane
Przykład deklaratywny
const [items, setItems] = useState(() => [])
axios
.get('http://localhost')
.then(({ dane }) => dane)
.then(tap(setItems)) // (dane) => { setItems(dane); return dane }
.then(tap(console.log)) // jeden then = jedna funkcja = jedna odpowiedzialność
.then(tap(onLoadData))
.then(...) // wciąż dostęp do oryginalnych danych
// łatwe w utrzymaniu zasady otwórz/zamknij
Currying
Rozdziela argumenty funkcji i umożliwia ich sekwencyjne wywoływanie.
Deklaracja
const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args);
};
return curried
};
Przykład
const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args);
};
return curried
};
const sum = (a, b, c) => a + b + c
const currySum = curry(sum)
/*
możliwe wywołania
currySum(a)(b)(c),
currySum(a)(b,c),
currySum(a,b)(c),
currySum(a,b,c)
*/
currySum(1) // (b, c) => 1 + a + b lub (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) // zwraca 5
const multiplyByFive = multiplyBy(5)
multiplyByFive(10) // zwraca 50
Pipe/Compose
Dzięki kompozycji możliwe jest przekazywanie danych z jednej funkcji do drugiej. Ważne jest, aby funkcje przyjmowały taką samą liczbę argumentów. Różnica między pipe i compose polega na tym, że pierwsza z nich wykonuje funkcje od pierwszej do ostatniej, a compose wywołuje je od końca.
Deklaracja
const pipe = (...fns) => (value, ...args) =>
fns.reduce((v, f, i) =>
i === 0
? f(v, ...args)
: f(v),
value);
const compose = (...fns) => (value, ...args) =>
fns.reduceRight((v, f, i) =>
i == (fns.length - 1)
? f(v, ...args)
: f(v),
value);
const pipe = (...fns) => (value, ...args) =>
fns.reduce((v, f, i) =>
i === 0
? f(v, ...args)
: f(v),
value);
const compose = (...fns) => (value, ...args) =>
fns.reduceRight((v, f, i) =>
i == (fns.length - 1)
? f(v, ...args)
: f(v),
value);
Przykład imperatywu
const sine = (val) => Math.sin(val * Math.PI / 180) // nie do odczytania
sin(90) // zwraca 1
Przykład deklaratywny
const sine = pipe(
multiplyBy(Math.PI) // ↓ val * Math.PI
divideBy(180), // ↓ val * Math.PI / 180
Math.sin, // ↓ Math.sin(val * Math.PI / 180)
)
const sin = compose(
Math.sin, // ↑ Math.sin(val * Math.PI / 180)
divideBy(180), // ↑ val * Math.PI / 180
multiplyBy(Math.PI) // ↑ val * Math.PI
)
sin(90) // zwraca 1
Widelec
Kombinator rozwidleń jest przydatny w sytuacjach, gdy chcemy przetworzyć wartość na dwa sposoby i połączyć wyniki.
Deklaracja
const fork = (join, fn1, fn2) => (value) => join(fn1(value), fn2(value));
Przykład
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]) // zwraca 4
Alternacja
Kombinator ten pobiera dwie funkcje i zwraca wynik pierwszej z nich, jeśli jest on prawdziwy. W przeciwnym razie zwraca wynik drugiej funkcji.
Deklaracja
const alt = (fn, orFn) => (value) => fn(value) || orFn(value)
Przykład
const users = [{
uuid: '123e4567-e89b-12d3-a456-426655440000',
name: 'William'
}]
const findUser = ({ uuid: searchesUuid }) =>
users.find(({ uuid }) => uuid == searchesUuid)
const newUser = data => ({ ...data, uuid: uuid() // utwórz nowy uuid })
const findOrCreate = alt(findUser, newUser)
findOrCreate({ uuid: '123e4567-e89b-12d3-a456-426655440000' }) // zwraca obiekt William
findOrCreate({ name: 'John' }) // zwraca obiekt John z nowym uuid
Sekwencja
Akceptuje wiele niezależnych funkcji i przekazuje ten sam parametr do każdej z nich. Zazwyczaj funkcje te nie zwracają żadnej wartości.
Deklaracja
const seq = (...fns) => (val) => fns.forEach(fn => fn(val))
Przykład
const appendUser = (id) => ({ name }) => {
document.getElementById(id).innerHTML = name
}
const printUserContact = pipe(
findOrCreate, // zwraca użytkownika
seq(
appendUser('#contact'), // użytkownik => void
console.log, // użytkownik => void
onContactUpdate // użytkownik => void
)
)
printUserContact(userData)
