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

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

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Funktionimien kuormitus. TIES341 Funktio ohjelmointi 2 Kevät 2006

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

Algebralliset tietotyypit ym. TIEA341 Funktio ohjelmointi 1 Syksy 2005

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

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Java-kielen perusteet

Haskell ohjelmointikielen tyyppijärjestelmä

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

15. Ohjelmoinnin tekniikkaa 15.1

Ohjelmoinnin perusteet Y Python

TIEA341 Funktio-ohjelmointi 1, kevät 2008

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 Y Python

Tietueet. Tietueiden määrittely

Ohjelmoinnin perusteet Y Python

815338A Ohjelmointikielten periaatteet Harjoitus 2 vastaukset

ITKP102 Ohjelmointi 1 (6 op)

TIEA341 Funktio-ohjelmointi 1, kevät 2008

TIEA341 Funktio-ohjelmointi 1, kevät 2008

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

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

Java-kielen perusteet

Ohjelmoinnin perusteet Y Python

15. Ohjelmoinnin tekniikkaa 15.1

Luku 3. Listankäsittelyä. 3.1 Listat

Ohjelmoinnin perusteet Y Python

ELM GROUP 04. Teemu Laakso Henrik Talarmo

Luokka Murtoluku uudelleen. Kirjoitetaan luokka Murtoluku uudelleen niin, että murtolukujen sieventäminen on mahdollista.

Demo 7 ( ) Antti-Juhani Kaijanaho. 9. joulukuuta 2005

Ohjelmointi 2. Jussi Pohjolainen. TAMK» Tieto- ja viestintäteknologia , Jussi Pohjolainen TAMPEREEN AMMATTIKORKEAKOULU

Algoritmit 1. Luento 6 Ke Timo Männikkö

Abstraktit tietotyypit. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Ohjelmoinnin peruskurssi Y1

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

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

Ohjelmoinnin perusteet Y Python

Diskreetin matematiikan perusteet Laskuharjoitus 2 / vko 9

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

TIEA341 Funktio-ohjelmointi 1, kevät 2008

Ohjelmoinnin jatkokurssi, kurssikoe

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

Alkuarvot ja tyyppimuunnokset (1/5) Alkuarvot ja tyyppimuunnokset (2/5) Alkuarvot ja tyyppimuunnokset (3/5)

Taas laskin. TIES341 Funktio ohjelmointi 2 Kevät 2006

Tähtitieteen käytännön menetelmiä Kevät 2009 Luento 4: Ohjelmointi, skriptaus ja Python

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

Ydin-Haskell Tiivismoniste

12. Monimuotoisuus 12.1

Tämä tarina on Fibonaccin lukujen ongelman alkuperäinen muotoilu.

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

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

Algoritmit 2. Luento 3 Ti Timo Männikkö

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

System.out.printf("%d / %d = %.2f%n", ekaluku, tokaluku, osamaara);

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

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

Algoritmit 2. Luento 3 Ti Timo Männikkö

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

20. Javan omat luokat 20.1

Sisällys. 20. Javan omat luokat. Java API. Pakkaukset. java\lang

Ohjelmoinnin peruskurssien laaja oppimäärä

Java-kielen perusteita

Ohjelmoinnin peruskurssi Y1

5.5 Jäsenninkombinaattoreista

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

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

Python-ohjelmointi Harjoitus 2

Ohjelmointi 1 Taulukot ja merkkijonot

7/20: Paketti kasassa ensimmäistä kertaa

Ohjelmoinnin peruskurssi Y1

Algoritmit 1. Luento 10 Ke Timo Männikkö

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

Sisällys. 6. Metodit. Oliot viestivät metodeja kutsuen. Oliot viestivät metodeja kutsuen

Informaatioteknologian laitos Olio-ohjelmoinnin perusteet / Salo

7. Näytölle tulostaminen 7.1

Kielioppia: toisin kuin Javassa

17. Javan omat luokat 17.1

8.5 Takarekursiosta. Sanoimme luvun 8.3 foldl -esimerkissämme että

Ohjelmoinnin peruskurssien laaja oppimäärä

useampi ns. avain (tai vertailuavain) esim. opiskelijaa kuvaavassa alkiossa vaikkapa opintopistemäärä tai opiskelijanumero

Harjoitus 3 (viikko 39)

17. Javan omat luokat 17.1

Java-kielen perusteet

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

Ohjelmointiharjoituksia Arduino-ympäristössä

System.out.printf("%d / %d = %.2f%n", ekaluku, tokaluku, osamaara);

Algoritmit 2. Luento 7 Ti Timo Männikkö

Ohjelmoinnin peruskurssi Y1

Ohjelmoinnin peruskurssi Y1

Ohjelmoinnin peruskurssi Y1

TIEA341 Funktio-ohjelmointi 1, kevät 2008

niin järjestys on tämä: ensin kerto- ja jakolaskut vasemmalta oikealle, sen jälkeen plus- ja miinuslaskut vasemmalta oikealle.

58131 Tietorakenteet ja algoritmit (kevät 2016) Ensimmäinen välikoe, malliratkaisut

Ohjelmoinnin perusteet Y Python

Diskreetin matematiikan perusteet Laskuharjoitus 1 / vko 8

Ohjelmoinnin perusteet, syksy 2006

Tutoriaaliläsnäoloista

815338A Ohjelmointikielten periaatteet Harjoitus 7 Vastaukset

Uusi näkökulma. TIEA341 Funktio ohjelmointi 1 Syksy 2005

Transkriptio:

