wiki:haskell
Last modified 3 years ago Last modified on 2014-01-21 07:41:49

Kurssin esimerkkiohjelmia Haskell-kielellä tehtynä

Tällä sivulla on esimerkkinä kurssin malliohjelmia ja demojen mallivastauksia Haskell-kielellä tehtynä. Tämän sivun tarkoitus ei ole niinkään opettaa Haskellia vaan tarjota kurssin opiskelijoille näkemys siitä, että Javan ja C#:n tapainen tapa ohjelmoida ei ole suinkaan se ainoa, saati kaikissa tilanteissa paras, tapa luoda tietokone-ohjelmia.

Alla olevat ohjelmat ovat kirjoitettu 'literate haskell' moodissa, jossa kommentoinnin ja koodin merkitys on käännetty. Rivit, jotka alkavat merkilla > ovat koodia ja kaikki muu kommenttia. Toisinsanoen, voit leikkaa-liimata ohjelmat suoraan wikisivulta sopivaan tiedostoon ja kääntää (ghc --make foo.hs) tai suorittaa ne tulkissa (ghci foo.hs) ilman muutoksia.

Yleistä Haskellista

Haskell on puhdas, laiska ja funktionaalinen kieli. Käytännössä tämä tarkoittaa sitä, että ohjelmat mallinnetaan (matemaattisina) funktioina, joilla ei ole sivuvaikutuksia. Toisinsanoen, ohjelmissa ei ole muuttuvia muuttujia, silmukoita tai mitään erityistä laskujärjestystä, ellei niitä erikseen rakenneta ohjelmakirjastoina.

Lisäksi Haskellia kuvaa poikkeuksellisen tehokas tyyppijärjestelmä. Samoin kuin Javassa ja C#:ssa, jokaisella funktiolla ja vakiolla on tietotyyppinsä. Toisin kuin edellä mainituissa kielissä, Haskell pystyy suurimmaksi osaksi itse päättelemään tyypit, joten ohjelmoijan ei usein tarvitse kirjoittaa niitä näkyviin, paitsi tietysti dokumentoidakseen ohjelmansa toimintaa. Lisäksi tyyppijärjestelmä sallii huomattavasti kuvailevammat ja vapaammat tyypit kuin C# tai Java. On jopa mahdollista määritellä tyyppejä, kuten 'funktio N:n mittaiselta listalta N+1:n mittaiseksi listaksi'.

Lisäksi Haskell koodia lukiessa törmää myös usein varsin lyhyisiin muuttujan nimiin. Tämä ei kuitenkaan ole ristiriidassa Vesan ohjeiden kanssa -- kullakin kielellä on oma tapansa kirjoittaa. Java, Lisp ja Scheme suosivat pitkiä muuttujan nimiä, kun taas Haskell, O'caml ja ML suosivat lyhyitä nimiä, joita käytetään lokaaleissa määritelmissä. Osin tämä johtuu edellä mainitusta tyyppijärjestelmästä. Muuttujan tyyppi kertoo usein paremmin sen toiminnasta, kuin pitkäkin nimi.

Paras tapa aloittaa Haskell ohjelmointi on ladata Haskell Platform-asennusohjelma, jonka mukana tulee olennaisimmat kirjastot ja kääntäjä.

Hello World

Ensin se pakollinen hello world esimerkki:

>module Main where
>main = putStrLn "Terve maailma"

Tämän voi joko ajaa tulkissa:

bash$ ghci
...
*Main> :main
Terve maailma

Tai kääntää omaksi ohjelmakseen:

bash$ ghc --make Hello.lhs
[1 of 1] Compiling Main             ( Hello.lhs, Hello.o )
Linking Hello ...
bash$ ./Hello 
Terve maailma

Luvut esiintymisjärjestyksessä

Vesan tehtävä: Tee funktio, joka järjästää annetut luvut niiden esiintymien mukaiseen järjestykseen. Esim.

freqSort [1, 2, 3, 34, 34, 2, 1, 34, 1, 1, 1] == [3, 2, 34, 1]

Itse ohjelma kirjoitetaan yhdistelemällä olemassa olevia funktiota näin:

freqSort = map head                                      -- Otetaan kaikista listoista ekat alkiot ja saadaan: [3,2,34,1] 
                   . sortBy (compare `on` length) -- Järjestetään lukumäärän mukaan, saadaan: [[3],[2,2],[34,34,34],[1,1,1,1,1]]
                   . group                                           -- Ryhmitellään peräkkäiset ja saadaan [[1,1,1,1,1],[2,2],[3],[34,34,34]]
                   . sort                                               -- Ensin järjestetään alkiot ja saadaan [1,1,1,1,1,2,2,3,34,34,34]

Huomaa, että ohjelma luetaan alhaalta ylös ja . tarkoittaa samaa kuin pallo matematiikassa, eli yhdistettyä funktiota. Huomaa myös, että tämä ohjelma on todennäköisesti melko hidas ja sen oikea käyttötarkoitus on, että sitä käytetään varsinaisen nopeamman ohjelman (ks.alla) toimivaksi osoittamiseen.

