Rekurzivní funkce
Na rozdíl od mnohých programovacíh jazyků standardně v Reasonu nejde používat definované hodnoty (kam spadají i funkce) uvnitř své vlastní definice. Důsledkem je, že standardně nejde definovat rekurzivní funkci:
let repeat = (action, n) =>
if (n > 0) {
action();
repeat(action, n - 1)
};
Překladač si u rekurzivního volání repeat(action, n - 1) bude stěžovat, že nezná hodnotu repeat:
The value repeat can't be found
Příjemným důsledkem tohoto chování v praxi je, že nemusíme vymýšlet nové názvy proměnných:
let x = 7 /* computeSecretNumber() */;
let x =
switch x {
| 0 | 1 => 42
| x => x / 2
};
x ve switch x je první proměnná x, která má hodnotu 7. Kdyby definované hodnoty byly standardně dostupné uvnitř své definice, tak by x ve switch x byla proměnná, kterou právě definujeme, a k první proměnné, která je již zadefinována, by se nešlo dostat – museli bychom ji pojmenovat jinak např. x' nebo xTmp. Pak ovšem hrozí, že x' nebo xTmp omylem použijeme v dalším kódu místo x.
Potřebujeme-li definovat rekurzivní funkci, za let přidáme rec:
let rec repeat = (action, n) =>
if (n > 0) {
action();
repeat(action, n - 1)
};
Pokud chceme definovat vzájemně rekurzivní funkce, je třeba je definovat naráz (není možné volat funkci, která ještě nebyla definována):
let rec even = (x) => x == 0 || odd (x - 1) and odd = (x) => x != 0 && even (x - 1);
První funkce je uvozena let rec, další funkce jsou uvozené and, středník je až za poslední funkcí.
Seznamy
Rekurze je velmi užitečná při zpracování rekurzivních datových struktur, například seznamů:
type intList = | Nil | Cons(int, intList);
Definice typu intList říká, že seznam čísel může mít dva tvary. Buď je to prázdný seznam čísel Nil, nebo je to neprázdný seznam čísel z Cons truovaný z čísla a seznamu čísel.
Seznam s číslem 82 vytvoříme tak, že na začátek prázdného seznamu Nil přidáme číslo 82: Cons(82, Nil). Seznam s čísly 7 a 82 vytvoříme tak, že na začátek seznamu s číslem 82 přidáme číslo 7:
let listWithTwoElements = Cons(7, Cons(82, Nil));
Následuje seznam s čísly 43, 7, 82:
let listWithThreeElements = Cons(43, listWithTwoElements);
Jelikož jsou seznamy čísel neměnné, můžeme bez obav využít seznam listWithTwoElements při definici listWithThreeElements.
Napišme funkci, která sečte čísla v seznamu:
let rec sum = (list) =>
switch list {
| Nil => 0
| Cons(num, nums) => num + sum(nums)
};
Prvku na začátku seznamu se říká hlava (anglicky head) a seznamu, který zbyde po utržení hlavy, se říká tělo (anglicky tail). Ve funkci sum je num hlava a nums tělo. Název proměnné pro tělo často volíme jako množné číslo názvu proměnné pro hlavu, například: Cons(item, items) nebo Cons(x, xs).
Funkce fold
Co se stane, když konstruktory Cons a Nil nahradíme funkcemi? Konkrétně mějme čtyřprvkový seznam
Cons(5, Cons(4, Cons(3, Cons(2, Nil))));
a funkce
let nilS = 0; let consS = (i, j) => i + j;
Nahrazením dostaneme
consS(5, consS(4, consS(3, consS(2, nilS))));
což je po inlinování funkcí ekvivalentní
5 + 4 + 3 + 2 + 0
Shrnuto: Nahrazením konstruktorů se nám podařilo sečíst čísla v seznamu. Když bychom konstruktory nahradili funkcemi
let nilL = 0; let consL = (_, j) => 1 + j;
získáme počet prvků v seznamu. Funkce, která nahrazuje konstruktory, se obykle nazývá fold, v případě seznamů často foldRight. Řada užitečných funkcí jde zapsat pomocí funkce fold:
let rec fold = (cons, nil, list) =>
switch list {
| Cons(num, nums) => cons(num, fold(cons, nil, nums))
| Nil => nil
};
let length = fold((_, len) => len + 1, 0);
let sum = fold((+), 0);
let map = (f) =>
fold((num, mappedNums) => Cons(f(num), mappedNums), Nil);
let filter = (cond) =>
fold((num, filteredNums) => cond(num) ? Cons(num, filteredNums) : filteredNums, Nil);
let existsPositive =
fold((num, positiveNumberInTail) => num > 0 || positiveNumberInTail, false);
map na každé číslo v seznamu aplikuje funkci f a vrátí nový seznam čísel. filter ponechá v seznamu pouze čísla, která splňují podmínku cond. A existsPositive otestuje, zda seznam obsahuje kladné číslo. Nevýhodou existsPositive je, že neskončí po nalezení prvního kladného čísla, ale zbytečně bude testovat i všechna čísla po něm. Nejedná se tedy o ekvivalent
cislo1 > 0 || cislo1 > 0 || cislo2 > 0 || ...
Vtip je v tom, že funkce (||) nevyhodnocuje druhý argument, pokud je první argument true. My jsme však (||) zabalili do naší vlastní funkce (num, positiveNumberInTail) => num > 0 || positiveNumberInTail, která toto chování nesdílí.
Seznamy ze standardní knihovny
Pro zápis seznamů má Reason speciální syntaktickou podporu. Místo
Cons(5, Cons(4, Cons(3, Cons(2, Nil))))
píšeme
[5, 4, 3, 2]
čímž dostaneme seznam typu list(int). Pattern matching provádíme následovně
let rec sum = (list) =>
switch list {
| [] => 0
| [num, ...nums] => num + sum(nums)
};
Seznamy v Reasonu jsou něco úplně jiného něž pole v JavaScriptu (ačkoliv jejich syntax je stejná):
- Seznamy v Reasonu jsou neměnné.
- Všechny prvky seznamu v Reasonu musí mít stejný typ (jedná se o tzv. homogenní seznamy).
- Časová složitost operací se liší.
Příště
Ukázali jsme si, jak psát rekurzivní funkce a začali jsme se věnovat seznamům. Příště si povíme o tail-rekurzi, o modulu List ze standardní knihovny a funkcích v tomto modulu, které není vhodné používat. Nakonec se dostaneme i k zajímavějším strukturám, než jsou seznamy.