5.2.5 Konstruktoriluokat Edellisessä esimerkissä määrittelimme oman tyyppiluokan Isqrt jonka jäsenet olivat tyyppejä (kuten Int, Integer, Word,...).

Samankaltaiset tiedostot
Tyyppiluokat II konstruktoriluokat, funktionaaliset riippuvuudet. TIES341 Funktio-ohjelmointi 2 Kevät 2006

Algebralliset tietotyypit ym. TIEA341 Funktio ohjelmointi 1 Syksy 2005

TIEA341 Funktio-ohjelmointi 1, kevät 2008

tään painetussa ja käsin kirjoitetussa materiaalissa usein pienillä kreikkalaisilla

Tyyppejä ja vähän muutakin. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Laajennetaan vielä Ydin-Haskellia ymmärtämään vakiomäärittelyt. Määrittely on muotoa

6 Algebralliset tietotyypit

Abstraktit tietotyypit. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Ohjelmoinnin perusteet Y Python

TIEA341 Funktio-ohjelmointi 1, kevät 2008

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

TIEA341 Funktio-ohjelmointi 1, kevät 2008

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Ohjelmoinnin perusteet Y Python

Ohjelmoinnin perusteet Y Python

Ohjelmoinnin perusteet Y Python

IDL - proseduurit. ATK tähtitieteessä. IDL - proseduurit

Ohjelmoinnin perusteet Y Python

Luku 3. Listankäsittelyä. 3.1 Listat

ATK tähtitieteessä. Osa 3 - IDL proseduurit ja rakenteet. 18. syyskuuta 2014

Luku 4. Tietorakenteet funktio-ohjelmoinnissa. 4.1 Äärelliset kuvaukset

Ohjelmoinnin perusteet Y Python

AS C-ohjelmoinnin peruskurssi 2013: C-kieli käytännössä ja erot Pythoniin

Monadeja siellä, monadeja täällä... monadeja kaikkialla? TIES341 Funktio ohjelmointi 2 Kevät 2006

Sisällys. 12. Näppäimistöltä lukeminen. Yleistä. Yleistä

815338A Ohjelmointikielten periaatteet Harjoitus 7 Vastaukset

815338A Ohjelmointikielten periaatteet Harjoitus 2 vastaukset

TIEA341 Funktio-ohjelmointi 1, kevät 2008

8. Näppäimistöltä lukeminen 8.1

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

Haskell ohjelmointikielen tyyppijärjestelmä

Ohjelmoinnin perusteet Y Python

5.5 Jäsenninkombinaattoreista

Ohjelmoinnin perusteet Y Python

Ohjelmoinnin jatkokurssi, kurssikoe

ELM GROUP 04. Teemu Laakso Henrik Talarmo

main :: IO () main = interact (concatmap ((++"\n"). reverse). lines)

12. Näppäimistöltä lukeminen 12.1

8. Näppäimistöltä lukeminen 8.1

15. Ohjelmoinnin tekniikkaa 15.1

Sisällys. 1. Omat operaatiot. Yleistä operaatioista. Yleistä operaatioista

1. Omat operaatiot 1.1

Ohjelmoinnin perusteet Y Python

Ohjelmassa henkilön etunimi ja sukunimi luetaan kahteen muuttujaan seuraavasti:

Ohjelmoinnin perusteet Y Python

Tietojen syöttäminen ohjelmalle. Tietojen syöttäminen ohjelmalle Scanner-luokan avulla

Ohjelmoinnin perusteet Y Python

Lisää pysähtymisaiheisia ongelmia

Esimerkki: Laskin (alkua) TIEA341 Funktio ohjelmointi 1 Syksy 2005

Tämän vuoksi kannattaa ottaa käytännöksi aina kirjoittaa uuden funktion tyyppi näkyviin, ennen kuin alkaa sen määritemää kirjoittamaan.

Ohjelmoinnin perusteet, syksy 2006

Harjoitus 5 (viikko 48)

TIEA341 Funktio-ohjelmointi 1, kevät 2008

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

Harjoitustyö: virtuaalikone

Rekursiolause. Laskennan teorian opintopiiri. Sebastian Björkqvist. 23. helmikuuta Tiivistelmä

Java-kielen perusteet

15. Ohjelmoinnin tekniikkaa 15.1

Ohjelmoinnin perusteet Y Python

Se mistä tilasta aloitetaan, merkitään tyhjästä tulevalla nuolella. Yllä olevassa esimerkissä aloitustila on A.

A TIETORAKENTEET JA ALGORITMIT

Luku 5. Monadit. 5.1 Siirrännän ongelma

5.1 Tyyppiparametrit. Nyt lisäämme parametrit myös data-määrittelyihin: data Nimi tp 1 tp 2 tp 3... tp k =...

5.3 Laskimen muunnelmia 5.3. LASKIMEN MUUNNELMIA 57

Ohjelmoinnin peruskurssi Y1

koska sellainen vaaditaan jotta oma tyyppimme Tree k t pääsee jäseneksi Luokkaan Eq.

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Algoritmit 1. Luento 3 Ti Timo Männikkö

Ohjelmoinnin peruskurssien laaja oppimäärä

Tutoriaaliläsnäoloista

Pythonin Kertaus. Cse-a1130. Tietotekniikka Sovelluksissa. Versio 0.01b

Hakemistojen sisällöt säilötään linkitetyille listalle.

Ohjelmoinnin perusteet Y Python

Harjoitus 2 (viikko 45)

Ohjelmoinnin peruskurssien laaja oppimäärä

TIEA241 Automaatit ja kieliopit, kevät 2011 (IV) Antti-Juhani Kaijanaho. 19. tammikuuta 2012

Pythonin alkeet Syksy 2010 Pythonin perusteet: Ohjelmointi, skriptaus ja Python

Tietueet. Tietueiden määrittely

Geneeriset tyypit. TIES542 Ohjelmointikielten periaatteet, kevät Antti-Juhani Kaijanaho. Jyväskylän yliopisto Tietotekniikan laitos

5. HelloWorld-ohjelma 5.1

TIETORAKENTEET JA ALGORITMIT

Ohjelmoinnin peruskurssi Y1

811120P Diskreetit rakenteet

Tietorakenteet ja algoritmit

Turingin koneen laajennuksia

