Syntaxe ve funkcích
Vzory
Tato kapitola se bude týkat některých užitečných syntaktických konstruktů a nejprve se pustíme do vzorů (pattern matching). Vzory se sestávají z určitých schémat, kterým mohou data odpovídat a poté se ověřuje, jestli ano, a podle těchto schémat se data dekonstruují.
Při definování funkce je možnost napsat samostatně tělo funkce pro jiný vzor. Což vede k velice čistému kódu, který je jednoduchý a čitelný. Vzory se dají použít u jakéhokoliv datového typy — čísla, znaky, seznamy, n-tice atd. Vytvořme si opravdu triviální funkci, která ověřuje, jestli je zadané číslo sedmička nebo ne.
lucky :: (Integral a) => a -> String lucky 7 = "ŠŤASTNÉ ČÍSLO SEDM!" lucky x = "Je mi líto, máš pech, kámo!"
Když zavoláte funkci lucky, schéma se bude ověřovat shora dolů a pokud bude sedět, použije se odpovídající tělo funkce. Jediné číslo, jež může odpovídat prvnímu vzoru, je číslo 7. Pokud není zadáno, přejde se na druhý vzor, který zachytí cokoliv a spojí to s x. Tahle funkce může být implementována použitím výrazu if. Ale co když chceme funkci, která vypíše číslo od jedničky po pětku a napíše Není mezi 1 a 5. pro ostatní čísla? Bez vzorů bychom museli vytvořit celkem spletitý strom z if, then a else. Avšak se vzory:
sayMe :: (Integral a) => a -> String sayMe 1 = "Jedna!" sayMe 2 = "Dva!" sayMe 3 = "Tři!" sayMe 4 = "Čtyři!" sayMe 5 = "Pět!" sayMe x = "Není mezi 1 a 5."
Všimněte si, že kdybychom přesunuli poslední vzor (obecný, který zachytí všechno) úplně nahoru, tak by funkce vždycky vypsala Není mezi 1 a 5., protože by zachytil všechna čísla a nebyla by šance, že by se pokračovalo dál a přešlo na další vzory.
Pamatujete si na funkci factorial, jež jsme implementovali předtím? Definovali jsme faktoriál čísla n jako product [1..n]. Můžeme také definovat faktoriál rekurzivně, způsobem, jakým se obvykle definuje v matematice. Začneme tvrzením, že faktoriál nuly je jednička. Pak uvedeme, že faktoriál každého přirozeného čísla je to číslo vynásobené faktoriálem jeho předchůdce. Takhle to vypadá přeložené do jazyka Haskellu.
factorial :: (Integral a) => a -> a factorial 0 = 1 factorial n = n * factorial (n - 1)
Poprvé jsme tu definovali funkci rekurzivně. Rekurze je v Haskellu důležitá a my se na ni podíváme později podrobněji. Ale zatím si v rychlosti ukážeme, co se děje při výpočtu faktoriálu řekněme trojky. Pokusí se vyhodnotit 3 * factorial 2. Faktoriál dvojky je 2 * factorial 1, takže zatím máme 3 * (2 * factorial 1). Rozepsaný factorial 1 je 1 * factorial 0, takže dostáváme 3 * (2 * (1 * factorial 0)). A teď přichází trik — definovali jsme faktoriál nuly, že je jedna, a protože se narazí na vzor před tím obecným, jednoduše vrátí jedničku. Takže se finální forma podobá 3 * (2 * (1 * 1)). Kdybychom napsali druhý vzor před první, tak by zachytil všechna čísla včetně nuly a náš výpočet by nikdy neskončil. To je důvod, proč je pořadí vzorů důležité a je vždycky lepší uvádět dříve vzory pro konkrétní hodnoty a obecné až na konec.
Ověřování vzorů může také selhat. Jestliže si definujeme takovouto funkci:
charName :: Char -> String charName 'a' = "Albert" charName 'b' = "Bedřich" charName 'c' = "Cecil"
a poté bude zavolána se vstupem, který jsme neočekávali, stane se tohle:
ghci> charName 'a' "Albert" ghci> charName 'b' "Bedřich" ghci> charName 'h' "*** Exception: tut.hs:(53,0)-(55,21): Non-exhaustive patterns in function charName
GHCi si oprávněně stěžuje, že nemáme definovány vzory kompletně. Při vytváření vzorů bychom nikdy neměli zapomenout přidat obecný vzor, aby náš program nepadal po zadání neočekávaném vstupu.
Vzory mohou být použity také s n-ticemi. Co když chceme vytvořit funkci, jež vezme dva vektory ve dvoudimenzionálním prostoru (tedy ve formě dvojic) a sečte je dohromady. Při sčítání vektorů sečteme odděleně jejich xové složky a pak jejich ypsilonové složky. Zde je ukázáno, jak bychom toho mohli dosáhnout, kdybychom nevěděli o vzorech:
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a) addVectors a b = (fst a + fst b, snd a + snd b)
Prima, funguje to, ale je lepší způsob, jak to udělat. Upravíme tu funkci, aby používala vzory.
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a) addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
A je to! Mnohem lepší. Všimněte si, že tohle je už obecný vzor. Typ funkce addVectors (v obou příkladech) je addVectors :: (Num a) => (a, a) -> (a, a) - > (a, a), takže můžeme zaručit, že dostaneme dvě dvojice jako parametr.
Funkce fst a snd získají složky dvojic. Ale co trojice? No, není na to standardní funkce, ale můžeme si napsat vlastní.
first :: (a, b, c) -> a first (x, _, _) = x second :: (a, b, c) -> b second (_, y, _) = y third :: (a, b, c) -> c third (_, _, z) = z
Znak _ představuje to stejné co představoval u generátorů seznamu. Což znamená, že se vůbec nestaráme o hodnotu v té části, takže prostě napíšeme _.
Což mi připomíná, že se vzory dají použít i v generátorech seznamu. Sledujte:
ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)] ghci> [a+b | (a,b) <- xs] [4,7,6,8,11,4]
Při nesplnění vzoru se prostě přejde na další prvek.
Samotné seznamy mohou být také použity jako vzory. Můžete ověřit prázdný seznam [] nebo jakýkoliv vzor, který obsahuje dvojtečku : a prázdný seznam. Protože je [1,2,3] jenom syntaktický cukr pro 1:2:3:[], dá se použít prvně uvedený vzor. Vzor jako x:xs spojí první prvek ze seznamu s x a zbytek s xs, i když by byl seznam jednoprvkový, to by se z xs stal prázdný seznam.
Pokud chcete spojit s proměnnými, řekněme, první tři prvky a zbytek seznamu s jinou proměnnou, můžete použít něco jako x:y:z:zs. Tohle bude fungovat jenom se seznamem, jež má tři prvky a více.
A když teď víme, jak používat vzory se seznamy, napišme si vlastní implementaci funkce head.
head' :: [a] -> a head' [] = error "Nemůžeš zjistit první prvek prázdného seznamu, hňupe!" head' (x:_) = x
Vyzkoušíme, jestli to funguje:
ghci> head' [4,5,6] 4 ghci> head' "Nazdar" 'N'
Nádhera! Všimněte si, že pokud chceme spojit několik proměnných (i když je nějaká z nich jenom _ a ve skutečnosti se vůbec nespojí), musíme vzor obklopit kulatými závorkami. Také si všimněte funkce error, jenž jsme použili. Vezme řetězec a vygeneruje běhovou chybu a použije ten řetězec jako informaci o tom, jaká chyba nastala. To způsobí pád programu, takže není moc dobrý nápad tuto funkci používat příliš často. Jenže zavolání funkce head na prázdný seznam nedává smysl.
Vytvořme si triviální funkci, která nám vypíše první prvky ze seznamu v (ne)vhodné formě v češtině.
tell :: (Show a) => [a] -> String tell [] = "Seznam je prázdný." tell (x:[]) = "Seznam obsahuje jeden prvek: " ++ show x tell (x:y:[]) = "Seznam obsahuje dva prvky: " ++ show x ++ " a " ++ show y tell (x:y:_) = "Seznam je dlouhý. První dva prvky jsou: " ++ show x ++ " a " ++ show y
Tato funkce je bezpečná, protože se vypořádá s prázdným seznamem, jedno- a dvouprvkovým seznamem a se seznamem s více než dvěma prvky. Všimněte si, že vzory (x:[]) a (x:y:[]) mohou být zapsány jako [x] a [x,y] (syntaktický cukr, u něhož nepotřebujeme kulaté závorky). Nemůžeme přepsat vzor (x:y:_) na tvar s hranatými závorkami, protože potřebujeme, aby to ověřovalo seznamy délky dva a více.
Již jsme implementovali svou vlastní funkci length pomocí generátoru seznamu. Teď na to použijeme vzory a trochu rekurze:
length' :: (Num b) => [a] -> b length' [] = 0 length' (_:xs) = 1 + length' xs
Podobá se to funkci na počítání faktoriálu, jež jsme napsali dříve. Nejprve jsme si definovali výsledek známého vstupu — prázdného seznamu. To je též známo jako okrajová podmínka. Poté jsme ve druhém vzoru vzali kus seznamu pomocí rozdělení na první prvek a zbytek. Napsali jsme, že délka seznamu se rovná jedna plus délka zbytku. Je zde použito podtržítko _ na ověření prvního prvku, protože se nezajímáme o konkrétní hodnotu. Také si všimněte, že jsme se vypořádali s každým možným typem seznamu. První vzor ověřuje prázdný seznam a druhý cokoliv, co není prázdný seznam.
Podívejme se, co se stane, jestliže zavoláme funkci length' na řetězec "pes". Nejprve se funkce přesvědčí, zda není vstupem prázdný seznam. Protože není, přejde se na druhý vzor. Druhý vzor odpovídá a výraz se přepíše na 1 + length' "es", protože se rozdělí na první prvek a zbytek, který se dále zpracuje. Dobrá. Délka length' řetězce "es" je, podobně, výraz 1 + length' "s". Takže teď máme výraz 1 + (1 + length' "s"). Hodnota výrazu length' "s" je 1 + length' "" (což může být zapsáno jako 1 + length' []). A my jsme si definovali, že length' [] bude 0. Takže nám nakonec zbude výraz 1 + (1 + (1 + 0)).
Vytvoříme si funkci sum. Víme, že součet prázdného seznamu je 0. Napíšeme to jako vzor. A víme také, že součet seznamu je první prvek plus součet zbytku seznamu. Takže když tohle celé zapíšeme, dostaneme:
sum' :: (Num a) => [a] -> a sum' [] = 0 sum' (x:xs) = x + sum' xs
Také existuje věc, nazvaná zástupný vzor. Je to užitečný způsob, jak rozdělit něco podle vzoru a navázat to na názvy, zatímce stále uchováváme referenci na tu celou věc. Provede se to vložením názvu a zavináče @ před vzor. Kupříkladu vzor xs@(x:y:ys). Tento vzor bude ověřovat přesně stejnou věc jako x:y:ys, jenom můžete přistupovat jednoduše k celému seznamu přes xs, aniž byste se museli opakovat a psát znovu x:y:ys do těla funkce. Tady je rychlý a hrubý příklad:
capital :: String -> String capital "" = "Prázdný řetězec, jejda!" capital all@(x:xs) = "První písmeno řetězce " ++ all ++ " je " ++ [x]
ghci> capital "Drákula" "První písmeno řetězce Drákula je D"
Normálně používáme zástupné vzory, abychom se vyhnuli opakování při ověřováňí složitějších vzorů, když musíme využít celý výraz znovu v těle funkce.
Ještě jedna věc — nemůžete ve vzorech použít operátor ++. Pokud se budete snažit ověřit výraz pomocí vzoru (xs ++ ys), jaký bude první a jaký druhý seznam? Nedává to příliš smysl. Mohlo by dávat smysl, kdybychom chtěli udělat něco jako (xs ++ [x,y,z]) nebo jenom (xs ++ [x]), ale není to možné, už ze samotné podstaty seznamů.
Stráže, stráže!
Zatímco vzory jsou určeny k ujištění, že hodnota vyhovuje určité formě, a k její dekonstrukci, stráže jsou pro testování, jestli je nějaká vlastnost hodnoty (či více hodnot) pravdivá nebo nepravdivá. To zní skoro jako výraz if a opravdu je to velice podobné. Věc se má tak, že stráže jsou mnohem čitelnější, když je těch podmínek více, a chovají se spíše jako vzory.
Místo vysvětlování jejich syntaxe se do toho rovnou pustíme a vytvoříme si funkci s využitím stráží. Napíšeme si jednoduchou funkce, která na vás bude nadávat v závislosti na vašem indexu tělesné hmotnosti (BMI). Hmotnostní index se rovná váze člověka vydělené druhou mocninou jeho výšky. Pokud je index menší než 18,5, je považován za podvyživeného. Jestliže je mezi 18,5 a 25, je považován za normálního. Hodnota mezi 25 a 30 je nadváha a lidé s BMI nad 30 jsou obézní. Takže tady je ta funkce (nebudeme ji teď počítat, tahle funkce pouze vezme BMI a vynadá vám).
bmiTell :: (RealFloat a) => a -> String bmiTell bmi | bmi <= 18.5 = "Jsi podvyživený, ty emo, ty!" | bmi <= 25.0 = "Jsi údajně normální. Pche, vsadím se, že jsi šereda!" | bmi <= 30.0 = "Jsi tlustý! Zhubni, špekoune!" | otherwise = "Jsi velryba, gratuluji!"
Stráž se zadává svislítkem, který následuje za názvem funkce a jejími parametry. Obvykle jsou svislítka o kousek odsazena doprava a zarovnána. Stráže jsou vlastně booleovské výrazy. Jestliže se vyhodnotí jako True, je použito odpovídající tělo funkce. Jestliže se vyhodnotí jako False, ověřování pokračuje na další stráž a tak dále. Pokud zavoláme tuto funkci s parametrem 24.3, nejprve se ověří, jestli je menší nebo rovný 18.5. Protože není, propadne se k další stráži. Ověření se provede u druhé stráže a protože je 24.3 menší než 25.0, je vrácen druhý řetězec.
Tohle celé očividně připomíná velký strom z if a else v imperativních jazycích, jenomže stráže jsou lepší a čitelnější. Zatímco z velkých if a else stromů se nebudete tvářit nadšeně, zvláště když je problém zadaný diskrétním způsobem, ze kterého se těžko přepracovává. Stráže jsou velice příjemnou alternativou.
Poslední stráž bývá častokrát otherwise. Výraz otherwise je definován jednoduše jako otherwise = True a odchytí všechno. Tohle je velice podobné vzorům, s tím rozdílem, že se u nich ověřuje, jestli vstup odpovídá nějakému schématu a u stráží se kontroluje, zdali vstup vyhovuje booleovským podmínkám. Jestliže se všechny stráže ve funkci vyhodnotí jako False (a neposkytli jsme stráž otherwise, která odchytí všechno), vyhodnocení přejde na následující vzor. Takhle spolu vzory a stráže nádherně spolupracují. Pokud není nalezena vyhovující stráž nebo vzor, vyhodnocování skončí chybou.
Samozřejmě můžeme použít stráže ve funkci, která bere tolik parametrů, kolik chceme. Místo abychom nutili uživatele počítat svůj hmotnostní index před zavoláním funkce, upravíme tuto funkci, tak, že bude požadovat váhu a výšku a vypočítá to pro nás.
bmiTell :: (RealFloat a) => a -> a -> String bmiTell weight height | weight / height ^ 2 <= 18.5 = "Jsi podvyživený, ty emo, ty!" | weight / height ^ 2 <= 25.0 = "Jsi údajně normální. Pche, vsadím se, že jsi šereda!" | weight / height ^ 2 <= 30.0 = "Jsi tlustý! Zhubni, špekoune!" | otherwise = "Jsi velryba, gratuluji!"
Podívejme se, jestli jsem tlustý…
ghci> bmiTell 85 1.90 "Jsi údajně normální. Pche, vsadím se, že jsi šereda!"
Jéje! Nejsem tlustý! Ale Haskell mě nazval šeredným. Co už!
Další velmi jednoduchý příklad: napišme si vlastní funkci max. Určitě si pamatujete, že vezme dvě porovnatelné věci a vrátí větší z nich.
max' :: (Ord a) => a -> a -> a max' a b | a > b = a | otherwise = b
Stráže se dají také zapsat jednořádkově, i když to nedoporučuji, protože je to méně čitelné, i u krátkých funkcí. Ale pro demonstraci můžeme napsat max' následovně:
max' :: (Ord a) => a -> a -> a max' a b | a > b = a | otherwise = b
Fuj! To není moc čitelné! Budeme pokračovat: napíšeme si svou vlastní funkci compare pomocí stráží.
myCompare :: (Ord a) => a -> a -> Ordering a `myCompare` b | a > b = GT | a == b = EQ | otherwise = LT
ghci> 3 `myCompare` 2 GT
Lokální definice pomocí where
V předchozí sekci jsme si definovali funkci na počítání BMI a nadávání. Bylo to nějak takhle:
bmiTell :: (RealFloat a) => a -> a -> String bmiTell weight height | weight / height ^ 2 <= 18.5 = "Jsi podvyživený, ty emo, ty!" | weight / height ^ 2 <= 25.0 = "Jsi údajně normální. Pche, vsadím se, že jsi šereda!" | weight / height ^ 2 <= 30.0 = "Jsi tlustý! Zhubni, špekoune!" | otherwise = "Jsi velryba, gratuluji!"
Všimněte si, že tu třikrát opakujeme kód. Třikrát opakujeme kód. Opakování kódu (třikrát) při programování je asi tak žádoucí jako kopačka do hlavy. Jelikož jsme opakovali stejný výraz třikrát, bylo by ideální, kdybychom ho mohli spočítat, navázat na proměnnou a poté ji používat namísto výrazu. Můžeme tedy upravit naši funkci následovně:
bmiTell :: (RealFloat a) => a -> a -> String bmiTell weight height | bmi <= 18.5 = "Jsi podvyživený, ty emo, ty!" | bmi <= 25.0 = "Jsi údajně normální. Pche, vsadím se, že jsi šereda!" | bmi <= 30.0 = "Jsi tlustý! Zhubni, špekoune!" | otherwise = "Jsi velryba, gratuluji!" where bmi = weight / height ^ 2
Vložili jsme klíčové slovo where za stráže (obvykle je nejlepší ho odsadit stejně jako svislítka) a poté definovat několik názvů nebo funkcí. Tyto definice jsou viditelné všem strážím a mají tu výhodu, že nemusíme opakovat kód. Jestliže jsme se rozhodli, že budeme počítat BMI jinak, stačí nám změnit kód na jednom místě. Také to zlepšuje čitelnost, když pojmenováváme věci a můžeme tím naše programy zrychlit, protože věci jako je tady proměnná bmi stačí vypočítat pouze jednou. Mohli bychom jít o kousek dál a přepsat naši funkci takhle:
bmiTell :: (RealFloat a) => a -> a -> String bmiTell weight height | bmi <= skinny = "Jsi podvyživený, ty emo, ty!" | bmi <= normal = "Jsi údajně normální. Pche, vsadím se, že jsi šereda!" | bmi <= fat = "Jsi tlustý! Zhubni, špekoune!" | otherwise = "Jsi velryba, gratuluji!" where bmi = weight / height ^ 2 skinny = 18.5 normal = 25.0 fat = 30.0
Názvy, jež definujeme u funkce v části s where, jsou dostupné pouze v té funkci, takže se nemusíme obávat o zaneřádění jmenných prostorů jiných funkcí. Všimněte si, ze jsou všechny názvy zarovnány do jednoho sloupce. Pokud je pořádně nezarovnáme, Haskell bude zmatený, protože nebude vědět, co je součástí stejného bloku.
Konstrukce where není sdílená v tělu funkce mezi různými vzory. Pokud chcete přistupovat u více vzorů v jedné funkci k nějakým definicím, musíte je definovat globálně.
Je také možné ve where definicích použít vzory! Můžeme přepsat sekci s where v naší předchozí funkci na:
... where bmi = weight / height ^ 2 (skinny, normal, fat) = (18.5, 25.0, 30.0)
Vytvořme si další poctivě triviální funkci, ve které dostaneme někoho jméno a příjmení a vypíšeme jeho iniciály.
initials :: String -> String -> String initials firstname lastname = [f] ++ ". " ++ [l] ++ "." where (f:_) = firstname (l:_) = lastname
Mohli bychom to ověřovat přímo v parametrech funkce (bylo by to ve skutečnosti kratší a zřejmější), ale tohle mělo jenom ukázat, že je možné to udělat taky pomocí where definic.
Stejně jako jsem si definovali konstanty ve where blocích, můžete také definovat funkce. Abychom zůstali u našeho programovacího zdravotního tématu, vytvoříme si funkci, která vezme seznam dvojic vah a výšek a vrátí jejich index hmotnosti.
calcBmis :: (RealFloat a) => [(a, a)] -> [a] calcBmis xs = [bmi w h | (w, h) <- xs] where bmi weight height = weight / height ^ 2
A to je všecho, co je potřeba! Důvod, proč jsme v tomto příkladu zavedli bmi jako funkci, je protože nemůžeme vypočítat jedno BMI z parametrů funkce. Musíme projít celý seznam předaný funkci a každá dvojice ze seznamu má rozdílné BMI.
Konstrukce where se mohou také větvit. Je to běžný postup, vytvořit funkci a definovat k ní nějaké pomocné funkce se svými where klauzulemi, a pak těm funkcím vytvořit další pomocné funkce, každou s vlastními where klauzulemi.
… a pomocí let
Velmi podobné konstrukci where je konstrukce let. Where je syntaktický konstrukt, který umožní navázání proměnných na konec funkce a celá funkce k nim může přistupovat, včetně všech stráží. Let vám umožní navázat proměnné kamkoliv a je sama o sobě výrazem, ale je lokálnější, takže se nedostane přes stráže. Stejně jako každý konstrukt v Haskellu, jež se používá k navázání hodnoty na název, konstrukce let mohou být použity pro ověřování vzorů. Podívejme se na ně v činnosti! Takhle bychom mohli definovat funkci, která nám vrátí vypočítaný povrch válce na základě jeho výšky a poloměru:
cylinder :: (RealFloat a) => a -> a -> a cylinder r h = let sideArea = 2 * pi * r * h topArea = pi * r^2 in sideArea + 2 * topArea
Podoba zápisu je let <definice> in <výraz>. Názvy, které definujete v části s let jsou přístupné výrazu v části za in. Jak můžete vidět, dalo by se to také zapsat pomocí konstrukce where. Všimněte si, že názvy jsou také zarovnány do jednoho sloupce. Takže jaký je rozdíl mezi těmito dvěma zápisy? Zatím to vypadá, že se u let píše definice jako první a používaný výraz až později, zatímco u where to je naopak.
Rozdíl je v tom, že konstrukce let je sama o sobě výraz. Kdežto where je pouhý syntaktický konstrukt. Pamatujete si, když jsme se zabývali výrazem if a vysvětlovali jsme si, že if a else můžete nacpat téměř kamkoliv?
ghci> [if 5 > 3 then "Bla" else "Ble", if 'a' > 'b' then "Něco" else "Nic"] ["Bla", "Nic"] ghci> 4 * (if 10 > 5 then 10 else 0) + 2 42
Stejnou věc můžete udělat s konstrukcí let.
ghci> 4 * (let a = 9 in a + 1) + 2 42
Mohou být také použity na zavedení funkcí s lokální působností:
ghci> [let square x = x * x in (square 5, square 3, square 2)] [(25,9,4)]
Jestliže chceme definovat několik proměnných na jednom řádku, evidentně je nemůžeme zarovnat do sloupců. Proto je můžeme oddělit pomocí středníků.
ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hej "; bar = "ty!" in foo ++ bar) (6000000,"Hej ty!")
Nemusíte vkládat středník za poslední definici, ale můžete, pokud chcete. Jak jsme již řekli, v konstrukcích let je možnost použít vzory. Je to velice užitečné pro rychlé rozebrání n-tic na složky a navázání na názvy a tak.
ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100 600
Můžete také vložit konstrukci let dovnitř generátoru seznamu. Přepišme si náš předchozí příklad na počítání seznamů dvojic vah a výšek a použijme v něm let v generátoru seznamu místo abychom definovali pomocnou funkci přes where.
calcBmis :: (RealFloat a) => [(a, a)] -> [a] calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]
Zařadili jsme let do generátoru seznamu, jako by byl predikát, jenom nefiltruje seznam, ale definuje názvy. Názvy, definované pomocí let v generátoru seznamu jsou viditelné výstupní funkci (část před svislítkem |) a všem predikátům a případným částem, které následují po definicích. Takže bychom mohli funkci předělat, aby vracela pouze BMI tlustých lidí:
calcBmis :: (RealFloat a) => [(a, a)] -> [a] calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]
Nemůžeme použít název bmi v části s (w, h) <- xs, protože je definovaná před konstrukcí let.
Vynechali jsme část s in konstrukce let, když jsme pracovali s generátorem seznamu, protože tam je viditelnost názvů předdefinována. Nicméně jsme mohli použít konstrukci let-in v predikátu a názvy mohli definovat tak, aby byly viditelné pouze predikátu. Část s in může být také vynechána, když definujeme funkce a konstanty přímo v GHCi. Pokud to uděláme, názvy pak budou viditelné po celý čas interaktivní relace.
ghci> let zoot x y z = x * y + z ghci> zoot 3 9 2 29 ghci> let boot x y z = x * y + z in boot 3 4 2 14 ghci> boot <interactive>:1:0: Not in scope: `boot'
Když je konstrukce let tak skvělá, proč bychom ji nemohli použít všude namísto where, ptáte se? No, protože je konstrukce let výraz a je poctivě lokální ve své působnosti, nemůže být použita mezi strážemi. Někteří lidé mají raději konstrukci where, protože definice následují za funkcí, ve které se používají. V tomto zápisu je tělo funkce blíže názvu funkce a typové deklaraci, takže to je pro některé čitelnější.
Podmíněný výraz case
Mnoho imperativních jazyků (C, C++, Java apod.) mají case syntaxi a pokud jste v nějakém z nich programovali, pravděpodobně víte, co to je. Funguje to tak, že se vezme proměnná a potom se provedou bloky kódu pro určenou hodnotu té proměnné a je možnost na konec přidat blok, který zachytí cokoliv, pro případ, že by proměnná nabyla hodnoty, se kterou jsme nepočítali.
Haskell bere tento koncept a rozšiřuje ho. Jak název napovídá, výrazy case jsou, no, výrazy, podobně jako výraz if a konstrukce let. Nejenom že umí vyhodnocovat výrazy podle možných případů, ve kterých proměnná nabývá určitých hodnot, můžeme také vyhodnocovat na základě vzorů. Hmmm, vzít proměnnou, ověřit podle vzoru, vyhodnotit kus kódu na podle jeho hodnoty, kde jsme to už slyšeli? No jasně, ověřování vzorů podle parametrů v definici funkce! Takže to je ve skutečnosti pouhý syntaktický cukr pro case výraz. Tyhle dva kusy kódy dělají tu stejnou věc a jsou zaměnitelné:
head' :: [a] -> a head' [] = error "Prázdný list nemá první prvek!" head' (x:_) = x
head' :: [a] -> a head' xs = case xs of [] -> error "Prázdný list nemá první prvek!" (x:_) -> x
Jak můžete vidět, syntaxe výrazu case je pěkně jednoduchá:
case výraz of vzor -> výsledek vzor -> výsledek vzor -> výsledek ...
Obsah části výraz je ověřován vzory. Postup je stejný, jaký bychom čekali: je použit první vzor, který sedí. Jestliže pokračuje přes celé case a není nalezen vhodný vzor, nastane běhová chyba.
Zatímco vzory u parametrů funkcí mohou být použity pouze při definování těchto funkcí, case výrazy mohou být použity víceméně všude. Kupříkladu:
describeList :: [a] -> String describeList xs = "Seznam je " ++ case xs of [] -> "prázdný." [x] -> "jednoprvkový." xs -> "víceprvkový."
To je užitečné pro ověřování něčeho uprostřed zápisu výrazu. Protože je ověřování v definici funkce syntaktický cukr pro výraz case, mohli bychom to rovněž definovat takto:
describeList :: [a] -> String describeList xs = "Seznam je " ++ what xs where what [] = "prázdný." what [x] = "jednoprvkový." what xs = "víceprvkový."