Hlavní navigace

Úvod do jazyka Reason: rekurze

Radek Miček

Na začátku dnešního článku se naučíme psát rekurzivní funkce. Poté si ukážeme, jak lze v Reasonu reprezentovat seznamy čísel a jak s těmito seznamy pracovat pomocí rekurzivních funkcí.

Doba čtení: 4 minuty

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, 782:

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.

Našli jste v článku chybu?