Eelmine Üles Järgmine

Peatükk 3  Tüübitud programmeerimine

Haskellis on kolm võimalust tüüpe defineerida: type, data ja newtype. Võtmesõnaga type saab defineerida tüübisünonüümi, mis on peamiselt kasulik koodi dokumenteerimisel. Võtmesõnaga data saab luua aga päris uue nn. algebralise andmetüübi. Viimane variant newtype, mida me siin ei käsitle, paikneb kahe eelneva võimaluse vahepeal ning kasutatakse peamiselt koodi optimiseerimiseks.

3.1  Tüübisünonüümid

Koodi dokumenteerimiseks on hea anda kõigepealt mingitele tüüpidele uued nimed. Näiteks võime kirjutada sellise koodi.

type Hind = Double type Toode = String type Ostukorv = [(Toode, Hind)] kogumaksumus :: Ostukorv -> Hind kogumaksumus [] = 0 kogumaksumus ((t,h):xs) = h + kogumaksumus xs

Tüübisünonüüm on algse tüübiga äravahetatavalt võrdne, s.t.  5.0 :: Hind ja kogumaksumus [] :: Double.

Keerulisematel juhtudel võivad tüübisünonüümid omada tüübiargumente.

type Rida a = [a] type Veerg a = [a] type Tabel a = [Rida a] rida :: Int -> Tabel a -> Rida a rida n xs = xs !! n veerg :: Int -> Tabel a -> Veerg a veerg n = map (!!n)

Tüübisünonüümi definitsioon ei tohi olla rekursiivne.

Prelude> type X = (Int, X) <interactive>:1:1: Cycle in type synonym declarations: <interactive>:1:1-17: type X = (Int, X)

3.2  Algebralised andmetüübid

Haskellis uute tüüpide defineerimise meetodite kohta kasutatakse sõna algebraline. Seda terminit kasutatakse programmeerimiskeelte märkimaks, et uusi andmetüüpe saab olemasolevatest tüüpidest luua liitmise ja korrutamise abil. Esmalt vaatamegi algebraliste andmetüüpide teooriat ja siis selle implementatsioon Haskellis.

Liitmise all mõeldakse seda, et kui meil on väärtused a :: A ja b :: B siis nii a :: (A + B) ja ka b :: (A + B). Kusjuures iga väärtuse x :: (A + B) kohta peab saama kontrollida, kas väärtus tuli paremalt x :: A või vasakult x :: B. Näiteks võime mõelda arvutuse, mille tagastustüüp on Int + Error — tagastakse arv või veateade.

Korrutamise all mõeldakse seda, et kui meil on väärtused a :: A ja b :: B siis (a,b) :: (A * B). Ning iga paari p :: (A * B) korral, peab olema võimalus nii parema kui vasema komponendi leidmine p-st. Näiteks arvutus kompleksarvudega võib anda meile väärtuse tüübiga Double * Double — tagastatakse arvu reaal ja imaginaarosa.

Hea teada: Olgu meil operaator |·|, mis arvutab iga tüübi kohta, mitu erinevat puhast väärtust selles tüübis on. Teame, et |Bool| = 2, |()| = 1, |Int| = 264 ja |Integer| = ∞. Siis iga tüübi A ja B puhul kehtivad distributiivsuse valemid.
     
    |A + B| = |A| + |B|          
|A * B| = |A| * |B|           
Seega |Bool * Bool| = 4, |() + ()| = 2. Seetõttu nimetataksegi mainitud tüüpide kombinatsioone vastavalt liitmiseks ja korrutamiseks.

Nüüd vaatame, kuidas nii summa kui korrutise tüüpe Haskellis praktiliselt defineerida. Järgnevalt defineerimegi tüüpide A1, A2, …, An summa tüübi S.

data S = C1 A1 | C2 A2 | … | Cn An

See deklaratsioon defineeris lisaks tüübile S konstruktorid Ci :: Ai -> S, millega Ai tüüpi väärtusi saab teisendada summa tüübiga väärtusteks. Andmete kättesaamiseks väärtusest x :: S kasutatakse mustrisobitust — näiteks funktsiooniga f, mille sisendtüübiks S.

f :: S -> … f (C1 a1) = … f (C2 a2) = … … f (Cn an) = …

Pane Tähele! Tüübinimed algavad suurtähega. Prefixsed konstruktorid algavad suurtähega ja infixed kooloniga (:).

Järgnevalt defineerime tüüpide A1, A2, …, An korrutise tüübi K.