5.1 Tyyppiparametrit Nyt lisäämme parametrit myös data-määrittelyihin: data Nimi tp 1 tp 2 tp 3... tp k =... Lisäämme ne myös type- ja newtype-määrittelyihin. Nämäkin parametrit tp i kirjoitetaan pienellä alkukirjaimella futen funktioidenkin parametrit. Sitten voimme käyttää näitä tyyppiparametreja tp i kuten tyypinnimiä tämän datamäärittelyn haaroissa. Siis eri haarojen kenttien tyypeissä. Intuitio on, että Nimi Ty 1 Ty 2 Ty 3... Ty k on sellainen uusi tyyppi, jossa tämä annettu tyyppi Ty i esiintyy niissä kohdissa, joissa data-määrittelyn haarassa esiintyi sitä vastaava tyyppiparametri tp i. Siis tällainen tyyppiparametrisoitu Nimi ei olekaan enää itsenään minkään tyypin nimi, vaan nimi funktiolle joka saatuaan jotkut tyypit Ty i synnyttää niistä tämän uuden tyypin Nimi Ty 1 Ty 2 Ty 3... Ty k. Siksi sitä kutsutaankin tyyppikonstruktoriksi. Otetaan esimerkiksemme aiempi hakupuumme, johon lisätään uusi kenttä nimeltään tieto. Ideana on, että sinne voisi tallettaa tähän avaimeen liittyvän tietoalkion, ja näin saada hakupuustamme sanakirjan (dictionary): Tietorakenteen, jolta voisi kysyä mikä on tätä avainarvoa x vastaava talletettu tietoalkio? Mikä sen tietoalkiokentän tyyppi pitäisi olla? Joskus voimme tarvita sellaista sanakirjaa, jossa se on String. Joskus muulloin taas sellaista, jossa se onkin Bool, ja niin edelleen... Niinpä teemmekin sen tyypistä tyyppiparametrin t, jolloin saamme molemmat näistä, ja niin edelleen... data Puu t = Tyhja Solmu{ vasen :: Puu t, avain :: Int, 63

tieto :: t, oikea :: Puu t } deriving (Show) lisaa :: Int -> t -> Puu t -> Puu t lisaa x y Tyhja = Solmu{ vasen = Tyhja, avain = x, tieto = y, oikea = Tyhja } lisaa x y haara@(solmu{ avain=k }) x < k = haara{ vasen = lisaa x y $ vasen haara } x > k = haara{ oikea = lisaa x y $ oikea haara } otherwise = haara listasta :: [(Int,u)] -> Puu u listasta = foldr (uncurry lisaa) Tyhja listaksi :: Puu t -> [(Int,t)] listaksi = let listaksia Tyhja a = a listaksia haara a = listaksia (vasen haara) $ (avain haara,tieto haara) : listaksia (oikea haara) a in flip listaksia [] Nyt siis Puu String on tyyppi sellaisille puille, joissa tämä tietokenttä on merkkijonotyyppiä Puu Bool taas sellaisille, joissa se onkin totuusarvotyyppiä ja niin edelleen... Voi ajatella, että tyyppimuuttuja t korvataan vastaavalla tyypillä, eli että Puu String on data Puu String = Tyhja Solmu{ vasen :: Puu String, avain :: Int, tieto :: String, oikea :: Puu String } Puu Bool on data Puu Bool = Tyhja Solmu{ vasen :: Puu Bool, avain :: Int, tieto :: Bool, oikea :: Puu Bool } 64

Ertyisesti Tyhjan puun tyyppi on Tyhja :: Puu t jonka mukaan tämä vakioarvo on yhteinen kaikille kenttätyypeille t, mutta tyypinpäättelyssä Tyhja :: Puu String ja Tyhja :: Puu Bool ovat erillään. Tyypinpäättelynsä vuoksi Haskell osaa täydentää itse, millaista tyyppiä tämän tietokentän pitää milloinkin olla. Esimerkiksi listasta [(10,"kymmenen"),(100,"sata"),(1000,"tuhat")] tuottaa tuloksen, jonka tyyppi on Puu String, kun taas listasta [(10,True),(100,False),(1000,False)] tuottaakin tuloksen, jonka tyyppi onkin Puu Bool, ja niin edelleen... Nämä tyypit ovat erilliset. Haskell torjuu esimerkiksi yrityksen listasta [(10,True),(100,"sata"),(1000,False)] ilmoittaen ettei Bool ole sama tyyppi kuin String. Näitä tyyppimuuttujia käytetään myös funktioiden tyypityksissä. Esimerkiksi listasta :: [(Int,u)] -> Puu u ilmaisee, että 1. sen parametri on lista pareja, joissa ensimmäinen osa on Int (koska siitä tulee Solmuun avain) ja toinen osa on jotakin mielivaltaista tyyppiä u 2. arvo on sitten sellainen Puu jonka tietokentän tyyppinä on tämä sama u. Näin Haskellin tyypinpäättely löytää listasta sen käyttöyhteyden, jossa tämä ja sen, jossa se onkin ja niin edelleen... u = String u = Bool Myös esimerkiksi korkeamman kertaluvun yleiskäyttöiset listankäsittelyfunktiot ovat tällä samalla tavalla monimuotoisia. Vaikkapa tyypitys map :: (a -> b) -> [a] -> [b] 65

vaatii että syötelistan alkiotyypin a pitää olla sama kuin sovellettavan syötefunktion f parametrityypin takaa että silloin tämän funktion f tulostyyppi b on sama kuin tuloslistan alkiotyyppikin mutta muuten nämä tyypit a ja b saavatkin olla mitä tahansa. Tämä takaa funktion map monikäyttöisyyden: Se mukautuu mihin tahansa f, ja sen syöte- ja tuloslistojen alkiotyypit päätellään automaattisesti nyt käytetyn f mukaisiksi. Tämä automaattinen mukautuminen tekee parametrisesta monimuotoisuudesta ohjelmoijalle paljon vaivattomampaa kuin geneerisyydestä, jossa hänen täytyykin itse kirjoittaa arvot näille tyyppiparametreille. Kun jatkamme tätä hakupuuesimerkkiämme, niin törmäämme ongelmaan: Silloin kun hakupuussamme oli vain avainkenttä, niin riitti kirjoittaa Haskellfunktio tyyppiä puussa :: Int -> Puu -> Bool joka vain ilmoitti, onko tämä annettu avainarvo x tässa annetussa hakupuussa vaiko ei. (Sellaisen kirjoittaminen jätetään harjoitustehtäväksi.) Nyt kun hakupuussamme onkin myös avainta vastaava tietokenttä, niin vastaan Haskell-funktion katso :: Int -> Puu t ->? pitääkin palauttaa myös avainarvoa x vastaavan tietokentän sisältö, jos se on tässä hakupuussa. Mutta mikä olisi tämän funktion katso vastauksen tyyppi? Prelude tarjoaa juuri tähän käyttöön tyypin data Maybe a = Nothing Just a eli joko ei mitään tai just tällainen tulos. Siten voimme ottaa funktiomme tyypiksi katso :: Int -> Puu t -> Maybe t eli joko ei mitään (jos tätä avainta x ei ole tässä hakupuussa) tai just sitä vastaavan tietokentän arvo (joka on siis samaa tyyppiä t kuin hakupuussakin) (mutta itse funktion kirjoittaminen jätetään harjoitustehtäväksi). 66

