Javascriptaření: hrajte si s funkcemi!

Javascriptaření

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í!

Seriál: Javascriptaření (9 dílů)

  1. Javascriptaření: hrajte si s funkcemi! 31.1.2011
  2. ECMAScript Strict mode ve Firefoxu 4 8.2.2011
  3. Javascriptaření: nejen jQuery živ je JavaScriptař 8.3.2011
  4. Javascriptaření: fyzika, grafika a společenská konverzace 23.3.2011
  5. JavaScriptaření: drátujeme, překládáme, spojujeme 31.3.2011
  6. Javascriptaření: ukažte mi, označte mě, opravte mě 13.4.2011
  7. Kontrola JavaScriptu s JSLint a JSHint 14.7.2011
  8. Základní vzory pro vytváření jmenných prostorů v JavaScriptu 10.10.2011
  9. Javascriptaření: překladače, pakovače 9.11.2011

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

Začal programovat v roce 1984 s programovatelnou kalkulačkou. Pokračoval k BASICu, assembleru Z80, Forthu, Pascalu, Céčku, dalším assemblerům, před časem v PHP a teď je rád, že neprogramuje…

Čtení na léto

Jaké knihy z oboru plánujete přečíst během léta? Pochlubte se ostatním ve čtenářské skupině Zdrojak.cz na Goodreads.com.

Komentáře: 20

Přehled komentářů

juraj fcie ad 2
Martin Malý Re: fcie ad 2
David Grudl No tedy...
Martin Malý Re: No tedy...
Ped Re: No tedy...
Martin Malý Re: No tedy...
Ped Re: No tedy...
Martin Malý Re: No tedy...
Martin Malý Re: No tedy...
Rum funkce = function() {... vs function funkce() {...
fos4 Re: funkce = function() {... vs function funkce() {...
BurgetR Umění programování
paranoiq PHP má first-class funkce
Martin Malý Re: PHP má first-class funkce
Richard Šerý Má to i své nevýhody
Steida Re: Má to i své nevýhody
Richard Šerý Re: Má to i své nevýhody
Ales Hakl Re: Má to i své nevýhody
blizzboz Re: Javascriptaření: hrajte si s funkcemi!
Martin Malý Re: Javascriptaření: hrajte si s funkcemi!
Zdroj: http://www.zdrojak.cz/?p=3419