Přejít k navigační liště

Zdroják » JavaScript » Javascriptaření: hrajte si s funkcemi!

Javascriptaření: hrajte si s funkcemi!

Články JavaScript, Různé

Funkcionální programování si častěji spojujeme s Lispem, Haskellem či F# než s něčím, co by se odehrávalo na webu. A přitom funkcionální jazyk má každý webař po ruce… Ukážeme si tento opomíjený rys JavaScriptu na příkladech, které budou lispařům určitě důvěrně známé. Vítejte do světa javascriptaření!

Javascriptaření. Činnost, které se věnují mnozí weboví vývojáři taknějak samovolně, na půl plynu, bokem, protože jsou přesvědčení, že JavaScript je vlastně jen zmatené céčko, co slouží pro práci s DOMem a pro obsluhu událostí. Mnozí věří, že vrcholu dokonalosti dosáhl JavaScript v okamžiku, kdy vznikla knihovna jQuery, a tím že se možnosti tohoto jazyka vyčerpaly. Tak, je pravda, že naprostá většina webových stránek toho o moc víc nepotřebuje, ale to neznamená, že JavaScript toho víc neumí. V seriálu Javascriptaření si budeme postupně ukazovat metody, postupy a knihovny, které možná nevyužijete hned, ale je dobré o nich vědět.

Dnes začneme funkcionálním programováním a funkcemi vyšších řádů (Higher Order Functions).

Inspiraci k článku a k úvodním příkladům poskytl Piers D. Cawley svým skvělým textem Higher Order JavaScript, za což mu patří velký dík, stejně jako za nápad ukázat tyto možnosti na příkladech zapsaných v CoffeeScriptu. 

Na úvod stručná definice: Higher order funkce jsou takové, které:

  1. přebírají funkci (funkce) jako argument
  2. vrací funkci jako výsledek
  3. dělají obojí

Funkce ad 1 jsou v JavaScriptu důvěrně známé – používají se často při asynchronních událostech jako tzv. „callback“ funkce. Funkce ad 2 a 3 slouží jako generátory funkcí.

Mějte prosím na paměti, že příklady jsou demonstrační; pro ilustraci postupů používají velmi jednoduché problémy, které byste v praxi řešili určitě jinak, přímočařeji. Ukázky nejsou celý kód! 

Trocha teorie – Map, Reduce a další

V příštím díle Javascriptaření si představíme knihovnu Underscore, jejíž podstatnou částí jsou právě tyto funkce pro práci se seznamy. Budeme mít tedy drobný náskok.

Map

Funkce map() má dva argumenty. Jedním argumentem je pole, druhým funkce:

var ys = map(xs, fn)

Výsledkem funkce map() je opět pole, které má stejný počet položek jako argument xs. Funkce vezme každý prvek pole xs, jeden po druhém, předá ho funkci fn() a výsledek této funkce vloží do výstupního pole. Interpretery ECMAScript5 mají nativní implementaci zabudovanou, ale nám se teď nejedná o konkrétní realizaci, ale o algoritmus. Naivní implementace funkce map() může vypadat třeba takto:

var map = function(xs, fn) {
    var rs = [];
    if (xs === null) {return rs;}
    for (var i=0; i<xs.length; i++) {
      rs[rs.length] = fn(xs[i]);
    };
    return rs;
  };

S drobným testem (test map()):

var a = [1, 2, 3, 4, 5, 6];
var y = map(a, function(i){return i*2;});

Reduce / foldl

Funkce reduce(), zvaná též foldl(), má tři parametry: Pole, funkci a počáteční hodnotu (pořadí se liší dle zvyklostí v tom kterém jazyce; budeme se držet konvence knihovny Underscore, kterou si představíme příště).

var y = foldl(xs, fn, init)

Název foldl (Fold Left) odkazuje na skládání (fold). S trochou fantazie můžete vidět pole jako „traktorový“ papír do staré tiskárny – pás s naznačeným přehybem. Vezmete první list, v přehybu k němu přiložíte druhý list, přehyb, třetí list, přehyb, čtvrtý list… a postupně skládáte sloupeček listů. Stejně tak vezme funkce foldl() počáteční hodnotu a první položku pole xs a předá je funkci fn(). Výsledek vezme a spolu s druhou položkou jej předá opět funkci fn(). Výsledek vezme a s třetí položkou je předá funkci fn()… atakdále, dokud nedojde na konec pole. Poslední výsledek pak vrátí.