data K = C A1 A2An

See deklaratsioon defineerib lisaks tüübile K konstruktor C :: A1 ->  A2 ->  … -> An ->  K. Andmete kättesaamiseks väärtusest x :: K kaustatakse jällegi mustrisobitust — näiteks funktsiooni f, mille sisendtüübiks K.

f :: K -> … f (C a1 a2an ) = …

või

f :: K -> … f x = … where C a1 a2an = x

Liitmise ja korrutamise mustrit võib loomulikult kasutada koos ning tüübikonstruktorile võib anda tüübiparameetreid. Järgnevalt vaatame Haskellis laialt kasutust leidvaid algebralisi tüüpe.

Pane Tähele!

Järgnevate alampeatükkide juures peame kasutama tüübiklasse kuigi tüübiklassidest on juttu alles peatükis 3.3. Kokkuvõetult: selleks et defineeritud andmetüüpe saaks mugavalt kasutada tuleks lisada data deklaratsiooni lõppu vähemalt deriving (EqOrdShow). Nendest Eq ja Ord võimaldavad defineeritud väärtusi võrrelda ja Show võimaldab väärtust muuta sõneks. Ilma neid lisamata oleks GHCi-s koodi testimine oluliselt raskendatud. Aga nagu öeldud, täpsem selgitus sellele asub peatükis 3.3.

Bool

Üks lihtsamaid algebralisis andmetüüpe on tõeväärtuste tüüp Bool. Kuigi teoorias saame defineerida Bool := () + () siis Haskellis defineeritakse see hoopis järgnevalt.

data Bool = False | True deriving (Eq, Ord, Show)

Konstruktoritel True :: Bool ja False :: Bool pole argumente vaid nad on kohe tüübiga Bool.

Vaatame, kuidas defineerida lihtsaid funktsioone üle tõeväärtuste tüübi.

not :: Bool -> Bool not False = True not True = False

Funktsiooni tagastusväärtuse kindlaks tegemiseks piisab vaadata, mis on sisestatud argument: kui argument on False siis tagastame True, kui aga argument on True, siis tagastame False.

Konjunktsiooni ja disjunktsiooni kirjutamiseks piisab teha mustrisobitus ainult esimesel argumendil.

(&&) :: Bool -> Bool -> Bool True && x = x False && _ = False (||) :: Bool -> Bool -> Bool True || _ = True False || x = x

Maybe a

Nüüd vaatame üheargumendilist tüübiperet Maybe. Tüübiperele erinevaid argumente andes saame erinevaid tüüpe, näiteks Maybe Bool või Maybe Char. Järgnevalt toodud tüübipere võimaldab lisada tüübile ühe väärtuse lisaks.

data Maybe a = Nothing | Just a deriving (Eq, Ord, Show)

Saame kirjutada järgneva funktsiooni default, mis tagastab Maybe Int sees oleva numbri, või numbri puudumisel teise argumendi.

default :: Maybe Int -> Int -> Int default Nothing x = x default (Just y) _ = y

Tüübipere Maybe on tihti kasutusel juhtudel, kui arvutus võib mingis mõttes ebaõnnestuda. Näiteks paaride listist esimese paari komponendi järgi paari teise komponendi otsimiseks on funktsioon lookup :: Int -> [(Intb)] -> Maybe b. See tagastab Nothing juhul, kui esimese argumendiga võrdset arvu paaride listis pole.

Either a b

Nüüd vaatame kaheargumendilist tüübiperet Either, mis implementeerib kahe tüübi summat.

data Either a b = Left a | Right b deriving (Eq, Ord, Show)

Saame kirjutada järgneva funktsiooni f, mis tagastab Either Int Int sees oleva numbri olenemata, kummast liidetavast poolest see tuleb

f :: Either Int Int -> Int f (Left x) = x f (Right x) = x

Listid --- [a]

Kuna listid on Haskellis laialt kasutusel, on nende jaoks süntaksis erireeglid, mille sarnast tavaprogrammeerija ise lisada ei saa. Kui aga saaks, võiks liste defineerida nii

data [a] = [] | a : [a]

Listide ja ka teiste tüübiperede paremaks arusaamiseks soovitame defineerida oma andmestruktuurid. Näiteks

data MyList a = Nil | Cons a (MyList a) deriving (Eq, Ord, Show) data Pair a b = Pair a b myHead :: MyList a -> a myHead = ??? myfst :: Pair a b -> a myfst = ???

Nüüd kirjutage ise funktsioonide myHead ja myfst definitsioonid.

