Úvod do jazyka Reason: funktory

6. 3. 2018
Doba čtení: 5 minut

Sdílet

V dnešním díle se budeme zabývat funktory, jejichž pochopení nám umožní používat další datové struktury ze standardní knihovny. Tento díl rovněž ukazuje, jak implementovat vlastní datovou strukturu.

Jednoduché použití modulů

Chceme naprogramovat datovou strukturu množina. Modul implementující množinu bude mít následující rozhraní:

module type MySetInterface = {
  type t('a);
  let empty : t('a);

  let add : 'a => t('a) => t('a);

  let mem : 'a => t('a) => bool;
};

Typ t('a) sloužít k reprezentaci množiny prvků typu 'a. Typ t je generický, 'a zastupuje libovolný typ prvků v množině. Funkce empty vrátí prázdnou množinu. Funkce add přidá do množiny prvek a vrací novou množinu. Funkce mem vrací, zda prvek je v množině.

Množina může být implementována pomocí binárních vyhledávacích stromů:

module MySet: MySetInterface = {
  type t('a) =
    | Fork(t('a), 'a, t('a))
    | Leaf;
  let empty = Leaf;
  let rec add = (x, node) =>
    switch node {
    | Fork(left, value, right) =>
      let i = compare(value, x);
      /* value in the current node is smaller than x - insert into left subtree */
      if (i < 0) {
        Fork(add(x, left), value, right)
      } else if (i > 0) {
        Fork(left, value, add(x, right))
      } else {
        node
      }
    | Leaf => Fork(Leaf, x, Leaf)
    };
  let rec mem = (x, node) =>
    switch node {
    | Fork(left, value, right) =>
      let i = compare(value, x);
      /* value in the current node is smaller than x */
      if (i < 0) {
        mem(x, left)
      } else if (i > 0) {
        mem(x, right)
      } else {
        true
      }
    | Leaf => false
    };
};

Implementace binárních vyhledávacích stromů porovnává hodnoty typu 'a pomocí funkce compare. compare je speciální funkce ze standardní knihovny typu 'a => 'a => int. Funkce compare tedy zdánlivě funguje s libovolným typem 'a. Například pro řetězce:

let stringSet = MySet.add("Root.cz", MySet.empty);
Js.log(MySet.mem("Root.cz", stringSet));

Ve skutečnosti však existuje řada typů, kde funkce compare nefunguje. compare například neumí porovnat funkce:

let pairSet = MySet.add(((x) => x + 1, 17), MySet.empty);
Js.log(MySet.mem(((x) => x + 1, 17), pairSet));

Výše uvedený kód vyhodí výjimku. Pokud bychom chtěli do naší množiny ukládat dvojice (funkce, číslo), nesmíme používat funkci compare ze standardní knihovny.

Nejjednodušší řešení je předat vlastní compare explicitně každé funkci, která doposud volala knihovní  compare:

module type MySetInterfaceExplicit = {
  type t('a);
  let empty : t('a);

  let add : (~compare:(('a, 'a) => int), 'a, t('a)) => t('a);

  let mem : (~compare:(('a, 'a) => int), 'a, t('a)) => bool;
};

Nevýhodou tohoto řešení je, že kompilátor již nehlídá, zda pokaždé předáváme stejný compare. Například, zda množinu nenaplníme pomocí jedné funkce compare a pak funkci mem nepředáme jiný compare. Druhou nevýhodou je hůře čitelný kód – balast u každého volání funkcí memadd.

Další možností je předat compare při vytváření prázdné množiny a uložit ho přímo v datové struktuře. Tímto přijdeme o některé optimalizace kompilátoru a každou strukturu zvětšíme o jednu referenci na  compare.

Funktory

Řešením našich problémů je pro každý compare vytvořit novou implementaci množiny, celý modul MySet specializovaný pro daný compare. Nechceme však kopírovat kód. Používaným řešením jsou funktory – jakési funkce, jež na vstupu dostávají modul(y) a vrací modul.

Množinu tedy budeme realizovat jako funktor, který dostane modul s funkcí compare, jenž funguje pro jeden pevně daný typ (není generická), a vrátí implementaci množiny pro tento konkrétní typ.

Začneme tím, že popíšeme rozhraní vstupního modulu. Jelikož funkce compare bude pracovat s jedním konkrétním typem, nebude generická, tak rozhraní nemůže vypadat takhle:

module type ComparableBad = {
  let compare: 'a => 'a => bool
};

Toto rozhraní je špatně, protože požaduje generickou funkci compare, tu ale nejde rozumně naimplementovat. Nemůžeme však psát ani

module type ComparableBad2 = {
  let compare: string => string => int
};

neb tato verze compare může pracovat pouze na řetězcích, naše množiny by tak mohly obsahovat pouze řetězce, což je nepřípustné omezení. My potřebujeme takové rozhraní modulu, kterému bude odpovídat modul s funkcí compare porovnávající řetězce, ale i modul s funkcí compare porovnávající dvojice, ale i modul s funkcí compare porovnávající libovolný pevně daný typ. Takové rozhraní vytvoříme pomocí abstraktního typu

module type Comparable = {
  type t;
  let compare: t => t => int
};

Comparable.t je typ, s kterým Comparable.compare umí pracovat. Zde jsou příklady modulů s tímto rozhraním:

module StringComparable = {
  type t = string;
  let compare = compare;
};

module PairComparable = {
  type t = (int => int, string);
  let compare = ((_, a), (_, b)) => compare(a, b);
};

První modul je pro porovnávání řetězců, druhý modul je pro dvojice. Nyní vytvoříme funktor MakeMySet, který přijímá moduly s rozhraním Comparable a vrací moduly s rozhraním MySetInterface, kde abstraktní typ value nahradíme konkrétním typem z rozhraní Comparable (bez použití konstrukce with by typ value zůstal abstraktní a množina by se prakticky nedala použít; vyzkoušejte si to!):

module type MySetInterface = {
  type value;
  type t;
  let empty : t;

  let add : value => t => t;

  let mem : value => t => bool;
};

module MakeMySet = (C: Comparable) : (MySetInterface with type value = C.t) => {
  type value = C.t;
  type t =
    | Fork(t, value, t)
    | Leaf;
  let empty = Leaf;
  let rec add = (x, node) =>
    switch node {
    | Fork(left, value, right) =>
      let i = C.compare(value, x);
      /* value in the current node is smaller than x - insert into left subtree */
      if (i < 0) {
        Fork(add(x, left), value, right)
      } else if (i > 0) {
        Fork(left, value, add(x, right))
      } else {
        node
      }
    | Leaf => Fork(Leaf, x, Leaf)
    };
  let rec mem = (x, node) =>
    switch node {
    | Fork(left, value, right) =>
      let i = C.compare(value, x);
      /* value in the current node is smaller than x */
      if (i < 0) {
        mem(x, left)
      } else if (i > 0) {
        mem(x, right)
      } else {
        true
      }
    | Leaf => false
    };
};

Aplikací funktoru na moduly s porovnávacími funkcemi vytvoříme moduly pro množiny:

module MyStringSet = MakeMySet(StringComparable);
module MyPairSet = MakeMySet(PairComparable);

Použití množin je snadné:

let pairSet = MyPairSet.add(((x) => x + 1, "foo"), MyPairSet.empty);
Js.log(MyPairSet.mem(((x) => x + 1, "foo"), pairSet));

Řada datových struktur ve standardní knihovně je k dispozici ve formě funktorů:

bitcoin_skoleni

Nyní už je umíme používat.

Polymorfní varianty

V příštím díle se budeme zabývat polymorfními variantami, objekty a řádkovým polymorfismem, který se v Reasonu používá místo klasického podtypového polymorfismu kvůli snazší typové inferenci.

Autor článku

Zajímá se o programovací jazyky a typové systémy. Téměř deset let svého života zasvětil zkoušení různých programovacích jazyků, aby našel ten nejlepší.