Hlavní navigace

Úvod do jazyka Reason: varianty, pattern matching a ošetřování chyb

Radek Miček

Dnes si ukážeme, jak Reason zobecnil výčtový typ známý například z C++ nebo Javy a jak s tím souvisí pattern matching. Zobecněné výčtové typy i pattern matching využijeme při ošetřování chybových stavů.

Doba čtení: 7 minut

Varianty

Posledně jsme si ukázali, jak vytvořit výčtový typ, enum:

type inputField =
  | CrownsPerUnit
  | Crowns;

V Reasonu se tomuto typu říká varianta a CrownsPerUnit a Crowns jsou konstruktory nebo tagy. Na rozdíl od populárních objektově orientovaných jazyků konstruktor není funkce a není s ním spjat žádný kód. A na rozdíl od klasických výčtových typů můžeme do konstruktorů uložit data. Například

type contact =
  | NoContact
  | Address(string, string, string)
  | Phone(string);

Vytváření hodnot typu contact vypadá podobně jako volání funkce, ale znovu opakuji, konstruktory žádné funkce nejsou:

let no = NoContact;
let shortPhone("11");
let address = Address("Komunardu", "Prague", "17000");

Abychom mohli provést větvení na základě konstruktoru, použijeme  switch:

let processContact = (c) =>
  switch c {
  | NoContact => Js.log("Missing contact")
  | Address(_street, _city, postcode) => Js.log2("Postcode", postcode)
  | Phone(p) when String.length(p) <= 2 => Js.log("Suspicious phone")
  | Phone(p) => Js.log2("Phone", p)
  };

switch umožňuje provádět pattern matching na variantách. Výše uvedený switch vezme hodnotu c typu contact a postupně ji srovnává se vzory NoContact, Address(_street, _city, postcode), … Až najde vzor, kterému hodnotu c odpovídá, vykoná kód za šipkou => napravo od vzoru.

Pokud hodnota neodpovídá žádnému vzoru, bude vyhozena výjimka. Nicméně takové situace lze eliminovat díky varování kompilátoru. Například odstraníme-li poslední vzor

let processContact2 = (c) =>
  switch c {
  | NoContact => Js.log("Missing contact")
  | Address(_street, _city, postcode) => Js.log2("Postcode", postcode)
  | Phone(p) when String.length(p) <= 2 => Js.log("Suspicious phone")
  };

Visual Studio Code podtrhne kód vlnovkou a vypíše varování

this pattern-matching is not exhaustive 

a kompilátor vypíše hlášení

  You forgot to handle a possible value here, for example:
Phone _
(However, some guarded clause may match this value.) 

jímž se nám snaží říci, že některé hodnoty tvaru Phone(x), pro nějaké x nejspíše neodpovídají žádnému vzoru.

Pokud za vzorem následuje when s booleovskou podmínkou, kód za šipkou => se vykoná pouze tehdy, je-li podmínka splněna. Pokud podmínka splněna není, hledá se další vzor. V našem případě se Js.log("Suspicious phone") vykoná pouze v případech, kdy c odpovídá vzoru Phone(p) a telefonní číslo má nejvýše 2 znaky. Vzorům s when  se česky říká vzory se stráží, anglicky se používá termín guarded clause a podmínka za when se nazývá guard.

Když kompilátor analyzuje, zda existuje hodnota, jenž neodpovídá žádnému ze vzorů v konstrukci switch, předpokládá, že podmínka ve when není splněna. Kvůli tomu vypíše varování i pro následující kód:

let processContact3 = (c) =>
  switch c {
  | NoContact => Js.log("Missing contact")
  | Address(_street, _city, postcode) => Js.log2("Postcode", postcode)
  | Phone(p) when true => Js.log("Suspicious phone")
  };

Ve vzoru Address(_street, _city, postcode) jsou dvě proměnné _street a _city, jejichž název začíná podtržítkem. Důvodem je, že se nepoužívají a podtržítky na začátku názvu dáváme kompilátoru na vědomí, že se nepoužívají záměrně. Klidně bychom mohli psát stručnější a méně přehledné  Address(_, _, postcode).

Anti-pattern podtržítko