3.3  Tüübiklassid

Polümorfism (i.k. polymorphism) on see, kui samale funktsioonile f saab rakendada vähemalt kahe eri tüüpi andmeid. Näiteks, kui meil on x :: Int ja y :: Bool ning ma saan kirjutada nii f x kui ka f y siis peab funktsioon f olema polümofne.

Haskellis on kaks erinevat polümofismi liiki. Kõige tavalisem on Haskellis parameetriline polümofism, mis tähendab, et sama definitsiooni saab rakendada eri andmetel. Kõige lihtsam parameetriliselt polümofne funktsioon on identsusfunktsioon, mis on defineeritud järgnevalt:

id :: a -> a id x = x

On selge, et id on polümofne, kuna id 10  10 ja id False  False — selgelt 10 ja False on eri tüüpidega.

Parameetriliselt polümorfse funktsiooniga ei saa aga implementeerida näiteks võrdsusoperaatorit eq :: a -> a -> Bool, mida saaks kasutada nii arvude kui tõeväärtuste võrdlemiseks. Põhjus seisneb selles, et eri tüüpi väärtuste võrdlemiseks on vaja erinevaid algoritme. Lahenduse pakub ad-hoc polümorfism, mis lubab erinevate tüüpide puhul kirjutada erinevaid algoritme.

Pane Tähele! OOP on suuresti üles ehitatud ad-hoc polümorfismile.

Klassid

Haskellis luuakse ad-hoc polümorfseid funktsioone kasutades tüübiklasse (i.k. typeclass), mida saab intuitiivselt vaadata kui tüübimuutuja kitsendust. Järgnevalt vaatame, kuidas võrduse kontrolli saab defineerida tüübiklassidega.

Esmalt defineerime tüübiklassi, mis omab ühiseid jooni objektorienteeritud programmeerimisest tulevate liideste või abstraktsete klassidega.

class Equal a where eq :: a -> a -> Bool

Peale sellist deklaratsioon on defineeritud väärtus eq :: Equal a => a -> a -> Bool. See tähendab, et tingimusel kui tüüp a on tüübiklassist Equal, saame kasutada funktsiooni a -> a -> Bool. Kuna aga ükski tüüp pole veel sellest tüübiklassist, pole funktsioon f veel praktiliselt kasutatav.

Instantisd

Teise sammuna saame defineerime instantse (i.k instance) tõeväärtuste võrdlemiseks.

instance Equal Bool where eq True True = True eq False False = True eq _ _ = False

Nüüd on Bool tüübiklassi Equal instants ja me saame kasutada funktsiooni eq võrduse kontrollimiseks.

eq False False ⇝  True 

Kui lisada veel instantse, saab funktsiooni ka teistel tüüpidel kasutada.

instance Equal () where eq _ _ = True
eq () () ⇝  True 

Parametriseeritud tüüpide (näiteks listid) puhul saab instantside deklareerimisel lisada tingimus, et parameeter peab oleama mingist tüübiklassist. Niimoodi saab võrdsust defineerida kõikide listide puhul, mille elemendid on võrreldavad.

instance Equal a => Equal [a] where eq [] [] = True eq (x:xs) (y:ys) = eq x y && eq xs ys eq _ _ = False

Pane Tähele! Mittetühjade listide võrdsuse kontrollis kasutatakse kaks korda funktsiooni eq: esimesel puhul elementide võrdsuse kontrolliks ja teisel puhul (rekursiivseks) listi sabade võrdsuse kontrolliks.

     
  eq [False,True] [False]  =  eq (False:True:[]) (False:[])          
 ⇝  eq False False && eq (True:[]) []          
 ⇝  True && eq (True:[]) []          
 ⇝  eq (True:[]) []          
 ⇝  False           

Samamoodi saab defineerida paaride ja ennikute võrdsust:

instance (Equal a, Equal b) => Equal (a,b) of eq (x1,x2) (y1,y2) = eq x1 y1 && eq x2 y2
     
  eq ((),True) ((), True)⇝  eq () () && eq True True          
 ⇝  True && eq True True          
 ⇝  eq True True          
 ⇝  True           

Pärimine ja vaikedefinitsioonid

Haskelli tüübiklassidest saab moodustada hierarhiaid, kui panna ühele tüübiklassile eeltingimuseks teise tüübilkassi. Samuti saab lisada vaikeimplementatsiooni, mida kasutatakse vaid siis kui instants seda üle ei defineeri.