K funkci foldl() existuje alternativa foldr() (alternativně reduceRight()), která postupuje při skládání v opačném směru, tedy od konce (zprava).

Pomocí foldl() můžeme snadno nadefinovat třeba funkci sum(), která sečte všechny prvky pole:

var sum = function(xs) {
    return foldl(xs,
                 function(a,b) {
                     return a+b;
                 },
                 0);
};

Analogicky vytvoříme funkci prod(), která vrátí výsledek vzniklý vzájemným vynásobením prvků:

var prod = function(xs) {
    return foldl(xs,
                 function(a,b) {
                     return a*b;
                 },
                 1);
};

A opět ukázka: foldl, sum a prod.

Samotná implementace foldl může vypadat např. takto (opět upozorňuji, že ECMAScript5 má tuto funkci zabudovanou):

var foldl = function(xs, fn, init) {
    var r = init;
    if (xs === null) {return r;}
    for (var i=0; i<xs.length; i++) {
      r = fn(r, xs[i]);
    };
    return r;
  };

Pauza na kávu

V článcích o CoffeeScriptu jsme si slibovali, že se v budoucnu podíváme na příklady, které ukáží výhodu syntaxe CoffeeScriptu. Ta chvíle právě nastala. Podívejme se na zápis výše uvedených funkcí v tomto jazyce:

var foldl = (xs, fn, init) ->
   r = init
   (return init) if xs is null
   (r = fn(r,x)) for x in xs
   r


var sum = (xs) -> foldl xs, ((a,b) -> a+b), 0
var prod = (xs) -> foldl xs, ((a,b) -> a*b), 1

V dalším textu sáhneme k CoffeeScriptu vždy, když by se algoritmus zapsaný v JavaScriptu topil v závorkách.

Pomocí foldl() můžeme definovat i funkci map() nebo iterační funkci  each():

var map = (xs, fn) -> foldl xs, ((rs, x) -> rs.push fn x; rs), []
var each = (xs, fn) -> foldl xs, ((_, x) -> fn x; _), null

Odpovídající podoba v JavaScriptu (i s testem map a each):

var map = function (xs, fn) {
    return foldl(xs,
                 function(rs,x) {
                     rs.push(fn(x));
                     return rs;
                 },
                 []);
};

var each = function (xs, fn) {
    return foldl(xs,
                 function(_,x) {
                     fn(x);
                     return _;
                 },
                 null);
};

Továrna na funkce

Všimli jste si v předchozích příkladech toho opakování? Všechny případy, kdy jsme používali foldl(), měly stejný tvar:

var blabla = function(xs, ...) {return foldl(xs, .....);}

V PHP (do verze 5.3, díky za upřesnění) a podobných jazycích, které nemají first-class funkce, bychom tímto konstatováním skončili. JavaScript je ale má, takže jsme v něm schopní vytvořit továrnu na funkce tohoto typu. Můžeme zapisovat:

var blabla = foldlize (...)
var sum = foldlize (function(a,b){return a+b;}, 0);

kde foldlize() je funkce, která vrátí funkci s jedním argumentem, v níž se projde pole výše uvedeným způsobem. Můžeme si ji napsat:

foldlize = (fn, init) ->
   (xs) -> foldl xs, fn, init

JavaScriptová podoba:

var foldlize = function(fn, init) {
  return function(xs) {
    return foldl(xs, fn, init);
  };
};

Foldlize nám vygeneruje funkci pro práci s polem pomocí foldl(). Ale kde je radost z programování?

var __slice = Array.prototype.slice;
var with_array = function(f) {
  var arity;
  arity = f.length;
  return function() {
    var args;
    args = 1 <= arguments.length ? __slice.call(arguments,0) : [];
    if (arity > 0 && args.length !== arity - 1) {
      throw "Error!";
    }
    return function(xs) {
      return f.apply(null, [xs].concat(__slice.call(args)));
    };
  };
};

