Kordamine: liidesed
Liidesed (Haskellis tüübiklassid) on viis kuidas kasutada ad-hoc polümorfismi s.t. erinevatele tüüpidele erinev implementatsioon.
Meile juba tuttav liides Eq
on defineeritud kui:
interface Eq ty where (==) : ty -> ty -> Bool (/=) : ty -> ty -> Bool
kus Eq
on liidese nimi ja ty
liidese parameeter.
Siiani nähtud liidesed on näiteks:
Prelude.Eq : Type -> Type Prelude.Num : Type -> Type Prelude.Fractional : Type -> Type Prelude.Show : Type -> Type
aga ka
Prelude.Foldable : (Type -> Type) -> Type Prelude.Functor : (Type -> Type) -> Type
Liidesed defineerivad tüübile teatud polümorfsed funktsioonid, näiteks:
Prelude.(==) : Eq ty => ty -> ty -> Bool Prelude.(/=) : Eq ty => ty -> ty -> Bool Prelude.show : Show ty => ty -> String Prelude.foldr : Foldable t => (elem -> acc -> acc) -> acc -> t elem -> acc Prelude.map : Functor f => (a -> b) -> f a -> f b
Liidestel Eq
, Num
, Fractional
, Show
on üks parameeter tüübiga Type
, kusjuures liidese parameetri nimi on antud sel juhul lühendiga ty
. Parameeter ty
võib olla ükskõik millist tüüpi. Nende liideste kohta öeldakse, et need on tüübi Type
poolt parametriseeritud.
Küll aga saab liidestel olla mitu parameetrit ning nad võivad olla parametriseeritud ka muu tüübi poolt kui Type
, näiteks Type -> Type
, nagu liidesed Foldable
ja Functor
. Nende parameetrid t
ja f
tähistavad parametriseeritud tüüpe, nagu näiteks List
või Tree
, mis käituvad justkui konteinerid, mis hoiavad mingit tüüpi väärtuseid. Üks meile tuttav parametriseeritud tüüp on ka IO
.
Functor, Applicative ja Monad
Huvilistele: hea blogi Funktoristest, Aplikatiivsetest funktoritest ja Monaadidest. See on Haskelli baasil, aga peaks olema arusaadav ka praeguse Idrise baasi pealt. Lihtsalt peab mäletama, et funktsiooni tüübideklaratsioon on Haskellis ::
, vastupidiselt Idrisele :
.
Prelude.map : Functor f => (a -> b) -> f a -> f b Prelude.pure : Applicative f => a -> f a Prelude.<*> : Applicative f => f (a -> b) -> f a -> f b Prelude.(>>=) : Monad m => m a -> (a -> m b) -> m b
Parameetrid f
ja m
tähistavad parametriseeritud tüüpe.
Funktor
interface Functor (f : Type -> Type) where map : (func : a -> b) -> f a -> f b
Funktorid võimaldavad rakendada funktsiooni geneerilise parametriseeritud tüübi kõigile elementidele. Idrise prelüüdis on defineeritud funktorid kõigile ühe parameetriga (parameetrilistele) tüüpidele, millele on võimalik seda defineerida, näiteks List
, Maybe
ja IO
.
Funktsiooni map
kasutades saame võtta misiganes funktori struktuuri ja transformeerida kõik selle funktori elemendid, tagastades uue objekti, mis on täpselt sama struktuuriga, kuid erinevate elementidega. Uued elemendid saavad olla kas sama või hoopis teist tüüpi.
Näiteks:
Idris> map (*2) [1,2,3,4] [2, 4, 6, 8] : List Integer
Seda ideed saame kasutada mitmete erinevate andmestruktuuridega, kuid see töötab vaid puhaste funktsioonide korral. Kui me tahame rakendada igale elemendile mingit funktsiooni, mis ei ole puhas, s.t. vajab monaadilist efekti, ei saa me kasutada map
funktsiooni. Selleks vajame aplikatiivset funktorit.
Applikatiivsed funktorid
interface Functor f => Applicative (f : Type -> Type) where pure : a -> f a (<*>) : f (a -> b) -> f a -> f b
Idrise puhtad funktsioonid ei võimalda teha mittepuhtaid arvutusi. Funktsiooniga pure
saame konteinerisse väärtuseid panna. Ning, funktsiooniga (<*>)
(tuntud ka kui "apply"), kui meil on konteiner funktsiooniga ja konteiner argumendiga, peame saama konteineri funktsiooni tulemusega.
Liides Traverse
Funktsioon traverse
map-ib kõik struktuuri elemendid avaldisteks, väärtustab need avaldised ning kombineerib saadud tulemused.
interface (Functor t, Foldable t) => Traversable t where traverse : Applicative f => (a -> f b) -> t a -> f (t b)
Funktsioon traverse
võtab kaks argumenti:
- Funktsioon, mis transformeerib objekti kasutades mõnda efekti (aplikatiivset või monaadilist)
- Konteiner nendest objektidest (ja see konteiner on nii funktor kui foldable)
ja tagastab uue konteineri, kus kõigile elementidele on rakendatud transformatsiooni, ning see konteiner on omakorda rakendatud efekti sees.
Intuitsioon: tegemist on for-tsükliga.
readMaybe : IO (Maybe Int) readMaybe = do input <- getLine if all isDigit (unpack input) then pure (Just (cast input)) else pure Nothing loeArv : IO Int loeArv = do putStrLn "Sisesta arv:" v <- readMaybe case v of Nothing => do putStrLn "Viga! Ei tunne sellist arvu!" loeArv Just n => pure n summaN2 : IO () -- Variant 1 (traverse) summaN2 = do putStr "Liidetavaid. " n <- loeArv xs <- traverse (const loeArv) [1..n] printLn (sum xs)
Liides Sequence
Funktsioon sequence
väärtustab kõik avaldised struktuuris ning kogub tulemused kokku.
sequence : Applicative f => Traversable t => t (f a) -> f (t a) sequence = traverse id
summaN2 : IO () -- Variant 2 (sequence) summaN2 = do putStr "Liidetavaid. " n <- loeArv xs <- sequence (map (\a => loeArv) [1 .. n]) printLn (sum xs)
Monaadid
Monaadid võimaldavad teha järjestikulisi toiminguid sõltuvalt eelneva toimingu tulemusest. Intuitsioon: järjestikku täidetavad arvutusmasinad.
Kõik monaadid peavad olema funktorid. S.t. neil on funktsioon map
. Lisaks peavad monaadid olema aplikatiivsed funktorid. S.t. neil on ka funktsioonid pure
ja (<*>)
. Ning monaad pole mitte ainult aplikatiivne funktor, vaid tal peab olema ka >>=
operaator.
interface Applicative m => Monad m where (>>=) : m a -> (a -> m b) -> m b
Ülesanne 1: Kivi-paber-käärid mäng
Implementeeri kivi-paber-käärid mäng arvuti vastu.
Lisa faili algusesse impordid System.Random
, Data.Vect
, Data.String
Random valiku tegemiseks kasuta funktsiooni rndSelect'
.
data Valik = Kivi | Paber | Käärid valik : Vect 3 Valik valik = [Kivi, Paber, Käärid] Show Valik where show Kivi = "kivi" show Paber = "paber" show Käärid = "käärid" Eq Valik where x == y = ?rhs1 loeValik : IO Valik loeValik = do putStr "Sisesta: kivi, paber või käärid: " v <- getLine case (Data.String.toLower v) of "kivi" => pure Kivi "paber" => pure Paber "käärid" => pure Käärid _ => putStr "Ei tea sellist valikut, proovi uuesti. " >> loeValik kpk : IO () kpk = ?rhs2
Näiteks:
K8a> :exec kpk Sisesta: kivi, paber või käärid: käärid Viik. Proovi uuesti. Sisesta: kivi, paber või käärid: paber Sina võitsid paber vs kivi K8a> :exec kpk Sisesta: kivi, paber või käärid: käärid Kaotasid :( käärid vs kivi
Ülesanne 2: Maybe
monaad
Olgu antud järgmine avaldispuude andmestruktuur:
data Expr = Const Int | Add Expr Expr | Div Expr Expr expr1 : Expr expr1 = Div (Add (Const 3) (Const 1)) (Const 2) expr2 : Expr expr2 = Add (Const 1) (Div (Const 1) (Add (Const 1) (Const (-1))))
Kasutada do
-notatsiooni, kirjuta funktsioon, mis väärtustab avaldispuu või tagastab Nothing
kui selles tekib nulliga jagamine:
evalExpr : Expr -> Maybe Int evalExpr e = ?rhs_evalExpr
Näiteks:
evalExpr expr1 == Just 2 evalExpr expr2 == Nothing
NB! Jagamiseks kasuta funktsiooni div
mitte /
.