class Equal a => Order a where (<) :: a -> a -> Bool (<=) :: a -> a -> Bool a <= b = eq a b || a < b

Tõevaäärtuste tüübi instantsi saab kirjutada nii, eeldades et Equal instants on juba defineeritud:

instance Order Bool where False < True = True _ < _ = False

Sellisel juhul kasutab instants vaikedefinitsiooni. Mõnel juhul saame aga ise parema definitsiooni anda.

instance Order () where _ < _ = True _ <= _ = True

Vaatame ühte näidet kummagi juhu kohta:

     
  False <= False⇝  eq False False || False < False          
 ⇝  True || False < False          
 ⇝  True           
     
  () <= ()⇝  True           

Haskelli standardteek

Eelnevalt toodud näited olid lihtsustatud versioonid standardteegis olevatest tüübiklassidest, mida iga Haskelli programmeerija teadma peab. Tähtsaimad neist on Eq, Ord, Show, Read, Enum ja Bounded.

Tüübiklassid Eq ja Ord implementeerivad võrdust ja täielikku järjestust.

class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool class Eq a => Ord a where compare :: a -> a -> Ordering (<) :: a -> a -> Bool (>=) :: a -> a -> Bool (>) :: a -> a -> Bool (<=) :: a -> a -> Bool max :: a -> a -> a min :: a -> a -> a

Tüübiklassid Show ja Read võimaldavad väärtuste konverteerimist sõneks. S.t saate vastavalt kasutada funktsioone show :: Show a => a -> String ja read :: Read a => String -> a.

*Main> read "55" + 5 60 *Main> show 10 "10"

Kasulik on teada ka Enum ja Bounded tüübiklasse.

class Enum a where succ :: a -> a pred :: a -> a toEnum :: Int -> a fromEnum :: a -> Int enumFrom :: a -> [a] enumFromThen :: a -> a -> [a] enumFromTo :: a -> a -> [a] enumFromThenTo :: a -> a -> a -> [a] class Bounded a where minBound :: a maxBound :: a

Äsjanimetatud tüübiklasse oskab Haskell uutele tüüpidele ise defineerida, kasutades deriving võtmesõna. Oma andmetüüpidele on seega soovitatav lisada deriving (EqOrdShowRead). Juhul kui teie uuel andmetüübi konstruktoritel pole argumente, on automaatselt tuletatav ka Enum ja Bounded. Näiteks saame kirjutada nii.

*Main> data Weekday = Mon | Tue | Wen | Thur | Fri | Sat | Sun deriving (Eq, Ord, Show, Read, Enum, Bounded) *Main> [Mon .. Fri] [Mon,Tue,Wen,Thur,Fri] *Main> Sat == Sun False *Main> maxBound :: Weekday Sun

3.4  Listikomprehensioon

Matemaatikas kasutatakse mõndade hulkade defineerimiseks hulgakomprehensiooni. Näiteks kõikide paarisarvude hulk a saame defineerida kui

{ 2n  |  n∈ ℕ}

või kõiki ratsionaalarve kui

p/q  |  p,q∈ ℤ, q≠ 0 } .

Sarnane võimalus on (natuke erineva süntaksiga) nii Pythonis, Scalas kui ka Haskellis. Kui võtame täisarvude listiks [0..], siis saame Haskellis paarisarvude listi niimoodi [ 2*n | n <- [0..] ].

Listikomprehensiooni avaldised on kujul a | p1p2pn], kus a on avaldis ja iga pi on kas generaator, tingimus või let-konstruktsioon. Kusjuures avaldises saab kasutada kõiki generaatorite või let-idega defineeritud muutujaid.

Paarisarvude listi avaldis [ 2*n | n <- [0..] ] koosneb generaatorist n <- [0..] ja avaldisest 2*n. Intitisooni kohaselt genereeritakse n-le väärtusi listist [0..] ning iga n puhul luuakse väljastatavasse listi väärtus 2*n.

Mitme generaatori puhul, näiteks avaldises [10*a+b | a <- [1..3], b <- [1..3]], töödeldakse generaatorid nii, et parempoolsed generaatorid muutuvad kiiremini kui vasakpoolsed. S.t tulemus on sellises järjekorras [11,12,13,21,22,23,31,32,33].

Generaatoreid, tingimusi ja let-e töödeldakse listikomprehensioonis vasakult paremal, mistõttu on parempoolse generaatori sees võimalik kasutada vasakul defineeritud muutujaid. Näiteks [10*a+b | a <- [1..3], b <- [1..a]] väärtustub listiks [11,21,22,31,32,33]