var foldlize = with_array(foldl);

Co se to tu děje? Funkce with_array() má jako parametr funkci f, která má n argumentů. Vrací funkci s n-1 argumenty (args…), která vrací funkci, jež má jediný argument (xs) a volá funkci f, které předá svůj argument xs a argumenty args. Funkce tedy generuje kód, který generuje kód.

Druhý šálek kávy

Nastal čas na druhé CoffeeScriptové intermezzo. Ukážeme si funkci with_array zapsanou v CoffeeScriptu:

with_array = (f) ->
  arity = f.length
  (args...) ->
    if arity > 0 && args.length != arity - 1
      throw "Error!"
    (xs) -> f xs, args...

foldlize = with_array foldl

Proto CoffeeScript: namísto plavání v syntaxtickém balastu, závorkách, střednících a function() se programátor soustředí na problém. Vytváří krátké funkce, které se jednoduše ladí i testují, a nemusí přemýšlet, jestli všechno správně uzávorkoval.

Konec intermezza…

Pomocí with_array() tak můžeme generovat generátory funkcí, jako je právě foldlize, pomocí nichž můžeme generovat funkce pro práci s polem. Ještě jinak řečeno – jakoukoli funkci, která má jako první argument pole, můžeme s with_array  přeměnit na generátor odvozených funkcí.

Mapper

Pomocí funkce map() lze řešit velké množství příbuzných úloh, kde na vstupu i na výstupu jsou pole stejné velikosti a pro každý prvek se provádí operace nezávisle na ostatních. Například zjištění, zda je prvek – číslo sudý, nebo lichý:

var arreven = function (xs) {
   return map (xs, (function (a) {
      return (a%2)==0;
   }));
}

//CoffeeScript:
//arreven = (xs) -> map xs, ((a)->(a%2 == 0))

Výsledné pole bude obsahovat jen hodnoty false a true. Obdobně můžeme pro pole řetězců zjistit jejich délku:

var arrslen = function(xs) {
  return map(xs, (function(s) {
    return s.length;
  }));
};

//CoffeeScript:
//arrslen = (xs) -> map xs, ((s) -> s.length)

Netřeba v tuto chvíli vymýšlet další příklady, princip je jasný. Opět je vidět, že funkce jsou vždy speciální aplikací funkce map(), vždy s jinou iterační funkcí. Připomeneme si výše napsané: jakoukoli funkci, která má jako první argument pole, můžeme s with_array přeměnit na generátor odvozených funkcí. Můžeme tedy vytvořit funkci mapper()  – právě pomocí with_array, které jako parametr předáme patřičnou iterační funkci, a nechat starosti s psaním stále stejného kódu… tedy jinému kódu.

mapper = with_array(map);

S mapperem můžeme výše uvedené funkce snadno přepsat:

var arreven = mapper(function(x) {
  return x % 2 === 0;
});
var arrslen = mapper(function(s) {
  return s.length;
});
var arrinvert = mapper(function(x) {
  return !x;
});

//CoffeeScript
//arreven = mapper (x) -> x%2 == 0
//arrslen = mapper (s) -> s.length
//arrinvert = mapper (x) -> !x

(Alespoň trochu) praktické použití

Běžný JavaScriptař si teď pravděpodobně říká: To je všechno hezké, map i foldl i další funkce někdy můžu použít, ale normálně pro takové to každodenní matlání JS na web to asi nebude…

Mno, jak se to vezme… Dejme si příklad, že si někdo (a nebudeme konkrétně jmenovat, ať je to zákazník nebo designér nebo projektový vedoucí) vymyslel, že na nějaké stránce budou dvě pole pro zadání čísla a u nich tlačítka +1 a +5, to jako aby se dala měnit hodnota klikáním. Nějak takto:

<input id="p1" value="0">
<button id="p11">+1</button>
<button id="p15">+5</button>
<br>
<input id="p2" value="0">
<button id="p21">+1</button>
<button id="p25">+5</button>

K tomu, aby to fungovalo, je zapotřebí ještě nějaký kód (předpokládáme použití jQuery). První naivní implementace bude vypadat třeba takto (neřešíme výjimečné stavy, např. NaN – pro naši ukázku to není podstatné):

