Funkční programování (FP) jakýmkoli jiným názvem…

Nebojte se, toto není YAMA (další monádní článek)! Místo toho chci mluvit o knihovně, kterou jsem nedávno vydal a která nabízí užitečné zkroucení typických operací funkcionálního programování ("FP") (jako 03 , 14 , atd.).

Než se do toho pustíme:pokud jste jako já a snažili jste se porozumět FP (a jak jej aplikovat na JavaScript), pak vás frustruje a inspiruje šílená terminologie jako „funktory“ nebo luxusní notace jako 29 , možná se budete chtít podívat na mou nejnovější knihu Functional-Light JS (kterou si můžete přečíst zdarma online!).

Moje kniha má velmi odlišný pohled; přistupuje k FP neformálně, od základu, aniž by byl tak náročný na terminologii, a nespoléhá se téměř na žádný zápis. Cílem je pragmaticky vysvětlit důležité základní pojmy způsoby, které můžete skutečně použít ve svých programech.

Poznámka: Od této chvíle očekávám, že budete obeznámeni s funkcemi ES6, jako je 36 šíření a ničení. Stále je na nich nejasný? Žádný strach, taky jsem o tom napsal knihu! Podívejte se na You Don't Know JS:ES6 &Beyond, zejména na kapitolu 2.

Problém

V JS už je spousta skvělých knihoven FP, tak proč mě napadlo vybudovat novou!? Dovolte mi vysvětlit mé motivace. Mějte se mnou trpělivost, protože chci, abyste jim plně porozuměli, abyste je získali proč potřebujeme YAFPL. :)

Začněme nejprve tím, že se podíváme na nějaký kód, který ilustruje jednu z mých mnoha frustrací, když jsem se učil a snažil se více pracovat s FP v mém JavaScriptu. Pro toto srovnání použiji Ramdu, ale jakákoliv běžná knihovna FP-JS bude stačit:

function lowercase(v) { return v.toLowerCase(); }
function uppercase(v) { return v.toUpperCase(); }

var words = ["Now","Is","The","Time"];
var moreWords = ["The","Quick","Brown","Fox"];

var f = R.map( uppercase );
f( words );                        // ["NOW","IS","THE","TIME"]
f( moreWords );                    // ["THE","QUICK","BROWN","FOX"]

Jako u všech metod v Ramda, 46 je curried, což znamená, že i když očekává 2 argumenty, můžeme jej zavolat pouze 55 , čímž vznikne specializovanější 68 funkce, která pak čeká na přemapování pole. To nám umožní zavolat 78 s různými poli, přičemž každá hodnota v nich se píše velkými písmeny.

Možná si neuvědomujete, že na pořadí těchto argumentů ze své podstaty záleží. 82 očekává nejprve funkci mapovače a poté pole. V tomto případě je to pro nás výhodné, protože ji chceme specializovat na tuto sekvenci (nejprve funkce mapovače, později pole).

Ale co když se potřebujeme specializovat na jinou sekvenci (nejprve pole, později funkce mapovače). To je možné, ale vyžaduje to trochu práce navíc:

var p = R.flip( R.map )( words );

p( lowercase );                 // ["now","is","the","time"]
p( uppercase );                 // ["NOW","IS","THE","TIME"]

Chceme zadat 96 nejprve vytvořte 101 který později převezme funkci mapovače. Naše specializace je s druhým argumentem namísto prvního.

Abychom toho dosáhli, musíme 112 121 funkce. 134 vytvoří obal funkce, který při předání základní funkci zamění první dva argumenty. Převrácením pořadí argumentů 144 , nyní očekává nejprve pole a jako druhé funkci mapovače.

Jinými slovy, abyste mohli pracovat se standardními metodami FP v kterékoli z knihoven FP, musíte si zapamatovat pořadí jejich argumentů - mějte tyto dokumenty po ruce! -- a pokud je to náhodou v nepohodlném pořadí, zaseknete se v tom žonglování. Při více než jedné příležitosti jsem musel přehodit metodu, předat argument, znovu to přehodit, abych předal další argument atd. Všechno to žonglování se může rychle vymknout kontrole!

Další frustrace, která vzniká z pozičních argumentů, je, když potřebujete jeden přeskočit (pravděpodobně proto, že má výchozí nastavení, ke kterému se chcete vrátit). Pro tento příklad použiji 157 :

function concatStr(s1,s2) { return s1 + s2; }

var words = ["Now","Is","The","Time"];

_.reduce( concatStr, _, words );
// NowIsTheTime

_.reduce( concatStr, "Str: ", words );
// Str: NowIsTheTime