Tämä on tyyppiturvallinen tapa ohjelmoida sellainen funktio, jonka muissa kielissä ohjelmoisi vaikkapa palauttamaan NULL-osoittimen, jos avainta x ei hakupuussa ole: Funktiota käytetään lausekkeessa case katso x puu of Nothing ->... Just k ->... jossa vastaavan tietokentän sisältö k näkyy vain siinä haarassa, jossa sellainen todellakin on olemassa. Parametrinen monimuotoisuus on siten tarkoitettu sellaisten funktioiden kirjoittamiseen, joiden toiminta ei riipu tyyppiparametriensa arvoista: Esimerkiksi listasta toimii aina samalla tavalla riippumatta tietokentän nykyisestä tyypistä se tekee aina syötelistastaan hakupuun. Alityyppimonimuotoisuus on taas tarkoitettu juuri sellaisten funktioiden kirjoittamiseen, joiden toiminta riippuu käsiteltävien alkioiden tyypeistä: Esimerkiksi Kuviolistan piirtäminen tapahtuu siten, että jokaista sen alkiota pyydetään piirtämään itsensä ja jokainen niistä osaa piirtää itsensä. Haskell osaa siis mainiosti parametrisen monimuotoisuuden. Voisiko sitä laajentaa joillakin piirteilla alityyppimonimuotoisuudesta? 5.2 Tyyppiluokat Laajennetaan hakupuuesimerkkiämme edelleen yrittämällä sallia myös sen avainkenttään sama monimuotoisuus, jonka sen tietokenttä jo sallii. Toisin sanoen tavoittelemme kaksiparametrista tyyppikonstruktoria Puu a t jonka tyyppiparametri a on sen avainkenttien, ja t on sen tietokenttien tyyppi. Tärmäämme ongelmaan: Tämä avainkenttien tyyppi a ei voikaan olla aivan mikä tahansa...... koska meidän täytyy olettaa, että sille on määritelty suuruusjärjestys eli operaattori (<) :: a -> a -> Bool koska funktio lisaa edellyttää sellaisen. 67

Siten meidän täytyy lisätä tyyppeihimme mahdollisuus rajoittaa monimuotoisuutta sanomalla mikä tahansa sellainen tyyppi a, jolle on määritelty seuraavat funktiot.... Tai jos vaihdamme näkökulman tyypeistä funktioihin, mahdollisuus kuormittaa (overload) yksi ja sama funktion nimi kuten (<) monelle eri tyypille a. Tämä kuormitusnäkökulma lähestyy olio-ohjelmointia: Jokainen tyyppi a tietää itse miten sen tyyppisiä arvoja vertaillaan toisiinsa. Toisin sanoen, jokaisella tyypillä a on oma funktionsa nimeltään (<), jota sen tyyppisten avainten lisaamisessa käytetään. Olio-ohjelmoinnin tapaan myös Haskellissa kutsutaan metodeiksi niitä funktioiita, jotka voidaan kuormittaa eri tyypeille a. Miten siis lisätään tämä mahdollisuus siten, että tyypinpäättelykin yhä toimii? Haskellin ratkaisu ovat tyyppiluokat (type classes): Tyyppiluokka on kokoelma sellaisia tyyppejä, jotka kaikki toteuttavat jonkin tietyn rajapinnan. Esimerkiksi tyyppiluokka Ord eli järjestetyt (Ordered) tyypit koostuu niistä tyypeistä t jotka toteuttavat sellaisen rajapinnan, johon kuuluu funktio (<) :: t -> t -> Bool eli joilla on järjestys. Tyyppi t voi liittyä tyyppiluokan instanssiksi eli jäseneksi määrittelemällä, miten se toteuttaa kyseisen rajapinnan. Tyyppi t voi määritellä sen itse kuten (ohjelmoija) haluaa, joten tyyppiluokan rajapinnan funktiot ovat kuormitettuja. Sitten tyyppejä kirjoittaessa voidaan ilmoittaa, että nyt pitääkin rajoittua vain tiettyjen tyyppiluokkien instansseihin. Esimerkiksi Data.List.sort :: (Ord a) => [a] -> [a] tarkoittaa, että syötelista voidaan järjestää ( sortata ) jos sen alkiotyypillä a on järjestys mutta muuten a voi olla millainen tyyppi tahansa. Tällaiset rajoitteet tyyppimuuttujille kirjoitetaan siis (Rajoite 1,Rajoite 2,Rajoite 3,...,Rajoite k ) =>... jossa jokainen Rajoite i on TyyppiLuokanNimi tyyppimuuttuja 68

ja se tarkoittaa, että tämän tyyppimuuttuja n arvot pitää valita tästä tyyppiluokasta. Tämä merkintä => on valittu muistuttamaan logiikan implikaationuolta : Jos nämä rajoitteet ovat voimassa niin sitten... Ne voidaan sijoittaa Haskell-ohjelmassa eri paikkoihin: datatyypissä se voidaan kirjoittaa heti datan jälkeen eli data (...) => Nimi... ja silloin se tarkoittaa, että koko tämännimi nen tyyppi on olemassa vain, jos nämä rajoitteet ovat voimassa. Tämä vaatimus on globaali: Se koskee myös kaikkia niitä funktioita, joissa tämännimistä tyyppiä käytetään. funktion tyypissä se voidaan kirjoittaa heti merkinnän :: jälkeen eli nimi :: (...) =>... ja silloin se tarkoittaa, että tämännimi stä funktiota ei voi käyttää, elleivät nämä rajoitteet ole voimassa. Se on siis lokaali ehto. Tyyppiluokan määrittelyn sisäpuolella esitellyt funktiot ovat siis niitä metodeja, jotka voidaan ja pitääkin kuormittaa ulkopuolella eli tavallisessa Haskell-ohjelmakoodissa esitellyt funktiot voivat kutsua näitä metodeja nimellä, ja silloin Haskell valitsee tyypinpäättelynsä perusteella sen version, jota nyt pitää käyttää. Esimerkiksi Haskell päättelee, että kutsussa Data.List.sort ["hei","vaan","kaikki","ohjelmoijat"] pitää käyttää merkkijonojen vertailua (<) :: String -> String -> Bool kun taas kutsussa Data.List.sort "hei vaan kaikki ohjelmoijat" pitääkin käyttää yksittäisten merkkien vertailua (<) :: Char -> Char -> Bool mutta molemmissa käytetään samaa sort-algoritmia. 69