$("#p11").click(function(){
    $("#p1").val(parseInt($("#p1").val())+1);
});
$("#p15").click(function(){
    $("#p1").val(parseInt($("#p1").val())+5);
});
$("#p21").click(function(){
    $("#p2").val(parseInt($("#p2").val())+1);
});
$("#p25").click(function(){
     $("#p2").val(parseInt($("#p2").val())+5);
 });

Každý něco takového stokrát viděl, dvěstěkrát psal, třistakrát opravoval. I nepříliš zkušený skriptař vidí na první pohled problém: opakuje se stále totéž dokola a i při programování metodou copy-and-paste se člověk snadno uklepne.

Aplikujeme princip DRY a přičítání hodnoty k hodnotě v políčku vytkneme jako funkci:

var chg = function(id, value) {
    $(id).val(parseInt($(id).val())+value);
}

$("#p11").click(function(){
    chg("#p1",1);
});
$("#p15").click(function(){
    chg("#p1",5);
});
$("#p21").click(function(){
    chg("#p2",1);
});
$("#p25").click(function(){
     chg("#p2",5);
 });

… a v tomto stavu, věřte nebo ne, mnozí skriptaři (a bude jich možná i většina) prohlásí kód za dostatečně optimalizovaný, právě proto, že nejsou s higher order kamarádi a JavaScript berou spíš jako objektový a procedurální. Znalí funkcionálního programování použijí funkci vyššího řádu, kterou si vygenerují příslušný callback rovnou jako funkci, takže jej nebudou muset stále balit do function(){…}:

var chgen = function(id, value) {
    return function(){
        $(id).val(parseInt($(id).val())+value);
    };
}

$("#p11").click(chgen("#p1",1));
$("#p15").click(chgen("#p1",5));
$("#p21").click(chgen("#p2",1));
$("#p25").click(chgen("#p2",5));

Pak přijde někdo a vzpomene si, že je potřeba (čert ví proč) mít i tlačítko na vynásobení hodnoty dvěma:

<input id="p1" value="0">
<button id="p11">+1</button>
<button id="p15">+5</button>
<button id="p1t2">*2</button>
<br>
<input id="p2" value="0">
<button id="p21">+1</button>
<button id="p25">+5</button>
<button id="p2t2">*2</button>

Rutinér pokrčí rameny, přejmenuje si „chgen“ (Change Generator) na „cbplus“ (jako že callback-plus) a naklonuje ji jako „cbtimes“, kde zamění plus za krát. Copy-paste-programming. Obsluha bude přímočará:

$("#p11").click(cbplus("#p1",1));
$("#p15").click(cbplus("#p1",5));
$("#p1t2").click(cbtimes("#p1",2));
$("#p21").click(cbplus("#p2",1));
$("#p25").click(cbplus("#p2",5));
$("#p2t2").click(cbtimes("#p2",2));

Ovšem ostřílenému veteránovi je jasné, že odpoledne přijde tentýž někdo s požadavkem na tlačítko ^2, které udělá z čísla druhou mocninu, a když už bude všechno připravené, tak někomu konečně dojde, že bude potřebovat i tlačítko na vynulování a nastavení na hodnotu 100. Ostřílený veterán se proto připraví:

var gencb = function (fn) {
    return function(id, value){
        return function() {
            $(id).val(fn(parseInt($(id).val()),value));
        };
    };
};

var cbplus = gencb(function(a,b) {return a+b;});
var cbtimes = gencb(function(a,b) {return a*b;});
var cbpow = gencb(function(a,b) {return a*a;});
var cbset = gencb(function(a,b) {return b;});

Proč?

Protože funkce gencb je jednoduchá – i když možná nevypadá, obzvlášť pak když si ji zapíšete jako  gencb = function (fn) {return function(id, value) {return function() {$(id).val(fn(parseInt($(id).val()),value));};};}; (Pokud ji takto opravdu napíšete, tak vás kolegové od srdce proklejí.)

