N-tice
Minule jsme se naučili, jak do jedné proměnné uložit jednu hodnotu, například číslo nebo řetězec, dnes si ukážeme, jak do jedné proměnné uložit hodnot více.
Nejjednodušším způsobem, jak toho dosáhnout, jsou n-tice. Například souřadnice můžeme reprezentovat jako dvojice:
let coordinates = (23, 42);
Nebo vstup pro náš převodník měn můžeme rovněž reprezentovat jako dvojici (kurz, počet korun)
:
let input = (25.4, 200);
Pattern matching
Dříve či později bude třeba z dvojice získat jednotlivé hodnoty. To lze udělat pomocí pattern matchingu (česky: srovnání se vzorem):
let (x, y) = coordinates;
Pattern (vzor) (x, y)
je dvojice, kde místo konkrétních hodnot jsou názvy proměnných. Pattern matching (srovnávání se vzorem) je proces, který do proměnných ve vzoru (tj. x
a y
) přiřadí takové hodnoty, aby se vzor shodoval s hodnotou napravo od =
(tj. coordinates
).
Hodnota napravo od =
je (23, 42)
. Aby se vzor (x, y)
shodoval s hodnotou napravo od =
, je třeba do x
přiřadit 23
a 42
do y
.
Jiný příklad:
let foo = (1, ("nested", "pair"), true);
Pokud je pro nás důležitá druhá hodnota z vnořeného dvojice, použijeme následující pattern matching:
let (_, (_, importantValue), _) = foo;
Pro hodnoty, jenž nás nezajímají, používáme podtržítka místo proměnných. Tímto jsme definovali proměnnou importantValue
s hodnotou "pair"
. Pokud chceme celou vnořenou dvojici, použijeme následující vzor:
let (_, nestedPair, _) = foo;
Proměnná nestedPair
bude obsahovat dvojici ("nested", "pair")
. Pokud chceme vnořenou dvojici i její druhou hodnotu, použijeme následující vzor:
let (_, (_, importantValue) as nestedPair, _) = foo;
Vzor můžeme chápat jako hodnotu s dírami (proměnnými) a srovnávání se vzorem se snaží tyto díry vyplnit. Speciálním případem vzoru je vzor, který neobsahuje žádnou díru:
let (23, 42) = coordinates;
Definice proměnné z minula je rovněž speciální případ pattern matchingu:
let bar = (2, 3);
Proměnná bar
je jediná díra. Proces srovnání se vzorem do proměnné bar
přiřadí celou hodnotu napravo od =
.
Pokud hodnota napravo od =
neodpovídá vzoru (do proměnných ve vzoru nejde přiřadit, aby se hodnota napravo od =
shodovala se vzorem), bude vyhozena výjimka. Například
let (32, y) = coordinates;
Ať do proměnné y
přiřadíme cokoliv, (32, y)
se nebude shodovat s (23, 42)
. Pattern matching tedy vyhodí výjimku.
Dobrou zprávou je, že výjimkám při pattern matchingu lze snadno předejít, neb překladač Reasonu vypisuje varování this pattern matching is not exhaustive, když existují hodnoty, které neodpovídají vzoru. Důležité je uvědomit si, že překladač kouká pouze na vzor, nikoliv na hodnotu napravo od =
. Takže nás bude varovat i v případech, kdy žádná výjimka vyhozena nebude:
let (23, 42) = coordinates; let (23, y) = coordinates; let 1 = 1;
V praxi se toto varování nikdy nebere na lehkou váhu a pattern matching, jenž jej způsobuje, se nepoužívá!
Pattern matching a funkce
Pattern matching můžeme použít i při definování funkcí. Například místo
let processCoordinates = (coordinates) => { let (x, y) = coordinates; Js.log3("Processing", x, y); };
můžeme psát rovnou
let processCoordinates = ((x, y)) => Js.log3("Processing", x, y);
Chceme-li funkci zavolat, nesmíme zapomenout na závorky kolem dvojice:
processCoordinates((3, 4));
Pouhé processCoordinates(3, 4)
je chyba, neboť funkce processCoordinates
nebere dva argumenty nýbrž jeden.
N-tice se také hodí v situacích, kdy z funkce vracíme více hodnot.
Záznamy
Nepříjemnost n-tic je, že hodnoty v nich nejsou pojmenované. Například u vstupu pro převodník měn
let input = (25.4, 200);
si programátor musí pamatovat, že kurz je první a počet korun druhý. V těchto případech bývá lepší definovat nový datový typ záznam:
type calcInput = { crownsPerUnit: float, crowns: int };
Jméno typu je calcInput
. Nyní jej můžeme použít místo dvojice:
let input = {crownsPerUnit: 25.4, crowns: 200};
Při vytváření záznamu typu calcInput
není nutné psát název typu, Reason typ odovdí podle názvů polí crownsPerUnit
a crowns
.
Abychom dostali hodnotu ze záznamu, můžeme opět použít pattern matching:
let {crownsPerUnit: cpu, crowns: crowns} = input;
Princip je stejný. Vzor je záznam, kde místo hodnot používáme názvy proměnných. Výše uvedený řádek tedy zadefinuje 2 nové proměnné cpu
a crowns
. Pokud je název definované proměnné stejný jako název pole záznamu, nemusíme jej psát. Stačí tedy
let {crownsPerUnit: cpu, crowns} = input;
Pokud nás navíc nějaké pole záznamu nezajímá, můžeme jej vynechat:
let {crownsPerUnit: cpu} = input;
Kromě pattern matchingu lze k polím záznamu přistupovat pomocí tečky. Kurz tedy můžeme získat input.crownsPerUnit
.
Jak změnit záznam
Hodnoty polí v záznamu měnit nelze, musíme vytvořit záznam nový. Například:
let input' = {crownsPerUnit: input.crownsPerUnit, crowns: 300};
Abychom ručně nemuseli kopírovat původní nezměněné hodnoty, existuje speciální syntax používající tři tečky:
let input' = {...input, crowns: 300};
Za třemi tečkami je záznam, ze kterého se vezmou hodnoty polí, jež explicitně nenastavíme – v našem případě explicitně nastavujeme pouze hodnotu pole crowns
.
Třítečková syntax se používá i v souboru src/page.re
ve funkci make
.
let make = (~message, _children) => { ...component, render: (self) => <div onClick=(self.handle(handleClick))> (ReasonReact.stringToElement(message)) (ReasonReact.stringToElement({j|BTW $crowns CZK is equal to $dollars5 USD!|j})) </div> };
Záznam component
reprezentuje komponentu se standardním chováním. Tato komponenta nic nedělá. My měníme funkci render
, aby komponenta něco vykreslila do stránky.
Reactí komponenty se stavem
Zatím náš převodník měn neumožňuje uživateli zadat kurz a ani částku v korunách, kterou chce převést na cizí měnu. Oboje je pevně určeno přímo v kódu. To nyní změníme. Do webové stránky přidáme vstupní pole pro kurz a pro částku. Kdykoliv se obsah jednoho z polí změní, spočteme částku v cizí měně a výsledek zobrazíme ve stránce.
Začneme tím, že z nestavové komponenty Page
uděláme komponentu stavovou. Stav nám umožní uložit obsah vstupních polí.
Změna nestavové komponenty na stavovou se provede úpravou řádku
let component = ReasonReact.statelessComponent("Page");
na
let component = ReasonReact.reducerComponent("Page");
Tato úprava však způsobí, že se náš program nepřeloží. Překladač hlásí chybu:
This expression's type contains type variables that can't be generalized:
ReasonReact.componentSpec
('_a, ReasonReact.stateless, ReasonReact.noRetainedProps,
ReasonReact.noRetainedProps, '_b)
Problém spočívá v tom, že se překladači nepodařilo zjistit konkrétní typ pro stav a konkrétní typ pro akce, jenž slouží ke změně stavu.
Akce, jenž slouží ke změně stavu? Ano, naše komponenta začne s nějakým počátečním stavem, který vrátila funkce initialState
. Změny stavu se budou povádět tak, že se funkci reducer
předá akce a funkce reducer na základě předané akce zajistí změnu stavu. Funkce initialState
a reducer
jsou součástí záznamu, jenž vrací make
, a musíme je naimplementovat. Až je naimplementujeme, zmizí i chybové hlášení, protože překladač bude schopen zjistit konkrétní typy pro stav a akce, které slouží ke změně stavu.
Implementace initialState
Funkce initialState
vrací počáteční stav naší komponenty. Abychom ji mohli naimplementovat, musíme zadefinovat datový typ pro stav:
type state = { crownsPerUnit: string, crowns: string };
Datový typ je třeba zadefinovat před proměnnou component
, neboť bude použit jako součást jejího typu. Nyní předefinujeme funkci initialState
v záznamu, jenž vrací make
. Zde je celý kód funkce make
:
let make = (~message, _children) => { ...component, initialState: () => {crownsPerUnit: "0.0", crowns: "0"}, render: (self) => <div onClick=(self.handle(handleClick))> (ReasonReact.stringToElement(message)) (ReasonReact.stringToElement({j|BTW $crowns CZK is equal to $dollars5 USD!|j})) </div> };
Implementace reducer
Abychom mohli naprogramovat funkci reducer
, potřebujeme typ akcí, jež slouží ke změně stavu. Tento typ bude dvojice, jejíž první složka určuje, jaké vstupní pole se mění, a druhá složka je nová hodnota, nový řetězec. Musíme tedy definovat typ, kterým vyjádříme, jaké vstupní pole se mění:
type inputField = | CrownsPerUnit | Crowns;
Výše uvedený kód zadefinuje výčtový typ, enum, který má dvě možné hodnoty – buď CrownsPerUnit
, nebo Crowns
. Definici typu inputField
opět musíme umístit před proměnnou component
, důvod je stejný jako v případě typu state
. Předefinujme reducer
:
let make = (~message, _children) => { ...component, initialState: () => {crownsPerUnit: "0.0", crowns: "0"}, reducer: ((inputField, value), state) => switch inputField { | Crowns => ReasonReact.Update({...state, crowns: value}) | CrownsPerUnit => ReasonReact.Update({...state, crownsPerUnit: value}) }, render: (self) => <div onClick=(self.handle(handleClick))> (ReasonReact.stringToElement(message)) (ReasonReact.stringToElement({j|BTW $crowns CZK is equal to $dollars5 USD!|j})) </div> };
Všimněme si, že předefinováním funkce reducer
zmizela chyba kompilátoru. Pojďme si vysvětlit, co reducer
dělá. První parametr funkce je akce, jenž slouží ke změně stavu, druhý parametr je původní stav. reducer
napřed pomocí pattern matchingu rozloží akci, jenž slouží ke změně stavu, na inputField
a value
. inputField
značí, které vstupní pole se změnilo, a value
je jeho nová hodnota.
reducer
musí vrátit instrukci, jak změnit stav komponenty. V našem případě se vždy bude jednat o instrukci tvaru ReasonReact.Update(novýStav)
, kde novýStav
je záznam typu state
obsahující nový stav. Abychom mohli spočítat nový stav, musíme vědět, které ze vstupních polí se změnilo, což zjistíme pomocí konstrukce switch
.
Zobrazení výsledku
Funkce pro práci se stavem máme napsané a program se přeloží. Ze stavu můžeme ve funkci render
spočítat částku v cizí měně:
let make = (~message, _children) => { ...component, initialState: () => {crownsPerUnit: "0.0", crowns: "0"}, reducer: ((inputField, value), state) => switch inputField { | Crowns => ReasonReact.Update({...state, crowns: value}) | CrownsPerUnit => ReasonReact.Update({...state, crownsPerUnit: value}) }, render: (self) => { let crownsPerUnit = float_of_string(self.state.crownsPerUnit); let crowns = int_of_string(self.state.crowns); let dollars = crownsToForeignCurrency2(~crownsPerUnit, crowns); <div onClick=(self.handle(handleClick))> (ReasonReact.stringToElement(message)) (ReasonReact.stringToElement({j|BTW $crowns CZK is equal to $dollars USD!|j})) </div> } };
K aktuálnímu stavu komponenty lze z funkce render
přistupovat přes self.state
. self
je obyčejný parametr funkce render
, nejedná se o analogii this
z objektově orientovaných jazyků (záznamy v Reasonu jsou něco úplně jiného než objekty v Reasonu, bohužel).
Vstupní pole
Zbývá už jen přidat vstupní pole pro uživatele:
let make = (~message, _children) => { ...component, initialState: () => {crownsPerUnit: "0.0", crowns: "0"}, reducer: ((inputField, value), state) => switch inputField { | Crowns => ReasonReact.Update({...state, crowns: value}) | CrownsPerUnit => ReasonReact.Update({...state, crownsPerUnit: value}) }, 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 inputValue
použitá v handlerech událostí onChange
získá novou hodnotu ze vstupního pole. V definici inputValue
používáme ##
, jenž slouží pro přítup k polím a metodám objektů. ##
je pro objekty totéž co .
pro záznamy. self.send
dostane akci ke změně stavu a zavolá funkci reducer
.
Hotovo. Vše zdánlivě funguje, dokud uživatel zadává platná čísla do vstupních polí.
Příště
Příště se budeme zabývat ošetřováním chyb, variantami a pattern matchingem. Základy pattern matchingu jsme si představili dnes, příště se podíváme na to, jak pattern matching funguje s variantami. Nově nabyté znalosti nám umožní vylepšit převodník měn, aby se choval slušně, i když uživatel zadává řetězce, jenž není možné převést na čísla.
Repozitář s kódy k tomuto dílu najdete na GitHubu.