Todennäköisyystehtävä

>module Pallot where

Tämä Haskell-moduli ratkaisee Vesan pallo-todennäköisyystehtävän.

Ladataan listojen erotusoperaattori Data.List -modulista.

>import Data.List ((\\)) 

Palloja on neljää eri väriä. Ne voidaan tulostaa (Show) ja niiden yhtäsuuruutta voidaan verrata (Eq) ja luetella (Enum)

>data Ball = Red | Green | Blue deriving (Show,Eq,Enum)

Lista joka sisältää kaikki kombinaatiot pallotehtävälle.1

>kombinaatiot = do
>    a <- [Red .. Blue] 
>    b <- [Red .. Blue] 
>    c <- [Red .. Blue] 
>    d <- [Red .. Blue] 
 
>    x <- [Red .. Blue]
>    y <- [Red .. Blue]
>    return ([a,b,c,d],[x,y])

Pelin voittaa, jos kaikki toisessa listassa olevat pallot löytyvät ensimmäisestä listasta:

>voittaa (hand,draw) = length (hand \\ draw) == (length hand - length draw) 

Annetun esimerkkitapauksen todennäköisyys saadaan laskemalla missä suhteessa True alkiota esiintyy kombinaatiot-listassa:

>tn = ratio voittaa kombinaatiot

Lasketaan kuinka monta kertaa predikaatti p on tosi annetulle listalle:

>count p = length . filter p

Lasketaan missä suhteessa predikaatin p mukaisia alkoita on listassa.2

>ratio p xs = fromIntegral (count p xs) / fromIntegral (length xs)

Käyttö: Aja tämä ohjelma ghci-tulkissa. Kirjoita tn jos haluat todennäköisyyden ja kombinaatiot, jos haluat nähdä kaikki mahdolliset tapaukset mitä Veskun pallopelissä voi esiintyä. Muita hauskoja komentoja ovat filter voittaa kombinaatiot, joka näyttää voittokombinaatiot tai mapM_ print kombinaatiot, joka tulostaa kombinaatiot helpommin luettavassa muodossa.


1: Haskellin syntaksiin tottunut ohjelmoija saattaisi kirjoittaa tämän lyhyemmin:

import Control.Applicative
kombinaatiot2 = (,) <$> draw 4 <*> draw 2
 where draw n = replicateM n [Red .. Blue]

2: edellä määritelty ratio funktio on siitä huono, että se käy listan läpi kahteen kertaan. Tämä on vielä huonompi juttu kuin vastaava tilanne Javassa, sillä mikäli kävisimme listan läpi vain yhteen kertaan, sitä ei varattaisi kokonaan muistiin missään vaiheessa! Tämän voisi korjata esimerkiksi näin:

ratio p xs = fromIntegral good / fromIntegral count
 where 
  (count,good) = foldr (\x (cnt,gd) -> if p x then (cnt+1,gd+1) else (cnt+1,gd))
		       (0,0) xs

Todennäköisyystehtävä kokonaisluvuilla

import Data.List
import Control.Monad
import Data.Ratio

test as bs colors = do
    hand  <- replicateM as [1..colors]
    raise <- replicateM bs [1..colors]
    return $ length (hand \\ raise) == as - bs

tn :: Rational
tn = let combinations = test 4 2 3 
     in fromIntegral (length . filter id $ combinations) 
    / fromIntegral (length combinations) 

Lumiukko

Piirretäänpä lumiukko Haskell kielellä. Koska JY-peli kirjasto, jota Vesa käyttää kursseillaan, ei ole käytettävissä muista kielistä, joudumme korvaamaan sen Gloss nimisellä kirjastolla. Toisin kuin JY-peli, Gloss on tarkoitettu yksinkertaiseen ja nopeaan grafiikan piirtoon, eikä se sisällä esimerkiksi fysiikan mallinnusta.

Ensin määritellään Main-moduli ja otetaan Gloss käyttöön.

>module Main where
>import Graphics.Gloss

Seuraavaksi käsketään Gloss piirtämään grafiikka nimeltä lumiukko pieneen, siniseen ikkunaan.

>main = display (InWindow "Lumiukko" (400, 400) (50, 50)) blue lumiukko

Lumiukko puolestaan on valkoinen ja koostuu kahdesta pallosta ja niiden päällä olevasta naamasta (face), jotka on siirretty sopiviin kohtiin ruutua.

>lumiukko = color white $
>            pictures [circleSolid 50
>                     ,translate 0 77  $ circleSolid 38 
>                     ,translate 0 120 $ face]

Naama puolestaan koostuu valkeasta pallosta ja kahdesta mustasta ja oranssista pallosta sopivissa kohdin.

>face = pictures [color white $ circleSolid 26
>                ,color black $ translate 10 10 $ circleSolid 8
>                ,color black $ translate (-10) 10 $ circleSolid 8 
>                ,color orange $ translate 0 5 $ circleSolid 8 
>                ]

Lopputulos näyttää tältä

Attachments