Jednou napsaná a otestovaná ale slouží jako generátor spolehlivých funkcí, funkcí, v nichž se (na rozdíl od přepisování zkopírovaného kódu) neuklepneme, funkcí, které lze opět velmi dobře otestovat a kterým lze věřit. Nehledě na to, že už při počtu nějakých pěti vygenerovaných funkcí bude náš kód kratší než copypastovaný.

Dobře napsaný a otestovaný generátor podobných funkcí nás zbaví nutnosti udržovat iks vzájemně si podobných funkcí v konzistentní podobě, dovoluje nám rychle změnit formát všech funkcí (co když budeme chtít v callback funkcích zpracovat předaný event?) a především pak při psaní podobných funkcí umožňuje soustředit se na to podstatné. Takže v definici funkce cbplus řešíme pouze operaci, kterou má callback vykonat, a režijní kód za nás dodá generátor.

Proč CoffeeScript?

Protože:

gencb = (fn) ->
  (id, value)->
    () -> $(id).val(fn(parseInt($(id).val()),value));

genplus = gencb (a,b) -> a+b
genkrat = gencb (a,b) -> a*b
genset = gencb (_,b) -> b

Závěr

Funkcionální podstata JavaScriptu bývá velmi často opomíjena; většina skriptů používá spíš objektový a procedurální přístup a s higher order funkcemi pracují pouze u callbacků. I někteří programátoři, znalí funkcionálního programování, tento přístup v JavaScriptu nepoužívají – jak autorovi řekl jeden z nich: „ani mě to nenapadlo, protože ten jazyk k podobným věcem moc nevybízí…“

V článku jsme si připomněli funkcionální prvky, které v JavaScriptu existují, a které mohou leckdy usnadnit a urychlit práci, pokud jsou použity s rozvahou. Informace však jistě využijí i vývojáři, používající jiné dialekty ECMAScriptu (ActionScript) či jiných jazyků s funkcionální­mi prvky.

A nezapomeňte: Podobné techniky jsou jako koření – pokud to s nimi přeženete, zničíte celý výsledek, ale když je použijete přiměřeně, dokáží udělat divy!

Ke studiu

Komentáře

Subscribe
Upozornit na
guest
20 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
juraj

Dúfam, že v druhej časti seriálu uvidíme použitie bind(), ešte niekto začne takéto vnáranie xy fcií do seba bežne používať :-)
Mimochodom, vážne v kóde všade priradzujete do neinicializovaných premenných (chýba var)? Fuj.

David Grudl

Tož tedy nevím. Článek o Coffeescriptu nazvat „JavaScriptaření“ je docela zavádějící, oba jazyky jsou naprosto jiná liga, navíc ukázky v jednotlivých jazycích jsou odlišné, ať už názvy identifikátorů (r versus result) nebo funčností (test na null ve foldl). Ukázky v JavaScriptu obsahují chyby (__split, závorka v arrsuda), názvy arrsuda a cbkrat mě už jen dorazili, smrdí to Pecinovským :-(

Sorry za negativní koment a stručnost (čtu a píšu v mobilu), ale víš jak ;)

Ped

Uz duch samotneho clanku mi znel trochu jako „je jedno jak se to bude cist, hlavne at se pri psani neuzavorkuju k smrti“ (mne osobne se treba uzavorkovany kod na vice radku cte docela dobre, nerikam ze cofeescript verze se mi nelibi, ale bez spetky zvyku se mi cte o neco hur (co je dobre znameni, obvykle se mi nove veci ctou MNOHEM hur)) (a vubec se zavorkami mam malo problemu (jak je mozna znat uz z tohohle prispevku)).

Rikal jsem si ze to mam asi jen takovou naladu po ranu a autor to tak nemyslel.

Ale tahle reakce na „nelibi nazvy“ mi ukazuje ze autor nejspis nepochopil jednu ze zakladnich vlastnosti dobreho kodu, a to ze dobry kod se dobre cte, i za cenu toho ze se spatne pise. Protoze kod se pise cca. jednou, ale cte se mnohokrat.
„arrsude“ je cechoanglikopra­sizmus ktery dela cteni kodu slozitejsim a tudiz celou ukazku shazuje minimalne o tridu dolu. A stacilo by tak malo, treba alespon „arrayeven“ nebo „arr_even“. Nebo pripadne cely nazev cesky (to se mne osobne nebude libit uplne stejne jako arrsude, ale to uz je o mych preferencich).