Sisällys. JAVA-OHJELMOINTI Osa 7: Abstrakti luokka ja rajapinta. Abstraktin luokan idea. Abstrakti luokka ja metodi. Esimerkki

Tietorakenteet ja algoritmit - syksy

Lyhyt kertaus osoittimista

Ohjelmoinnin perusteet Y Python

2. Lisää Java-ohjelmoinnin alkeita. Muuttuja ja viittausmuuttuja (1/4) Muuttuja ja viittausmuuttuja (2/4)

Ohjelmoinnin peruskurssi Y1

Sisällys. 18. Abstraktit tietotyypit. Johdanto. Johdanto

Ohjelmoinnin perusteet Y Python

T Syksy 2004 Logiikka tietotekniikassa: perusteet Laskuharjoitus 7 (opetusmoniste, kappaleet )

Ohjelmoinnin perusteet Y Python

5/20: Algoritmirakenteita III

Funktionimien kuormitus. TIES341 Funktio ohjelmointi 2 Kevät 2006

14. Poikkeukset 14.1

7. Näytölle tulostaminen 7.1

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Transkriptio:

5.2.5 Konstruktoriluokat Edellisessä esimerkissä määrittelimme oman tyyppiluokan Isqrt jonka jäsenet olivat tyyppejä (kuten Int, Integer, Word,...). Voimme määritellä tyyppiluokkia myös tyyppikonstruktoreille eli niille joilla on tyyppiparametreja. Sellaista luokkaa kutsutaan konstruktoriluokaksi (constructor class). Esimerkiksi hakupuumme ovat yksi esimerkki tietorakenteen sanakirja toteutuksesta. Millainen olisi yleensä luokka sellaiset s joita voi käyttää sanakirjana? Siis tämä s on nyt tyyppikonstruktori, jolla on kaksi tyyppiargumenttia: s k t on tyyppi, jossa k on sanakirjan avainten ja t kuhunkin avaimeen liittyvän tieokentän tyyppi. Tällöin määrittelemme tyyppiluokan jonka tyyppiparametrina on tämä s ilman parametrejaan metodeina on ne operaatiot, jotka haluamme sanakirjalla s k t olevan. Näiden metodien tyypeissä tällä s on siis ne kaksi parametriaan, ja täällä ilmaisemme niiden rajoitteet. Voimme vaatia sanakirjalta seuraavat operaatiot: luonti listasta (avain,tieto)-pareja metodilla fromlist muunnos takaisin tällaiseksi listaksi metodilla intolist haku annetulla avain arvolla metodilla lookfrom. Laitamme luonti- ja hakumetodien tyyppeihin rajoitteen että avain tyypin pitää olla järjestetty. Sellaisen yksi mahdollinen instanssi on siis s = Puumme: Meillä on sopiva toteutus jokaisellä näistä kolmesta metodista. import Data.List class Dictionary s where lookfrom :: (Ord k) => k -> s k t -> Maybe t fromlist :: (Ord k) => [(k,t)] -> s k t intolist :: s k t -> [(k,t)] {- Hakupuumme ovat yksi sanakirjatyyppi. -} instance Dictionary Puu where 86

fromlist = listasta intolist = listaksi lookfrom = undefined -- harjoitustehtävä data Puu k t = Tyhja Solmu (Puu k t) k t (Puu k t) deriving (Show) lisaa :: (Ord k) => k -> t -> Puu k t -> Puu k t lisaa x y Tyhja = Solmu Tyhja x y Tyhja lisaa x y haara@(solmu vasen avain tieto oikea) x < avain = Solmu (lisaa x y vasen) avain tieto oikea x > avain = Solmu vasen avain tieto (lisaa x y oikea) otherwise = haara listasta :: (Ord k) => [(k,t)] -> Puu k t listasta = foldr (uncurry lisaa) Tyhja listaksi :: Puu k t -> [(k,t)] listaksi = let listaksia Tyhja a = a listaksia (Solmu vasen avain tieto oikea) a = listaksia vasen $ (avain,tieto) : listaksia oikea a in flip listaksia [] {- Toinen sanakirjatyyppi on lista (avain,tieto)-pareja. -} newtype DictList u v = DictList { dictlist :: [(u,v)] } deriving (Show) instance Dictionary DictList where lookfrom x = lookup x. dictlist fromlist = DictList intolist = dictlist Toinen mahdollisuus tällaiseksi sanakirjatyypiksi s on lista (avain,tieto)-pareja. Määritellään se uudeksi tyypiksi DictList joka on haluttua muotoa: tyyppikonstruktori jolla on kaksi tyyppiparametria. Näin Dictionarystä tulee rajapinta, jonka voi toteuttaa monella eri tyypillä. 87

Kun muu koodi käyttää tätä rajapintaa eikä juuri jonkin tietyn sanakirjaksi sopivan tyypin omia funktioita, niin sanakirjatoteutuksen vaihtaminen toiseen on vaivattomampaa. Moniparametrisista konstruktoriluokista Edellinen sanakirjaesimerkkimme herättää välittömästi kysymyksen: Miksi emme määritelleet tyyppiluokkaamme suoraan siten, että sillä voisi olla monta parametria? Eli miksi emme aloittaneetkaan tyyppiluokkamme määrittelyä class Sanakirja s k t where joka tarkoittaisi suoraan että s on sellaisten sanakirjojen tyyppi, joiden avaimet ovat tyyppiä k ja tietokentät tyyppiä t. Yksiparametriset tyyppiluokat sanovat että tämä tyyppi kuuluu tuohon tyyppikokoelmaan.... Tällainen moniparametrinen (multiparameter) tyyppiluokka sanookin yleisemmin näillä tyypeillä on yhdessä sellainen ominaisuus että.... Tällaiset moniparametriset tyyppiluokat tulevatkin hyvin hyödyllisiksi kun tyyppejä alkaa käyttää mutkikkaammin kuin tällä kurssilla teemme. Niinpä ne ovatkin Haskell-yhteisössä kiivaan tutkimus- ja kehitystyön aihe. Haskell 2010 -standardiin niitä ei kuitenkaan ole vielä otettu, koska yhteisössä ei vielä vallitse yksimielisyys siitä, miten ne tarkkaan ottaen pitäisi määritellä ja toteuttaa. Erityisen hankalaksi on osoittautunut se, miten näiden tyyppien väliset mahdolliset riippuvuudet pitäisi ilmaista. Miten esimerkiksi kätevimmin ilmaistaisiin että jos avaintyyppi k on tämän lainen, niin sitä vastaavaksi sanakirjatyypiksi s pitääkin valita tuon lainen, jne.? Tällaisilla riippuvuuksilla voisi sitten ilmaista samantapaisia tyyppien välisiä suhteita kuin vaikkapa Javan sisäluokilla (inner classes) sekä paljon muuta... Aiemmin tähän on ehdotettu ns. funktionaalisia riippuvuuksia (functional dependencies). Uudempi ehdotus ovat tyyppiperheet (type families). Tulkki ghci sisältää laajennuksinaan nämä molemmat ehdotukset. Oletusarvoisesti ghci käynnistyy Haskell 98 -standardin mukaisena, jos se on versiota 6 Haskell 2010 -standardin mukaisena, jos versiota 7. 88

