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

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)
baner współpracy
pl_PLPolish