Javan rajapinnat eli interfacet muistuttavat näitä Haskellin tyyppiluokkia mutta siis ilman tyypinpäättelyä. Tyyppiluokat ovat yksi Haskellin mukanaan tuomista uutuuksista ohjelmointikielten tyyppijärjestelmiin. Niitä pidetäänkin hyvänä ratkaisuna ylikuormittamiseen silloin kun ohjelmointikielessä on tyypinpäättely mutta ei ole alityypin käsitettä. Lisäksi Haskellissa on sellaisia sääntöjä kuin jos tyyppi t kuuluu tyyppiluokkaan Ord niin myös tyyppi [t] kuuluu tyyppiluokkaan Ord koska... Esimerkiksi merkkijonojen aakkosjärjestys onkin saatu merkkien aakkosjärjestyksestä tällaisella säännöllä. Tällaiset säännöt ovat jo valmiina niille tyypeille, jotka Haskell tarjoaa. Ohjelmoija voi myös lisätä sellaisia sääntöjä omille tyypeilleen, ja Haskellin tyypinpäättely osaa huomioida myös ne. Haskellissa tyyppi voi kuulua tyyppiluokkaan eli toteuttaa sen rajapinnan vain yhdellä tavalla. Esimerkiksi tyyppiluokan Ord metodi (<) tarkoittaa tämän tyypin kanonista eli sitä ainoaa luonnollista järjestystä. Siten esimerkiksi tyypin String tämä oikea järjestys on edellisen säännön nojalla merkkien oikeaan järjestykseen perustuva sanakirjajärjestys. Siinä siis suuret ja pienet kirjaimet erotellaan toisistaan. Joskus ohjelmoija haluaisikin sellaisen järjestyksen, jossa niitä ei eroteltaisikaan toisistaan eli liittääkin Stringit tällä toisella tavalla tyyppiluokkaan Ord. Haskell-ohjelmointifilosofiassa sellaiset merkkijonot ovat kuitenkin uusi eri tyyppi kuin String koska niiden ominaisuudet poikkeavat toisistaan. Tällaisessa tilanteessa on luontevaa määritellä se newtype MunString = SunStringista { sunstringiksi :: String } jonka jälkeen ohjelmoija voi sitten määritellä itse miten tämä hänen oma MunStringinsa on tyyppiluokan Ord instanssi. Jatketaan sitten sanakirjaesimerkkiämmme, mutta palataan takaisin nimettömiin kenttiin. Tehdään nyt myös sen avainkentästä, joka ennen oli tyyppiä Int, polymorfinen tyyppiä k. Koska avainarvoja pitää vertailla funktiossa lisaa, niin meidän täytyy asettaa sille rajoite (Ord k) =>... Nyt voisi olla luontevaa asettaa se koko datatyyppimäärittelyyn, koska voitaisiin luontevasti ajatella, ettei koko tyyppiä hakupuu k t olekaan olemassa ellei sen avaintyyppi k ole järjestetty. Asetetaan se kuitenkin sen sijaan funktioittain nähdäksemme, mihin se tarvitaan ja missä pärjätään ilman sitä. 70

Se tarvitaan siis ainakin funktioon lisaa......sekä myös funktioon listasta koska se kutsuu sitä. Mutta se tarvitaan vain niihin, koska Puun muuntamiseen listaksi ei tarvita avainten järjestystä. 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 [] Nyt siis Haskell päättelee esimerkiksi seuraavat tyypit: *Main> listasta [("hei",12),("taas",5.5)] Solmu (Solmu Tyhja "hei" 12.0 Tyhja) "taas" 5.5 Tyhja it :: Puu [Char] Double *Main> listasta [((1,3),True),((2,7),False)] Solmu (Solmu Tyhja (1,3) True Tyhja) (2,7) False Tyhja it :: Puu (Integer, Integer) Bool Siten Haskell todellakin osaa päätellä nyt myös avaintyypin k kuten tavoittelimmekin. Haskell on jopa niin nokkela, että se osaa päätellä tyypin(integer,integer) olevan järjestetty eli kelpaavan avaimeksi! Sen Haskell tekee säännöllään Jos Ord a ja Ord b niin myös Ord (a,b). Näin ohjelmoijan on varsin vaivatonta ottaa käyttöön sellaisiakin hakupuutyyppejä, joiden avaintyyppi on monimutkainen koska hänen ei tarvitse itse kirjoittaa niiden tyyppejä eikä määritellä niiden arvojen keskinäistä järjestystä mutta hän voi silti tehdä niinkin, jos haluaa. 71

5.2.1 Johdetut instanssit Olemme tähän saakka kirjoittaneet data- ja newtype-määrittelyjemme loppuun...deriving (Show) selvittämättä mitä se oikein tarkoittaa. Se tarkoittaa intuitiivisesti että liitä tämä minun uusi tyyppini tyyppiluokan Show instanssiksi siihen tavalliseen tapaan. Hyvin tavallinen tyyppimäärittelyn lopetus on esimerkiksi deriving (Eq,Ord,Show) joka intuitiivisesti tarkoittaa, että salli tämän tyypin arvoille tavalliset yhtäsuuruusvertailut (==), järjestysvertailut (<) ja näyttämisen (show) käyttäjälle. Se siis säästää ohjelmoijalta sen tavallisen tavan kirjoittamisen mutta sekin on sallittua, eli tämän deriving-automatiikan voi myös ohittaa, jos haluaakin tehdä jotakin muuta kuin sen tavallisen tavan. Tutustutaan seuraavaksi sellaisiin keskeisiin tyyppiluokkiin, joille Haskell tarjoaa tämän deriving-automatiikan, eli joille se osaa tarvittaessa johtaa instanssit automaattisesti. Yhtäsuuruus Tyyppiluokka Eq tarkoittaa niitä tyyppejä, joille on määritelty yhtäsuuruusvertailu (==). Silloin niille on automaattisesti määritelty myös sen vastakohta eli erisuuruusvertailu (/=). Yhtäsuuruusvertailun voi johtaa automaattisesti valtaosalle Haskellin tyypeistä. Poikkeuksena ovat vain funktiotyypit ja I/O-operaatioiden tyyppi (johon palaamme myöhemmin) sekä kaikki tyypit joiden osina ne esiintyvät. (Esimerkiksi funktioiden yhtäsuuruusvertailuhan tiedettiin ratkeamattomaksi ongelmaksi Ricen lauseen nojalla.) Jos ohjelmoija haluaakin määritellä tyypilleen yhtäsuuruusvertailun itse, niin hänen riittää kuormittaa sille toinen näistä operaattoreista (==) ja (/=). Järjestys Olemme jo hakupuuesimerkissämme kohdanneet tyyppiluokan Ord eli ne tyypit, joilla on järjestysrelaatio (<). Myös järjestysrelaation voi johtaa automaattisesti valtaosalle Haskellin tyypeistä. Poikkeuksena ovat vain funktiotyypit sekä I/O-operaatioiden ja I/O-virheiden tyypit sekä kaikki tyypit joiden osina ne esiintyvät. Tämä automaattisesti johdettu järjestys vertailee tyypin arvoja seuraavasti: 1. Jos niillä on eri konstruktorit, niin sitten se arvo on pienempi, jonka konstruktori esiintyy ensin data-määrittelyssä 72

