@Silv
Nie znam się na składaniu. Co to daje, tak ogólnie?
Odpowiem w poscie, bo więcej miejsca.
Załóżmy, że mamy taka tablicę użytkowników:
const users = [
{ name: 'John', age: 30, gender: 'male' },
{ name: 'Alice', age: 25, gender: 'female' },
{ name: 'Joe', age: 18, gender: 'male' },
]
Zakładam, że wiesz co to jest chainowanie metod w JSie i jak wygodne to jest. Stwórzmy sobie nową tablicę z wiekiem mężczyzn:
const isMale = (user) => user.gender === 'male'
const getAge = (user) => user.age
const malesAge = users
.filter(isMale)
.map(getAge)
Ok, fajnie - wygląda elegancko. Załózmy teraz, że dodatkowo chcemy stworzyć funkcję, która znajdzie wiek najstarszego mężczyzny, zserializuje dane i zapisze je do pliku, mogłoby to wyglądac tak:
const serialize = (name, value) => {
return JSON.stringify({ name, value })
}
const fakeSave = (file, content) => {
console.log(`Saved as ${file} with content: ${content}`)
}
const isMale = (user) => user.gender === 'male'
const getAge = (user) => user.age
const saveOldestMaleAge = (users) => {
const malesAge = users
.filter(isMale)
.map(getAge)
const oldestMaleAge = Math.max(...malesAge)
const serialized = serialize('oldestMaleAge', oldestMaleAge)
fakeSave('example.txt', serialized)
}
saveOldestMaleAge(users)
Nie ma tragedii, ale rozwiązanie ma kilka minusów:
- nie można chainować funkcji jeśli nie jest metodą obiektu na którym operujemy - mamy mix chainowania i zwykłych wywołań funkcji,
- tworzymy trzy dodatkowe zmienne, dla których trzeba wymyślić nazwy,
- recznie musimy przepychać wynik poprzedniej funkcji do funkcji następnej.
Oczywiście możemy pominąć dwa ostatnie punkty, ale robi się wtedy nieczytelna, zagnieżdżona kupa:
const serialize = (name, value) => {
return JSON.stringify({ name, value })
}
const fakeSave = (file, content) => {
console.log(`Saved as ${file} with content: ${content}`)
}
const isMale = (user) => user.gender === 'male'
const getAge = (user) => user.age
const saveOldestMaleAge = (users) => {
fakeSave(
'example.txt',
serialize(
'oldestMaleAge',
Math.max(
...users
.filter(isMale)
.map(getAge)
)
),
)
}
saveOldestMaleAge(users)
Stosując compose
(lub w moim przypadku pipe
, które działa niemal identycznie tylko ma imo bardziej naturalną kolejność wywołań), możemy zrobić to bardziej elegancko:
const pipe = (...fns) => (initialArg) => {
return fns.reduce((arg, fn) => fn(arg), initialArg)
}
const filter = (fn) => (arr) => arr.filter(fn)
const map = (fn) => (arr) => arr.map(fn)
const isMale = (user) => user.gender === 'male'
const getAge = (user) => user.age
const getMax = (arr) => Math.max(...arr)
const serialize = (name) => (value) => {
return JSON.stringify({ name, value })
}
const fakeSave = (file) => (content) => {
console.log(`Saved as ${file} with content: ${content}`)
}
const saveOldestMaleAge = pipe(
filter(isMale),
map(getAge),
getMax,
serialize('oldestMaleAge'),
fakeSave('example.txt'),
)
saveOldestMaleAge(users)
Minusem jest to, że JS, w przeciwieństwie do języków functional-first
- ma mało funkcyjną bibliotekę standardową, więc by móc w ten sposób skorzystać z Array.prototye.filter
, Array.prototype.map
i Math.max
trzeba napisać proste wrapery, ale jest to czynność jednorazowa i banalna (można też użyć gotowych bibliotek, które maja mnóstwo funkcji gotowych do takiego łączenia, np: ramda
, lodash/fp
).
Niewątpliwą zaletą jest natomiast to, że główna funkcja saveOldestMaleAge
nie potrzebuje pośrednich zmiennych i automatycznie przekazuje wyniki z jednej podfunkcji do drugiej, przy zachowaniu czytelności (czyta się wręcz jak pseudokod z krokami algorytmu). Takie podejście jest też maksymalnie reużywalne i komponowalne.
Można powiedzieć, że pipe
i compose
to taki funkcyjny odpowiednik chainowania, tyle, że nie jest ograniczony do metod obiektu, ale może uzywać dowolnych funkcji o zgodnym typie.
CodePen: https://codepen.io/caderek/pen/RwbLZMO?editors=0012
PS
Wersja compose
, której ja używam (imo prostsza do zrozumienia niż wersja OP), analogicznie do pipe
:
const compose = (...fns) => (initialArg) => {
return fns.reduceRight((arg, fn) => fn(arg), initialArg)
}
Jest tu jedna róznica w API w stosunku do wersji OP - w wersji OP pierwsza funkcja może przyjąć więcej niż jeden argument, co nie jest konieczne jak masz currying.
PPS
W rzeczywistym przykładzie pewnie podzieliłby odpowiedzialności funkcji saveOldestMaleAge
, ale jako przykład komponowania funkcji myślę, że ujdzie.