Currying jest kolejną częścią serii o programowaniu funkcyjnym. Currying można opisać na kilka sposobów, jednak ja postaram się przedstawić wykorzystanie currying posługując się przykładami krok po kroku. W szczególności zachęcam do przeczytania artykułu do końca, gdyż ostatni przykład sprawi, że użycie currying stanie się jasne i zrozumiałe. 🙂
Na początek trochę teorii…
Czym zatem jest currying?
Currying jest techniką, która pozwala przekształcić funkcję składającą się z wielu parametrów np. funkcja(param1, param2, param3)
na funkcja(param1)(param2)(param3)
. Currying wykorzystywany jest nie tylko w JavaScript, ale też w innych językach, natomiast bardzo często technika ta jest kojarzona z programowaniem funkcyjnym. Jak widać w powyższym opisie, currying to też proces, dla którego którego funkcję przyjmującą wiele parametrów możemy przedstawić za pomocą sekwencji zagnieżdżonych funkcji, z których każda przyjmuje jeden parametr.
Dodatkowo zagnieżdżona funkcja może zwracać kolejna funkcję i tak dalej…
Na koniec, ostatnia funkcja zwraca wartość końcową. Brzmi trochę dziwnie, dlatego od razu posłużę się przykładem.
const addStandard = (a, b, c) => { return a + b + c; } function addCurrying(a) { return function(b) { return function(c) { return a + b + c; } } } console.log(addStandard(1,2,3)); // 6 console.log(addCurrying(1)(2)(3)); // 6
Drugą funkcję celowo zapisałem w standardowy sposób bez użycia arrow function, aby return
jawnie wskazywał, w którym miejscu i co jest zwracane… Wróćmy jeszcze raz do definicji. W standardowym podejściu widzimy funkcję z 3 parametrami, która dodaje do siebie liczby. Przy wykorzystaniu currying pierwsza funkcja przyjmuje jeden parametr, druga również jeden parametr, trzecia też jeden parametr, natomiast ta ostatnia korzysta z poprzednio przekazanych parametrów, dodaje do siebie liczby i zwraca wynik końcowy.
const add1 = addCurrying(1); // przekazaliśmy 1 parametr add1(2); // [Function] - została zwrócona funkcja
Wszystko się zgadza. Funkcja addCurrying()
przyjmuje jeden parametr i zwraca funkcję.
Pierwszy przykład z wykorzystaniem currying
Przejdźmy może do pierwszego przykładu… Posłużymy się częścią kodu z innego artykułu dot. programowania funkcyjnego, dotyczącego higher order functions.
Wyobraźmy sobie, że chcemy wyświetlić tylko produkty o typie chleb. Możemy zrobić to w następujący sposób, bez użycia currying’u:
let products = [ { type: 'bread', price: 10 }, { type: 'bread', price: 15 }, { type: 'bread', price: 20 }, { type: 'cake', price: 100 }, { type: 'cake', price: 120 }, { type: 'cake', price: 140 } ]; let hasProductType = (typeElement, obj) => obj.type === typeElement let breadProducts = products.filter(product => hasProductType('bread', product)) console.log(breadProducts); // [ { type: 'bread', price: 10 }, // { type: 'bread', price: 15 }, // { type: 'bread', price: 20 } ]
Zmodyfikujmy teraz hasProducType()
, zgodnie z currying. Funkcja zewnętrzna przyjmuje jeden parametr i zwraca kolejną funkcję, która również przyjmuje jeden parametr. Na koniec funkcja zwraca rezultat.
let hasProductTypeCurrying = function(typeElement) { return function (obj) { return obj.type === typeElement } } let breadProductsCurryingResult = products.filter(product => hasProductTypeCurrying('bread')(product)); console.log(breadProductsCurryingResult); // [ { type: 'bread', price: 10 }, // { type: 'bread', price: 15 }, // { type: 'bread', price: 20 } ]
Teraz wewnątrz products.filter()
zmieniło się wywołanie funkcji hasProductTypeCurrying()
.
W tym miejscu warto skorzystać z wbudowanych rozwiązań do obsługi currying’u, które są dostępne w bibliotekach takich jak lodash czy Ramda.
Ja skorzystam z biblioteki lodash, gdyż takiej biblioteki używam na co dzień w projekcie. Obsługę curryingu możemy uzyskać poprzez:
_.curry(funkcja)
Dzięki temu unikniemy tworzenia zagnieżdżonych funkcji, a nasza funkcja i późniejsze jej wywołanie będą wyglądała następująco:
let hasProductTypeCurrying = _.curry((typeElement, obj) => obj.type === typeElement); let breadProductsCurryingResult = products.filter(hasProductTypeCurrying('bread'));
Zmienił się sposób wywołania products.filter()
oraz hasProductTypeCurrying()
. Unikamy przekazywania drugiego parametru, gdyż w tym przypadku wartość przekazywana jest automatycznie.
Kolejny przykład z wykorzystaniem currying
Wyobraźmy sobie, że w aplikacji chcemy zaimplementować obsługę logowania błędów i innego typu wiadomości. Logowanie może mieć kilka typów. Możemy logować błędy, logi mogą być informacyjne, lub zawierać informację o sukcesie wykonania operacji. Dodatkowo podczas logowania przydałby się czas wystąpienia oraz komunikat błędu. Prosta funkcja obsługująca logowanie mogłaby wyglądać w ten sposób:
const log = (type, objDate, message) => { console.log(`[${type}] ${objDate.toGMTString()} - ${message}`); } log('ERROR', new Date(), 'error example'); // [ERROR] Sun, 11 Jul 2020 12:00:00 GMT - error example
Teraz jeżeli chcemy ponownie skorzystać z funkcji, musimy na nowo podawać typ itd.. Wygląda to niezbyt fajnie…
log('ERROR', new Date(), 'error example two'); log('ERROR', new Date(), 'error example three'); log('SUCCESS', new Date(), 'some info');
Przekształcimy teraz na funkcję z użyciem currying, która będzie wyglądać następująco…
const log = (type) => ( (objDate) => ( (message) => ( console.log(`[${type}] ${objDate.toGMTString()} - ${message}`) ) ) ) log('ERROR')(new Date())('error example'); // [ERROR] Sun, 11 Jul 2020 12:00:00 GMT - error example
Funkcja zwróciła dokładnie to samo. Jednak dzięki zastosowaniu currying możemy zastosować pewien trik. 🙂 Aby nie powielać za każdym razem typu logowania wiadomości, możemy utworzyć interesujące nas typy logów…
const logError = log('ERROR'); const logInfo = log('INFO'); logError(new Date())('error one'); // [ERROR] Sun, 11 Jul 2020 12:00:00 GMT - error one logError(new Date())('error two'); // [ERROR] Sun, 11 Jul 2020 12:00:00 GMT - error two logInfo(new Date())('info message'); // [INFO] Sun, 11 Jul 2020 12:00:00 GMT - info message logInfo(new Date())('info message next'); // [INFO] Sun, 11 Jul 2020 12:00:00 GMT - info message next
Powyższy przykład pokazuje, że zamiast przekazywać za każdym razem typ logowania, korzystamy z funkcji logError()
oraz logInfo()
unikając przekazywania pierwszego parametru. Tutaj możemy pójść krok dalej i stworzyć metodę, która zawsze będzie wyświetlała typ oraz aktualny czas, a my będziemy zobowiązani tylko do przekazania wiadomości…
const logErrorNow = log('ERROR')(new Date()); const logInfoNow = log('INFO')(new Date()); logErrorNow('error one'); // [ERROR] Sun, 11 Jul 2020 12:00:00 GMT - error one logInfoNow('info message'); // [INFO] Sun, 11 Jul 2020 12:00:00 GMT - info message
Mam nadzieję, że ten ostatni przykład pokazał do czego możemy zastosować currying. W standardowym podejściu, gdzie funkcja log()
posiada 3 parametry nie byłoby to możliwe. W normalnym podejściu powielamy parametry, a w przypadku zmian konieczne jest odszukanie wszystkich wystąpień i zamiana fragmentów w wielu miejscach… Wykorzystując currying przekazujemy jeden parametr, co sprawia, że kod jest łatwiejszy do debugowania, dodatkowo mamy możliwość ponownego użycia kodu.
Implementacja curry() bez użycia bibliotek
Na koniec chciałbym jeszcze przedstawić przykład implementacji metody curry()
bez użycia zewnętrznych bibliotek:
function curry(f) { return function currify() { const args = Array.prototype.slice.call(arguments); return args.length >= f.length ? f.apply(null, args) : currify.bind(null, ...args) } } const add = (a,b,c) => ( a + b + c ); const addCurry = curry(add); console.log(addCurry(1)(2)(3)); // 6
Dzięki za Twój czas… 🙂
Źródła:
Obraz: https://unsplash.com/photos/ZN-TT10kf4o
Implementacja metody curry(): https://medium.com/@juliomatcom/an-elegant-and-simple-curry-f-implementation-in-javascript-cf36252cff4c