Krásnou vlastností Reasonu je, že nás kompilátor varuje, pokud existují hodnoty, kterým žádný vzor neodpovídá. Toho se hojně využívá při pridávání nových konstruktorů. Například přidáme-li konstruktor Email do našeho typu  contact

type contact =
  | NoContact
  | Address(string, string, string)
  | Phone(string)
  | Email(string);

kompilátor nám oznámí, jaké funkce Email zatím nepodporují a je nutné je upravit. V našem případě dostaneme varování pro funkci  processContact.

Nicméně, pokud bychom definovali

let processContact4 = (c) =>
  switch c {
  | Address(_, _, _) => Js.log("Address")
  | _ => Js.log("No address")
  };

tak bychom žádné varování nedostali, neboť vzoru podtržítko v poslední větvi odpovídají všechny konstruktory – i ty, co nově přidáme. Správným řešením je nepoužívat vzory, kterým odpovídá libovolný konstruktor u typů, kde by v budoucnu mohl být nový konstruktor přidán. Vzor podtržítko nahradíme vzorem, který explicitně uvádí všechny zastoupené konstruktory:

let processContact5 = (c) =>
  switch c {
  | Address(_, _, _) => Js.log("Address")
  | NoContact | Phone(_) => Js.log("No address")
  };

Ořítko | mezi vzory NoContact a Phone(_) slouží jako nebo. Stačí tedy, když hodnota c odpovídá alespoň jednomu ze vzorů.

Funkce processContact5 je v pořádku, kompilátor nás po přidání konstruktoru Email do typu contact varuje a my můžeme provést nápravu:

let processContact5 = (c) =>
  switch c {
  | Address(_, _, _) | Email(_) => Js.log("Address")
  | NoContact | Phone(_) => Js.log("No address")
  };

Díky varování lze bezpečně rozšiřovat typy o nové konstruktory a spolehnout se na kompilátor, že najde všechna místa, jenž je třeba opravit.

Výjimkou, kde lze bezpečně používat podtržítko pro zastoupení libovolného konstruktoru, jsou varianty, do nichž se nový konstruktor přidávat nikdy nebude:

type day = Mon | Tue | Wed | Thu | Fri | Sat | Sun;

let isWeekend = (d) => switch d {
| Sat | Sun => true
| _ => false
};

Datový typ option

Jednou z aplikací variant jsou funkce, jenž mohou selhat. Například funkce převádějící řetězec na číslo může vracet následující variantu:

type intParsingResult =
  | InvalidString
  | Number(int);

Když vstupní řetězec jde naparsovat, je vrácen konstruktor Number s naparsovaným číslem, jinak je vrácen konstruktor InvalidString bez čísla.

Podobné situace nastávají tak často, že standardní knihovna obsahuje generický typ  option:

