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.
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.