161 funkce očekává argumenty v tomto pořadí:171 , 183 , 195 . Běžné chápání 204 v JS je to, pokud nechcete poskytnout 210 , nenastaví pouze nějakou magickou prázdnou hodnotu, ale spíše změní chování samotné operace. V podstatě zahájí redukci druhým prvkem v poli, přičemž první prvek použije jako 223; výsledkem je celkově o jedno volání funkce reduktoru méně (236 ).

Bohužel, JS nás nenechá jen vynechat argument v seznamu hovorů, jako je 240 . To by bylo fajn, ale žádné takové štěstí. Místo toho, nešikovně, musíme předat zástupný symbol. Lodash nám umožňuje používat 254 jako zástupný symbol ve výchozím nastavení, ale obecně musíte použít 263 .

Tip: existuje způsob, jak použít syntaktický trik, abyste se vyhnuli nutnosti zástupného symbolu v běžném volání funkce JS:271 . To, co děláme, je použití literálu pole, který umožňuje "ellision" (přeskočení hodnoty), a pak jej rozložíme pomocí ES6+ 287 operátor šíření. 299 zde obdrží argumenty 309 , 318 , 329 a 337 jeho první čtyři pozice parametrů. Nejsem si jistý, jestli je to skákání s obručí lepší (a může mít nějaké nevýhody!).

V každém případě žonglování s pořadím argumentů a přeskakování přeskakování argumentů na místě volání je běžnou frustrací v JS. Náhodou je to v FP docela akutní bolest, protože nakonec budete muset používat tyto metody API různými způsoby častěji než jen s běžnými funkcemi aplikace.

Řešení:Pojmenované argumenty

Některé jazyky mají syntaxi pro pojmenování argumentů na místě volání (nejen pojmenování parametrů v deklaraci funkce). Například v Objective-C:

[window addNewControlWithTitle:@"Title"
                     xPosition:20
                     yPosition:50
                         width:100
                        height:50
                    drawingNow:YES];

Zde voláte 345 funkce a sdělování systému, na který parametr by měla být každá hodnota aplikována, bez ohledu na to, v jakém pořadí mohou být uvedeny v deklaraci dané funkce.

Výhodou pojmenovaných argumentů je to, že na stránce volání máte kontrolu v jakém pořadí chcete argumenty vypsat, a také nemůžete jeden vypsat, pokud pro něj nechcete předávat hodnotu. Kompromisem je, že si musíte pamatovat, jak se parametry nazývají . Jazyky a balíčky obvykle přijmou standardizované konvence pojmenování, aby názvy parametrů byly intuitivnější a zapamatovatelné.

Dovolte mi jen říci, že to není situace buď/nebo, pokud jde o čitelnost kódu. Jsou chvíle, kdy jsou výhodnější poziční argumenty, a jasně případy, kdy jsou výhodnější pojmenované argumenty. V ideálním případě by vám jazyk umožnil vybrat si na stránce hovoru, jak si přejete.

Bohužel JS nemá pojmenované argumenty. Máme však vzorec, který nám poskytuje téměř všechny výhody pojmenovaných argumentů. Například:

function foo(x,y = 2,z) {
    console.log( x, y, z );
}

function bar({ x, y = 2, z }) {        // <--- parameter object destructuring
    console.log( x, y, z );
}

foo( 1, undefined, 3 );                // 1 2 3
bar( {z:3, x:1} );                     // 1 2 3

Poznámka: Obvykle budete chtít 357 deklarace funkce stylu vypadá takto:362 . To 375 výchozí parametr znamená 385 funkce elegantně degraduje, pokud je volána zcela bez objektu.

S 398 používáme tradiční styl pozičních argumentů, včetně prostředního (409 ) s výchozím nastavením. S 419 používáme však JS idiom pojmenovaných argumentů. Nejprve použijeme destrukci objektu parametrů v seznamu parametrů. To v podstatě znamená, že prohlašujeme, že očekáváme 427 být vždy volán s jediným objektem jako jeho argumentem. Vlastnosti tohoto objektu jsou poté destruovány, aby byly interpretovány jako skutečné jednotlivé argumenty funkce, 432 , 445 a 454; znovu 461 má také výchozí.

Stránka volání pro 477 a 488 se také liší. Pro 492 , předáme objekt s vlastnostmi místo jednotlivých hodnot s 507 jako poziční zástupný symbol. Objektový argument může vypsat vlastnosti (pojmenované argumenty) v libovolném pořadí a vynechat ty, které nechce specifikovat. Pěkné!

Přizpůsobení

Mým osobním pravidlem je, že nyní dávám přednost definici jakékoli funkce, která přebírá 3 nebo více argumentů (zvláště pokud jeden nebo více má výchozí hodnoty!) se stylem pojmenovaných argumentů. Ale to je užitečné pouze tehdy, když mám kontrolu nad deklarací funkce a mohu se rozhodnout.

Co když mám funkci jako 518 (nebo jakákoli jiná normální funkce v aplikaci!), ale chci na stránce volání používat pojmenované argumenty?