Tingimused on lihtsalt Bool tüüp avaldised, mis võivad kasutada tingimusest vasakul defineeritud muutujaid. Kui avaldis osutub vääraks, generaeeritakse järgmine väärtuste komplekt mida tingimusavaldisega testida. Näiteks a | a <- [1..5], even a [2,4]. (Funktsioon even tagastab True parajasti siis, kui argument on paarisarv.)

Listikomprehensioonis saab kasutada let-konstruktusiooni, et vahetulemusi muutujasse salvestada. Näiteks nii

*Main> [ (a,h,b) | a <- [1..5], let h = a `div` 2, b <- [1..h]] [(2,1,1),(3,1,1),(4,2,1),(4,2,2),(5,2,1),(5,2,2)]

Pane Tähele! Pikki listikomprehensiooni avaldisi ei peeta Haskellis heaks tavaks. Sellistel juhtudel tuleks kasutada abifunktsioone avaldiste lühemaks ja selgemaks tegemiseks.

3.5  IO ehk sisend ja väljund

Puhtad Haskelli funktsioonid ei ole teoreetiliselt võimelised lugema ega kirjutama faile, küsima kasutajalt sisendit ega tegema võrgust päringuid. Puhas funktsioon tähendabki seda, et tagastusväärtus on täielikult määratud funktsiooni argumentide poolt.

Kuna aga programmeerijad siiski soovivad nn. mittepuhtaid arvutusi teha, mõeldi selle tarbeks välja IO monaad. Idee seisneb selles, et puhas funktsioon saab kombineerida mittepuhtaid arvutusi (ja lisaks puhtaid arvutusi) omakorda mittepuhtaks arvutuseks.

Potentsiaalselt mittepuhast arvutust kutsume lühidalt protseduuriks. Haskellis on protseduuri tüübiks IO α, kus α on protseduuri poolt tagastatava väärtuse tüüp.

Esmalt vaatamegi paari sisseehitatid protseduuri ning seejärel püüame neid kombineerida. Sõne trükkimiseks ekraanile võime kasutada protseduuri

putStrLn :: String -> IO ().

Tegemist on puhta funktsiooniga, mis saab argumendiks sõne ja tagastab protsetuuri. Seda protseduuri käivitades trükitaksegi argumendina antud sõne konsooli ja tagastatakse väärtus (). Nagu puhtaid programmegi, saab ka protseduure GHCi-s testida.

*Main> putStrLn "Tere, maailm!" Tere, maailm!

Pane Tähele! Kuigi GHCI väljundis ei paista vahe eriti välja, on suur konseptuaalne erinevus, kas sõne trükitakse või ainult tagastatakse. Erinevus paistab väike, kuna GHCi ise trükib tagastatud väärtuse välja. Erandina, IO () tüübiga protseduuri tagastusväärtus () välja ei trükita.

Üks lihtsamaid sisend-protseduure on getLine :: IO String. See protseduur ootab konsoolilt kasutaja sisestust ning enter-klahvile vajutust; tagastatakse sisestatud sõne.

*Main> getLine tere, tere! "tere, tere!"

Kuna sel korral sisestasin klaviatuurilt teretere!, siis see väärtus hiljem ka tagastati.

Näide: Protseduuride omavaheliseks kombineerimiseks kasutame do-süntaksit.
juust :: IO () juust = do putStrLn "Kirjuta oma nimi:" nimi <- getLine putStrLn ("Tere "++nimi++"! Sul on ilus nimi.")

Pane Tähele! Haskellis on taanded väga tähtsad. Do-süntaksis peavad alamprotseduurid olema joondatult üksteise all nii, et avaldiste esimesed tähed on kohakuti ning esimese avaldise esimene täht on rohkem vasakul kui temast välimise struktuuri esimene täht.

Protseduuride definitsiooni alustame do võtmesõnaga, mille järele saab kirjutada kolme liiki lauseid, mis täidetakse järjest ehk ülevalt-alla. Kõige lihtsamal juhul on lauseks mõni protseduur; kusjuures viimane lause peab olema protseduur. Eelnevas näites on esimene ja viimane lause putStrLn funktsiooni rakendus.

Teine liik lauseid on protseduuri tagastusväärtuse sidumine muutujaga — näites rida nimi <- getLine. Defineeritud muutuja on seejärel järgnevate lausete juures kasutatav. Muutujaga sidumist võib muidugi kasutada ka unit tüübi puhul ehk kirjutada hoopis a <- putStrLn "Kirjuta oma nimi:", kuid juba tüübi järgi on teada, et a väärtuseks on ().