2. Jos niiden konstruktorit ovat samat, niin sitten aletaan vertailla niiden kenttiä siinä järjestyksessä kuin ne esiintyvät data-määrittelyssä. Jos ohjelmoija haluaakin määritellä omalle tyypilleen a järjestyksen itse, niin silloin hän varmistaa, että tämä tyyppi a on myös tyyppiluokan Eq instanssi. Tyyppi ei voi olla järjestetty ellei sillä ole yhtäsuuruutta, eli kyseessä on totaali järjestysrelaatio. kuormittaa tyypilleen operaattorin (<=) eli pienempi tai yhtasuuri kuin. Muut operaatiot (<), (>), (>=), max, min ja compare tulevat määritellyiksi automaattisesti sen pohjalta. Tai vaihtoehtoisesti hän voi määritellä tyypilleen tämän metodin, jonka tyyppi on data Ordering = LT EQ GT deriving (Eq, Ord, Enum, Read, Show, Bounded) compare :: (Ord a) => a -> a -> Ordering eli Less Than, EQual to tai Greater Than. Tulostaminen merkkijonoksi Tyyppiluokka Show on ne tyypit t, joiden arvot voi tulostaa merkkijonoksi. Kun juuri laskettu arvo kuuluu tällaiseen luokkaan, niin silloin Haskell-tulkkimme ghci näyttää sitä vastaavan merkkijonotulosteen. Muuten ghci ilmoittaakin, että se kyllä sai laskettua pyydetyn arvon, muttei tiedä kuinka sen näyttäisi käyttäjälleen. Tämä keskeisin tulosta merkkijonoksi -funktio on nimeltään show. (Tyyppiluokkaan Show kuuluu muitakin sen sukuisia funktioita.) Sen automaattisesti johdettu muoto tulostaa arvon samaan tapaan kuin sen voisi kirjoittaa Haskell-lausekkeenakin. Merkkijonosta lukeminen Tyyppiluokan Show vastaluokka on Read eli ne tyypit t, joiden arvot voi lukea merkkijonosta. Funktion show vastineena on lue merkkijonosta -funktio nimeltään read. (Tyyppiluokkaan Read kuuluu muitakin sen sukuisia funktioita.) Jos tyyppi t kuuluu tyyppiluokkiin Eq, Show ja Read, niin silloin on syytä vaatia, että sen jokaiselle arvolle x pätee read $ show x == x 73

eli että jos arvo tulostetaan ensin merkkijonoksi ja sitten se luetaan takaisin arvoksi, niin saadaan takaisin juuri sama arvo. Jos tämä luku-kirjoitus-muuttumattomuus (read-write invariance) ei pädekään, niin silloin ohjelman I/O ei olekaan aivan uskollinen viittausten läpinäkyvyydelle. Se pätee automaattisesti johdetuille funktioille read ja show. Usein funktiota read käytettäessä tarvitaan jokin ohjelmoijan kirjoittama tyyppiinformaatio kertomaan, minkä tyyppiseksi arvoksi tämä merkkijono on tarkoitus tulkita. Miten Haskell muuten tietäisi esimerkiksi lausekkeesta read "123" onko nyt tarkoitus lukea luku 123 Inttinä, Integerinä, Floatina vai Doublena? Siis esimerkiksi funktio muunna annettu merkkijono matemaattisen kokonaisluvun ja pitkän liukuluvun pariksi voidaan kirjoittaa vaikkapa pariksi :: String -> (Integer,Double) pariksi = read jossa sen tyyppi ja tyypinpäättely pitävät huolen siitä, että tässä tämä readin versio lukee mitä sen pitääkin. Tämän tyyppiluokan funktiot ovat melko yksinkertaisia, joten syötteiden monimutkaistuessa ohjelmoijan on usein syytä kirjoittaa itse parempi jäsennin (parser). Luetellut tyypit Tyyppi on lueteltu (enumeration) jos sen yhdelläkään konstruktorilla ei ole yhtään kenttää. Esimerkiksi Bool, Ordering ja Char ovat lueteltuja. Samoin kokonaislukutyypit ajatellaan luetelluiksi, koska niilläkin voitaisiin ajatella olevan data-määrittely konstruktorittomin kentin kuten data Integer =. -2-1 0 1 2. vaikka sellaista ei tietenkään todellisuudessa voida kirjoittaa. Sellaiselle voi sanoa... deriving (Enum,...) joka liittää sen lueteltujen tyyppien tyyppiluokkaan. 74