Tästä standardista pääsee tällaisiin laajennuksiin esimerkiksi aloittamalla lähdekooditiedostonsa erikoisella kommentilla {-# OPTIONS_GHC -fglasgow-exts #-} joka kertoo Glasgow Haskell Compilerille että tässä lähdekooditiedostossa käytetään kaikkia sen omia laajennuksia. Sopivilla optioilla -X... on mahdollista ilmoittaa tarkemmin, mitkä kaikista laajennuksista haluaa ottaa käyttöön. Näistä laajennuksista kiinnostuneet voivat tutustua niihin omin neuvoin; tämä kurssi pysyttelee standardissa. Tyyppijärjestelmän muita laajennuksia ovat esimerkiksi: Yleistetyt algebralliset tietotyypit (Generalized ADTs, GADTs) joiden datatyyppimäärittelyissä ohjelmoija voi antaa konstruktoreilleen tarkemman tyypin kuin minkä Haskell antaisi. Eksplisiittinen tyyppikvanttori forall. Haskellissä on näkymätön eli implisiittinen tyyppikvanttori: Jos tyypissä esiintyy muuttuja kuten a, niin se tarkoittaa mikä tahansa tyyppi (mahdollisin rajoittein). Tämän voi ilmaista myös näkyvästi eli eksplisiittisesti muodossa jokaisella tyypillä a (jolla)... Sillä voi ilmaista mutkikkaampiakin tyyppejä kuin hiljaisella esimerkiksi että on olemassa jokin tyyppi b (jolla)... joka tuo entistä enemmän olio-ohjelmoinnin piirteitä Haskell-ohjelmointiin. Tyyppijärjestelmä onkin se osa Haskellia, jota kehitetään tutkimuksessa voimakkaimmin sitähän voi aina kehittää yhä ilmaisuvoimaisemmaksi... 5.2.6 Valmiiksi määritellyistä konstruktoriluokista Prelude määrittelee erityisesti seuraavat kaksi yksiparametrista konstruktoriluokkaa Funktorit johon kuuluvilla tyyppikonstruktoreilla m on sellainen yhteinen ominaisuus, että niille on mielekästä määritellä korkeamman kertaluvun operaatio sovella tätä funktiota f jokaiseen alkioon x minun sisälläni ja kokoa nämä yksittäiset tulokset f(x). Monadit johon kuuluvilla tyyppikonstruktoreilla m taas on sellainen yhteinen ominaisuus, että niille on mielekästä määritellä sama operaatio mutta siten, että jokainen f(x) onkin jokin kokoelma tuloksia ja ne kaikki kootaan yhteen. Monadeilla on monia muitakin tulkintoja ja käyttökohteita! Funktorit ja monadit ovat kotoisin matematiikan haarasta nimeltään kategoriateoria (category theory). Erityisesti karteesisesti suljetut kategoriat (Cartesian Closed Categories, CCC) ovat matemaattinen malli Churchin λ-laskennalle......ja sitä kautta siis myös funktionaaliselle ohjelmoinnille. CCC:t antavat siis funktionaaliselle ohjelmoinnille taustateorian samaan tapaan kuin Turingin koneet antavat sellaisen tilaperustaiselle ohjelmoinnille. 89

Funktorit Mutta CCC:t siis tekevätkin sen puhtaassa matematiikassa vetoamatta mihinkään laskulaitteisiin. Termi karteesinen tarkoittaa, että näissä kategorioissa on karteesiset tulot eli parit. Kategoriset konstruktiot voi kääntää toisinpäin. Kun parien konstruktio käännetään toisinpäin, niin saadaan summat. Nämä summat vastaavat Preluden tyyppejä data Either a b = Left a Right b deriving (Eq, Ord, Read, Show) Termi suljettu taas tarkoittaa, että nämä kategoriat sisältävät kaikki ne funktiot, jotka voi määritellä näiden konstruktioiden pohjalta. Haskell-standardi määrittelee siis konstruktoriluokan class Functor m where fmap :: (a -> b) -> m a -> m b jossa m on siis yksiparametrinen tyyppikonstruktori. Sen instancet ilmaisevat, että seuraavat kaksi järkevyysehtoa pätevät: identiteettikuvaukselle id x = x ja yhdistetylle kuvaukselle f g. fmap id = id (8) fmap (f. g) = fmap f. fmap g (9) Jos esimerkiksi tämä m olisi matematiikan joukko, niin silloin nämä ehdot (8) ja (9) saataisiin pätemään valitsemalla fmap f S = {f(x): x S}. Esimerkiksi listoille samat saa pätemään Preluden instance Functor [] where fmap = map jossa [] on yksiparametrisen tyyppikonstruktorin [u] nimi ilman syntaktista sokerointia map todellakin täyttää nämä kaksi ehtoa listan voi tulkia joukon sijaan luetteloksi alkiota. Toinen esimerkki Preludesta on instance Functor Maybe where fmap f Nothing = Nothing fmap f (Just x) = Just (f x) jossa taas tyypin Maybe a voi tulkita tarkoittavan 0- tai 1-alkioista joukkoa tai listaa. 90

Monadit Haskell-standardi määrittelee myös konstruktoriluokan class Monad m where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a fail :: String -> m a m >> k = m >>= \_ -> k fail s = error s jossa m on siis yksiparametrinen tyyppikonstruktori. Vertaamalla funktioiden (>>=) ja flip fmap korkeamman kertaluvun f parametrin tyyppejä huomaa yhteyden: funktoreissa f palauttaa arvon tyyppiä b monadeissa taas tyyppiä m b. Toinen keskeinen funktio return taas tekee yhdestä yksittäisestä arvosta kokoelman. Monadin instance on lupaus, että seuraavat järkevyysehdot eli monadilait (monadic laws) pätevät: return a >>= k = k a (10) m >>= return = m (11) m >>= (\ x -> k x >>= h) = (m >>= k) >>= h (12) ja jos lisäksi pätee myös Functor m niin silloin myös ne yhdistävän lisäehdon pitää päteä. fmap f xs = xs >>= return. f (13) Jos tämä m olisi matematiikan joukko niin silloin nämä monadilait tulisivat voimaan valitsemalla return x = {x} S >>= f = x S f(x). Esimerkkejä sellaisista instansseista Preludessa ovat instance Monad Maybe where (Just x) >>= k = k x Nothing >>= k = Nothing return = Just fail s = Nothing instance Monad [] where m >>= k = concat (map k m) return x = [x] fail s = [] 91

joissa toistuu sama idea 0/1-alkioisille ja yleisille luetteloille. Kun Haskell-ohjelmaa ei käytetä tulkissa vaan se on käännetty itsenäiseksi suorituskelpoiseksi koodiksi, niin silloin sen yhteydenpito ympäristönsä kanssa (eli syötteiden lukeminen ja tulosteiden kirjoittaminen, jne.) on mallinnettu matemaattisesti tällaisena monadina. Ohjelmoijalle tarjotaan (onneksi!) do-notaatio syntaktisena sokerina monadisen koodin kirjoittamisen helpottamiseksi. Tätä vaihtoehtoista proseduraalista notaatiota voikin käyttää kaikilla monadeilla, ei vain I/O-monadissa. Saman kaltainen monadinen mallinnus on alkanut nousta esille myös esimerkiksi tiedonhallinnassa. Perinteinen relaatiotietomalli ja SQL-kyselykieli pohjautuvat logiikkaan ja ns. relaatioalgebraan. Monadien pohjalta taas voi kehittää niille vaihtoehtoisia tietomalleja ja kyselykieliä: Olkoon m a jokin a-tyyppisten tietojen varasto... Ne taas ovat kiinnostavia esimerkiksi Web-hakukoneiden tiedonhallinnassa, jota edustaa esimerkiksi edellä mainittu Googlen MapReduce: Funktorit ja monadit tyypittävät sellaisten järkeviä operaatioita. 92

6 Modulit Haskell-kielessä on yksinkertainen modulijärjestelmä. Moduleilla ilmaistaan, mitkä ohjelman yhdessä osassa määritellyt funktiot, tyypit ja tyyppiluokat ovat paikallisia eli käytettävissä vain tässä samassa osassa julkisia eli käytettävissä kaikissa muissakin osissa, jos nämä muut osat niin haluavat. Jos eri ohjelmointikielten modulijärjestelmät alkavat kiinnostaa, niin kannattaa tutustua Standard ML (SML) -kielen järjestelmään se on hyvin kehittynyt. Jos Haskell-lähdekooditiedosto on oma modulinsa, niin silloin se alkaa seuraavasti: module Hierarkinen.Modulin.Nimi(viennit) where tuonnit. jonka jälkeen tulevat Haskell-esittelyt ja -määrittelyt. Tulkki ghci olettaa näin otsikon modulin löytyvän tiedostosta nimeltä Tuonnit Linuxissa Hierarkinen/Modulin/Nimi.hs Windowsissa hierarkinen\modulin\nimi.hs jonka polku alkaa nykyisestä työhakemistosta josta ohjelmoija voi siis aloittaa tämän nyt tekeillä olevan ohjelman oma moduli- ja tiedostohierarkia tulkin asennuksen yhteydessä määritellystä pakkaushakemistosta josta alkaen se löytää vakiokirjastonsa kuten Data.Word jne. muista hakemistoista joita voi antaa tulkille optioina joko komentorivillä tai lähdekooditiedoston erikoiskommentissa {-# OPTIONS_GHC... #-} Ainakin Linuxissa sen voi asettaa myös työhakemistokohtaisessa alustustiedostossa./.ghci tai käyttäjäkohtaisessa alustustiedostossa ~/.ghci. Modulin tuonnit koostuvat tuontilauseista jollaisen perusmuoto on import Toinen.Hierarkinen jota olemmekin jo käyttäneet omissa lähdekooditiedostoissamme. 93

Jos jokin nimi esiintyy modulin nimeltään Toinen.Hierarkinen otsikon kohdassa viennit niin silloin tähän moduliin ilmestyvät sen lyhyt nimi eli pelkkä nimi sekä sen pitkä nimi eli Toinen.Hierarkinen.nimi. Haskell sallii sen, että samassa modulissa on monelle eri asialle sama nimi tuontien seurauksena. Mutta jos ohjelmassa yrittää käyttää tällaista moneen eri kertaan määriteltyä nimeä, niin silloin saakin virheilmoituksen. Niiden erottamiseksi toisistaan importin ja modulin nimen väliin voi lisätä määreen qualified jolloin tähän moduliin tulevat sieltä vain pitkät nimet Toinen.Hierarkinen.nimi. Koska tällaisten nimien kirjoitusasut voivat tulla pitkiksi, niin modulin nimen jälkeen voi lisätä määreen as Lyhenne jolloin nämä pitkät nimet kirjoitetaankin muodossa Lyhenne.nimi. Esimerkiksi tulkin ghci mukana tulee kirjasto, joka toteutta tietorakenteen joukko. Koska osa sen nimistä on samoja kuin Preludessa niin se tuodaan tavallisesti muodossa import qualified Data.Set as Set jotta esimerkiksi Preluden funktio null ja kirjaston funktio Set.null pysyvät erillään. Jos haluaa tuoda modulista vain joitakin sen nimi stä niin ne voidaan listata suluissa pilkuin, eroteltuna koko importin lopussa. Jos haluaakin tuoda modulin nimi stä kaikki muut paitsi näitä listattuja, niin silloin tämän listan eteen voi lisätä määreen hiding Siten tuontilause kaikilla näillä optioilla on import qualified Toinen.Hierarkinen as Lyhenne hiding (nimi 1,nimi 2,nimi 3,...,nimi k ) 94

Viennit Modulin viennit ovat pilkuin, eroteltu lista siinä itsessään tunnettuja alkio ita ne jotka tämä moduli tarjoaa muiden modulieen tuotaviksi pitkinä niminään Hierarkinen.Modulin.Nimi.alkio. Tällaisen alkion yksinkertaisin muoto on funktion nimi: Silloin tämä moduli siis sallii muiden modulien käyttävän tätä funktiota. Toinen alkion muoto on module Toinen.Hierarkinen jollekin tämän modulin itseensä tuomista moduleista. Silloin tämä moduli myös vie sen ne osat, jotka se toikin itseensä. Jos tämän modulin käyttö vaatii että myös tuo Toinen.Hierarkinen moduli on käytössä, sen käyttäjät selviävät yhdellä importilla kahden sijasta. Kolmas alkion muoto on TyyppiLuokanNimi(metodit) jossa metodit ovat lista pilkuin, eroteltuja sen metodien nimiä, jotka siis viedään joita voi siis kuormittaa tämän luokan uusia instansseja määritellessä.. joka on lyhennemerkintä sen kaikkien metodien lista poissa jolloin myös turhat sulutkin () voidaan jättää pois. Silloin viedään vain tämän luokan nimi mutta ei ainoatakaan sen metodeista. Silloin tätä luokkaa voi käyttää rajoitteissa mutta sille ei voi lisätä uusia instansseja koska sen metodeja ei voi kuormittaa, koska niitä ei vietä tästä modulista. Kaikki tässä modulissa määritellyt eri tyyppiluokkien instanssit viedään siitä ulos automaattisesti ilman eri mainintaa, ja siten myös tuodaan jokaiseen moduliin joka tuo mitään tästä modulista. Tämä takaa sen, että koko ohjelmassa on vain yksi tapa määritellä tietty instanssi. Modulin otsikosta voi jättää pois kaikki viennit sekä turhat sulutkin (): Sellainen moduli vie muihin moduleihin kaiken muun sisältönsä paitsi importit. 95

6.1 Abstraktit tietotyypit Neljäs alkion muoto on TyypinNimi(konstruktorit) jossa tyyppiluokkien tapaan konstruktorit ovat lista pilkuin, eroteltuja sen konstruktoreiden nimiä jotka siis viedään joita voi siis käyttää muisaakin moduleissa.. joka on lyhennemerkintä sen kaikkien konstruktoreiden lista poissa jolloin myös turhat sulutkin () voidaan jättää pois. Silloin viedään vain tämä tyyppi mutta ei ainoatakaan sen konstruktoreista. Jos tyypissä on käytetty kentännimiä, ja ne haluaan viedä, niin ne voi laittaa joko tähän samaan listaan kuin sen konstruktoritkin tai omina alkioinaan. Modulaarisessa ohjelmoinnissa puhutaan paljon abstrakteista tietotyypeistä (Abstract Data Type, ADT): Tyyppi T on abstrakti, jos sen jos sen sisäinen rakenne ja toiminta on suojattu siten, ettei tyyppiä T käyttävä ohjelmakoodi pysy mitenkään ei vahingossa eikä tahallaan käsittelemään sen arvoja toisin kuin tyypin T ohjelmoija on tarkoittanut. Algebrallinen tietotyyppi T (josta myös käytetään samaa lyhennettä ADT...) on abstrakti, jos sitä käyttävä ohjelmakoodi ei pysty käyttämään sen konstruktoreita eikä kentännimiä. Siten Haskell-tyyppi T on abstrakti, jos sen määrittelevä moduli M mainitsee sen vientilistassaan pelkkänä alkiona T ilman sulkeita (...). Silloinhan tyyppiä T käyttävä ohjelmakoodi voi luoda sen arvoja ja tutkia niiden sisältöä vain niillä funktioilla, jotka tyypin T ohjelmoija on ohjelmoinut tähän moduliinsa M. Haskell-moduli toteuttaakin usein yhden abstraktin tietotyypin operaatioineen. Klassinen esimerkki abstraktista tietotyypistä on pino (pushdown stack) operaatioineen Luo tyhjä pino. Onko tämä pino tyhjä? Työnnä tämä alkio tuohon pinoon sen päällimmäiseksi. Ota tästä epätyhjästä pinosta sen päällimmäinen. 96

module Stack(Stack, empty, isempty, push, pop) where newtype Stack a = IntoStack{ fromstack :: [a] } empty :: Stack a empty = IntoStack [] isempty :: Stack a -> Bool isempty = null. fromstack push :: a -> Stack a -> Stack a push x = IntoStack. (x :). fromstack pop :: Stack a -> (a,stack a) pop (IntoStack (top:more)) = (top,intostack more) 6.2 Päämoduli Koko ohjelman päämoduli on poikkeus: Se saa olla minkä nimisessä tiedosto ssa tahansa (kunhan sen loppuliite on.hs). Sen nimi on aina Main. Sitä ei ole pakko otsikoida. Silloin sen otsikoksi oletetaan module Main(main) where jossa main on Haskell-pääohjelman standardoitu nimi. Jos päämodulin sisältävä tiedosto halutaan kääntää suorituskelpoiseksi ohjelmaksi, joka ei tarvitse tulkkia ghci, niin silloin käännöskomento riippuu kääntäjän ghc versiosta. Jos se on 6.x.y niin komento on ghc --make tiedosto jossa optio--make ohjaa kääntäjän käymään läpi kaikki importit ja kääntämään (uudelleen) kaiken mitä tarvitaan ohjelman kokoamiseksi 7.u.v niin optiota--make ei enää tarvitse mainita erikseen, vaan se on oletusarvona. 97

Tämän suorituskelpoisen ohjelman nimeksi tulee tiedosto ilman loppuliitettään.hs. 98

7 Monadit ja I/O Tämän Haskell-pääohjelman main tyypin pitää olla muotoa IO τ jossa IO on tyyppikonstruktori, jonka voi lukea sellainen laskenta jolla on lupa tehdä I/O- eli syöttö- ja tulostusoperaatioita (Input/Output) kun se muodostaa tulostaan, joka on tyyppiä... τ joka voi olla mikä tahansa tyyppi. Siis main :: IO τ on sellainen laskenta että jos joku sen käynnistäisi niin se kommunikoisi ulkomaailman kanssa muodostaessaan tuloksensa tyyppiä τ. Tässä siis tyyppikonstruktori IO ei välttämättä synnytäkään mitään tietokenttiä, joissa olisi tyypin τ arvoja vaan se voi synnyttää vaikkapa funktioita tyyppiä -> τ tms. Kenellä sitten on oikeus käynnistää tällainen laskenta joka kommunikoi ulkomaailman kanssa? Kääntäjän ghc tuottama koodi alkaa laskea mainin arvoa ja sen laskennan lopuksi hylkää saamansa arvon, joka siis oli tyyppiä τ... Useimmiten mainin tyyppinä τ onkin () eli ei mitään kiinnostavaa. Tulkissa ghci voi testailla tyyppiä IO τ olevia lausekkeita ja nähdä myös niiden tulokset. Mutta kenelläkään muulla ei ole oikeutta käynnistää tällaista laskentaa. Tämä saavutetaan sillä, että tyyppikonstruktori IO on täysin abstrakti, eikä sille ole tarjolla operaatiota aja tämä laskenta. Tämä takaa sen, että heti kun lausekkeen tulostyyppi on jotakin muuta kuin tämä IO τ niin se on puhdasta funktionaalista koodia, joka ei tee lainkaan I/O-operaatioita. Kuten jo aiemminkin mainittiin, niin tällaiset I/O-operaatiot ovat ongelmallisia puhtaassa funktionaalisessa ohjelmoinnissa: Syöttö kuten lue seuraava syöterivi ei ole funktio, koska eri kutsukerroilla on tarkoitus lukea eri rivit. Funktionhan pitää palauttaa aina saman arvon saadessaan samat parametrit. Tulostus kuten tulosta ruudulle teksti... taas vaikuttaa tarpeettomalta. Laiska laskentahan laskee välituloksen vain jos sitä tarvitaan lopputuloksen laskemiseen, ja tulostaminenhan ei tuota mitään mielenkiintoista välitulosta. Haskell siis ratkaisee tämän ongelman tyypittämällä maailmansa kahteen osaan: I/O-laskentaan jossa pääohjelma main ja sen kutsumat muut IO-tulostyyppiset funktiot voivat tehdä näitä I/O-operaatioita sekä tehdä kutsuja 99

puhtaisiin funktioihin, jotka voivat vain palauttaa arvojaan kutsujilleen mutta eivät itse voi tehdä mitään I/O-operaatioita. Haskell määrittelee tämä abstraktin tyyppikonstruktorinsa IO monadina. Sitä ei kuitenkaan tulkitakaan kokoelmana kuten aiemmin, vaan idea on nyt seuraava: Keskeisen monadioperaattorin tyyppi on nyt (>>=) :: (Monad IO) => IO a -> (a -> IO b) -> IO b Siis lausekkeessa p >>= f jälkimmäinen funktio f tarvitsee argumentikseen tyyppiä a olevan arvon. Edellinen lauseke p voi tuottaa sellaisen arvon funktiolle f mutta sen tuottaminen vaatii, että ensin pitää suorittaa lausekkeen p kuvaama laskenta. Siten IO onkin sellainen monadi, jonka operaattori (>>=) tarkoittaakin, että suorita ensin laskenta p ja valitse sitten funktiolla f sen tuloksen perusteella, millaisella laskennalla jatketaan siitä eteenpäin. Koska pääohjelma saa tyypin main :: IO () niin se sanoo että aloita laskenta pääohjelman alusta, ja jatka sitten näin askel askeleelta sen loppuun saakka, jotta saisit selville sen lopullisen (tarpeettoman) arvon. Näin tyyppikonstruktorin IO monadisuus antaa halutun I/O-askelluksen, vaikka laskenta onkin laiskaa. Katsotaan sitten tyyppiluokan Monad määritelmää tästä I/O-askelluksen näkökulmasta: Operaattori (>>) on operaattorin (>>=) sellainen erikoistapaus, jossa seuraava askel f ei riipu edellis(t)en tuloksesta. Esimerkiksi jos edellä tulostettiin eikä laskettukaan mitään kiinnostavaa arvoa. Toinen keskeinen funktio return muuntaa tavallisen arvon x laskennaksi joka tuottaa saman tuloksen x tekemättä I/O-operaatioita. Tavallisesti ohjelmointikielissä return x tarkoittaa että palaa tästä aliohjelmasta heti sen kutsujaan vastauksena x. Haskell on valinnut tämän saman nimen tälle muunnokselle, koska sen tavallisin (mutta ei ainoa!) käyttö on oleellisesti sama: Se esiintyy yleensä IO-tyyppisen lausekkeen lopussa, ja muuntaa sen laskeman vastauksen IO-tyyppiseksi. Erona kuitenkin on, että Haskellin return ei vaikuta askellukseen: Jos lauseke jatkuu returnin jälkeenkin, niin sen arvon laskenta etenee sinne loppuosaan. Katsotaan sitten monadilakeja (10) (13) tästä I/O-askelluksen näkökulmasta: (10) sanoo, että jos edellinen laskenta palauttaa tässä mielessä arvon a, niin seuraava askel k ottaa sen käyttöönsä. (11) taas sanoo, että jos edellinen laskenta on tuottanut jonkin arvon m ja seuraava askel on return niin silloin palautetaan tämä m tässä mielessä. 100

Nämä kaksi monadilakia siis ilmoittavat yhdessä sen, miten return toimii suhteessa operaattoriin (>>=). Niiden intuitio on, että return on operaattorin (>>=) eräänlainen neutraalialkio nyt kun kyseessä ovat funktiot. Toisin sanoen, return muuntaa arvon a sellaiseksi laskennaksi joka ei tee itse mitään se vain välittää arvon a laskennassa eteenpäin. (12) taas sanoo intuitiivisesti, että operaattori (>>=) on liitännäinen. Tämä näkyy selvemmin, jos lain ensimmäiseksi operaattoriksi vaihdetaankin (>>) jolloin x katoaa: m >> (k >>= h) = (m >> k) >>= h Siten (>>=)-operaattoreiden lauseketta ensimmäinen (>>=) toinen (>>=) kolmas (>>=) lasketaan askeltamalla järjestyksessä ensimmäisestä toiseen, toisesta kolmanteen,... riippumatta siitä, miten lausekkeen sulut ovat. (13) sanoo, että funktion fmap :: (a -> b) -> IO a -> IO b kutsu fmap f p tuottaa sellaisen laskennan joka 1. suorittaa ensin laskennan p 2. soveltaa sitten sen tulokseen funktiota f ja jatkaa näin saadulla arvolla. Siten Haskell-pääohjelma main ja kaikki sen kutsumat IO-tyyppiset funktiot, jotka suorittavat I/O-operaatioita, voitaisiin kirjoittaa (>>=)-lausekkeina. 7.1 do-syntaksi Tällaisten pitkien (>>=)-lausekkeiden kirjoittaminen tulisi kuitenkin kovin vaivalloiseksi, ja siksi Haskell tarjoaakin syntaktisena sokerina niille seuraavanlaisen syntaksin: do lause 1 lause 2 lause 3. lause q lauseke jonka lopussa oleva lauseke on tyyppiä M τ jossa tyyppikonstruktori M on jokin Monadi. Yleensä tämä lauseke on siis return x jossa arvon x tyyppi on τ. Pääohjelmassa main tämä M on siis IO. Tätä viimeistä lauseketta edeltävät lauseet voivat olla: 101

laskentakutsuja muotoa hahmo <- kutsuttava jossa kutsuttava lauseke on Monadista tyyppiä M µ ja hahmo sen alkiotyyppiä µ. 1. Ensin suoritetaan kutsuttavan lausekkeen määrittelemä laskenta. 2. Sitten sovitetaan sen tuottama arvo a tähän hahmoon. 3. Jos se sopii, niin tämänhahmon antamat nimet arvon a osille näkyvät tästä lause eesta alkaen tämän do-lausekkeen loppuun (ellei jokin myöhäisempi hahmo tai määrittely peitä niitä näkyvistä). 4. Jos se ei sovikaan, niin silloin kutsutaankin Monadin M metodia fail. Intuitiona on siis suorita tämä kutsuttava aliohjelma ja nimeä kutsun palauttama arvo a hahmolla. Tämä <- on siis se kohta, jossa do-notaatio sokeroi operaattorin (>>=) käytön. Jos palautusarvo a ei kiinnosta (esimerkiksi jos kutsuttava aliohjelma on jokin tulostusrutiini) niin silloin hahmo <- voidaan jättää poiskin tarpeettomana. Se puolestaan on kohta, jossa do-notaatio sokeroikin operaattorin (>>) käytön. Esimerkiksi z <- getline 1. kutsuu ensin vakiokirjaston I/O-aliohjelmaa lue seuraava syöterivi 2. antaa sitten näin luetulle merkkijonolle nimeksi z. määrittelyjä jotka ovat let-lausekkeita. Niihin ei kuitenkaan kirjoiteta omaa inosaa, vaan määrittelyt näkyvät tästä lauseeesta alkaen tämän do-lausekkeen loppuun (ellei jokin myöhäisempi hahmo tai määrittely peitä niitä näkyvistä). lausekkeita jotka voivat olla mitä tahansa Haskell-lausekkeita, kunhan ne vain ovat Monadista tyyppiä M ν. Esimerkiksi lauseke if null z then return () else do putstrln z main tutkii, onko merkkijono nimeltä z tyhjä vaiko ei. Jos se on tyhjä, niin ei tehdä mitään; muuten ensin tulostetaan z ja sitten palataan takaisin pääohjelman main alkuun. Kummankin if-haaran tyyppi on IO (). Näin saammekin ensimmäisen itsenäisen Haskell-ohjelmamme, joka lukee merkkijonoja syötteestä ja kaiuttaa ne tulosteenaan kunnes saa tyhjän merkkijonon jolloin se lopettaa: module Main(main) where 102

main :: IO () main = do z <- getline if null z then return () else do putstrln z main 7.2 I/O-kirjaston operaatioita Preluden perusfunktioita tekstimuotoiseen tulostukseen oletustulosvirtaan stdout ovat putchar :: Char -> IO () joka tulostaa yhden merkin putstr :: String -> IO () joka tulostaa merkkijonon putstrln :: String -> IO () joka tulostaa merkkijonon ja rivinvaihdon print :: Show a => a -> IO () joka on putstrln. show ja jota tulkki ghci kutsuu laskemalleen arvolle silloin kun sen tyyppi kuuluu tähän tyyppiluokkaan. syötteen lukemiseen oletussyötevirrasta stdin ovat getchar :: IO Char joka lukee yhden merkin getline :: IO String joka lukee yhden rivin. Jos syötteen tiedetään esittävän jokin tyypina jotakin arvoa x, niin silloin tämä x saadaan seuraavilla funktioilla: readio :: Read a => String -> IO a joka konvertoi annetun merkkijonon tyypin a arvoksi x readln :: Read a => IO a joka lukee syöterivin oletussyötevirrasta stdin ja konvertoi sen. Jos merkkijonoa konvertoitaessa törmätäänkin syntaksivirheeseen, niin siitä seuraa I/O-poikkeus, jonka ohjelmoija voi käsitellä. Oletussyötevirran stdin laiskaan lukemiseen on funktio getcontents :: IO String joka palauttaa sen koko (jäljellä olevan) sisällön laiskana merkkijonona, jota luetaan sitä mukaa kun ohjelma etenee sen merkistä seuraavaan. Se on kauniin deklaratiivinen (joskin hieman hidas) tapa lukea syöte eräajo-ohjelmaan, koska siihen voi soveltaa kaikkia Haskellin tarjoamia listankäsittelyfunktioita. Yksinkertaisten tekstipohjaisten tilattomien vuorovaikutteisten ohjelmien laatimiseen taas on funktio interact :: (String -> String) -> IO () jonka parametri 103

saa syötteenään oletussyötevirran stdin koko (jäljellä olevan) sisällön laiskasti luettavana merkkijonona antaa tulosteensa toisena merkkijonona jota se voi rytmittää rivinvaihdoilla. Esimerkiksi: module Main(main) where main :: IO () main = interact (concatmap ((++"\n"). reverse). lines) Mutkikkaammat I/O-operaatiot kuten esimerkiksi tiedostojen käsittely löytyvät vakiokirjastosta System.IO. Esimerkiksi usein halutaan oletustulosvirta stdout sellaiseksi, ettei sitä puskuroidakaan. Siihen tämä kirjasto tarjoaa kutsun hsetbuffering stdout NoBuffering jonka voi sijoittaa sellaisen ohjelman mainin alkuun. Poikkeuksista Poikkeuksia (exception) on hankala sovittaa yhteen puhtaan funktionaalisen ohjelmoinnin kanssa: Se milloin poikkeus käsitellään olettaa, että ohjelmoija tietäisi milloin jotakin tapahtuu laskennan kuluessa mutta puhtaassa funktionaalisessa ohjelmoinnissahan ohjelmoija tietää korkeintaan että se joskus tapahtuu... Haskellin vakiokirjasto System.IO.Error tarjoaakin yksinkertaisen poikkeusmekanismin erilaisten I/O-virheiden käsittelyyn koska Monadissa IO on tällainen järjestys ensin lasketaan tämä, ja sitten vasta tuo... Toteutuksen ghc mukana tulee myös sen oma toinenkin kirjasto Control.Exception jolla poikkeuksia voi nostaa muuallakin kuin Monadissa IO mutta nekin voi käsitellä vain siellä tämän järjestyksen vuoksi. Vakiokirjaston I/O-poikkeusmekanismi koostuu tyypistä IOError eli jokin I/Ovirhe sekä funktioista ioerror :: IOError -> IO a joka nostaa parametrina ilmoitetun I/O-poikkeuksen catch :: IO a -> (IOError -> IO a) -> IO a jonka ensimmäinen parametri on I/O-tyyppinen lauseke (kuten esimerkiksi jokin do-lauseke) jonka laskenta voi aiheuttaa I/O-poikkeuksia. Jos niitä ei aiheudu, niin catchin arvoksi tulee tämän laskennan antama arvo. jälkimmäinen parametri on funktio, jota kutsutaan, jos näin käy. Se saa parametrinaan sen aiheutuneen poikkeuksen. 104

Se voi käsitellä kyseisen poikkeustilanteen palauttamalla jonkin arvon, josta tulee tämän catchin arvo. Jos se ei osaakaan käsitellä tilannetta, niin sen pitääkin nostaa sama poikkeus uudelleen, jolloin tilanteen käsittelyvastuu siirtyykin seuraavalle ulommalle catchille. Jos mikään catch ei osaakaan sitä käsitellä, niin silloin koko mainin laskenta keskeytyy suoritusaikaiseen virheeseen. try :: IO a -> IO (Either IOError a) joka yrittää suorittaa parametrinaan saamansa laskennan. Jos sen arvo on Right x niin tämä laskenta onnistui virheettä ja x on sen lopputulos Left e niin silloin tämä laskenta johtikin poikkeukseen e. Otetaan sitten esimerkiksi I/O-ohjelmoinnista nelilaskin, jossa pidetään muistissa yhtä liukulukua käyttäjä voi antaa komentonaan lisää/vähennä/kerro/jaa se luvulla... tämän komentorivin mahdolliset virheet käsitellään tyhjä komentorivi lopettaa ohjelman suorituksen. 7.3 Maybe monadina Aiemmin näimme, miten Maybe on Monadi: instance Monad Maybe where (Just x) >>= k = k x Nothing >>= k = Nothing return = Just fail s = Nothing Tarkastellaan sitä tässä valossa, että lausekkeen p >>= k voi lukea myös suorita ensin laskenta p ja sovella sitten sen tulokseen x funktiota k. Silloin näemme uuden tulkinnan Maybelle: Jos laskenta p sai jonkin tuloksen x niin sovella siihen funktiota k, mutta jos se ei saanutkaan mitään tulosta, niin koko loppulaskentakaan ei saa mitään tulosta koska siitä alkaen jokaisen (>>=)-operaattorin vasen parametri on aina Nothing. Silloin siis jokainen Maybe-tyyppisen do-lauseen hahmo <- kutsuttava ohitetaan kokonaan, jos jokin sitä edeltävistä tällaisista lauseista on jo tuottanut arvon Nothing siinä kutsuttava laskenta tuottaakin sellaisen arvon, joka ei olekaan muotoa olekaan muotoa Just hahmo. Silloin siis tästä alkaen kaikki lauseet ohitetaan. Siten tässä valossa Maybe antaakin tyypin sellaiselle laskennalle joka päättyy epäonnistumiseen jos yksikin siinä kutsuttava laskenta epäonnistuu. 105

7.4 Lista monadina Olemme nähneet jo aiemmin, miten listat ovat Monadi: instance Monad [] where m >>= k = concat (map k m) return x = [x] fail s = [] Tarkastellaan sitäkin tässä valossa, että lausekkeen p >>= k voi lukea myös suorita ensin laskenta p ja sovella sitten sen tulokseen x funktiota k. Nyt sen koko laskenta etenee seuraavasti: 1. Laskenta p tuottaa tuloksenaan listan m eli [x 1, x 2, x 3,...]. 2. Sen jokaiseen alkioon x i sovelletaan (map) funktiota k. 3. Jokainen sovellus k x i tuottaa tuloksenaan jonkin listan [y 1 i, y 2 i, y 3 i,...]. 4. Yhdistetään (concat) nämä listat yhdeksi listaksi k x 1 ++ k x 2 ++ k x 3 ++... 5. Sitten ympäröivässä listamonadilausekkeessa >>=k sovelletaan seuraavaa funktiota k jokaiseen tämän listan alkioon y j i, jne. Vaihdetaan näkökulmaa kokonaisista listoista niiden yksittäisiin alkioihin: Ajatellaan, että alkio x i edustaisi jotakin tilaa, jossa koko laskenta voisi olla tällä hetkellä. Silloin k x i tuottaa sille monta eri mahdollista seuraavaa tilaa y j i. Tässtä näkökulmasta k on siis epädeterministinen seuraava laskenta-askel. Tässä valossa listamonadi ja sen do-syntaksi kuvaavat epädeterministisen laskennan kaikkien eri vaihtoehtojen seuraamiseen. Deklaratiivisesti yksi tällainen epädeterministinen askel hahmo <- kutsuttava voidaan lukea valitse mikä tahansa hahmo on sopiva vaihtoehto kutsuttava n laskennan antamista mahdollisuuksista. Tässä luennassa <- on. 106