type option('a) =
  | None
  | Some('a);

'a je typová proměnná, místo 'a se dosazují konkrétní typy, díky čemuž option není omezen na jeden typ, jako tomu je v případě  intParsingResult.

Na jednoduchém příkladu je vidět, jak se option používá:

let safeDiv = (a, b) => if (b == 0) None else Some(a / b);

Bohužel ve standardní knihovně je zatím velmi málo funkcí, jež v případě problému vrací option, většina funkcí vyhazuje výjimky.

Výjimky

S datovým typem option a konstrukcí switch si při ošetřování chybových stavů nevystačíme. Je třeba se naučit chytat výjimky. Například při převodu řetězce na číslo funkcí int_of_string může být vyhozena výjimka Failure. Pro chycení výjimky použijeme  try:

let int_of_string_opt = (s) =>
  try (Some(int_of_string(s))) {
  | Failure(_msg) => None
  };

Výraz Some(int_of_string(s)) může vyhodit výjimku, takže jej obalíme konstrukcí try. Ke zjištění konkrétní výjimky používáme pattern matching. Pokud naparsování řetězce projde, bude vrácena hodnota tvaru Some(naparsovanéČíslo), jinak bude vrácena hodnota  None.

Typ výrazu uvnitř try se musí shodovat s typy výrazů za šipkami =>. Konkrétně typ Some(int_of_string(s)) se musí shodovat s typem None. Důsledkem tohoto pravidla je, že kdybychom do try dali pouze int_of_string(s) (bez Some), tak bychom v případě neúspěšného převodu museli vrátit číslo, například

let int_of_string_strange = (s) =>
  try (int_of_string(s)) {
  | Failure(_msg) => 42
  };

Funkce int_of_string_opt se objeví v jedné z příštích verzí BuckleScriptu.

Převodník měn

Náš převodník měn je skoro hotový. Jediným kazem na jeho kráse je vyhození výjimky, kdykoli uživatel zadá neplatné číslo do pole pro kurz nebo do pole pro počet korun. Problém vyřešíme úpravou funkce render, v níž dochází k převodu řetězců na čísla:

  render: (self) => {
    let crownsPerUnit = float_of_string(self.state.crownsPerUnit);
    let crowns = int_of_string(self.state.crowns);
    let dollars = crownsToForeignCurrency2(~crownsPerUnit, crowns);
    let inputValue = (event) => ReactDOMRe.domElementToObj(ReactEventRe.Form.target(event))##value;
    <div onClick=(self.handle(handleClick))>
      <input
        value=self.state.crownsPerUnit
        onChange=((event) => self.send((CrownsPerUnit, inputValue(event))))
      />
      <input
        value=self.state.crowns
        onChange=((event) => self.send((Crowns, inputValue(event))))
      />
      (ReasonReact.stringToElement(message))
      (ReasonReact.stringToElement({j|BTW $crowns CZK is equal to $dollars USD!|j}))
    </div>
  }

Funkce pro převod řetězců na čísla umístíme do try, který bude vracet zprávu o převodu korun na dolary (v případě úspěchu) nebo chybové hlášení (v případě vyhození výjimky):

  render: (self) => {
    let conversionSummary =
      try {
        let crownsPerUnit = float_of_string(self.state.crownsPerUnit);
        let crowns = int_of_string(self.state.crowns);
        let dollars = crownsToForeignCurrency2(~crownsPerUnit, crowns);
        {j|BTW $crowns CZK is equal to $dollars USD!|j}
      } {
      | Failure(_) => "Input field does not contain a valid number"
      };
    let inputValue = (event) => ReactDOMRe.domElementToObj(ReactEventRe.Form.target(event))##value;
    <div onClick=(self.handle(handleClick))>
      <input
        value=self.state.crownsPerUnit
        onChange=((event) => self.send((CrownsPerUnit, inputValue(event))))
      />
      <input
        value=self.state.crowns
        onChange=((event) => self.send((Crowns, inputValue(event))))
      />
      (ReasonReact.stringToElement(message))
      (ReasonReact.stringToElement(conversionSummary))
    </div>
  }

Alternativou k výjimkám je použít funkce int_of_string_opt a float_of_string_opt, které se v budoucnu objeví ve standardní knihovně BuckleScriptu:

    let crownsPerUnitOpt = float_of_string_opt(self.state.crownsPerUnit);
    let crownsOpt = int_of_string_opt(self.state.crowns);
    let conversionSummary =
      switch (crownsPerUnitOpt, crownsOpt) {
      | (Some(crownsPerUnit), Some(crowns)) =>
        let dollars = crownsToForeignCurrency2(~crownsPerUnit, crowns);
        {j|BTW $crowns CZK is equal to $dollars USD!|j}
      | _ => "Input field does not contain a valid number"
      };

Výhodou tohoto řešení je, že snadno poznáme, jaká vstupní pole obsahují neplatná čísla. Poznamenejme, že podtržítko ve vzoru není anti-pattern, který jsem popsali výše, neboť do typu option se nové konstruktory přidávat nebudou.

MIF18 tip v článku ROSA

Závěr

Příště otevřeme téma rekurze. Napíšeme naši první rekurzivní funkci v Reasonu a budeme se zabývat rekurzivními typy, zejména variantami. Rekurze bude pomyslná tečka za základy Reasonu a funkcionálního programování. Po jejím probrání se můžeme vrhnout na některé pokročilejší konstrukce, jež nemají ekvivalent v jiných jazycích.

Kód pro tento díl naleznete na GitHubu.

Našli jste v článku chybu?