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.