Abychom tak učinili, musíme upravit funkci stylu pozičních argumentů tak, aby byla pojmenována jako styl argumentů. Představme si k tomu takového pomocníka; budeme to nazývat 521 :

function apply(fn,props) {
    return function applied(argsObj) {
        // map properties from `argsObj` to an array,
        // in the order of property names in `props`
        var args = [], i = 0;

        for (let prop of props) {
            args[i++] = argsObj[prop];
        }

        return fn( ...args );
    };
}

Protože objekty jsou zásadně neuspořádané, předáváme 533 pole, které uvádí názvy vlastností v pořadí, v jakém je chceme mapovat na poziční argumenty základní funkce.

Nyní použijeme tento nástroj:

var map = apply( R.map, ["fn","arr"] );

map( {arr: words, fn: lowercase} );            // ["now","is","the","time"]

Dobře, docela v pohodě, co?

Bohužel výsledný 547 již není užitečně kari, takže nemůžeme skutečně využít této schopnosti žádným zajímavým způsobem. Nebylo by opravdu skvělé, kdybychom to mohli udělat:

var map = someSuperCoolAdapter( R.map, ["fn","arr"] );

var f = map( {fn: uppercase} );
f( {arr: words} );                            // ["NOW","IS","THE","TIME"]
f( {arr: moreWords} );                        // ["THE","QUICK","BROWN","FOX"]

var p = map( {arr: words} );
p( {fn: lowercase} );                         // ["now","is","the","time"]
p( {fn: uppercase} );                         // ["NOW","IS","THE","TIME"]

K tomu bychom pravděpodobně potřebovali 559 to bylo dost chytré na to, aby automaticky přecházelo přes více volání pojmenovaných argumentů. Nebudu ukazovat, jak bychom to udělali, kvůli stručnosti. Ale pro čtenáře je to zajímavé cvičení. Další vráska:existuje nějaký způsob, jak by tento adaptér mohl zjistit, jaké názvy vlastností použít ve výchozím nastavení? Je to možné, pokud analyzujete definici funkce (analýza regulárního výrazu řetězce!). Opět to nechám na prozkoumání čtenáře!

A co přizpůsobení druhým směrem? Řekněme, že máme funkci stylu pojmenovaných argumentů, ale chceme ji použít pouze jako normální funkci stylu pozičních argumentů. Potřebujeme doprovodný nástroj, který dělá opak 567; budeme to nazývat 574 :

function unapply(fn,props) {
    return function unapplied(...args) {
        // map `args` values to an object,
        // with property names from `props`
        var argsObj = {}, i = 0;

        for (let arg of args) {
            argsObj[ props[i++] ] = arg;
        }

        return fn( argsObj );
    };
}

A pomocí:

function foo({ x, y, z } = {}) {
    console.log( x, y, z );
}

var f = unapply( foo, ["x","y","z"] );

f( 1, 2, 3 );            // 1 2 3

Stejný problém je zde s kari. Ale alespoň si nyní dokážeme představit, jak vyzbrojeni těmito dvěma nástroji můžeme spolupracovat s funkcemi stylu pozičních argumentů a stylu pojmenovaných argumentů, jak uznáme za vhodné!

Připomenutí:to vše je zcela oddělené od toho, zda máme co do činění s knihovnou FP nebo ne. Tyto koncepty platí (zamýšlená slovní hříčka) s kteroukoli z vašich funkcí ve vaší aplikaci. Nyní můžete libovolně definovat funkce s oběma styly podle potřeby a na stránce volání si vybrat, jak chcete s funkcí pracovat. To je velmi silné!

Už jste knihovnu FP?

Dobrý zármutek, to byla opravdu dlouhá preambule k zdánlivě hlavnímu tématu tohoto článku, který má představit novou knihovnu FP, kterou jsem vydal. Aspoň chápeš, proč jsem to napsal. Tak a teď mi to dovolte!

Při koncipování 589 / 594 a když jsem si s nimi hrál, napadlo mě:co kdybych měl celou knihovnu FP, kde všechny metody už byly ve stylu pojmenovaných argumentů? Tato knihovna samozřejmě může také poskytovat 603 / 619 pomocníci, kteří usnadňují spolupráci. A neměla by tato knihovna pro pohodlí také exportovat všechny stejné metody (v samostatném jmenném prostoru) pomocí standardního stylu pozičních argumentů? Konečná volba v jedné knihovně FP, že!?

To je to, o čem FPO (vyslovuje se "eff-poh") je. FPO je knihovna JS pro FP, ale všechny její základní metody jsou definovány ve stylu pojmenovaných argumentů. Jak je běžné u knihoven FP, všechny metody jsou také curry, takže můžete poskytnout argumenty v jakémkoli pořadí a pořadí, jaké potřebujete! A 625 má všechny metody stylu pozičních argumentů, pokud je chcete.

