IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

3. diel - Haskell - Teba by som typoval na funkciu ...

V minulej lekcii, Prvá funkcia v Haskell , sme si vytvorili prvú jednoduché funkcie v Haskell. Dnes budeme pokračovať s dátovými typmi a vytvoríme si funkcie zložitejšie.

Typy

Haskell je silne staticky typizovanom jazyk, tzn. všetko má svoj typ a ten sa nemení. Pre zistenie typu slúži skratka :t nasledovaná tým, čoho typ nás zaujíma. Skúsme si pár jednoduchých príkladov:

Aký je typ znaku? Napíšme :t 'a':

'a' :: Char -- neboli 'a' má typ Char

A čo napríklad textu? Zadáme :t "Ahoj světe!":

"Ahoj světe!" :: [Char]

Tu vidíme, že string v Haskell ako taký neexistuje, je to len pole znakov.

Typ je niečo ako nálepka, aby sám kompilátor vedel, čo je tá "vec" zač. To má rad výhod, napríklad ak sa pokúsime deliť Integer typom Boolean, kompiler takýto program ani nepreloží. Vedie to teda k veľkej efektivite. Navyše, oproti nudným mainstreamovým jazykom, v Haskell nemusíme explicitne hovoriť, čo je aký typ. Až doteraz sme sa bez typov obišli. Haskell sa sám rozhodne pre vhodný typ podľa toho čo definujeme.

Základné typy

Int, Integer, Char, Float, Double, Bool, n-tica. Všetko je asi jasné, za zmienku stojí rozdiel medzi Int a Integer. Int sa používa pre celé čísla od cca 2147483647 do -2147483648. Tieto čísla sa môžu líšiť, ale ak budete blbnúť s kalkulačkou, zaručene skončíte v Integeru. Ten je totiž neobmedzený čo sa týka veľkosti, práca s ním však môže byť preto o chlp pomalší.

Typové triedy

Nenechajte sa zmiasť objektovo orientovaným programovaním, typová trieda neoznačuje nejaký objekt. Je to rozhranie, ktoré definuje správanie.

Num

Skúsme zistiť typ konštanty 8:

:t 8

výsledok:

8 :: Num a => a.

Num a znamená, že a je nejaké číslo (Int, Integer, Decimal, ... kto vie) a teda patrí do typovej triedy. Skúste si:

8 :: Integer
8 :: Int
8 :: Double
8 :: Float.

Už je asi jasné, že ak vložíme :t 8.0, výsledok nebude v triede Num, ale Fractional, podmnožine Num. 8 teda dedia vlastnosti z triedy Num. Môže sa napríklad k niečomu pripočítať atd ...

Eq ==

Eq je typová trieda pre ekvivalenciu. Pokiaľ vo svojej funkcii používate niekde test na == či /=, je potrebné mať prvky v tejto triede, aby mali možnosť byť porovnávané.

Ord

Ak napríklad premenná a patrí do triedy Ord ako ordinál, môže byť porovnaná. To sa hodí, ak máte funkciu na väčšie ako a menšie ako.

Ďalšie triedy si preberieme, až ich budeme potrebovať. Najčastejšie sú: Eq, Ord, Show (prevádza na reťazec), Read (akési "Unshow", prevádza z reťazca na typ), Enum (zoradené typy v sekvencii, u ktorých je možné použiť napríklad funkciu succ, ktorá vráti ďalší prvok), ďalej Num, Integral, Floating, ...

Typy funkcií

Zoberme si napríklad funkcie head a tail:

head :: [a] -> a
tail :: [a] -> [a]

Čo to znamená? Veľmi jednoducho, head vezme zoznam a vráti prvok zo zoznamu, tail vezme zoznam a vráti zoznam. Prvok a môže byť čokoľvek, Int, ďalšie pole, String, pár. To nie je podstatné.

A čo napríklad taká funkcia reverse ? Aký tá má typ?

reverse :: [a] -> [a]

Jednoduché. Čo tak naše funkcie mult3 ?

mult3 :: Int -> Int -> Int -> Int

Tento zápis nám hovorí len to, že funkcia berie na vstupe tri parametre typu Int a vracia tiež Int. Ako to spoznáme? No, jednoducho z definície funkcie. Tá vracia iba jednu hodnotu, takže posledný typ je vždy návratový. Z toho plynú ďalšie veci. Napríklad každá funkcia musí mať aspoň dva typy vo svojej definícii - vstupné a výstupné.

Aký bude mať typ treba fst ?

fst :: (a,b) -> a

A tak ďalej ... Prečo to vlastne hovoríme? Nie je jedno, aký má funkcie typ? Veď si ho tam Haskell doplní sám. Odpoveď znie: áno aj nie. Rozhodne typy písať nemusíte, ale je veľmi odporúčané je písať. Ak totiž váš program funguje tak, ako má, nikto s ničím nemá problém. Akonáhle ale niekde nastane chyba a vy budete lúštiť, čo sa deje, chybové hlášky sú často veľmi neprehľadné. Napíšte si schválne triedu sum' s definíciou sumy. Ak vám zobraz zdroj nepôjde preložiť, mal by vás Haskell sám naviesť k tomu, čo vám v nej chýba. Až ju napíšete, skúste si zadať sum 1 a sum' 1. A teraz jedna malá habaďura. My sme si (teda aspoň ja) definovali sum' takto:

sum' :: (Eq a, Num a) => [a] -> a
sum' (x:xs)
   | xs == [] = x
   | otherwise = x + sum' xs

Vtip je v tom, že sme mohli svoju sum definovať ešte inak:

sum'' :: Num a => [a] -> [a]
sum'' [] = 0
sum'' (x:xs) = x + sum'' xs

V druhej definícii sme nepotrebovali vôbec triedu Eq. Aj v tom je dobré písať vlastné typy funkcií, presne viete, čo váš program robí a čo robí navyše.

Chybová hláška u nášho sum' či dokonca sum'' je tiež oveľa čitateľnejší, než tá u toho vstavaného. To je jeden z dôvodov, prečo sa oplatí definícia písať ručne. Ďalšia výhoda je čitateľnosť. Keď napíšete funkciu a dobre ju pomenujete, tak potom už vás nezaujíma vyložene kód, ale ako tú funkciu napojiť do vláčika ďalších funkcií, prípadne aké argumenty potrebuje. Preto si sami skúste ku všetkým funkciám napísať ich typy. Ak budete v koncoch, vždy sa môžete inšpirovať u Haskell pomocou funkcie :t, ale odporúčam si to najskôr skúsiť každý sám ...

Keď by som vám zadal treba typ funkcie replicate, už o nej viete celkom dosť:

replicate :: Int -> a -> [a]

Čo tá funkcia robí? Vezme Int, vezme niečo a potom vráti pole s tým niečím. Skúste teraz sami napísať funkciu, ktorá dostane n a prvok a skopíruje funkciu n -krát.

Zložitejšie funkcie

Na dnešnej lekcii máme takisto sľúbené, že si skúsime implementovať zložitejšie funkcie. Skúsme implementovať trochu zložitejšie funkciu zip:

zip :: [a] -> [b] -> [(a,b)]

Čo táto funkcia robí? Vezme dva zoznamy a spáruje prvý s prvým, druhý s druhým atď ... kým aspoň jeden zoznam neskončí. Zvyšok druhého zoznamu zahodí. Na tomto príklade si opäť ukážeme výhodu logického programovania. V procedurálnych jazykoch by sme túto úlohu riešili nejakým slovníkom, alebo novým poľom obsahujúcim dvojice, teraz ho šikovne alokovať atď ... V Haskell sa to píše samo.

Prvýkrát definíciu:

zip' :: [a] -> [b] -> [(a,b)]
--  Pak se musíme podívat na krajní podmínky, aneb kdy má program skončit.
zip' [] _ = []
zip' _ [] = []
--  A jsme za půlkou. Nyní už musíme vyřešit samotné párování.
zip (x:xs) (y:ys) = (x,y) : zip xs ys

A ... to je celé. Nič zložité. Celý program teda vyzerá takto:

zip' :: [a] -> [b] -> [(a,b)]
zip' [] _ = []
zip' _ [] = []
zip (x:xs) (y:ys) = (x,y) : zip xs ys

Funkcia zip sa hodí, keď máme napr. Triedu žiakov a chceme ich očíslovať. Tu sa navyše ukazuje jedna veľmi príjemná vlastnosť Haskell, jeho lenivosť. Nepočíta, čo nemusí.

zip [1..] ["Kunhuta", "Ctibor", "Vasyl", "Mateo", "Mahulena", "Kvivo", "Naomi", "Stella"]

Skúsme si teraz napísať úlohu nahradenie prvku v zozname:

nahr :: (Eq a) => a -> a -> [a] -> [a]

To je všetko, čo potrebujeme. Len sa nenechajme zmiasť zápisom. Tu len hovoríme, že budeme používať všade rovnaký typ. Môžeme ale mať rôzne premenné:

nahr a _ [] = []
nahr a b (x:xs)
    | x == a = b : nahr a b xs
    | otherwise = x : nahr a b xs

Je dobrým zvykom pýtať sa na zložitosť algoritmu. V Haskell je to trochu oriešok, pretože on sám robí dosť zložité optimalizácia a ako navyše uvidíme v ďalšej lekcii, je tzv. Lenivý, takže nepočíta to, čo nepotrebuje. To sme napokon už videli u nekonečných zoznamov. V budúcej lekcii, Haskell - Stráže, zoznam uteká do nekonečna! , si predstavíme generátory zoznamov a temnú mágiu okolo nich.


 

Predchádzajúci článok
Prvá funkcia v Haskell
Všetky články v sekcii
Haskell
Preskočiť článok
(neodporúčame)
Haskell - Stráže, zoznam uteká do nekonečna!
Článok pre vás napísal Ondřej Michálek
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje teoretické informatice. Ve svých volných chvílích nepohrdne šálkem dobrého čaje, kaligrafickým brkem a foukací harmonice.
Aktivity