Tällaisella luetellulla tyypillä on luontevasti funktiot successor eli seuraava arvo sekä sen vastinpari predecessor eli edellinen arvo. Liukuluvutkin ajatellaan luetelluiksi tyypeiksi ottamalla näiksi funktioksi (± 1.0). Jos tyyppi t kuuluu tyyppiluokkaan Enum, niin silloin vastaavassa listatyypissä [t] voidaan käyttää luvun 3.2 lukusarjanotaatiosta tuttua merkintää... Siten esimerkiksi seuraava tulee mahdolliseksi: data Viikonpaivat = Ma Ti Ke To Pe La Su deriving (Enum,Eq,Ord,Show,Bounded) arki = [Ma.. Pe] jokatoinen = cycle $ [Ma,Ke..] ++ [Ti,To..] Ala- ja ylärajat Tyyppiluokka Bounded on ne tyypit, joilla on ala- ja ylärajat, eli pienin arvo nimeltään minbound suurin arvo nimeltään maxbound. Siis esimerkiksi Int kuuluu tähän luokkaan, ja siten minbound :: Int on pienin ja maxbound :: Int suurin ohjelmoijan kokonaisluku. Jokaisella äärellisellä luetellulla tyypillä joko jo on nämä ala- ja ylärajat tai ne voidaan määritellä sille deriving-automatiikalla. Automatiikka ottaa silloin alarajaksi ensimmäisen ja ylärajaksi viimeisen konstruktorin. Automatiikka soveltuu myös jokaiseen sellaiseen tyyppiin, jolla on vain yksi konstruktori jonka jokaisella kentällä on ala- ja ylärajat. Erityisesti k-kenttäinen monikko on tällainen tyyppi, joten esimerkiksi *Main> minbound :: (Viikonpaivat,Char) (Ma, \NUL ) it :: (Viikonpaivat, Char) Sivuhuomautus: Jos joskus täytyy kirjoittaa tämä k-kenttäisen monikon ainoa konstruktori, niin se on (,,,...,) :: τ }{{} 1 -> τ 2 -> -> τ k -> (τ 1,τ 2,...,τ k ). k 1 kpl. 75

5.2.2 Lukutyyppiluokat Tutustutaan seuraavaksi joihinkin Haskellin lukutyyppiluokkiin. Jotkut niistä vastaavat jotakin algebrallisten rakenteiden kokoelmaa. Perusluokka Num eli luvut yleensä vastaa algebrallista rakennetta nimeltään rengas (ring). Siellä on määritelty seuraavat operaatiot: yhteenlasku (+) vähennyslasku (-) etumerkin käsittely eli luvun itseisarvo (abs), pelkän etumerkin otto (signum) ja etumerkin vaihto (negate) vakio frominteger joka muuntaa annetun pitkän kokonaislukuvakion tämän tyyppiseksi vakioksi. Sen avulla Haskell-koodissa esiintyvät kokonaislukuvakiot tulkitaan automaattisesti halutun lukutyypin vakioarvoksi. Perusluokka Integral eli kokonaisluvut jakolaskulla vastaa algebrallista rakennetta nimeltään Euklidinen (Euclidean) rengas. Siinä luokkaan Num lisätään myös sellainen jakolasku, josta jää tätä samaa tyyppiä oleva osamäärä ja jakojäännös. Siis sellaiset funktiot kuin quot, rem,... Perusluokka Fractional eli luvut yleensä jakolaskulla vastaa algebrallista rakennetta nimeltään kunta (field). Siinä luokkaan Num lisätäänkin sellainen jakolasku (/), jonka tuloskin on tätä samaa tyyppiä. Siten kokonaislukutyypit eivät kuulukaan tähän luokkaan, mutta liuku- ja murtolukutyypit kuuluvat. Liukulukutyyppien erilaisia funktioita on ryhmitelty seuraaviin tyyppiluokkiin: Floating sisältää trigonometrisia yms. funktioita kuten sin RealFrac sisältää muunnokset kokonaislukuihin kuten floor yms. RealFloat sisältää liukulukujen fyysiseen esitykseen liittyviä funktioita kuten kuinka monen bitin laskentatarkkuutta ne käyttävät yms. Haskell-kieli tarjoaa siis myös murtoluvut, mutta ne eivät ole heti käyttövalmis osa itse kieltä, vaan ohjelmoijan pitää ottaa ne käyttöön kirjoittamalla lähdekoodinsa alkuun import Data.Ratio jonka jälkeen hänellä on käytössään tyyppi Rational eli matemaatikon mielivaltaisen tarkkuuden murtoluvut, joiden osoittaja ja nimittäjä ovat tyyppiä Integer 76

tyyppikonstruktori Ratio jolla hän voi käyttää sen sijasta omia pienemmän tarkkuuden mutta nopeampia murtolukutyyppejään. Esimerkiksi tyyppi Ratio Int on sellaiset murtoluvut, joiden osoittaja ja nimittäjä ovatkin tyyppiä Int. Sen jälkeen hän voi laskea myös niillä. Vastaavalla tavalla voi ottaa käyttöön kompleksiluvut kirjastosta Data.Complex. 5.2.3 Instanssin määrittely Kun ohjelmoija haluaa liittää itse määrittelemänsä tyypin t tyyppiluokkaan C joka ei tue deriving-automatiikkaa, tai tukisi, mutta ohjelmoija haluaakin liittää tyyppinsä t tyyppiluokkaan C jollakin muulla kuin automatiikan tarjoamalla tavalla niin hän kirjoittaa instanssin määrittelyn muotoa instance C t where. jonka sisällä hän kuormittaa tämän tyyppiluokan C funktiot tälle omalle tyypilleen t. Otetaan ensimmäiseksi esimerkiksemme tilanne, jossa ohjelmoija tarvitsee sellaiset matemaattiset kokonaisluvut, joissa on myös negatiivinen ja positiivinen äärettömyys. Ensiksi ohjelmoija määrittelee oman tyyppinsä InfInteger jonka arvot ovat Hän saa deriving-automatiikasta niiden,..., 2, 1, 0, +1, +2,..., +. }{{} tyypin Integer arvot yhtäsuuruuden Eq koska hänelle kelpaa se tavallinen yhtäsuuruus kuten valtaosassa ohjelmointitilanteista järjestyksen Ord kunhan hän kirjoittaa konstruktorinsakin haluttuun järjestykseen. Toisaalta hän haluaakin tulostaa äärettömyyksiinsä ± etumerkit, joten hän ei käytäkään automatiikkaa tyyppiluokalle Show vaan kirjoittaakin sen instanssin itse. Tämän instanssinsa sisällä hän sitten kuormittaa sen metodin show tälle omalle tyypilleen InfInteger haluamallaan tavalla. Tulkki ghci käyttää tätä tulostusasua sen arvoille. data InfInteger = NegInf Fin Integer PosInf 77

deriving (Eq,Ord) instance Show InfInteger where show NegInf = "-Infinite" show PosInf = "+Infinite" show (Fin n) = "Finite " ++ show n instance Bounded InfInteger where minbound = NegInf maxbound = PosInf instance Num InfInteger where -- kokonaislukuvakiosta frominteger = Fin -- yhteenlasku (Fin x) + (Fin y) = Fin (x + y) (Fin _) + y = y x + (Fin _) = x NegInf + NegInf = NegInf PosInf + PosInf = PosInf -- vastaluku negate NegInf = PosInf negate (Fin x) = Fin (negate x) negate PosInf = NegInf -- itseisarvo abs NegInf = PosInf abs (Fin x) = Fin (abs x) abs PosInf = PosInf -- etumerkki signum NegInf = -1 signum (Fin x) = Fin (signum x) signum PosInf = 1 -- kertolasku (Fin 0) * _ = 0 (Fin x) * (Fin y) = Fin (x * y) (Fin x) * y x>0 = y otherwise = negate y NegInf * NegInf = PosInf NegInf * PosInf = NegInf PosInf * NegInf = NegInf PosInf * PosInf = PosInf x * y = y * x Tyypillä InfInteger on myös luontevat ala- ja ylärajat, joten ohjelmoija voi ilmaista myös ne. Tyyppiä InfInteger ei kuitenkaan voi pitää lueteltuna: Minkä arvon antaisit esimerkiksi lausekkeelle succ NegInf eli arvoa seuraava suurempi arvo? 78

Ohjelmoija haluaa myös laskeakin tämän tyyppisillä luvuillaan, joten hän liittää tämän tyyppinsä InfInteger myös luokkaan Num. Haskell-standardin mukaan ohjelmoijan täytyy kuormittaa sen metodeista kaikki muut paitsi joko vähennyslaskua tai etumerkin vaihtoa niistä toisen Haskell osaa kuormittaa itsekin kunhan toinen on kuormitettu. Erityisesti frominteger-kuormitus mahdollistaa InfInteger-tyyppisten kokonaislukuvakioiden käytön. Tosin vähennyslaskun tulosta ei voi määritellä, koska sitä varten pitäisi tietää, kumpi näistä äärettömyyksistä on suurempi ja paljonko suurempi. Siinä tilanteessa Haskell keskeyttää laskennan suoritusaikaiseen virheeseen muuta ei liene tehtävissä. Otetaan toisena esimerkkinämme tyyppikonstruktori, jotta nähdään sen tyyppiparametrien käsittelyä. Käytetään jälleen hakupuutamme (ilman kentännimiä) jossa sekä avain- että tietokenttä on monimuotoinen. Kaksi sanakirjaa ovat samat, jos ne sisältävät täsmälleen samat (avain,tieto)- parit. Jos hakupuuta ajatellaan sanakirjana, niin silloin kaksi hakupuuta ovat siis samat, jos kummassakin on täsmälleen samat(avain,tieto)-parit riippumatta siitä, minkä muotoisia nämä puut ovat. Jos käytettäisiin hakupuulle automatiikkaa deriving (Eq) niin saataisiin(==) joka ottaisi huomioon puiden muodon. Määritellään siis sen sijaan hakupuulle käsin sellainen tyyppiluokan Eq-instanssi, joka vertaileekin vain niiden (avain,tieto)-pareja. Ensimmäinen versiomme alkaa siten instance Eq (Puu k t) where jonka voi lukea että tyyppi Puu k t kuuluu tyyppiluokkaan Eq siten, että... Nyt kuitenkin törmäämme ongelmaan: Jotta voisimme määritellä instanssin sisällä sen haluamamme operaattorin (==) :: Puu k t -> Puu k t -> Bool me tarvitsemme sitä varten vastaavat operaattorit (==) :: k -> k -> Bool (==) :: t -> t -> Bool myös sen avainkentän tyypille k ja tietokentän tyypille t. Toisin sanoen, myös sen avainkentän tyypin k ja tietokentän tyypin t pitää kuulua tyyppiluokkaan Eq. 79

Kun lausumme nämä rajoitteet julki, niin saamme lopullisen versiomme alkamaan instance (Eq k,eq t) => Eq (Puu k t) where jonka voi lukea että jos tyyppi k kuuluu tyyppiluokkaan Eq ja tyyppi t kuuluu tyyppiluokkaan Eq niin siinä tapauksessa myös tyyppi Eq Puu k t kuuluu tyyppiluokkaan Eq siten, että... Nyt pääsemme kirjoittamaan tämän instanssimme sisältöä eli tuota siten, että... -osaa eli ohjelmoimaan haluamaamme operaattoria (==) :: Puu k t -> Puu k t -> Bool 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 [] instance (Eq k,eq t) => Eq (Puu k t) where p == q = listaksi p == listaksi q Itse ohjelmointi taas on suoraviivaista: 1. Ensin muunnetaan kumpikin syötepuu p ja q listaksi (avain,tieto)-pareja. Tämä muunnos listaksi ohjelmoitiin siten, että nämä parit ovat järjestyksessä avain tensa suhteen. Koska kyseessä on sanakirja, niin yhtä avain ta kohden on vain yksi tieto. 2. Sitten riittää verrata näitä (avain,tieto)-parien listoja toisiinsa niiden operaattorilla 80

(==) :: [(k,t)] -> [(k,t)] -> Bool jonka olemassaolon varmistamiseksi me itse asiassa tarvitsimme sen rajoitteen että tyyppi k kuuluu tyyppiluokkaan Eq ja tyyppi t kuuluu tyyppiluokkaan Eq. Haskellin tyypinpäättely osaa nyt tuottaa tämän listavertailuoperaattorin: Haskell tuntee säännön jos alkiotyyppi u kuuluu tyyppiluokkaan Eq niin myös listatyyppi [u] kuuluu luokkaan Eq siten, että.... Haskell tuntee myös säännön jos tyyppi v kuuluu tyyppiluokkaan Eq ja tyyppi w kuuluu tyyppiluokkaan Eq niin myös tyyppi (v,w) kuuluu tyyppiluokkaan Eq siten, että.... Sitten Haskell yhdistelee nämä siten, että... -osat sopivasti. Itse asiassa me kirjoitimme itsekin juuri yhden tällaisen säännön lisää: Oman instanssimme joka alkoi instance (Eq k,eq t) => Eq (Puu k t) where ja jonka sisältö antoi sen siten, että... -osan. Tällaisen säännön voi tulkita eräänlaisena perintänä : Koko hakupuutyyppimme perii oman yhtäsuuruutensa (==) kenttiensä yhtäsuuruuksista. Näillä instanssimäärittelyillä voi lisätä uusia tyyppejä tyyppiluokkiin. Mutta niillä ei voi muuttaa jo aikaisemmin määriteltyjä instansseja muutenhan niillä voisi muuttaa kaikessa hiljaisuudessa jo kertaalleen kirjoitettua ja testattua ohjelmakoodia koskematta sihen... 5.2.4 Oman tyyppiluokan määrittely Siirrytään sitten kokonaan oman tyyppiluokan määrittelyyn. Sen syntaksi on class TyyppiLuokanNimi tyyppiparametri where. jonka sisällä esitellään sen jokaisen kuormitettavan metodin nimi ja tyyppi voi määritellä sen metodeille oletustoteutuksia. Tällaista oletustoteutusta käytetään silloin, jos instanssimäärittely ei annakaan sille omaa toteutustaan. Lisäksi heti varatun sanan class jälkeen jälleen olla rajoiteosa jossa voi ilmaista, mitä tältä tyyppiparametrilta pitää olettaa. Tässä luokkamäärittelyssä 81

TyyppiLuokanNimi on nyt määriteltävän luokan nimi tyyppiparametri edustaa sitä tyyppiä, jonka nyt on tarkoitus kuulua tähän luokkaan eli sen voi lukea että tyyppiparametri n arvo t kuuluu tyyppiluokkaan nimeltä TyyppiLuokanNimi silloin, kun tälle tyypille t on määritelty metodit nimeltä... Otetaan esimerkiksemme määritella kokonaislukujen neliöjuurifunktio: Sovitaan, että kokonaisluvun n neliöjuuri isqrt n on suurin sellainen kokonaisluku m jolla m 2 n. Siis matemaattisesti m = n (7) jota ei kuitenkaan voi aina käyttää ohjelmoinnissa: Entä jos n onkin niin iso Integer ettei Doublen neliöjuurifunktion sqrt laskentatarkkuus enää riitäkään? Asetetaan siis tavoitteeksemme määritellä sellainen kuormitettu funktio isqrt :: t -> t jossa käytetään yhtälöä (7) niille tyypeille t joilla ei ole vielä pyöristysvirheen riskiä. Sen sijaan tämä isqrt toteutetaankin hitaampana binäärihakuna sellaiselle tyypeille t jolla tiedetään olevan pyöristysvirheen riski, tai josta tiedetään vain, että se on jonkinlainen kokonaislukutyyppi. Lisäksi ohjelmoijalle halutaan tarjota mahdollisuus saada hänen itsensä määrittelemille uusille kokonaislukutyypeille automaattisesti ainakin tämä binäärihakumenetelmä, sekä manuaalisesti käyttöön yhtälön (7) sitä nopeampi menetelmä. Tätä varten määritellään uusi oma tyyppiluokka Isqrt jossa on rajoite että siihen kuuluvan tyypin pitää kuulua myös tyyppiluokkaan kokonaisluvut eli Integral ainoa metodi on tämä isqrt sille annetaan oletusmäärittely binäärihakuna. Sitä käytetään, ellei ohjelmoija erikseen sano että käytä tälle tyypille yhtälöä (7). Lisäksi kirjoitetaan yhtälö (7) funktiona hwsqrt. Se jätetään tämän tyyppiluokan Isqrt ulkopuolelle, koska sitä itseään ei ole tarpeen kuormittaa: Yhtälö (7) ei riipu kokonaisluvun n tyypistä. 82

Se kuitenkin määritellään monimuotoiseksi: Siinä käytetty operaatio muunna n Doubleksi riippuu kokonaisluvun n tyypistä funktio fromintegral on kuormitettu. Matemaatikon kokonaisluvuille Integer käytetään tätä oletusmäärittelyä, koska niissä on varmasti ylivuodon riski. Silloin voimme jättää where-osan pois. {- Kokonaisluvun n neliöjuuri on suurin kokonaisluku m jolla m^2<=n. -} import Data.Int import Data.Word import Data.Bits {- Perusratkaisu on käyttää binäärihakua. -} class (Integral t) => Isqrt t where isqrt :: t -> t isqrt 0 = 0 isqrt 1 = 1 isqrt n n > 1 = {- binäärihaun invariantti: p^2 < n < q^2 -} let bsearch p q = if q-p>1 then let m = (p+q) div 2 in case (m^2) compare n of LT -> bsearch m q EQ -> m GT -> bsearch p m else p in bsearch 0 n {- Laitetason neliöjuuri IEEE-liukulukuaritmetiikalla. -} hwsqrt :: (Integral t) => t -> t hwsqrt = floor. (sqrt :: Double -> Double). fromintegral {- Matemaatikon kokonaisluvuille oletusmenetelmällä. -} instance Isqrt Integer {- Laskeeko laitetaso oikein tämän luvun neliöjuuren? -} hwisok :: (Integral t) => t -> Bool hwisok n = 83

let m :: Integer m = isqrt $ fromintegral n in m == fromintegral (hwsqrt n) {- Jos hwisok (maxbound::t) niin silloin tyypille t sallitaan laitetason käyttö. -} instance Isqrt Int8 where isqrt = hwsqrt instance Isqrt Int16 where isqrt = hwsqrt instance Isqrt Int32 where isqrt = hwsqrt instance Isqrt Int64 where isqrt = hwsqrt instance Isqrt Int where isqrt = hwsqrt instance Isqrt Word8 where isqrt = hwsqrt instance Isqrt Word16 where isqrt = hwsqrt instance Isqrt Word32 where isqrt = hwsqrt instance Isqrt Word64 where isqrt = sqrt64 instance Isqrt Word where isqrt = if bitsize (undefined::word) < 64 then hwsqrt else sqrt64 {- Etumerkittömän 64-bittisen luvun viimeinen 1k aiheuttaisi pyöristysvirheen laitetasolla. -} sqrt64 :: (Bounded t,integral t) => t -> t sqrt64 n = hwsqrt $ min n $ maxbound - 1024 Apufunktio hwisok ilmoittaa, onnistuiko hwsqrt laskemaan syötelukunsa neliöjuuren ilman pyöristysvirhettä. Tämä selviää vertaamalla sen tulosta Integerilla laskettuun virheettömään tulokseen m. Silloin kutsu hwisok (maxbound :: t) ilmoittaa, riittääkö hwsqrt-tarkkuus tälle tyypille t... tällä ghci-toteutuksella! Jos se vastaa True niin silloin voimme kirjoittaa vastaavan instance Isqrt t where isqrt = hwsqrt 84

ottamaan sen käyttöön. Vakiokirjastossa Data.Int määritellään useita kokonaislukutyyppejä niiden bittimäärän mukaan: Tyyppi Intb on b-bittiset etumerkilliset kokonaisluvut jossa tämä bittimäärä b = 8, 16, 32, 64. Tässä siis tuo b on osa tyypin nimeä tekstinä, eli tyyppi nimeltään Word16 on 16-bittiset kokonaisluvut jne. Myös kaikille niillekin hwisok-testi sallii hwsqrt-funktion käytön. Vastaavasti vakiokirjastossa Data.Word määritellään useita etumerkittömiä kokonaislukutyyppejä: Wordb samoille bittimäärille b = 8, 16, 32, 64. Word toteutuksen luonnolliselle bittimäärälle. Samoin kuin Int on toteutuksen luonnollinen etumerkillinen kokonaislukutyyppi. Luennoijan kannettavassa nämä molemmat luonnolliset tyypit ovat 64- bittisiä. Opetussaleissa tämä luonnonvakio taas vaikuttaa olevan 32 bittiä... Kun lähestytään etumerkittömien 64-bittisten kokonaislukujen eli tyypin Word64 (ja luennoijan koneessa siis myös tyypin Word) laskentatarkkuuden ylärajaa, niin havaitaan (funktion hwisok avulla) että sen 1024 suurimmalla arvolla hwsqrt alkaakin laskea väärin. Tämä pyöristysvirhe voidaan korjata funktiolla sqrt64. Nyt olemme saavuttaneet tavoitteemme: Funktio isqrt on määritelty kaikille Haskellin mukana tuleville kokonaislukutyypeille...... siten, että jokainen niistä käyttää nopeaa funktiota hwsqrt mikäli mahdollista. Jos ohjelmoija määrittelee oman kokonaislukutyyppinsä, niin sillekin tulee automaattisesti oikean tuloksen joskin hitaasti antava isqrt, koska tämä uusi tyyppi liittyy tyyppiluokkaan Integral jolle teimme vastaavan säännön. Ohjelmoija voi ottaa käyttöön myös tälle uudelle tyypilleen nopeamman funktion hwsqrt kirjoittamalla vastaavan instancen. 85