Chcete přejít přímo k dokumentům?

  • Core API -- metody stylu pojmenovaných argumentů (639 , atd.)

  • Standardní API – standardní metody stylu pozičních argumentů (646 , atd). Tyto většinou fungují jako jejich protějšky Ramda.

Rychlé příklady

// Note: these functions now expect named-arguments style calls
function lowercase({ v } = {}) { return v.toLowerCase(); }
function uppercase({ v } = {}) { return v.toUpperCase(); }

var f = FPO.map( {fn: uppercase} );
f( {arr: words} );                            // ["NOW","IS","THE","TIME"]
f( {arr: moreWords} );                        // ["THE","QUICK","BROWN","FOX"]

var p = FPO.map( {arr: words} );
p( {fn: lowercase} );                         // ["now","is","the","time"]
p( {fn: uppercase} );                         // ["NOW","IS","THE","TIME"]

657 je stylem pojmenovaných argumentů a je již kari. Velmi snadné použití, jak chcete!

Jak si všimnete, očekává, že jeho funkce mapování bude také následovat styl pojmenovaných argumentů. Pokud místo toho chcete předat funkci mapovače standardního stylu, stačí 662 to první:

function firstChar(v) { return v[0]; }

var f = FPO.apply( {fn: firstChar} );          // <-- auto detects `props`!
FPO.map( {fn: f, arr: words} );                // ["N","I","T","T"]

Aplikaci a kari lze také snadno smíchat ve vašem vlastním kódu:

function foo(x,y,z) {
    console.log( x, y, z );
}

var f = FPO.apply( {fn: foo} );
var g = FPO.curry( {fn: f, n: 3} );

g( {y: 2} )( {x: 1} )( {z: 3} );               // curried named-arguments!
// 1 2 3

Zrušení aplikace funguje podobně:

function foo({x, y = 2, z} = {}) {
    console.log( x, y, z );
}

var f = FPO.unapply( {fn: foo, props: ["x","y","z"]} );

f( 1, undefined, 3 );
// 1 2 3

Ale nezapomeňte na snadné přeskakování pojmenovaných argumentů pro výchozí hodnoty:

function foo(x,y = 2,z) {
    console.log( x, y, z );
}

var g = FPO.curry( {
    fn: FPO.apply( {fn: foo} ),
    n: 2    // use `2` here for currying-count to allow skipping
} );

g( {z: 3} )( {x: 1} );
// 1 2 3

Složení funkcí stylu pojmenovaných argumentů funguje také:

function plus2({ v } = {}) { return v + 2; }
function triple({ v } = {}) { return v * 3; }
function decrement({ v } = {}) { return v - 1; }

FPO.map( {
    arr: [1,2,3,4,5],
    fn: FPO.compose( {fns: [
        decrement,
        triple,
        plus2
    ]} )
} );
// [8,11,14,17,20]

FPO.map( {
    arr: [1,2,3,4,5],
    fn: FPO.pipe( {fns: [
        plus2,
        triple,
        decrement
    ]} )
} );
// [8,11,14,17,20]

A konečně, standardní metody stylu pozičního argumentu jsou stále dostupné, pokud je chcete:

function concatStr(s1,s2) { return s1 + s2; }

FPO.std.reduce( concatStr, undefined, words );
// NowIsTheTime

Poznámka: BTW, pokud vás nebaví psát 677 nebo 689 před všemi svými metodami jednoduše přiřaďte tyto objekty k čemukoli, co preferujete, například 698 . Nakonec bude FPO dokonce podporovat importy stylu modulů ES6, kde budete moci importovat pouze metody, které chcete, do svého vlastního lexikálního rozsahu!

To je rychlý přehled toho, co můžete dělat s FPO. Podívejte se na přehled README a API Docs, kde najdete další informace!

Konvence pojmenovávání parametrů

FPO má poměrně přímočarý přístup ke konvencím pojmenovávání parametrů, které by mělo být rozumné intuitivně se naučit. Letmý pohled:

  • Když metoda očekává funkci, pojmenovaný argument je 701 .
  • Když metoda očekává číslo, pojmenovaný argument je 711 .
  • Když metoda očekává hodnotu, pojmenovaný argument je 725 .
  • ...

Úplný seznam pravidel je uveden zde.

739 ing Up

OK, to je FPO.

Nesnažím se konkurovat knihovnám jako Ramda nebo lodash/fp. Jsou skvělí. Jen jsem chtěl poskytnout určitou flexibilitu. A v mém dosavadním kódování FP zjišťuji, že kompromisy a flexibilita jsou příjemným vylepšením!

Doufám, že FPO považujete za užitečné! Pokud máte návrhy nebo dotazy, dejte mi vědět v komentářích nebo se ozvěte k problémům s úložištěm.