Lisaks mittepuhtale arvutusele saab do-süntaksiga kasutada ka eelnevalt õpitud puhast arvutust — selleks saame kasutada let-lauseid.

Näide: Modifitseerime eelnevat protseduuri selliseks, et küsitud nimi teisendatakse standardteegi funktsiooniga toUpper :: Char -> Char läbivalt suurtähti kasutavaks.
import Data.Char vestlus :: IO () vestlus = do putStrLn "Kirjuta oma nimi:" nimi <- getLine let nimiSuur :: String nimiSuur = map toUpper nimi putStrLn ("Tere "++nimiSuur++"!")

Protseduuri tagastusväärtuseks on do-ploki viimane alamprotseduuri tagastusväärtus. Kuna meie näites on viimaseks lauseks putStrLn ""  ::  IO () ehk tagastatakse (), siis ka vestlus :: IO () ehk tagastatakse (). Kuidas aga teha protseduur, mis tagastab midagi mittetriviaalset? Saame kasutada primitiivset protseduuri return :: a -> IO a

Näide: Protseduuri, mis tagastab sisestatud sõne pikkuse) saab kirjutada niimoodi.
kysiRidaTagastaPikkus = do putStrLn "Kirjuta midagi:" tekst <- getLine return (length tekst)

Funktsioon return :: a -> IO a tagastab iga argumenti x korral protseduuri, mille tagastusväärtus on x.

Kokkuvõttes pole protseduuride kirjutamiseks midagi muud konseptuaalset vaja teada peale do-süntaksi ja return funktsiooni. Kõik järgnev mittepuhas programmeerimine on eelnevates peatükkides õpitu rakendamine protseduuride defineerimiseks. Järgnevalt vaatame mõningaid näiteid.

FizzBuzz

Võtame ülesandeks kirjutada programm, mis trükib numbrid ühest kuni 100ni. Kolmega jaguvate arvude asemel kirjutame aga “Fizz” ja viiega jaguvate arvude asemel “Buzz”. Kui arv jagub nii viie kui ka kolmega kirjutame “FizzBuzz”.

Kasutades abifunktsiooni saame kirjutada rekursiooni abil tsükli ning do sees saame kasutada tingimuslauset.

fizzBuzz1 :: IO () fizzBuzz1 = fb 1 where fb n = if n > 100 then return () else do if n`mod`3 == 0 then do putStrLn "Fizz" if n`mod`5 == 0 then putStrLn "Buzz" else return () else if n`mod`5 == 0 then putStrLn "Buzz" else print n fb (n+1)

Pane Tähele! Näites kasutasime esimest do-d et omavahel siduda väljatrükk ja rekursiivne kutse. Sisemine do seob “Fizz” väljatrüki ja tingimusliku “Buzz” väljatrüki.

See kood on üsna ebaselge. Püüame refaktoreerida kasutades valvureid, kuid võit on minimaalne.

fizzBuzz2 :: IO () fizzBuzz2 = fb 1 where fb n | n > 100 = return () | otherwise = do if n`mod`3 == 0 then do putStrLn "Fizz" if n`mod`5 == 0 then putStrLn "Buzz" else return () else if n`mod`5 == 0 then putStrLn "Buzz" else print n fb (n+1)

Järgnevalt püüame eraldada puhta loogika, mittepuhtast arvutusest. Tulemus on järgnev.

fizzBuzz3 :: IO () fizzBuzz3 = fb 1 where fizz n | n`mod`3 == 0 = "Fizz" | otherwise = "" buzz n | n`mod`5 == 0 = "Buzz" | otherwise = "" num n "" = show n num _ xs = xs fb n | n > 100 = return () | otherwise = do putStrLn (num n (fizz n ++ buzz n)) fb (n+1)

Protseduur fizzBuzz3 on juba palju ülevaatlikum — kasutatud on mõnerealisi funktsioone, mis on koheselt silmaga haaratavad. Kasutades kõrgemat järku funktsiooni mapM_, kirjutaks keskmine Haskelli programmeerija tsükli osa hoopis nii.

fizzBuzz4 = mapM_ f [1..100] where f n = putStrLn (num n (fizz n ++ buzz n)) fizz n | n`mod`3 == 0 = "Fizz" | otherwise = "" buzz n | n`mod`5 == 0 = "Buzz" | otherwise = "" num n "" = show n num _ xs = xs

Eelmine Üles Järgmine