Ped

To se pak omlouvam za trochu ostrejsi reakci, clanek jsem cetl v jeho puvodni podobe kde mi nektere nazvy trochu zvedali oboci, ale puvodne jsem to ani neminil komentovat, ale ta diskuze byla tou povestnou kapkou… :)

Rum

Je nějaký rozdíl mezi funkce = function() { a function funkce() {

fos4

function funkce():
vytvori funkci s nazvem „funkce“ (function declaration) a automaticky priradi do promene stejneho jmena

funkce = function():
priradi do promene „funkce“ annonymni funkci (function expression)

BurgetR

Moc pěkný článek. Líbí se mi, že konečně někdo řeší opravdu programování jako takové. Zvlášť ve webařských kruzích už dnes není moc zvykem se zabývat tím, jestli je něco napsáno efektivně nebo dokonce pěkně. BTW – já pořád říkám, že JavaScript zůstává většinou svých současníku nedoceněn :-)

paranoiq

jen pro upřesnění. PHP má first-class funkce. funkci lze předat jako argument, lze ji vracet jako výsledek funkce. lze ji obdržet jako argument, zkonstruovat z ní jinou funkci a tuto vrátit atd.

_____
poznámka na později: napsat ukázku prototypové dědičnosti v PHP :P

Richard Šerý

Funkcionální programování v JavaScriptu patří k základům, které by měl umět každý, kdo s tímto jazykem přijde do styku. Na druhou stranu, v praxi není vše tak krásné, jak by se z článku mohlo zdát.

Funkce si s sebou nese svůj kontext, což za určitých podmínek může vést ke kruhovým referencím a memory leakům. Čím více „vnořených“ funkcí, tím větší riziko hrozí, protože na větších projektech se skoro vždycky najde někdo, kdo si nedá pozor. Občas radši udělám méně úsporný zápis, než riskovat, že při příští změně tam někdo zanese kruhovou referenci.

S funkcemi vyšších řádů často prudce klesá srozumitelnost. Každý programátor se musí vždy znovu rozhodovat, jestli raději DRY nebo srozumitelný kód. Bohužel, programátoři mají tendence volit DRY, což vede ke špagetoidnímu, často paradoxně těžko udržovatelnému kódu.

Takže poslední věta článku je rozhodně na místě, a varování by mělo být velkým, ohnivě planoucím písmem.

Steida

„Funkce si s sebou nese svůj kontext, což za určitých podmínek může vést ke kruhovým referencím a memory leakům.“

O nic vážného nejde, všechny dnešní js frameworky deregistrují listenery. Ad kruhové referencé a memory leaky.., takový kód jistě ano:

foo(function() {});

Takový kód ne:

foo(xyz.bind(so­mething.bla, this));

Bind binduje this/that, přesto nevytváří closure otisk, to je cajk.

Richard Šerý

Delegování je sice chvályhodná praktika, ale jsou situace v kterých nejde použít – například když event „nebublá“ nebo když ho zastaví někdo na vyšším elementu. Memory leaky nemůže řešit framework, to musí programátor. To je jako říct, že moderní bagry jsou tak dobré, že nepotřebují bagristu…

Ales Hakl

Ony ty kruhove reference by nemeli nicemu vadit (a ciste v JS taky nicemu nevadi).

Vadi to, ze treba v takovem Gecku ma XPCOM (C++ vrstva) a JS vlastni memory managery ktere o sobe tak uplne nevedi (a navic XPCOM pouziva jenom pocitani referenci) a kruhovou referenci ktera jde pres oboji JS GC proste nevidi. Jak to resi ostatni browsery netusim, ale cekam ze to bude povetsinou dost obdobne (jak by to bylo implementacne cele jednodussi, kdyby se pouzival GC i v tom nativnim kodu…).

blizzboz

Vracanie funkcií a predávanie funkcií ako parametrov, vytváranie kolekcií funkcií atď, umožňuje skoro každý programovací jazyk. funkcionálne programovanie na takej úrovni ako JS umožňoval už aj starý TurboPascal.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.