Hlavní navigace

Úvod do jazyka Reason: moduly

Radek Miček

Systém modulů v Reasonu je velice mocný nástroj. Dnes se naučíme základy práce s moduly. Ukážeme si, jak definovat modul, jak skrýt části modulu před jeho uživateli a co to jsou abstraktní a privátní typy.

Doba čtení: 4 minuty

Moduly jako jmenné prostory

Modul s funkcemi je na první pohled něco jako třída se statickými metodami. Již minule jsme viděli modul List. Modul List obsahuje funkce pro manipulaci se seznamy, například  List.map.

Funkci map můžeme napsat i pro typ option a dáme ji do modulu Option  – ten ve standardní knihovně zatím neexistuje, takže si jej vytvoříme:

module Option = {
  let map = (f, opt) => switch opt {
    | None => None
    | Some(value) => Some(f(value))
  };
};

Reason nepodporuje overloading. Pokud bychom napsali funkci, jež převádí seznam čísel na řetězec

let toString = (nums : list(int)) => "TODO";

a pak funkci se stejným jménem převádějící option(int) na řetězec

let toString = (opt : option(int)) => "TODO";

tak bude dostupná pouze poslední funkce pro option(int). Následujcí kód se tedy nepřeloží, protože pod názvem toString se nyní skrývá funkce pro  option(int):

toString([1, 2, 3]);

Moduly umožňují existenci obou funkcí. Na rozdíl od ostatních jazyků v Reasonu nebývá zvykem moduly otevírat, jinak řečeno importovat všechno z modulu

open List;

díky čemuž bychom mohli psát map místo List.map. Důvodem, proč se to nedělá, je, že moduly často obsahují funkce s dosti obecnými názvy jako např. map nebo equal a psaní názvu modulu před funkci zjednodušuje čtení kódu. Například z

map(x => x < 0 ? 0 : x, result);

není poznat, zda voláme List.map nebo Option.map. Pokud funkce z nějakého modulu používáme často, vytvoříme alias s kratším názvem

module O = Option;

Pak můžeme psát O.map místo Option.map. Je časté, že modul obsahuje kromě funkcí i typy. Pokud se funkce v modulu vztahují k jednomu typu, nazveme tento typ t. Například pro Option  modul:

module Option = {
  type t('a) = option('a);

  let map = (f, opt) => switch opt {
    | None => None
    | Some(value) => Some(f(value))
  };
};

Typické použití modulů

Typické použití modulů je právě definice jednoho datového typu a funkcí pro práci s tímto typem. Například pro telefonní číslo:

module Phone = {
  type t = string;

  let equal = (a, b) => a == b;

  let ofString : string => t = (s) =>
    String.length(s) == 9 ? s : failwith("Invalid phone number");
};

Řetězce na telefonní čísla převádíme pomocí funkce Phone.ofString. Chtěli bychom, aby všechna telefonní čísla měla 9 znaků. Jenže není problém vytvořit telefonní číslo i bez použití  Phone.ofString

let invalidNumber : Phone.t = "123";

neboť každý uživatel modulu Phone přece vidí, že t je pouhý řetězec.

Rozhraní modulu a abstraktní typy

Každý modul má rozhraní, které určuje, co vidí uživatelé modulu zvenku. Pokud rozhraní explicitně neuvedeme, uživatelé modulu uvidí všechno. Například pro náš moudul Phone jsme rozhraní explicitně neuvedli a kompilátor si tedy odvodí následující:

module Phone : {
  type t = string;

  let equal : 'a => 'a => bool;

  let ofString : string => t
} = {
  type t = string;

  let equal = (a, b) => a == b;

  let ofString : string => t = (s) =>
    String.length(s) == 9 ? s : failwith("Invalid phone number");
};

Smažeme-li z rozhraní fakt, že t = string, nebudou moci uživatelé modulu vytvořit hodnotu typu t jednoduchým přiřazením řetězce. Kromě toho opravíme i typ funkce equal, kompilátor odvodil příliš obecný typ:

module Phone : {
  type t;

  let equal : t => t => bool;

  let ofString : string => t
} = {
  type t = string;

  let equal = (a, b) => a == b;

  let ofString = (s) =>
    String.length(s) == 9 ? s : failwith("Invalid phone number");
};

Typ t je nyní abstraktní a kompilátor zajistí, že kód zvenčí (mimo modul) netuší, že t je řetězec. Díky tomu můžeme kdykoliv změnit implementaci t a nerozbijeme kód mimo modul  Phone.

Nevýhodou tohoto řešení je, že si hodnoty typu t už nemůžeme vypsat ani na obrazovku pomocí print_endline. Důvod je samozřejmě ten, že nikdo mimo modul Phone nesmí vědět, že t je řetězec.

Nicméně v tomto konkrétním případě by nám nevadilo, kdyby okolní kód věděl, že t je řetězec, stačí nám, když hodnoty typu t půjde vytvářet pouze pomocí funkce ofString, která zkontroluje, že se jedná o platné telefonní číslo. To lze zařídit pomocí tzv. privátních typů (důležité je klíčové slovo pri v rozhraní u typu  t):

module Phone : {
  type t = pri string;

  let equal : t => t => bool;

  let ofString : string => t
} = {
  type t = string;

  let equal = (a, b) => a == b;

  let ofString = (s) =>
    String.length(s) == 9 ? s : failwith("Invalid phone number");
};

Díky privátním typům nemusíme psát funkci převádějící Phone.t na řetězec, stačí použít konverzní operátor  :>:

let p = Phone.ofString("123456789");
print_endline(p :> string);

Rozhraní modulu lze využít i ke skrytí pomocných funkcí – to se udělá vynecháním funkcí z rozhraní.

Soubory

Každý soubor se zdrojovým kódem s příponou .re reprezentuje modul. Jméno modulu vznikne odstraněním přípony souboru a změnou prvního písmene na velké.

Kromě souborů s příponou .re existují i soubory s příponou .rei. Tyto soubory obsahují rozhraní rozhraní modulu. Jak již bylo řečeno výše, pokud rozhraní není uvedeno explicitně (soubor .rei neexistuje), odvodí si jej kompilátor sám tak, že okolnímu světu ukáže všechny informace.

Příkladem je modul Phone. Do souboru phone.re dáme definice typů a funkcí:

type t = string;

let equal = (a, b) => a == b;

let ofString = (s) =>
  String.length(s) == 9 ? s : failwith("Invalid phone number");

A v souboru Phone.rei popíšeme, jak modul vypadá zvenčí:

type t = pri string;

let equal : t => t => bool;

let ofString : string => t

Příště nás čekají funktory

Příště se začneme zabývat funktory, jež umožňují parametrizovat modul jinými moduly. Jedná se vlastně o speciální druh funkcí, jejichž parametry jsou moduly a jež vrací také modul.

Našli jste v článku chybu?