Tämän viikon harjoituksissa on tarkoitus oppia Haskellin syntaksi päällisin puolin. Lisäksi opiskelemme Haskellin tyyppijärjestelmää niin paljon, että pystymme tulkitsemaan suuren osan kääntäjän virheilmoituksista ja ennakoimaan millaisia tyyppejä lausekkeilla on.
Näissä tehtävissä esitetyt asiat löytyvät seuraavista lähteistä:
Hyvä strategia tehtävien suorittamiseen on lukea ensin LYAH kevyesti läpi ja tehdä tehtäviä palaten materiaaliin siinä vaiheessa kun tiedot loppuvat. Jos jokin asia jää täysin mysteeriksi, siitä pitää kysyä. Kysymykset lähetetään tiistai-aamuksi luennoijalle (ville.tirronen@jyu.fi), jolloin niihin ehditään saada kunnolliset vastaukset tiistain harjoituksissa.
Tehtävien tekemistä varten joudut noutamaan Haskell-ympäristön … Tutustu erityisesti GHCi -tulkkiin, jolla voit testata Haskell lausekkeita.
Kaikki tehtävät tulee olla tehtynä ja palautettuna torstaina kello 12. Mutta ei hätää, vaikka näitä on paljon, niin ne ovat helppoja ja niitä saa (ja tulee) tehdä porukalla!
Tässä kappaleessa tarkastelemme, mitkä ovat Haskell-kielen yleiset rakenteet ja miten ne eroavat muiden, tutujen kielten vastaavista. Syntaksin opetteluun voidaan käyttää aluksi GHCi-tulkkia. Tulkkiin voit sekä kirjoittaa lausekkeen, esimerkiksi 1 + 1 tai siltä voit kysyä antamasi lausekkeen tyyppiä: :t 1+1. Huomaa kuitenkin, että et voi määritellä arvoja1 tai tyyppejä tulkissa.
Kokeile seuraavia rakenteita GHCi-tulkissa ja kuvaile esimerkkien kanssa mitä ne tekevät. (Kokeile siis esimerkin lisäksi myös muita vastaavia lausekkeita)
even 8(1,'a') ja ("kissa","minttu",12)[1..10][x^2 | x <- [1..20], even x]caseifletKutsu seuraavia funktiota tulkissa. Selvitä niiden tyyppi tulkin avulla ja kerro kuinka monta parametria kullekin funktiolle on annettava.
(+), (-), (/), mod(:), head, tail(++)(+1), (2*), (/3), (3/)zip, concat, take, drop, null, lengthfst, sndsum(.) (Testaa vaikkapa d-kohdan arvoilla)Just, Nothing, Left, RightHuomaa, että tuttujen + ja - operaattoreiden ympärillä on sulut. Mitä se tarkoittaa?
Edellisen tehtävän viimeisessä kohdassa on kirjoitettu vakioiden nimiä isolla alkukirjaimella. Yleensähän vain tyypit kirjoitetaan isolla. Mistä on kysymys?
Tietotyypit poikkeavat haskellissa varsin paljon olio-ohjelmoinnin tietyypeistä. Esimerkiksi lista määritellään haskellissa näin:
> data MyList a = Cons a (MyList a)
> | EmptyList
Tietysti, konstruktori Cons vastaa edellisen tehtävän : operaattoria ja EmptyList merkintää []. Itse lista [1,2,3,4] on vain “syntaktista sokeria” määritelmälle 1:2:3:4:[]. Huomaa, että ylläoleva määrittely on rekursiivinen: lista voi olla Cons elementti, jossa on yksi alkio ja toinen Lista. Koska haskell käyttää laiskaa laskentaa, rekursion ei tarvitse sinänsä päättyö koskaan. Voimme määritellä äärettömiä tietorakenteita ilman huolta. Itseasiassa, varuskirjaston ja edellisten demojen lista on yksi tälläinen:
> data MyList a = Cons a (MyList a)
> | EmptyList
Nimeä seuraavat osat yo. määritelmästä ja selvitä mitä ne ovat
a|Cons ja EmptyListMäärittele seuraavat tietotyypit a) Kelmien kerhon kelmi, jolla on nimi ja useita harrastuksia. c) Sellainen tietotyyppi, jonka palauttaisit funktiosta, joka saattaa epäonnistua tietyissä virhetilanteissa. b) Takametsän asukas, joka saattaa olla kelmi tai possu. c) Binääripuu.
Haskellissa operaattorista (esim. +) voi tehdä funktion ja funktiosta (esim. mod) operaattorin. Miten?
ifHaskell kielessä on tuttu if rakenne:
> eval:
> if 5>7 then "Oho!" else "Fiksua!"
Haskellin if eroaa kuitenkin Javan iffistä olennaisella tavalla, miten? Miksi esimerkiksi seuraava ohjelma ei toimi:
> eval:
> if (5>7) then "Oho!" else True
Mitä voit kuitenkin tehdä Haskellin if:llä, mitä et voi tehdä Javan if:llä? (Vinkki: minne sen saa kirjoittaa?) Anna esimerkki.
let ja whereRakenteilla let ja where voidaan sitoa muuttujille arvoja.
where ja let rakenteilla on? (Vinkki: ed. tehtävä)Määrittele Haskellissa seuraavat vakiot käyttäen hahmonsovitusta (Pattern Matching):
> and' :: Bool -> Bool -> Bool
> or' :: Bool -> Bool -> Bool
> not' :: Bool -> Bool
> implication' :: Bool -> Bool
> lift' :: (a -> b) -> ([a] -> [b]) -- Vihje: mitä tämäntyyppinen yksinkertainen mutta hyödyllinen funktio mahtaisi tehdä?
Määritelmän heittomerkki tulee siitä, että Haskellin standardikirjasto toteuttaa muuten samannimisiä funktioita.
"Juliaanisessa kalenterissa karkausvuosi oli
poikkeuksetta joka neljäs vuosi[1], vuosiluvun ollessa jaollinen neljällä.
Gregoriaanisessa kalenterissa tästä on se poikkeus, että täydet vuosisadat (eli
sadalla jaolliset vuodet) eivät ole karkausvuosia muulloin kuin joka 400. vuosi
(eli vuosi on jaollinen 400:lla). Esimerkiksi vuodet 1700, 1800 ja 1900 eivät
olleet karkausvuosia, mutta 2000 oli." -- *Wikipedia*
Tee funktio, joka kertoo onko annettua numeroa vastaava vuosiluku karkausvuosi vai ei
if-lausekkeitaquard:ejacase-lauseketta ja quard:ejaOhjelmien suluttaminen on hieman sama asia kuin tavuttaminen ala-asteella. Käsittämättömän tylsää ja turhantuntuista, mutta pakko osata, ellei sitten aio lisp-ohjelmoijaksi.
Lisää kaikki sulut mitä voit seuraaviin lausekkeisiin. Korvaa myös . ja $ operaattorit suluilla määritelmiensä ((f . g) x = f (g x) ja a $ b = a b) mukaisesti.
Lisää sulut seuraaviin tyyppilausekkeisiin
aa -> Maybe aa -> a -> ba -> a -> b -> Int (Joo, en keksinyt useampaa olennaista tapausta)Poista tarpeettomat sulut seuraavista tyyppilausekkeista
(a -> (a -> b))((a->a) -> b)(Maybe (a-> (a->b))) -> (a -> (a))Eräs poikkeuksellisen tärkeä ominaisuus Haskellissa on viite-eheys, eli Referential Transparency. Mitä sillä tarkoitetaan? (Vinkki. Kurssilla voi käyttää wikipediaa ja googlea).
Edellisen tehtävän nojalla voidaan havaita, että pöytätestaushan on kertaluokkaa helpompaa kuin Javalla tai C:llä. Tästä havainnosta innostuneena pöytätestaammekin heti pari Haskell ohjelmaa! Olkoon seuraavat määritelmät annettuja:
> doubleIt x = 2*x
> (f . g) x = f (g x)
> both op (a,b) = (op a, op b)
> fst (x,_) = x
> snd (_,x) = x
>
> fromMaybe a (Just x) = x
> fromMaybe a Nothing = a
Esimerkiksi fst (both (doubleIt . doubleIt) (1,2)) sievenisi näin:
> fst (both (doubleIt . doubleIt) (1,2))
> == {- 1. both:n määritelmä -}
> fst ((doubleIt . doubleIt) 1, (doubleIt . doubleIt) 2)
> == {- 2. fst:n määritelmä -}
> (doubleIt . doubleIt) 1
> == {- 3. (.) määritelmä -}
> doubleIt (doubleIt 1)
> == {- 4. doubleIt määritelmä -}
> 2 * (doubleIt 1)
> == {- 5. doubleIt määritelmä -}
> 2 * (2 * 1)
> == {- 6. kertolasku -}
> 2 * 2
> == {- 7. kertolasku -}
> 4
fst ennen lauseketta (doubleIt . doubleIt) 1. Miksi näin? Mitä seurauksia tälläisellä laskutavalla on?fst (both (1/) (10,0))(fst . snd . fst) ((1, (2, 3)), 4)Tyypit ovat ehkäpä tärkein asia, mikä aloittelevan Haskell-ohjelmoijan tulee tuntea. Erityisesti siksi, että aina kun jotain menee pieleen, edessä on nippu tyyppejä, jotka kertovat mikä meni pieleen. Tyyppivirheet ovat aluksi hankalan tuntuisia ja rasittavia, mutta tämä johtuu vain siitä, että emme vielä osaa lukea niitä kunnolla. Myöhemmin tyypit ovat Haskell ohjelmoijalle suureksi avuksi!
Haskellissa kaksi parametrinen funktio määritellään usein seuraavanlaisella tyypillä String -> Int -> String. Saman asian voisi määritellä myös tyypillä (String,Int) -> String, joka näyttää paljon tutummalle.
curry ja uncurry. Niillä on kovin hankalt tyypit, mutta edellisen perusteella, mitä ne tekevät?Arvaa millaiset tyypit seuraavilla funktioilla olisi Haskellissa:
> muutaMerkkijonoIsoiksiKirjaimiksi :: ?
> merkkijononPituus :: ?
> merkkijonoSanoiksi :: ?
> listanAlkioN :: ?
> listanSumma :: ?
> kelminHarrastuksetTiedot :: ?
> mitaKerhossaHarrastetaan :: ?
GHCi tulkissa voidaan tarkastella muuttujan tai lausekkeen tyyppiä komennolla :t <lauseke>. Esimerkiksi näin:
> ghci> :t (==)
Olkoon annettu seuraavat tyypit:
> (>>=) :: Monad m => m a -> (a -> m b) -> m b
> (.) :: (b -> c) -> (a -> b) -> a -> c
> getLine :: IO String
> print :: Show a => a -> IO ()
> reverse :: [a] -> [a]
Mikä on seuraavien lausekkeiden (yleisin mahdollinen) tyyppi? Älä käytä GHCi:tä.
> a = x y z
> b = (\x -> x) 3
> c = \x -> \y -> False
> d = getLine >>= print
> h = print . reverse
> g = (.) . (.) -- Tämä on ihan tarkoituksella hankala.
Kurssilla pyritään järjestämään viikottaisia miniprojekteja, joissa käytetään demotehtävien tietoja ja tuotetaan jokin pieni ohjelma tai kirjasto.
Gloss-piirroskirjasto ei liene paras Haskell piirrostyökalu, mutta se on hyvin yksinkertainen. Gloss koostuu olennaisesti seuraavista funktioista:
> animateInWindow :: String -> (Int, Int) -> (Int, Int) -> Color -> (Float -> Picture) -> IO ()
> blank :: Picture
> polygon :: Path -> Picture
> line :: Path -> Picture
> circle :: Float -> Picture
> text :: String -> Picture
> color :: Color -> Picture -> Picture
> translate :: Float -> Float -> Picture -> Picture
> rotate :: Float -> Picture -> Picture
> scale :: Float -> Float -> Picture -> Picture
> pictures :: [Picture] -> Picture
Näistä ensimmäinen, animateInWindow tuottaa, viisi parametria saatuaa IO ()-tyyppisen arvon. Käännettäessä Haskell ohjelmia, main on nimenomaan tätä tyyppiä:
> import Graphics.Gloss
> main = animateInWindow
> "Ympyra"
> (500, 650)
> (20, 20)
> black
> piirros
>
> piirros aika = color green (circle (30*aika))
Paitsi käyttämällä erityistä, tulkissa toimivaa syntaksia let kaksi = 1 + 1. ↩