libs:
- lodash
Curryování neboli currying je pokročilá technika práce s funkcemi. Používá se nejen v JavaScriptu, ale i v jiných jazycích.
Curryování je transformace funkce, která přeloží funkci volatelnou způsobem f(a, b, c)
na funkci volatelnou způsobem f(a)(b)(c)
.
Curryování nevolá funkci, jen ji transformuje.
Nejprve se podívejme na příklad, abychom lépe porozuměli tomu, o čem se tady mluví, a pak na praktické aplikace.
Vytvořme pomocnou funkci curry(f)
, která provádí curryování dvouargumentové funkce f
. Jinými slovy, curry(f)
na dvouargumentové funkci f(a, b)
ji přeloží na funkci, která se bude spouštět jako f(a)(b)
:
*!*
function curry(f) { // curry(f) provede curryovací transformaci
return function(a) {
return function(b) {
return f(a, b);
};
};
}
*/!*
// použití
function součet(a, b) {
return a + b;
}
let curryovanýSoučet = curry(součet);
alert( curryovanýSoučet(1)(2) ); // 3
Jak vidíte, implementace je přímočará: jsou to pouhé dva obaly.
- Výsledkem
curry(func)
je obalfunction(a)
. - Když je zavolán způsobem
curryovanýSoučet(1)
, argument se uloží do lexikálního prostředí a vrátí se nový obalfunction(b)
. - Pak je tento obal volán s argumentem
2
a předá volání původní funkcisoučet
.
Pokročilejší implementace curryování, např. _.curry z knihovny lodash, vrátí obal, který umožní volat funkci jak obvyklým způsobem, tak parciálně:
function součet(a, b) {
return a + b;
}
let curryovanýSoučet = _.curry(součet); // použijeme _.curry z knihovny lodash
alert( curryovanýSoučet(1, 2) ); // 3, stále volatelná normálně
alert( curryovanýSoučet(1)(2) ); // 3, voláno parciálně
Abychom pochopili výhody, potřebujeme cenný příklad z reálného života.
Mějme například logovací funkci log(datum, důležitost, zpráva)
, která naformátuje a vypíše zadané informace. Ve skutečných projektech mají takové funkce mnoho užitečných možností, např. posílání logů po síti, zde jenom zavoláme alert
:
function log(datum, důležitost, zpráva) {
alert(`[${datum.getHours()}:${datum.getMinutes()}] [${důležitost}] ${zpráva}`);
}
Zcurryujme ji!
log = _.curry(log);
Pak bude log
fungovat normálně:
log(new Date(), "LADĚNÍ", "nějaké ladění"); // log(a, b, c)
...Ale bude fungovat i v curryované formě:
log(new Date())("LADĚNÍ")("nějaké ladění"); // log(a)(b)(c)
Nyní můžeme snadno vytvořit pohodlnou funkci pro aktuální logování:
// logNyní bude parciální log s pevným prvním argumentem
let logNyní = log(new Date());
// použijeme ji
logNyní("INFO", "zpráva"); // [HH:mm] INFO zpráva
Nyní logNyní
je log
s pevným prvním argumentem, jinými slovy „parciálně aplikovaná funkce“ nebo krátce „parciální funkce“.
Můžeme jít dál a vytvořit pohodlnou funkci pro aktuální logování ladění:
let laděníNyní = logNyní("LADĚNÍ");
laděníNyní("zpráva"); // [HH:mm] LADĚNÍ zpráva
Tedy:
- Po curryování nic neztratíme:
log
se stále dá volat běžným způsobem. - Můžeme snadno generovat parciální funkce, např. pro logování s dnešním datem.
Pro případ, že byste chtěli zajít do detailů, je zde „pokročilá“ implementace curryování pro víceargumentové funkce, kterou bychom mohli použít výše.
Je opravdu krátká:
function curry(funkce) {
return function curryovaná(...args) {
if (args.length >= funkce.length) {
return funkce.apply(this, args);
} else {
return function(...args2) {
return curryovaná.apply(this, args.concat(args2));
}
}
};
}
Příklady použití:
function součet(a, b, c) {
return a + b + c;
}
let curryovanýSoučet = curry(součet);
alert( curryovanýSoučet(1, 2, 3) ); // 6, stále normálně volatelná
alert( curryovanýSoučet(1)(2,3) ); // 6, curryování 1. argumentu
alert( curryovanýSoučet(1)(2)(3) ); // 6, úplné curryování
Nová funkce curry
může vypadat komplikovaně, ale ve skutečnosti je snadné jí porozumět.
Výsledkem volání curry(funkce)
je obal curryovaná
, který vypadá takto:
// funkce je funkce, která se má transformovat
function curryovaná(...args) {
if (args.length >= funkce.length) { // (1)
return funkce.apply(this, args);
} else {
return function(...args2) { // (2)
return curryovaná.apply(this, args.concat(args2));
}
}
};
Když ji spustíme, obsahuje dvě běhové větve if
:
- Je-li počet předaných argumentů
args
stejný nebo větší, než v definici původní funkce (funkce.length
), pak jí jen předáme volání pomocífunkce.apply
. - V opačném případě získáme parciální funkci: ještě funkci
funkce
nebudeme volat. Místo toho se vrátí další obal, který znovu aplikuje funkcicurryovaná
a poskytne jí předchozí argumenty společně s novými.
Když ji pak znovu zavoláme, získáme buď novou parciální funkci (nemáme-li ještě dost argumentů), nebo nakonec výsledek.
Curryování vyžaduje, aby funkce měla pevný počet argumentů.
Funkci, která používá zbytkové parametry, např. `f(...args)`, nelze tímto způsobem curryovat.
Podle definice by curryování mělo převést `součet(a, b, c)` na `součet(a)(b)(c)`.
Většina implementací curryování v JavaScriptu je však pokročilá, jak bylo uvedeno: udržují funkci volatelnou i ve víceargumentové variantě.
Curryování je transformace, která umožní volat f(a,b,c)
jako f(a)(b)(c)
. Implementace v JavaScriptu obvykle současně ponechají funkci volatelnou normálně, ale není-li poskytnuto dost argumentů, vrátí parciální funkci.
Curryování nám umožní snadno získat parciální funkci. Jak jsme viděli v příkladu s logováním, univerzální tříargumentová funkce log(datum, důležitost, zpráva)
nám po curryování vydá parciální funkci, když je volána s jedním argumentem (např. log(datum)
) nebo se dvěma argumenty (např. log(datum, důležitost)
).