266
Sissejuhatus Funktsionaalne programmeerimisparadigma Paradigma järgi liigitub funktsionaalne (ingl functional) programmeerimine koos loogilise (ingl logic) programmeerimisega deklaratiivse programmeerimise alla, vastandudes imperatiiv- sele programmeerimisele. Kui imperatiivses (ingl imperative) programmeerimises kirjeldab kood vahetult arvutusprot- sessi käiku, siis deklaratiivne (ingl declarative) programmeerimine tähendab seda, et prog- rammikood kirjeldab otseselt võttes matemaatilisi väärtusi ja abstraktseid seoseid nende vahel. Loomulikult vastab deklaratiivses keeles kirjutatud korrektsele koodile ka mingi arvutusprotsess, mis koodi jooksutamisel toimub, sest jutt on ikkagi programmeerimiskeeltest. Kuid, erinevalt im- peratiivsest programmeerimisest, pole arvutusprotsessi detailid deklaratiivses keeles kirjutatud koodis ilmutatud, need on määratud kaudselt. Distantseerumine arvutusprotsessi detailidest on üks modulaarsuse vorm. Modulaarsus prog- rammeerimises tähendab üldiselt igasugust erilaadse info selget üksteisest lahushoidmist koodis. Eri vaadete, erilaadse info hoidmine eraldi võimaldab inimesel kergemini neid vaateid ja ko- gu koodi mõista ja muudab koodi hoolduse vähem töömahukaks. Modulaarsuse mitmeid vorme realiseerivad ka tänapäeva tuntud imperatiivsed programmeerimiskeeled. Niisiis võimaldab deklaratiivne paradigma esitada abstraktsed seosed arvutusprotsessi osaliste — näiteks sisendi ja väljundi — vahel ilma protsessi detailideta. Nii saab deklaratiivses keeles kirjutatud koodi mõtet suurel määral mõista ka täiesti ilma tähelepanu pööramata sellele, kuidas masin selle järgi asju reaalselt arvutaks. Võrdluseks, imperatiivses keeles kirjutatud koodi mõte selgubki alles arvutusprotsessi detailidest. Teisest küljest nõuab edukas programmeerimine deklaratiivses keeles rohkem õppimist ja mõtte- tööd kui imperatiivses, sest kuigi arvutusprotsessi detaile pole vaja kirjutada, tuleb nendega siiski arvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te- ma kirjutatud koodi järgi arvutus toimub, kuigi seda koodis otseselt kirjas ei ole. Sama abstraktset funktsiooni on võimalik arvutada kiiremini või aeglasemalt ja pole kasu koodist, mis kirjeldab abstraktselt küll õiget seost, kuid mille järgi arvutamine võtab rohkem aega kui on Universumi 1

Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Sissejuhatus

Funktsionaalne programmeerimisparadigma

Paradigma järgi liigitubfunktsionaalne (ingl functional) programmeerimine koosloogilise

(ingl logic) programmeerimisega deklaratiivse programmeerimise alla, vastandudes imperatiiv-sele programmeerimisele.

Kui imperatiivses (ingl imperative) programmeerimises kirjeldab kood vahetult arvutusprot-sessi käiku, siisdeklaratiivne (ingl declarative) programmeerimine tähendab seda, et prog-rammikood kirjeldab otseselt võttes matemaatilisi väärtusi ja abstraktseid seoseid nende vahel.

Loomulikult vastab deklaratiivses keeles kirjutatud korrektsele koodile ka mingi arvutusprotsess,mis koodi jooksutamisel toimub, sest jutt on ikkagi programmeerimiskeeltest. Kuid, erinevalt im-peratiivsest programmeerimisest, pole arvutusprotsessidetailid deklaratiivses keeles kirjutatudkoodis ilmutatud, need on määratud kaudselt.

Distantseerumine arvutusprotsessi detailidest on üks modulaarsuse vorm.Modulaarsus prog-rammeerimises tähendab üldiselt igasugust erilaadse infoselget üksteisest lahushoidmist koodis.Eri vaadete, erilaadse info hoidmine eraldi võimaldab inimesel kergemini neid vaateid ja ko-gu koodi mõista ja muudab koodi hoolduse vähem töömahukaks.Modulaarsuse mitmeid vormerealiseerivad ka tänapäeva tuntud imperatiivsed programmeerimiskeeled.

Niisiis võimaldab deklaratiivne paradigma esitada abstraktsed seosed arvutusprotsessi osaliste— näiteks sisendi ja väljundi — vahel ilma protsessi detailideta. Nii saab deklaratiivses keeleskirjutatud koodi mõtet suurel määral mõista ka täiesti ilmatähelepanu pööramata sellele, kuidasmasin selle järgi asju reaalselt arvutaks. Võrdluseks, imperatiivses keeles kirjutatud koodi mõteselgubki alles arvutusprotsessi detailidest.

Teisest küljest nõuab edukas programmeerimine deklaratiivses keeles rohkem õppimist ja mõtte-tööd kui imperatiivses, sest kuigi arvutusprotsessi detaile pole vaja kirjutada, tuleb nendega siiskiarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodi järgi arvutus toimub, kuigi seda koodisotseselt kirjas ei ole. Sama abstraktsetfunktsiooni on võimalik arvutada kiiremini või aeglasemalt ja pole kasu koodist, mis kirjeldababstraktselt küll õiget seost, kuid mille järgi arvutaminevõtab rohkem aega kui on Universumi

1

Page 2: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

eluiga.

Kuigi ajalooliselt ilmusid imperatiivne ja deklaratiivneparadigma enam-vähem samaaegselt (esi-mese imperatiivse programmeerimiskeelena Turingi masin 1936, esimese deklaratiivse program-meerimiskeelena lambdaarvutus samuti 1936), on tänapäeval imperatiivsed keeled levinumad jakasutatavamad kui deklaratiivsed.

Funktsionaalse paradigma olemuslikud omadused

Käsitleme siin lähemalt tunnuseid, mis on omased eelkõige funktsionaalsele paradigmale.

Esimene tunnus on avaldiste väärtustamise keskne roll arvutusprotsessis. Arvutus, mida tahe-takse programmeerida, esitatakse avaldisena ja masina tööseisneb selle avaldise väärtustamisesja täitmises. Tüüpilisest funktsionaalsest programmist moodustavad suurema osa definitsioonid,mis kirjeldavad abstraktseid seoseid andmete vahel, ühtlasi aga kujutavad endast avaldiste väär-tustamise reegleid, mille järgi masin neid teisendab.

Teine tunnus on üksikandmete, andmestruktuuride, funktsioonide ja protseduuride süntaktili-ne eristamatus. Funktsioonid on andmed nagu näiteks täisarvudki ja funktsioonid saavad see-ga esineda kõigis neis rollides kus täisarvud. Näiteks võibiga funktsioon olla mõne avaldiseväärtuseks, teise funktsiooni argumendiks või mõne andmestruktuuri komponendiks. Selle kohtaöeldakse, et funktsioonid onesimese kategooria (ingl first-class) väärtused; sama kehtib kaandmestruktuuride ja protseduuride kohta.

Puhtalt funktsionaalses keeles nagu Haskell, mida me hiljem lähemalt tundma saame, on tõe-poolest ka protseduurid — arvutusprotsessid — käsitatud abstraktsete andmetena. On olemasfunktsioonid, mis väiksematest protsessidest panevad kokku suuremaid. See võimaldabki soovi-tavat arvutusprotsessi esitada avaldise kujul. Avaldise “väärtustamine ja täitmine”, millele ülalviitasime, tähendab avaldise väärtustamist ja väärtuseksoleva protsessi läbiviimist.

Kolmandana pöörame tähelepanu sellisele väga olulisele tunnusele naguilmutatud viidata-

vus (ingl referential transparency), mis sätestab, et alamavaldis mõjutab kogu avaldise väär-tuse kujunemist alati ainult oma väärtusega. See tähendab,et alamavaldist võib alati asendadasama väärtusega avaldisega, tulemus sellest ei muutu. Näiteks kui log tähendab naturaalloga-ritmi ja aritmeetilised avaldised1 + 1 ja 2 on oma tavapärase tähendusega (seega mõlemadväärtusega2), siis ilmutatud viidatavuse põhjal onlog (1 + 1) ja log 2 alati ühesuguseväärtusega.

Ilmutatud viidatavus võib esmapilgul tunduda enesestmõistetav. Matemaatikas ta kehtib ja kan-nabLeibnizi seaduse nime (“võrdsete omavahelisel asendamisel saame võrdsed”). Samas im-peratiivses programmeerimises ei saa ilmutatud viidatavuse kehtimisest üldiselt juttugi olla. Im-peratiivses keeles võib mingi funktsioonf muuta mõne globaalse muutujax väärtust ja teinefunktsioong seda muutujat arvutuses kasutada. Oletame, etf () väärtus on1; sellest ei järeldu,et f () ja 1 on vastastikku asendatavad. Avaldisedg ( f ()) ja g (1) võivad olla erineva

2

Page 3: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

väärtusega, sest teisel juhul arvutabg oma väärtusex algse väärtuse põhjal, kuid esimesel juhulf poolt muudetud väärtuse põhjal.

Nähtust, kus avaldise väärtuse leidmine toob kaasa mingi olekumuutuse, mis võib edaspidi lei-tavaid väärtusi mõjutada, nimetataksekõrvalefektiks (ingl side-effect). Niisiis on ilmutatudviidatavus teisiti öeldes kõrvalefektide puudumine avaldiste väärtustamisel. Väärtustamine onabsoluutselt läbipaistev; kui meid huvitab ainult tulemus, võib väärtustusprotsessi detailid jul-gelt arvestusest välja jätta. Tegelikult puuduvad kõrvalefektid funktsionaalse programmi puhultäiesti, sest kogu tegevus on kirjeldatud avaldiste kaudu.

Ilmutatud viidatavusel on kolossaalsed järeldused keele võimaluste kohta.

Ühegi muutuja väärtus ei saa kunagi sama ploki samal lugemisel muutuda, muidu saaks se-da muutujat kasutada kõrvalefekti tekitamiseks nagu äsja nägime. Muutujad pole muutujad sa-mas mõttes nagu tavaks imperatiivses programmeerimises. Funktsiooni erinevatel väljakutsetelvõivad formaalsed parameetrid omandada küll erinevaid väärtusi, kuid funktsiooni väärtuse leid-mise käigus nad enam muutuda ei saa. See on nagu matemaatikas: valemis esinevad muutujadvõivad küll omandada mitmeid väärtusi, kuid valemi samal lugemisel on sama muutuja kogu va-lemi ulatuses sama väärtusega (x + x = 2x on õige ainult tänu sellele, et valemi samal lugemiselon x igal pool ühe ja sama väärtusega). Samuti on keerulisemad avaldised oma skoobi samallugemisel alati sama väärtusega.

Isegi välise maailma sündmustel (klahvivajutused jms) ei tohi olla avaldiste väärtustele mingitmõju. Funktsionaalne keel tekitab jäiga piiri väärtuste maailma ning sündmuste maailma vahele.Alalises muutumises olev sündmuste maailm on “puhtast” väärtuste maailmast eraldatud nagudžinn pudelis. See on veel üks modulaarsuse vorm.

Samamoodi saab selgeks, et ilmutatud viidatavuse puhul väljendavad kõik keeles kirja pandudfunktsioonid matemaatilisi funktsioone, mis seovad sisendväärtusi väljundväärtusega. Tõepoo-lest, funktsiooni rakendamisel argumendile saame avaldise, mille väärtus ei sõltu sellest, kuidasargumenti arvutatakse, oluline on vaid argumendi väärtus.

Teisest küljest, need kaks tingimust koos — elementaaravaldiste (nagu muutujate) väärtuse muu-tumatus ja funktsioonide interpreteeritavus matemaatiliste funktsioonidena väärtustel — annavadilmutatud viidatavuse. On olemas poolfunktsionaalseid keeli nagu Standard ML, kus kehtivadsiin loetletud kaks esimest funktsionaalse keele tunnust ning ka muutujate väärtused ei muu-tu, kuid funktsioonid väljendavad pigem algoritme, mis võivad tulemuse arvutamisel kasutadavälist, muutuvat infot. Ilmutatud viidatavus neis keeltesei kehti.

Et muutuja väärtust muuta ei saa, pole funktsionaalses keeles omistamist ja puuduvad ka impe-ratiivsetest keeltest tuttavad tsüklikonstruktsioonid.Kõik tsüklilised protsessid tuleb realiseeridarekursiooniga. See ühelt poolt küll ahendab programmeerimisvõimalusi, kuid tulemusena para-neb tunduvalt programmide loetavus.

Ilmutatud viidatavuse tõttu puudub funktsionaalses keeles objekti identiteet sellisena, nagu seeesineb objektorienteeritud paradigmas. Ei ole võimalik, et objekt (mõtleme näiteks mõnd andme-

3

Page 4: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

struktuuri) jääks samaks, kui mõni tema väljadest muutub. Funktsioon, mis võtab selle objektiargumendiks ja loeb seda välja, annaks erinevatel kordadelerinevaid väärtusi. Sama andmest-ruktuur tähendab funktsionaalses keeles alati samu väärtusi samades positsioonides. Seetõttuvõiks objektorienteeritud keeltele vastanduvalt nimetada funktsionaalseid keeli väärtusorientee-ritud keelteks.

Objekti identiteeti on võimalik funktsionaalse paradigmaraames lavastada, lugedes objekti väl-jade väärtused välise maailma sündmusteks, aga seda me lähemalt vaatlema ei hakka.

Tihti peetakse funktsionaalse paradigma tunnusteks veel üht-teist, mis siiski paljudes funktsio-naalsetes keeltes puuduvad, näiteks ülitugevat tüübisüsteemi või laiska väärtustamist. Haskellison viimatimainitud omadused olemas, nii et nendega teeme nii või teisiti tutvust.

Programmeerimiskeelega määratud andmete, väärtuste, avaldiste jne klassifikatsiooni tüüpides-se koos tüüpide omaduste ja omavaheliste suhete võrgustikuga nimetataksetüübisüsteemiks

(ingl type system). Haskelli tüübisüsteem on nii hea, et enamasti on koodis esinevate avaldistetüübid automaatselt kindlaks tehtavad, programmeerija eipea tüüpe ise deklareerima. Igal juhultehakse avaldiste tüübid kindlaks enne programmi töö algust kompileerimise ajal ja mida rangemtüübisüsteem, seda suurema tõenäosusega leitakse programmeerimisviga juba siis ja vigane koodei lähe kunagi käiku.

Laisk (ingl lazy) väärtustamine tähendab seda, et avaldisi väärtustataksevastavalt vajadusele:kui mõni funktsioon oma väärtuse leidmisel argumenti ei kasuta, siis argumenti ei väärtustata-gi, ja kompleksseid argumente (andmestruktuure) väärtustatakse ainult sellisel määral, milliselfunktsioon neid vajab. See teenib tööaja kokkuhoiu eesmärki. Samas on see kahe teraga mõõk,sest laisa väärtustamise korral kipub mälu arvutuse käigustäituma pikkade väärtustamata aval-distega, mis väärtustatuna nõuaksid palju vähem ruumi. Laisale väärtustamisele vastandubagar

(ingl eager) väärtustamine, mis tähendab, et funktsiooni väljakutselkõigepealt väärtustatakseargument ja seda täielikult.

Kuigi mitmetes levinud funktsionaalsetes keeltes tüübisüsteem puudub ja väärtustamine on agar,on ülitugeva tüübisüsteemi ja laisa väärtustamise pidamine funktsionaalse paradigma omadus-teks õigustatud selles mõttes, et neist on rohkem kasu just funktsionaalse paradigma konteks-tis. Imperatiivsetes keeltes tüübisüsteeme nii tugevaks ei arendata ja laisad on tavaliselt vaidloogilised operatsioonid, kõik omadefineeritud funktsioonid käituvad agaralt. Ohjeldamatu laiskväärtustamine ilma ilmutatud viidatavuseta muudaks programmide mõtte äärmiselt raskesti haa-ratavaks, sest funktsiooni argumendi väärtus sõltuks sellest, millisel hetkel ta välja arvutatakse.

Funktsionaalse paradigma tunnused on konkreetsed teed saavutamaks deklaratiivse program-meerimise põhieesmärki, mille järgi kood peab otseselt esitama abstraktseid seoseid matemaati-liste väärtuste vahel.

4

Page 5: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Head ja halvad küljed

Tuues välja funktsionaalse programmeerimise eeliseid, mainime kõigepealt koodi suurt väljen-dusvõimsust ja paindlikkust erinevate ülesannete lahendamisel. Tänu sellele, et funktsioonid onesimese kategooria väärtused ja iga funktsiooni saab kasutada teise funktsiooni argumendina,on arvutused piiranguteta esitatavad parameetrilisena teise arvutuse suhtes. See võimaldab prob-leemilahendused viia abstraktsemale tasemele kui tavalistes programmeerimiskeeltes. Tihti saabühe ja sama koodijupiga programmeerida ümberkäimist väga erinevat laadi andmetega, mis vä-hendab koodikordusi.

Arvutuste spetsifitseerimine abstraktsel tasemel vähendab vajadust arvutusprotsessi detailide kal-lal nokitsemise järele. Kood tuleb lühem ja ülevaatlikum kui imperatiivsetes keeltes. See tä-hendab programmeerija tööaja vähenemist sama ülesande lahenduse programmeerimisel võrrel-des imperatiivse programmeerimisega. Vilunud programmeerija on võimeline probleemilahen-duse esimese töötava versiooni (mis ei pruugi küll olla kõige efektiivsem) produtseerima vägakiiresti. Ericssoni kogemus näitab, et üleminekul imperatiivselt keelelt funktsionaalsele mobiil-tarkvara arendamisel kiirendas programmeerijate töökiirust isegi kuni kümme korda.

Ilmutatud viidatavus võimaldab koodi mõista matemaatikast tuttaval moel, see lihtsustab koo-dist arusaamist ja programmi mõtte tabamist ilma arvutuskäigu detailidesse laskumata. Juhtudel,kus programmi korrektsus on nii tähtis, et tuleb kõne alla korrektsuse formaalne tõestamine,teeb ilmutatud viidatavus ja süntaksi-semantika matemaatikalähedus selle töö palju lihtsamaksja inimlikumaks kui see oleks imperatiivse keele puhul. Veenduda tuleb vaid soovitavate abst-raktsete seoste kehtimises, arvutuse korrektsus tuleb sellega tasuta kaasa (reaalse arvutuse kor-rektsus sõltub muidugi veel kasutatava kompilaatori või interpretaatori korrektsusest, aga neidon programmeerija nii või teisiti sunnitud usaldama).

Funktsionaalse programmeerimise esimese puudusena võib tuua koodi ressursinõudlikkuse võr-reldes imperatiivsetes keeltes kirjutatud koodiga ja sedanii aja- kui mälutarbe osas. Mida enamon keel funktsionaalne, seda enam aega võtavad arvutused neis kirjutatud programmidega ja sedaenam söövad neis kirjutatud programmid mälu (viimane reegel kehtib ka programmide kompi-leerimisel saadud masinkeelse programmi suuruse kohta — isegi triviaalne Haskell-programmvõtab kompileeritult sadu kilobaite ruumi).

Programmeerimises näib kehtivat kuldreegel: mida vähem kulutame ressursse programmeeri-misele, seda rohkem kulutame ressurssi programmide kasutamisel. Funktsionaalne keel või-maldab olulist säästu programmeerija tööajas, kuid funktsionaalses keeles kirjutatud program-mid jooksevad suurusjärke aeglasemalt. Oleneb konkreetsest olukorrast, kumb kulu on olulisem.Kui programmi töökiirus on esmatähtis, tuleb funktsionaalsest paradigmast paraku loobuda. Kuiprogrammi töökiirus on teisejärguline, on funktsionaalneparadigma hea valik. Seoses protsesso-rite kiiruse pideva suurenemisega kasvab nende ülesannetehulk, mille puhul viimase peal kiiruspole enam vajalik, ja ühtlasi funktsionaalse programmeerimise läbilöögivõime.

Teine funktsionaalse programmeerimise puudus seondub programmeerimisega abstraktsel tase-mel. Et see oleks võimalik, peab mäluhaldus (mälu reserveerimine ja vabastamine, prügikoristus)

5

Page 6: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

toimuma automaatselt. Programmeerijal pole võimalik endal mäluhaldust organiseerida. Funkt-sionaalne keel ei võimalda nii madala taseme asju väljendada. Kuid mõnikord oleks otsene mä-luhaldamine vajalik, sellisel juhul ei saa funktsionaalset keelt tööks kasutada.

Funktsionaalse programmeerimise kitsaskohaks peetakse tihti üldisemalt igasuguse interaktiiv-se protsessi programmeerimist, st selliste arvutuste programmeerimist, kus pidevalt tuleb võttakeskkonnast uut infot ja seda sinna produtseerida. Kui imperatiivses keeles käib infovahetu-se programmeerimine keeles loomuldasa olemasolevate vahenditega, siis funktsionaalses kee-les seab ilmutatud viidatavus sellele piirangud. Programmeerija ei saa keskkonnast loetud infotlihtsalt niisama kuhugi muutujasse omistada. Lisaks nõuabfunktsionaalne keel protsessi väljen-damist mingi avaldise väärtusena, imperatiivses keeles pole vaja sellist tsirkust etendada. Mispuutub Haskelli, siis seal on niisuguste avaldiste kirjutamiseks välja töötatud spetsiaalne sün-taks, mis võimaldab Haskellis interaktiivseid tegevusi programmeerida peaaegu niisama ladusaltkui mistahes imperatiivses keeles. Seega võib öelda, et siin on tegemist juba praktiliselt ületatudprobleemiga.

Haskelli väärtuste maailm

Käesolevas õppematerjalis on funktsionaalse paradigma näidiskeelena kasutatud Haskelli. Enneaga, kui tutvume Haskelli süntaksiga ja jõuame esimeste funktsionaalsete programmide kirjuta-miseni, anname lühiülevaate Haskellist just semantika poole pealt.

Järgnevad jaotised visandavad pildi maailmast, milles Haskell-koodi tuleb mõista. Selle maailmaobjekte nimetame üldiseltväärtusteks (ingl value), aga et vältida sõna “väärtus” paralleelsetkasutamist mitmes tähenduses (samal ajal funktsiooni väljaantava väärtuse või avaldise väärtusemõistes), ütleme nende kohta tihti lihtsaltobjektid (see kasutus ei vasta objektorienteeritudparadigmale) või “semantilised objektid”.

Haskellis programmeerides tuleb mõelda üsnagi teistsugustes terminites võrreldes imperatiivse-te keeltega, see maailm sarnaneb palju rohkem matemaatika struktuuridega. Samas tuleb tundamitmeid mõisteid nende tähenduses, mis matemaatikas pole standardne, vaid on spetsiifiline justHaskellile (ja võibolla veel mõnele sarnasele programmeerimiskeelele). Olles nende iseärasuste-ga eelnevalt kursis, on süntaksi õppimine ja süntaktilisteobjektide tähenduse tabamine hõlpsam.

Kõige jämedamas plaanis jagunevad Haskelli väärtused andmeteks, tüüpideks, liikideks ja klas-sideks. Tüüpidel, liikidel ja klassidel on klassifitseerivroll. Tüübid klassifitseerivad andmeid,liigid ja klassid omakorda tüüpe. Järgnev tutvustav ringkäik on üles ehitatud nende klassifitsee-rimisvahekordade kaupa.

6

Page 7: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Andmed ja tüübid

Nagu juba funktsionaalse paradigma kirjelduses juttu oli,peetakse selles paradigmas väga eri-nevaid objekteandmeteks (ingl data). Haskellis teevad erinevat laadi andmete vahel vahettüübid (ingl type). Tüübisüsteem on väga rikas.

Alustame lihtsamast. TüüpiInt kuuluvad4-baidised märgiga täisarvud. Vähim täisarv tüübisInt on −2147483648, suurim on2147483647. On ka teine täisarvutüüp —Integer, mis arvu-dele suuruspiiranguid ei sea. TüüpInteger on niisiis põhimõtteliselt lõpmatu. TüübidFloat jaDouble on kaks erinevat ujukomaarvutüüpi. Väärtused neis tüüpides võivad erineda oma täpsusepoolest, ehk teisi sõnu, nende esitamisele kuluv baitide arv võib erineda. Mõlemas tüübis on seesiiski fikseeritud, ujukomaarvud ei saa minna kuitahes suureks. Täpne baitide arv sõltub realisa-tsioonist, aga kehtib tingimus, et tüübiDouble arvud peavad olema vähemalt niisama täpsed kuitüübiFloat omad. TüüpChar hõlmab sümbolid (mitte ainult1-baidised).

TüüpBool sisaldab tõeväärtused, mida tähistameFalse ja True; esimene tähendab väära ja teinetõest. Mõneti sarnane on tüüpOrdering — suurusvahekorratüüp. Väärtused sellest tüübist tähis-tavad kahe võrreldava objekti võrdlemise tulemusi. Võimalused on “väiksem”, “niisama suur”,“suurem”, mida tähistame vastavaltLT, EQ, GT.

Reaalses elus ei pruugi arvutus lõpetada normaalse väärtuse väljastamisega. Hõlmamaks ka n-öhalbu juhte, loetakse iga tüüp sisaldavaks peale oma põhiväärtuste ka erilist väärtust⊥, midanimetataksebottomiks (ingl bottom). Bottom tähendab normaalsel kujul info täielikku puudu-mist väärtuses. Näiteks täisarvude1 ja 0 jagamisel on väärtuseks⊥. Koos bottomiga sisaldabnäiteksBool kolme jaOrdering nelja väärtust.

Tüüpi nimetatakseloetelutüübiks (ingl enumeration type), kui ükski tema väärtus ei sisaldaendas väärtust mõnest teisest tüübist. Teisiti öeldes, loetelutüübid on need, mis pole konstruee-ritud mõne teise tüübi peale. Mõnes mõttes võib loetelutüüpi mõista ka kui sellist, mida polevõimalik lihtsamalt formaalselt defineerida kui loetledeskõik temasse kuuluvad väärtused. Kõiksenivaadeldud tüübid on loetelutüübid.

Loetelutüüpidele vastanduvad struktuuritüübid, mille väärtusteks onandmestruktuurid (ingldata structure), kuid ka loetelutüübi väärtusi on mugav lugeda triviaalseteks andmestruktuuri-deks, mis kunagi midagi ei sisalda.

Näitena mittetriviaalsetest andmestruktuuritüüpidest vaatleme kõigepealt listitüüpe.List (ingllist) on üht tüüpi andmete kogum, kus andmed on struktureeritud ühte jorru. List on Haskellis jaüldse funktsionaalses programmeerimises põhilisimaks kasutatavaks andmestruktuuriks.

Lihtsaim list on tühi (ingl empty) — tähistagu seda kirjutis[]. Mittetühjad listid avaldu-vad operatsiooni: abil, mis tähendab ühe andme lisamist listi algusse. Nii on täisarvude listidnäiteks1 : [], 0 : 1 : [], 13 : 31 : [], 1 : 3 : 5 : 7 : 9 : [] ja muidugi [] ise. NäiteksTrue : [], False : [],True : False : [], False : False : False : False : [] — ja jälle muidugi[] ise — on tõeväärtuste listid.

Andmeid, mis on operatsiooniga: listi paigutatud, nimetatakse listielementideks (ingl ele-

7

Page 8: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ment). Kui a on anne jal ona-ga sama tüüpi andmete list, siisa : l on sama tüüpi andmete list,kus esimene element ona ja ülejäänud elemendid on parajastil elemendid samas järjekorras.Niisiis a : b : l on tegelikult sama misa : (b : l). Listisa : l nimetatakse elementia listi peaks (inglhead) ja alamstruktuuril listi sabaks (ingl tail). Kui l on mingi list, siisl alamlistideks (inglsublist) nimetatakse listil ja, kui ta on mittetühi, kal saba kõiki alamliste.

Vastanduvalt eespool toodud näidetele listidest on oluline rõhutada, et Haskelli list eipea olema lõplik. Näiteks on olemaslõpmatud (ingl infinite) listid 0 : 1 : 2 : 3 : . . .,False : False : False : False : . . . jms. Sellised listid ei sisalda alamstruktuurina tühja listining listi saba on niisama lõpmatu kui list ise. Kuid on veelgi liste, mis alamstruktuurina tühjalisti ei sisalda: need, mille mõni alamlist on bottom. Selliseid nimetatakseosalisteks (inglpartial). Osalised listid on näiteks1 :⊥, True : False : False :⊥ ja⊥ ise.

Laisk väärtustamine teeb lõpmatud ja osalised listid omaette väärtustena mõttekaks. Seejuuresnäiteks⊥ ja 1 :⊥ on erinevad väärtused: esimeses puudub struktuur täiesti,teises aga on erista-tavad pea ja saba. Pangem lisaks tähele, et listi lõplikkus,lõpmatus või osalisus ei sõltu mingilgimääral listi elementidest, vaid ainult struktuurist.

Kõik listid jagunevad tüüpidesse vastavalt elementide tüübile; ei ole olemas üht listitüüpi, kuhukuuluksid kõik listid. Listid elementidega tüübistInt moodustavad tüübiList Int, sümbolitüüpielementidega listid tüübiList Char jne, mistahes tüübiA korral listitüüpi, millesse kuuluvadlistid elementidega tüübistA, tähistameList A. KunaA võib olla tõesti ükskõik milline tüüp, siissaame konstrueerida näiteks tüüpide jadaInt, List Int, List (List Int), . . .. See näitab, et Haskellison tüüpe lõpmata palju.

String on Haskellis sama mis sümbolite list. Niisiis tüüpList Char on ühtlasi stringitüüp. Vas-tavus on järgmine: tühi string kujutab endast tühja listi; mittetühja stringi pea on tema esimenesümbol ning saba tema alamstring alates teisest sümbolist kuni lõpuni. Seega on Haskellis olemaslõpmatud stringid.

Kuigi Haskellis on igas tüübis olemas väärtus⊥, mis tähendab oodatava normaalse väärtuse puu-dumist, ei ole see alati piisav vahend potentsiaalselt ebaõnnestuva arvutusega ümberkäimisel.Bottomi alla kuuluvad ka kõige hullemad ebaõnnestumised, mida ei ole kunagi võimalik üldsekindlaks teha, nagu mõtlema jäämine lõpmata kauaks. Seepärast on kasulikud tüübid, mis või-maldavad infot oodatava väärtuse puudumise kohta esitada ka normaalse väärtuse kujul. Sellekson olemas tüübidMaybe A.

Kui A on mingi tüüp, siis tüüpMaybe A sisaldab väärtustena kõikiA väärtusia kujul Just a jalisaks väärtustNothing. Näiteks tüüpMaybe Int sisaldab muuhulgas väärtusiJust 0, Just 1,Just (−18) ja Nothing, tüüp Maybe Bool koosneb aga väärtustestJust True, Just False jaNothing (rohkem normaalseid väärtusi pole). VäärtusNothing tähendab tavalise väärtuse puu-dumist, väärtusJust a aga seda, et oodatavA-tüüpi väärtus ona. TüübistMaybe A võib tinglikultmõelda kui listitüübist, kus list ei saa sisaldada üle ühe elemendi.

Palju kasutust leiavadjärjendid (ingl tuple), mis on sarnased järjendite ehk vektoritega mate-maatikas. Järjenditüüpide kohta öeldakse kakorrutistüüp (ingl product type), sest kui järjen-

8

Page 9: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ditüübi bottom kõrvale jätta, siis on järjenditüübi näol matemaatiliselt tegemist otsekorrutisega.Korrutistüüpide alla kuuluvad paaritüübid, kolmikutüübid jne. KuiA ja B on tüübid, siisA × Bon tüüp, mis sisaldab väärtustepaarid (ingl pair) (a , b), kusa tüüp onA ja b tüüp onB. Sama-moodiA × B × C sisaldabkolmikuid (ingl triple) (a , b , c), kusa tüüp onA, b tüüpB ja ctüüpC.

On olemasühiktüüp (ingl unit type), milles on ainult üks normaalne väärtus, mida tähistame(). Ühiktüüpi võib pidada korrutistüübiks, kus tegurtüüpidearv on0.

Korrutistüübile vastandubsummatüüp (ingl sum type). Kui A ja B on tüübid, siisEither A Bon tüüp, mis sisaldab väärtusedLeft a ja Right b, kusa tüüp onA ja b tüüp onB. Summatüübinimetus tuleb sellest, et kui bottomeid mitte arvestada, onväärtusi tüübisEither A B samapaljukui tüüpidesA ja B kokku.

Andmestruktuuride käsitlus on seega vägagi matemaatikapärane, tuletades meelde üldist algeb-rat. Täieliku vastavuse matemaatikaga rikuvad ära bottomid, mida kõik andmestruktuuritüübidsisaldama peavad, kuid millel pole mingit struktuuri.

Näiteks paaritüübi bottomit võidakse Haskellis küll paariks nimetada, kuid matemaatilises mõt-tes ta paar ei ole. Nõudes paaritüübi bottomi esimest või teist komponenti, saame mõlemal juhultulemuseks bottomi (siis vastavalt esimese ja teise komponenttüübi oma), sest kust midagi võttaei ole, sealt ei võta Haskell ka. Samas sisaldab see paaritüüp ka väärtust(⊥ ,⊥), mis on paar nagumuiste, kuid annab sarnaselt⊥-ga komponentide pärimisel tulemuseks bottomid. Siit on näha,et matemaatika seadus, mille kohaselt paarid on võrdsed, kui nende vastavad komponendid onvõrdsed, siin alati ei kehti.

Analoogselt võime matemaatikas kehtiva võrduskriteeriumi paikapidamatust näidata kõigi and-mestruktuuritüüpide korral. Teisalt võib puhta südamega märkida, et ainsad selle kriteeriumikontranäited on põhimõtteliselt sellised, nagu eelmises lõigus kirjeldatud.

Andmestruktuuridest hoopis erinevat laadi väärtused on funktsioonid.Funktsioon (ingl func-

tion) saab mingi väärtuseargumendiks (ingl argument) ja arvutab selle põhjal mingi (üldju-hul uue) väärtuse. Argumendi andmist funktsioonile nimetatakse funktsioonirakendamiseks

(ingl application) argumendile. Kuif on funktsioon jax talle sobiv argument, siisf rakenda-mise tulemust argumendilex (teisi sõnu,f väärtust kohalx) tähistamef x.

Iga funktsioon võtab argumente ühest kindlast tüübist ja annab tulemusi ka ühest kindlast tüü-bist. Funktsioonid jagunevad tüüpidesse vastavalt oma argumendi- ja väärtusetüübile. KuiA jaB on suvalised tüübid, siisA → B on tüüp, mis koosneb funktsioonidest, mille argumendi-tüüp onA ja väärtusetüüpB. Näiteks funktsioon, mis teisendab sümboli tema koodiks, on tüüpiChar → Int, naturaallogaritm on tüüpiFloat → Float või Double → Double (st on kaksnaturaallogaritmfunktsiooni, üks kummagi ujukomaarvutüübi jaoks) jne.Maybe-tüüpide juurestutvusime funktsioonidegaJust, mis on tüüpiA → Maybe A iga tüübiA jaoks.

Kuna funktsionaalses keeles võib iga funktsioon olla funktsiooni argument, samuti väärtus min-gil argumendil, võivad funktsiooni argumenditüüp ja väärtusetüüp olla omakorda funktsioonitüü-

9

Page 10: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

bid. Näiteks tüüpi(Integer → Integer) → (Bool → Char) kuuluvad parajasti funktsioonid,mis võtavad argumendiks täisarve teisendavaid funktsioone ja annavad väärtuseks funktsioone,mis omakorda võtavad argumendiks tõeväärtusi ja annavad tulemuseks sümboleid. Kokkulep-peliselt võib paremalt sulud ära jätta:A → B → C tähendab sama misA → (B → C).Seega selles lõigus näitena toodud funktsioonitüübi võib kirjutada ka ühe sulupaariga kujul(Integer → Integer) → Bool → Char.

Haskellis pole mitme muutuja funktsioone. Sõltuvused mitmest parameetrist tuleb esitada raa-mistikus, millega äsja tutvusime. Loomulik vahend sellekson funktsioonid, mille argumenditüüpon korrutistüüp. Näiteks kahe reaalarvulise argumendiga funktsioonif (x, y) = sin x · cos y saabkujutada funktsioonina, mille argumenditüüp onDouble ×Double ja väärtusetüüpDouble; siisfunktsioon ise on tüüpiDouble × Double → Double. Teine võimalus on kasutadacurried-kujulisi funktsioone.

Kui f on funktsioon, mille argumentideks on mingid järjendid, siis f curried-kuju curry f onfunktsioon, mis töötab sisuliselt samamoodi naguf , kuid võtab argumendid ükshaaval. Kuiftüüp onA1 × . . . × Al → B, siis curry f tüüp onA1 → . . . → Al → B ning mistahesväärtustea1, . . . , al korral vastavalt tüüpidestA1, . . . , Al kehtib

f (a1 , . . . , al) = curry f a1 . . . al.

Ülal vaatlesime funktsioonif , mis võtab argumendiks ujukomaarvude paari(x , y) ja annabsellel tulemuseks ujukomaarvusin x · cos y. Temacurried-kuju curry f on funktsioon tüüpiDouble → Double → Double, mis võtab argumendiks ühe ujukomaarvux ja annab tule-museks funktsiooni, mis omakorda võtab argumendiks ujukomaarvuy ning annab tulemuseksujukomaarvusin x · cos y.

Lugeja võis märgata, et kacurry on isecurried-kujuline funktsioon. Eelmises lõigus oli tematüüp (Double × Double → Double) → Double → Double → Double, sest ta võttisargumendiks funktsiooni tüübistDouble×Double → Double ja andis tulemuseks funktsioonitüübistDouble → Double → Double.

Curried-kujuline funktsioon on seega sama mis funktsioon, mille väärtusetüüp on funktsioo-nitüüp. Haskellis oncurried-kujulised funktsioonid väga tavalised, enamik standardsetest mit-meparameetrilistest funktsionaalsetest suhetest (mh kõik aritmeetilised tehted) on realiseeritudcurried-kujuliselt.

Haskellis (ja üldse funktsionaalses programmeerimises) on väga tavalised ka funktsioonid, milleargumenditüüp on funktsioonitüüp. Selliseid funktsioonenimetataksekõrgemat järku (inglhigher-order) funktsioonideks. Järgu mõistet võib ka täpsustada, see käiks järgmiselt.

1. 0. järku funktsiooniks nimetatakse suvalist väärtust, mis pole funktsioon.

2. Kui n on naturaalarv, siis(n + 1). järku funktsiooniks nimetatakse suvalist funktsiooni,mille argumenditüüp sisaldabn. järku funktsioone.

10

Page 11: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Näiteks naturaallogaritm tüübistDouble → Double on 1. järku funktsioon, sestDouble po-le funktsioonitüüp;f (x , y) = sin x · cos y on 1. järku funktsioon, sestDouble × Double polefunktsioonitüüp; kacurry f on1. järku;curry ise on aga2. järku funktsioon, sest tema argumen-ditüüp on1. järku funktsioonide tüüp. Funktsioon on kõrgemat järku, kui tema järk on vähemalt2.

Öeldakse, et funktsioonf on agar (ingl strict), kui f ⊥ = ⊥. Vastasel korral ütleme, etfon laisk. Agara funktsiooni mõiste on agara väärtustamise peegeldus funktsioonidele väärtus-te maailmas. Agara väärtustamise korral on funktsioonid agarad, sest kõigepealt väärtustatakseargument ja kui see on bottom, st väärtustamisel tekib ületamatu viga, on ühtlasi ka funktsiooniväärtuse arvutamisel tekkinud ületamatu viga, st see väärtus on ka paratamatult bottom. Naguvõib arvata, tuleb Haskellis tihti ette funktsioone, mis pole agarad.

Funktsionaalse paradigma omadused tagavad, et Haskelli funktsioonide käsitlemine nii, nagufunktsioone ka matemaatikas mõistetakse, on täiesti ohutu— kui bottomid kõrvale jätta. Botto-mitega on lood sarnased andmestruktuuride bottomitega. Igas funktsioonitüübis on sees bottom,mis matemaatilises mõttes ei ole funktsioon. Suvalisele argumendile rakendades annab sellineväärtuseks bottomi. Kui matemaatikas kehtib ekstensionaalsus — mistahes funktsioonidef, gkorral kui f ja g töötavad igal argumendil ühtmoodi, siisf ja g on võrdsed —, siis Haskellison bottom eristatav funktsioonist, mis ise ei ole bottom, kuid annab igal argumendil tulemuseksbottomi.

Reaalses elus on enamasti tarvis, et programm saaks oma töö jooksul keskkonnast uut infot luge-da ja oma tööd loetust sõltuvalt teha. Selle realiseeriminefunktsioonidega on mõneti keeruline,sest põhimõtteliselt peaks selline funktsioon võtma kõikvõimaliku keskkonnainfo lisaks omamuudele parameetritele argumendiks. See on programmeerijale tülikas ja nõuaks ka võimalustkeskkonnainfot keele väljendusvahenditega esitada.

Haskellis on mindud teist teed ja loodud eraldi tüübid, millesse kuuluvad justprotseduurid

(ingl action), mis võivad välisest keskkonnast sõltuda ja keskkonda mõjutada. (Need tüübidvõivad olla teatud erilised funktsioonitüübid, kuid see onjäetud realisatsiooniküsimuseks japrogrammeerija ei pea sellest midagi teadma.) Iga tüübiA jaoks on olemas selline tüüpIO A,kusjuuresA on protseduuri pooltedastatava (ingl return) väärtuse tüüp. Mudel on nimeltselline, et iga protseduur saab kasutada seda infot, mida varasemad protseduurid on edastanud,ja edastab lõpuks ise mingi väärtuse, mida järgnevad protseduurid saavad kasutada. Kogu arvu-tust võib võrrelda konveieriga, mis koosneb protseduuridest, mis teevad mingeid operatsioone jaedastavad üksteisele järjest väärtusi. Edastamine on ainus võimalus kasutada ja teha kättesaada-vaks keskkonnast loetud uut infot.

Edastamisele kuulubki tavaliselt uus keskkonnast saadud info. Näiteks protseduur, mis loeb stan-dardsisendist ühe sümboli ja edastab selle, kuulub tüüpiIO Char. Protseduur, mis loeb standard-sisendist sümboli, aga edastab tema koodi, on tüüpiIO Int.

Seevastu protseduuril, mis kirjutab midagi standardväljundisse, näiteks sümboli a, ei ole tingima-ta tarvis midagi sisulist edastada, sest see protseduur ei too sisse uut infot. Haskelli standardteegis

11

Page 12: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

edastavad sellised protseduurid tüüpiliselt väärtuse() ühiktüübist. Muidugi on võimalik ka selli-ne protseduur, mis kirjutab sümboli standardväljundisse ja ka edastab selle sümboli, selline olekstüüpi IO Char.

Sama protseduur võib erinevatel kordadel sõltuvalt keskkonnast loetud infole teha erinevaid asjuja edastada erinevaid väärtusi (väärtuse tüüp ei muutu). See tähendab, et protseduur on abstrakt-sem kui lihtsalt mingi konkreetne vaadeldav tegevus. Näiteks sama protseduur, mille ülesanne onlugeda standardsisendist sümbol ja see edastada, ootab erinevatel kordadel erineva ajavahemiku(kuni kasutaja sümboli sisestab) ja võib erinevatel kordadel edastada erineva sümboli (selle, millekasutaja valis).

Põhimõtteliselt on võimalik kõik algoritmid realiseeridaprotseduuride kaudu, aga selline lähe-nemine on funktsionaalses keeles halb programmeerimisstiil. Funktsionaalse keele üks eesmärkeon eraldada matemaatilise funktsioonina esituvad seosed kõigist ülejäänutest, muuta kood selliselviisil ülevaatlikumaks. Seepärast tuleks funktsionaalses keeles programmeerides kasutada prot-seduure ainult interaktiivsete protsesside programmeerimiseks ja kodeerida niipalju andmetegamanipuleerimisi kui võimalik funktsioonidega.

Võib tekkida küsimus, kas mõni anne võib kuuluda ka mitmessetüüpi, ehk teisiti väljendudes,kas tüüpidel saab olla mittetühi ühisosa. Vastus on eitav: Haskellis on tüübid täiesti lõikumatud.Niisiis tüüpideInt ja Integer elemendid on üksteisest erinevad täisarvud, kuigi me neid üht-moodi tähistame, samamoodi onFloat ja Double elemendid üksteisest erinevad ujukomaarvud.Muidugi erineb väärtusJust a väärtusesta, kuid ka väärtusNothing tüüpidestMaybe A onkõikide tüüpideA korral unikaalne. Isegi⊥ on igal tüübil oma.

Tüübid ja liigid

Eelmises jaotises nähtud tüübindus toob peale andmeid klassifitseerivate tüüpide kaasa ka tüüpi-del määratud funktsioone ehk tüübifunktsioone.

Näiteks nägime, et iga tüübiA korral on olemas listitüüpList A — see tähendab, etList onfunktsioon, mis igale tüübileA seab vastavusse tüübiList A. KunaList töötab tüüpidel ja annabvälja tüüpe, on ta põhimõtteliselt erinev kõigist funktsioonidest funktsioonitüüpides, sest noodvõtavad argumendiks ja annavad tulemuseks andmeid.

Analoogselt näiteksIO on tüübifunktsioon, mis igale tüübileA seab vastavusse tüübiIO A. Onvõimalikud ka keerulisemad tüübifunktsioonid: näiteks summatüüpe konstrueerivEither seabigale tüübileA vastavusse tüübifunktsiooni, mis igale tüübileB seab vastavusse summatüübiEither A B. TüübifunktsioonEither on curried-kujuline. Mõnes rakenduses on mõttekad isegikõrgemat järku tüübifunktsioonid.

Kui jutuks on korraga tüübid ja tüübifunktsioonid, kasutatakse tavaliselt mõlema kohta sõna“tüüp”. See annab sõnale “tüüp” teise, endisest laiema tähenduse (nii nagu funktsionaalne pa-radigma annab andme mõistele tavalisest laiema tähenduse,haarates kaasa ka funktsioonid ja

12

Page 13: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

protseduurid). Selle järgi on kaList, Maybe, IO ja Either tüübid, kuigi nad andmeid ei sisalda,vaid on hoopis teistlaadi objektid. Siiani kasutasime me tüübi mõistet vaid kitsamas, andmehulgatähenduses, edaspidi võib juhtuda nii või teisiti, mõista tuleb sõltuvalt kontekstist.

Laiemalt mõistetud tüüpidel on oma klassifikatsioon, milleannavadliigid (ingl kind). Tüübid,mis kujutavad endast andmekogumit, st tüübid kitsamas tähenduses, on kõik üht ja sama liiki∗.Funktsioon, mis võtab argumendiks tüüpe, mille liik onK, ja annab väärtuseks tüüpe, mille liikonL, on ise liikiK → L. Seega näiteksList, Maybe ja IO on liiki ∗ → ∗, kuid Either on liiki∗ → ∗ → ∗.

Juhime tähelepanu sellele, et tüübifunktsioonid ja funktsioonitüübid on täiesti erinevad asjad.Funktsioonitüüp on tüüp, mis koosneb andmete maailmas töötavatest funktsioonidest; tema liikon ∗. Seevastu tüübifunktsioon on funktsioon, mis töötab tüüpide maailmas; tema liik on kõikemuud kui∗.

Kokkuvõtvalt võib nentida, et kui tüübid on pikemalt väljendudes andmetüübid, siis liigid ontüübitüübid.

Tüübid ja klassid

On veel teinegi tüüpe klassifitseeriv süsteem —klassid (ingl class). Ühte klassi kuuluvad tüü-bid peavad olema sama liiki, seega klassid täpsustavad liikide antavat klassifikatsiooni. Erinevalttüüpidest ja liikidest võivad klassid omada mittetühja ühisosa. Üks klass võib lausa tervikuna tei-se sees sisalduda, sellisel juhul nimetatakse esimest teisealamklassiks (ingl subclass) ja teistesimeseülemklassiks (ingl superclass). Ühel klassil võib olla mitu alam- või ülemklassi.

Võrreldes liikidega, mis klassifitseerivad tüüpe nende olemuse järgi, on klassid suhteliselt mee-levaldsed tüüpide hulgad. Klassikuuluvus tähendab täpsemalt teatavate muutujate defineeritustja see sõltub üldiselt programmeerija suvast.

Näiteks on Haskellis eeldefineeritud klassEq, millesse kuuluvad sellised tüübid, mille elementi-de võrdumist ja mittevõrdumist saab kontrollida. Sellesseklassi kuulub üsna palju tüüpe, näitekskõik arvutüübid, sümbolitüüp, tõeväärtusetüüp, suurusvahekorratüüp, tüübidList A ja Maybe Atingimusel, et kaA kuulub sinna, ühiktüüp, korrutis- ja summatüübid tingimusel, et kompo-nenditüübid kuuluvad sinna, jpt. Funktsioonitüübid ega protseduuritüübid klassiEq vaikimisi eikuulu.

KlassiEq alamklassiOrd kuuluvad sellised tüübid, millel lisaks elementide võrdumisele ja mit-tevõrdumisele on võimalik kontrollida nende omavahelist suurusvahekorda. KlassiOrd kuu-lub enamik arvutüüpe, sümbolitüüp, tõeväärtusetüüp, suurusvahekorratüüp, tüübidList A jaMaybe A tingimusel, et kaA kuulub sinna, ühiktüüp, korrutis- ja summatüübid tingimusel, etkomponenditüübid kuuluvad sinna, jpt. Sümbolite järjestus on defineeritud kooditabeli järgi; tõe-väärtustüübil loetakseFalse väiksemaks kuiTrue; suurusvahekorrad on suurenemise järjestusesLT, EQ, GT; listi- ja korrutistüüpidel on järjestus leksikograafiline. Funktsioonid ega protseduu-

13

Page 14: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

rid sellesse klassi vaikimisi ei kuulu, kuna siia kuuluminenõuaks kuulumist klassiEq.

KlassiShow kuuluvad tüübid, mille elementidel on defineeritud stringiksteisendamine. KlassisRead aga on tüübid, mille elemente on võimalik stringikujust sisse lugeda. Enamik üldkasutata-vaid tüüpe peale funktsiooni- ja protseduuritüüpide kuulub mõlemasse neist klassidest.

KlassNum sisaldab arvutüüpe. Kuuluvuskriteeriumiks on tüübis sisalduvatel väärtustel definee-ritud aritmeetilised operatsioonid. Sellel klassil on omakorda palju alamklasse (näiteksIntegral,Fractional, Floating, mille nimed räägivad iseenda eest), ise aga on ta klassideEq ja Showalamklass.

KlassEnum sisaldab tüüpe, mille elementide puhul saab rääkida järgmise ja eelmise leidmi-sest. Tüüpiliselt kuuluvad siia loetelutüübid. KlassisBounded on sellised tüübid, millel on ole-mas vähim ja suurim element. Tuttavatest tüüpidest kuuluvad siia ühiktüüp,Int, Char, Boolja Ordering. KlassiRandom kuuluvad tüübid, millest on võimalik juhuarvude generaatorite-ga juhuslikult väärtusi valida. Siia kuuluvad vaikimisi olemasolevad täis- ja ujukomaarvutüübid,Char ja Bool.

Tüübiklassindus sarnaneb äratuntavalt objektorienteeritud programmeerimise klassisüsteemiga.Ka terminoloogia on siin sarnane, näiteks nimetatakse muutujaid, mis kannavad klassikuuluvu-seks vajalikke operatsioone, meetoditeks. Oluline vahe onselles, et Haskelli klasside esindajadon tüübid, objektorienteeritud programmeerimises aga objektid ehk tavalised andmed. Objekt-orienteeritud programmeerimises täidavad klassid tüüpide aset, Haskellis asuvad klassid järguvõrra kõrgemal.

Haskelli laiendustes, mis on mõnes kompilaatoris ka realiseeritud, kujutavad klassid endast üldi-semalt relatsioone tüüpidel, tüübirelatsioone. Tüübirelatsioone võib käsitleda kui tüübijärjenditeklasse. Kui relatsioon on unaarne, on tegemist standardse klassiga, binaarse relatsiooni puhultüübipaaride klassiga jne, komponentide arv pole piiratud.

Analoogselt sellega, kuidas liikide sissetoomisel laiendasime tüübi mõistet tüübifunktsioonidele,võib vajadusel tüübi mõistet kasutada ka tüübijärjendite kohta. Näiteks tüübipaar võiks olla tüüp,mille liik on ∗ × ∗ või üldisemaltK × L mingite liikideK, L jaoks jne. (Juhime jällegi tähelepa-nu, et tüübipaar on hoopis midagi muud kui paaritüüp — viimase liik on ∗.) Seega tegelikult onka laiendatud klasside kohta mõttekas öelda, et nad klassifitseerivad tüüpe ja täpsustavad liikideantavat klassifikatsiooni.

14

Page 15: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Esmatutvus töövahendite ja süntaksiga

Töö Haskelliga

Selles jaotises anname ülevaate levinuimatest Haskellis programmeerimise töövahenditest ja tut-vustame kõige üldisemal tasemel programmide ülesehituse põhimõtteid. Käsitlus jääb siin võrd-lemisi pealiskaudseks. Lugejal on soovitav mõningase vilumuse tekkimisel oma lemmikvahen-dite kohta nende kodulehelt või manuaalist täiendavat teavet otsida. Koodipaigutuse põhimõteteja moodulisüsteemi kohta saab hõlpsasti lisainfot leida Haskelli tutvustavatest allikatest.

Haskelli töövahendid

Haskellis programmeerimiseks on loodud nii kompilaatoreid kui interpretaatoreid. Nagu ikka,on programmi jooksutamine interpretaatoriga suurusjärkeaeglasem kui sama programmi kom-pileerimisel saadud masinakoodi jooksutamine.

Lisaks on olemas interaktiivsed keskkonnad, mis on tõhusaks abiks lihtsamate programmide kii-rel koostamisel ja jooksutamisel, muuhulgas Haskelli õppimisel. Interaktiivse keskkonna mõteon võimaldada testida koodi “käigu pealt” ilma, et peaks selleks iseseisvalt käivitatava program-mi looma. Kasutaja saab vahetult testida ühe või teise koodiosa tööd.

Täpsemalt, kasutaja sisestab interaktiivse keskkonna käsurealt avaldisi ja süsteem väärtustabneid. Kui avaldise väärtus on protseduur, siis süsteem täidab selle protseduuri. Muul juhul vastabsüsteem avaldise väärtusega, mille ta väljastamiseks mingil moel stringiks teisendab.

Kõigi Haskelli realisatsioonidega tuleb kaasa süsteemne moodulite teek, kus palju asju on eel-defineeritud. Teegis on kesksel kohal moodulPrelude, mis sisaldab põhilisimat vajalikku, muu-hulgas näiteks aritmeetikat. Iga interaktiivse keskkonnakäivitumisel loetakse moodulPreludeautomaatselt sisse, teistes teegi moodulites defineeritudning omadefineeritud nimede kasuta-miseks tuleb eelnevalt anda mooduli või koodifaili sisselugemise käsk.

Koodis võib esineda erineva iseloomuga vigu. Mõned, peamiselt süntaksi- ja tüübivead, avastabiga Haskelli realisatsioon juba koodifaili sisselugemisel ilma koodi jooksutamata; neid nimeta-taksestaatilisteks vigadeks. Nende esinemisel pole koodi kasutamine enne nende parandamist

15

Page 16: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

võimalik. Osa vigu ilmneb muidugi alles koodi jooksutamisel, need ontäitmisaegsed (inglruntime) vead.

Kõigil järgnevalt käsitletavail töövahendeil on olemas vähemalt Windowsi ja Unixi versioonidja neid saab veebist tasuta alla laadida. Käesolevas õppevahendis eeldame Unixi-laadset operat-sioonisüsteemi.

Üks standardsemaid töövahendeid lihtsate Haskell-programmide koostamiseks, eriti keele õppi-misel, on interaktiivne interpretaatorHugs. Hugsi koduleht asub aadressil

http://www.haskell.org/hugs/ .

Terminali käsurealt käivitub Hugs käsugahugs . Seejärel on ta kohe valmis vastu võtma Haskell-avaldisi ja neile reageerima. Kuna Haskelli süntaks on matemaatikalähedane, on korrektsete aval-diste sisestamine ja niiviisi interaktiivse keskkonna kasutamine taskuarvuti eest täiesti võimalikka inimesel, kes Haskelliga kunagi varem kokku pole puutunud.

Ülesandeid

1. Käivitada Hugs.

2. Lasta Hugsil väärtustada lihtsaid Haskell-avaldisi, ntmõni aritmeetiline avaldis nagu2 + (-3) , pi vms.

Hugsi käsurealt saab lisaks Haskell-avaldistele anda ka Hugsi käske, millest peamised on kirjel-datud joonisel 1. Hugsi käsku eristab Haskell-avaldisest alguskoolon.

Käsk :r on kasulik, kui käib ühe ja sama programmifaili korduv muutmine. Siis igal järgnevalkorral loetakse käsuga:r samast failist värske versioon sisse, ilma et kasutaja peaks faili nimepidevalt kordama.

Kuigi tekstiredaktorit ei pea muidugi Hugsist käivitama, oma failid võib teha valmis ükskõikmilliste vahenditega, on Hugsi käsk:e hea selle poolest, et redaktorist väljumisel sooritab Hugskohe automaatselt ka staatiliste vigade kontrolli.

Lisaks käsule:s saab Hugsi parameetrite vaikeväärtusi sättida ka keskkonnamuutujagaHUGSFLAGS. Näiteks võib sellele väärtuseks omistada teksti

-Eemacs -h32M +kls .

Siin -Eemacs seab käsu:e poolt käivitatavaks tekstiredakoriks Emacsi,-h32M reserveerib ar-vutusteks kasutatava kuhjamälu suurusega32 megabaiti ning+kls seab teatavad kolm eelistust,mille tähendust võib uurida Hugsis, andes käsu:s ilma argumentideta.

16

Page 17: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Käsk Argument Tegevus

:q väljumine

:l faili/mooduli nimi mooduli sisselugemine

:r viimase sisselugemiskäsu kordamine.

:t avaldis avaldise tüübi kuvamine

:i nimede loend info nimede kohta

:b moodulite loend info moodulite eksporditavate nimede kohta

:e faili nimi tekstiredaktori avamine oma mooduli kirjutamiseks

:? Hugsi käskude nimekirja kuvamine.

:s parameetrite ja nende tähistuste nimekirja kuvamine

muutuse tähistus parameetrite muutmine

Joonis 1: Peamised Hugsi käsud.

Ülesandeid

3. Lasta Hugsil kuvada lihtsate avaldiste tüüpe.

4. Lasta Hugsil anda infot mõne teile tuntud identifikaatorikohta.

5. Lasta Hugsil loetleda mooduliPrelude eksporditavad nimed.

6. Lahkuda Hugsist.

Suuremate programmide tegemiseks, eriti kui loodava tarkvara töökiirus on oluline, on interp-retaatori asemel parem kasutada kompilaatoreid. Levinuimja arenenuim Haskelli kompilaatormaailmas on Glasgow ülikoolis arendatavGHC (lühend sõnadest “Glasgow Haskell Compi-ler”). Tema koduleht asub aadressil

http://www.haskell.org/ghc/ .

Kompilaatoriga on kaasas Hugsiga sarnane interaktiivne keskkond, mis kannab nimeGHCi (sõ-nadest “GHCinteractive”). Hoolimata nimes sisalduvast C-tähest, pole GHCi ise kompilaatorega kasutagi oma töös kompileerimist, sest standardmoodulite korral kasutab ta varem GHC-gavalmis kompileeritud masinakoodi ning kasutaja omadefineeritud koodi ta interpreteerib.

GHCi käivitub terminali käsurealt käsugaghci . Interaktiivse keskkonna käsud:q , :t , :i , :b ,:? on sarnased Hugsiga. GHC teegi moodulite sisselugemisel tuleb kasutada käsku:m , omafailide puhul aga:l ja :r nagu Hugsis. Käsu:s asemel tuleb kasutada:set .

17

Page 18: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

7. Proovida kätt ka GHCi peal, lastes väärtustada mõned avaldised, andes mõned käsud jalahkudes süsteemist.

GHC võimaldab Haskell-koodi kompileerida masinakoodiks,mida saab iseseisvalt käivitada.Selleks peab käivitatav protseduur olema failis defineeritud muutujamain väärtuseks.

Teeme näiteks programmi, mis kirjutab ekraanile avaldise2 + (-3) väärtuse ja mis asub fai-lis nimegaHello.hs . Selle avaldise väärtus on arv, mitte protseduur; ekraanile kirjutamisekstuleb talle rakendada operaatorprint . Sellega on kõik soovitud operatsioonid kirjeldatud ningesimene Haskellis kirjutatud programm oleks

main= print (2 + (-3))

. (1)

Programmip kompileerimiseks GHC-ga masinakoodiks võib anda käsu

ghc --make -o q p.

See tekitab masinakoodis programmi nimegaq. Lõik “ -o q ” võib käsus ka ära jätta, kuid siisleiutab GHC ise väljundfailile mingi nime, mis ei pruugi kasutajale meeldida.

Ülesandeid

8. Tehes läbi ülalkirjeldatud sammud, tekitada GHC-ga programmHello . Käivitada see javeenduda, et programm töötab oodatud viisil.

9. Lugeda failHello.hs sisse interaktiivses keskkonnas ja käivitada sama protseduur inter-aktiivse keskkonna käsurealt.

GHC kõrval on üsna levinud ka kompilaatorNHC. Tema koduleht on aadressil

http://www.haskell.org/nhc98/ .

NHC töötab ise kiiremini kui GHC, kuid tema produtseeritud masinakood on GHC tekitatustaeglasem.

NHC kutsumiseks terminali käsurealt on käsknhc98 . GHC-ga vajalik käsureaargument--make jääb ära, nii et programmip kompileerimiseks ja tulemuse kirjutamiseks failiq sobibkäsk

18

Page 19: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

nhc98 -o q p.

NHC-ga tuleb kaasa kompileerimismanageerijahmake koos interaktiivse keskkonnagahi.Need programmid võimaldavad ühtsel viisil kasutada suvalist kompilaatorit. Töötamine interak-tiivses keskkonnas on sarnane töötamisele Hugsis või GHCi-s, aga süsteemis on põhimõttelineerinevus: kogu kood, kaasa arvatud kasutaja sisestatavad avaldised, kompileeritakse enne käivi-tamist masinakoodiks. Kuna kompileerimine toimub jooksvalt, kulub selleks iga avaldise väär-tustamise eel tuntav ajavahemik; kui aga avaldise väärtustamine on mahukas, teeb kompileeritudkoodi kiirus selle kaotuse kuhjaga tasa.

Koodifaili struktuur

Haskell võimaldab koodi kirjutada ilma selliste tülikate eraldajateta nagu on semikoolonid pal-judes keeltes, aga selleks peavad koodi osad olema failis üksteise suhtes normaalselt rajastatud.

Normaalne rajastamine ei tähenda midagi väga konkreetset.Väga erineva maitse järgi rajastami-ne on lubatud. Näiteks esimene Haskell-programm oli ülal antud kaherealisena kujul (1), kuidsama programmi võib kirjutada ka kujul

main = print (2 + (-3)) .

Kui kasutada kaherealist varianti, peab teine rida olema esimese suhtes positiivse taandega, mui-du loetaks kaks rida eraldi deklaratsioonideks. Näites (1)on taane+2 tühikut, mis pikas perspek-tiivis ei ole liiga väike, kuna Haskell-kood kipub olema paljutasemelise struktuuriga.

Käesolevas õppevahendis on järgitud üht võimalikku paigutussüsteemi, mida täpselt jäljendadesei tohiks rajastusvigu tekkida. Igaüks võib leida katseliselt omale meelepäraseima rajastusviisi,mida ka Haskell aktsepteerib.

Soovitav on taanete tekitamisel mitte kasutada tabulaatorit, vaid ainult tühikuid, sest tabulaato-reid interpreteeritakse erineval pool erinevalt ja seetõttu võib juhtuda, et kood, mis tekstitöötlus-programmi aknas näib olevat õigesti rajastatud, ei ole sedateps mitte Haskelli programmeerimis-keskkonna jaoks.

Haskell-koodi on siiski võimalik struktureerida ka semikooloni abil. Parsimisel Haskell lisab-ki struktureerimata koodile tema struktuuri ilmutatult näitavad semikoolonid. See kajastub tihtiveateadetes, mis viitavad ootamatule semikoolonile mingis reas, kus pole ühtki semikoolonit.Sellisel juhul tuleb aru saada, et viga on rajastamises.

Haskell-koodi saab kommentaare lisada kahel viisil. Pikkusest sõltumata on võimalik kommen-taare paigutada kommentaarisulgude{- ja-} vahele. Kommentaar võib asuda ka teise kommen-taari sees. Lühemad kommentaarid võib aga panna kahekordsesidekriipsu järele, selle kommen-taari lõpetab reavahetus. Sellise kommentaari puhul on üldjuhul vaja kriipsude ja kommentaarialguse vahele eraldavat tühisümbolit, kuid kui kommentaaralgab tähega, siis võib teda alustadaotse sidekriipsude järelt.

19

Page 20: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

main -- Esimene Haskell-programm!= print (2 + (-3)) -- See kommentaar ulatub rea lõpuni.

{-

Pikem kommentaar.{- Sisemine kommentaar. -}

Pikem kommentaar jätkub. -} -- Pikema kommentaari lõpp.

Joonis 2: Kommentaaride kirjutamist illustreeriv näidisfail.

Joonisel 2 esitatud sisuga näitekoodis on varem kirjutatudesimese Haskell-programmi koodile(1) lisatud hulk kommentaare.

Ülesandeid

10. Lisada oma Haskell-failiHello.hs erinevatesse kohtadesse mõlemat sorti kommentaare.

Haskell-koodifailide standardsed nimelaiendid on.hs ja .lhs , mis tähendavad vastavalt “Has-kell script” ja “ literate Haskell script”. Nende laienditega failide lugemisel Haskelli töövahendi-tega pole laiendit vaja kirjutada. (Kui sama failinime põhiosa jaoks eksisteerivad nii.hs - kui ka.lhs -versioonid, siis loetakse.hs -fail.)

Laiendiga.lhs failides loetakse koodiridadeks ainult need read, mis algavad sümboliga>, koodmoodustub nende ridade algussümbolile järgnevatest osadest. Muudel ridadel võib lisada kom-mentaare või hoida millist tahes koodiga seonduvat infot. Süntaksireeglid.lhs -faili koodiosakohta on samad mis.hs -faili puhul terve faili kohta.

Ülesandeid

11. Teha failHello.lhs , milles on sama programm mis ülalkirjeldatud failisHello.hs .Lisada kommentaare koodivälistele ridadele.

Moodulid

Hetkel kehtiv Haskelli standardHaskell 98 määrab17 moodulit. Kõik need sisalduvad kõigimeie käsitletud Haskelli realisatsioonide teekides.

Standardteek pole just suur, kuid ta moodustab tänapäeval vaid pisikese osa kogu olemasolevastmoodulite hierarhiast. Viimane sarnaneb Java klassi- ja paketihierarhiaga, kus klassi täisnimi

20

Page 21: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kujutab endast aadressi, milles esinevad järjest hierarhia üksused kõrgemast madalama suunas,üksteisest punktiga eraldatud. Näiteks Haskelli standardteegi moodul, millest saab sümbolitegaseonduvaid operatsioone, on nimegaData.Char. Standardmoodulid on kättesaadavad ka ilmatäisteeta, nii et moodulileData.Char saab viidata ka palja nimegaChar .

Moodulid teenivad Haskellis ühelt poolt väärtuste temaatilise jagamise eesmärki: ratsionaalarvu-dega seonduvaid operatsioone saab moodulistData.Ratio, kompleksarvudega seonduvaid moo-dulistData.Complex jne. See on vaade mooduli kasutaja aspektist.

Näiteks on Haskellis võimalik teha täpseid arvutusi harilike murdudega vastanduvalt ujukomaar-vudele, millega opereerimine on ebatäpne. Kuid murrujooneaset täidab protsendimärk%, midapole loetud põhiliste operaatorite hulka ja mida seetõttu moodulistPrelude ei saa. Selle sümbolikasutamiseks tuleb sisse lugeda moodulData.Ratio. Teinud seda interaktiivses keskkonnas, saa-me anda arvutile ülesandeks teha tehteid nagu näiteks1 % 2 - 1 % 6, millele saame täpsevastuse1 % 3(ratsionaalarvude lõppkuju, millele nad väärtustamisel viiakse, on esitus taandu-matu murruna).

Ülesandeid

12. Lugeda interaktiivses keskkonnas sisse moodulData.Ratio ja sooritada mõned tehted ha-rilike murdudega.

Mooduli kirjutaja aspektist teenivad moodulid kõigepealtkoodi struktureerimise eesmärki, mison mõneti erinev. Mitte kõik, mis teatavas moodulis defineeritakse, ei pea jääma selle moodulikaudu kasutajale kättesaadavaks. Väljaspoolt on kättesaadavad ainult nimed, mida mooduleks-

pordib (ingl export). Teisest küljest ei pea kõik, mis mooduli kaudu kasutajalekättesaadav on,olema just selles moodulis defineeritud. Moodul võib ka teiste moodulite eksporditavaid nimesidimportida (ingl import) ja omakorda edasi eksportida samadel alustel moodulis endas definee-ritud nimedega.

Vaadeldud kahes aspektis on Haskelli moodul Java klassi analoog. Samas kui objektorienteeritudkeelte reeglid nõuavad samalaadsete objektidega seonduvate isendimeetodite koodi paigutamistsama klassi sisse, siis Haskell mingeid taolisi kitsendusiei sea. Haskelli moodulid võivad lau-sa vastastikku teineteist importida ja kumbki enda ja imporditud nimesid segiläbi eksportida.On täielikult programmeerija vastutusel, et kood moodulite vahel tõepoolest mõistlikult oleksjaotatud. See on tarkvara hoolduse aspekt.

Esimese tutvuse moodulitega nende programmeerija aspektist võib teha Hugsi teegi peal, sestHugsi distributsioonis on teegi moodulid algteksti kujul.GHC ja NHC distributsioonis on teekvaid kompileeritud kujul, algtekste pole. Kõigi teekide algtekstid on siiski valdavalt samad, kunapärinevad samast repositooriumist.

21

Page 22: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

13. Visata silm peale Hugsi moodulite teegile.

14. Lugeda interaktiivses keskkonnas sisse mõni uus moodulja küsida selle mooduli ekspordi-tavate nimede loend.

Moodul koosneb kas mooduli päisest ja mooduli kehast või ainult mooduli kehast. Mooduli päiselihtsaim kuju on

module m where,

kusm on mooduli nimi. Lihtsaim mooduli keha on lihtsalt tühi, agaüldiselt mooduli keha sisal-dab deklaratsioone.

Programmi moodul, mis defineerib selle muutujamain , mille väärtuseks on käivitatav protse-duur, peab olema nimegaMain . Kui mooduli päis puudub, siis loetaksegi mooduli nimeksMain .Meie moodul failisHello.hs (joonis 2) oli ilma päiseta ja sisaldas ühe deklaratsiooni.Et sellemooduli nimeks loetakseMain , näitab ka interaktiivne keskkond faili sisselugemisel.

Eri moodulid peavad paiknema eraldi failides. Mooduli nimivõib sisaldada tähti, numbreid jaalakriipsu ning peab algama suurtähega. Failinimi ilma laiendita peab reeglina kokku langemaselles failis defineeritava mooduli nimega. Hugs ning GHC jaGHCi siiski lubavad olla moodulistsõltumatu nimega failil, mida nad otse sisse loevad (muidu pidanuks meie näitemoodul olemafailis Main.hs ).

Ülesandeid

15. Milline on kõige väiksem võimalik fail, mille interaktiivsed keskkonnad Haskelli mooduli-na sisse loevad?

Moodulis saab kasutada nimesid oma tähenduses, mis defineeritakse samas moodulis või impor-ditakse mõnest teisest. PeamooduliPrelude eksporditavad nimed imporditakse vaikimisi, teistemoodulite nimede importimiseks tuleb kirjutadaimpordideklaratsioon. Impordideklaratsioo-nid peavad asuma mooduli keha alguses enne kõiki muid deklaratsioone. Lihtsaim impordidek-laratsioon on kujul

import m,

kusm on mooduli nimi.

Näiteks kui tahame kirjutada harilikke murde oma moodulis,peame seal importima mooduli

Data.Ratio. Joonisel 3 on esitatud programm, mis sooritab tehte1

2− 1

6ja kirjutab vastuse ek-

raanile.

22

Page 23: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

import Data.Ratio

main= print (1 % 2 - 1 % 6)

Joonis 3: Programm impordideklaratsiooniga.

Importimisel võib tekkidanimekollisioon, kui mõni nimi, mis moodulis defineeritakse, tulebka impordi kaudu sisse või imporditakse mõni nimi mitmest moodulist. Niikaua kui nime alg-päritolu on üheselt tuvastatav, ei ole nime kasutamisel vaja täiendavalt mooduli nime lisada ehknimeadresseerida (ingl qualify). Kuid see on alati lubatud, näiteks võib%asemel kirjutadaData.Ratio.% .

Eksportimise kohta võib lühidalt öelda niipalju, et vaikimisi ekspordib moodul parajasti kõik sel-le, mis on otseselt temas defineeritud, st kõik, mis on temas kasutatav, peale imporditud nimede.Aga programmeerija saab esitada mooduli päises mooduli nime järel sulgudesekspordiloendi

(ingl export list), kus ekspordiüksused eraldatakse üksteisest komaga. Näiteks joonisel 3 ku-jutatud moodulile võiksime mõtet muutmata lisada päisemodule Main where või ka päisemodule Main (main) where. Võib kirjutada ka päisemodule Main () where —siis see moodul midagi ei ekspordi. Samas võib selle moodulipanna hoopis rohkem nimesideksportima, lisades ekspordiloendisse moodulistData.Ratio imporditud nimesid. Näiteks päisemodule Main (main , Rational) where korral ekspordib moodul ka ratsionaalarvu-tüübi nimeRational .

Iga eksporditav nimi eksporditakse ainult ühes tähenduses, isegi kui ta mooduli sees esineb im-portide tõttu mitmes. Seega võib moodulite importimisel arvestada, et ükski import ei too üksikaasa nimekollisiooni.

Põhimõtteliselt saab ka impordideklaratsioonile lisada mooduli nime järele täpsustuse, millisednimed importida ja millised mitte.

Ülesandeid

16. Kirjutada moodul, mille sisselugemisel interaktiivses interpretaatoris on adresseerimata ka-sutatavad nii ratsionaalarvud kui kompleksarvud.

17. Kirjutada moodul joonisel 3 faili. Lugeda moodul interaktiivses keskkonnas sisse ja nõu-da käsuga:b tema ekspordiloendit. Proovida ka olukordi, kus moodulileon lisatud ülalkirjeldatud viisidel päis.

23

Page 24: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Lihtsad avaldised

Avaldised (ingl expression), mida saab anda interaktiivsele keskkonnale väärtustamiseks võitäitmiseks, pole ainus süntaktilise objekti liik Haskellis. Iseseisvalt käivitatava programmi tege-misel juba koostasime avaldisest erineva süntaktilise objekti — deklaratsiooni —, mis defineeristeatava protseduuri muutujamain väärtuseks.

Alustame oma ülevaadet Haskelli süntaksist siiski just avaldistega kahel põhjusel. Esiteks, neidsaab kiiresti testida interaktiivse keskkonna käsurealt ilma programme kirjutamata. Teiseks, aval-dised, kuigi nad pole kõige lihtsamad süntaktilised objektid Haskellis, on lugejale neist kõigetuttavamad, sest avaldisi kasutatakse üldjoontes samal kujul ka matemaatikas.

Vastavalt väärtusele tehakse vahet andmeavaldisel ja tüübiavaldisel:andmeavaldise väärtuson anne,tüübiavaldise väärtus on tüüp. Enamasti on asi kontekstist selge ja räägime lihtsaltavaldistest. Interaktiivse interpretaatori käsurealt saab väärtustada vaid andmeavaldisi.

Avaldist nimetataksemonomorfseks (ingl monomorphic), kui tema väärtuse tüüp on üheseltmääratud, japolümorfseks (ingl polymorphic), kui tal on väärtusi mitmest tüübist. Näiteksaritmeetilised avaldised on enamasti polümorfsed, sest arvud võivad kuuluda paljudesse eri tüü-pidesse (Int, Integer, Double jne). Nähtust, kus sama süntaktiline objekt võtab väärtusimitmesttüübist, nimetataksepolümorfismiks (ingl polymorphism).

Kuna tihti on tüüpide käsitlemisel mugav ja tarvilik seostada andmeavaldisi mitte otseselt tüüpi-dega, mida võib olla üks või palju, vaid tüübiavaldisega, mis neid tüüpe väljendab, siis mõiste-takse avaldisea tüübi all sellist tüübiavaldist, mis võtab kokkua kõigi väärtuste tüübid.

Haskellis on väga palju avaldiste alaliike, ühed lihtsamad, teised keerulisemad. Siin ja järgmisesjaotises tegeleme lihtsate avaldistega, mille struktuuris ei leidu muid kompleksseid süntaktilisiobjekte peale avaldiste ega ilmutatud hargnemist, ja teemetutvust nende koostisosadega.

Märgime, et järgnevas on pidevalt paralleelselt vaatluse all ühelt poolt süntaktiline maailm, teisaltsemantiline väärtuste maailm ning kolmandalt poolt ka arvutusprotsessid. Jõudumööda on neidilmutatult üksteisest eristatud, kuid kuna need maailmad peegelduvad üksteises, siis on ühtesidja samu termineid kasutatud erinevate maailmade objektidemärkimiseks.

Näiteks on väärtuste maailmas paarid ja ka süntaktilises maailmas on paarid (formaalsed paa-rikujul kirjutised). Väärtuste maailmas räägime funktsioonide rakendamisest, kuid süntaktili-si konstruktsioone, mille tähenduseks on funktsiooni rakendamine, nimetame samuti rakenda-miseks ning seejuures saab veel vahet teha rakendamisel kuioperaatoril ja rakendamisel kuirakendamisega konstrueeritud avaldisel. Lõpuks nimetamerakendamiseks ka rakendamisegakonstrueeritud avaldise väärtustamisprotsessi.

24

Page 25: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Muutujad ja konstruktorid

Haskelli lihtsaimad tähendust kandvad süntaktilised objektid on muutujad (ingl variable) jakonstruktorid (ingl constructor). Analoogselt avaldiste jaotamisele jaotuvad muutujad väär-tuse järgiandmemuutujateks ja tüübimuutujateks, konstruktorid agaandmekonstruk-

toriteks ja tüübikonstruktoriteks. Muutujad ja konstruktorid vastanduvad üksteisele sellepoolest, kuidas arvutusprotsessi käigus nendega ümber käiakse.

Muutuja tähistab Haskell-programmi täitmisel täiesti tundmatut väärtust. Paljas muutuja ei saaolla ühegi avaldise väärtuseks ega anda selle väärtuse kohta isegi mitte osalist informatsiooni.Kui arvutusprotsessis nõuab olukord muutuja väärtust, siis üritab Haskell teda väärtustada.

Nii juhtub näiteks, kui interaktiivse keskkonna käsurealtsisestadapi . Kuna pi on muutuja,leitakse tema väärtus ja antakse päringuvastuseks.

Muutujal võib normaalset väärtust ka mitte olla. Näiteks muutuja undefined väärtustamineei anna välja muud kui veateate. Kui sõltumata keskkonna seisundist (kasutatav mälumaht, sig-naalid jms) ei õnnestu avaldise väärtustamise käigus leidatema väärtuse normaalset kuju, siisloetakse teoorias avaldise väärtuseks⊥. Normaalne kuju võib puududa kahel põhjusel: kas väär-tustamine lõpeb veaga või ei lõpe üldse.

Niisiis muutujaundefined väärtus on⊥. Pangem tähele, et tegemist on defineeritud muutuja-ga, muidu tekiks süntaksiviga, mitte täitmisaegne viga.

Seevastu konstruktor tähistab Haskell-programmi täitmisel lõppväärtust, mida enam teisendadapole vaja. Konstruktor loetakse täisinformatsiooniks. Sellised on näiteks numbritega kirjutatudarvulised konstandid0, 1, 1.5 jne, tõeväärtusedTrue ja False ning tühja listi märkiv[] .Sama kehtib sümbolkonstantide kohta. Need kirjutatakse Haskellis sümbolina apostroofide va-hel (nt’A’, ’! ’), kusjuures sümbolit võib asendada ka tema langjoonega algav kood. Nii onkorrektne sümbolkonstant näiteks’\n ’ (\n kodeerib Unixi reavahetust), ka teised C-st tuntudkoodid on lubatud. Lisaks saab Haskellis kodeerida sümboleid ka kujul \ s, kus s on sümboliASCII-koodi kümnendesitus, ja kujul\x s, kuss on koodi kuueteistkümnendesitus. Nii tähenda-vad näiteks\97 ja \x61 sümbolita. Langjoon sümbolina tuleb alati asendada koodiga, lihtsaimvõimalus on kujul\\ . Sümbolkonstandis on ka apostroofi kodeerimine kohustuslik, apostroofisaab kodeerida kujul\’ .

Kui interaktiivse keskkonna käsurealt sisestada mõni eelpooltoodud konstruktor, tuleb vastusekspõhimõtteliselt seesama asi. Tõsi, mõnikord näeb see vastus teisiti välja, kuna vahepeal on toi-munud sisendi parsimine ja tulemuse stringiksteisendamine, mis võivad kokku anda sisestatusterineva stringikuju.

Haskelli leksika seab muutuja- ja konstruktorinimedele piirangud. Muutujanimed peavad üldju-hul koosnema tähtedest, numbritest, alakriipsust ja apostroofist, kusjuures andmemuutuja nimipeab siis algama väiketähe või alakriipsuga. Andmemuutujate puhul on lubatud ka nime koosne-

25

Page 26: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

mine sümbolitest nimekirjas

+, - , * , / , ^ , =, <, >, &, | , $, : , ! , ?, . , %, \ , @, ~, #, (2)

mispuhul nimi ei tohi alata kooloniga. Ka konstruktorinimed peavad üldjuhul koosnema tähte-dest, numbritest, alakriipsust ja apostroofist, kuid peavad algama suurtähega. Andmekonstruktorinimi võib koosneda ka sümbolitest loetelus (2), mispuhul tapeab algama kooloniga. Eeldefinee-ritud konstruktorite hulgas on ka mõned erandlikud, mille nimi ei vasta kirjeldatud tingimuste-le. Ülalvaadelduist on reeglipärased konstruktoridTrue ja False , ülejäänud on sisseehitatuderandlikud.

Seega on võimalik muutuja ja konstruktori vahel kergesti vahet teha kirjapildi järgi. Võtmesõnadon enamasti keelatud nii muutuja kui konstruktorina.

Ülesandeid

18. Anda interaktiivse interpretaatori käsurealt avaldis, mis sisaldab (a) väiketähega algavatdefineerimata identifikaatorit, (b) suurtähega algavat defineerimata identifikaatorit. Lugedamõlemal korral veateadet ja võrrelda veateatega, mis tulebmuutujaundefined väärtus-tamisel. Mida väljendavad erinevused?

19. Millised nimedestp, p1 , p_1, pP, p_P, p-P , , P, P_p, Pp, _P, P+, +-+ , :-: , : sobivadmuutujaks ja millised konstruktoriks?

Kõik senitoodud näited olid andmemuutujatest ja -konstruktoritest. Et tutvuda ka tüübimuutujateja -konstruktoritega, vaatleme siinkohal avaldiste annoteerimist tüüpidega.

Annoteeritud avaldis on kujul

a :: t, (3)

kusa on andmeavaldis jat tüübiavaldis. NäiteksTrue :: Bool ja ’a’ :: Char on kor-rektsed annoteeritud avaldised. SiinBool ja Char on tüübikonstruktorid, mis tähendavad vas-tavalt tõeväärtustüüpi ja sümbolitüüpi.

Andes need avaldised interaktiivse keskkonna käsurealt väärtustada, saame vastuseks vastavaltTrue ja’a’. Üldiselt ongi tüübikorrektne annoteeritud avaldis väärtuselt võrdne annoteerimataavaldisega.

Kujul (3) oleva avaldise tüübikorrektsuse jaoks on tarvilik, et t väljendaks ainult selliseid tüüpe,millesse kuuluvaid väärtusi saaba omada. Eelnevad näited seda tingimust ka rahuldasid, kunatõese tüüp onBool ja sümboli a tüüpChar.

Polümorfse avaldise tüübi väljendamiseks võetakse appi tüübimuutujad. Näiteks muutu-ja undefined on polümorfne, tema väärtus on⊥ suvalisest tüübist. See tähendab, et

26

Page 27: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

undefined tüübiks sobib kirjutada näiteks tüübimuutujaa. See märgib üht suvalist tüüpi.Siin muutujaa on lokaalne ja ei too kaasa tähendust väljaspoolt, seega võib tema asemelkasutada ka suvalist teist muutujat. Lokaalsete tüübimuutujate nimed peavad algama väiketähevõi alakriipsuga.

Osa polümorfsete avaldiste tüüpe nõuab tüübimuutujatele klassikitsendusi. Näiteks arvkonstandi1 tüüp on(Num a) => a; see tüübiavaldis väljendab suvalist tüüpiA, mille korralA kuulubklassiNum. Järelikult1 omab väärtust parajasti igas tüübisA klassistNum ehk igas arvulisestüübis. Tingimust esitavat osa(Num a) nimetataksetüübikontekstiks. Üldjuhul kujutab taendast komadega eraldatud klassikuuluvustingimuste nimekirja ümarsulgudes.

Annoteerimise mõte on üldjuhul polümorfse avaldise väärtuse võimalike tüüpide hulka kitsenda-da. Kuigi me võime kirjutadaTrue :: Bool või 1 :: (Num a) => a, pole sellest min-git kasu, kuna süsteem teab niisamagi, etTrue tüüp onBool ja 1 tüüp on(Num a) => a.Me võime aga kirjutada annoteeritud avaldise1 :: Int . Selle avaldise väärtus on1 nimelt tüü-bist Int. Korrektsed on ka kirjutised1 :: Integer , 1 :: Float , 1 :: Double . Täiestimõttekas on ka1 :: (Floating a) => a. Selle avaldise väärtus saab olla1 suvalisestujukomaarvulisest tüübist.

Pangem tähele, et annoteerimine ei võimalda avaldise tüübimeelevaldset teisendamist, vaid ai-nult täpsustamist. Annoteeritav avaldis peab talle antavaid tüüpe saama omada. NäiteksChar eikuulu klassiNum, mistõttu1 ei saa mitte kuidagi omada väärtust tüübistChar, annoteeritudavaldis1 :: Char annab lihtsalt tüübivea.

Kui polümorfse avaldise kontekstist ei selgu, millisesse tüüpi kuuluvat väärtust on mõeldud, võibsüsteemil tekkida raskusi avaldise väärtustamisel. Põhimõtteliselt on arvavaldiste nagu1 korralolukord just selline, sest kui teda interpreteerida täisarvuna, siis tuleb vastus väljastada kujul1,kui aga ujukomaarvuna, siis kujul1.0 .

Üldjuhul võib taolises olukorras tüübiviga tekkida. Arvutüüpide puhul aga nii siiski ei juhtu, vaidsüsteem valib võimalike tüüpide seast välja ühe. Vaikimisiloetakse avaldised, mis saavad omadakõiki täisarvutüüpe, tüüpiInteger ning avaldised, mis saavad omada kõiki ujukomaarvutüüpe,kuid mitte täisarvutüüpe, tüüpiDouble .

Näiteks andes interaktiivses keskkonnas väärtustada kas avaldise1 või 1 :: (Num a) => a,saame vastuseks1, sest süsteem loeb ta vaikimisi täisarvuks (täpsemalt tüüpi Integer ). Kirju-tades aga1 :: (Floating a) => a, tuleb vastuseks1.0 , sest tüübiannotatsioon välistabtäisarvulise interpretatsiooni (nüüd loetakse tüübiksDouble ).

Ülesandeid

20. Küsida interaktiivses keskkonnas muutujapi tüüp ja saada sellest aru.

21. Teha katseliselt kindlaks, kas teie kasutatavas versioonis on ujukomaarvud tüüpidesFloatja Double erineva või ühesuguse täpsusega.

27

Page 28: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Infiksoperaatorid

Operaatoriks (ingl operator) nimetame muutujat või konstruktorit, mille väärtus on funkt-sioon.

Operaatorit nimetatakseinfiksoperaatoriks (ingl infix operator), kui teda kasutatakse infiks-selt, st oma argumentide vahel. Interaktiivsete interpretaatoritega tutvumisel tõenäoliselt proo-visite juba väärtustada aritmeetilisi avaldisi nagu nt2 + 3 ja 2 * pi / 3 , kus avaldised onkonstrueeritud atomaarsetest avaldistest infiksoperaatorite +, - , * , / abil.

Infiksoperaatorite ring on Haskellis väga lai. Aritmeetikagi ei piirdu mainitud nelja tehtemär-giga. Juba jagamisest on olemas mitu varianti, kui võtta arvesse mooduliData.Ratio pakutavoperaator%, mis vastab murrujoonele; täpsemalt on tegu operaatoriga,mis võtab argumendikskaks ühetüübilist täisarvu ja annab tulemuseks nende suhteratsionaalarvuna. Astendamiseks onolemas lausa kolm infiksoperaatorit:^ , ^^ ja ** . Neist esimene sobib juhul, kui astendaja onnaturaalarv, teine on murdarvu tõstmiseks täisarvulisse astmesse, kolmas on ujukomaarvude as-tendamiseks.

Ka võrdlemiseks kasutatavad==, /= , <=, <, >=, > on infiksoperaatorid. Nende abil on võimalikkirjutada näitekspi == 3 , 2 + 2 /= 5 , 9 ^ (9 ^ 9) <= (9 ^ 9) ^ 9 , mis kõikon süntaktiliselt ning tüübiliselt korrektsed avaldised.Operaatorid== ja /= tähendavad vastavaltvõrdust ja mittevõrdust, ülejäänute tähendus on sarnane teiste programmeerimiskeeltega.

Loogiliste avaldiste tegemiseks on kasutatavad infiksoperaatorid&& ja || , mille väärtusteks onvastavalt konjunktsioon ja disjunktsioon. Nendega moodustatud loogilise avaldise väärtustamistalustatakse vasakust argumendist. Kui&&vasak argument väärtustub vääraks, siis paremat argu-menti ei väärtustata. Samuti kui|| vasak argument väärtustub tõeseks, siis paremat ei väärtus-tata.

Iga infiksoperaatoriga seonduvad temaprioriteet (ingl priority) ja assotsiatiivsus (ingl as-

sociativity), mida on vältimatult vaja arvestada infiksoperaatorite vähegi keerukamal kasuta-misel. Need atribuudid määravad avaldise struktuuri kohtades, kus sulud seda ei tee.

Prioriteete märgivad Haskellis täisarvud0-st 9-ni, suurem arv vastab kõrgemale prioriteedile.Kohtades, kus sulud alamavaldiste järjekorda ei määra, tuleb kõrgema prioriteediga tehted tehavarem. Teisi sõnu, kõrgema prioriteediga infiksoperaatorid seovad lekseeme enda ümber tugeva-malt kui madalama prioriteediga infiksoperaatorid.

Aritmeetiliste operaatorite prioriteetide järjekord on sama mis matemaatikas, st astendamisedon kõrgema prioriteediga kui korrutamine ja jagamine, mis omakorda on kõrgema prioritee-diga kui liitmine ja lahutamine. Seega näiteks avaldis2 * 3 ^ 8 + 1 on sama mis avaldis(2 * (3 ^ 8)) + 1 . Võrdlusoperaatorid on aritmeetilistest operaatoritestmadalama priori-teediga. Loogilistest operaatoritest on&& kõrgema prioriteediga kui|| ning nad mõlemad onmadalama prioriteediga isegi võrdlusoperaatoritest.

Kui sulud tehete järjekorda ei määra ja ka prioriteet on võistlevail infiksoperaatoritel võrdne,

28

Page 29: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

siis tuleb mängu assotsiatiivsus. Infiksoperaator saab olla vasak-, parem- või mitteassotsiatiivne.Vasakassotsiatiivse (ingl left associative) operaatori esinemisi tuleb rakendada vasakult pa-remale,paremassotsiatiivse (ingl right associative) operaatori esinemisi paremalt vasakule.Näiteks astendamisoperaatorid on paremassotsiatiivsed,seega avaldis9 ^ 9 ^ 9 on ekviva-lentne avaldisega9 ^ (9 ^ 9) , mitte avaldisega(9 ^ 9) ^ 9 .

Seevastu- ja / on sarnaselt matemaatikale vasakassotsiatiivsed. Assotsiatiivsus on oluline kanendel operaatoritel, mille puhul lõppväärtus sooritamise järjekorrast ei sõltu, sest süsteem peabju mingis järjekorras need tehted sooritama. Nii on määratud ka+ ja * vasakassotsiatiivseks.

Mitteassotsiatiivsete (ingl non-associative) operatsioonide ahelesinemise puhul on järje-korra näitamine sulgudega kohustuslik, vastasel korral ontegemist süntaksiveaga. Sama kehtibjuhul, kui ahelas esinevad vastandliku assotsiatiivsusega infiksoperaatorid.

Prioriteet ja assotsiatiivsus on kõigile matemaatikast mingil määral tuttavad mõisted, kuid kunaHaskellis on infiksoperaatoritel nii suur osa, on vaja neid atribuute Haskellis programmeerimiselveelgi tähelepanelikumalt jälgida.

Vaikimisi on infiksoperaatorid vasakassotsiatiivsed prioriteediga 9. Infiksoperaatorite kohta, mil-le prioriteet-assotsiatiivsus erinevad vaikeväärtustest, annab käsk:i interaktiivses keskkonnasteada lisaks tüübile ka prioriteedi ja assotsiatiivsuse.

Ülesandeid

22. Teha interaktiivse keskkonna abil kindlaks operaatori%prioriteet teiste käsitletud infiksope-raatorite suhtes.

23. Kirjutada interaktiivse interpretaatori käsurealt avaldis, mille tüüp onBool ja mille väär-

tustamisega kontrollitakse, kasπ 622

7<

√10. Kasutada võimalikult vähe sulge.

24. Arvutada interaktiivses interpretaatoris arvu1.616 esitus taandumatu murruna.

Kõik seni vaadeldud infiksoperaatorid on muutujad. Ka konstruktorid saavad olla infiksoperaa-torid. Sellisel juhul on tüüpiliselt tegemist andmestruktuure koostavate operatsioonidega.

Lihtsaimaks näiteks võiks tuua paaride moodustamise. Avaldise(2 , -3) väärtuseks on paar(2 ,−3). Siin asub, argumentide2 ja -3 vahel. Paar võib olla ka keerulisemate komponentidega,nt (1 + 1 , 2 - 5) , mille väärtus on samuti(2 ,−3).

Kui avaldis on paarikujul, siis tema väärtustusprotsess võib teisendada paari komponente, kuidsulud ja koma arvutuse käigus ära kaduda ei saa — paar jääb paariks. See näitab, et paarimoo-dustamisoperaator on konstruktor. See on ühtlasi esimene näide konstruktorist, mille väärtus onfunktsioon — varasemad vaadeldud konstruktorid argumenteei võtnud.

29

Page 30: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Haskelli paarimoodustamisoperaator pole siiski reeglipärane konstruktor, vaid erandlik, sest see, nõuab alati konstrueeritud avaldise ümber sulge ja pealegiei alga ta kooloniga.

Ülesandeid

25. Väärtustada interaktiivses keskkonnas võimalikult lühike avaldis, mis sisaldab kaht paari.

Teise näitena, seekord reeglipärasest konstruktorist, toome listikonstruktori: . Selle konstruktoriväärtus on funktsioon, mis, saades argumendiks objektix ja sama tüüpi objektide listil, annabvälja uue listi, mille saab objektix lisamisel listil algusse esimeseks elemendiks.

Näiteks 5-elemendilist listi, mille elemendid on järjest1, 3, 5, 7, 9, väljendab avaldis1 : (3 : (5 : (7 : (9 : [])))) . Kuid infiksoperaator: on paremassotsiatiivne,mistõttu võib sulud ära jätta ja kirjutada lihtsalt1 : 3 : 5 : 7 : 9 : [] .

Nagu ka paarikonstruktor, märgib ka: teatava kompleksse väärtuse väljade eraldajat ning ope-raatoriga: moodustatud avaldisest see operaator teisendamise käiguskuhugi kaduda ega paigastnihkuda ei saa. Kui avaldis on kujula : b, siis : jääb paika ja teisendatakse ainult avaldisia jab. See tähendab, et infiksoperaator: on konstruktor.

Konstruktori: rakendamisel mingitele argumentidelea ja b toimub ainult uue struktuuri lisami-ne;a ja b jäävad samale kujule nagu nad argumendiks tulid. Kui nad olid väärtustamata, siis nadjäävad väärtustamata, mis tähendab, et konstruktor: on laisk ja tema rakendamine käib konstant-se keerukusega. Haskellis on enamik eeldefineeritud konstruktoreid-operaatoreid sellised, kaasaarvatud paarikonstruktor.

Interaktiivses keskkonnas listidega töötamisel võib algul segadust põhjustada asjaolu, et needkeskkonnad esitavad liste kasutajale elementide loendinakantsulgudes. See ei tähenda, et arvu-tatud väärtusest need koolonid ja tühi list puuduvad, vastupidi. Aga interaktiivses keskkonnaslisandub väärtuse leidmisele tema stringina esitamise samm, mis võib väärtuse originaalstruk-tuuri moonutada.

Ülesandeid

26. Sisestada interaktiivses interpretaatoris mõned listid, jälgida, mida ta vastab, ja saada arupõhimõttest.

27. Selgitada välja operaatori: prioriteet senituntud infiksoperaatorite suhtes.

28. Lasta interpretaatori käsurealt väärtustada3-elemendiline list, mille elemendid on baitidearvud vastavalt kilobaidis, megabaidis ja gigabaidis. Kasutada võimalikult vähe sulge.

29. Lasta interpretaatori käsurealt väärtustada2-elemendiline list, mille esimene element ütleb,kas radiaan on väiksem kui57◦, ja teine, kas30 < π

36 31. Kasutada võimalikult vähe

sulge.

30

Page 31: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

30. Sisestada interpretaatori käsurealt list, mille elemendid on omakorda listid, millest vähemaltüks on tühi ja üks mittetühi. Kasutada võimalikult vähe sulge.

Paarikonstruktori ja listikonstruktori näidete baasil peaks argumente võtva konstruktori mõteolema arusaadav. Selliseid konstruktoreid võib avastada isegi Haskelli arvude maailmas. Stan-dardteegi moodulData.Complex annab kompleksarvude esituse kujula :+ b, kusa väljendabreaalosa jab imaginaarühikui kordajat imaginaarosas ning need mõlemad peavad olema samaujukomaarvulist tüüpi. Infiksoperaator:+ on konstruktor. See muidugi tähendab, et Haskelliskujutatab kompleksarv endast paarilaadset andmestruktuuri. Mõneti erandlikult on konstruktor:+ agar, mis tähendab, et avaldisea :+ b väärtustamisel väärtustatakse kõigepealta ja b ja al-les siis moodustatakse konstruktoriga:+ kompleksarv. Sellise korralduse põhjuseks on asjaolu,et enamasti pole mõtet hoida mälus osaliselt väärtustamatakompleksarve.

Ülesandeid

31. Arvutada interaktiivses keskkonnasi2.

Õigus on neil, kes nüüd oletavad, et paaritüüpi, mille komponenditüübid onA ja B ja mi-da teoorias kirjutatakseA × B, tähistatakse Haskellis tüübitaseme infiksoperaatori ehkka-he argumendiga tüübikonstruktori abil. Mõneti ebajärjekindlalt on selleks infiksoperaatorikssulud ja koma sarnaselt paarikonstruktoriga. Näiteks avaldise ( ’a’ , False) tüüp on(Char , Bool) . Paaritüübi segiminekut tüübipaariga pole karta, kuna Haskellis pole kunagivaja tüübipaare kirjutada.

Ka võtmesõna:: võib tinglikult käsitada infiksoperaatorina, mille vasak argument peab ole-ma andmeavaldis, parem aga tüübiavaldis. Selle infiksoperaatori prioriteet on madalam ükskõikmillise reeglipärase infiksoperaatori omast ning ta on mitteassotsiatiivne.

Ülesandeid

32. Küsida interaktiivses keskkonnas avaldise(2 , 3) tüüp ja saada vastusest aru.

33. Annoteerida avaldise(2 , 3) tüüp korrektselt kahel viisil: ühel, kus paari komponendi-tüübid on võrdsed, ja teisel, kus nad on erinevad.

34. Demonstreerida interaktiivses keskkonnas, et võtmesõna:: on infiksoperaatorina mitteas-sotsiatiivne ja tema prioriteet on madalam mõne tuntud reeglipärase infiksoperaatori priori-teedist.

35. Millist assotsiatiivsust, kas vasak- või parem-, oleksvõtmesõnal:: infiksoperaatorina mõt-tekas omada?

Olulise täpsustusena tuleb märkida, et kõigi infiksoperaatorite väärtuseks loetakse funktsioonid

31

Page 32: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

omacurried-kujul. Seega näiteks&& väärtuseks olev funktsioon mitte ei võta argumendiks tõe-väärtuste paari, vaid kaks tõeväärtust ükshaaval. Esimesel neist annab see funktsioon tulemuseksfunktsiooni, mis omakorda võtab argumendiks teise tõeväärtuse ja annab siis tulemuseks nendekahe tõeväärtuse loogilise korrutise ehk konjunktsiooni.

Sellele vastavalt näitab infiksoperaatorite tüüpe ka interaktiivne keskkond. (Kuna paljas in-fiksoperaator ei moodusta iseseisvat avaldist, tuleb tema tüüpi küsida käsuga:i , mitte :t .)Näiteks infiksoperaatori&& tüübiks annab interaktiivne keskkondBool -> Bool -> Boolja + tüübiks(Num a) => a -> a -> a .

Siin -> on funktsioonitüübikonstruktor, tema väärtuseks on tüübifunktsioon, mis suvalisel kaheltüübil A, B annab tulemuseks tüübiA → B, millesse kuuluvad parajasti funktsioonid argumen-ditüübigaA väärtusetüübigaB. Nagu näha, on ka-> infiksoperaator ja seejuures paremassot-siatiivne, muidu oleks tulnud kirjutadaBool -> (Bool -> Bool) ja a -> (a -> a) .

See, et operaatori rakendamisel tekivad vahetulemustena mingid funktsioonid, ei tähenda, et mä-lus tekitataks mingit uut koodi, vaid see on lihtsalt sobiv matemaatiline tõlgendus. Arvutusprot-sess hakkab nii või teisiti reaalselt peale alles siis, kui kõik selleks vajalikud argumendid on käes.Curried-kujude variant on eelistatud argumentide võtmisele komplektina sellepärast, et viimanetähendaks Haskellis argumentide paigutamist järjenditesse, kuid andmestruktuuride (isegi paari-de) moodustamiseks ja neist väärtuste kättesaamiseks kulub mõnevõrra lisaressurssi.

Ülesandeid

36. Küsida interaktiivses keskkonnas infiksoperaatorite* , / , ^ , ^^ , ** , || , ==, < tüübid jasaada neist aru.

Prefiksoperaatorid

Vastanduvalt infiksoperaatoritele võiks funktsiooniväärtusega muutujat või konstruktorit, kui takirjutatakse oma argumendi ette, nimetadaprefiksoperaatoriks. Iseseisvalt käivitatava prog-rammi tegemisel juba kasutasime üht prefiksoperaatoritprint , mille väärtuseks on funktsioon,mis argumendilx annab väärtuseks protseduuri, mis kirjutab standardväljundisse stringikujulx.

Prefikssel rakendamisel kasutatakse üldiselt eraldajana tühikut, nagu on näha avaldiseslog 5või programmis (1). Tühiku asemel on lubatud ka muu mittetühi tühisümbolite jada, mis ei tekitarajastusvigu (tühisümboliteks on peale tühiku näiteks tabulaator ja reavahetus). Kui kas operaa-tor või argument on sulgudes, võib neid eraldav tühisümbolite jada isegi tühi olla, ntlog(5) ,(log)5 , (log)(5) on kõik samad mislog 5 .

MoodulisPrelude on defineeritud väga palju väga erinevaid prefiksoperaatoreid. Näitekssin— väärtuseks siinusfunktsioon,cos — koosinus,tan — tangens,asin — arkussiinus jne,

32

Page 33: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

log — naturaallogaritm,exp — eksponentfunktsioon alusele, abs — absoluutväärtus jms,not — loogiline eitus jpm.

Moodulis Data.Char on näiteks muutujaord — teisendus sümbolist koodiks,chr — temapöördfunktsioon ehk teisendus koodist sümboliks. Samast moodulist tuleva muutujaisUpperväärtuseks on predikaat (st funktsioon, mille väärtustüüpon tõeväärtusetüüp, ehk siis tõeväärtusivälja andev funktsioon), mis kontrollib, kas argument on suurtäht, muutujatoUpper väärtusekson aga funktsioon, mis võtab argumendiks sümboli ja kui see on väiketäht, siis annab väärtuseksvastava suurtähe, muidu aga argumendi enda. Samast moodulist saab palju teisigi analoogseidpredikaate ja teisendusfunktsioone.

Prefiksse rakendamise kasutamisel tuleb arvestada, et see seob identifikaatoreid tugevamini igastinfiksoperaatorist ning lugema hakatakse vasakult. Teisiti öeldes, kui võtta prefiksse rakenda-mise osalisi eraldavat tühikut (või muud tühisümbolite jada, mis prefiksset rakendamist tähistab)infiksoperaatorina, siis selle infiksoperaatori prioriteet on10 ja ta on vasakassotsiatiivne.

Näiteks tahtes leida siinuse arvu2 + 3 logaritmist, tuleb kirjutadasin (log (2 + 3)) . Si-semised sulud on vajalikud, sest muidu rakendukslog vaid arvkonstandile2, välimised aga onvajalikud, sest muidu rakendukssin vaid operaatorilelog .

Ülesandeid

37. Kontrollida ujukomaarvuliste arvutuste täpsust, arvutades arvuπ mõne arkusfunktsioonikaudu ja võrreldes muutujapi väärtusega.

38. Kompleksmuutuja funktsioonide teooriasii = e−π

2 . Arvutada see arv interaktiivse interpre-taatoriga kahel viisil: kompleksarve kasutades ja kompleksarve kasutamata.

39. Kirjutada avaldis, kus kolm trigonomeetrilist operaatorit oleksid järjest rakendatud.

40. Kontrollida ühe avaldise väärtustamisega, kas sümbol,mis asub kooditabelis A-st30 süm-boli võrra tagapool, on väiketäht.

41. Katsetades interaktiivses keskkonnas paljude erinevate argumentidega, uurida mooduliData.Char mõne ülal mitte kirjeldatud muutuja väärtust.

42. Küsida interaktiivses keskkonnas muutujatelog , abs , not , ord , isUpper , toUppertüübid ja saada neist aru.

43. Mis on avaldiselog 0 väärtus?

Paaridega seonduvad moodulisPrelude defineeritud muutujadfst ja snd , mille väärtuseks onfunktsioonid, mis võtavad argumendiks suvalise paari ja annavad tulemuseks vastavalt selle paariesimese ja teise komponendi. Näiteksfst (2 , -3) väärtus on2, agasnd (2 , -3)väärtus on−3. Kummagi muutuja rakendamisel seisneb arvutus vaid paari järgi komponendi

33

Page 34: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

leidmises, komponendi sisusse ei tungita ja ei puututa ka paari seda komponenti, mida väljaandma ei pea. Seega onfst ja snd rakendamine konstantse keerukusega.

Näite paare konstrueerivast funktsioonist saame mooduliPrelude muutujastproperFraction . Tema väärtuseks on funktsioon, mis võtab argumendiks murdarvu-tüüpi arvu ja annab väärtuseks paari tema täis- ja murdosaga, kus positiivse argumendi korraltuleb mittenegatiivne murdosa ja negatiivse argumendi korral mittepositiivne murdosa.

Ülesandeid

44. Kirjutada interaktiivses keskkonnas avaldis, kusfst ja snd esinevad järjest rakendatunaja mille väärtustamine lõpetab normaalselt.

45. Kirjutada interaktiivse interpretaatori käsurealt avaldis, mille väärtus väljendab arvue10

murdosa. Mis juhtub, kui astendaja on suurem, nt100?

46. Küsida interaktiivses keskkonnas muutujatefst , snd , properFraction tüübid ja saa-da neist aru.

Listide valdkonnas peaks tundma muutujaidhead ja tail , mille väärtuseks on funktsioo-nid, mis võtavad argumendiks listi ja kui see on mittetühi, siis annavad tulemuseks vastavaltselle listi pea ja saba. Näiteks avaldisehead (1 : 0 : []) väärtus on arv1, avaldisetail (1 : 0 : []) väärtus aga1-elemendiline list0 : []. Muutuja null väärtuseks onpredikaat, mis kontrollib, kas argumentlist on tühi. Kõiginende muutujate rakendamine toimubkonstantse keerukusega argumentlisti pikkuse suhtes.

Ainuüksi moodulPrelude annab programmeerijale väga palju listidega seonduvaid operaato-reid. Kõige lihtsamad neist lisaks seniõpituile onlength , sum ja product . Nende väärtuseksolevad funktsioonid võtavad argumendiks listi ja annavad tulemuseks vastavalt oma tema pikkuseehk elementide arvu, elementide summa ja elementide korrutise. Esimene neist funktsioonideston tüübikorrektselt rakendatav igat tüüpi elementidega listidele, ülejäänudd kaks ainult arvulisttüüpi elementidega listidele.

Näiteks avaldiselength (1 : 0 : []) väärtus on2, sest1 : 0 : [] väärtuseks on2-elemendiline list, avaldiseproduct (1 : 2 : 3 : []) väärtus aga6.

Veel on kasulik tunda muutujatreverse , mille väärtuseks on funktsioon, mis võtab argumen-diks listi ja annab tulemuseks ümberpööratud listi, kui selline leidub (näiteks lõpmatu listi korralei leidu). See funktsioon rakendub korrektselt mistahes tüüpi elementidega listidele. Kõigi selleslõigus vaadeldud funktsioonide arvutamine toimub argumentlisti pikkuse suhtes lineaarses ajas.

Ülesandeid

47. Testida viimastes lõikudes käsitletud muutujaid interaktiivses interpretaatoris, koostades

34

Page 35: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

igaühe jaoks neist avaldise, milles ta on rakendatud mingile argumendile, mille väärtusekson vähemalt3-elemendiline list, nii et avaldise väärtustamine lõpeb normaalselt.

48. Kirjutada interaktiivses keskkonnas üks avaldis, milles esinevad mingis järjekorras järjestrakendatuna muutujadhead , sum ja reverse ja mille väärtustamine lõpeb normaalselt.

49. Milline on muutujatelength , sum, product , reverse väärtuseks olevate funktsioo-nide väärtus (a) tühjal listil, (b) lõpmatul listil, (c) osalisel listil?

Kui head ja tail väärtuseks olevad funktsioonid jagavad listi osadeks temaesimese elemendijärelt, siis muutujatelast ja init väärtuseks on analoogsed funktsioonid, mis jagavad mit-tetühja argumentlistil osadeks tema viimase elemendi eest, andes välja vastavaltl viimase ele-mendi ja listi l elementidest kuni eelviimaseni. Kuid erinevalt muutujatest head ja tail toi-mub nende muutujate rakendamine lineaarse keerukusega argumentlisti pikkuse suhtes. Põhjusseisneb selles, et kuna listi elemendid üldiselt ei paikne mälus kompaktselt, vaid lististruktuurehitatakse mälus üles järjestikuste viitade ahelana, siisviimase elemendi kättesaamiseks on vajalist algusest lõpuni läbi lugeda.

Hulk listifunktsioone, mida moodulistPrelude ei saa, on kättesaadavad standardteegi mooduliData.List kaudu. Seal on näiteks muutujatails väärtuseks funktsioon, mis võtab argumendikslisti ja annab tulemuseks listi, mille elementideks on argumentlist, tema saba, saba saba jne,kuni tühja listini, st argumentlisti kõik alamlistid. Analoogiline funktsioon on muutujainitsväärtuseks: tema võtab samuti argumendiks listi ja annab tulemuseks listi, mille elementidekson tema kõik algusjupid alates lühemast: tühi list,1-elemendiline list esimese elemendiga, list2esimese elemendiga jne, kuni kogu listini. Mõlemad funktsioonid töötavad ka tühjal listil.

Kui tails rakendamine toimub lineaarse keerukusega argumentlisti pikkuse suhtes, siisinitsrakendamine nõuab koguni ruutkeerukust. Vahe tuleb sisse sellest, et esimesel juhul on konst-rueeritava listi kõik elemendid argumentlisti struktuuris olemas, nende aadressid on vaid vajakokku koguda, kuid argumentlisti algusjupid, mida vajabinits , on vaja ka valmis ehitada.

Ülesandeid

50. Arvutada interaktiivses keskkonnas niisugune list, mille elementideks on listi1 : 3 : 6 : 10 : 15 : 21 : 28 : [] kõik mittetühjad algusjupid pikkuse järgi kasvavas järjes-tuses.

51. Arvutada interaktiivses keskkonnas niisugune list, mille elementideks on listi1 : 3 : 6 : 10 : 15 : 21 : 28 : [] kõik mittetühjad lõpujupid pikkuse järgi kasvavas järjestuses.Püüda teha nii efektiivselt, kui senised teadmised võimaldavad.

Ka prefiksoperaatori väärtus võib ollacurried-kujuline funktsioon, st süüa pärast üht argumentiveel argumente, kuni tulemuseks olev väärtus pole enam funktsioon. Valdav enamik mitmepara-meetrilisi funktsioone standardteegis ongi realiseeritud curried-kujulistena, olgu nad siis antudväärtuseks infiks- või prefiksoperaatoritele.

35

Page 36: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Üks lihtsaimaid võimalikkecurried-kujulisi funktsioone on muutujaconst väärtuseks. See an-nab oma argumendilx väärtuseks funktsiooni, mis suvalisel oma argumendil annab väärtuseksx. Teisiti öeldes,const väärtus onfst väärtusecurried-kuju.

Muutujaconst rakendamiseks argumendile5 kirjutame, nagu tavalistegi funktsioonide puhul,const 5 . Selle avaldise väärtuseks on funktsioon, seega saame veelkorra rakendada — näiteksargumendile0.2 . Kompleksse avaldise rakendamine toimub alati prefiksseltnagu prefiksope-raatori rakendamine, seega saame avaldise(const 5) 0.2 . Vastavaltconst ülalkirjeldatuddefinitsioonile on viimase avaldise väärtuseks5.

Taolises kirjutises nagu(const 5) 0.2 võib sulud ära jätta, kuna funktsiooni rakendaminejärjestkirjutamisega on vasakassotsiatiivne, sta b c = ( a b) c. See on veel üks omadus, misteebcurried-kujul funktsioonide kasutamise Haskellis mugavaks.

Äsjakäsitletud funktsiooni juures on huvitav märkida, et tema väärtus normaalsel argumendilon alati laisk funktsioon. Et seda näha, võib interaktiivses keskkonnas anda väärtustada näiteksavaldiseconst 0 undefined . Vastuseks tuleb0, mis on normaalne väärtus. Argumendiundefined antav potentsiaalne veateade jääb olemata, järelikult seda argumenti ei püütagiväärtustada.

Arvulistest funktsioonidest oncurried-kujulised muutujatediv ja modväärtused. Need funkt-sioonid võtavad ükshaaval argumentidena kaks arvu ja annavad väärtuseks vastavalt täisarvulisejagatise ja jäägi esimese arvu jagamisel teisega. Näiteks avaldistediv 10 7 ja mod 10 7väärtuseks on vastavalt1 ja 3, sest10 jagamisel7-ga on jagatis1 ja tekib jääk3. Kui on vaja niijagatist kui ka jääki, võib kasutada muutujatdivMod , mille väärtuseks on funktsioon, mis annabvälja oma argumentide jagatise ja jäägi ühes paaris koos; näiteksdivMod 10 7 väärtuseks onpaar(1 , 3).

Veel võib näiteks tuua moodulistData.Complex saadava muutujamkPolar , mille väärtusekson curried-kujul funktsioon, mis võtab argumendiks ujukomaarvudr ja x ja annab väärtusekskompleksarvu polaarkoordinaatidega(r, x).

Võrdlusoperatsioonidega seonduvad operaatoridmin ja max, mille väärtuseks oncurried-kujulised funktsioonid, mis võtavad kaks argumenti samastjärjestusega tüübist ja annavad väljavastavalt neist kahest maksimaalse ja minimaalse.

Listide poolelt võiks näiteks tuuatake ja drop . Kummagi muutuja väärtuseks on funkt-sioon, mis võtab argumendiks lühikese täisarvun ja annab välja funktsiooni, mis võtab ar-gumendiks mingi listil. Tulemuseks ontake puhul list, mis koosneb listil esimesestnelemendist järjekorda muutmata, kuil-s on vähemaltn elementi, ja tervest lististl, kui l-son vähem kuin elementi,drop puhul aga nende elementide list, mistake puhul üle jää-vad. Näitekstake 2 (5 : 6 : 7 : []) väärtuseks on2-elemendiline list5 : 6 : [] jadrop 2 (5 : 6 : 7 : []) väärtuseks on1-elemendiline list7 : []. Analoogselt jäägigajagamisega on ka siin olemas võimalus arvutada mõlemad osadkorraga, selline funktsioon onmuutujasplitAt väärtuseks. SeegasplitAt 2 (5 : 6 : 7 : []) väärtuseks on paar(5 : 6 : [], 7 : []).

36

Page 37: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Muutujate take , drop ja splitAt rakendamine toimub lineaarse keerukusega nendetäisarvargumendi suhtes. Kui listi elementide arv on suurem, ei puutu ülejäänud elemendidasjasse.

Veel läheb tihti vaja muutujatreplicate , mille väärtuseks oncurried-kujul funktsioon, misvõtab argumendiks lühikese täisarvua ja suvalise objektix ning annab välja listi pikkusegaa,mille iga element onx. Selle muutuja rakendamine toimub lineaarse keerukusega täisarvargu-mendi suhtes.

Ülesandeid

52. Küsida interaktiivses keskkonnas muutujateconst , div , mod, divMod , min , max tüü-bid ja saada neist aru.

53. Väärtustada interaktiivses keskkonnas avaldis, milleväärtuseks on500-elemendiline tõe-väärtuste list.

54. Väärtustada interaktiivses keskkonnas avaldis, milleväärtuseks on1001-elemendiline list,mille esimesed1000 elementi võrduvad0-ga ja viimane on1.

55. Väärtustada interaktiivses keskkonnas avaldis, milleväärtuseks on list, mille elementidekson listid19, 18, jne, kuni10 elemendiga1.

56. Kirjutada võimalikult lühike avaldis, mille väärtuseks on list, mille elementideks on listi1 : 3 : 6 : 10 : 15 : 21 : 28 : [] vähemalt3-elemendilised algusjupid pikkuse järgi kasvavas jär-jestuses.

Kõik senivaadeldud prefiksoperaatorid on muutujad. Kasutatavaim konstruktor, mis noolib ar-gumendi ja mida kirjutatakse oma argumendi ette, onJust . Tema väärtus on funktsioon, misvõtab argumendiks suvalise väärtusex mingist tüübistA ja annab tulemuseks väärtuseJust xtüübistMaybe A. TüübiperegaMaybe seondub veel konstruktorNothing , mis pole operaator.

Ülal mainisime muutujathead , mille väärtuseks on listi järgi tema pea leidmise funktsioon.See funktsioon töötab listitüübist vastavasse elemenditüüpi. Kui argumendiks juhtub tulema tühilist, siis pead ei ole, pole võimalik midagi vastuseks anda ja head [] väärtustamine lõpetabtäitmisaegse veaga. Standardteegi moodulistData.Maybe leiab aga muutujalistToMaybe ,mille väärtuseks on funktsioon, mis töötab listitüübistMaybe-tüüpi: kui argumentlist on mittetü-hi ja tema pea onx, siis annab see funktsioon väärtuseksJust x. Kui argument on tühi, siis tulebväärtuseksNothing. See funktsioon on sisult sarnane, kuid ei tekita ühelgi juhul täitmisaegsetviga.

Kui on kindel, et avaldise väärtus poleNothing, võib konstruktoristJust lahtisaamiseks raken-dada muutujatfromJust moodulistData.Maybe. Selle muutuja väärtuseks on funktsioon, misargumendilJust x annab tulemuseks väärtusex ja argumendilNothing lõpetab veaga. Samastmoodulist saab veel palju funktsioone, mis seonduvadMaybe-tüüpidega.

37

Page 38: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Eelnevalt mainisime funktsiooni rakendamist märkiva järjestkirjutamise tinglikku käsitlust in-fiksoperaatorina. Märgime, et prefiksse rakendamise jaoks on olemas ka päris infiksoperaator$.Näiteks avaldiselog 5 kohal võib samaväärselt kirjutadalog $ 5 .

Operaatori$ prioriteet ja assotsiatiivsus on tavalise järjestkirjutamisega võrreldes vastupidised:ta on paremassotsiatiivne prioriteediga0. Seega on$ kasulik olukorras, kus mingile objektilerakendatakse järjest mitut funktsiooni. Näiteks avaldise

take 20 (tails (replicate 100 (9 ^ 9)))

asemel võib kasutada samaväärset sulgudeta avaldist

take 20 $ tails $ replicate 100 $ 9 ^ 9 .

Pangem tähele, et$ väärtus on kõrgemat järku funktsioon, kuna võtab argumendiks funktsiooni.

Suundume nüüd tüübimaailma ja otsime näiteid prefiksoperaatoritest tüüpidel. Loomulik mõ-te on, et tüübifunktsioonid, mille liik on∗ → ∗, nagu näiteksList, Maybe ja IO, võiksidolla realiseeritud prefiksoperaatoritena. See oletus peabka paika: tüübifunktsiooniList kodee-rib Haskellis kirjutis [] , tüübifunktsiooneMaybe ja IO aga vastavalt sõnadMaybe ja IO .Näiteks avaldise1 : 0 : [] tüübiks võib kirjutada näiteks[] Int või [] Double võika üldiselt(Num a) => [] a , avaldiseJust pi tüübiks võib pannaMaybe Float võiMaybe Double või (Floating a) => Maybe a.

Lisaks sellele, et operaator[] on erandliku kujuga — ta ei vasta leksikanõuetele —, ei käsit-le interaktiivsed keskkonnad kasutajale vastuseid koostades teda üldse prefiksoperaatorina, vaidtäiesti erilaadsena. Nimelt asetavad interaktiivsed keskkonnad operaatori[] argumendi kantsul-gude sisse, mitte välja järele.

Näiteks infiksoperaatori: väärtuseks olev funktsioon on tüüpiA → List A → List Amistahes tüübiA jaoks; operaatori: tüüpi näitavad interaktiivsed keskkonnad kujula -> [a] -> [a] .

Ülesandeid

57. Küsida interaktiivses keskkonnas konstruktori[] ning muutujatehead , tail , null ,length , sum, reverse , tails , inits , take , drop , splitAt , replicate tüü-bid ja saada neist aru.

58. Küsida interaktiivses keskkonnas konstruktoriteJust , Nothing ning muutujatelistToMaybe , fromJust , print tüübid ja saada neist aru.

59. Kirjutada korrektne tüübiga annoteeritud avaldis, mille tüüp on listitüüp, mille elemendi-tüüp on mingiMaybe-tüüp.

Prefiksoperaatorid tulevad mängu ka kompleksarvutüübi juures. Nimelt sõltub kompleksarvu-tüüp sellest, millist ujukomaarvutüüpi on väljad. Moodulis Data.Complex defineeritud tüübi-

38

Page 39: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

konstruktoriComplex väärtuseks on funktsioon, mis võtab argumendiks väljatüübi A (see peabolema klassistFloating) ja annab tulemuseks kompleksarvutüübi, mille väljatüüp on A. Näi-teks avaldise0 :+ 1 tüübiks võib kirjutadaComplex Float või Complex Double või(Floating a) => Complex a .

Analoogselt on olemas ka mitu ratsionaalarvutüüpi. Ratsionaalarvu esitatakse andmestruktuuri-na lugejast ja nimetajast, mis peavad olema täisarvutüüpi.Moodul Data.Ratio defineerib tüü-bikonstruktoriRatio väärtuseks funktsiooni, mis võtab argumendiks täisarvutüübi A ja annabtulemuseks ratsionaalarvutüübi, mille ratsionaalarvudelugeja ja nimetaja on tüüpiA. Niisiis onvaikimisi võimalikud ratsionaalarvutüübidRatio Int ja Ratio Integer .

On defineeritud tüübimuutujaRational , mis on ekvivalentne avaldisegaRatio Integerja mis on nähtav ka mooduliPrelude kaudu. Tegemist on globaalse tüübimuutujaga ja sellistenimed peavad algama suurtähega. Tavaliselt nimetatakse sellist tüübimuutujattüübisünonüü-

miks (ingl type synonym).

Nime üldkuju ja infikskuju

Infiksoperaatorite tüüpide seletamisel märkisime möödaminnes, et paljas infiksoperaator ei moo-dusta omaette avaldist. See võib tunduda funktsionaalse paradigma rikkumisena, sest funktsio-naalse keele süntaks ei tohiks teha funktsioonide ja muude andmete vahel vahet.

Tegelikult paradigma rikkumist pole. Asi on selles, et kõigil andmemuutujatel ja -konstruktoritelon põhimõtteliselt olemas kaks nimekuju: üks infiksseks ja teine muuks kasutamiseks. Ütlemenende kujude kohta vastavaltinfikskuju ja üldkuju.

Nimekirja (2) sümbolitest koosnevad nimed on kõik infikskujul ja et neid kasutada mitteinfiks-selt, tuleb nad asetada sulgudesse. Näiteks* üldkuju on (* ). Tähtedest, numbritest ja ala-kriipsust koosnevad nimed on üldkujul ja et neid kasutada infiksselt, tuleb nad panna tagurpi-diülakomade vahele. Näiteksmod infikskuju on‘mod‘.

Mistahes muutuja või konstruktor moodustab omaette avaldise küll, aga ta tuleb selleks esitadaüldkujul, sest omaette avaldisena esitades pole kasutus infiksne. Näiteks* ei ole avaldis, kuid(* ) on.

Iga infiksoperaatori saab teha prefiksoperaatoriks ja, vähemalt süntaktiliselt, saab iga prefiksope-raatori teha infiksoperaatoriks. Muidugi saab muutuja või konstruktori infiksne kasutus mõttekasolla vaid juhul, kui tal leidub kaks argumenti, mille vaheleta panna.

Täpsemalt, muutuja või konstruktor on reaalselt infiksseltkasutatav parajasti siis, kui tema väär-tuseks oncurried-kujul funktsioon. Sellisel juhul vastab prefiksse rakendamise esimene argu-ment infiksse rakendamise vasakule argumendile ja prefiksserakendamise teine argument infiks-se rakendamise paremale argumendile. Teistsuguse muutujavõi konstruktori infiksne kasutusannab tüübivea.

39

Page 40: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Vaatleme näiteks infiksoperaatorit++, mille väärtuseks on binaarnekonkatenat-

sioon (ingl concatenation), st funktsioon, mis võtab argumentideks kaks listilja k, mille elemendid on sama tüüpi, ja annab välja listi, mille alguses on jär-jest kõik l elemendid ja kui l on lõplik, siis sellele järgnevad järjest kõikk ele-mendid. Näiteks avaldise replicate 3 0.5 ++ replicate 4 0.8 väärtuson list 0.5 : 0.5 : 0.5 : 0.8 : 0.8 : 0.8 : 0.8 : []. Kuid sama avaldis on esitatav ka kujul(++) (3 ‘replicate ‘ 0.5) (4 ‘replicate ‘ 0.8) , kus varasem infiksope-raator++ on kasutatud prefiksselt ja varasem prefiksoperaatorreplicate infiksselt.

Konkatenatsiooni arvutuse käigus ehitatakse parempoolselisti ette vasakpoolse koopia, pa-rempoolset listi ei uurita üldse. See tähendab, et konkatenatsiooni teostamine sõltub lineaar-selt vasakpoolse listi pikkusest ja ei sõltu parempoolse listi pikkusest. Sellel põhjusel on kon-katenatsioon tehtud paremassotsiatiivseks, sest nii on arvutus efektiivsem. Arvutades avaldist( a + + b) ++ c, kopeeritakse kõigepealta poolt märgitav listb poolt märgitava listi ette jaseejärel mõlemad koosc poolt märgitava listi ette. Esimese listi elemente tõstetase seega ümberkaks korda. Sulgude parempoolse paigutuse korral kopeeritakse iga elementi ülimalt üks kord.Efektiivsuse vahe on seda suurem, mida rohkem liste järjesttuleb konkateneerida.

Kõik infikssed rakendamised kirjutab Haskelli süntaksianalüsaator samaväärselt prefikssete ra-kendamiste kaudu ümber. Näiteks avaldisesta * b saab(* ) a b ja avaldisesta ‘mod‘ b

saabmod a b.

Prefiksoperaatorist tagurpidiülakomade abil tuletatud infiksoperaatorid on, nagu ülejäänudki,vaikimisi prioriteediga 9 vasakassotsiatiivsed, kuid muutujate div ja mod infikskujud onkorrutamise-jagamisega sama prioriteediga7 vasakassotsiatiivsed. Haskell-programmis on või-malik oma defineeritavate nimede infikskujude vaikeatribuute muuta, kuid selleni jõuame hiljem.

Pangem tähele, et nimesid imporditakse ja eksporditakse ainult oma algkujul. MoodulData.Complex näiteks ekspordib nimekuju:+ , kuid mitte nimekuju(:+ ), ning ekspordibnimekujumkPolar , kuid mitte kuju‘mkPolar ‘. Kuju teisendamine on puhtlokaalne ette-võtmine. Nime adresseerimine tema kuju ei mõjuta, ntData.Ratio.% on infikskuju ja tallevastav üldkuju on(Data.Ratio.% ).

Ülesandeid

60. Kirjutada avaldises2 + 3 + 6 infikssed rakendamised prefikssetena ümber.

61. Kirjutada avaldises2 ^ 3 ^ 6 infikssed rakendamised prefikssetena ümber.

62. Kirjutada avaldises4 * 0.3 : 4 - 0.5 : [] infikssed rakendamised prefikssetenaümber.

63. Arvutada nii jagatis kui jääk arvu10000000 jagamisel arvuga4649, lastes väärtustada vaidühe võimalikult lühikese avaldise, kus prefiksset rakendamist ei esine.

40

Page 41: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

64. Määrata muutujadivMod infikskuju prioriteet ja assotsiatiivsus.

65. Nagu ülal väidetud, saab iga infiksoperaatori teha prefiksoperaatoriks ja vastupidi. Kirjel-dada ammendavalt, kuidas.

Mis puutub tüübimuutujatesse ja -konstruktoritesse, siisinfiksoperaatoritel on siingi üldkuju ole-mas. Näiteks funktsioonitüüpiInt → Char saab Haskellis kirjutada nii kujulInt -> Charkui ka kujul (-> ) Int Char . Infikskuju moodustamine tähtedest, numbritest ja alakriipsustkoosneva nimega tüübioperaatoritest on standard-Haskellis keelatud. GHC laiendustes on aga kasee võimalik.

Sektsioonid

Andmemaailma infiksoperaatoritest saab spetsiaalse süntaktilise konstruktsiooniga moodustadann sektsioone (ingl section), mis kujutavad endast avaldist, mille väärtus on funktsioon. Sekt-sioonid jagunevad vasak- ja paremsektsioonideks.

Vasaksektsioon (ingl left section) näeb välja kujul(a ⊕ ), kus a on avaldis ja⊕ on in-fiksoperaator. Sellise vasaksektsiooni väärtus on funktsioon, mis suvalisel argumendilb annabväärtuseksf a b, kusf on infiksoperaatori⊕ väärtus jaa avaldisea väärtus. Analoogselt näebparemsektsioon (ingl right section) välja kujul(⊕ b), kus⊕ on infiksoperaator jab aval-dis. Sellise avaldise väärtus on funktsioon, mis igale argumendilea seab vastavussef a b, kusfon⊕ väärtus jab onb väärtus.

Seega sektsiooni rakendamine argumendile on mõlemal juhulsamaväärne selles sektsioonis esi-neva infiksoperaatori rakendamisega sellele argumendile ja sektsiooni sees olevale avaldisele,kusjuures sektsiooni sees olev avaldis jääb operaatorist samale poole nagu ta on sektsioonis.Mõlemat laadi sektsioone rakendatakse prefiksselt.

Näiteks (* 2) on funktsioon, mis korrutab oma argumendi2-ga. Sama väärtusega on ka(2 * ), sest korrutamine on kommutatiivne. Avaldised(/ 2 ) ja (2 / ) on aga erineva väär-tusega: esimene on funktsioon, mis jagab oma argumendi2-ga, teine aga funktsioon, mis jagabarvu 2 oma argumendiga. Nii on näiteks(* 5) 7 väärtuseks35, (3 ^) 2 väärtuseks9 ja(^ 3) 2 väärtuseks8.

Kui vasaksektsioonide moodustamisel infiksoperaatoreistpiiranguid pole, siis paremsektsioonimoodustamine miinusest pole lubatud, sest kirjutis kujul(- a) läheks segi unaarse miinuserakendamisega. Olukorras, kus oleks vaja miinuse paremsektsiooni, aitab hädast välja muutujasubtract , mille väärtuseks oncurried-kujul funktsioon, mis arvulisel argumendilx annabväärtuseks funktsiooni, mis oma argumendist lahutabx. Näitekssubtract 2 3 väärtus on1.Puuduva sektsiooni(- a) asemel tuleb kasutada avaldistsubtract a.

41

Page 42: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

66. Kirjutada ülesannetes 60 ja 61 toodud avaldistes infikssed rakendamised vasaksektsiooni-dega ümber.

67. Kirjutada ülesannetes 60 ja 61 toodud avaldistes infikssed rakendamised paremsektsiooni-dega ümber.

68. Selgitada võimalikult lihtsate sõnadega, mis on avaldise(: [] ) väärtus.

69. Teha kindlaks, kas avaldiste

(True || ), (False && ), (|| True ), (&& False )

väärtuseks olevad funktsioonid on agarad või laisad.

Infiksset rakendamist sektsiooniga ümber kirjutada pole muidugi mõtet. Sektsioonid on aga mu-gavad olukordades, kus infiksoperaatoril on ainult üks argument fikseeritud, teine on lahtine.

Vaatleme näiteks muutujatmap, mille väärtuseks oncurried-kujul funktsioon, mis võtab argu-mentideks funktsioonif ja listi l ning annab tulemuseks listi, mille elemendid on saadud listil vastavatele elementidele funktsioonif rakendamisel. Avaldises, kus rakendatakse operaatoritmap, ei esine ühtki tema argumentfunktsiooni rakendamist, argumentfunktsiooni argument jääblahtiseks. Kui meil on mingi avaldisl tüüpi [Integer] ja tahame arvutada tema väärtuse kõigielementide ruudud, siis kirjutismap (^ 2) l on selleks lihtsaim tee.

Tähelepanelik lugeja märkas, etmap väärtuseks on kõrgemat järku funktsioon: ta võtab argu-mendiks funktsioone.

Ülesandeid

70. Lahendada ülesanne 28, kasutades sektsioone ja muutujat map.

71. Küsida interaktiivses keskkonnas muutujamap tüüp ja saada sellest aru.

Listide erisüntaksid

Kuna listid on praktilises funktsionaalses programmeerimises põhilised andmestruktuurid, onHaskellis mitu listidega seonduvat erisüntaksit. Peale selles jaotises vaadeldavate on olemas veellistikomprehensioonsüntaks, mille oma suhtelise keerulisuse pärast jätame hilisemaks.

42

Page 43: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Listitüübid, elementide loendid, stringikonstandid

Oleme näinud, et interaktiivsed keskkonnad esitavad kasutajale listitüüpe mitte reeglipärasel ku-jul, vaid kujul, kus listitüübikonstruktori[] argument on konstruktori sees kantsulgude vahel.

See esitus kuulub ka Haskelli süntaksisse, Haskell lubab tüübioperaatorit[] lisaks prefiksselekasutusele rakendada ebareeglipäraselt ülalkirjeldatudviisil. Tähenduse vahet võrreldes prefiksserakendamisega pole.

Niisiis on näiteks[Integer] ja [Bool] tüübiavaldised, mis väljendavad tüüpe, kuhu kuulu-vad vastavalt täisarvuliste elementidega listid ja tõeväärtustüüpi elementidega listid. Kuna strin-gid on sümbolite listid, tähendab stringitüüpi tüübiavaldis [Char] ; kuid on olemas ka ekviva-lentne tüübimuutujaString .

Samuti oleme näinud, kuidas interaktiivsed keskkonnad esitavad kasutajale mittetühje liste: ko-maga eraldatud elementide loendina kantsulgudes. Ka see kuulub tegelikult Haskelli süntaksisse.

Näiteks[pi ] on avaldisegapi : [] samaväärne avaldis,[log 1 , log 3 , log 5 ] agaavaldisegalog 1 : log 3 : log 5 : [] samaväärne avaldis.

Selle süntaksiga saab liste esitada pisut lühemalt kui koolonite kaudu. Kuid selle süntaksi kasuta-mine pole kaugeltki alati võimalik. Listi esitamine elementide loendina eeldab, et listi struktuuron kodeerimise ajal lõpuni teada, sest taoline esitus näitab ära listi täpse pikkuse. See tingimuson aga praktikas harva täidetud. Kunagi ei ole võimalik elementide loendina esitada lõpmatuidvõi osalisi liste.

Kuna stringid on listid, on stringitüüpi objekte võimalik kodeerida kõigi sobivate listisüntaksiteabil. Stringikonstandid on siiski kodeeritavad üldlevinud kirjaviisiga — sümbolite joruga jutu-märkide vahel. See on sisseehitatud erisüntaks sümbolite listi jaoks. Seejuures on kasutatavadkõik sümbolite langjoonkoodid (ainult et stringis pole neid muidugi vaja apostroofide vahelepanna). Apostroofi kodeerimine kujul\’ pole kohustuslik, küll aga jutumärgi kodeerimine kujul\" .

Stringikonstant ei saa sisaldada muutujaid. Stringikonstandi kasutamine seega eeldab, et tervestring koos oma sümbolitega on kodeerimise ajal teada.

Ülesandeid

72. Kirjutada interaktiivse interpretaatori käsurealt avaldisi, mille väärtuseks on string “Tere!”.Kasutada kolme erinevat süntaktilist konstruktsiooni.

JavatoString -meetodiga analoogse Haskelli muutujashow väärtus on funktsioon, mis annabvälja oma argumendi stringikujul. See muutuja on defineeritud arvude, sümbolite, tõeväärtustejpt, üldiselt klassiShow kuuluvate tüüpide esindajate jaoks, kuid mitte kõigil Haskellis ettetulevatel objektidel.

43

Page 44: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Näiteks funktsioonide stringiks teisendamine on standardteegis eeldefineerimata. Samuti võibjuhtuda, etshow väärtuseks on defineeritud funktsioon, mille tulemus ei kajasta täpselt argu-mendi originaalset struktuuri. Nii on lugu näiteks listidega, mis teisendatakse enamasti argu-mentide komadega eraldatud nimekirjaks kantsulgude vahel, sarnaselt erisüntaksile. Stringid agapannakse jutumärkidesse ja asendatakse temas leiduvad erisümbolid nende langjoonkoodidega.Kodeerimist kasutatakse ka üksikute sümbolite korral. Kõigil loetletud juhtudel kehtib põhimõte,et stringiksteisendamise tulemus on ise korrektne Haskelli kirjaviis antud väärtuse esitamiseks.

Muutujashow rakendamise toimet saab interaktiivses keskkonnas testida isegi ilma teda ilmu-tatult rakendamata, sest interaktiivsed keskkonnad rakendavad just teda vaikimisi kõigile aval-distele, mida kasutaja neil väärtustada laseb, et nende väärtusi väljastamiseks stringikujule viia.Hugsis on võimalik see käitumine vajadusel ära muuta, võttes maha optsiooniu käsuga:s -u— siis teisendab Hugs kõik avaldised stringiks sisseehitatud viisil, sõltumatashow väärtusest.

Vaikiv show lisamine interaktiivses keskkonnas põhjustab selle, et täiesti korrektse avaldise and-misel käsurealt tekib tüübiviga, kui see avaldis on tüüpi, mille jaoks on muutujashow definee-rimata, näiteks funktsioonitüüpi. Kui muutujale või konstruktorile on avaldises antud niipaljuargumente, et tulemus pole funktsioonitüüpi, siis öeldakse, et ta on sealtäisargumenteeri-

tud. Seega saab interaktiivse keskkonna käsurealt väärtustamiseks anda vaid täisargumenteeri-tud avaldisi, kuigi Haskellis on igati korrektsed ka täisargumenteerimata avaldised.

Ülesandeid

73. Anda interaktiivse keskkonna käsurealt avaldis, milles muutujashow on rakendatud mõne-le arvule, sümbolile või tõeväärtusele ilmutatult. Mis vahe on saadavas vastuses võrreldessellega, kuishow on puudu, ja miks?

74. Loetleda järjest sümbolid avaldiseshow 3 , avaldise show (show 3) ja avaldiseshow (show (show 3)) väärtuseks olevas stringis.

75. Anda interaktiivse interpretaatori käsurealt täisargumenteerimata avaldisi ja saada aru järg-nevast veateatest.

76. Saavutada Hugsis olukord, kus ta täisargumenteerimataavaldise väärtustamisel ei anna vea-teadet.

Muutujaprint rakendamisel mingile argumendile rakendatakse kõigepealt muutujatshow jaseejärel kirjutatakse saadud string standardväljundisse. Seegaprint on kasutatav ainult sellis-tel argumentidel, millel kashow. Kui objekt, mida soovitakse standardväljundisse saata, jubaon string, siis võib olla sobivamprint asemel kasutada muutujaidputStr ja putStrLn .Nende muutujate väärtuseks on funktsioonid, mis võtavad argumendiks stringi ja kirjutavad tastandardväljundisse,putStrLn seejuures lisab ka reavahetuse.

44

Page 45: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Stringikonstantide võibolla et levinuim kasutus on veateadete kirjutamisel. Veateadete tekita-miseks on olemas operaatorerror , mille väärtus on funktsioon, mis võtab argumendiks su-valise stringis ja annab tulemuseks⊥; selle operaatori rakendamine lõpeb veateategas. Andesnäiteks avaldiseerror "Minu viga! " interaktiivses keskkonnas väärtustada, tekib täitmis-aegne viga, kus veateateks on “Minu viga!”.

Muutuja error rakendamise tulemus võib olla suvalist tüüpi, seetõttu on niimoodi võimaliktäitmisaegset viga tekitada ühtmoodi sõltumata sellest, millist tüüpi tulemust kontekst nõuab.

Ülesandeid

77. Küsida interaktiivses keskkonnas muutujaerror tüüp ja saada sellest aru.

78. Anda interaktiivse keskkonna käsurealt korrektne avaldis tüüpiInt , mille väärtustamisellõpetatakse töö teie ette antud veateatega.

Aritmeetilised progressioonid

Liste, mille elemendid moodustavad aritmeetilise progressiooni, saab moodustada omaette eri-süntaksiga.

Üldine tõkestatud progressioon esitatakse kahe esimese elemendi ja tõkke kaudu kujul[a, b .. c]. Kui a, b, c väärtuseks on vastavalt arvuda, b, c, siis[a, b .. c] väärtus onlist, mille elemendid võetakse järjest aritmeetilisest jadast esimese elemendigaa vahegab − aparajasti senikaua, kui jada liikmed pole arvteljel möödunud arvustc (täpsemalt, sattunud teiselepoolec-d vaadates arvuc− (b− a) poolt). Näiteks avaldis[1, 3, 5, 7, 9] on samaväärneavaldisega[1, 3 .. 9], samuti avaldisega[1, 3 .. 10].

Kui progressioon on sammuga1, võib aritmeetilise jada süntaksis koma ja talle järgneva elemen-di ära jätta. Näiteks avaldise[1 .. 10] väärtuseks on list järjekorras arvudest1 kuni 10.

Ülesandeid

79. Koostada aritmeetilise jada süntaksiga avaldisi ja anda neid interaktiivse interpretaatori kä-surealt. Proovida nii positiivse kui negatiivse vahega progressioone ning kummalgi juhulnii esimesest elemendist suurema kui väiksema väärtusega tõkkega.

80. Arvutada kõigi3-kohaliste1-ga lõppevate arvude korrutis.

81. Arvutada suurim2006-ga jaguv arv esimese100000000 naturaalarvu seas, lastes selleksväärtustada aritmeetikatehteid mitte sisaldava avaldise.

45

Page 46: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

82. Kirjutada interaktiivses keskkonnas avaldis, mille väärtuseks on list, milles on kasvavasjärjestuses parajasti kõik positiivsed täisarvud kuni1000-ni peale paaritute kolmekohaliste.

Haskellis võib avaldise väärtus olla ka lõpmatu andmestruktuur, muuhulgas lõpmatu list. Selli-seid avaldisi saab praktikas edukalt kasutada, sest liste konstrueeritakse laisalt, parajasti niipaljukui hetkel vaja. Asjaolu, et avaldise väärtuseks on lõpmatulist, tähendab seda, et avaldise aseta-misel sobivasse konteksti on põhimõtteliselt võimalik lõpliku ressursiga arvutada selle listi kui-tahes kauget fragmenti. Sobiv kontekst peab täpsustama, millist osa listist tahetakse. Lõpmatulisti enda täielik väärtustamine on muidugi ükskõik kui suure lõpliku ressursiga võimatu.

Lõpmatud listid on mugavad, sest võimaldavad tihti vältidaülesande seisukohalt kunstliku piirisissetoomist.

Kui proovisite aritmeetilise progressiooni süntaksi harjutamisel kirjutada progressiooni, millesvahe on0 ja tõke pole esimesest elemendist väiksem, näiteks avaldisega[0, 0 .. 1], siisolete lõpmatu listiga juba vastamisi sattunud. Lõpmatu listi täieliku väärtustamise protsess eipeatu enne, kui miski väline teda katkestab. Interaktiivses keskkonnas saab protsessi katkesta-da, vajutades <Ctrl>+c . Parem variant lõpmatu listi testimiseks on tema erinevaidlõplikke osiküsida.

Lõplike algusjuppide küsimiseks sobib juba varem vaadeldud muutujatake . Teine võimaluson pärida üksikuid elemente, milleks sobib infiksoperaator!! . Kui l väärtuseks on listl ja a

väärtuseks naturaalarva, mis on väikseml pikkusest, siisl !! a väärtus onl element järje-korranumbrigaa. Seejuures nummerdatakse listi elemente alates0-st. Kuia väärtus pole nõutudpiirides, siis tekib täitmisaegne viga.

Näiteks avaldise(1 : 2 : 3 : []) !! 1 väärtus on2. Avaldise[0, 0 .. 1] !! a

väärtus on alati0, kui vaida väärtuseks on naturaalarv.

Tuleb arvestada, et listi elemendi leidmine operaatoriga!! toimub järjekorranumbri suhtes li-neaarses ajas. Lineaarne sõltuvus järjekorranumbrist tekib listi ahelalaadse ülesehituse tõttu, kusiga elemendini on võimalik jõuda vaid listi läbikäimisega algusest temani.

Kuna lõpmatud listid on praktilised, siis pole midagi imestada, et aritmeetilise progressioonisüntaksist leiduvad Haskellis ka tõkestamata variandid. Need saame senivaadeldutest, kui jätametõket märkiva avaldise ära. Sellisel juhul on avaldise väärtuseks lõpmatu list, mille elemendidmoodustavad terve aritmeetilise jada, mis on määratud esimese ja teise elemendiga või, kui kateine element puudub, esimese elemendiga ja vahega1. Näiteks[1, 3 .. ] väärtuseks onlist kõigi paaritute naturaalarvudega kasvavas järjestuses,[1 .. ] väärtuseks aga list kõigipositiivsete naturaalarvudega kasvavas järjestuses.

Ülesandeid

83. Teha kindlaks operaatori!! prioriteet ja assotsiatiivsus.

46

Page 47: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

84. Väärtustada interaktiivse interpretaatori käsurealtavaldis, mille väärtuseks on lõpmatu list.Katkestada arvutus ja seejärel täiendada avaldist nii, et saadud avaldise väärtustamine lõpebkiiresti.

85. Kirjutada interaktiivses keskkonnas avaldis, mille väärtuseks on lõpmatu list, mille esime-sed elemendid on1 ja 2 ning kõik ülejäänud elemendid on0-d. Testida seda listi temaüksikuid elemente küsides.

86. Kirjutada avaldis, mille väärtuseks on list, mille elementideks on kasvavas järjestuses kõikpositiivsed täisarvud, mis annavad7-ga jagades jäägi6.

87. Kirjutada ühele reale (koos interaktiivse keskkonna viibaga alla80 sümboli) mahtuv arit-meetikatehteid mitte sisaldav avaldis, mille väärtuseks on list, milles on kasvavas järjestuses12 esimest899979996999599949993999299919990999-ga jaguvat positiivset täisarvu.

88. Katseliselt veenduda, et!! abil listi elemendi leidmine on järjekorranumbri suhtes lineaar-se keerukusega.

Aritmeetilise progressiooni süntaksi kasutamiseks ei pealistide elemendid tingimata arvud ole-ma. Nii saab koostada liste, mille elemenditüüp on suvalineklassiEnum esindaja. Tõlgenda-maks aritmeetilise progressiooni avaldist mittearvuliste elementide puhul teisendatakse need ele-mendid lühikesteks täisarvudeks operaatorigafromEnum , koostatakse list aritmeetilise progres-siooniga nende baasil ja seejärel teisendatakse listi elemendid tagasi algsesse tüüpi operaatorigatoEnum .

MuutujadfromEnum ja toEnum omavad tähendust parajasti klassiEnum tüüpide jaoks. Nen-de väärtust võib mõista tüübiteisendusfunktsioonina või ka väärtuste kodeerimis- ja dekodeeri-misfunktsioonina, kusjuures koodid on lühikesed täisarvud, nii nagu juba varem käsitletudordja chr moodulistData.Char vastavalt kodeerivad ja dekodeerivad sümboleid. Enamgi, süm-bolitüüp kuulub klassiEnum, kusjuuresfromEnum ja toEnum ongi väärtuselt võrdsed justnendesamade muutujategaord ja chr .

See näitab esiteks, et aritmeetilise progressiooni erisüntaksit saab kasutada ka stringide esita-miseks. Teiseks, sümbolite järjestus vastab nende koodidekui arvude standardsele järjestusele.Kolmandaks tuleneb siit, et sümbolite koodide kasutamiseks pole moodulitData.Char laadidavajagi, kunafromEnum ja toEnum saab ka moodulistPrelude. Kuid toEnum kasutamiselvõib olla vaja annoteerida tulemustüüp, kui kontekstist pole see tüüp üheselt selge. Näiteks tah-tes leida sümbolit koodiga33, tuleb kirjutadatoEnum 33 :: Char , sest muidu süsteem eitea, millisesseEnum-klassi tüüpi on vaja teisendada.

Ülesandeid

89. Küsida interaktiivses keskkonnas muutujatefromEnum ja toEnum tüübid ja saada neistaru.

47

Page 48: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

90. Moodulit Data.Char sisse lugemata teha interaktiivse keskkonna abil kindlakssümbol,mille kood on39.

91. Teha kindlaks tõeväärtuste täisarvkoodid.

92. Arvutada interaktiivses keskkonnas kiiresti ladina tähestiku tähtede arv.

93. Kirjutada interaktiivse keskkonna käsurealt võimalikult lühike avaldis, mille väärtuseksoleks ühes stringis ladina tähestik suurtähtedest, millele järgneks ladina tähestik väiketäh-tedest.

KlassiEnum tüübid on tihti lõplikud, mistõttu vaid lõplik arv koode on kasutuses. Seetõttu võibtõkestamata aritmeetilise progressiooni süntaksi puhul juhtuda, et mingist kohast alates koodide-le enam elemente ei vasta. Sellisel juhul sellel kohal list lõpetatakse. Seega tõkestamata aritmee-tilise progressiooni süntaks võib väärtuseks ka lõpliku listi anda.

Näiteks[False .. ] väärtuseks on2-elemendiline listFalse : True : [].

Kõige universaalsem moodus lõpmatut listi tekitada on mitte aritmeetilise progressiooni süntak-siga, vaid hoopis muutujagarepeat . Selle muutuja väärtuseks olev funktsioon võtab suvaliseargumendix ja annab väärtuseks lõpmatu listi, mille iga element onx. See sobib ka juhul, kuix tüüp ei kuulu klassiEnum. Kui vaja huvitavamat listi, võib kasutada muutujatcycle , milleväärtuseks on funktsioon, mis võtab argumendiks listil ja kui see pole tühi, siis annab välja lõp-matu listi, kusl elemendid korduvad tsükliliselt. Näitekscycle [1, 2, 1] väärtuseks onlõpmatu list elementidega1, 2, 1, 1, 2, 1, 1, 2, 1, . . . . . ..

Haskellis programmeerides puutume tihti kokku mitte ainult eraldiseisvate lõpmatute listi-dega, vaid andmestruktuuridega, mille komponentideks on lõpmatud listid. Näiteks avaldise( [1, 3 .. ] , [2, 4 .. ]) väärtus on paar, mille kumbki komponent on lõpmatulist. Testides niisugust andmestruktuuri, on oht osa kogustruktuurist ära kaotada. Näiteks kui la-seksime seda paari lihtsalt käsurealt väärtustada, näeksime ainult paarituid arve, arvutus ei jõuakskunagi paari teise komponendini. Ometi on ka paarisarvud olemas, selles veendumiseks piisablihtsalt võtta struktuurist teine komponent:snd ( [1, 3 .. ] , [2, 4 .. ]) annabtulemuseks paarisarvudest koosneva listi.

Ülesandeid

94. Kirjutada interaktiivses keskkonnas avaldis, mille väärtuseks on lõpmatu list, mille iga ele-ment on kõigi naturaalarvude list. Testida seda lõpmatut maatriksit tema üksikute elemen-tide küsimisega.

95. Kirjutada interaktiivses keskkonnas avaldis, mille väärtuseks on kahes suunas lõpmatu liit-mistabel listide listina, st listi nrn element nrm peab oleman + m, kus nummerdaminekäib alates0-st.

48

Page 49: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Oma muutujate kasutamine

Lihtsad konstruktsioonid näidistega

Süntaktilised objektid, mida oleme seni vaadelnud, on konstrueeritud vaid mujal defineeritudmuutujate ja konstruktorite kaudu; uute muutujate ja konstruktorite defineerimist me veel vaa-delnud pole. (Erandiks olid polümorfsete avaldiste tüübid, milles sisalduvad lokaalsed tüübimuu-tujad.)

Uute muutujate defineerimiseks on vaja kasutada näidiseid —avaldistele tähenduselt vastandu-vaid süntaktilisi objekte. Nemad moodustavad käesoleva jaotise põhiteema. Enam ei saa möödaka deklaratsioonidest. Käesolevas jaotises piirdume lihtsate näidiseid kasutavate deklaratsiooni-de ja avaldistega, milles puudub hargnemine.

Konstruktorite defineerimine on hoopis teistsugune asi ja jääb esialgu vaatluse alt välja. Vahe onselles, et muutuja defineerimisel seotakse muutuja juba põhimõtteliselt olemasoleva väärtusega,kuid konstruktori defineerimine tähendab väärtuste maailma laiendamist.

Avaldised ja näidised

Enne kui selgitada näidiste ja deklaratsioonide olemust, vaatame korraks üldplaanis peale kasenituntud struktuursetele süntaktilistele objektidele— avaldistele.

Avaldis esitab skeemi, kuidas üks väärtus — avaldiseväärtus — sõltub avaldise elementaarosa-de väärtustest. Näiteks kuix väärtus on2, y väärtus on3 ja + väärtus on liitmine, siis avaldisex + y väärtus on2 + 3 ehk5. Kui agax väärtus on5, y väärtus on8 ja + väärtus on korrutami-ne, siis sama avaldise väärtus on5 · 8 ehk40. Niisiis avaldisx + y esitab skeemi, kuidas üksväärtus sõltub muutujatex , y ja + väärtusest.

Teisiti öeldes, avaldis esindab väärtuste laine liikumist“seest välja”.

Võtame teise näitena avaldise(a , b) . Kui a väärtus on2 ja b väärtus on3, siis avaldise(a , b) väärtus on paar(2 , 3). Kui a väärtus on5 ja b väärtus on8, siis sama avaldise väärtuson paar(5 , 8). Avaldis (a , b) esitab skeemi, kuidas üks väärtus sõltub muutujatea ja b

49

Page 50: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

väärtustest.

Avaldise teisendamist info hankimiseks tema väärtuse kohta nimetatakse avaldiseväärtusta-

miseks (ingl evaluation).

Näidis (ingl pattern) kujutab endast avaldise suhtes tähenduse poolest duaalset süntaktilist ob-jekti. Näidis esitab skeemi, kuidas näidise elementaarosade väärtused sõltuvad ühest suvalisest(sobivat tüüpi) väärtusest — näidiseväärtusest. Näidis esindab väärtuste laine liikumist “väl-jast sisse”.

Näiteks kui näidise(a , b) väärtus on paar(2 , 3), siis a väärtus on2 ja b väärtus on3. Kuiaga sama näidise väärtus on(5 , 8), siisa väärtus on5 ja b väärtus on8. Näidis(a , b) esitabskeemi, kuidas muutujatea, b väärtused sõltuvad ühest etteantud väärtusest. Nagu näha,võivadavaldis ja näidis ühtmoodi välja näha. Kontekst näitab, kasmõtteks on konstrueerida väärtusetteantud muutujate väärtuste järgi või leida muutujate väärtused etteantud väärtuse järgi.

Sellises vaates avaldistele ja näidistele käsitletakse muutujate väärtusi muutuvatena, kuid konst-ruktorite väärtusi konstantsetena. Näiteks sulud ja koma tähendavad üheselt paarikonstruktorit,kuid + väärtus võis varieeruda. See näitab, et avaldise ja näidise“elementaarosa”, millele saiviidatud, ei tähenda siin igasugust süntaktiliselt atomaarset osa: konstruktorid, olgugi et süntak-tiliselt atomaarsed, jäävad mängust välja, nemad saavad oma väärtuse väljaspool.

Oleme tõele lähedal, kui ütleme, et elementaarosadeks on parajasti kõik muutujad. Avaldistepuhul selline arusaam töötab ja töötaks ka näidiste puhul, kui me Haskelli asemel tegeleks loogi-kaga, mille semantikas puudub arvutusprotsess. Haskell aga võimaldab väärtustamise järjekordavarieerida, märkides suvalisi näidiseid vaadeldavas mõttes elementaarseteks. Niisuguse näidisejuures väärtuste laine progresseerumine peatub, sissepoole ei jätku.

Sõltumata näidise konstruktsioonist kehtib nõue, et ükskimuutuja ei tohi näidises korduda. Näi-teks(a , a) on näidisena illegaalne. Põhjus on selles, et niisugune näidis ei võimalda muutu-jate väärtusi üheselt määrata. Kui tema väärtus oleks(2 , 3), mida ei saa välistada, siisa väärtuspeaks olema nii2 kui ka 3.

Isegi kui näidis on legaalne, võib juhtuda, et tema kasutamine muutujate või teiste elementaarosa-de väärtuste määramiseks pole võimalik. Elementaarosade väärtuste määramiseks nõutakse, etnäidis ja väärtus klapiksid omavahel oma struktuuri poolest. Seda, et näidis ja väärtus klapivadstruktuurilt, nimetatakse näidisesobitumiseks (ingl matching) väärtusega. Kui näidis sobitubväärtusega, siis väärtus määrab näidise kõigi elementaarosade väärtused üheselt.

Näiteks näidis(a , b) sobitub väärtustega(2 , 3) ja (5 , 8). Sama näidis aga ei sobitu väärtu-sega⊥, sest⊥ pole paarikujul, struktuur ei klapi. Sama näidis siiski sobitub väärtusega(2 ,⊥) jaisegi väärtusega(⊥ ,⊥).

Vaatame veel näidistx : xs . Tema määrab muutujatex ja xs väärtused mistahes mittetühjalisti järgi: kui näidise väärtus on näiteks1 : 0 : [], siis x väärtuseks on1 ja xs väärtuseks0 : [],kui aga näidise väärtus on lõpmatu list1 : 2 : 3 : . . ., siisx väärtus on1 ja xs väärtus lõpmatu list2 : 3 : . . .. Kuid kui näidisex : xs väärtus on tühi list, siis näidis ei sobitu.

50

Page 51: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Haskellis teeb asja keerulisemaks asjaolu, et väärtus, millega näidise sobitumist tuleb kontrollida,on üldjuhul välja arvutamata. Selle asemel on mingi avaldisning näidise sobitumise kontrolli-misel väärtustatakse seda avaldist ainult niikaugele, et selguks, kas tema väärtuse struktuur vastabnäidise omale sedavõrd, et saab rääkida näidise elementaarosadele vastavatest väärtuse osadest.Teisi sõnu, avaldist teisendatakse niikaua, kuni tulemus kas vastab näidise struktuurile kuni ele-mentaarosadeni, mispuhul näidis loetakse sobituvaks ja tema elementaarosadega seotakse vas-tavad alamavaldised, või võtab näidise struktuurile vastukäiva kuju, mida edasiste teisendustekäigus enam muuta ei saa — siis loetakse näidis mittesobituvaks. Seega isegi kontrolli lõppedesei ole enamasti väärtus teada. Näiteks võiks näidisex : xs väärtust1 : 2 : 3 : . . . reaalselt esin-dada avaldis[1 .. ]. Ka niisugusel juhul tehakse näidise sobitumine kindlaks,kuigi väärtuseväljaarvutamine on võimatu.

Protsessi, millesse on haaratud üks näidis ja avaldis ning mis hõlmab näidise sobitumise kont-rolli avaldise väärtusega ja positiivse tulemuse korral näidise iga elementaarosa jaoks avaldiseleidmise, millega ta väärtuselt võrdub, nimetatakse näidisesobitamiseks (ingl match).

Reaalselt on avaldise väärtustamise ja näidise sobitamiseprotsessid üksteisega tihedalt põimu-nud: peaaegu iga avaldise väärtustamine nõuab näidiste sobitamisi ning peaaegu iga näidise so-bitamine omakorda avaldiste väärtustamisi.

Lihtsad deklaratsioonid

Andmedeklaratsioonid, mis defineerivad uusi muutujaid, koosnevadvasakust ja paremast

poolest. Lihtsaimal juhul on vasakuks pooleks üks näidis, parem pool aga koosneb võrdusmär-gist ja avaldisest. Selline deklaratsioon on niisiis kujul

p = e. (4)

Kõige abstraktsemalt on deklaratsiooni (4) mõte omistada näidisep väärtuseks avaldisee väärtus.See loob potentsiaali näidisesp esinevate muutujate väärtuste määramiseks: avaldise muutujadmääravad avaldise väärtuse, mis on deklaratsiooni põhjal sama mis näidise väärtus, ning see võibmäärata näidise muutujate väärtused. Kuidas see konkreetselt realiseerub, on iseküsimus, midavaatleme hiljem. Vaatame kõigepealt sellise deklaratsiooni näitel läbi tähtsamad näidiseliigid.

Kujul (4) oli ka meie esimene kirjutatud deklaratsioon (1).Seal oli vasakuks pooleks muutujamain ja paremaks pooleks avaldisprint (2 + (-3)) . Muutuja ongi üks võimalik näidisealaliik, nagu ülal juba ilmnes. Näiteks deklaratsioon

base= 10

(5)

defineerib muutujabase väärtuseks10.

51

Page 52: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

96. Kirjutada deklaratsioon (5) oma Haskell-faili. Testida defineeritud muutujat interaktiivsesinterpretaatoris.

97. Lisada deklaratsioon, mis defineerib muutujae väärtuseks arvue ujukomalise lähendi.

Teine põhiline näidiseliik on näidis, mis on konstruktori abil koostatud osadest, mis on omakordanäidised. Seni vaatlesime kaht sellist näidist:(a , b) , mis on koostatud muutujatesta ja bpaarikonstruktori abil, jax : xs , mis on koostatud muutujatestx ja xs listikonstruktori: abil.Siia kuuluvad ka näidised, mis koosnevad paljast konstruktorist, nagu näiteksTrue , False ,Nothing .

Näidistest konstruktoriga uue näidise koostamine käib sarnaselt avaldistest konstruktoriga uueavaldise koostamisega. Muuhulgas infiksoperaatorite prioriteedid ja assotsiatiivsus on samad misavaldistes, samuti on võimalik kasutada muutujate infiks- ja üldkuju. Näiteks näidisx : xs onsamaväärne näidisega(: ) x xs . Kuid on ka oluline erinevus: konstruktoriga koostatud näidisei tohi olla funktsioonitüüpi. Teisi sõnu, konstruktor peab näidises olema täisargumenteeritud.Näiteks näidise(: ) x lükkab Haskell tagasi, sest ta on funktsioonitüüpi, ta vajab veel üht ar-gumenti.

Konstruktoriga koostatud näidise elementaarosad on üldjuhul parajasti konstruktori argumenti-de elementaarosad. Kui konstruktoril argumendid puuduvad, siis puuduvad näidisel ka elemen-taarosad ja sobitumine sõltub ainult sellest, kas väärtus vastab konstruktorile. Näiteks näidisTrue sobitub väärtusegaTrue, kuid ei sobitu väärtusegaFalse.

Kuna konstruktor sisuliselt moodustab oma argumentidest andmestruktuuri, siis tema abil ehita-tud väärtuse järgi on konstruktori argumentide väärtused alati üheselt määratud. Seega küsimusekonstruktoriga ehitatud näidise sobitumisest oma väärtusega saab lahendada järgnevalt. Kui väär-tus ei ole ehitatud sama konstruktoriga, siis näidis ei sobitu temaga, sest struktuur ei klapi. Kuiväärtus on ehitatud sama konstruktoriga, siis sobitub kogunäidis väärtusega parajasti siis, kuikõik konstruktori argumentnäidised sobituvad väärtuse vastavate osadega, ning sealt saame kaelementaarosade väärtused.

Lihtne näide deklaratsioonist kujul (4), mille vasak pool on konstruktoriga ehitatud näidis, on

(x , y)= (5 , 0.2)

. (6)

Kuna näidise ja avaldise struktuurid sobivad, siis see deklaratsioon määrabx väärtuseks5 ja yväärtuseks0.2.

Deklaratsioon

p : ps= 1 : 3 : 4 : []

(7)

52

Page 53: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

annab muutujalep väärtuseks1 ja muutujaleps väärtuseks3 : 4 : []. Soovi korral võime rohkemelemente algusest muutujatega välja tuua, näiteks deklaratsioon

p : q : qs= 1 : 3 : 4 : []

(8)

defineeribp väärtuseks1, q väärtuseks3 ja qs väärtuseks4 : [].

Muidugi saame deklaratsioonid (7), (8) samaväärselt ümberkirjutada, kasutades paremates pool-tes listide erisüntaksit. Saame vastavalt

p : ps= [1, 3, 4]

,

p : q : qs= [1, 3, 4]

.

Kuna stringid on sümbolite listid, on korrektne ka näiteks deklaratsioon

c : cs= "Hei! "

, (9)

sest vasakul olev näidis sobitub iga mittetühja listiga, muuhulgas stringiga “Hei!”. Selle dekla-ratsiooni tulemusel saabc väärtuseks H-tähe ningcs stringi “ei!”.

Ka näidistes lubab Haskell kasutada listide erisüntaksit elementide loeteluga. Näiteks on võima-lik muutujadx , y , z defineerida deklaratsiooniga

[x, y, z]= [1, 3, 4]

.

Põhimõtteliselt on näidisena kasutatavad ka stringkonstandid, samuti sümbol- ja arvkonstandid.

Ülesandeid

98. Millised näidistest

(: ), (: ) x xs xss , (x : xs) : xss , [] , [1],Just , Just a , Just sin , Just (+), Just Nothing

on korrektsed?

99. Defineerida ühe deklaratsiooniga kaks muutujat, mille väärtusteks on vastavalt Teie ees- japerekonnanimi stringina. Anda interaktiivse keskkonna käsurealt neid muutujaid sisaldavavaldis, mille väärtuseks oleks Teie täisnimi eesnimega ees. Seejärel anda avaldis, milleväärtuseks oleks Teie ametlik täisnimi perekonnanimega ees. Tulemused peavad vastamaeesti keele reeglitele.

53

Page 54: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

100. Kirjutada oma moodulisse deklaratsioon, mis defineerib muutujateq ja r väärtusteks vas-tavalt arvujärjendi0.501, 0.502, . . . , 1.499 arvude korrutise täis- ja murdosa. Anda inte-raktiivse keskkonna käsurealt avaldis, mille väärtuseks on korrektne eestikeelne lause, misütleb, mis on selle korrutise täisosa ja mis on murdosa.

101. Modifitseerida deklaratsiooni (9) paremat poolt selliselt, et deklaratsiooni tulemusel saakscs väärtuseks tühi string.

102. Kas saab kirjutada deklaratsiooni (7) vasaku poole samaväärselt ümber elementide loetelukujul?

Näidise moodustamisel konstruktoriga viitavad konstruktori argumentnäidised struktuuri omava-hel lõikumatutele osadele. Näiteks näidises(x , y) viitavadx ja y paari erinevatele kompo-nentidele ja need ei lõiku omavahel. Haskell lubab ka olukorda, kus näidise muutujate väärtustestüks on teise osa.

Seda võimaldab nnehknäidis (ingl as-pattern), mis konstrueeritakse, ühendades mingi muu-tuja ja mingi näidise infiksselt sümboliga@ . See sümbol tähendab põhimõtteliselt näidiste võr-dust; st kui näidisex@ p väärtus onx, siis onx väärtusx ja p väärtusx. Tulemusena on näidisep muutujate väärtused muutujax väärtuse osad. Sümboli valik tuleb ingliskeelsest sõnast “as”.Eesti keeles võib näidist kujulx@ p lugeda “x ehkp”.

Ehknäidisex@ p elementaarosad on parajasti näidisep elementaarosad jax.

Näiteks võiksime täiendada deklaratsiooni (6) vasakut poolt selliselt, et lisaks muutujatex ja yväärtuse määramisele kirjutataks kogu paari väärtus muutujassez :

z@ (x , y)= (5 , 0.2)

. (10)

Ehknäidise konstrueerimisel tuleb arvestada, et@ seob tugevamini kõigist infiksoperaatoritestja isegi funktsioonirakendamisest (piltlikult öeldes on@ prioriteediga11).

Vaatleme veel deklaratsiooni

t : ts @ (u : us)= "string "

. (11)

Kuna @ on kõigist infikssetest seostest tugevaim, siis on vasaku poole näidis sama mist : (ts @ (u : us)) . Seega deklaratsiooni (11) tulemusena saab muutujat väärtuseks s-tähe, näidists @ (u : us) aga stringi “tring”. Järelikult nii muutujats kui näidiseu : usväärtuseks saab “tring”. Siit tulenevalt saab muutujau väärtuseks t-täht ja muutujaus väärtusekssõna “ring”.

Ülesandeid

54

Page 55: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

103. Kasutades ülesande 97 lahendust, kirjutada võimalikult lühike deklaratsioon, mis defineeribmuutujaeee väärtuseks arvueee

ujukomalise lähendi ja muutujaeeeList väärtusekslisti, mille ainsaks elemendiks on seesama väärtus.

104. Kirjutada võimalikult lühike deklaratsioon, mis defineerib muutujatec1 , c2 , c3 väärtuseksvastavalt1, 2, 3 ning muutujad väärtuseks(2 , 3).

Veel üks näidiseliik onjokker (ingl wildcard), mis kirjutatakse üksiku alakriipsuna. Jokker onelementaarosadeta näidis, mis sobitub iga väärtusega. Jokkerit võib lugeda väljendiga “ükskõikmis”. Sisuliselt tähistab jokker ebaolulist väärtust, seeväärtus unustatakse ära, kuna ühtki temaosa ei seota.

Näiteks on täiesti legaalne deklaratsioon

_ = 15 .

Selline deklaratsioon on muidugi täiesti kasutu. Küll aga on jokkerit mõnikord mõistlik kasutadasuurema näidise koosseisus. Vaatleme näiteks deklaratsiooni (10), mis defineeris muutujadx , y ,z . Oletame, et meil on vajalik kasutada muutujaidx väärtusega5 ja z väärtusega(5 , 0.2), kuidy väärtusega0.2 tarvis ei lähe. Siis võib deklaratsioonis (10)y asendada jokkeriga, kirjutades

z@ (x , _)= (5 , 0.2)

.

Mittekasutatavate muutujate asendamine jokkeriga hoiab kokku arvuti mälu ja säästab ka koodiuurivat inimest nende muutujate tähenduse meelespidamisest.

Ülesandeid

105. Lahendada ülesanne 103 lisatingimusel, et listide erisüntakseid näidistes ei kasuta. Lisa-muutujaid mitte defineerida.

106. Kirjutada võimalikult lühike deklaratsioon, mis defineerib muutujaae väärtuseks strin-gi “ARKTIKA-EKSPEDITSIOONI MÄLESTUSED” ja muutujaae’ väärtuseks stringi“ANTARKTIKA-EKSPEDITSIOONI MÄLESTUSED” ning muid muutujaid ei defineeri.

Viimane liik näidiseid, mida me käsitleme, on nnlaisad (irrefutable) näidised. Laisk näidissaadakse suvalisest näidisest prefiksse tildega, st kuip on näidis, siis~p on laisk näidis.

Näidise väärtust tilde ei muuda:p väärtus võrdub~p väärtusega. Laisk näidis on aga üks või-malus suuremat näidist “musta kasti panna” ehk elementaarseks kuulutada: kuip võib omadakuitahes keerulist struktuuri, siis näidisel~p on parajasti üks elementaarosa ja see onp.

Laisk näidis sobitub iga väärtusega, sest sõltumata tema väärtusest seotakse see väärtus näidisegap, mille struktuuri ei uurita. Seepärast kasutatakse terminit “laisk” tihti ka üldisemalt igasugusenäidise jaoks, mis sobitub suvalise väärtusega, nagu näiteks muutujad ja jokker.

55

Page 56: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Vaatleme näiteks deklaratsiooni

p : q : qs= 1 : []

. (12)

Siin näidis ei sobitu, sest alamnäidisq : qs ei sobitu tühja listiga, struktuur ei klapi. Kui agateha probleeme tekitanud argument laisaks näidiseks, saame deklaratsiooni

p : ~(q : qs)= 1 : []

. (13)

Siin vasak pool sobitub parema poole väärtusega, sestq : qs on nüüd elementaarosa, mistõttutema sobitumine oma väärtusega pole vajalik. Tulemusena saabp väärtuseks1.

Laiska näidist kasutatakse reaalselt selleks, et ennetadatäitmisaegset viga. Näiteks deklaratsioo-nist (12) muutujap väärtust küsides tekib näidisesobituse viga, kuid deklaratsiooni (13) puhulseda ei teki. Deklaratsioon (13) annab vea alles siis, kui küsida muutujaq või muutujaqs väär-tust. Muutujap väärtuse erinevust deklaratsioonide (12) ja (13) korral saab kergesti kontrollidainteraktiivses keskkonnas.

Sellega oleme tutvunud põhiliste Haskelli näidistega ja anname nüüd üldise kirjelduse sellest,mis täpsemalt toimub, kui eksisteerib deklaratsioon kujul(4).

Kuni ühegi selle deklaratsiooni poolt defineeritava muutuja väärtust vaja ei lähe, seda dekla-ratsiooni ei puudutata. (Seetõttu aktsepteerib Haskell vaikides taolisi tüübikorrektseid, kuid ab-surdseid deklaratsioone naguTrue = False .) Oletame nüüd, et vajatakse muutujax väärtust,kusx esineb kujul (4) oleva deklaratsiooni vasakus poolesp. Sellisel juhul sobitatakse näidistp

avaldisee vastu. See tähendab, et avaldise väärtustatakse mingile kujulee, millest onp sobitu-mine või mittesobitumine tema väärtusega kujude võrdlusesotse näha. Mittesobitumise korrallõpetatakse töö täitmisaegse veaga. Sobitumise korral seotakse näidisep kõik elementaarosadavaldisee vastavate alamavaldistega (need võivad veel olla väärtustamata). Kuix oli üks elemen-taarosadest, siis on leitud avaldis, millest võib edasi hakata tema väärtust arvutama. Kuix polnudelementaarosa, siis otsitakse välja elementaarosap′, kusx esineb; olgue′ temaga eelmisel etapilseotud avaldis. Edasi toimitakse analoogiliselt sellega,nagu oleks kirjutatud deklaratsioon kujul(4), kus vasak ja parem pool on vastavaltp′ ja e′.

Illustreerime seda protsessi deklaratsiooni (6) peal. Oletame, et on vaja muutujax väärtust. Kunaparema poole avaldis on paarikujul, on vasaku poole näidisesobitumine tema väärtusega ilmamidagi tegemata selge. Muutujax seotakse avaldisega5 ja muutujay avaldisega0.2 . Edasi võibdeklaratsiooni (6) kõrvale panna, sestx väärtuse arvutamiseks sobiv avaldis on käes. Protsessikõrvalt sai omale väärtuse ka muutujay , nii et kui edaspidi selle muutuja väärtust vaja on, siis eipea deklaratsiooni (6) enam kasutama.

Põhimõtteliselt samamoodi käivad asjad deklaratsiooni (8) puhul. Ükskõik, kas on vaja muutujap, muutujaq või muutujaqs väärtust, seotakse nad kõik vastavalt avaldistega1, 3 ja 4 : [] .

56

Page 57: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Vaatame nüüd deklaratsiooni

p : ~(q : qs)= 1 : 3 : 4 : []

.

Kui nüüd on vaja muutujap väärtust, siis seotakse sellest deklaratsioonistp avaldisega1 janäidisq : qs avaldisega3 : 4 : [] . Kunap väärtuse arvutamiseks vajalik avaldis on käes,siis pannakse deklaratsioon kohe kõrvale,q ja qs jäävad sidumata. Kui aga oleks olnud vajamuutujaq väärtust, siis tulnuks uurida veel näidiseq : qs sobitumist avaldise3 : 4 : []väärtusega, millestq seotuks avaldisega3.

Ülesandeid

107. Kirjutada deklaratsioon (12) oma Haskell-faili ja tekitada selle kaudu interaktiivses kesk-konnas näidisesobituse viga.

108. Olgu meil deklaratsioonid

p : q : qs= [1]

,

p : ~(q : qs)= [1 : 3 : [] ]

,

[p : ps ]= "Has" : "kell " : []

.

Iga deklaratsiooni kohta otsustada, (1) kas ta on tüübikorrektne, (2) kui jah, siis mis saabmuutujap väärtuseks.

109. Demonstreerida arvutis, et muutuja, jokkeri ja laisa näidisega sobitub ka bottom.

110. Olgu meil deklaratsioon

~(x , (a , b))= (1 , error "VIGA")

.

Selgitada, miks muutujax väärtustamine lõpeb veateatega. Tõsta antud deklaratsioonis 1sümbol ümber nii, etx väärtuseks saaks1.

Muutujate definitsioonidele võib lisadatüübisignatuure (ingl type signature) — deklarat-sioone, mis näitavad defineeritava muutuja tüüpi.

Süntaktiliselt on tüübisignatuur kujul

x1, . . ., xl :: t,

57

Page 58: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kusx1, . . . , xl on muutujad jat tüübiavaldis. Tulemusena loetakse muutujatex1, . . . , xl tüübikst.

Erinevalt annoteeritud avaldisest kujutab tüübisignatuur endast omaette deklaratsiooni. Signatuurmuutujat ei defineeri, tüübisignatuuri vasakus pooles olevad muutujad peavad olema defineeritudteiste deklaratsioonidega. Muutuja tüübisignatuuri paremas pooles olev tüüp peab olema võrdnetüübiga, mille see muutuja saaks oma definitsioonist, või seda tüüpi täpsustama, vastasel korraltekib tüübiviga.

Näiteks deklaratsioonile (5), mis defineerib muutujabase väärtuseks10, võib lisada tüübisig-natuuri

base:: (Num a) => a

. (14)

Kui tahame kitsendada muutujabase tüübiksInteger , võib signatuuri (14) asemel kirjutada

base:: Integer

.

Enamasti ongi tüübisignatuuri vaja just tüübi kitsendamiseks, kuna süsteem suudab üldjuhuldefinitsiooni järgi muutujate tüübid tuletada. Mõnikord siiski vajab süsteem tüübituletamisel abi,mille saab anda tüübisignatuuri lisamisega. Tüübisignatuuride lisamine on mõttekas igal juhul,kuna see hõlbustab koodist arusaamist.

Tüübiteema juurde märgime, et kuigi seni oleme kasutanud vaid andmenäidiseid, on näidistestmõtet rääkida ka tüüpide tasemel. Ka globaalseid tüübimuutujaid saab defineerida deklaratsioo-niga kujul (4), kui ette lisada võtmesõnatype.

Näiteks võime nii defineerida tüübimuutujaRida , mille väärtuseks on täisarvude listide tüüp.Deklaratsiooniks tuleb

type Rida= [Integer]

.

Pärast seda on võimalik kirjutada annoteeritud avaldisi nagu[1 .. ] :: Rida jms.

Siiski on tüübimuutujate defineerimisel tugevad kitsendused võrreldes andmemuutujatega. Tüü-binäidis tohib sellises deklaratsioonis olla ainult maksimaalselt lihtne ehk üksik muutuja ningtüübiavaldis peab olema kontekstita.

Ülesandeid

111. Lisada ülesande 97 lahendusele tüübisignatuur, teha seda mitmel viisil, varieerides tüübi-pere suurust.

112. Otsida Hugsi teegist üles tüübimuutujaString definitsioon ja saada sellest aru.

58

Page 59: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Lambdaavaldised

Teine tüüpiline koht, kus näidised figureerivad, on funktsioonide definitsioonid, seal täidavadnäidised formaalsete parameetrite aset.

Vaatame kõige lihtsamat sellistest süntaktilistest konstruktsioonidest —lambdaavaldist (ingllambda-expression), mis määravad funktsioonitüüpi väärtuse ilma talle nime andmata.

Lambdaavaldis on kujul

\ p -> e,

kusp on näidis jae avaldis. Noole moodi kombinatsioon-> nagu varem võrdusmärkki jagabkirjutise vasakuks ja paremaks pooleks. Sümbolit\ loetakse “lambda”. Kogu selle avaldiseväärtuseks on funktsioon, mis igal oma argumendilx annab tulemuseks avaldisee väärtuse eel-dusel, et näidisep väärtus onx ning iga muutuja, mis esineb nii näidisesp kui ka avaldisese, onneis sama väärtusega. Kui näidis argumendi väärtusega ei sobitu, siis on funktsiooni väärtus selargumendil⊥.

Näiteks avaldis\ x -> x * x väärtuseks on funktsioon, mis igal oma argumendilx annabväärtuseks avaldisex * x väärtuse eeldusel, etx väärtus onx, ehk väärtusex2. Öeldes lü-hidalt, \ x -> x * x väärtus on ruutfunktsioon. Seega on korrektne näiteks rakendamine( \ x -> x * x) (1 + 2) , selle avaldise väärtus on9.

Avaldise\ (x , _) -> x väärtuseks on funktsioon, mis igal oma argumendil(a , b) annabväärtuseks muutujax väärtuse eeldusel, et näidise(x , _) väärtus on(a , b), niisiis a. Öeldeslühidalt,\ (x , _) -> x väärtus on paari projektsioon esimesele komponendile, sama miseeldefineeritud muutujalfst . Jokkeri asemel võiks olla ka mõnix -st erinev muutuja, aga kunaseda muutujat kuskil ei kasutata, on jokker sobivam.

Lambdaavaldise vasakus pooles esinevad muutujad on lokaalsed, nähtavad ainult selle lamb-daavaldise piires. Näiteksx näiteavaldises\ x -> x * x on lokaalne muutuja, tema nähta-vus piirdub ainult selle lambdaavaldisega. Põhimõtteliselt tohivad lokaalsete muutujate nimedlangeda kokku globaalsete muutujate nimedega, sellisel puhul varjutatakse vastavad globaal-sed muutujad lokaalse muutuja nähtavuspiirkonnas, nad pole seal kasutatavad. Näiteks avaldi-se\ sin -> sin * sin väärtus on seesama ruutfunktsioon, kuigisin on eeldefineeritudmuutuja.

Pangem veel tähele, et lambdaavaldiste puhul on väärtuste liikumise suund vastupidine varem-vaadeldud deklaratsioonidega võrreldes. Kui deklaratsioonis kujul (4) liigub väärtus avaldisestnäidisesse, siis lambdaavaldises liiguvad muutujate väärtused näidisest avaldisse, näidisesse lii-gub aga argumendi väärtus.

Lambdaavaldist saab väärtustada ainult koos argumendiga.Avaldise( \ p -> e) a väärtusta-misel väärtustatakse kõigepealt argumentia parajasti niipalju, et ilmneks näidisep sobituminevõi mittesobitumine tema väärtusega. Sobitumise korral seotakse näidise elementaarosad avaldi-

59

Page 60: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

se vastavate osadega, kogu avaldis aga kirjutatakse ümber lihtsalt avaldisekse. Mittesobitumisekorral antakse täitmisaegne viga.

Vaatleme näitena avaldise( \ x -> x * x) (1 + 2) väärtustamist. Kuna näidisx onmuutuja, siis ta sobitub suvalise väärtusega, nii et argumenti 1 + 2 sellel etapil ei väätustata.Näidisesobitus seob muutujax avaldisega1 + 2 . Algne avaldis aga kirjutatakse ümber avaldi-seksx * x . Nüüd on juba näha, et edasine väärtustamine annab tulemuseks 9, aga väärtusta-mise selles osas ei mängi lambdaavaldis enam mingit rolli.

Keerulisema näitena vaatleme avaldise

( \ ~(x : xs) -> const 5 x) [] (15)

väärtustamist. Kuna näidis~(x : xs) on laisk, siis ta sobitub tühja listiga. Näidisx : xsseotakse avaldisega[] . Avaldis (15) kirjutatakse ümber avaldiseksconst 5 x . Nüüd kunaconst 5 on laisk, siis muutujax väärtust vaja ei ole ja väärtustamise lõpptulemuseks on5.

Ülesandeid

113. Testida ruutfunktsiooni interaktiivse interpretaatori käsurealt.

114. Kuidas testida, et\ (x , _) -> x ja fst on sama väärtusega?

115. Kirjutada lambdaavaldis, mille väärtus on funktsioon, mis võtab argumendiks arvupaari jaannab väärtuseks komponentide vahe absoluutväärtuse.

116. Kirjutada lambdaavaldised, mille väärtused on vastavalt võrdsed eeldefineeritud muutuja-te head ja tail väärtustega, seejuures neid muutujaid endid kasutamata. Kus võimalik,kasutada jokkerit.

117. Kirjutada lambdaavaldis, mille väärtuseks on funktsioon, mis võtab argumendiks listi ningannab välja tema teise elemendi, kui see leidub, vastasel korral lõpetab täitmisaegse veaga.Kasutada võimalikult vähe muutujaid.

118. Kirjutada võimalikult lühike avaldis, mille väärtus on funktsioon, mis võtab argumendikspaari ja annab välja paari, mille esimene komponent on argumentpaar ja teine komponenton argumentpaar vahetatud komponentidega.

119. Kirjutada avaldis, mille väärtuseks on funktsioon, mis võtab argumendiks arvu ja annabtulemuseks lõpmatu listi, mille elementideks on kõik sellearvu naturaalarvkordsed kordajakasvamise järjestuses.

120. Kirjeldada detailselt avaldise( \ (x , _) -> x) (1 + 1 , 2 - 5) väärtustus-protsessi.

60

Page 61: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

121. Asendada avaldises (15) muutujaconst sellise muutujaga, et tulemuseks oleva avaldiseväärtustamine lõpetab täitmisaegse veaga.

122. Kui avaldises (15) kaotada tilde, mis on tulemuseks oleva avaldise väärtus?

Lambdaavaldisega on võimalik üles kirjutada kacurried-kujul funktsioone. Selleks peab vaidparemas pooles olema funktsioonitüüpi avaldis. See võib olla omakorda lambdaavaldis.

Näitekscurried-kujul funktsiooni, mis leiab kahe arvu aritmeetilise keskmise, määrab avaldis

\ a -> \ b -> (a + b) / 2 .

Analoogne funktsioon kolme argumendi korral oleks

\ a -> \ b -> \ c -> (a + b + c) / 3 .

Curried-kujul funktsioonide jaoks on olemas ka kirjutamist lihtsustav erisüntaks, mis võimaldabkorduvad lambdad ja nooled kaotada, jättes alles ainult esimese lambda ja viimase noole ning kir-jutades kõik argumendid lihtsalt üksteise järele neid vajadusel tühisümbolitega eraldades. Kaheja kolme arvu aritmeetilist keskmist arvutavad funktsioonid oleks selle süntaksiga vastavalt

\ a b -> (a + b) / 2 ,

\ a b c -> (a + b + c) / 3 .

Selle erisüntaksiga lisandub nõue, et sama lambda alla kogutud näidiste reas ei tohi ükski muutujaesineda korduvalt — muidu kehtib see nõue vaid igas näidiseseraldi.

Ülesandeid

123. Kirjutada lambdaavaldis, mille väärtuseks on funktsioonisnd curried-kuju.

124. Kirjutada ja testida interaktiivse interpretaatori käsurealt ülesandes 115 kirjutatud funkt-sioonicurried-kuju.

125. Kirjutada lambdaavaldis, mille väärtuseks oncurried-kujul funktsioon, mis võtab argu-mendiks kaks listi; kui need mõlemad on kaheelemendilised,siis annab välja sellise2 × 2maatriksi determinandi, mille ridades on parajasti argumentlistide elemendid, vastasel kor-ral lõpetab täitmisaegse veaga.

Pannes deklaratsiooni (4) paremaks pooleks lambdaavaldise, saame defineerida muutuja väärtu-seks endakoostatud funktsioone. Näiteks võime defineeridaruutfunktsiooni muutujasqr väär-tuseks deklaratsiooniga

sqr= \ x -> x * x

, (16)

61

Page 62: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kahe ja kolme arvu aritmeetilise keskmise operatsioonid vastavalt muutujateam2 ja am3väärtu-seks aga deklaratsioonidega

am2= \ a b -> (a + b) / 2

, (17)

am3= \ a b c -> (a + b + c) / 3

. (18)

Deklaratsioonide (17) ja (18) olemasolul on muutujadam2 ja am3 oma uues tähenduses kasu-tatavad nii prefiks- kui infikskujul. Näiteks avaldisteam2 3 5 ja 3 ‘am2‘ 5 väärtus on4,avaldisteam3 1 6 8 ja (1 ‘am3‘ 6) 8 väärtus aga5.

Kui on ette näha, et uut muutujat hakatakse oma argumentidele rakendama peamiselt infiksselt,on mõistlik muutujanimi koostada sümbolitest reas (2). Näiteks deklaratsioon

(// )= \ a x -> fromIntegral a + 1 / x

(19)

annab muutujale// väärtusekscurried-kujulise funktsiooni, mis võtab argumendiks täisarvu

a ja murdarvux ja annab tulemuseks arvua +1

x. Muutuja fromIntegral väärtuseks on

tüübiteisendus, mis viib täisarvu üle suvalisse vajalikkuarvutüüpi (tema matemaatilist väärtustmuutmata); deklaratsioonis (19) onfromIntegral vajalik selleks, et+ argumendid oleksidüht tüüpi.

Värskelt kasutusele võetud nime infikskuju prioriteet ja assotsiatiivsus võrduvad vaikeväärtus-tega, kui neid pole ümber defineeritud. Viimast saab tehainfiksdeklaratsiooniga. Infiks-deklaratsiooniks kõlbab selline rida, mille esitavad interaktiivsed keskkonnad infiksoperaatoriteprioriteedi-assotsiatiivsuse kohta kasutajale, kuid sama infiksdeklaratsiooniga saab sama priori-teeti ja assotsiatiivsust seada mitme nime jaoks.

Infiksdeklaratsiooni üldkujud on

infixa p ⊕1 , . . ., ⊕l ,infixa ⊕1 , . . ., ⊕l ,

kusa on l , r või tühjus, mis tähendavad vastavalt vasak-, parem- ja mitteassotsiatiivsust,p onnumber0-st 9-ni, mis tähendab prioriteeti, ja⊕1, . . . ,⊕l on infiksoperaatorid, kusjuuresl > 0.Sellise deklaratsiooni tulemusena muutub infiksoperaatorite ⊕1, . . . ,⊕l assotsiatiivsus vastavaltsellele, kuidas nad on deklaratsioonis märgitud. Kui prioriteet puudub, jääb jõusse vaikepriori-teet.

Näiteks meie aritmeetilist keskmist arvutavate infiksoperaatorite assotsiatiivsuse ja prioriteedimuudab sarnaseks korrutus- ja jagamismärgiga deklaratsioon

infixl 7 ‘am2‘, ‘am3‘.

Infiksdeklaratsioon nime mujal infiksselt kasutama ei kohusta.

62

Page 63: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

126. Lisada definitsioonile (19) infiksdeklaratsioon, mis annab infiksoperaatorile// jagamisegavõrdse prioriteedi ja mõistliku assotsiatiivsuse.

Hargnemiskonstruktsioonid

Tutvume järgnevalt nelja süntaktilise konstruktsiooniga, mis lubavad erinevate juhtude läbivaa-tust. Kaks esimest neist moodustavad avaldise. Hoolimata sellest vaatleme neid kõiki uute muu-tujate definitsioonide koosseisus, sest ühelt poolt on nende konstruktsioonidega moodustatudsüntaktilised objektid enamasti liiga pikad, et neid interaktiivses keskkonnas käsurealt sisestada,ja teiselt poolt on hargnemine mõttekas ainult siis, kui ta toimub mingi tundmatu väärtuse põhjal,milleks tüüpiliselt on defineeritava funktsioonitüüpi muutuja argument.

Tingimusavaldis

Lihtsaim hargnemisega konstruktsioon ontingimusavaldis kujul

if b then e1 else e2, (20)

kusb, e1 ja e2 on avaldised. Tüübikorrektsuseks on vaja, etb tüüp oleksBool ninge1 ja e2 tüübidoleksid võrdsed.

Kui b väärtus onTrue, siis avaldise (20) väärtus võrdube1 väärtusega; kuib väärtus onFalse,siis avaldise (20) väärtus võrdube2 väärtusega; kuib väärtus on⊥, siis avaldise (20) väärtus on⊥.

Näiteks deklaratsioon

gm2= \ x y

-> if x > 0 && y > 0then sqrt (x * y)else error "gm2: argumendid olgu positiivsed "

(21)

defineerib muutujagm2 väärtusekscurried-kujulise funktsiooni, mis võtab kaks arvulist argu-menti: kui mõlemad on positiivsed, annab välja nende geomeetrilise keskmise, vastasel korralkatkeb arvutus veateatega. Siin muutujasqrt tuleb moodulistPrelude ja tema väärtus on ruut-juure leidmise funktsioon.

Deklaratsioon

tõeväärtusArvuks= \ x -> if x then 1 else 0

(22)

63

Page 64: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

defineerib muutujatõeväärtusArvuks väärtuseks funktsiooni, mis teisendab tõeväärtusi ar-vulisele kujule.

Väärtustamaks avaldist (20), väärtustatakse kõigepealt avaldisb, niikaua kui tuleb väljaTruevõi False . Vastavalt sellele jätkatakse kase1 või e2 väärtustamisega.

Tingimusavaldis on lihtne ja sarnaneb teiste keelteif-konstruktsiooniga. See sarnasus on siiskimõnevõrra petlik. Kõigepealt tuleb tähele panna, et Haskellis peab tingimusavaldisel alati olemanii then- kui ka else-haru. Teiseks pangem tähele, et tegemist on tõepoolest avaldisega, mittemingi deklaratsiooni ega käsuga. C-s ja Javas on Haskelli tingimusavaldise analoogiks pigemkonstruktsioon<tingimus> ? <avaldis> : <avaldis>kui if-lause. Kuna Haskellis saab avaldi-se väärtuseks olla protseduur, saab Haskelli tingimusavaldis muidugi tähistada ka imperatiivsetif-lauset.

Kuna tegemist on avaldisega, on konstruktsiooni (20) kasutamine niisama vaba kui avaldise ka-sutamine üldse. Näiteks võib ta asetada teise avaldise sisse. Defineerimaks muutujavaliKääneväärtuseks funktsiooni, mis võtab argumentideks arvu ja kaks stringi ning annab välja esimesestringi, kui arv võrdub1-ga, ja teise stringi ülejäänud juhtudel, kõlbab kood

valiKääne= \ n s1 sm -> show n ++ ’ ’ : if n == 1 then s1 else sm

.

Nüüd näiteks avaldisevaliKääne 1 "ärtu " "ärtut " väärtus on “1 ärtu”, avaldisevaliKääne 7 "ärtu " "ärtut " väärtus aga “7 ärtut”.

Valikuavaldis

Valikuavaldise puhul tehakse valik harude vahel, sobitades erinevaid näidiseid ühe kindla väär-tusega. Valikuavaldis koosneb päisest kujulcase e of, kuse on avaldis, mille vastu näidiseidsobitatakse, ja juhtude loendist. Iga juht sisaldab näidise koos vastava parema poolega, mis tulebvalida selle näidise sobitumise korral.

Lihtsaimal juhul koosneb parem pool noolemärgist-> ja ühest avaldisest. Siis näeb valikuavaldisvälja kujul

case a ofp1 -> e1

. . . . . . . . .pl -> el

, (23)

kus a ja e1, . . . , el on avaldised,p1, . . . , pl on näidised ningl > 0. Igas näidisespi esinevadmuutujad on lokaalsedi-nda juhu piires. Tüübikorrektsuseks on vaja, etp1, . . . , pl oleksid kõiksama tüüpi nagua ja e1, . . . , el oleksid omavahel sama tüüpi.

Kui a väärtus on normaalne (st pole⊥) ja i on vähim selline arv, mille korralpi sobituba väärtu-sega, siis avaldise (23) väärtuseks onei väärtus eeldusel, et näidisepi väärtus võrdub avaldisea

64

Page 65: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

väärtusega ja iga muutuja, mis esineb nii näidisespi kui ka avaldisesei, on neis sama väärtusega.Samamoodi on asi, kuia väärtus on⊥ ja näidisp1 sobitub sellega. Kui ükski näidistestp1, . . . , pl

ei sobitu avaldisea väärtusega, võia väärtus on⊥ ja p1 ei sobitu⊥-ga, siis on avaldise (23)väärtus⊥.

Näiteks deklaratsioon

üksLõppu= \ xs

-> case xs ofz : zs

-> zs ++ [z]_

-> []

(24)

defineeribüksLõppu väärtuseks funktsiooni, mis võtab argumendiks listi: kui see on mittetühi,siis annab väärtuseks listi, mis on argumendist saadud esimese elemendi ümbertõstmisel listilõppu; tühjal listil aga annab tühja listi. Tõepoolest, kuixs väärtus on mittetühi list, siis sobitubsellega valikuavaldise esimene näidisz : zs . Muutujaz saab väärtuseks listi pea, muutujazslisti saba ning kogu valikuavaldis on samaväärne esimese juhu parema poolegazs ++ [z].Kui xs väärtus on tühi list, siis esimene näidis ei sobitu, kuid teine sobitub ja kogu valikuavaldison samaväärne teise juhu parema poolega[] .

Deklaratsioon

kordaEsimest= \ xs

-> case xs ofz : _

-> z : xs_

-> []

(25)

defineeribkordaEsimest väärtuseks funktsiooni, mis võtab argumendiks listi: kui see on mit-tetühi, siis annab väärtuseks listi, mis on argumentlistist saadud tema esimese elemendi veelkord-sel lisamisel listi algusse; tühja listi puhul annab välja tühja listi.

Deklaratsioon

primLoenda= \ xs

-> case length xs of0

-> "Null "1

-> "Üks"_

-> "Mitu "

(26)

65

Page 66: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

defineeribprimLoenda väärtuseks funktsiooni, mis võtab argumendiks listi: kui see on lõplik,siis annab väärtuseks eestikeelse teksti, mis ütleb, kas listis oli elemente null, üks või mitu; kuilist on lõpmatu, siis on väärtuseks⊥ (arvutus jääb lõpmatult tööle).

Valikuavaldise (23) väärtustamine käib järgnevalt. Avaldis a väärtustatakse parajasti niikaugele,et selguks näidisep1 sobitumine või mittesobitumine tema väärtusega. Kui ta sobitub, siis seo-taksep1 elementaarosad ja kirjutatakse avaldis (23) ümber avaldisekse1. Kui ta ei sobitu, siisp1 elementaarosi ei seota. Kui seejuuresa väärtus on normaalne, siis heidetakse esimene variantkõrvale ja jätkatakse järjest samamoodi ülejäänud variantidega. Kui variandid saavad otsa, siisantakse täitmisaegne viga.

Illustreerime seda skeemi deklaratsiooni (26) peal. Oletame, et on vaja väärtustada avaldis

primLoenda (take 1 "ABC") . (27)

Kõigepealt kirjutatakseprimLoenda deklaratsioonist (26) ümber deklaratsiooni parema poo-lega, milleks on lambdaavaldis. Vastavalt lambdaavaldiseväärtustusskeemile tuleb nüüd väär-tustada argumentitake 1 "ABC" seni, kuni selgub näidisexs sobitumine tema väärtusega.Kuna muutuja sobitub triviaalselt iga väärtusega, siis siin ei tehta ühtki väärtustussammu,xs seo-takse avaldisegatake 1 "ABC" ja kogu avaldis kirjutatakse ümber lambdaavaldise paremakspooleks, mis on valikuavaldis. Edasi võetakse valikuavaldise päisestlength xs ja väärtusta-takse teda seni, kuni selgub esimese näidise0 sobitumine või mittesobitumine tema väärtuse-ga. See ei selgu enne, kuilength xs on väärtustatud lõpuni. Kunaxs on seotud avaldisegatake 1 "ABC", mille väärtus on string “A”, siislength xs väärtustamine annab tulemu-seks1. Kuna0 ei sobitu väärtusega1, kuid 1 on siiski normaalne väärtus, siis proovitakse edasijärgmist varianti. Seal on näidis1, mis sobitub väärtusega1. Seetõttu kirjutatakse kogu avaldisümber stringikonstandiks"Üks". See on ka lõppväärtus.

Mõeldes väärtustamise käigu peale, saab selgeks, etprimLoenda defineerimine deklaratsioo-niga (26) on tegelikult väga ebaõnnestunud, sest valikuavaldise esimese näidise sobitamisekstuleb argumentlisti pikkus välja arvutada, see on aga lineaarse keerukusega töö listi pikkuse suh-tes. Kui list on väga pikk, siis töötabprimLoenda väga kaua, enne kui midagi mõistlikku väljaannab. Veel hullem, list võib olla lõpmatu või osaline, mispuhul primLoenda rakendamiseljääb arvutus lõpmatusse tsüklisse või lõpetab töö veateatega, samas kui kahe esimese elemendileidmise järel võiks vastuse “Mitu” välja anda.

66

Page 67: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Sama funktsionaalsuse peaks realiseerima hoopis koodiga

primLoenda= \ xs

-> case xs of[]

-> "Null "[_]

-> "Üks"_

-> "Mitu "

. (28)

Väärtustades selle definitsiooni järgi, selgub esimese näidise sobitumine või mittesobituminexsväärtusega juba siis, kuixs väärtuse kujust on välimine konstruktor käes. Niipea kui onnäiteksselgunud väärtuse jagunemine peaks ja sabaks, on teada, et esimene näidis ei sobitu. Sarnaseltpiisab teise näidise sobitumise või mittesobitumise kindlakstegemiseks listi saba välimine konst-ruktor kätte saada. Seetõttu annab see kood mõistliku tulemuse alati, kuixs väärtuseks on lõplikvõi lõpmatu list või selline osaline list, kus on vähemalt kaks elementi.

Teine asi, mida avaldise (27) deklaratsiooni (26) järgi väärtustamise käik selgitab, on põhjus,miks bottomiga sobitatakse ainult valikuavaldise esimestnäidist. Kui avaldises (23) ona väär-tus⊥ sellepärast, et tema väärtustamine lõpetab töö täitmisaegse veaga, siis oleks ju võimaliksee viga unustada ja proovida ka järgmisi näidiseid, millest mõni võib sobituda. Samas kuia

väärtus on⊥ sellepärast, et tema väärtustamine jääb lõpmatusse tsüklisse ja ei anna iialgi ühtkikonstruktorit välja, mille alusel näidise sobituvust kontrollida — nii juhtub, kuiprimLoendaargumendi väärtus on lõpmatu —, siis protsess esimesest näidisest kaugemale lihtsalt ei jõua. Etveaga lõpetamine ja lõpmatu arvutus on teooria järgi üks bottom kõik, siis ilmutatud viidatavusenõude tõttu tuleb järelikult ka täitmisaegse vea puhul teiste näidiste kontrollimisest loobuda.

Kõik tingimusavaldised on võimalik asendada valikuavaldisega: kehtib samaväärsus

if b

then e1

else e2

case b ofTrue-> e1

_-> e2

.

Mõlemal juhul on väärtusekse1 väärtus, kuib väärtus onTrue, ja e2 väärtus, kuib väärtus onFalse; kui b väärtus on⊥, siis on mõlema avaldise väärtus⊥.

67

Page 68: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Näiteks võiks definitsiooni (21) asendada deklaratsiooniga

gm2= \ x y

-> case x > 0 && y > 0 ofTrue-> sqrt (x * y)

_-> error "gm2: argumendid olgu positiivsed "

(29)

Mitme tingimuse järgi hargnemist, mida vaid tingimusavaldistega kodeerides tuleks tingimus-avaldisi üksteise sisse panna, saab valikuavaldisega mõnikord ühe hargnemisega teha.

Olgu meil näiteks tarvis muutujaabsSuurem väärtuseks defineeridacurried-kujuline funkt-sioon, mis võtab argumentideks kaks täisarvu: kui nad on absoluutväärtuselt erinevad, siis annabtulemuseks absoluutväärtuselt suurema, vastasel korral peab arvutus lõpetama töö veateatega.

Siin on vaja eristada kolm juhtu, mis tulenevad argumentideabsoluutväärtuste võrdlemisest.Kolmeks hargnemine võrdlustulemuse põhjal on tüüpiline olukord, kus kasutatakse valikuaval-dist ja tüüpiOrdering. MoodulistPrelude saab muutujacompare , mille väärtus oncurried-kujuline funktsioon, mis võtab argumentideks kaks objektisamast järjestusega tüübist ja annabtulemuseks nende võrdlustulemuse tüüpiOrdering kuuluva väärtusena. Neid väärtusi esitavadkonstruktoridLT, EQ, GT. Nii saame nõutud muutuja defineerida deklaratsiooniga

absSuurem= \ x y

-> case compare (abs x) (abs y) ofGT-> x

LT-> y

_-> error "absSuurem: absoluutväärtused võrdsed "

.

Kasutades võrdlemist muutujagacompare , saavutame mitte ainult ühe hargnemisega koodi,vaid ka ühe võrdlusega arvutuse, mis on oluline efektiivsuse kaalutlustel.

Huvitaval kombel on ka argumendile rakendatud lambdaavaldis asendatav valikuavaldisega, kusvalida on vaid ühe variandi vahel, ehk kehtib samaväärsus

( \ p -> e) a ≡ case a ofp -> e

.

Mõlemal juhul on väärtuseks avaldisee väärtus eeldusel, et näidisep väärtus võrduba väärtuseganing näidisesp esinevad muutujad onp ja e piires sama väärtusega.

68

Page 69: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

127. Defineerida muutujaarvTõeväärtuseks väärtuseks funktsioon, mis võtab argumen-diks täisarvun ja annab tulemuseks tõeväärtuseTrue, kui n = 1, ja tõeväärtuseFalse, kuin = 0. Kui n /∈ {0, 1}, siis peab rakendamine lõppema eestikeelse veateatega.

128. Defineerida muutujaesipaar väärtuseks funktsioon, mis võtab argumendiks listi ning,kui selles on vähemalt2 elementi, annab väärtuseks paari kahest esimesest elemendist,vastasel korral lõpetab sobiva eestikeelse veateatega. Arvutus peab käima konstantse kee-rukusega listi pikkuse suhtes.

129. Defineerida muutujaesiAbs väärtuseks funktsioon, mis võtab argumendiks arvude listi:mittetühja listi korral annab väärtuseks listi, mille saabargumentlistist esimese elemendiasendamisel tema absoluutväärtusega; muul juhul tühja listi.

130. Kasutades tüüpiOrdering, defineerida muutujavõrdle väärtusekscurried-kujul funkt-sioon, mis võtab ükshaaval argumendiks täisarvudx, y ja annab väärtuseks stringi “Võrd-sed”, “Järjestikused” või “Kauged” vastavalt sellele, kasx = y, x ja y on järjestikusedtäisarvud (ükskõik kummas järjekorras) või ei kehti kumbkineist tingimustest.

131. Kirjeldada detailselt avaldiseüksLõppu "ABC" väärtustusprotsessi, kusüksLõppu ondefineeritud deklaratsiooniga (24).

132. Kirjeldada detailselt avaldisekordaEsimest [3, 5, 15] väärtustusprotsessi, kuskordaEsimest on defineeritud deklaratsiooniga (25).

133. Kirjutada avaldis (22) samaväärselt ümber valikuavaldisena.

134. Kirjutada avaldis (15) samaväärselt ümber valikuavaldisena.

Deklaratsioonisüsteemid

Funktsioonide defineerimiseks muutujate väärtuseks võib kasutada deklaratsioone, mis erinevadsenivaadelduist selle poolest, et vasak pool koosneb mitmest näidisest. Selline nnfunktsionaal-

ne vasak pool on kujul

f p1 . . . pl, (30)

kusf on muutuja ningp1, . . . , pl on näidised, kusjuuresl > 0.

Deklaratsioon, mille vasak pool on kujul (30), defineerib muutujatf, tema väärtuseks saab teatavfunktsioon. Näidisedp1, . . . , pl märgivad funktsiooni argumente. Kuid see funktsioon ei pruugiolla määratud ainuüksi selle deklaratsiooniga; võib leiduda veel deklaratsioone, mille vasak poolon samal kujul ja esimene näidis onf. Sellised deklaratsioonid moodustavad kokkudeklarat-

sioonisüsteemi ja defineeritava muutujaf väärtuse määrab süsteem koos.

69

Page 70: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Süsteemi moodustavad deklaratsioonid peavad asetsema koodis järjest, nende vasakud pooledpeavad sisaldama ühepalju näidiseid ning ükski muutuja ei tohi esineda sama deklaratsiooni ar-gumendinäidiste reas korduvalt. Tüübikorrektsuseks peavad erinevate deklaratsioonide vastavadnäidised olema sama tüüpi ja kõik paremad pooled määrama ühesugust tüüpi väärtuse. Paremadpooled võivad olla sellised, nagu seni vaadeldud; siis on süsteem kujul

f p1,1 . . . p1,l

= e1

. . . . . . . . . . . . . .f pk,1 . . . pk,l

= ek

. (31)

Deklaratsioonisüsteemi sisuks on juhtude läbivaatus, igadeklaratsioon märgib üht juhtu. Muutu-ja f väärtuseks saab funktsioon, mis võtab ükshaaval argumendiks objektidx1, . . . , xl. Kui i onvähim arv, mille korrali-nda deklaratsiooni kõik näidised sobituvad nende argumentidega vasta-valt ja ükski argument pole⊥, siis annab funktsioon tulemuseksei väärtuse eeldusel, et näidistespi,1, . . . , pi,l esinevad muutujad on seal ja avaldisesei sama väärtusega. Samamoodi on asi, kuimõni argument on⊥, kuid esimesei deklaratsiooni selle argumendi näidised sobituvad temaga.Ülejäänud juhtudel on funktsiooni väärtuseks neil argumentidel⊥.

Kui deklaratsioonide arv süsteemis on1 ehk süsteem koosneb ühest deklaratsioonist

f p1 . . . pl

= e, (32)

siis on see deklaratsioon samaväärselt ümber kirjutatav deklaratsiooniga

f = \ p1 . . . pl -> e,

kus vasak pool pole funktsionaalne.

Funktsionaalse vasaku poolega deklaratsioonidega saame pisut lühemalt ja loetavamalt ümberkirjutada kõik senised deklaratsioonid, mis defineerivad muutujate väärtuseks funktsioone.

Näiteks ruutfunktsioon sai muutujasqr väärtuseks defineeritud deklaratsiooniga (16); uues sün-taksis teeb sama deklaratsioon

sqr x= x * x

. (33)

Deklaratsioonid (17) ja (18) on samaväärselt ümber kirjutatavad vastavalt definitsioonideks

am2 x y= (x + y) / 2

,

am3 x y z= (x + y + z) / 3

.

70

Page 71: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Curried-kujul funktsioonide defineerimisel võib defineeritav muutuja olla ka infiksne. Niisugusedeklaratsiooni funktsionaalne vasak pool on kujul

(p1 ⊕ p2) p3 . . . pl,

kus⊕ on defineeritava muutuja infikskuju jal > 2. Seega muutujadam2, am3saame varasemagasamaväärselt defineerida ka vastavalt deklaratsioonidega

x ‘am2‘ y= (x + y) / 2

,

(x ‘am3‘ y) z= (x + y + z) / 3

.

Prefiksne või infiksne esinemine definitsioonis ei kohusta defineeritavat muutujat mujal sama-moodi kasutama.

Funktsionaalses vasakus pooles kehtivad kõik prioriteedid ja assotsiatiivsused nagu avaldisteski,kusjuures näidiste järjestkirjutamine on sama prioriteediga nagu avaldises funktsioonirakenda-mist tähistav järjestkirjutamine, st see on kõigist infiksoperaatoritest kõrgem.

Hargnemise võimaluste nägemiseks võib deklaratsioonisüsteemiga ümber kirjutada varemantuddefinitsioone, mille paremas pooles on valikuavaldis. Näiteks deklaratsiooniga (24) defineeritudmuutujaüksLõppu defineerib samaväärselt süsteem

üksLõppu (z : zs)= zs ++ [z]

üksLõppu _= []

. (34)

Nii otsene ümberkirjutamine on võimalik siiski ainult juhul, kui avaldis valikuavaldise päiseslangeb kokku mõne argumendinäidisega ja argumendinäidisemuutujad mujal samas tähendusesei esine, nagu see on deklatsioonides (24) ja (28).

Deklaratsioonisüsteemi eelised valikuavaldise ees ilmnevad juhul, kui hargnemisotsuseid tehestuleb arvestada korraga mitme argumendi väärtusi.

Näiteks süsteem

risti (x : _) ( _ : ys)= x : ys

risti _ _= []

(35)

defineerib muutujaristi väärtusekscurried-kujul funktsiooni, mis võtab kaks argumentlisti:kui mõlemad on mittetühjad, siis annab välja esimese listi peast ja teise listi sabast koostatud

71

Page 72: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

listi; vastasel korral annab välja tühja listi. Esimene deklaratsioon läheb käiku parajasti juhul,kui mõlemad argumendid on mittetühjad listid, sest parajasti siis sobituvad mõlemad argumendi-näidised. Ülejäänud juhtudel läheb käiku teine deklaratsioon, sest seal argumentidele tingimusiei seata.

Et valikuavaldisega sama saavutada, tuleks argumendid üheks järjendiks kokku võtta, sest vali-kuavaldise päisesse saab kirjutada ainult ühe avaldise. See aga viib liigsete andmestruktuuridemoodustamiseni ka koodi täitmise ajal ning kood jookseks mõnevõrra aeglasemalt.

Ülesandeid

135. Otsida Hugsi teegist üles muutujatefst , snd , head , tail , const definitsioonid ja saa-da neist aru.

136. Defineerida uuel viisil muutujaerinevus väärtuseks ülesandes 115 kirjeldatud funkt-sioon.

137. Võimalikult lühikese ja võimalikult vähe muutujaid kasutava deklaratsiooniga kujul (32)defineerida muutujaimelik väärtuseks ülesandes 118 kirjeldatud funktsioon.

138. Defineerida muutujatakeLõpust väärtuseks funktsioon, mis töötab nagutake , agavõtab elemente listi lõpust. NäitekstakeLõpust 2 [3, 2, 4] väärtustamine peabandma[2, 4].

139. Defineerida muutujacurrErinevus väärtuseks ülesandes 115 kirjeldatud funktsioonicurried-kuju, kasutades defineeritavat muutujat prefiksselt.

140. Modifitseerida ülesandes 139 kirjutatud definitsiooninii, et defineeritav muutuja oleks ka-sutatud infiksselt.

141. Muuta ülesandes 139 defineeritud muutuja infikskuju prioriteet ja assotsiatiivsus sarnaseksplussi ja miinusega.

142. Lahendada ülesanded 127, 128, 129 deklaratsioonisüsteemiga.

143. Kirjutada muutujaprimLoenda definitsioon (28) samaväärselt ümber deklaratsioonisüs-teemiga.

144. Kirjutada muutujakordaEsimest definitsioon (25) samaväärselt ümber mitme deklarat-siooni tehnikaga, kasutades samu muutujaid samas tähenduses.

145. Defineerida muutujapead väärtusekscurried-kujul funktsioon, mis võtab argumentidekskaks listi ja annab väärtuseks listi, mille elementideks onilma midagi kordamata kõik needobjektid, mis esinevad esimese või teise listi esimese elemendina.

72

Page 73: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

146. Kirjutada oma Haskell-faili muutujaristi definitsioon (35) ja veenduda, et see töötab.Seejärel asendada teine deklaratsioon deklaratsiooniga

risti= \ _ _ -> []

.

Tutvuda veateatega, mis tekib.

147. Kas saame süsteemiga (35) samaväärse definitsiooni, kui viime korraga mõlema deklarat-siooni argumendid lambdaavaldisega paremasse poolde (niinagu ülesandes 146 tehti teisedeklaratsiooniga)?

Kuna tüüpidel on võimalikud ainult muutujanäidised (Haskelli laiendused aktsepteerivad ka jok-kerit), ei ole tüübimuutujate definitsioonides hargneminevõimalik. Sellegipoolest on võimalikkasutada funktsionaalset vasakut poolt.

Näiteks deklaratsiooniga

type Maatriks a= [[a]]

võime defineerida muutujaMaatriks väärtuseks tüübifunktsiooni, mis igal argumendilA an-nab väärtuseksA-tüüpi väärtustega listide listide tüübi. (Mõtteks võib olla käsitleda listide ele-mentliste maatriksi ridadena.)

Valvurikonstruktsioon

Deklaratsiooni või valikuavaldise juhu paremaks pooleks võib lisaks taolistele, mida seni ole-me kasutanud, ollavalvurikonstruktsioon. Selle konstruktsiooni mõte on esitada ülemineksõltuvalt lisatingimustest.

Deklaratsiooni parema poolena näeb valvurikonstruktsioon välja kujul

| g1 = e1

. . . . . . . . . . .| gl = el

.

Vasakul pool olevaid objektegi nimetataksevalvuriteks (ingl guard). Iga valvur on tõeväär-tusetüüpi avaldis ehk tingimus. Vastavas paremas pooles onavaldis, mis tuleb valida juhul, kuisee valvur on esimene, mis väärtustub tõeseks, ja varasemate väärtused on normaalsed. Mõistagipeavad kõik paremad pooled olema ühesugust tüüpi.

Valikuavaldise juhu parema poolena näeb valvurikonstruktsioon välja samamoodi, ainult võrdus-märkide asemel on noolemärgid->.

73

Page 74: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Näitena valvuritega deklaratsioonist defineerime muutujaarvuKlass väärtuseks funktsiooni,mis arvulisel argumendil annab väärtuseks teksti “Negatiivne”, “Nullilähedane” ja “Suur”, kuiargument on vastavalt negatiivne, mittenegatiivne1-st väiksem, või1 või suurem. Deklaratsioo-niks sobib

arvuKlass x| x < 0

= "Negatiivne "| x >= 0 && x < 1

= "Nullilähedane "| x >= 1

= "Suur "

. (36)

Iga valvurini jõutakse ainult siis, kui ükski eelnev samas komplektis pole sobinud. Seega dek-laratsiooni (36) teine valvurx >= 0 && x < 1 tuleb kontrollimisele ainult siis, kui esimenevalvur on juba väärtustatud ja on selgunud, etx väärtus negatiivne ei ole. Järelikult poleks teisesvalvuris enam mõtet uurida tingimustx >= 0 , kuna selle väärtus on kindlasti tõene. Analoo-giliselt tuleb kolmas valvurx >= 1 kontrollimisele ainult siis, kui esimese kahe valvuri kohtaon juba selgunud, et nende väärtus on väär. See aga tähendab,et x väärtus pole ei negatiivneega1-st väiksem mittenegatiivne, ehk ta on1-st suurem või sellega võrdne. See on aga just seetingimus, mida kolmas valvur kontrollib. Järelikult poleks kolmanda valvurini jõudes vaja enammidagi kontrollida.

Neist kaalutlustest lähtuvalt saame deklaratsiooni (36) lihtsustada, võites ühtaegu koodi lühi-duses kui ka arvutuse kiiruses. Viimase valvuri asemele võiksime kirjutada mistahes avaldise,mille väärtus onTrue, näiteksTrue enda, et arvutuste hulka võimalikult vähendada; loetavu-se huvides kasutatakse sellises olukorras moodulisPrelude defineeritud muutujatotherwiseväärtusegaTrue. Kokkuvõttes saame deklaratsiooni

arvuKlass x| x < 0

= "Negatiivne "| x < 1

= "Nullilähedane "| otherwise

= "Suur "

. (37)

Selle järgi arvutades tuleb teha maksimaalselt2 võrdlust.

Valvurikonstruktsioon on eriti mugav juhul, kui tingimusion palju. MuutujaarvuKlass olekssiiski võimalik elegantselt samaväärselt defineerida ka valikuavaldise abil, kasutades võrdlemistmuutujacompare abil. Märkame, et kolm juhtu vastavad sellele, kas argumendi täisosa onnegatiivne, null või positiivne. Arvu täisosa leidmise funktsioon on väärtuseks mooduliPrelude

74

Page 75: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

muutujalfloor . Seega saame deklaratsiooni

arvuKlass x= case compare (floor x) 0 of

LT-> "Negatiivne "

EQ-> "Nullilähedane "

_-> "Suur "

.

Valvurikonstruktsioonid ei ole avaldised, nad moodustavad omaette süntaktilise kategooria. Kuivalvurikonstruktsiooni ükski valvur ei väärtustu tõeseks, siis loetakse vastav deklaratsioon võivalikuavaldise haru mittesobivaks ja valitakse järgmine,justnagu selle deklaratsiooni või haruvasaku poole mõne näidise sobitus oleks ebaõnnestunud. Niisiis, erinevalt senistest hargnemis-test, ei anna siin kõigi juhtude mittesobivus automaatselttäitmisaegset viga, vaid hargnemistepuus ühe taseme võrra tagasi pöördumise.

Vaatleme valvurikonstruktsiooni sisaldavat definitsiooni

listiKlass (z : _)| z > 0

= ":) "| z == 0

= ":| "listiKlass _= ":( "

.

Mängime läbi muutujalistiKlass rakendamise mingile listitüüpi avaldiselel. Kui l väärtuson mittetühi list, siis sobitub temaga esimese deklaratsiooni argumendinäidis jaz saab väärtu-seks listi pea. Edasi sõltub asi sellest, milline see listi pea on. Kui ta on positiivne, siis esimenevalvur väärtustub tõeseks ja rakendamise tulemus on string“:)”. Kui listi pea on null, siis esi-mene valvur väärtustub vääraks, teine aga tõeseks, mistõttu rakendamise tulemus on string “:|”.Kui listi pea on negatiivne, siis tõeseks väärtustuvat valvurit ei leidu, kogu deklaratsioon ja muu-tuja z sidumine hüljatakse ja võetakse järgmine deklaratsioon jokkeriga. Seal takistusi ei teki jarakenduse tulemus on “:(”.

Kasutades laiska näidist, saab sama funktsiooni kirjutadasiiski ka ühe deklaratsiooni ja ühel

75

Page 76: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

tasemel hargnemisega. Sobib deklaratsioon

listiKlass xs @ ~(z : _)| null xs || z < 0

= ":( "| z > 0

= ":) "| otherwise

= ":| "

. (38)

Vaatame, kuidas toimublistiKlass l väärtustamine definitsiooni (38) korral. Alguses seo-takse näidisedxs ja z : _ avaldisegal, siis minnakse valvurikonstruktsiooni juurde. Kuil

väärtus on tühi, väärtustub esimese valvuri disjunktsiooni vasak pool ja seega ka kogu valvurtõeseks ilma disjunktsiooni teist poolt uurimata ninglistiKlass l väärtuseks saadakse “:(”.Tänu laisale väärtustamisele niisugune definitsioon töötab: kui oleks nõutud ka|| parema argu-mendi väärtustamine, siis järgneks siin viga, kuna muutujat z pole võimalik siduda. Kuil väärtuson mittetühi, siis esimese valvuri disjunktsiooni vasak pool väärtustub vääraks ja seega asutakseväärtustama disjunktsiooni paremat poolt. See nõuab muutuja z väärtust, seepärast sobitataksenäidisz : _ temaga seotud avaldise vastu, milleks onl. Kuna l väärtus on mittetühi, siis seeõnnestub jaz seotakse avaldisega, mille väärtus on listi pea. Edasi on juba selge — käitutaksevastavaltz väärtusele.

Ülesandeid

148. Defineerida muutujaesiNegSgn väärtuseks funktsioon, mis võtab argumendiks arvudelisti: kui selle esimene element on negatiivne, siis annab väärtuseks listi, mille saab argu-mentlistist esimese elemendi asendamisel arvuga−1, kõigil ülejäänud juhtudel annab väljaargumentlisti enda.

149. Lahendada ülesanne 145 valvurikonstruktsiooni abil.

150. Defineerida muutujanihe väärtuseks funktsioon, mis võtab argumendiks täisarvun jasümbolic: kui c on ladina tähestiku täht, siis annab väärtuseksc-st ladina tähestikusn kohtatagapool oleva tähe; vastasel korral annab väärtuseksc. Tähestikku vaatame tsüklilisena, stz järel tuleb jälle a. Suurtähe puhul peab välja tulema suurtäht, väiketähe puhul väiketäht.

151. Kas deklaratsioonis (38) disjunktsiooni poolte vahetamisel saadakse samaväärne deklarat-sioon?

76

Page 77: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Suuremad konstruktsioonid

Selles jaotises käime läbi keerulisemad süntaktilised konstruktsioonid, mis võivad endas sisalda-da mitmeid deklaratsioone.

Lokaalsete deklaratsioonidega konstruktsioonid

Lokaalsed deklaratsioonid võimaldavad võtta muutujaid kasutusele lokaalses tähenduses.

Põhiline olukord, kus lokaalne deklaratsioon on abiks, on ühe ja sama mittetriviaalse avaldi-se korduv esinemine samas deklaratsioonis. Sama operatsiooni kordumisel koodis võidakse seeoperatsioon ka arvutuse käigus korduvalt sooritada, kui aga selle avaldise esinemised asenda-da lokaalse muutujaga, millega on see avaldis lokaalse deklaratsiooniga seotud, teostatakse seearvutus ülimalt üks kord. Isegi kui avaldise korduva väärtustamise ohtu pole, näiteks esineb tahargnemiskonstruktsiooni eri harudes, on suurema avaldise korduv esinemine klassikaline koo-dikordus ja sellisena taunitav.

Lokaalsete muutujate sissetoomine võib aidata ka lihtsaltkoodi loetavust parandada, kui on vajakirjutada mõni väga pikk ja keeruline avaldis. Siis võib selguse mõttes tähtsamad alamavaldisedvälja tuua ja lokaalsete muutujatega siduda (valides muutujatele vastavate vaheetappide tulemustsisuliselt iseloomustavad nimed).

Põhilisim konstruktsioon lokaalsete deklaratsioonidegaon let-avaldis kujul

letd1

.dl

ine

.

Tema väärtuseks on avaldisee väärtus eeldusel, et deklaratsioonidesd1, . . . , dl defineeritudmuutujate väärtused saadakse neist deklaratsioonidest. Deklaratsioonidesd1, . . . , dl defineeritudmuutujad on selles tähenduses nähtavad parajasti kogu selleslet-avaldises.

Näiteks deklaratsioonis

letNäide x= let

y = x + 0.5a = x * x + 3 * x * y + 2 * y * yb = sin x - 3 * sin x * cos y + cos x

inb / a * 100

77

Page 78: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

on muutujagay seotud korduvalt vajamineva väärtusega avaldisx + 0.5 , et vältida sama ar-vutuse ja koodi kordumist. Samuti tuuakse eraldi muutujategaa ja b välja arvutatava avaldisepikemad alamavaldised.

Ülesandeid

152. Defineerida muutujasummanali väärtuseks funktsioon, mis võtab argumendiks arvupaarija annab väärtuseks selle paari komponentide summa siinuseja summa enda suhte. Samasummat ei tohi arvutada korduvalt. Kõik vajalikud oma abimuutujad defineerida lokaalselt.

153. Defineerida muutujaõigeProperFraction väärtuseks funktsioon, mis võtab argu-mendiks reaalmurrulist tüüpi (st klassiRealFrac kuuluvat tüüpi) arvux ja annab tulemu-seks paarix täisosa ja murdosaga, kus murdosa on alati mittenegatiivne.

Kui mõnd avaldist kasutatakse korduvalt mitmes valvuris või mitmele valvurile järgnevatesavaldistes, siis polelet-avaldisega võimalik olukorda parandada, kuna valvurikonstruktsioon eiole avaldis. Selleks puhuks on Haskellis olemaswhere-konstruktsioon, mis võimaldab defi-neerida lokaalseid muutujaid, mis on nähtavad üle kogu parema poole, koosnegu ta siis ühestavaldisest või valvurikonstruktsioonist.Where-konstruktsioon kujutab endast deklaratsiooni võivalikuavaldise juhu parema poole jätku, mis kirjutatakse kogu parema poole lõppu.Where-konstruktsioon on kujul

whered1

.dl

.

Näide tüüpilisest olukorrast, kuswhere-konstruktsioon on kasulik, on deklaratsioon

veerand x| a > 0 && b > 0

= "I " ++ v| a > 0 && b < 0

= "II " ++ v| a < 0 && b < 0

= "III " ++ v| a < 0 && b > 0

= "IV " ++ v| otherwise

= "vahepeal "where

a = sin xb = cos xv = " veerandis "

.

78

Page 79: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Muutujaveerand väärtuseks saab funktsioon, mis võtab argumendiks ujukomaarvu ja, interp-reteerides seda nurga suurusena, tuvastab, millises veerandis on vastav nurk.

Ülesandeid

154. Defineerida muutujasümbolid väärtuseks funktsioon, mis võtab argumendiks täisarvu jaannab väärtuseks teksti, mis ütleb, kas sellele arvule kooditabelis vastav sümbol on suurtäht,väiketäht, number või midagi muud; kui antud arvule kooditabelis midagi ei vasta (st arvpole lõigus0-st255-ni), siis anda seda ütlev tekst. Ühtki avaldist ei tohi arvutada korduvalt.Kõik vajaminevad oma abimuutujad defineerida lokaalselt. Vajalikke kontrollfunktsiooneotsida moodulistData.Char.

155. Lahendada ülesanne 153 valvurikonstruktsiooni abil.

Mis puutub arvutuste käiku, siis lokaalseid deklaratsioone iseloomustab asjaolu, et neis definee-ritud näidiseid ei sobitata enne, kui mõnd sellises näidises esinevat muutujat vaja läheb. Sellesmõttes käitutakse lokaalsete deklaratsioonidega nagu globaalsetegagi.

Olgu p suvaline näidis ninga, b suvalised avaldised. Esmapilgul paistab, et lokaalse deklarat-siooniga avaldis

letp = a

inb

(39)

on samaväärne avaldisega

(\ p -> b) a, (40)

sest mõlema väärtus võrdubb väärtusega eeldusel, etp ja a väärtused on võrdsed. Arvutusekäigus on siiski erinevus. Avaldise (39) väärtustamisel seotakse näidisp avaldisegaa, kuid eisobitata, ja minnakse kohe avaldistb väärtustama; näidistp sobitama hakatakse alles siis, kuip mõnda muutujat tarvis on. Avaldise (40) väärtustamisel aganäidisp sobitatakse avaldiseaväärtusega enneb juurde asumist.

See erinevus võib avalduda ka avaldise väärtuses. Kui näiteksp = x : xs , a = [] ja b = 0, siisavaldise (39) väärtus on0, kuid (40) väärtus on⊥, tema väärtustamine lõpetab näidisesobituseebaõnnestumise tõttu veateatega.

Erinevus puudub juhul, kui näidisp on selline, mille sobitamine praktiliselt tähendabki ainulttema sidumist avaldisega: muutuja, jokker või laisk näidis.

79

Page 80: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Listikomprehensioon

Viimased süntaktilised konstruktsioonid avaldiste moodustamiseks, mida vaatame, kannavadkomprehensioonsüntaksi (ingl comprehension syntax) nime. Komprehensioonsüntaksigasaab elegantse kompaktsusega väljendada küllaltki keerulisi väärtusi.

Haskellis on kaks komprehensioonsüntaksit — monaadikomprehensioon ja listikomprehensioon—, kusjuures monaadikomprehensioon on üldisem. Vaatleme kõigepealtlistikomprehensioo-

ni (ingl list comprehension), mis kujutab endast avaldist kujul

[a | q1, . . ., ql]. (41)

Vaatame algul lihtsustatud olukorda, kus igaqi on nn generaator jal > 1 ning a on avaldis.Generaator (ingl generator) on süntaktiline konstruktsioon kujulp <- e, kus e on mingilistitüüpi avaldis ningp on näidis, mille muutujad on nähtavad järgmistes generaatorites ja aval-disesa, kuid mitte selles generaatoris ega eelmistes. Erinevate generaatorite paremad pooled eipea olema sama tüüpi, elemenditüübid võivad olla erinevad.Kogu avaldise (41) tüüp on listitüüp,kus elemenditüüp võrdub avaldisea tüübiga.

Avaldise (41) väärtus on list kõigist sellistest väärtustest, mille avaldisa omandab, kui generaa-toriteq1, . . . , ql näidistes defineeritud muutujate väärtused saadakse nendenäidiste sobitamisestvastavate avaldistee1, . . . , el poolt määratud listide mingite elementidega. Iga variant edukalt so-bitada kõik näidised vastava listi mingi elemendiga annab tulemuslisti ühe elemendi. Kui näidismõne elemendiga ei sobitu, siis seda elementi ignoreeritakse. Tõsi, kui seejuures element on⊥,siis jääb kogu arvutus sealkohal toppama ja tulemuslist jääb osaliseks.

Näiteks avaldise

[x * x | x <- [0 .. ]] (42)

väärtuseks on list kõigi naturaalarvude ruutudest. Tõepoolest, siin on ainult üks generaator, kusolevat näidistx sobitatakse vastava parema poolega määratud listi iga elemendiga ehk kõigi na-turaalarvudega; sobitamine alati õnnestub, kuna näidis onmuutuja, see muutujax saab iga kordväärtuseks järgmise naturaalarvu ning tulemuslisti tekibx * x väärtus ses olukorras ehk sellearvu ruut. Märkame, et näidist sobitatakse listi elementidega nende seal esinemise järjekorras.

Analoogselt on avaldise

[(x , x * x) | x <- [1 .. 9]] (43)

väärtuseks list kõigist ühekohalistest arvudest paaris oma ruuduga.

Kui on vaja funktsiooni, mis võtaks argumendiks listide listi ja leiaks neist kõigi mittetühjade lis-tide pead, siis komprehensioonsüntaks on sobiv valik, võimaldades elegantselt vältida ilmutatudhargnemisi listide tühjuse-mittetühjuse järgi. Deklaratsioon

pead xss= [x | x : _ <- xss ]

(44)

80

Page 81: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

defineerib just sellise funktsiooni muutujapead väärtuseks. Tõepoolest, näidistx : _ sobi-tatakse argumentlisti iga elemendiga; tühjad listid ei sobitu ja jäävad vahele, mittetühjad agasobituvad ja annavadx väärtuseks oma pea ning igast sellisest juhtumist tekibx väärtus ehkseesama pea ka tulemuslisti.

Näitekspead ["ABC", "", "hi ", "! ", ""] väärtus on “Ah!”.

Kui meid huvitavad mittetühjade elementide pikkused, siisvõime kirjutada deklaratsiooni

mittetühjadePikkused xss= [length xs | xs @ ( _ : _) <- xss ]

. (45)

Nüüd näiteksmittetühjadePikkused ["ABC", "", "hi ", "! ", ""] väärtus on3 : 2 : 1 : [].

Mitme generaatoriga listikomprehensiooni näitena võtameavaldise

[(x , y) | x <- [1 .. 9], y <- [1 .. 5]], (46)

mille väärtuseks on list täisarvupaaridest kujul(x , y), kus1 6 x 6 9 ja 1 6 y 6 5. Iga võimalusvõtta kummagi generaatori parema poolega kirjeldatud listist üks element annab ühe elemenditulemuslisti.

Avaldise (46) väärtuseks olevas listis on paarid järjestatud kõigepealt vasaku ja seejärel paremakomponendi järgi. See tuleneb otseselt elementide järjekorrast algsetes listides. See näide iseloo-mustab listikomprehensiooni väärtustamise põhimõtet, mille kohaselt mida hilisem generaator,seda kiiremini ta käib. Kuix on sobitatud elemendiga1, siisy sobitatakse järjest kõigi väärtus-tega1 kuni 5 (iga sobitamine annab ühe paari tulemusse), seejärel sobitataksex elemendiga2 jay jälle järjest elementidega1 kuni 5 (jälle saame iga kord ühe paari tulemusse), siis sobitatakseuuestix jne.

Kuna eespoolsete generaatorite näidistes esinevad muutujad võivad tagapoolsete generaatoriteavaldistes esineda, siis saab kirjutada näiteks avaldise

[(x , y) | x <- [1 .. 9], y <- [x .. 9]], (47)

mille väärtuseks on list kõigist ühekohaliste arvude paaridest, kus teine komponent pole esime-sest väiksem.

Ülesandeid

156. Kui deklaratsioonis (44) muuta näidisx : _ tildega laisaks, kas ja kuidas see muudabdefineeritava funktsiooni väärtust?

157. Kui deklaratsioonis (45) muuta näidisxs @ ( _ : _) tildega laisaks, kas ja kuidas seemuudab defineeritava funktsiooni väärtust?

81

Page 82: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

158. Lahendada ülesanne 28 komprehensioonsüntaksi abil.

159. Kirjutada listikomprehensioonavaldis, mille väärtuseks on stringide list, mille elemendidon kahekohalised kümnendarvud suuruse järjestuses.

160. Modifitseerida ülesande 159 lahendust nii, et kõigi kahekohaliste kümnendarvude asemelon kõik kahekohalised kuueteistkümnendarvud.

161. Kirjutada avaldis, mille väärtus on kahes suunas lõpmatu korrutustabel listide listina, st listinr n element nrm peab oleman · m, kus nummerdamine käib alates0-st.

162. Kasutades ära ülesandes 150 defineeritud muutujatnihe , defineerida muutujanihkesiffer väärtusekscurried-kujul funktsioon, mis võtab argumendiks täisar-vu n ja stringi s ning annab väärtuseks stringi, mille saab stringists iga ladina täheasendamisel temast tsüklilises ladina tähestikusn koha võrra tagapool oleva tähega.

163. Defineerida muutujadictCompare väärtusekscurried-kujul funktsioon, mis võtab argu-mendiks kaks stringi ja annab välja tähestiku järjekorras võrdlemise tulemuse väärtusenatüübistOrdering. Sama tähe suur- ja väikevariandid lugeda ekvivalentseks,kuid kui selli-selt võttes on tegemist sama sõnaga, siis lugeda suurtähed väiketähtedest eespool seisvaks.

Üldisemalt võib listikomprehensioonis kujul (41) igaqi olla generaator, valvur või lokaalne dek-laratsioon. Senitutvustatud reeglid muutujate tähendusulatuse ja väärtustamise kohta jäävad keh-tima.

Valvur on tõeväärtustüüpi avaldis, tema roll on esitada näidiste väärtuste komplektile lisakitsen-dusi: kui valvur väärtustub vääraks, siis vastavat elementi tulemuslisti ei kirjutata. Põhimõtteliselton valvurg listikomprehensioonis samaväärne generaatorigaTrue <- [g].

Lokaalsed deklaratsioonid võimaldavad sisse tuua uusi muutujaid ilma neid listist genereerimata.Lokaalsed deklaratsioonid algavad võtmesõnagalet.

Vaatame uuesti komprehensioonavaldist (43), mille väärtuseks on list kõigi ühekohalistest arvu-dest paaris oma ruuduga. Kui meid arvu5 ruut ei huvita, siis võime selle listist välja jätta, lisadesvalvuri, mis sellisel juhul väärtustub vääraks. Saame avaldise

[(x , x * x) | x <- [1 .. 9], x /= 5 ]. (48)

Avaldise (47) saab aga samaväärselt ümber kirjutada kujul

[(x , y) | x <- [1 .. 9], y <- [1 .. 9], x <= y ]. (49)

Erinevus on selles, et avaldises (49) on tingimus, et paari teine komponent on vähemalt niisamasuur kui esimene, esitatud ilmutatult valvurina, samas kuiavaldises (47) tuleb see välja sellest,et teine komponent genereeritakse listist, kust esimesestkomponendist väiksemad arvud on jubavälja jäetud.

82

Page 83: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Võrreldes aga avaldiste (47) ja (49) väärtustussammude arve, saab selgeks, et arvutuslikus mõt-tes pole nad sugugi võrdväärsed, vaid (49) teeb rohkem samme. Tõepoolest: avaldise (49) puhulgenereeritakse muutujatex ja y väärtustuseks kõik variandid ühekohaliste arvudega, millest val-vur ligemale pooled kõlbmatuks tunnistab, samas kui avaldise (47) korral genereeritaksegi ainultneed väärtustused, mis annavad lõpptulemisse elemendi.

Samal põhjusel ei ole avaldis (43) üldse samaväärne avaldisega

[(x , x * x) | x <- [1 .. ], x <= 9]. (50)

Avaldise (50) väärtustamist pole võimalik lõpetada, sest generaator annab muutujalex muudkuiuusi väärtusi. Tulemuslist on osaline, ehkki sisaldab parajasti kõik need elemendid mis avaldise(43) väärtus.

Selliseid nüansse tuleb listikomprehensiooni kasutamisel arvestada. Muuhulgas võib arvutuskii-rus oluliselt sõltuda ka generaatorite ja valvurite järjekorrast. Valvurid ja generaatorid, mis osavariante välistavad, on kasulik panna nii ette kui võimalik, sest väärtustamisel loetakse kompre-hensiooni vasakult paremale ja kui valvur annab väära või näidis mõne elemendiga ei sobitu, siisseda varianti lõpuni ei koostatagi, vaid minnakse kohe järgmise kallale.

Olgu meil näiteks millegipärast vaja kasutada funktsiooni, mis võtab argumendiks täisarvude listija annab tulemuseks stringi, mis koosneb plokkidest, millest igaühe alguses on number 1 ja sellejärel niimitu numbrit 0, nagu näitab argumentlisti järjekordne element, kusjuures negatiivseid jaliiga suuri (ütleme alates20-st) arve argumentlistis tuleb ignoreerida. Nõutud plokidon lihtneleida kümne astmetena.

Esimene idee on koostada algul plokkide list ja plokid ühekslistiks kokku konkateneerida. KunamoodulisPrelude on täiesti olemas operaatorconcat , mille väärtus on listide listi üheks listikskokkusidumise funktsioon, siis saame vajaliku operatsiooni defineerida koodiga

plokid ns= concat [show (10 ^ n) | n <- ns, 0 <= n && n < 20]

. (51)

Näiteks avaldiseplokid [1, 99999 , 4] väärtus on “1010000”.

Samas on mõttekas ka teistsugune lähenemine, mis ilmutatudkonkateneerimist ei kasuta, vaidesitab kogu nõutava sümbolite listi komprehensioonikujul. Vahetulemusi (üksikuid plokke) väl-jendav avaldisshow (10 ^ n) peab siis liikuma püstkriipsust paremale, uue generaatoripa-remaks pooleks, ja ploki üksikut numbrit tähistagu uus muutujac . Nii võime saada definitsiooni

plokid ns= [c | n <- ns, c <- show (10 ^ n) , 0 <= n && n < 20]

. (52)

Kuid definitsiooni (52) korral võtab avaldiseplokid [1, 99999 , 4] arvutamine palju roh-kem aega kui definitsiooni (51) korral. See on nii sellepärast, et väärtustamise käigus proovitakseläbi kõik muutujac väärtused muuhulgas juhul, kuin väärtus on99999, mida poleks üldse vaja

83

Page 84: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

teha, sestn väärtus pole piirides, mida järgnev valvur lubab. See lisatöö aga kulutab antud juhulmeeletu aja, sestc omandab järjest väärtuseks kõik numbrid100000-kohalisest arvust ja iga kordväärtustatakse valvur uuesti.

Kuna see valvur muutujatc ei sisalda, siis saab ta tuua eelneva generaatori ette. Nii saame veelühe definitsiooni

plokid ns= [c | n <- ns, 0 <= n && n < 20, c <- show (10 ^ n) ]

. (53)

Selle korral leitakse avaldiseplokid [1, 99999 , 4] väärtus välkkiirelt, sest nüüd väär-tustatakse valvur kohen sidumise järel, nii et kokku väärtustatakse teda ainult3 korda ningviimast generaatorit uurima asutakse ainult juhul, kuin väärtus oli nõutud piirides.

Tasub veel tähele panna, et aeglus pole definitsiooni (52) ainus probleem. Näiteks avaldisplokid [1, -1 , 4], mis definitsioonide (51) ja (53) korral annab välja samuti “1010000”,annab (52) korral tulemuseks osalise listi, sest negatiivse arvuga astendamine tekitab vea.

Listikomprehensioon on analoogne matemaatikast tuntud hulgakomprehensiooniga. Hul-gakomprehensioon on viis esitada hulki teatud kitsendustekaudu nagu näiteks kujus{

x2 | 1 6 x 6 9, x 6= 5}

, mis märgib hulka kõigi ühekohaliste5-st erinevate positiivsetetäisarvude ruutudest. Haskelli listikomprehensioon lisab hulgakomprehensiooni tähenduselearvutusliku aspekti.

Ülesandeid

164. Defineerida muutujaastmed väärtuseks funktsioon, mis võtab argumendiks täisarvude lis-ti ja annab tulemuseks listi, mille elemendid saadakse, kuiargumentlisti iga mittenegatiivseelemendin kohta võetakse list esimesest10 positiivsest täisarvustn-ndas astmes.

165. Kirjutada avaldis, mille väärtus on kolmas täisruut, mis on suurem kui100000. Vältidakorduvaid arvutusi.

Monaadikomprehensioon

Monaadikomprehensioon on ehituselt sarnane listikomprehensiooniga, kuid võimaldab kirjapanna protseduure, liste,Maybe-tüüpi väärtusi ja üldiselt väärtusi kõikidest tüüpidest kujul M A,kusA on tüüp jaM on tüübifunktsioon, mis kuulub klassiMonad. Protseduuride korralM = IO,Maybe-tüüpide korralM = Maybe, listide korralM = List jne.

Monaadikomprehensioon kannab paralleelnimetustdo-süntaks võtmesõnado järgi, mis teda

84

Page 85: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Haskellis alustab. Üldkuju on

doq1

. . .ql

a

, (54)

kus igaqi on generaator või lokaalne deklaratsioon jal > 0 ninga on avaldis. Generaatorit kujul_ <- e võib kirjutada lihtsalt avaldisenae. Valvureid ei ole, nii et segadust ei teki.

Generaatorites olevate näidiste muutujad on nähtavad järgmistes generaatorites ja avaldisesa,kuid mitte selles generaatoris ega eelmistes. Avaldised generaatorites ninga peavad olema mingittüüpi M X, kusjuures tüüpX ei pea olema kõigis generaatorites ega avaldisesa sama, kuidMpeab. Kogudo-avaldise tüüp langeb kokku avaldisea tüübiga.

Me ei hakka siin õppima monaadikomprehensioonsüntaksi tähendust üldjuhul, kuigi tähenduseühtne kirjeldus kõigi tüübifunktsioonideM jaoks on põhimõtteliselt võimalik, vaid vaatame ai-nult protseduuride juhtu, st juhtuM = IO.

Protseduuride puhul võimaldab monaadikomprehensioon siduda mitu tegevust kokku üheks suu-remaks tegevuseks.Do-avaldise (54) väärtus on protseduurd, mis täidab järjest kõigi generaa-torite paremate poolte väärtuseks olevaid protseduure. Generaatorip <- e korral sobitataksee väärtuseks oleva protseduuri täitmise järel näidistp väärtusega, mille see protseduur edastab.Sobitumise korral saadaksep elementaarosade väärtustus, millest edaspidi saab vajadusel muu-tujaid arvutada, mittesobitumise korral protseduurid täitmine katkeb täitmisaegse veaga. Kuikõik generaatorid on edukalt läbitud, täidetakse lõpuks kaa väärtuseks olev protseduur.

Kui vigu ei teki, siis kogudo-avaldise (54) väärtuseks olev protseduur edastab avaldise a pooltedastatava väärtuse. See ei tähenda topeltedastamist, vaid niimoodi lihtsalt kujuneb selle tõttu, etdo-avaldise read läbitakse järjekorras esimesest viimaseni.

Oleme juba tuttavad muutujategaputStr , putStrLn , print , mille väärtuseks on funktsioo-nid, mis oma argumendil annavad välja protseduure. Siia võiks veel lisada muutujaputChar ,mille väärtuseks on funktsioon, mis võtab argumendiks sümboli ja annab protseduuri, mis kirju-tab selle sümboli standardväljundisse. Nende muutujate abil võime do-süntaksiga kombineeridakompleksse protseduuri, mis teeb mitu sellist tegevust järjest. Näiteks avaldise

doputStrLn "Tere, Haskell! "putChar ’ ’putStr "2 + (-3) = "print (2 + (-3))

(55)

väärtuseks on protseduur, mis kirjutab ekraanile ühte ritta “Tere, Haskell!” ja teise ritta tühikujärele “2+(-3)=-1”. Kuna väärtusi polnud töö käigus tarvismeelde jätta, võisime generaatoritekohale kirjutada ainult nende paremad pooled.

85

Page 86: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Üldjuhul nii lihtsalt hakkama ei saa. Vaatleme muutujaidgetChar ja getLine : nende väär-tuseks on protseduurid, mis loevad (vajadusel oodates) standardsisendist vastavalt ühe sümbolija ühe rea ning edastavad loetud info (rea korral ilma reavahetussümbolita). Nüüd näiteks prog-rammi, mis loeb standardsisendist kõigepealt ühe rea ja seejärel üksiku sümboli ning kirjutab siisstandardväljundisse teate selle kohta, mis kokku tuli, annab kood

main= do

cs <- getLinec <- getCharputStrLn ( "\nKokku " ++ cs ++ c : ". ")

. (56)

Tõepoolest, esimeses reas loetakse rida ja see saab muutujacs väärtuseks, seejärel teises reasloetakse sümbol ja see saab muutujac väärtuseks, lõpuks kolmandas reas kirjutatakse ekraanileuuelt realt sõna “Kokku ”, siiscs väärtus ehk loetud rida, siisc väärtus ehk loetud sümbol jalõpuks punkt. Siin tuleb siiski täpsustada, et taoline on käitumine juhul, kuipuhverdamine

(ingl buffering) on keelatud, sest puhverdamise korral ei jõua loetud üksiksümbol pärale enne,kui kasutaja sisestab reavahetuse; GHCi-s on puhverdaminevaikimisi keelatud, nii et koodi (56)võiks testida just selle keskkonnaga.

Samas paneb GHC kompilaator puhverdamise vaikimisi peale.Et programm (56) annaks kakompileerimisel soovitud käitumise, tuleb protseduuris esimese asjana anda käsk puhverdaminekeelata.

Antud juhul piisab puhverdamine keelata standardsisendil(standardväljund tehakse niikuiniiprogrammi töö lõppedes puhtaks). Selleks on tarvis importida moodulSystem.IO. Sellest moo-dulist saab muutujastdin , mille väärtuseks on standardsisend, konstruktoriNoBuffering ,mis tähistab puhverdamise puudumist, ning muutujahSetBuffering , mille väärtuseks oncurried-kujul funktsioon, mis võtab argumentideks failipideme japuhverdusviisi tähistava ob-jekti ning annab väärtuseks protseduuri, mis seab vastava pidemega failil puhverdusviisi sel-liseks. Standardsisendi puhverdamise keelamiseks tuleksseegado-avaldisse programmis (56)esimeseks reaks lisada avaldis

hSetBuffering stdin NoBuffering . (57)

Muutujastdin tüüp onHandle . KonstruktoriNoBuffering tüüp onBufferMode ja tei-sed konstruktorid samast tüübist onLineBuffering ja BlockBuffering .

Proovime nüüd kirjutada programmi, mis ootab ühe sümboli sisestamist klaviatuurilt ja kohe, kuisee tuleb, trükib ekraanile selle sümboli koodi. Võiks sobida kood

main= do

hSetBuffering stdin NoBufferingc <- getCharprint (ord c)

. (58)

86

Page 87: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Selle programmi käitumine ei pruugi olla ootuspärane, sestekraanile ilmuvad ka sümbolid ise.

Kui on soov, et klahvivajutuse puhul ei ilmuks sümbolit ekraanile, siis tuleb keelata standardväl-jundi kaja (ingl echo). Selleks võib lisadado-avaldisse rea

hSetEcho stdout False ,

kus muutujastdout väärtuseks on mõistagi standardväljund jahSetEcho väärtuseks funkt-sioon, mis võtab argumendiks failipideme ja tõeväärtuse jaannab tulemuseks protseduuri, misselle pidemega failil vastavalt tõeväärtusele kas keelab või seab kaja. Mõlemad muutujad tulevadmoodulistSystem.IO. Sellisel juhul on soovitav lisada sobivasse kohta ka käsk,mis kaja uuestiseab, muidu ollakse sunnitud pärast programmi töö lõppu edasi pimedas töötama.

Selleks, et programmeerida käsureaargumentide kasutamist, tuleb sisse lugeda moodulSystem.Environment. Sellest moodulist tuleb muutujagetArgs , mille väärtuseks onprotseduur, mis ei tee muud midagi kui edastab käsureaargumendid stringide listina. Näiteksprogramm, mis toob ekraanile käsureaargumentidest esimese ja viimase, kui neid on vähemaltüks, ja vastasel korral teatab, et argumente pole, võiks välja näha kujul

main= do

args <- getArgsif null args

then putStrLn "Argumente pole! "else do

putStrLn ( "Esimene argument " ++ head args)putStrLn ( "Viimane argument " ++ last args)

.

Väärtusi, mida protseduur edastab, saab vastu võtta ainultteine protseduur. Ei ole põhimõtteli-seltki võimalik kirjutada funktsioone tüübigaIO A → A, mis argumendilx annaksid väärtusekstegevusex poolt edastatava väärtuse. Selline funktsioon rikuks ilmutatud viidatavust, sest samaabstraktne tegevus võib erinevatel kordadel edastada erinevaid väärtusi.

Nimi IO tuleb ingliskeelsest väljendistinput-output, mis tähendab “sisend-väljund”. Haskellistuleb seda mõista arvutusprotsessi seisukohalt, niisiis ei kuulu siia vaid interaktiivsed protsessidkasutajaga, vaid üldisemalt igasugune andmevahetus arvutusprotsessi ja keskkonna vahel, sestjust siis tekib oht, et protsessi käigus omandatakse infot,mis pole programmiga üheselt määratud.

Nii tuleb protseduuritüüpidega tegemist isegi juhuarvudega opereerimisel, sest tavaliselt me soo-vime, et programm ei määraks juhuarve üheselt, vaid erinevatel käivituskordadel ilmuksid erine-vad juhuarvud.

Juhuarvudega seonduvad operatsioonid saab moodulistSystem.Random. Peamiselt läheb vajamuutujaidrandomIO ja randomRIO . MuutujarandomIO väärtus on protseduur, mis edas-tab ühe väärtuse, milleks võib olla võrdse tõenäosusega ükskõik milline normaalne väärtus üle

87

Page 88: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kogu tüübi. Tüüp peab olema kontekstist selge või annoteeritud. MuutujarandomRIO väär-tus on funktsioon, mis võtab argumendiks paari ja annab tulemuseks protseduuri, mis edastabühe väärtuse, mis on võrdse tõenäosusega ükskõik milline väärtus paariga määratud lõigus. Kasiin võib olla vajalik tüüpi annoteerida. Tüüp, millest juhusuurusi genereeritakse, peab kuulumaklassiRandom.

Näiteks avaldise

dox <- randomRIO (-99 , 99) :: IO Intprint x

(59)

väärtus on protseduur, mis genereerib juhuslikult ühe täisarvu, mille absoluutväärtus on ülimaltkahekohaline, ja saadab selle standardväljundisse.

Ülesandeid

166. Küsida interaktiivses keskkonnas muutujateputChar , putStrLn , getChar ,getLine , getArgs , randomIO , randomRIO tüübid ja saada neist aru.

167. Kirjutada programm, mis küsib kasutajalt eesti keelesrida ja kui see tuleb, siis kirjutabekraanile selle rea sümbolid vastupidises järjekorras.

168. Kirjutada programm, mis küsib kasutajalt eesti keelessümbolit ja kui see tuleb, kirjutabselle sümboli kohe tühikuga eraldatult küsimuse järele, järgmisele reale aga kirjutab, mit-menda tähega tähestikus tegu on (kui tegu pole tähega, siis kirjutab seda).

169. Kasutades ülesandes 150 defineeritud muutujatnihe , defineerida muutujaniheKajamisel väärtuseks funktsioon, mis võtab argumendiks täisarvun ja an-nab välja protseduuri, mis loeb standardsisendist ühe täheja kirjutab standardväljundissesellest tsüklilises ladina tähestikusn koha võrra tagapool oleva sama “suurusega” tähe(kui loetud sümbol pole täht, siis kirjutab sellesama sümboli). Kasutades seda operaatorit,kirjutada programm, mis ootab kasutajalt klahvivajutust ja kui tuleb täht, toob ekraanilehoopis sellest tsüklilises ladina tähestikus järgmise tähe, muidu aga sisestatud sümbolienda.

170. Kirjutada programm, mis teatab ekraanil käsureaargumentide arvu ja kui see oli2, siisteatab lisaks, kumb argumentidest on tähestikus eespool.

171. Kirjutada programm, mis väljastab ekraanile mittenegatiivse1-st väiksema arvu täpsusega4 kohta peale koma, mis oleks võrdse tõenäosusega ükskõik milline selline arv.

172. Kirjutada programm, mis väljastab ekraanile juhuslikult valitud ladina tähestiku tähe, misvõib võrdse tõenäosusega olla üks suurtähtedest ja üks väiketähtedest.

88

Page 89: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

173. Mida teeb ja mida edastab protseduur, mille kirjeldab avaldis

doputStr "A\n "getLine

?

Protseduuri poolt edastatavat väärtust saab ilmutatult seada muutujareturn abil. Iseseisvalt onmuutujareturn väärtuseks funktsioon, mis suvalisel argumendilx annab tühja, mitte midagitegeva protseduuri, mis lihtsalt edastab väärtusex. Pangem tähele, et Haskellireturn ei tä-henda tagasipöördumist alamprogrammist nagu paljudes teistes keeltes. Nimereturn valik onselles mõttes ebaõnnestunud (inglise keelesreturn tähendab tagasipöördumist või tagastamist).

Olgu meil näiteks vaja kirjutada protseduur, mis küsib kasutajalt eraldi ridadel kaks täisarvu jaedastab nende summa. Sellise protseduuri defineerib muutujaküsiSumma väärtuseks deklarat-sioon

küsiSumma= do

s <- getLinet <- getLinereturn (read s + read t :: Integer)

. (60)

Siin kasutatud muutujaread väärtuseks on funktsioon, mis võtab argumendiks stringi jaannabväärtuseks täisarvu, mida see string väljendab, ehk sisuliselt parsimine. (Kunaread on definee-ritud paljude tüüpide jaoks, täpsemalt klassiRead tüüpide jaoks, siis on annotatsioon vajalik.)

Deklaratsioon (60) võib olla mõttekas, kui kirjutatakse mõnd pikemat programmi, mille erineva-tes osades on vaja küsida kaks arvu ja leida nende summa. Siisselle asemel, et igale poole vastavkood otse sisse kirjutada, võib kutsuda väljaküsiSumma . Kuid küsiSumma on sellel otstarbelkasutatav ainult juhul, kui küsitud arve endid edaspidi ei vajata, sestküsiSumma väärtuseksolev protseduur edastab ainult summa, liidetavad unustab ära.

MuutujatküsiSumma kasutavaks näiteprogrammiks sobiks

main= do

putStrLn "Anna kaks täisarvu. "sum <- küsiSummaputStrLn ( "Summa on " ++ show sum ++ " . ")

. (61)

Ülesandeid

174. Defineerida muutujaküsiVahe väärtuseks protseduur, mis küsib eestikeelse dialoogi abilstandardsisendist kaks sümbolit ja edastab nende koodide vahe. Kirjutada programm, mis

89

Page 90: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

seda protseduuri kasutades küsib kasutajalt kaks sümbolitja teatab ekraanil eestikeelse lau-sega, kas esimene või teine sümbol on kooditabelis teisest ees ja mitme sümboli võrra.Lauses ei pea küsitud sümboleid esitama, võib rääkida “esimesest” ja “teisest” sümbolist.

175. Modifitseerida ülesandes 174 kirjutatud protseduuri edastatavat väärtust ja põhiprogrammitööd nii, et programm sõnastaks lõpus teate kasutaja poolt sisestatud sümbolite terminites,mitte “esimese” ja “teise” terminites.

176. Defineerida muutujaküsiAlgus väärtuseks protseduur, mis küsib kasutajalt rea ja edas-tab sellest reast kuni10 esimest sümbolit (terve rea, kui seal on vähem sümboleid). Kirju-tada programm, mis seda protseduuri kasutades küsib kasutajalt kaks rida ja teatab seejärelekraanil, millised on nende ridade algusosad, mis meelde jäeti.

90

Page 91: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Tsükliliste arvutuste programmeerimine

Rekursioon

Rekursiivseks (ingl recursive) nimetatakse definitsiooni, mille parem pool sisaldab midagi,mida see definitsioon ise defineerib. Avaldise väärtustamine rekursiivse definitsiooni järgi tekitabiteratiivse protsessi: kirjutades defineeritava muutuja asemele avaldise, mille see definitsioontemaga seob, võib tulemuses esineda seesama muutuja, mida tuleb siis omakorda asendada.

Ühe definitsiooni asemel võib rekursiivne olla terve definitsioonide plokk. See tähendab olukor-da, kus on antud definitsioonidD0, D1, . . . , Dn = D0, nii et iga i = 0, . . . , n − 1 korral Di+1

kasutab midagi, mis on defineeritud definitsioonisDi. Sellisel juhul nimetatakse definitsiooneD0, . . . , Dn−1 vastastikku rekursiivseteks (ingl mutually recursive).

Funktsionaalses paradigmas on rekursioon ainus tee tsüklilisi protsesse programmeerida. Seetõt-tu on rekursioon siin äärmiselt tähtis.

Efektiivsuse seisukohalt tuleb rekursiivseid definitsioone kirjutades jälgida, et rekursiivne arvu-tusprotsess sisaldaks ainult osi, mis sõltuvad rekursiooni parameetritest, st osi, mis erinevatelrekursioonitasemetel muutuvad. Sõltumatud osad tuleks viia rekursioonist välja, et neid soorita-taks nii vähe kordi kui võimalik.

Rekursiivselt defineeritud funktsioonid

Funktsiooni rekursiivsel defineerimisel tüüpiliselt avaldatakse funktsiooni väärtus keerulisemateargumentide korral funktsiooni väärtuste kaudu lihtsamatel argumentidel. Seda üleminekut ni-metatakserekursiooni sammuks. Kõige lihtsamatel argumentidel rekursiivset pöördumisteitoimu, need moodustavadrekursiooni baasi.

Keerulisem-lihtsam-seos on erinevatel juhtumitel erinev, seda juba sellepärast, et argumendi-tüübid on erinevad. Keerulisem-lihtsam-seos võib kokku langeda arvude suurusjärjestusega —see tähendab, et funktsiooni argumendid on arvulised. Baasjuhuks on siis tüüpiliselt argument0. Samas on võimalikstruktuurne (ingl structural) rekursioon, kus funktsiooni käituminesuurematel andmestruktuuridel spetsifitseeritakse funktsiooni väärtuste kaudu väiksematel and-

91

Page 92: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

mestruktuuridel. Andmestruktuuriks on väga tihti just listid, mispuhul definitsioon tüüpiliseltsätestab funktsiooni väärtuse mittetühjal listil funktsiooni väärtuse kaudu selle listi sabal ningtüüpilise baasjuhuna sätestatakse funktsiooni väärtus tühjal listil.

Siin tüüpilisena kirjeldatud olukorrad pole kaugeltki ainuvõimalikud ega ainumõeldavad. Arvu-lise argumendiga funktsioon võib olla0-l määramata ja baasjuhuks võib olla näiteks1. Listideltöötaval funktsioonil võib olla baasjuhuks midagi muud peale tühja listi ja rekursiivsel pöör-dumisel ei pruugi argumendiks anda tingimata listi saba. Keerulisem-lihtsam-seos ja baasjuhudvõivad üldse puududa; sellisel juhul toimuvad rekursiivsed pöördumised küll piiramatult, kuidnäiteks lõpmatu listi arvutamisel see ongi eesmärk.

Rekursiivse definitsiooni tuletamise esimese näitena olgumeil vaja defineerida muutujasuurSumma väärtuseks funktsioon, mis võtab argumendiks naturaalarvu n ja annab tulemuseksarvu14 + . . . + n4 ehk1-stn-ni kõigi täisarvude4. astmete summa.

Paneme tähele, et kuin > 0 ja meil on funktsiooni väärtus kohaln−1 ehk14 + . . .+(n−1)4 jubateada, siis leidmaks funktsiooni väärtust kohaln, piisab funktsiooni väärtusele kohaln − 1 liitaarv n4. Kui agan = 0, siis summas liidetavaid pole, mistõttu funktsiooni väärtus on0. Võttesmängu ka negatiivse argumendi võimaluse, mispuhul arvutusvõiks lõpetada töö informatiivseveateatega, saame koodi

suurSumma:: (Integral a)=> a -> a

suurSumma n= case compare n 0 of

GT-> suurSumma (n - 1) + n ^ 4

EQ-> 0

_-> error "suurSumma: negatiivne liidetavate arv "

. (62)

Iga funktsioon, mis antakse argumendiln kui summa mingi teise funktsiooni väärtustest argu-mentidel1-st n-ni, on sarnasel viisil rekursiivselt defineeritav. See tuleneb asjaolust, et summa-operaator põhimõtteliselt on matemaatiliselt rekurrentselt defineeritav. Seejuures argumendil0on funktsiooni väärtus alati0, sest0 on liitmise ühikelement. Analoogne jutt kehtib, kui summaasemel rääkida korrutisest, ainult et argumendil0 on sellise funktsiooni väärtus1, kuna korruta-mise ühikelement on1.

Rekursioon võib toimuda mitme parameetri järgi korraga. Vaatame näiteks diskreetsest mate-maatikast tuntud kahaneva faktoriaali funktsiooni

(x)k = x · (x − 1) · . . . · (x − (k − 1)), (63)

millel on kaks argumenti: arvx ja naturaalarvk. Kui k > 0, siis saab seose (63) paremas poo-

92

Page 93: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

les eraldada esimese tegurix ning ülejäänud tegurite korrutis on(x − 1)k−1. Juhulk = 0 ontegurite arv0, mistõttu korrutis on1. Neil tähelepanekutel põhinev kood kahaneva faktoriaaliarvutamiseks on

kahFact:: (Num a, Integral b)=> a -> b -> a

kahFact x k= case compare k 0 of

GT-> x * kahFact (x - 1) (k - 1)

EQ-> 1

_-> error "kahFact: negatiivne teine argument "

. (64)

Ülesandeid

177. Arvun faktoriaaln! avaldub kahaneva faktoriaali kaudu seosegan! = (n)n. Seetõttu saabdefinitsiooniga (64) antud muutujatkahFact kasutades kodeerida faktoriaalifunktsioonidefinitsiooniga

fact n= kahFact n n

.

Anda muutujalefact samaväärne rekursiivne definitsioon ilma muutujatkahFact kasu-tamata.

178. Kirjutada muutujalekahFact koodiga (64) samaväärne definitsioon, mis põhineks rekur-siooniskeemil, mis igal sammul eraldab viimase teguri.

179. Arvux kasvavaks faktoriaaliks k järgi nimetatakse arvu(x)k = x·(x+1)·. . .·(x+(k−1)).Defineerida muutujakasvFact väärtuseks kasvava faktoriaali funktsiooncurried-kujul.

180. Defineerida muutujamaxModväärtusekscurried-kujuline funktsioon, mis võtab argumen-diks täisarvudm ja n: kui m on positiivne jan mittenegatiivne, siis annab väärtuseks mak-simaalse jäägi, mis arvude1 kuni n ruutude jagamiselm-ga tekib (n = 0 korral0); vastaselkorral lõpetab arvutus töö veateatega, mis ütleb, kumb muutuja nõutud tingimustele ei vas-tanud.

Rekursiivne definitsioon tuleneb tihti otseselt matemaatikas kasutatavast rekurrentsest võrran-dist. Kuigi Haskell on väljendusvahendina küllalt võimas,et tõlkida sellesse matemaatikas an-tavaid definitsioone, pole matemaatikas standardse definitsiooni otsene ülevõtmine alati arvu-tuslikult rahuldav. Matemaatikas on hea definitsiooni kriteeriumiks lihtne sisuline mõistetavus,

93

Page 94: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

võibolla ka lühidus, kuid arvutuslikul aspektil seal kaalupole. Programmeerimisel tuleb rekur-siooniskeemi valikul ka arvutuskeerukust silmas pidada.

Võtame näiteksFibonacci jada

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 . . . ,

mille esimesed elemendid on0 ja 1 ja ülejäänutest igaüks leitakse kui kahe eelmise summa.Matemaatikas pannakse see reegel kirja seostega

F0 = 0, F1 = 1, ∀n > 2 (Fn = Fn−1 + Fn−2) (65)

ning rekurrentse seoseFn = Fn−1 + Fn−2 kehtivuse laiendamiseks2-st väiksematele arvudelelisatakse definitsioonile negatiivseid indekseid puudutav klausel

∀n < 0(

Fn = (−1)n+1F−n

)

.

Nii saabF kõigil täisarvudel defineeritud täisarvuliste väärtustega funktsiooniks.

Kui tõlgiksime selle definitsiooni naiivselt Haskelli, võiksime saada Fibonacci jada liikmete ehkFibonacci arvude arvutamiseks näiteks koodi

fib:: (Integral a)=> a -> a

fib n| n >= 2

= fib (n - 1) + fib (n - 2)| n >= 0

= n| otherwise

= ( if odd n then 1 else -1) * fib (-n)

. (66)

Kasutatud muutujaodd väärtus on predikaat, mis annab väljaTrue parajasti siis, kui argumenton paaritu arv. On olemas ka muutujaeven , mille väärtus on predikaat, mis annabTrue parajastisiis, kui argument on paarisarv.

Koodi (66) järgi arvutades toob igafib väljakutse1-st suurema väärtusega argumendil kaasasama operaatori kaks uut väljakutset. Sellest tulenevalt kasvab väljakutsete arv eksponentsiaal-selt. Arvutus on seetõttu nii aeglane, et kuigi palju Fibonacci arve koodiga (66) reaalselt võimalikarvutada ei ole.

Lineaarse keerukuse saavutamiseks tuleks pidevalt hoida kaks viimast vahetulemust korraga kät-tesaadavana, siis piisaks igal tasemel ühest rekursiivsest väljakutsest. Otseseim võimalus sellekson muuta definitsiooni selliselt, et defineeritav funktsioon annaks välja paare kahest järjestikusestFibonacci arvust. Siis iga selline paar(Fn, Fn+1) on leitav eelmise sellise paari(Fn−1, Fn) kaudu,nihutades teise komponendi esimeseks ja pannes teiseks komponendiks komponentide summa.

94

Page 95: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Õige funktsiooni väärtus avalduks siis kui samal argumendil leitud abifunktsiooni väärtuse ükskomponent.

Paaride sissetoomisel saadav funktsioon on põhimõtteliselt abifunktsioon, mistõttu me ei kirjeldateda eraldi, vaid defineerime ta originaalfunktsiooni definitsiooni kehas lokaalse muutuja väärtu-seks. Kasutades lokaalse muutuja jaoks lihtsuse mõttes sama nimefib , saame definitsiooniks

fib n| n >= 0

= letfib 0

= (0 , 1)fib n

= let(u , v)= fib (n - 1)

in(v , u + v)

infst (fib n)

| otherwise= ( if odd n then 1 else -1) * fib (-n)

. (67)

Paari avaldamiseks eelmise kaudu on omakorda kasutatud lokaalset deklaratsiooni, mis seobmuutujadu ja v eelmise paari komponentidega. Nii pääseme operaatoritefst ja snd korduvastrakendamisest.

Definitsioon (67) näitab muuhulgas seda, et lokaalse deklaratsiooniga saab varjata isegi selles-samas deklaratsioonis defineeritava muutuja. See läheb läbi tänu sellele, et lokaalsefib näh-tavuspiirkonnas pole põhioperaatoritfib vaja välja kutsuda. Globaalses tähenduses onfibkasutatud ainult kahes kohas: kohe koodi alguses ja lõpu eelrakendatuna argumendile-n . Kõikteised kasutused on lokaalses tähenduses.

Paaride kasutamine on abiks ka järgmises olukorras. Oletame, et meile pakub huvi kaherealinelõpmatu tabel

0 1 2 5 12 29 70 169 408 985 2378 5741 13860 334611 1 3 7 17 41 99 239 577 1393 3363 8119 19601 47321

. . . , (68)

mille esimeses veerus on0 ja 1 ning iga ülejäänud veeru ülemine arv on eelmise veeru arvudesumma ja alumine arv on selle ja eelmise veeru ülemiste arvude summa.

Selle tabeli arvude arvutamiseks kujutame tabelit ette paaride jadana, kus paarid vastavad tabeliveergudele. Jada algab paariga(0, 1) ja iga ülejäänud liige on võimalik eelmise kaudu esitada:esiteks vasak komponent on eelmise paari komponentide summa, teiseks on parem komponentselle ja eelmise paari vasakute komponentide summa, millest tuleb, et parem komponent on

95

Page 96: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

eelmise paari esimese komponendi ja komponentide summa summa. Anname selle üleminekumuutujajärgmVeerg väärtuseks koodiga

järgmVeerg:: (Integral a)=> (a , a) -> (a , a)

järgmVeerg (u , v)= let

s = u + vin(s , u + s)

. (69)

Nüüd defineerime muutujasq2 väärtuseks funktsiooni, mis naturaalarvulisel argumendil n an-nab välja selle jada liikme nrn (nummerdades liikmeid alates0-st). Üldistades leitud rekurrentsetseost ka negatiivses suunas, saame koodi

sq2:: (Integral a)=> a -> (a , a)

sq2 n= case compare n 0 of

GT-> järgmVeerg (sq2 (n - 1))

EQ-> (0 , 1)

_-> let

(a , b)= sq2 (-n)

inif even n then (-a , b) else (a , -b)

. (70)

(Muutuja sq2 nimi tuleneb sellest, et nende paaride komponentide suhe lähendab arvu√

2.)Arvutus toodud koodiga toimub lineaarse keerukusega.

Ülesandeid

181. Lucas’ jada defineeritakse rekurrenteselt seostega

L0 = 2, L1 = 1, ∀n > 2 (Ln = Ln−1 + Ln−2).

Negatiivsetel indeksitel võib seda täiendada reegligaL−n = (−1)n · Ln.

96

Page 97: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Defineerida muutujaluc väärtuseks funktsioon, mis täisarvulisel argumendila annab väär-tuseksLa. Arvutus peab toimuma lineaarse keerukusega.

Kõik funktsioonid listidel, mis teevad midagi määramata arvu komponentidega, tuleb program-meerida rekursiooniga. Muidugi on palju kasulikke arvutusi võimalik realiseerida eeldefineeritudmuutujate abil, aritmeetilise jada süntaksiga või komprehensioonsüntaksiga, kuid sellised reali-satsioonid pole alati kõige efektiivsemad.

Olgu meil näiteks vaja arvulisti järgi leida tema nulliga võrdsete elementide arv.Listikomprehensioon võimaldab lühikesi ja elegantseid teid selleks, nagu näitekslength [x | x <- l, x == 0] ja length [0 | 0 <- l], kus l on avaldis,mille väärtus on meid huvitav list. Sellise koodi järgi arvutades luuakse kõigepealt originaallistinullidest omaette list ja loetakse siis sealt need nullid kokku.

Rekursiooniga on võimalik defineerida efektiivsem arvutus, mis ei tekitaks mõttetut nullide va-helisti, vaid loendaks neid originaallisti peal. Defineerime funktsiooni, mis võtab argumendikssuvalise arvulisti ja annab tulemuseks tema nullide arvu, muutuja nullideArv väärtuseks.Rekursiooniskeemi koostamiseks oletame, et mittetühja listi saba nullide arv on teada. Kui nüüdlisti pea on null, siis kogu listis on nulle ühe võrra rohkem kui sabas, ülejäänud juhtudel on kogulistis niisama palju nulle kui tema sabas. Rekursiooni baasiks on vaja ka tühja listi nullide arvu,mis on0. Saame koodi

nullideArv:: (Num a)=> [a] -> Int

nullideArv (x : xs)= let

ülejäänu= nullideArv xs

inif x == 0 then 1 + ülejäänu else ülejäänu

nullideArv _= 0

. (71)

Võtame veel näiteks avaldised kujul

(elem a l , delete a l) . (72)

MoodulistPrelude tuleva muutujaelem väärtuseks on funktsioon, mis võtab argumendiks ob-jekti x ja sama tüüpi elementidega listil: kui objektx esineb listisl, annab ta väärtuseksTrue,vastasel korral kuil on lõplik, siis annab väärtuseksFalse. MoodulistData.List tuleva muutujadelete väärtus on funktsioon, mis võtab samasugused argumendidx ja l: kui objektx esineblistis l, siis annab väärtuseks listi, mille saab lististl objekti x esimese esinemise väljajätmisel,vastasel korral annab väärtuseksl.

97

Page 98: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Nende funktsioonide arvutamisel tehtav töö on suures osas ühine, sest otsitakse sama elemen-ti samast listist. Seega pole avaldise (72) väärtustamine optimaalne tee tema väärtuseks olevapaari leidmiseks. Kui sellist paari on vaja, oleks mõttekasarvutada soovitud tõeväärtus ja listühekorraga, mitte teineteisest sõltumatult.

Defineerimegi nüüd muutujaeemaldaEsimene väärtuseks sellise funktsiooni, mis võtab sa-masugused argumendidx ja l naguelem ja delete väärtuseks olevad funktsioonid ja annabtulemuseks paari, mille esimene komponent võrdub neist kahest funktsioonist esimese väärtuse-ga argumentidelx ja l ning teine komponent teise funktsiooni väärtusega samadelargumentidel,kusjuures paari arvutamisel toimub elemendi otsimine listist ainult ühe korra. Kood võiks olla

eemaldaEsimene:: (Eq a)=> a -> [a] -> (Bool , [a])

eemaldaEsimene a (x : xs)| a == x

= (True , xs)| otherwise

= let(tv , us)

= eemaldaEsimene a xsin(tv , x : us)

eemaldaEsimene _ _= (False , [])

. (73)

Nüüd võime avaldise (elem a l , delete a l) asemel väärtustada avaldiseeemaldaEsimene a l.

Definitsiooni (73) saab veel pisut parandada. Paneme tähele, et defineeritava muutuja esimeneargument antakse uuel väljakutsel alati muutmata kujul edasi: ainus rekursiivne väljakutse asublet-plokis ja argument seal ona, mis langeb kokku argumendiga deklaratsiooni päises. Järelikultei peaks seda argumenti rekursioonis üldse olema. Viies rekursiooni lokaalsesse deklaratsiooni,

98

Page 99: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

saame definitsiooni

eemaldaEsimene a xs= let

eemalda (x : xs)| a == x

= (True , xs)| otherwise

= let(tv , us)= eemalda xs

in(tv , x : us)

eemalda _= (False , [])

ineemalda xs

, (74)

milles rekursioon muutumatu väärtusega argumenti kaasas ei kanna. Selline optimisatsioon pa-randab arvutuskiirust siiski vaid marginaalselt.

Ülesandeid

182. Otsida Hugsi teegist muutujate!! , take , drop , splitAt definitsioonid ja saada neistaru.

183. Defineerida muutujasuurtähega väärtuseks funktsioon, mis võtab argumendiks stringi-de listi ja annab tulemuseks suurtähega algavate stringidearvu selles.

184. Defineerida muutujaesitähed väärtuseks funktsioon, mis võtab argumendiks stringidelisti ja annab tulemuseks nende stringide algusosadest moodustatud lühendi. Stringidest,kus on vähemalt kaks tähte, peab lühendisse tulema parajasti kaks esimest tähte; ühetähe-listest stringidest peab tulema nende ainus täht; tühjad stringid annavad lühendisse tühiku.

185. Defineerida muutujaeemaldaKõik väärtusekscurried-kujul funktsioon, mis võtab argu-mendiks objektix ja sama tüüpi objektide listil ning annab väärtuseks paari, mille esimenekomponent on tõeväärtus, mis ütleb, kasx esineb listisl, ja teine komponent on list, missaadaksel-st kõigix esinemiste eemaldamisel. Minimiseerida korduvat tööd arvutuse käi-gus.

186. Defineerida muutujaeemaldaJaLoenda väärtusekscurried-kujul funktsioon, mis võtabsamasugused argumendidx ja l nagu ülesande 185 funktsioon ning annab väärtuseks paari,mille esimene komponent on objektix esinemiste arv listisl ja teine komponent on list,mis saadaksel-st kõigi x esinemiste eemaldamisel. Minimiseerida korduvat tööd arvutusekäigus.

99

Page 100: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

187. Defineerida muutujakorrutisteSumma väärtuseks funktsioon, mis võtab argumendiksarvulisti ja annab tulemuseks tema elementide paarikaupa korrutiste summa.

188. Defineerida muutujavõrdlePikkust väärtusekscurried-kujuline funktsioon, mis võ-tab argumendiks kaks listi ja annab tulemuseks nende pikkuste võrdlemise tulemuse tüübiOrdering väärtusena. Juhul, kui üks listidest on lõpmatu, teine lõplik, tuleb lõpmatu listtituleerida pikemaks.

Mõnikord ei piisa listirekursiooni puhul sellest, et programmeeritakse rekursiooni baas vaid tüh-ja listi jaoks. Olgu meil näiteks vaja defineerida muutujasummadväärtuseks funktsioon, misvõtab argumendiks arvude listi ja leiab tema kõikvõimalikekahest järjestikusest kohast võetudelementide summade listi. Et ühtki summat leida, peab olemavähemalt kaks elementi, seetõttuon nii tühja kui üheelemendilise listi juhud vaja käsitledaüldjuhust eraldi. Sobib definitsioon

summad:: (Num a)=> [a] -> [a]

summad (x : xs @ (y : _))= x + y : summad xs

summad _= []

. (75)

Esimene deklaratsioon vastab juhule, kus argumentlistis on vähemalt kaks elementi, sest listisaba näidises on sealne esimene element eraldi välja toodud. Kuna funktsiooni väärtus tühjalja üheelemendilisel listil on võrdne tühja listiga (ühtki summat pole), õnnestub need kaks juhtuteises deklaratsioonis ühekorraga ära kirjeldada ja piirduda kokku kahe deklaratsiooniga.

Toomaks näidet veel pisut ebastandardsemast rekursiooniskeemist, defineerime muutujasegmendid väärtuseks funktsiooni, mis argumendiks saadud listi järgi koostab tema seg-mentide listi, kus listisegmendiks nimetame maksimaalseid mittekahanevaid lõike listis(st mittekahanevaid lõike, mis ei sisaldu oma kohal üheski pikemas mittekahanevas lõigus).Esitades iga segmendi omaette listina, peab tulemuslist koosnema listidest ehk definitsioon peabrahuldama signatuuri

segmendid:: (Ord a)=> [a] -> [[a]]

.

100

Page 101: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Kõigepealt programmeerime esimese segmendi eraldamise. Kood

eraldaSegment:: (Ord a)=> [a] -> ([a] , [a])

eraldaSegment (x : xs @ ~(y : _))| null xs || x > y

= ( [x] , xs)| otherwise

= let(us , vs)

= eraldaSegment xsin(x : us , vs)

eraldaSegment _= ([] , [])

(76)

annab muutujaeraldaSegment väärtuseks funktsiooni, mis võtab argumendiks listi ja lõhubselle kaheks listiks kohalt, kus lõpeb esimene segment. Esimeses valvuris on kahanemise juhtkokku võetud juhuga, kus listi saba on tühi, sest mõlemal juhul otsitav segment lõpeb kohepärast listi pead ja väljund on sarnane.

Kasutades seda muutujat, saab nüüd defineerida muutujasegmendid koodiga

segmendid xs| null xs

= []| otherwise

= let(us , vs)

= eraldaSegment xsinus : segmendid vs

. (77)

Definitsioon (77) on küll rekursiivne, sest teise valvuri juht sisaldab pöördumistsegments poo-le, kuid rekursiooniskeem on ebatüüpiline, kuna rekursiivsel pöördumisel ei ole argumendiks listisaba. Kui argument on mittetühi list — käiku läheb siis teinedeklaratsioon, sest esimese dekla-ratsiooni argumendinäidis temaga ei sobitu —, kutsutakse välja funktsiooneraldaSegment ,mis annab kätte esimese segmendi ning järelejääva osa, ningrekursiivsel pöördumisel antakseargumendiks viimane.

Ülesandeid

101

Page 102: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

189. Defineerida muutujajärjestatud väärtuseks funktsioon, mis võtab argumendiks arvu-de listi ja annab väärtuseks tõeväärtuse vastavalt sellele, kas list on lõplik ja mittekahanevaltjärjestatud või leidub listis element, mis on talle eelnevast väiksem.

190. Defineerida kahel viisil muutujakõikVõrdsed väärtuseks predikaat, mis võtab argumen-diks listi ja kontrollib, kas listi elemendid on kõik võrdsed. Kui pole, siis peab väärtusekstulemaFalse, vastasel korral kui list on lõplik, siis peab väärtuseks tulemaTrue. Esimenedefinitsioon peab realiseerima idee kontrollida järjestikuste elementide võrdust, teine agaidee kontrollida esimese elemendi võrdumist ülejäänutega.

191. Defineerida muutujasummad’ väärtuseks funktsioon, mis on nagu koodiga (75) definee-ritud muutujasummadväärtus, kuid mittetühja argumentlisti puhul on tulemuslistis ükselement rohkem ja see võrdub argumentlisti viimase elemendiga.

192. Defineerida muutujakuniKorduseni väärtuseks funktsioon, mis võtab argumendiks lis-ti ja annab välja selle listi algusosa kuni esimese elemendini, mis võrdub talle järgnevaelemendiga, kui selline koht listis leidub, vastasel korral annab välja argumentlisti enda.

193. Defineerida muutujakordaViimast väärtuseks funktsioon, mis võtab argumendiks lis-ti: kui see pole tühi ega lõpmatu, siis annab tulemuseks listi, kus on järjest argumentlistielemendid ja viimane veel korra; lõpmatu listi puhul annab tulemuseks argumentlisti enda;tühja listi puhul lõpetab arvutus töö etteantud sobiva veateatega.

194. Defineerida muutujaneedi väärtusekscurried-kujul funktsioon, mis võtab argumendikskaks listi: kui esimene element lõpeb sama elemendiga, millega teine algab, siis annab tule-museks listi, kus argumentlistid on konkateneeritud, kuidleitud ühine element esineb ühe-kordselt; kui esimese listi viimane ja teise esimene element pole võrdsed, või kui esimenelist on tühi või lõpmatu, siis annab tulemuseks esimese listi.

195. Kasutades ülesandes 185 defineeritud muutujateemaldaKõik , defineerida muutujakorduvad väärtuseks funktsioon, mis võtab argumendiks listi ja annab tulemuseks paa-rikaupa erinevate elementidega listi, mis koosneb parajasti argumentlistis korduvatest ele-mentidest.

196. Defineerida muutujaläbivSuur väärtuseks funktsioon, mis võtab argumendiks stringija annab tulemuseks stringi, kus võrreldes argumentstringiga on stringi alustava väiketähe(kui string algab väiketähega) ja iga tühisümbolile järgneva väiketähe kohal vastav suurtäht.

197. Defineerida muutujasobita väärtusekscurried-kujul funktsioon, mis võtab argumendiksstringidr ja s ja annab väärtuseks tõeväärtuse vastavalt sellele, kas regulaaravaldisena in-terpreteeritudr sobitub stringigas. Regulaaravaldises on sümbolid ? ja * eritähenduslikud,mida võib asendada vastavalt ühe suvalise sümboliga või suvalise lõpliku sümbolijadaga.

198. Kas definitsioonis (77) esimese valvuri juhu ärajätmisel saame samaväärse definitsiooni?

102

Page 103: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

199. Kas definitsioonis (77) esimese valvuri juhus[] asendamiselxs -ga saame samaväärsedefinitsiooni?

Viimase näitena olgu meil vaja defineerida muutujakaheksLoe väärtuseks funktsioon, misvõtab argumendiks listil ja annab tulemuseks listipaari, kusl elemendid on üle ühe jaotatudkahte listi. Sellise jaotamise tegemisel tuleb igal sammulteada, kummasse listi käsilolev elementpaigutada. Põhiprobleemiks on, kuidas seda infot hoida.

Üks võimalus on töödelda igal rekursioonisammul kaks elementi. Sellisel juhul on alati teada, etesimene element läheb esimesse listi. Kuigi baasjuhte on kaks (tühjad ja1-elemendilised listid),on need kirjeldatavad ühtsel viisil. Koodiks saame

kaheksLoe:: [a] -> ([a] , [a])

kaheksLoe (x : y : ys)= let

(us , vs)= kaheksLoe ys

in(x : us , y : vs)

kaheksLoe xs= (xs , [])

. (78)

Elegantsem lahendus on igal sammul vahetada ehitatava paari komponendid. Selle idee realisee-rib kood

kaheksLoe (x : xs)= let

(us , vs)= kaheksLoe xs

in(x : vs , us)

kaheksLoe _= ([] , [])

. (79)

Niisugune lahendus on võimalik tänu sellele, et rekursiooni baasjuht on kahe variandi suhtessümmeetriline: baasjuhul me ei pea teadma, kumb komponent saab lõpuks olema vasak ja kumbparem. Me tuleme hiljem tagasi sarnase näite juurde, mis seda lahendust ei võimalda.

Veel ühe lahenduse pakub vastastikrekursioon. Kirjutame kaks definitsiooni, millest üks on ele-

103

Page 104: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

mendi paigutamiseks vasakpoolsesse, teine aga parempoolsesse listi. Saame koodi

kaheksLoe (x : xs)= let

(us , vs)= kaheksLoe’ xs

in(x : us , vs)

kaheksLoe _= ([] , [])

kaheksLoe’ (x : xs)= let

(us , vs)= kaheksLoe xs

in(us , x : vs)

kaheksLoe’ _= ([] , [])

. (80)

MuutujakaheksLoe definitsioon kasutab muutujatkaheksLoe’ , mille definitsioon omakor-da kasutab muutujatkaheksLoe . Seega on need definitsioonid vastastikku rekursiivsed.

Kuna meile pakub huvi ainult muutujakaheksLoe ja muutujakaheksLoe’ ainus väljakutseasubkaheksLoe definitsiooni kehas, võibkaheksLoe’ definitsiooni muuta lokaalseks, viiesta ülekaheksLoe definitsioonilet-plokki. Haskellis võivad nimelt vastastikku rekursiivsed ollaka definitsioon ja tema kehas esinev lokaalne definitsioon.

Ülesandeid

200. Kirjutada oma moodulisse definitsioon (79) ja veendudainteraktiivse keskkonna käsuridakasutades, et defineeritud muutujakaheksLoe rakendamisel lõpmatule argumentlistile ontulemuseks paar kahest lõpmatust listist.

201. Defineerida muutujavahelduv väärtuseks funktsioon, mis võtab argumendiks stringi jaannab väärtuseks stringi, mille igal paarituarvulisel kohal on algse stringi vastaval kohaloleva tähe suurtäheline variant ning igal paarisarvuliselkohal algse stringi vastaval kohaloleva tähe väiketäheline variant.

202. Defineerida muutujaaltern väärtuseks funktsioon, mis võtab argumendiks arvude listijaannab tulemuseks tema elementide alternatiivse summa, st üle ühe on elemendid liidetud,ülejäänud on lahutatud.

104

Page 105: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

203. Defineerida muutujavaheliti väärtusekscurried-kujul funktsioon, mis võtab argumen-diks kaks listi ja koostab vastuseks listi, mille elemendidtulevad vaheldumisi ühest ja teisestlistist, niikaua kui võtta saab. Kui ühe listi elemendid lõpevad, siis teise listi ülejääv osa jääbmuutmata kujul vastuslisti lõppu.

Rekursiivselt defineeritud protseduurid

Nagu juba öeldud, pole Haskellis tsükliliste arvutuste programmeerimiseks muud valikut kuikasutada rekursiooni. Muuhulgas kehtib see tsüklis jooksva interaktiivse protsessi kohta.

Kirjutame näitena programmi, mis harjutab kasutajat klaviatuuril vigadeta tippima. Programmpeab esitama ekraanil kasutajale tekstiridu, mida kasutaja peab järele kirjutama. Niipea, kui ka-sutaja mõne tähega eksib, esitab programm sama rea uuesti. Kui kasutaja viga ei tee, jätkabprogramm järgmise reaga või, kui kogu tekst on läbi, lõpetab.

Jagame koodi osadeks järgnevalt. MuutujakordaTähed väärtuseks defineerime funktsiooni,mis võtab argumendiks ühe rea ja annab tulemuseks protseduuri, mis ootab standardsisendistselle rea kordamist ja edastab tõeväärtuse vastavalt sellele, kas kordamine õnnestus või mit-te. See protseduur ise ei tegele rea uuesti esitamisega kasutajale, seda peab tegema väljakutsuvprotseduur vastavalt temalt saadud tõeväärtusele. Muutuja kordaRead väärtuseks olgu funkt-sioon, mis võtab argumendiks ridade listi ja annab tulemuseks protseduuri, mis esitab neid ridukasutajale kordamiseks. Tema definitsioon peab kutsuma välja operaatoritkordaTähed ja va-lima järgnevalt esitatava rea sellelt saadud tõeväärtuse järgi. Põhiprogramm, mis kutsub väljakordaRead , olgu muutujasharjutus . Signatuurid on selle spetsifikatsiooni põhjal

kordaTähed:: String -> IO Bool

,

kordaRead:: [String ] -> IO ()

,

harjutus:: IO ()

.

Definitsioonide kirjutamist alustame lihtsamast ehk põhiprogrammist. Lihtsuse mõttes annameharjutatava teksti praegu koodis ette. Sobivad deklaratsioonid

tekst= words "gladiool hüatsint krüsanteem rododendron "

, (81)

harjutus= do

hSetBuffering stdin NoBufferingkordaRead tekstputStrLn "Harjutus lõpuni läbitud. "

, (82)

105

Page 106: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kus operaatorwords tuleb moodulistPrelude ja tema väärtuseks on funktsioon, mis annabteksti järgi välja tema sõnade listi vastavalt tühisümbolite esinemisele tekstis. Kõrvalepõikenavõib märkida, et moodulPrelude pakub ka analoogilise operaatorilines , mis annab tekstijärgi välja ridade listi vastavalt reavahetussümbolite esinemisele. Seega deklaratsiooniga (81)samaväärne oleks deklaratsioon

tekst= lines "gladiool\nhüatsint\nkrüsanteem\nrododendron "

.

MuutujakordaRead definitsiooniks sobib

kordaRead rs @ (w : ws)= do

putStrLn wb <- kordaTähed wputStrLn ""if b then kordaRead ws else kordaRead rs

kordaRead _= return ()

. (83)

Mittetühja argumentlisti korral kirjutatakse tema esimene element, milleks on üks string, korda-miseks ekraanile ja kutsutakse väljakordaTähed sellel stringil. Spetsifikatsiooni põhjal edas-tab see protseduur tõeväärtuse, mis näitab kordamise õnnestumist; see tõeväärtus satub muutujas-seb. Selleks, etTrue korral jätkataks järgmise reaga,False korral aga sama reaga, on saavutatudnii, etTrue korral jäetakse rekursiivsel pöördumisel listist esimeneelement ära, kuidFalse korraltoimub rekursiivne pöördumine sama listiga uuesti.

Tühja argumentlisti korral pole vaja midagi teha, sest teate lõpu kohta kirjutab põhiprogramm.Seepärast on teise deklaratsiooni paremaks pooleks lihtsalt return () .

Viimaks muutujakordaTähed definitsiooniks võiks olla

kordaTähed (c : cs)= do

d <- getCharif d == c then kordaTähed cs else return False

kordaTähed _= return True

. (84)

Kui kasutaja sisestab õige tähe, siis jääb tal veel ülejäänud tähed õigesti korrata, seepärast onsellel juhul koodis rekursiivne väljakutse listi sabal. Kui kasutaja sisestab vale tähe, siis on kor-damine ebaõnnestunud, seetõttu sel juhul muud ei pea tegemakui edastama tõeväärtuseFalse.Tühja listi juht tuleb käiku ainult siis, kui kasutaja on kogu rea õigesti sisestanud, seepärast võibka siin lõpetada töö, edastades tõeväärtuseTrue.

106

Page 107: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Paneme tähele, et rekursiivse pöördumise juhul operaatorreturn puudub. Tüüpilistes impe-ratiivsetes keeltes tuleb tagastusoperaator ka sellisesse kohta märkida, kuid Haskellis oleks seevale. Põhjus on ikka see, etreturn tähendab siin midagi muud, ta teeb väärtusest protseduuri,mis seda väärtust edastab. KunakordaTähed cs väärtus juba on protseduur, siisreturnlisamine annaks tüübivea.

MuutujakordaRead definitsiooni (83) lahtiseletamise juures märkisime möödaminnes, etelse-harus toimub rekursiivne pöördumine sama listiga mis argumendina sisse tuli. See on asjaolu, misväärib lähemat vaatlemist. Seni ju oli koodi mõttekuse jaoks alati vaja, et rekursiivsel pöördu-misel argument mingis mõttes väheneks.

Tuleb välja, et see pole sugugi alati nõnda. Argumendi vähenemine on vajalik siis, kui arvutuspeab millalgi lõppema, kuid antud protseduuri spetsifikatsioon seda ei nõua: kui kasutaja ikkajääbki sama rea juures vigu tegema, siis programm peab jäämagi temaga seda rida harjutama.Võib tuua palju näiteid protseduuridest, mis võivad lõpmatusse tsüklisse jääda, nii et nende tööon seejuures mõttekas.

Olgu meil näiteks vaja protseduuri, mis jälgib standardsisendist tulevaid sümboleid ja lõpetabainult siis, kui tuleb teatav kindel sümbol. Võib kodeeridafunktsiooni, mis võtab argumendiksselle sümboli, mille peale töö lõppema peab, ja annab tulemuseks vastava protseduuri. Sobibkood

kuulaKuni:: Char -> IO ()

kuulaKuni a= do

c <- getCharif c == a then return () else kuulaKuni a

. (85)

Seda protseduuri võib kasutada olukorras, kus tarvis oodata kasutajalt kinnitust, et võib töögaedasi minna. Näiteks on mõttekas väljakutse koodist

jätkab:: Char -> IO ()

jätkab a= do

putStrLn ( "Jätkab sümbol " ++ show a ++ ". ")hSetBuffering stdin NoBufferinghSetEcho stdout FalsekuulaKuni ahSetEcho stdout True

,

107

Page 108: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kui põhiprogramm on näiteks

main= do

putStr "Ruudud: "print [x ^ 2 | x <- [0 .. 299]]jätkab ’X’putStr "Kuubid: "print [x ^ 3 | x <- [0 .. 299]]

.

Avaldisejätkab ’X’ väärtuseks on protseduur, mis ootab tummalt standardsisendist sümbo-leid kuni suure X-täheni, siis lõpetab töö.

Paneme tähele, et koodis (85) toimub rekursiivne pöördumine muutujakuulaKuni poole alatisama argumendiga. Lihtsustumine puudub täiesti, arvutusprotsessi lõpetamine sõltub siin muu-dest asjaoludest.

Kuna aga argument ei muutu, ei pruugi teda ka pidevalt kaasaskanda. Teeme seetõttu siin samaoptimeeringu, millega muutujaeemaldaEsimene definitsioonist (73) sai definitsioon (74):viime rekursiooni lokaalsesse definitsiooni, jättes argumendia maha. Tulemuseks on kood

kuulaKuni a= let

proc= do

c <- getCharif c == a then putChar ’\n ’ else proc

inproc

. (86)

Kui seni oleme näinud vaid selliseid rekursiivseid definitsioone, kus defineeritava muutuja väär-tus on funktsioon, siis koodis (86) defineeritakse rekursiivselt lokaalne muutujaproc , milleväärtus on hoopis protseduur.

Samamoodi otsese rekursiooniga on kirjeldatav näiteks protseduur, mis kuulab standardsisendit,kuni sisestatakse tühi rida, leiab sisestatud ridade arvu ning edastab selle väärtuse: sobib kood

loendaRead:: (Integral a)=> IO a

loendaRead= do

rida <- getLineif null ridathen return 0else do

ridadeArv <- loendaReadreturn (1 + ridadeArv)

. (87)

108

Page 109: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

204. Defineerida muutujagrammar väärtusekscurried-kujuline funktsioon, mis võtab argu-mendiks täisarvun ja stringi s ja annab tulemuseks protseduuri, mis kirjutab ekraanilenkorda stringis koos reavahetusega.

205. Defineerida muutujaloeList väärtuseks funktsioon, mis võtab argumendiks listi ja annabväärtuseks protseduuri, mis tsüklis ootab kasutajalt klahvivajutust: kui see onenter, kirjutabekraanile listi järgmise elemendi; kui see onescape, lõpetab; kui see on midagi muud, eireageeri kuidagi. Kui list on läbi, lõpetab programm kohe töö.

MuutujatloeList kasutades käivitada interaktiivses keskkonnas protseduur, mis kirjutabkasutajaenter-vajutuste peale koodide järjekorras kõik sümbolid. Sama muutujat kasuta-des käivitada ka protseduur, mis kirjutab kasutajaenter-vajutuste peale suuruse järjekorrasnaturaalarvude ruudud. Jälgida, et ekraanipilt oleks loetav.

206. Kirjutada klaviatuuril tippimise programm ümber nii,et vea korral programm rida uuestitippida ei laseks, kuid lõpus väljastaks ekraanile info, mitu rida mitmest õigesti tipiti.

207. Defineerida muutujapööra väärtuseks protseduur, mis tsüklis küsib loomulikus keeleskasutajalt sisendit, loeb klaviatuurilt rea ja toob rea lõppedes ekraanile selle rea sümbolidvastupidises järjekorras, kuni sisestatakse tühi rida.

208. Kasutades ülesandes 169 defineeritud muutujatniheKajamisel , defineerida muutujapüsivNihe väärtuseks funktsioon, mis võtab argumendiks täisarvun ja annab tulemu-seks protseduuri, mis kuulab standardsisendit kuni reavahetuseni ja jooksvalt kajastab loe-tud sümbolid ekraanil, seejuures tähed tsüklilises ladinatähestikusn-kohalise nihkega. Ka-sutada protseduurirekursiooni.

209. Kirjutada programm, millega testida muutujatloendaRead , mis on antud koodiga (87).

210. Defineerida muutujaloendaSümbolid väärtuseks protseduur, mis kuulab standardsi-sendit kuni tühja reani ja edastab loetud ridades olevate sümbolite koguarvu (ilma reavahe-tussümboliteta).

Rekursiivselt defineeritud andmestruktuurid

Nagu oleme kogenud, võib Haskellis täiesti korrektse ja mõtteka avaldise väärtuseks olla lõp-matu andmestruktuur, näiteks list. Seni oleme lõpmatuid liste tekitanud vaid aritmeetilise jadaerisüntaksi abil, lõpmatuid liste välja andvaid eeldefineeritud funktsioone kasutades ja selliseltgenereeritud lõpmatuid liste modifitseerides.

Kui tahetakse defineerida omal soovil mõni täiesti uus lõpmatu list, siis ei saa üle ega ümberrekursioonist, kuna lõpmatu listi tekitamiseks on vaja programmeerida lõpmatu arvutusprotsess,

109

Page 110: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

mis lõpliku eeskirjaga kirjeldatuna peab sisaldama tsüklit ja mida funktsionaalses keeles järeli-kult ilma rekursioonita väljendada ei ole võimalik.

Andmestruktuuri rekursiivsel defineerimisel on üldised printsiibid mõnevõrra teistsugused kuifunktsioonide puhul. Sedasorti baasjuht nagu funktsioonide puhul pole vajalik — selle puudu-misel on lihtsalt andmestruktuur lõpmatu. Rekursiooni baasi rollis on aga nõue, et kuni arvutuspole lõppenud, peab kogu aeg olema võimalik lõpliku ajaga mingi andmestruktuuri tükk juurdeehitada. Muidu jääb protsess tühja tsüklisse ja mingit struktuuri ei teki. Listi puhul tähendab seenõue, et definitsioonis tuleb vähemalt üks element listi algusest kirjeldada ilma rekursioonita,ülejäänud struktuuri jaoks võib kasutada rekursiooni.

Proovime kirjeldada funktsiooni, mis genereerib geomeetrilise jada: defineerime muutujageom,mille väärtuseks olekscurried-kujul funktsioon, mis võtab argumendiks arvuda ja q ning annabtulemuseks lõpmatu listi, milles on järjest kõik liikmed geomeetrilisest jadast algelemendigaategurigaq. Signatuur võiks olla

geom:: (Num a)=> a -> a -> [a]

.

Esimese elemendi määramisega raskusi ei ole, sest see tulebotse funktsiooni argumendist. Listiülejäänud osa võib kirjeldada mitme ideega. Üks variant on panna tähele, et geomeetrilise jadaliikmed alates teisest moodustavad geomeetrilise jada sama teguriga. Et teine liige ise on esimeseliikme ja teguri korrutis, saame definitsiooni

geom a q= a : geom (a * q) q

. (88)

Aga on ka võimalik võtta aluseks tähelepanek, et listi saba iga element peab olema saadud kogulisti vastava positsiooni elemendist korrutamisel geomeetrilise jada teguriga (sest listi saba ele-ment nri on kogu listi element nri + 1 ehk kogu listi elemendist nri järgmine). Selle põhjalsaame koodi

geom a q= a : [x * q | x <- geom a q]

. (89)

Arvutus definitsiooni (89) järgi toimub järgmiselt. Saadesargumendid, pannakse kõigepealt pai-ka listi pea. Listi saba arvutamiseks hakatakse uuesti arvutama avaldistgeom a q. Kuna ar-gumendid on samad, kopeerib see alamarvutus kogu arvutust.Seega alamarvutus suudab samutileida esimese elemendi. Kuid sellest on küll, et põhiarvutus saaks definitsioonist (89) määra-ta listi teise elemendi. Kuna alamarvutus kopeerib põhiarvutust, suudab teise elemendi leida kaalamarvutus. Kuid teise elemendi põhjal leiab põhiarvutuskolmanda elemendi. Nii jätkates lei-takse listi elemendid kuitahes kaugele.

Selles väärtustusprotsessis pole midagi põhimõtteliseltuut, kõik toimub varemkäsitletud reeglitealusel. Samas saab koodi (89) järgi toimuva arvutuse üle sügavamalt järele mõeldes selgeks, et

110

Page 111: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

see on kaugelt liiga ebaefektiivne. Kui definitsiooni (88) puhul on keerukus lineaarne (listi algus-jupi leidmise aeg on proportsionaalne jupi pikkusega), siis (89) puhul on tegu ruutkeerukusega,sest iga uue elemendi leidmiseks käivitub algusest peale uus kopeeriv arvutus.

See aga veel ei näita, et definitsiooni (89) aluseks olev ideeon halb. Kuna argumendid rekursiiv-sete pöördumiste käigus jäävad samaks, võime definitsioonis (89) teha samalaadse optimeeringunagu definitsioonide (73) ja (85) puhul: viime rekursiooni lokaalsesse definitsiooni, jättes argu-mendida ja q maha. Tulemuseks on kood

geom a q= let

gs= a : [x * q | x <- gs]

ings

. (90)

Lokaalne deklaratsioon defineerib rekursiivselt muutujags , mille väärtus on list.

Arvutus koodi (90) järgi käib niisama kiiresti kui koodi (88) järgi. Mingeid koopiaid ei teki. Toi-mub ainult üks rekursiivne pöördumine ja sellest edasi ei ole definitsiooni rekursiivsusel enammingit tähtsust. Kõik käib samamoodi, nagu arvutataks listi elemente mõne teise, juba olemas-oleva listi põhjal, kuigi tegelikult need listid juhtumisilangevad kokku. See töötab, sest “kirjutav”mehhanism on “lugevast” ühe lüli võrra ees. Nii arvutatakseainult üks list ja arvutus on lineaarsekeerukusega.

Lihtsaimad definitsioonid, kus list esitatakse iseenda kaudu ilma funktsiooni vahenduseta, onsellised, kus see list on konstantne. Näiteks lõpmatul listil, mille iga element on0, võrdub pea0-ga ja saba listi endaga. Siit saame deklaratsiooni

zs= 0 : zs

. (91)

Vaatame ka keerulisemaid näiteid. Oletame, et tahame arvutada listi, mille komponentideks olek-sid kasvavas järjekorras kõik positiivsed täisarvud, mille esituses algarvude astmete korrutisenaehk kanoonilises esituses ei esine muid algarve peale2 ja 5. Teisi sõnu, need arvud tohivadalgarvudest jaguda vaid2- ja 5-ga.

Esimene liige selles listis on kindlasti1, sest1 on vähim positiivne täisarv ja keelatud algarvu-dega ta ei jagu. Iga ülejäänud arvn selles listis on mingi positiivse täisarvun′ 2- või 5-kordne,kusjuures kan′ ei jagu muude algarvudega peale2 ja 5 ning ta onn-st väiksem. Seega iga arvmeie listi sabas on mingi listis temast eespool esineva arvu2- või 5-kordne. Samas kui meielisti kõiki elemente korrutada2-ga ja5-ga, siis tekivad meie listi elemendid; arvestades eelnevatvaatlust, saame sellisel viisil niisiis parajasti kõik listi saba elemendid.

Arutlusest tuleneb, et otsitava listi saba arvutamiseks terve listi kaudu tuleks leida selle listi ele-mentide2-kordsete list ja5-kordsete list, seejärel aga panna nende kahe listi elemendid kasvavas

111

Page 112: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

järjestuses ühte kokku, kaotades ka kordused. Kuna liste hoitakse kogu aeg elementide kasvavasjärjestuses ja2- või 5-ga korrutamine seda omadust ei muuda, siis on vaja operatsiooni, mis ka-he kasvavas järjestuses elementidega listist paneks kokkuühe kasvavas järjestuses elementidegalisti. Sellist operatsiooni nimetataksepõimimiseks (ingl merge). Selle saame koodiga

põimi:: (Ord a)=> [a] -> [a] -> [a]

põimi xs @ (a : as) ys @ (b : bs)= case compare a b of

LT-> a : põimi as ys

GT-> b : põimi xs bs

_-> a : põimi as bs

põimi xs []= xs

põimi _ ys= ys

. (92)

Nüüd võime otsitava listi muutujakords väärtuseks anda definitsiooniga

kords:: (Integral a)=> [a]

kords= 1 : põimi [x * 2 | x <- kords ] [x * 5 | x <- kords ]

.

Defineerime sama tehnikaga muutujafibs väärtuseks listi, mis sisaldab parajasti kogu Fibo-nacci jada. Sobib definitsioon

fibs:: (Integral a)=> [a]

fibs= 0 : 1 : summad fibs

, (93)

kus muutujasummadon antud definitsiooniga (75).

Definitsiooni (93) järgi toimub Fibonacci arvude arvutamine lineaarse ajaga järjekorranumbrisuhtes, iga järgneva elemendi leidmiseks tehakse vaid üks liitmine. Definitsiooni (67) kõrvalevõib seega tuua veel ühen-ndat Fibonacci arvu lineaarse keerukusega arvutamist realiseeriva

112

Page 113: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

definitsiooni

fib n| n >= 0

= fibs !! n| otherwise

= ( if odd n then 1 else -1) * fib (-n)

. (94)

Ülesandeid

211. Otsida Hugsi teegist üles muutujaterepeat ja cycle definitsioonid ja saada neist aru.

212. Defineerida muutujasq2s väärtuseks list, mille elemendid oleksid järjekorras tabeli (68)veerud paarikujul.

213. Ülesandes 181 defineeriti Lucas’ jada. Defineerida muutuja lucs väärtuseks list, milleelementideks on järjest kõik Lucas’ jada elemendid.

214. Defineerida muutujahamming väärtuseks list, mille elementideks on kasvavas järjekorraskõik positiivsed täisarvud, mille kanoonilises esituses ei esine muud algarvud peale2, 3, 5.

215. Defineerida muutujatuia väärtuseks lõpmatu list elementidega1, 2, 3, 2, 3, 4, 3, 4, 5, . . ..

216. Defineerida muutujavenivillem väärtuseks funktsioon, mis võtab argumendiks listilja annab väärtuseks listi, mis algab listil elementidega, millele järgnevad listil elemendidigaüks kahekordselt, seejärel järgnevad listil elemendid igaüks neljakordselt, edasi kahek-sakordselt jne.

217. Defineerida muutujabitistringid väärtuseks list, mille elementideks on kõik lõplikudbitistringid, igaüks üks kord.

218. Defineerida muutujakaheAstendajad väärtuseks list, milles on järjest arvu2 astenda-jad kõigi positiivsete täisarvude kanoonilises esituses.

Lõpmatu andmestruktuuri rekursiivne definitsioon, olgu siis läbi funktsiooni või otsene, on üld-juhul lihtsamgi kui lõpliku andmestruktuuri oma, sest viimasel juhul tuleb tegelda ka lõpetamis-tingimustega.

Vaatame lõpliku listi defineerimise näitena Collatzi jadade leidmist. Kuin on positiivne täisarv,siis vastavaksCollatzi jadaks nimetatakse järgmiste reeglite alusel arvutatud arvujada:

• esimene liige onn;

• kui senileitud liikmetest viimane on1, siis sellega on jada lõppenud;

113

Page 114: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

• kui senileitud liikmetest viimane on paaris, siis järgmineliige on täpselt pool sellest;

• kui senileitud liikmetest viimane on paaritu, siis järgmine liige on ühe võrra suurem kuiselle liikme kolmekordne.

Kodeerime Collatzi jada arvutusreeglid Haskellis, andes rekursiivse definitsiooni funktsioonile,mille väärtus positiivsel argumendiln onn-ga algav Collatzi jada. Paneme tähele, et kuin > 1,siis Collatzi jada esimese elemendi väljajätmisel saame samuti Collatzi jada, nimelt selle, misvastab algse jada teisele elemendile. Nii saame koodi

collatz:: (Integral a)=> a -> [a]

collatz n| n > 0

= n :if n == 1then []else let

(q , r)= n ‘divMod ‘ 2

incollatz ( if r == 0 then q else n * 3 + 1)

| otherwise= []

. (95)

Kuid ka Collatzi jadu on võimalik defineerida nii, et rekursiooniga määratletakse otse jada ise,

114

Page 115: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

mitte funktsioon. Seda teeb kood

collatz n| n > 0

= letf (x : xs)

| x == 1= []

| otherwise= let

(q , r)= n ‘divMod ‘ 2

in( if r == 0 then q else x * 3 + 1) : f xs

cs= n : f cs

incs

| otherwise= []

. (96)

Lokaalset muutujatf kutsutakse välja ainult mittetühjadel listidel, mistõttupole vajadust tühjaargumentlisti juhu lisamisele tema definitsiooni.

Definitsioonist tegelikult üldse ei selgugi, kas Collatzi jada on lõplik või lõpmatu. Interaktiivseskeskkonnas võib nüüd kergesti erinevaid Collatzi jadu tervikuna välja arvutada ja näha, et nadkipuvad lõppema. Küsimus, kas leidub sellinen, mille korral Collatzi jada on lõpmatu, oli kauaüks kuulsaid matemaatika lahendamata probleeme. Lõpuks õnnestus siiski tõestada, et Collatzijada on lõplik igan korral.

Märgime, et rekursiivselt võib defineerida põhimõtteliselt mida iganes. Näiteks võib koodiga(69) defineeritud operaatorijärgmVeerg samaväärselt defineerida koodiga

järgmVeerg (u , v)= let

p = (u + v , u + fst p)inp

,

kus lokaalse deklaratsiooniga määratletakse rekursiivselt paar. Sama asja võib ümber kirjutada

115

Page 116: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ka kujul

järgmVeerg (u , v)= let

p@ (s , _)= (u + v , u + s)

inp

,

kus rekursioon on ainult muutujas peal, mis tähistab paari vasakut komponenti.

Ülesandeid

219. Defineerida muutujakahendPööratud väärtuseks funktsioon, mis võtab argumendikstäisarvun ja kui see on mittenegatiivne, siis annab tulemuseks listina arvun kahendesitusenumbrid ilma algusnullideta tavalisele vastupidises (järgu kasvamise) järjekorras. Negatiiv-sel argumendil peab arvutus lõppema adekvaatse veateatega.

220. Olgu′ paremassotsiatiivne binaarne operatsioon, mis defineeritakse reegliga

a ′ r = a +1

r

(sama operatsioon defineeriti muutuja// väärtuseks koodiga (19)). Reaalarvur ahelmur-

ruks (ingl continued fraction) nimetatakse lõplikku või lõpmatut esitust vastavalt kujulr = a0 ′. . .′an või r = a0 ′a1 ′. . ., kus kõikai on täisarvud ja ainulta0 võib olla mittepositiivne,kusjuures lõplikkuse korralan > 1. Arve ai nimetatakse ahelmurruelementideks.

Defineerida muutujaahel väärtuseks funktsioon, mis võtab argumendiks ratsionaalarvu qja annab väljaq ahelmurru elementide listi. Soovitav on kasutada ülesandes 153 või 155defineeritud muutujatõigeProperFraction .

Arvutamine rekursiivse funktsiooni argumentidel

Imperatiivses keeles programmeerides hoitakse arvutuse jaoks pidevalt vajaminevad andmed tüü-piliselt muutujates ja vajadusel uuendatakse neid andmeidomistuskäskudega. Funktsionaalseskeeles pole muutujaid sellises mõttes nagu on imperatiivses keeles, omistus puudub. Kuid sel-legipoolest on funktsionaalses keeles võimalik arvutamine samasugusel põhimõttel, kasutadesjooksvate suuruste salvestamiseks rekursiivse funktsiooni argumente.

Taolised rekursiivse funktsiooni argumendid on oma iseloomult samasugused nagu need, mistulid ette lõpmatu listi defineerimisel. Seega oleme sellistega juba tuttavad. Üksteisele järgnevaterekursiivsete väljakutsete käigus nad ei lihtsustu, tüüpiliselt on hoopis vastupidi. Tavaliselt ei ole

116

Page 117: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

nende roll näidata, millal rekursiivsete pöördumiste jadalõpetada. Kui vaja, tehakse see otsusteiste argumentide või mõne spetsiaalse lõpetamistingimuse põhjal.

Kui varasemates näidetes tulenesid kõik kasutatud argumendid otse ülesande püstitusest, siisüldiselt on sageli vaja jooksvate andmete hoidmiseks viia rekursioon abioperaatorisse, millele onargumente kunstlikult lisatud. Sellised definitsioonid vastanduvad nnotserekursiivsetele, kusdefineeritav muutuja ise on rekursiivselt määratletud. Järgnevas vaatlemegi programmeerimistargumentide lisamisega.

Funktsionaalses keeles näeb imperatiivset paradigmat ahviv programm muidugi välja üsna teist-moodi kui imperatiivses keeles. Taoline stiil koodi loetavust vaevalt et parandab. Seepärast kuimuud eesmärki lisaargumentidega arvutamisel pole, siis onparem kirjutada kood otserekursiiv-sena või kombineerides juba defineeritud operaatoreid.

Loendurid

Lihtsamal juhul on salvestava argumendi ülesandeks millegi loendamine; nimetame sellist para-meetritloenduriks.

Näiteks oli meil koodiga (71) defineeritud muutujanullideArv väärtuseks funktsioon, misarvulisti järgi leidis tema nulliga võrduvate elementide arvu. Kood (71) on otserekursiivne. Kuidsama eesmärgini võib jõuda ka loenduriga.

Võtame kasutusele lokaalse operaatoriarv , millel lisaks listiargumendile on üks arvuline argu-ment, loendurn. Algsel väljakutsel anname listiargumendiks sama listi, mis tuleb argumendikspõhioperaatorile, lisaargumendiks aga nulli. Igal rekursiivsel pöördumisel anname selle argu-mendi edasi kas1 võrra suurendatuna, kui listist loeti0, või muutumatuna, kui listist loeti midagimuud. Listiargumendiks anname jooksva listi saba, sest peaon ära töödeldud. On selge, et nii-moodi tehes on lisaargumendi väärtus kogu aeg võrdne senileitud nullide arvuga, st ta toimibloendurina, ja kui list läbi saab, on seal parajasti nullidearv kogu listis. Seetõttu anname lõpuslihtsalt selle argumendi välja. Definitsioon, mis selle algoritmi realiseerib, on

nullideArv xs= let

arv n (z : zs)= arv ( if z == 0 then n + 1 else n) zs

arv n _= n

inarv 0 xs

. (97)

Lokaalse operaatoriarv definitsioon koodis (97) on näide nn sabarekursioonist.Sabarekur-

siivseks (ingl tail recursion) nimetatakse funktsiooni rekursiivset definitsiooni, mille järgi

117

Page 118: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

arvutades on rekursiivsest väljakutsest tagasipöördumine alati viimaseks operatsiooniks ka sel-lel rekursioonitasemel, kuhu tagasi pöörduti, nii et rekursiivse pöördumise tulemusega ei tehtamuud kui tagastatakse omakorda eelmisele tasemele. Lokaalsearv definitsioonis on ainus re-kursiivne pöördumine esimese deklaratsiooni paremas pooles ja pöördumise tulemus on ühtlasitagastatavaks väärtuseks. Võrdluseks näiteks muutujanullideArv definitsioon koodis (71)pole sabarekursiivne, sest kui argumentlisti pea on null, siis liidetakse rekursiivse pöördumisetulemusele veel1.

Sabarekursiooni tähtsus seisneb selles, et selliseid definitsioone on võimalik transleerimisel op-timeerida, sest kui rekursiivse pöördumisega on jooksva rekursioonitaseme töö sisuliselt lõppe-nud, võib rekursiivsel pöördumisel jooksva taseme lokaalsete muutujate väärtused unustada võiõigemini järgmise rekursioonitaseme väärtustega üle kirjutada. Nii tehes tekib funktsionaalsestrekursiivsest definitsioonist, nagu näiteks (97), imperatiivne tsükliga algoritm.

Veel ühe näitena defineerime muutujabilanss väärtuseks funktsiooni, mis võtab argumendiksstringi ja annab tulemuseks tõeväärtuse vastavalt sellele, kas ümarsulgude bilanss selles klapibvõi mitte. Võtame kasutusele lokaalse samanimelise abioperaatori lisaargumendigan, mille üles-andeks on stringi lugemise käigus meeles pidada jooksva asukoha sügavust sulgude suhtes ehkloendada alustatud, kuid lõpetamata sulupaare. Sulgude bilanss klapib parajasti siis, kui strin-gi lõpus on sügavus0 ja üheski kohas ei kipu ta negatiivseks (st lõpetavaid sulgerohkem kuiavavaid). Sobib kood

bilanss:: String -> Bool

bilanss str= let

bilanss n (c : cs)= let

d = case c of’( ’-> 1

’) ’-> -1

_-> 0

inn >= 0 && bilanss (n + d) cs

bilanss n _= n == 0

inbilanss 0 str

. (98)

Vaatamata sellele, et lokaalse definitsiooni rekursiivne pöördumine on argumendipositsioonis,võib koodi (98) definitsiooni lugeda sabarekursiivseks, sest operaatori&& teine argument väär-

118

Page 119: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

tustatakse ainult juhul, kui esimese tõesus on juba kinnitust leidnud, ja sellisel juhul tagastab kon-junktsiooni arvutus teise argumendi muutmata kujul. Kuna Haskellis on ka konstruktorid laisaväärtustamisega, on analoogselt võimalik õigustatult sabarekursiivseks lugeda ka definitsioonid(75), (77), (88), (92), (95).

Ülesandeid

221. Defineerida muutujamadal väärtuseks predikaat, mis võtab argumendiks arvude listi jaannab väärtuseksTrue, kui ükski element seal pole negatiivne ega suurem oma positsioo-ninumbrist (loeme alates0-st), muiduFalse.

222. Defineerida muutujadropÜksus väärtuseks funktsioon, mis võtab argumendiks kakssümbolit ja stringi ja annab tulemuseks stringi lõpuosa, mis järgneb esimesele sellisele ko-hale, kus teist sümbolit on esinenud rohkem kui esimest; kuiniisugust kohta pole, siis peabarvutus lõppema veateatega.

223. Selgitada, miks definitsioonid (62), (66), (79) pole sabarekursiivsed.

Järjehoidjad

Laiemalt vaadates on koodis (98) loenduri ülesandeks tööjärje meelespidamine. Nimetame sellistparameetritjärjehoidjaks. Üldisemalt võib järjehoidja olla ka selline parameeter, millel loen-damisega mingit pistmist ei ole.

Pöördume tagasi muutujakaheksLoe defineerimise ülesande juurde, millele oleme näinud ju-ba kolme lahendust: topeltsammudega (78), argumentide vahetamisega (79) ning vastastikrekur-siooniga (80). Et probleemiks on meeles pidada, kumba listituleb panna järjekordne element, onjärjehoidja kasutamine üks loomulikumaid lähenemisi.

Kuna vajalik lisainfo koosneb valikust kahe võimaluse vahel, siis sobib tõeväärtustüüpi järje-hoidja. Selle lisaparameetri sissetoomiseks kirjutame lokaalse abifunktsiooni. Nii saame koodi

kaheksLoe xs= let

kaheksLoe b (x : xs)= let

(us , vs)= kaheksLoe (not b) xs

inif b then (us , x : vs) else (x : us , vs)

kaheksLoe _ _= ([] , [])

inkaheksLoe False xs

. (99)

119

Page 120: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Siin lokaalse operaatorikaheksLoe esimene argumentb on järjehoidja. Tema väärtuseFalsekorral lisatakse jooksev element vasakusse listi, väärtuse True korral paremasse listi. Pangemtähele, kuidas igal rekursiivsel pöördumisel muudetakse tõeväärtusparameetri väärtus vastupidi-seks. See asendab funktsioonide vahel hüppamist vastastikrekursiooni puhul ja paari komponen-tide vahetust definitsiooni (79) puhul.

Teise näitena olgu meil vaja defineerida muutujanullsummadeArv väärtuseks funktsioon,mis võtab argumendiks listi ja annab tulemuseks tema mittetühjade algusjuppide arvu, mis an-navad elementide summaks nulli. Signatuur peaks olema

nullsummadeArv:: (Num a)=> [a] -> Int

.

Spetsifikatsioon on sarnane muutujanullideArv omaga, kus loeti listis elemendina esinevaidnulle. Nüüd on vaja listi algusest järjest arve kokku liita ja lugeda nulliga võrduvaid vahesum-masid. Teeme ümbernullideArv loendurit kasutava koodi (97), lisades lokaalsele operaato-rile veel ühe lisaparameetris , mis hoiab jooksvat summat. Uue summa, kus ka listi järgmineelement on sisse arvestatud, seome lokaalse muutujagas’ . Lihtsuse mõttes kasutame valvuri-konstruktsiooni asemel tingimusavaldist. Saame koodi

nullsummadeArv xs= let

arv s n (z : zs)= let

s’= s + z

inarv s’ ( if s’ == 0 then n + 1 else n) zs

arv _ n _= n

inarv 0 0 xs

, (100)

kus lokaalse operaatoriarv definitsioon on jällegi sabarekursiivne.

Sarnase näitena olgu meil vaja defineerida muutujamaxArv väärtuseks funktsioon, mis võtabargumendiks listi ja annab tulemuseks tema maksimaalsete elementide arvu. Ka see on tüüpilineolukord, kus lisaparameetrite kasutamine on loomulik. Muidugi on võimalik programmeeridakood, mis kõigepealt vaatab järele, milline element on maksimaalne, ja seejärel loeb nad kokku,kuid see tähendaks listi kahekordset läbivaatamist.

Kuna tühjal listil maksimaalsest elemendist rääkida ei saa, siis tuleb see juht eraldi läbi vaadata.

120

Page 121: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Lokaalse lisaargumentidega operaatori peale läheme vaid mittetühja listi puhul. Koodis

maxArv:: (Ord a)=> [a] -> Int

maxArv (x : xs)= let

arv m i (z : zs)= case compare m z of

GT-> arv m i zs

EQ-> arv m (i + 1) zs

_-> arv z 1 zs

arv _ i _= i

inarv x 1 xs

maxArv _= 0

, (101)

on lokaalsel operaatoril kaks lisaargumentim ja i : järjehoidjasmon jooksev maksimaalne ele-ment, i loendab nende esinemisi. Kolmandaks argumendiks on läbivaatamata listiosa. Lugejavõib juhte läbi mõeldes iseseisvalt veenduda, et argumentide tähendus püsib sellisena läbi koguarvutusprotsessi. Seetõttu on lõpuks, kui järelejäänud listiosa on tühi, otsitav väärtus muutujasi , mis ka välja antakse.

Lokaalse operaatoriarv definitsioon koodis (101) on järjekordne näide sabarekursioonist: kõikrekursiivsed väljakutsed, olgugi et eri alternatiivides,on viimased operatsioonid, mida antudrekursioonitasemel tehakse.

Järjehoidjaid on tihti otstarbekas kasutada ka juhul, kui eesmärgiks on defineerida mõni protse-duur või list. Nagu juba mainitud, on taolised argumendid oma iseloomult sarnased neile, midanägime lõpmatute listide definitsioonides. Parameetern Collatzi jadu defineerivas koodis (95)on ehtne järjehoidja, lihtsalt see polnud seal programmeerija omalooming, vaid oli juba ülesandepüstituses sees.

Järgnevas näites abijärjehoidja kasutamisest anname muutuja kuulaKorduseni väärtuseksprotseduuri, mis kuulab standardsisendist sümboleid, kuni sisestatakse mingit sümbolit kaks kor-da järjest, ja lõpetab siis töö.

Siin on põhiprobleemiks, kuidas osata otsustada, kas sümbol on eelmisega võrdne või mitte. Igasümbolit tuleb meeles hoida, kuni järgmine sümbol on sisse loetud. Toome sisse lokaalse operaa-tori kuula , mis saab argumendiks viimati loetud sümboli ja kuulab sealt edasi. Selle operaatori

121

Page 122: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

poole pöördumiseks peab üks sümbol olema juba kuulatud, seetuleb teha põhiprotseduuris. Väljatuleb kood

kuulaKorduseni:: IO Char

kuulaKorduseni= let

kuula d= do

c <- getCharif c == d

then putStrLn ""else kuula c

indo

hSetBuffering stdin NoBufferingc <- getCharkuula c

. (102)

Lisaargument on hädavajalik, kui on vaja potentsiaalselt lõpmatul listil otsida sobivas suhteselemente. Olgu meil näiteks vaja mistahes listi kohta kontrollida, ega temas ei ole korduvaidelemente; ootame definitsiooni vastavalt signatuurile

kordusteta:: (Eq a)=> [a] -> Bool

.

Otserekursiooniga saaks programmeerija kirjutada definitsiooni

kordusteta (x : xs)= not (elem x xs) && kordusteta xs

kordusteta _= True

. (103)

Kui argumentlist on tühi, tuleb väärtuseksTrue, mis on õige, sest tühjas listis ükski element eikordu. Mittetühja listi puhul peab elementide mittekorduvuseks olema täidetud parajasti kakstingimust: esimene element ei tohi esineda järgmiste hulgas — seda kontrollib parajasti koo-diosanot (elem x xs) — ja nende järgmiste elementide hulgas ei esine kordumisi — sedakontrollib koodiosakordusteta xs rekursiivselt. Paraku lõpmatul listil, milles esineb kor-dusi, kuid esimene element on unikaalne, selle definitsiooni järgi arvutus kordusi üles ei leia, sestpüüab kõigepealt esimest elementi kõigi järgnevatega võrrelda ja jääb tsüklisse.

Algoritm, mis siinkohal aitaks, peaks tegema võrdlusi teises järjekorras. Iga elementi tuleks võr-relda temast eespool olevatega, mitte tagapool olevatega nagu teeb (103). Igale elemendile eelneb

122

Page 123: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

listis vaid lõplik arv elemente ja seetõttu jõuaks niisugune algoritm suvalise kahe elemendi võrd-lemiseni lõpliku arvu võrdluste järel.

Niisuguse tööskeemi korral oleks listi iga elemendini jõudmisel kõik eelnevad juba omavahel läbivõrreldud. Võtame kasutusele lisaargumendi, millesse salvestame kõik läbivaadatud elemendid.Definitsioonis

kordusteta xs= let

kordusteta as (z : zs)= not (elem z as) && kordusteta (z : as) zs

kordusteta _ _= True

inkordusteta [] xs

(104)

on selleks lisaargumendiksas . Algul pole ühtki elementi analüüsitud, mistõttu lokaalseope-raatori algsel väljakutsel anname selleks argumendiks tühja listi. Töö käigus võrreldakse igajärgnevat elementi parajasti kõigi salvestatud elementidega ning kui ta osutub neist kõigist eri-nevaks, lisatakse temagi järgneval rekursiivsel pöördumisel abilisti. Arvutus selle definitsioonijärgi leiab korduse üles isegi juhul, kui list on osaline.

Pangem tähele, et elemendid salvestuvad abilisti vastupidises järjekorras võrreldes sellega, kui-das nad originaallistis ette tulevad — esimene läheb viimaseks jne. See tuleneb asjaolust, et igajärgmine element salvestatakse listi algusse. Rekursioonis elementide lisamine listi lõppu teekskoodi väga ebaefektiivseks. Antud juhul on elementide järjekorra ümberpöördumine tähtsusetuefekt, kuna abilisti kasutatakse vaid kontrolliks, kas ta sisaldab seda või teist elementi.

Ülesandeid

224. Defineerida muutujamaxKonst väärtuseks funktsioon, mis võtab argumendiks võrdusegatüüpi elementide listi ja annab väärtuseks selle listi pikima konstantse (st võrdsete elemen-tidega) lõigu pikkuse.

225. Defineerida muutujakümnendmurd väärtuseks funktsioon, mis võtab argumendiks ratsio-naalarvuq ja annab väärtuseks paari, mille esimene komponent onq täisosa ja teine kom-ponent on listq murdosa kümnendesituse numbritest nende esinemise järjekorras (lõplikuesituse puhul peab list olema lõplik, lõpmatu esituse puhullõpmatu). Soovitav on kasutadaülesandes 153 või 155 kirjutatud muutujatõigeProperFraction .

226. Defineerida muutujamurdTäpsusega väärtusekscurried-kujul funktsioon, mis võtabargumendiks ratsionaalarvuq ja täisarvun ning annab väärtuseks arvuq kümnendesitusestringikujul, kus on kunin kohta peale koma. Kui arvus on tegelikult rohkem kuin koma-järgset kohta, siis tagumised kohad jätta ära ilma ümardamata. Kui arvus on vähem kuin

123

Page 124: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

komajärgset kohta, siis arvu lõppu nulle mitte kirjutada. Kui tulemusstringis komajärgseidkohti pole, siis jätta ära ka koma. Negatiivsen korral ei pea töötama.

227. Defineerida muutujakuula01 väärtuseks protseduur, mis loeb standardsisendist sümbo-leid, kuni sealt tulevad järjest sümbolid 0 ja 1, ja lõpetab siis töö, edastades stringi, milleson õiges järjekorras kõik loetud sümbolid.

228. Defineerida muutujakuulaKorduseni’ väärtuseks protseduur, mis kuulab standardsi-sendist sümboleid, kuni mingi sümbol kordub (mitte tingimata järjest) ja lõpetab siis töö.

229. Kui definitsioonis (104) konjunktsiooni pooled ära vahetada, kas definitsioon muutuks hal-vemaks, paremaks või pole vahet?

230. Defineerida muutujalõplikEsitus väärtuseks funktsioon, mis võtab argumendiks rat-sionaalarvu ja annab tulemuseksTrue, kui tema kümnendesitus on lõplik, jaFalse, kui seeon lõpmatu.

231. Cantori tolm on reaalarvude hulk, kuhu kuuluvad parajasti need reaalarvud, mille murd-osa kolmendesituses ei esine numbrit1 (esitused, mis lõpevad2-ga perioodis, ei tule arves-se). Defineerida muutujacantoriKübe väärtuseks funktsioon, mis võtab argumendiksratsionaalarvuq ja annab väärtuseksTrue või False vastavalt sellele, kasq kuulub Cantoritolmu hulka või mitte.

232. Defineerida muutujadisjunktsed väärtusekscurried-kujuline funktsioon, mis võtabargumendiks kaks listi ja kui nad on lõplikud või lõpmatud jasisaldavad ühiseid elemente,siis annab välja tõeväärtuseFalse, kui aga mõlemad listid on lõplikud ja ühiseid elementeei sisalda, annab väljaTrue. Kuidas see funktsioon töötab, kui üks listidest on osaline?

233. Defineerida muutujauuriKolmikud väärtuseks funktsioon, mis võtab argumendiks täis-arvulisti ja annab tulemuseksTrue, kui listis leidub kolm erinevas positsioonis elementi,mis moodustavad aritmeetilise progressiooni, vastasel korral kui list on lõplik, siis annabvälja False.

Akumulaatorid

Akumulaator (ingl accumulator) on rekursiivse funktsiooni argument, mis arvutuse käiguskogub endasse infot, akumuleerib seda. See tähendab, et rekursiivsel pöördumisel lisatakse sinnaargumenti midagi juurde võrreldes tema varasema väärtusega.

Akumulaatori ja järjehoidja vahel pole kindlat piiri. Siinloeme akumulaatoriteks eelkõige sel-liseid akumuleerivaid argumente, millest saame lõpus kätte väljaantava väärtuse (kas vahetultvõi mõne teisenduse rakendamisel). Järjehoidjateks nimetame pigem selliseid, mis on tarvilikudarvutuse käigus, kuid lõpus visatakse minema. Näiteks muutuja kordusteta definitsioonis(104) on abioperaatori argumentas küll akumuleeriv, kuid omab tähtsust ainult arvutuse käigus.

124

Page 125: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Loendurid võivad olla mõlemat laadi, näiteks koodi (97) puhul kujuneb just loenduris lõpuksväljaantav tulemus.

Vaatame näiteks, kuidas võiks programmeerida listi ümberpööramise. Vastav funktsioon on moo-dulisPrelude muutujareverse väärtuseks; defineerime siin ta muutujassetagurpidi , sig-natuur on

tagurpidi:: [a] -> [a]

.

Otserekursiivselt tehes saame definitsiooni

tagurpidi (x : xs)= tagurpidi xs ++ [x]

tagurpidi _= []

. (105)

Arvutades selle järgi, tehakse rekursiooni igal vahesammul rekursiivne pöördumine listi sabal jaselle tulemus konkateneeritakse listi pea ette. See annab kokku ruutkeerukusega arvutuse, sestkui tähistadan-ga argumentlisti pikkust, siis protsessi käigus vasakultkonkateneeritavate listidepikkused on0, 1, . . . , n − 1, mille summa on ruutpolünoomn suhtes.

Vaatame aga definitsiooni

tagurpidi xs= let

tagurpidi (x : xs) as= tagurpidi xs (x : as)

tagurpidi _ as= as

intagurpidi xs []

; (106)

selle järgi arvutades kirjutatakse elemendid ükshaaval järjest akumulaatorisseas . Kuna elemen-did salvestuvad akumulaatorisse algsega võrreldes vastupidises järjestuses, nagu nägime jubakordusteta definitsiooni (104) juures, siis lõpuks ongi akumulaatori väärtuseks just ümber-pööratud esialgne list. Seejuures on arvutus listi pikkusesuhtes lineaarse keerukusega, sest igalrekursioonisammul tõstetakse ainult üks element ringi. Seepärast on siin akumulaatori kasutami-ne väga oluline.

Ülesandeid

234. Defineerime muutujasabad koodiga

sabad:: [a] -> [[a]]

sabad xs= reverse (tails xs)

,

125

Page 126: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

st tema väärtuseks on funktsioon, mis võtab argumendiks listi l ja kui see on lõplik, siisannab tulemuseks listi, milles onl kõik lõpuosad pikkuse kasvamise järjekorras. Selle de-finitsiooni järgi arvutades moodustub vahestruktuur — algse listi alamlistide list pikkusekahanemise järjekorras. Kirjutada muutujalesabad samaväärne definitsioon, mille järgiarvutades vahestruktuure ei teki.

235. Defineerida muutujakahend väärtuseks funktsioon, mis võtab argumendiks täisarvun jakui see on mittenegatiivne, siis annab tulemuseks listina arvu n kahendesituse numbrid ilmaalgusnullideta tavalises järjekorras. Negatiivsel argumendil väärtustamine peab lõppemaadekvaatse veateatega. Vältida lististruktuuri korduvatehitamist.

236. Defineerida muutujamaxJärjest väärtuseks funktsioon, mis võtab argumendiks võrdu-sega tüüpi elementide listil ja annab tulemuseks listi, mis koosnebl neist elementidest,mida listis kõige rohkem järjest esineb, leitud maksimumpikkusega lõikude esinemise jär-jekorras.

Arvutamine akumulaatoris võib muidugi olla keerulisem kuimingi listi koostamine. Akumulaa-toriga võib rekursiivsel väljakutsel teha mingi suuremamahulise operatsiooni; siis akumulaatorkui infokoguja tähendab arvutusoperatsioonide kogujat.

Esimene sedasorti näide on juba ülal vaadeldud definitsioon(88), mis kirjeldas geomeetrilisejada sõltuvalt esimest elemendist ja tegurist. Argumenta käitub akumulaatorina, sest igal uuelrekursiivsel väljakutsel lisandub sinna üks korrutamine teguriga.

Listide defineerimine akumulaatori abil on tihti lihtsam kui listirekursiooniga defineerimine. Ol-gu meil vaja koostada list, milles on järjest kõik arvud kujul 14 + . . . + n4, kusn muutub ülekõigi naturaalarvude; defineerime ta muutujasuuredSummad väärtuseks. Võttes jooksva sum-ma salvestamise jaoks kasutusele akumulaatoria ja liidetava järjekorranumbri jaoks loendur-järjehoidjai , saame koodi

suuredSummad:: (Integral a)=> [a]

suuredSummad= let

suuredSummad a i= a : suuredSummad (a + i ^ 4) (i + 1)

insuuredSummad 0 1

. (107)

Arvutamine definitsiooni (107) järgi toimub üsna sarnaseltimperatiivse programmeerimise tsük-lile, loenduri on sisuliselt tsükliindeks. Algselt on akumulaatori väärtus0 ja tsükliindeksi väär-tus1. Igal rekursiivsel pöördumisel kirjutatakse akumulaatori jooksev väärtus listi, akumulaato-risse aga lisatakse jooksva tsükliindeksi4. astme liitmine ja tsükliindeksile liidetakse1. Niimoo-

126

Page 127: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

di tekivad akumulaatorisse14 liitmine, 24 liitmine jne ning tulemuslist moodustub nende tehetetulemustest.

Et sama listirekursiooniga teha, tuleks kasutada abifunktsiooni, mille kaudu saaks avaldada listisaba terve listi kaudu. Siin pole see seos kõige lihtsam.

Deklaratsiooniga (62) defineeritud funktsioonisuurSumma saab nüüd samaväärselt defineeridaselle lõpmatu listi kaudu deklaratsiooniga

suurSumma n| n >= 0

= suuredSummad !! n| otherwise

= error "suurSumma: negatiivne liidetavate arv "

. (108)

Definitsiooni (108) järgi arvutamine on esimesel korral pisut ebaefektiivsem kui varasema defi-nitsiooni järgi. Vahe tuleb esiteks sellest, et andmestruktuuri moodustamisele kulub lisaressurss,teiseks sellest, et lõpus lisandub listist lugemine. Kui aga mingi arvutuse käigus on vaja arvekujul 14 + . . . + n4 paljude erin-de jaoks, tasub neist listi moodustamine kindlasti ära. Siis igaljärgmisel korral on vaja arvutada ainult neid summasid, mida seni veel arvutatud ja listi salves-tatud pole.

Täiesti analoogiliselt võime näiteks kirjeldada listi, milles on järjekorras kõik naturaalarvude ruu-dud, kasutades tuntud asjaolu, et need on saadavad järjestikuste paaritute arvude liitmisel. Võttesargumendia jooksva täisruudu hoidmiseks ja argumendid liidetava paaritu arvu hoidmiseks,saame koodi

sqrs:: (Integral a)=> [a]

sqrs= let

sqrs a d= a : sqrs (a + d) (d + 2)

insqrs 0 1

. (109)

Sama listi kirjeldasime ka listikomprehensiooni abil koodiga (42). Kui võrrelda arvutuskäiku, siiskood (109) on mõnevõrra efektiivsem, sest listikomprehensiooniga lahenduse puhul on kasutuselvaheandmestruktuur — kõigi naturaalarvude list —, pealegikasutab kood (109) ainult liitmisi,samas kui (42) nõuab korrutamist.

Niisama lihtne on akumulaatoritehnika abil kirjeldada lõpmatu list kõigist Fibonacci arvudest.Kuna järjekordse Fibonacci jada liikme arvutamisel pole vaja teada seda, mitmes liige see on,saab hakkama tsükliindeksita. Et aga iga elemendi arvutamiseks on vaja kaht eelmist, on vaja

127

Page 128: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kaht akumulaatorit, mis tähistavad jada kaht jooksvat elementi. Koodiks sobib

fibs= let

fibs a b= a : fibs b (a + b)

infibs 0 1

. (110)

Siin akumulaatora hoiab seda Fibonacci arvu, mis hetkel tuleks listi kirjutada, akumulaatorbjärgmist. Igal rekursioonisammul nihkub teine argument esimese kohale ja uueks teiseks argu-mendiks tuleb argumentide summa ehk järjekorras järgmine Fibonacci arv.

Nagu samaväärne listirekursiooniga definitsioon (93), annab ka akumulaatoritega definitsioon(110) lineaarses ajas arvutuse.

Ülesandeid

237. Defineerida muutujafacts väärtuseks list, mille elementideks on järjest kõigi naturaalar-vude faktoriaalid. Listi algusosa väljaarvutamine olgu lineaarse keerukusega arvutatud osapikkuse suhtes.

238. Defineerida muutujakolmnurgad väärtuseks list, mille elementideks on järjest kõik

kolmnurkarvud (ingl triangular number) ehk binoomkordajad

(

n + 1

2

)

, n ∈ N.

239. Lahendada ülesanded 212 ja 213 akumulaatoritehnikaga.

Kui oleme andnud akumulaatoritega definitsiooni lõpmatulelistile, on kerge vaevaga võimaliksee definitsioon ümber töötada nii, et ta arvutaks mingit konkreetset listi elementi ilma listi ennastkoostamata. Selleks tuleb ära jätta akumulaatori salvestamine listi, lisada aga lõpetamistingimus.Muus osas jääb senine kood muutmata, nii et arvutuspõhimõtesäilib.

Näiteks võime deklaratsiooni (107) eeskujul anda koodiga (108) defineeritud muutujale

128

Page 129: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

suurSumma otsese definitsiooni, mis vahelisti ei kasuta. Sobib kood

suurSumma n| n >= 0

= letsuurSumma a i

| i <= n= suurSumma (a + i ^ 4) (i + 1)

| otherwise= a

insuurSumma 0 1

| otherwise= error "suurSumma: negatiivne liidetavate arv "

. (111)

Võrreldes definitsiooniga (107) on lisandunud väline parameetern, mis näitab otsitava summajärjekorranumbrit, ja tema negatiivse väärtuse käsitlemine eraldi; listi koostamine on kadunud,kuid lisandunud on hargnemine lokaalses deklaratsioonis,kus juhul, kui tsükliindeksi väärtusületabn oma, arvutus lõpetatakse ja antakse akumulaator välja.

Muutujafib saame akumulaatoritega, kuid ilma vahestruktuurideta defineerida koodiga

fib n| n >= 0

= letfib a _ 0

= afib a b i

= fib b (a + b) (i - 1)infib 0 1 n

| otherwise= ( if odd n then 1 else -1) * fib (-n)

. (112)

Võrreldes Fibonacci arvude listi definitsiooniga (110) on siin toimunud analoogilised muudatu-sed nagu eelmises näites. Peale nende on lokaalsesse definitsiooni lisandunud tsükliindeks, millejärgi otsustatakse, kas töö lõpetada. Tsükliindeksi muutumise suund on siin vastupidine definit-siooniga (111); selline valik on tehtud vaid programmeerimise mugavuse pärast.

Arvutus definitsiooni (112) järgi toimub lineaarse keerukusega argumendi suhtes. See käib isegipisut kiiremini kui definitsiooni (67) korral, sest definitsiooni (67) puhul konstrueeritakse igalrekursioonisammul paar, mis nõuab lisaressurssi nii nagu listi moodustaminegi. Koodi (112)eelis arvutuskiiruses koodiga (67) võrreldes on siiski vaid marginaalne.

Ülesandeid

129

Page 130: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

240. Anda koodiga (64) defineeritud muutujalekahFact samaväärne definitsioon, mille järgitoimuks arvutamine akumulaatoris. Saada läbi kahe parameetriga.

241. Kirjutada koodiga (70) defineeritud muutujalesq2 uus samaväärne definitsioon, mille kor-ral arvutus toimub akumulaatorites ja vahestruktuure ei moodustata.

242. Kirjutada ülesande 181 uus lahendus, mille korral arvutus toimub akumulaatorites ja vahe-struktuure ei moodustata.

“Jaga ja valitse” tehnika

“Jaga ja valitse” (ingl divide and conquer) tehnika puhul jagatakse mittetriviaalne ülesan-ne igal rekursioonisammul kaheks või enamaks sama tüüpi alamülesandeks, mis lahendatakserekursiivselt samal põhimõttel ja mille lahendustest kombineeritakse kokku terve ülesande la-hendus. Triviaalsed juhud lahendatakse otse, ilma jagamata.

Mõnikord tuleneb selline lahendusviis loomuldasa ülesande püstitusest, mõnikord aga on seetaktika otstarbekas efektiivsuskaalutlustel. Kuid “jagaja valitse” tehnikaga lahenduse keerukussõltub paljudest asjaoludest ja alati selline arvutus kõige efektiivsem ei ole.

Järgnevalt on “jaga ja valitse” tehnika klassifitseeriminetehtud selle järgi, kas alamülesannetest,milleks ülesanne jaotatakse, on üks või rohkem mittetriviaalsed. Ainult ühe mittetriviaalse alam-ülesande puhul sarnaneb programmeerimine seninähtuga, sest samal rekursioonitasemel toimubigal juhul ülimalt üks rekursiivne pöördumine. Koodi koostamisel on aga vaja “jaga ja valitse”tüüpi mõtlemist.

Ühe mittetriviaalse alamülesandega olukorrad

Tüüpnäited “jaga ja valitse” tehnika kasutamisest, kus alati vaid üks alamülesannetest on mittetri-viaalne, on igasugused kahendotsingud.Kahendotsingu (ingl binary search) puhul jagatakseotsinguruum kaheks mingis mõttes võrdseks osaks nii, et otsitav objekt saab olla ainult ühes neist,ja edasi toimitakse samamoodi selles osas. Niisuguse protsessi keerukus on logaritmiline algseotsinguruumi suuruse suhtes. Kahendotsingut, kus otsinguruum moodustab lõigu arvteljel ja igalsammul jagatakse lõik enam-vähem keskelt pooleks, nimetatakselõigu poolitamise meetodiks.

Olgu meil vaja muutujatäisruut väärtuseks saada funktsiooni, mis võtab argumendiks täis-arvu tüübistInteger ja annab tulemuseks tõeväärtuse vastavalt sellele, kas seearv on täisruut.Kuna ülipikkade arvude esitus ujukomaga on nii ebatäpne, etantud küsimus muutub mõttetuks,jäävad kõrvale variandid muutujasqrt kasutamisega. Kuna aga ruutfunktsioon on monotoonne,on võimalik kasutada lõigu poolitamise meetodit.

130

Page 131: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Selguse mõttes kodeerime kahendotsimise protsessi põhifunktsiooni definitsioonist eraldi. Põhi-funktsiooni kood

täisruut:: Integer -> Bool

täisruut n| n > 1

= lähenda n (algpiirid n)| otherwise

= n >= 0

(113)

pöördub muutujatelähenda ja algpiirid poole. Neist esimese rakendamine peab reali-seerima otsinguprotsessi, teise rakendamine aga andma kätte lõigu, millest otsing peale võikshakata.

Nende muutujate definitsioonid järgnevad all. Nagu definitsioonist (113) näha, kasutatakse ka-hendotsimist ainult juhul, kui argument on1-st suurem. Muidu saab vastuse kohe välja kirjutada:argument on täisruut parajasti siis, kui ta on mittenegatiivne.

Realiseerime lõigu poolitamise protsessi nii, et jooksva lõigu alumine otspunkt saab ise ollatäpne ruutjuur, kuid ülemine otspunkt mitte. Kasutades deklaratsiooniga (33) defineeritud ruutu-tõstmisfunktsioonisqr , annab soovitava protsessi meile kood

lähenda:: Integer -> (Integer , Integer) -> Bool

lähenda n (a , b)| b - a <= 1

= sqr a == n| otherwise

= letm = (a + b) ‘div ‘ 2

incase compare (sqr m) n of

LT-> lähenda n (m , b)

GT-> lähenda n (a , m)

_-> True

.

Arvutus töötab üldjoontes järgmiselt. Kui lõigu pikkus on1, siis on uuritav arv täisruut parajastijuhul, kui ta on alumise otspunkti ruut, sest ülemine otspunkt on protsessi realisatsioonist tulene-valt ruutjuurest suurem. Kui lõigu pikkus on suurem kui1, siis leitakse lõigu seest täisarv, mis onvõimalikult lõigu keskel, tehakse tema ruututõstmise teelkindlaks, kummale poole temast jääb

131

Page 132: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

uuritava arvu ruutjuur, ning pöördutakse rekursiivselt uue lõiguga. Võib ka juhtuda, et tema ongiruutjuur — sellisel juhul on uuritav arv täisruut ja funktsioon annab väljaTrue.

Igal rekursiivsel pöördumisel on tagatud, et uus otspunkt ei ole uuritava arvu täpne ruutjuur.Seega jääb kehtima invariant, et jooksva lõigu ülemine otspunkt ei ole täpne ruutjuur. Kui min-gil rekursioonitasemel on lõigu pikkus1-st suurem, siis järgmisel väljakutsel on lõik kindlastilühem, mistõttu protsess varem või hiljem jõuab olukorda, kus lõigu pikkus on1, ja lõpetab töö.

Jääb defineerida veel algset lõiku leidev muutujaalgpiirid . Ka algne lõik peab respektee-rima invarianti, et ülemine otspunkt peab olema uuritava arvu ruutjuurest suurem, alumine agaruutjuurest väiksem või võrdne temaga. Sobib näiteks kood

algpiirid:: Integer -> (Integer , Integer)

algpiirid n= (1 , n)

. (114)

Kuna seda funktsiooni arvutatakse vaid1-st suurema argumendiga, siis on argumendi ruutjuurtõepoolest argumendist väiksem ning1 omakorda ruutjuurest väiksem.

Et alglõik sisaldab algparameetriga võrdse arvu arve, siisarvu n täisruuduks olemise kontrollseniste definitsioonide järgi on logaritmilise keerukusegan suhtes.

Alguslõiku saab aga ka kavalamalt määrata. On ju selge, etn on üldiselt liiga jäme hinnang ar-vule

√n. Selle asemel võib kasutada ka algpiiride leidmiseks kahendotsingut. Alustame lõigust

[1; 2] ning igal järgmisel sammul valime jooksvast kaks korda pikema lõigu, mille alumine ots-punkt langeb kokku jooksva lõigu ülemise otspunktiga. Nii ilmuvad lõigud[1; 2], [2; 4], [4; 8]jne. Ruutjuure otsingu alglõiguks võtame selles protsessis esimese, mis hõlmab uuritava arvuruutjuure oma sisepiirkonnas või alumise otspunktiga. Selle idee realiseerib definitsioon

algpiirid n= let

piirid x| sqr y > n

= (x , y)| otherwise

= piirid ywhere

y = x + xinpiirid 1

. (115)

Abimuutujapiirid argumendi väärtus on jooksva lõigu alguspunkt.

On ilmne, et selles protsessis sobiva lõiguni jõudmiseks kuluvate sammude arv on logaritmilinelõpplõigu pikkuse suhtes. Kuna lõpplõigu pikkus ei ületa meie ruutjuurt, siis on nii see otsing

132

Page 133: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kui ka järgnev lõigusisene otsing logaritmilise keerukusega selle ruutjuure suhtes. See on agakokkuvõttes sama mis logaritmilisus algparameetri suhtes, nii et olulist ajavõitu me selle opti-meeringuga ei saa.

Ka arvutust definitsiooni (115) järgi võib tinglikult nimetada lõigu poolitamiseks, sest vaadeldeskõigi otspunktide asemel nende pöördväärtusi, saame parajasti tavalise lõigu poolitamise prot-sessi, kus üheks otspunktiks on kogu aeg0.

Ülesandeid

243. Defineerida muutujakaheAste väärtuseks funktsioon, mis võtab argumendiks täisarvuntüübistInteger ja annab välja tõeväärtuse vastavalt sellele, kasn on2 aste või mitte.

244. Ülesandes 238 defineeriti kolmnurkarv. Defineerida muutuja kolmnurkne väärtuseksfunktsioon, mis võtab argumendiks täisarvun tüübistInteger ja annab välja tõeväärtusevastavalt sellele, kasn on kolmnurkarv või mitte.

245. Defineerida muutujaintSqrt väärtuseks funktsioon, mis võtab argumendiks täisarvuntüübistInteger: mittenegatiivsen korral annab tulemuseksn ruutjuure täisosa, negatiivsen puhul lõpetab töö spetsiaalse täitmisaegse veaga.

246. Defineerida muutujaintLog väärtusekscurried-kujul funktsioon, mis võtab argumenti-deks täisarvuda ja n: kui log

an eksisteerib, siis annab väärtusekslog

an täisosa, vastasel

korral lõpetab spetsiaalse täitmisaegse veaga.

247. Defineerida muutujacosFix väärtuseks võrrandix = cos x võimalikult täpne ujukomaar-vuga väljendatud lähend.

Mõneti teistsuguse näitena vaatleme ülesannet leida antudlisti suurusjärjestuses kohal paiknevelement. Täpsemalt, tahame kirjeldada funktsiooni, mis võtaks argumendiks täisarvun ja jär-jestusega tüüpi elementide listil: kui n on mittenegatiivne ja väiksem listi pikkusest, siis annabtulemuseks suuruse järjestuses elemendi nrn listis l, vastasel korral annab vea. (Nagu ikka, num-merdame elemente listis alates0-st.) Signatuur võiks olla

suuruseltNr:: (Ord a)=> Int -> [a] -> a

.

Sel ülesandel leidub kaval “jaga ja valitse” tehnikaga lahendus, mille juhatab kätte kahendotsinguidee. Jagame listi kaheks osaks selliselt, et ühes osas on kõik elemendid väiksemad kui teises.Kui esimeses osas on rohkem kuin elementi, siis suuruse järjestuses kohaln olev element jääbesimesse ossa, teise osa heidame lihtsalt kõrvale ja otsimeedasi esimesest samal viisil. Kui agaesimeses osas onn või vähem elementi, siis otsitav element jääb teise ossa, heidame esimese osakõrvale ja otsime edasi teisest.

133

Page 134: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Jääb küsimus, kuidas listi soovitud moel kaheks jagada. Kõige parem oleks muidugi, kui ja-gamisel tekkivad osad oleksid enam-vähem ühepikkused, sest siis kahaneks otsinguala igal re-kursioonisammul umbes kaks korda nagu kahendotsingus ja rekursioonisammude arv oleks listipikkuse suhtes logaritmiline. Kuid võrdsete tükkide saavutamine efektiivselt on problemaatili-ne. Seepärast jagame listi kaheks nii lihtsal viisil kui võimalik: kasutame tema esimest elementilahkmena.

Võtame lahkme järgi jagamise tarvis kasutusele eraldi operaatorikaheksLahuta , sest sedaläheb ka hiljem vaja. Kood

kaheksLahuta:: (Ord a)=> a -> [a] -> ( [a] , [a])

kaheksLahuta x (z : zs)= let

(us , vs)= kaheksLahuta x zs

inif z < x then (z : us , vs) else (us , z : vs)

kaheksLahuta _ _= ([] , [])

(116)

annab tema väärtuseks funktsiooni, mis võtab argumendiks objekti x järjestusega tüübist ja sa-ma tüüpi objektide listil ning annab tulemuseks paari kahest listist, kus ühes on listi l needelemendid, mis onx-st väiksemad, ja teises ülejäänud.

Soovitud operaator on nüüd defineeritav koodiga

suuruseltNr n (x : xs)= let

(us , vs)= kaheksLahuta x xs

l = length usincase compare l n of

GT-> suuruseltNr n us

LT-> suuruseltNr (n - l - 1) vs

_-> x

suuruseltNr _ _= error "suuruseltNr: mõõtmetest väljas "

. (117)

Erinevalt lahenduse ülaltoodud sõnalisele kirjeldusele toimub koodis (117) hargnemine kolmeks,

134

Page 135: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

sest kaheks lahutamisel jääb lahe kummastki listiosast välja ja lisajuht (valikuavaldise kolmas)vastab sellele, kui otsitud kohal suurusjärjestuses on parajasti lahe ise.

Kui igal rekursioonisammul on leitavad listiosad enam-vähem ühepikkused, siis arvutus defi-nitsiooni (117) järgi toimub lineaarse keerukusega listi pikkuse suhtes. Tõepoolest, kaheks la-hutamine nõuab argumentlisti pikkusega proportsionaalsel määral liigutusi, nii et kõigi kaheks

lahutamiste töömahud on proportsionaalsed arvudegal,l

2,

l

4jne, kusl on listi pikkus, ja nende

summa on ligikaudul ·(

1 +1

2+

1

4+ . . .

)

ehk2l.

Kui kaheks lahutamised ei tekita ligilähedaselt võrdse pikkusega osi, siis kulub arvutuseks kauemaega. Eriti halb juht on see, kui igal jaotamisel jääb üks osadest päris tühjaks. Siis kaob otsin-gualast igal rekursioonisammul vaid üks element — lahe ise —ja arvutus on ruutkeerukusega.

Ülesandeid

248. Koodi (117) eeskujul defineerida muutujakaheksSuuruselt väärtuseks funktsioon,mis võtab samasugused argumendid nagusuuruseltNr väärtuseks olev funktsioon, kuidannab tulemuseks listipaari, kus esimeses listis on esimese argumendi näidatav arv argu-mentlisti elemente ja teises listis kõik ülejäänud, selliselt et teise listi elemendid on kõikvähemalt niisama suured kui esimese omad. Kui arvuline argument on negatiivne, olgu tu-lemus sama mis0 korral; kui see argument on suurem argumentlisti pikkusestvõi sellegavõrdne, siis tulgu väärtuseks paar, kus kogu list või sellest elementide ümberpaigutamiselsaadav list on esimene komponent ja teine on tühi.

Mitme mittetriviaalse alamülesandega olukorrad

Vaatame siin tuntud Hanoi tornide ülesande lahendamist, mida imperatiivse keele baasil prog-rammeerimise õpetamisel kasutatakse tihti näitena rekursiooni kasulikkusest. Hanoi tornide üles-anne seisneb järgmises. On antudn erineva läbimõõduga ketast, igaühel auk keskel, ja kolm püs-tist varrast, millest igaüks mahub ketta august läbi. Alguses on kõik kettad esimese varda peal altüles suuruse kahanemise järjekorras. Ühe sammuga on lubatud tõsta suvalise varda pealt väik-seim ketas mõne teise varda peale, millel pole temast väiksemaid kettaid. Ülesandeks on vähimaarvu sammudega saavutada olukord, kus kõik kettad on teise varda peal.

Lahendamaks seda ülesannet arvutiga, tuleb programmeerida ümbertõstmiste jada, mis viib nõu-tud olukorrani. Lahenduse võti peitub tähelepanekus, et suurima ketta ümbertõstmiseks peavadkõik teised olema enne ümber paigutatud, kusjuures nad kõikpeavad olema kolmanda vardaotsas, sest suurimat ketast saab asetada ainult tühja vardaotsa. Pärast suurima ketta nihutamistsuurim ketas enam rolli ei mängi, teised saab tema peale asetada. Seega ülesanne jaguneb järg-misteks alamülesanneteks:n − 1 väiksema ketta ümbertõstmine esimeselt vardalt kolmandale;

135

Page 136: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

suurima ketta tõstmine esimeselt vardalt teisele;n−1 väiksema ketta tõstmine kolmandalt vardaltteisele. Seejuures äärmised alamülesanded on analoogilised tervikuga, keskmine aga triviaalne.

Loeme varraste nimedeks sümbolid A, B, C ja märgime üht ümbertõstmist kahetähelise stringi-ga, mille esimene täht näitab varrast, millelt väikseim ketas tuleb võtta, ning teine täht varrast,kuhu ketas tuleb panna. Sellega on tõstmine üheselt määratud. Defineerime muutujahanoiväärtuseks funktsiooni, mis võtab argumendiks täisarvun ja annab tulemuseks listi vajalikestümbertõstmistest nende tegemise järjekorras.

Võtame appi lokaalse operaatori, millel lisaks vardaid tähistavad kolm lisaparameetrit. Esimeneneist märgib varrast, millelt kettad tuleb ära võtta, teinevarrast, millele kettad tuleb peale saada,ja kolmas ülejäävat varrast. Esimese lähendusena saame koodi

hanoi:: Int -> [String]

hanoi n| n >= 0

= lethanoi 0 _ _ _

= []hanoi n x y z

= letr = n - 1

inhanoi r x z y ++ [x, y] : hanoi r z y x

inhanoi n ’A’ ’B’ ’C’

| otherwise= error "hanoi: negatiivne argument "

. (118)

Abioperaatori definitsioon ütleb, et selleks, et tõstan ketast vardaltx vardaley vardaz kaudu, kusn > 0, tuleb kõigepealt tõstan−1 ketast vardaltx vardalez varday kaudu, siis teha tõste vardaltx vardaley ja lõpuks tõstan− 1 ketast vardaltz vardaley vardax kaudu,0 ketta tõstmiseks agapole vaja ühtki tõstet teha.

Väärtustades näitekshanoi 2 , saame vastuseks listi “AC”: “AB” : “CB” : [], mis ütleb, et2 ket-ta korral tuleb tõsta algul ketas esimeselt vardalt kolmandale, siis ketas esimeselt vardalt teiseleja lõpuks kolmandalt teisele.

Paneme koodi (118) juures tähele, et list konstrueeritaksetsüklilise konkateneerimisega, kus-juures operaatori++ vasakus argumendis on rekursiivne pöördumine. Kuid++ vasak argumentteatavasti kopeeritakse. See tähendab, et tekib mitmekordne kopeerimine: kuna rekursiivse pöör-dumise tulemus arvutatakse samamoodi, siis juba seal toimub kopeerimine jne. Esimesel re-kursioonitasemel kopeeritakse pool kogu loodavast listist, edasi kummaski kahest rekursiivsestpöördumisest kopeeritakse omakorda pool sellest poolest ehk kokku jällegi pool kogu listist,

136

Page 137: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kolmandal sügavustasemel analoogiliselt ka pool kogu listist jne.

Et vältida ebaefektiivsust, mille operaatori++ vasakus argumendis olev rekursiivne pöörduminekaasa toob, kirjutame uue definitsiooni, kus käiku läheb sama nõks mis listide ümberpööramisedefinitsiooni (106) juures: koostame listi akumulaatoris.Saame koodi

hanoi n| n >= 0

= lethanoi 0 _ _ _ as

= ashanoi n x y z as

= letr = n - 1

inhanoi r x z y ( [x, y] : hanoi r z y x as)

inhanoi n ’A’ ’B’ ’C’ []

| otherwise= error "hanoi: negatiivne argument "

. (119)

Koodi (119) korral tühiümbertõstmisi ei toimu, iga elementsatub kohe oma lõplikku asupaika.

Ülesandeid

249. Babababi keeles on kasutusel ainult tähed A ja B. Sõnademoodustamisel kehtivad järg-mised ranged reeglid.

1. A on sõna.

2. Kui u ja v on ühepikkused babababi sõnad, siisu+v′ ja u+v∗ on babababi sõnad, kus

• w′ tähistab sõna, mille saame sõnastw tähtede järjekorra vastupidiseks muut-misel,

• w∗ on sõna, mille saame sõnastw iga tähe väljavahetamisel vastandtähega (stA-de asendamisel B-de ja B-de asendamisel A-dega),

• u + v tähistab sõna, mille saame, kui paarituarvulistele positsioonidele panemejärjekorrasu tähed ja paarisarvulistele järjekorrasv tähed (muutujavahelitiväärtus ülesandest 203).

137

Page 138: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

3. Kõik sõnad on saadavad punktide 1 ja 2 abil.

Defineerida muutujababababi väärtuseks funktsioon, mis võtab argumendiks stringi jaannab tulemuseksTrue või False vastavalt sellele, kas string on babababi keele sõna või ei.

Klassikalised näited “jaga ja valitse” tehnika rakendamisest on listi järjestamine kiir- ja põimi-mismeetodil. Vaatame siin kiirmeetodit, mille signatuur võiks olla

järjestaKiir:: (Ord a)=> [a] -> [a]

.

Kiirmeetodi idee on sama mis suurusjärjestuses kindlal kohal paikneva elemendi otsimisel, mi-da eespool vaatlesime. Nagu sealgi, jaotatakse listi elemendid kahte gruppi nii, et esimeses onkõik elemendid väiksemad teise grupi kõigist elementidest. Kui nüüd kumbki grupp saab järjes-tatud, pole lõpptulemuse saamiseks vaja muud teha kui järjestatud grupid üksteise järele kokkukirjutada.

Seega lahenduse “jagamise” osa on sama ja võib kasutada koodiga (116) defineeritud operaa-torit kaheksLahuta . Erinevused on “valitsemise” osas ning triviaalse juhu — tühja listi —käsitlemises. Tühja listi järjestamisel peab tulemuseks olema sama list. “Valitsemisel” on vajaalamülesannete lahenduseks olevad listid kokku panna — samamoodi oli asi aga Hanoi tornideülesandes. Viimase juures nägime, et seda on kasulik teha akumulaatoris, selleks et iga elementsatuks kohe oma õigesse paika.

Nii saame definitsiooni

järjestaKiir xs= let

kiir (x : xs) as= let

(us , vs)= kaheksLahuta x xs

inkiir us (x : kiir vs as)

kiir _ as= as

inkiir xs []

. (120)

Siin lokaalse operaatorikiir argumentas on akumulaator, kus toimub tulemuslisti ehitamine.

Et kogu jagamise etapp on ühine suurusjärjestuses üksikuteelementide otsimise ülesande la-hendusega, on ka järjestamise kiirmeetodi keerukus kapriisne ja sõltub sellest, kui ligilähedaseltvõrdseteks osadeks jaotamine toimub. Head ja halvad juhud on tolle ülesandega ühised. Heal ja

138

Page 139: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

keskmisel juhul on kiirmeetod keerukusklassisn log n listi pikkusen suhtes. Keerukus on suu-rem kui suurusjärjestuses üksikute elementide otsingul, sest mittetriviaalseid alamülesandeid onühe asemel kaks. Halvimal juhul, kui igal rekursioonisammul eraldub ainult lahe, tuleb siingiruutkeerukus.

Ülesandeid

250. Realiseerida järjestamise kiirmeetodi teisend, mille korral järjestamise käigus ühtlasi kaota-takse kordumised, st tulemuslistis esinevad kasvavas järjestuses kõik algse listi elemendid,igaüks üks kord.

251. Realiseerida järjestamise kiirmeetodi teisend, mille korral tulemusena tekib paaride list, kuspaaride esimesed komponendid on parajasti argumentlistisesinevad elemendid, igaüks ühekorra, kasvavas järjestuses ning vastavad teised komponendid näitavad elemendi esinemisekordsust argumentlistis.

252. Kooskõlas signatuuriga

kordusteta:: (Ord a)=> [a] -> Bool

defineerida muutujakordusteta väärtuseks funktsioon, mis annab lõplikul listil väärtu-seksTrue või False vastavalt sellele, kas listi elemendid on paarikaupa erinevad või esinebmõni element korduvalt.

253. Kooskõlas signatuuriga

onPerm:: [Int] -> Bool

defineerida muutujaonPerm väärtuseks predikaat, mis kontrollib etteantud listi kohta, kason tegemist mingi permutatsiooniga täisarvudest1 kuni mingin.

Alamülesannete listid

Erinevalt kiirmeetodist, on põimimismeetodil järjestamise põhimõtteks jagada list kärmelt ka-heks võimalikult ühepikkuseks osaks, kasutamata ühtki võrdlemist, järjestada kumbki osa samalmeetodil ning põimida tulemused üheks järjestatud listiks. Haskellis võiks põimimismeetodi rea-lisatsioon olla signatuuriga

järjestaPõim:: (Ord a)=> [a] -> [a]

.

139

Page 140: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Kahe järjestatud listi põimimisega oleme juba kokku puutunud, seda realiseeris definitsioon (92).Kuid seal oli vaja ühesuguste elementide kordumist vältida, järjestamisel aga tavaliselt tahetaksekõik koopiad alles hoida. Seepärast anname siin muutujalepõimi uue definitsiooni

põimi xs @ (a : as) ys @ (b : bs)| a <= b

= a : põimi as ys| otherwise

= b : põimi xs bspõimi xs []= xs

põimi _ ys= ys

, (121)

mille järgi arvutamisel kordusi ei kaotata.

Nüüd saaksime põimimismeetodil järjestamise defineerida näiteks koodiga

järjestaPõim xs @ ( _ : _ : _)= let

(us , vs)= kaheksLoe xs

inpõimi (järjestaPõim us) (järjestaPõim vs)

järjestaPõim xs= xs

. (122)

Listi jagamiseks kaheks võimalikult ühepikkuseks osaks onkasutatud muutujatkaheksLoe ,mis võib olla defineeritud koodiga (78), (79), (80) või (99).

Esimene deklaratsioon definitsioonis (122) sobib juhul, kui listis on vähemalt kaks elementi. Sel-lisel juhul on mõlemad kahekslugemisega saadavad osad algsest listist lühemad, nii et ülesandesuurus rekursiivsel pöördumisel väheneb. Kui listis on vähem kui kaks elementi, siis antakseoriginaallist välja. See on korrektne, sest0- ja 1-elemendilised listid on juba järjestatud.

Põimimismeetodil järjestamine definitsiooniga (122) sooritab alati suurusegan log n proportsio-naalse arvu samme, kusn on listi pikkus, sest kõigi ühepikkuste listide jagamine osadeks toimubühtviisi. Kui aga list on juba järjestatud või vaid mõned elemendid on vales positsioonis, on seeliiga ebaefektiivne, sest siis oleks võimalik järjestada ka lineaarse keerukusega.

Seni oleme “jaga ja valitse” tehnikaga lahendusi realiseerinud nii, et alamülesanded anti rekur-siivsel pöördumisel ühekaupa edasi. On võimalik ka lähenemine, kus jooksvaid alamülesandeidhoitakse karjakaupa ühes andmestruktuuris. Näitena sellest realiseerime nüüd põimimismeetodiljärjestamise uuesti, kasutades alamülesannete liste.

Täpsemalt seisneb mõte järgnevas. Kaheksjagamise protsessi võib lihtsalt ära jätta, sest seal mi-dagi sisulist ei toimu. Lähtume jagamisprotsessi lõpptulemusest, milleks on algse listi maksi-

140

Page 141: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

maalne hakitus väikesteks listijuppideks; paneme kõik need listijupid ühte listi. Seejärel hak-kame neid listijuppe iteratiivselt kahekaupa põimima, niiet tekivad järjest pikemad järjestatudlistid kuni lõpuks on kõik elemendid ühes järjestatud listis. Iteratiivse põimimise programmee-rimisel peab olema tähelepanelik, et põimimiste skeem oleks kahendpuukujuline, muidu saameruutkeerukusega järjestamise.

Koodi (122) järgi võttes on jagamisprotsessi lõpptulemuseks olukord, kus iga element moodustabomaette listi, sest kõigi pikemate listide puhul jagatakseedasi. Kõigi selliste alamülesannete listion lihtne saavutada koodiga

hakid:: [a] -> [[a]]

hakid xs= [[x] | x <- xs ]

. (123)

Need üheelemendilised jupid moodustavad hargnemiste puu kõige alumise taseme ehk lehetase-me. Juppide kahekaupa põimimise üle kogu listi annab definitsioon

põimiTase:: (Ord a)=> [[a]] -> [[a]]

põimiTase (xs : ys : yss)= põimi xs ys : põimiTase yss

põimiTase xss= xss

. (124)

OperaatoripõimiTase üks rakendamine viib läbi kõik puu ühe taseme põimimised. Selle tule-musel jääb juppide arv umbes kaks korda väiksemaks, kuid need jupid sisaldavad jätkuvalt kõikalgse listi elemendid.

Nüüd on vaja seda operatsiooni korrata, kuni on jäänud vaid üks jupp, ja see jupp välja anda.Selle realiseerib kood

põimiPuu:: (Ord a)=> [[a]] -> [a]

põimiPuu xss @ ( _ : _ : _)= põimiPuu (põimiTase xss)

põimiPuu [xs ]= xs

, (125)

itereerimine toimub akumulaatoris. Tühja listi jaoks me deklaratsiooni ei esita, kuna kannamehoolt, et seda operaatorit tühjal listil kunagi välja ei kutsutaks.

141

Page 142: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

OperaatorijärjestaPõim uueks definitsiooniks kujuneb

järjestaPõim []= []

järjestaPõim xs= põimiPuu (hakid xs)

. (126)

Kuna saadud järjestamisprotsess toimub sisuliselt samamoodi kui eelmine, on keerukus endiseltproportsionaalne suurusegan log n, kusn on listi pikkus, sõltumata listist.

Kuid nüüd järgneb puänt. Ilmselt on hakkimise tulemuse juures olulised parajasti kaks asja: et“hakid” sisaldaksid kokku parajasti algse listi kõik elemendid ja et iga “hakk” oleks omaettejärjestatud (muidu ei tööta põimimine korrektselt). Pole oluline, et põimimise etapp algaks justüheelemendilistest juppidest. Me võime “hakkideks” samahästi võtta algse listi juba järjestatudlõigud ehk segmendid. Siis lõpetab järjestamisprotsess seda kiiremini, mida rohkem elementealgses listis on juba järjestatud. Kui algne list on üleni järjestatud, on kogu järjestamisprotsesstriviaalne ja lineaarse keerukusega listi pikkuse suhtes (ainsa segmendi leidmiseks tuleb list korraläbi vaadata).

Seega meil on vaja vaid operatsiooni, mis leiaks listi järgitema segmendid ja moodustaks neistlisti. Kuid see on meil juba olemas, definitsioonidega (76) ja (77) muutujasegmendid väärtu-seks antud. Niisiis piisab kood (123) asendada koodiga

hakid:: (Ord a)=> [a] -> [[a]]

hakid xs= segmendid xs

. (127)

Ülesandeid

254. Realiseerida järjestamise põimimismeetodi teisend,mille korral järjestamise käigus ühtlasikaotatakse kordumised, st tulemuslistis esinevad kasvavas järjestuses kõik algse listi ele-mendid, igaüks üks kord. Kasutada alamülesannete listi.

255. Realiseerida järjestamise põimimismeetodi teisend,mille korral tulemusena tekib paaridelist, kus paaride esimesed komponendid on parajasti argumentlistis esinevad elemendid,igaüks ühe korra, kasvavas järjestuses ning vastavad teised komponendid näitavad elemendiesinemise kordsust argumentlistis. Kasutada alamülesannete listi.

256. Kujutame ette olümpiasüsteemis turniiri, milles võistlevad listid ja milles mäng kahe lis-ti vahel seisneb listide “jooksvate” elementide võrdlemises. Mängu võidab see list, kellejooksev element on suurem. Kui jooksvad elemendid on võrdsed, siis mängivad samad lis-tid omavahel kohe samamoodi uuesti, kuni tulemus selgub (võib eeldada, et kokku ei satu

142

Page 143: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kaks võrdset lõpmatut listi). Listi jooksev element võetakse pärast iga mängu uus (listisjärgmine). Kui listil pole mänguks enam elemente võtta, agavastasel on, on ta mängu ilmamängimata kaotanud (vastasel jääb element alles). Kui mõlemal vastasel on elemendid ot-sas, on mõlemad kaotanud (st järgmises voorus on list, mis pidanuks selle mängu võitjagamängima, ilma mängimata võitnud). Kui listile vastast ei jagu (paaritu arvu järelejäänudvõistlejate tõttu), saab ta ilma mängimata järgmisse vooru.

Defineerida muutujalistiOlümpia väärtuseks funktsioon, mis võtab argumendiks võr-reldavate elementidega listide listi ja annab tulemuseksJust l, kus l on listidest see, misolümpiasüsteemis turniiri võidab, kui võitja selgub, jaNothing, kui võitjat ei selgu.

Dünaamiline programmeerimine

Üks näide ülesandest, kus “jaga ja valitse” strateegia on juba püstituses sisse kodeeritud, on Fi-bonacci arvude leidmine järjekorranumbri järgi. Jada matemaatiline spetsifikatsioon (65) näitabotse, et ülesanne parameetri väärtusen korral taandub kahele sarnasele alamülesandele vastavaltparameetrite väärtustegan − 1 ja n − 2. Seega oli juba definitsiooni (65) põhjal kirjutatud kood(66) meie esimene näide “jaga ja valitse” tehnikast.

Nägime aga kohe, et kood (66) on praktiliseks kasutamiseks kõlbmatu, kuna töötab eksponent-siaalse keerukusega. Aegluse põhjuseks on samade alamülesannete korduv lahendamine. Näiteksfib n arvutamisel, kui argumendi väärtus on vähemalt2, kutsutakse väljafib (n - 1) jafib (n - 2) , kuid fib (n - 1) kutsub oma töö käigus samutifib (n - 2) välja.Selliseid kopeerivaid arvutusi koguneb tohutu hulk.

Hiljem esitasime terve rea võimalusi lineaarse keerukuse saavutamiseks. Mõned neist põhinesidparalleelselt kahe viimase alamülesande tulemuse meelespidamises, teised koostasid listi kõigisama tüüpi ülesannete vastustest (ehk Fibonacci arvudest)ja siis lugesid nõutud arvu sealt.

Programmeerimise võtet, kus alamülesannete lahenduste tulemusi kasutatakse korduvalt ilmaneid uuesti lahendamata, nimetataksedünaamiliseks programmeerimiseks (ingl dynamic

programming). Ülal vaadeldud Fibonacci arvude arvutamise optimeerimised olid kõik dünaa-milise programmeerimise näited.

Dünaamiline programmeerimine on “jaga ja valitse” tehnikapuhul tüüpiline optimeerimisvõte.Seda, et ülesannet rekursiivselt alamülesanneteks jagades tekib kuskil ühesuguseid alamüles-andeid, juhtub väga tihti. Seepärast võib dünaamilise programmeerimise näiteid lugeda ühtlasi“jaga ja valitse” tehnika näideteks, kuigi koodist võib algset — või ka vahepealset — “jaga javalitse” põhimõttel ülesandepüstitust raske olla välja lugeda.

Olgu meil vaja programmeerida tavaline arvu astendamine täisarvuga, mis positiivsel astenda-jal defineeritaks korduva korrutamise kaudu, negatiivsel aga pöördarvu leidmise abil. Kandes

143

Page 144: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

matemaatilise definitsiooni naiivselt üle Haskelli, saaksime definitsiooni

aste:: (Fractional a , Integral b)=> a -> b -> a

aste a n= case compare n 0 of

GT-> a * aste a (n - 1)

EQ-> 1

_-> 1 / aste a (- n)

, (128)

mille järgi arvutus toimub astendaja suhtes lineaarse keerukusega.

Seepärast võtame appi “jaga ja valitse” tehnika: tekitame kaks võrdse suurusega mõttelist tegu-rite gruppi, arvutame kummagi grupi korrutise ja leiame selle tulemuse põhjal lõppvastuse. Kuiastendaja on paaris, siis saab kõik tegurid kahte ühesuurusse gruppi ära jagada. Kui astendaja onpaaritu, siis jagame ära kõik peale ühe. Nii on grupid igal juhul ühesugused ja kahe rekursiivsepöördumise asemel saab läbi ühega. Kui astendaja on0, siis on tegu triviaalse juhuga, kus anna-me kohe lõppvastuse1 välja. Negatiivse astendaja korral astendame vastandarvuga ja leiame siispöördväärtuse. Kokku tuleb kood

aste a n= case compare n 0 of

GT-> let

(q , r)= n ‘divMod ‘ 2

x = aste a qinif r == 0 then x * x else a * x * x

EQ-> 1

_-> 1 / aste a (-n)

. (129)

Arvutades selle koodi järgi, väheneb astendaja igal rekursiivsel pöördumisel ligikaudu2 korda,nii et operatsioonide koguarv on astendaja suhtes logaritmiline.

Võtmesammuks efektiivsuse kolossaalse paranemise juurdeon siin mitte “jaga ja valitse” tehnikaiseenesest, vaid dünaamiline programmeerimine — ühe rekursiivse pöördumise saavutamine ka-he asemele. Just see vähendab korrutamiste arvu. Kui avaldistaste a q mitte siduda lokaalse

144

Page 145: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

muutujaga jain järel olevas avaldises pannax asemeleaste a q , oleks kogu ümbertöötamisevaev kasutu, korrutamiste arv võrduks vanaviisi astendajaväärtusega.

Tegureid teistmoodi grupeerides võiksime saada ka teistsuguseid logaritmilise keerukusega rea-lisatsioone, näiteks

aste a n= case compare n 0 of

GT-> let

(q , r)= n ‘divMod ‘ 2

y = aste (a * a) qinif r == 0 then y else a * y

EQ-> 1

_-> 1 / aste a (-n)

. (130)

Samuti on võimalik lahendus alamülesannete vastuste listibaasil, mis tähendaks mitte kõigi ast-mete, vaid ainult järjestikuste ruututõstmiste teel saadavate astmete salvestamist listi. Lähtudeskoodist (130), võiksime saada

aste a n| n >= 0

= letas

= a : [a * a | a <- as]aste 0 _

= 1aste n (a : as)

= let(q , r)= n ‘divMod ‘ 2

y = aste q asinif r == 0 then y else a * y

inaste n as

| otherwise= 1 / aste a (-n)

.

See kood töötab vahestruktuuri tõttu pisut aeglasemalt.

145

Page 146: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Pöördume veel tagasi ka Hanoi tornide ülesande juurde ja vaatame, kuidas seal dünaamilisestprogrammeerimisest kasu lõigata.

Oletame algul, et eesmärgiks on leida mitte sihile viiv tõstete jada, vaid selle käigus esinevate sei-sude jada. Esitame seisu listikolmikuna, kus kolmiku komponendid vastavad varrastele ja iga listkoosneb parajasti vastava varda otsas olevate ketaste järjekorranumbritest. Kettaid nummerdamesuuruse kasvamise järjekorras.

Jagame ülesande osadeks nii nagu enne. Selleks, et kahe rekursiivse pöördumise asemel ühe-ga hakkama saada, tuleb loobuda vardaparameetritest ning selle asemel rekursiivse pöördumisetulemust kahte moodi tõlgendada. Tõlgendamine seisneb varraste ümbermängimises — põhi-mõtteliselt sama asi mis varemkirjutatud koodide (118) ja (119) puhul toimus argumentide vahe-tamisega.

Sobib kood

type Seis= ([Int] , [Int] , [Int])

hanoiSeisud:: Int -> [Seis]

hanoiSeisud n= case compare n 0 of

GT-> let

hs= hanoiSeisud (n - 1)

in[(n : xs , zs , ys) | (xs , ys , zs) <- hs] ++[(zs , n : ys , xs) | (xs , ys , zs) <- hs]

EQ-> [([] , [] , []) ]

_-> error "hanoiSeisud: negatiivne argument "

.

(131)

Tõepoolest, rekursiivse pöördumisehanoiSeisud (n - 1) tulemus on kasutatud kahel vii-sil: loodava listi esimeses pooles vahetatakse igas seisusära teine ja kolmas varras ning lisataksekõikjal esimesele vardale suurim ketas, sarnaselt on teises pooles vahetuses esimene ja kolmasvarras ja suurim ketas on seal lisatud teisele vardale.

Uurime, kas ka tõstete jada leidmisel on võimalik optimeering dünaamilise programmeerimi-sega. Selle varasematest realisatsioonidest (118) ja (119) oli teine saadud esimesest arvutuseülekolimisega akumulaatorisse, mis andis teatava ajavõidu.

Märkame, et arvutamisel nende koodide järgi võivad lokaalse operaatori erinevad rekursiivsetepöördumiste jadad viia samade argumentide komplektideni:näiteks väljakutse argumentide väär-

146

Page 147: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

tustegan, A, B, C toob kaasa väljakutsed argumentide väärtustegan− 1, A, C, B ja argumentideväärtustegan− 1, C, B, A, need aga omakorda tekitavad kumbki väljakutse argumentide väär-tustegan− 2, A, B, C. Kuna rekursiivseid väljakutseid on eksponentsiaalne arv (igal tasemeltekib kaks uut), kuid võimalike argumentide kombinatsioonide arv on lineaarne, sest varrastepermutatsioone on kokku ainult6, siis on kopeerivate arvutuste osakaal väga suur.

Oleks vaja, et iga argumentide komplekti jaoks toimuks ainult üks väljakutse. Võimalikud argu-mentide komplektid rekursiivsetel pöördumistel võib paigutada6 × n tabelisse, kusn on alg-sisendi väärtus. Tabeli read vastavad varraste permutatsioonidele. Kirjutame lokaalse operaatoriselliselt, et tema töö tulemuseks argumendil väärtusegan on korraga terve selle tabeli veerg nrn (veerge nummerdame0-st). Väljendame veerge6-elemendiliste listidena. Elemendid selleslistis vastaku järjest varraste permutatsioonidele ABC, ACB, BAC, BCA, CAB, CBA. Varrastejärjekord permutatsioonis vastab argumentide järjekorrale varasemas koodis.

Selguse mõttes toome siin eraldi välja järgmise veeru leidmise operatsiooni koodiga

järgmHanoi:: [[String ]] -> [[String ]]

järgmHanoi [hs1 , hs2, hs3, hs4, hs5, hs6]= [

hs2 ++ "AB" : hs6 ,hs1 ++ "AC" : hs4 ,hs4 ++ "BA" : hs5 ,hs3 ++ "BC" : hs2 ,hs6 ++ "CA" : hs3 ,hs5 ++ "CB" : hs1

]

. (132)

Nüüd saame operaatorihanoi uuesti defineerida koodiga

hanoi n| n >= 0

= lethanoi 0

= replicate 6 []hanoi n

= järgmHanoi (hanoi (n - 1))inhead (hanoi n)

| otherwise= error "hanoi: negatiivne argument "

, (133)

mis töötab palju kiiremini isegi varasemast akumulaatoriga koodist (119).

Kuna aga juba Hanoi tornide ülesande vastus on ketaste arvu suhtes eksponentsiaalse suuruse-ga, siis käsitletud optimeeringud ega ka ükski muu optimeering ei alanda keerukust allapooleeksponentsiaalset.

147

Page 148: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

257. Lülitada Hugsis sisse reduktsioonide (arvutussammude) ja kasutatud mäluühikute loendur(:s +s ) ning arvutada ratsionaalarvude suuri astmeid nii definitsiooniga (128), definit-siooniga (129) kui definitsiooniga, mille saame viimasest,kui rekursiivse pöördumise tule-must muutujaga ei seota jax asemel on tingimusavaldisesaste a q . Veenduda, et viima-ne nõuab niisama palju ressurssi kui definitsioon (128), samas kui (129) järgi arvutamineon tunduvalt kiirem ja piirdub üliväikese reduktsioonide arvuga.

258. Stirlingi 1. liiki arvud s(n, k) defineeritakse seostega

s(0, 0) = 1, ∀n > 0 (s(n, 0) = 0), ∀k > 0 (s(0, k) = 0),∀n, k > 0 (s(n, k) = s(n − 1, k − 1) − (n − 1) · s(n − 1, k)).

Defineerida muutujas1 väärtuseks funktsioon, mis võtab argumendiks täisarvudn ja k jaannab tulemuseks arvus(n, k), kui see on neil argumentidel defineeritud.

259. Stirlingi 2. liiki arvud S(n, k) defineeritakse seostega

S(0, 0) = 1, ∀n > 0 (S(n, 0) = 0), ∀k > 0 (S(0, k) = 0),∀n, k > 0 (S(n, k) = S(n − 1, k − 1) + k · S(n − 1, k)).

Defineerida muutujas2 väärtuseks funktsioon, mis võtab argumendiks täisarvudn ja k jaannab tulemuseks arvuS(n, k), kui see on neil argumentidel defineeritud.

260. Stringis osastringiks nimetatakse suvalist stringi, mis on stringists saadud0 või enamasümboli väljajätmisel (allesjäävad sümbolid omavahelistjärjekorda ei muuda).

Defineerida muutujapü väärtuseks funktsioon, mis võtab argumendiks kaks stringija an-nab tulemuseks nende mingi pikima ühise osastringi.

Väärtustusjärjekorrapõhised programmeerimisvõtted

Vaatleme siin mõnda programmeerimisvõtet, mis on aktuaalsed ainult seetõttu, et Haskelli väär-tustamine on vaikimisi laisk. Sellised jagunevad loomulikult kaheks: need, kus nähakse vaevalaisa väärtustamine asendamiseks agaraga, ja need, kus laisast väärtustamisest lõigatakse kasu.

Agaraks sundimine

Võrreldes muutujasuurSumma algset definitsiooni (62) ja akumulaatoriga definitsiooni (111),märkame, et algse definitsiooni järgi arvutamisel ei saa liitmistehet sooritada enne, kui rekur-siivne pöördumine on andnud tulemuse, samas kui akumulaatoriga arvutamisel oleks võimalikliitmine ära teha ilma rekursiivselt pöördumata.

148

Page 149: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Pika rekursiivsete pöördumiste jada kontekstis nähtub sellest, et definitsiooni (62) puhul ei soo-ritata ühtki liitmist enne, kui rekursioon on jõudnud põhjani, kõik liitmised saab teha alles rekur-sioonisügavusest tagasipöördumisel. See tähendab, et enne baasjuhuni väljajõudmist kasvatabrekursiivsete pöördumiste jada mällu pikka väärtustamataavaldist. Akumulaatoris arvutamineaga võimaldab liitmised sooritada juba “minnes” ja kui nii teha, siis mälutarve väheneb järsult.

See võimalus jääbki ainult võimaluseks, kui programmeerija ei näe selleks lisavaeva. Reaal-selt on mäluhõive definitsiooniga (111) arvutades niisama suur kui definitsiooni (62) puhul, sestHaskelli väärtustamine on vaikimisi laisk, nii et ka akumulaatoris ei tehta liitmisi enne kui alleslõpus.

Näeme, et laisal väärtustamisel on peale soovitud omaduse,et mittevajalike asjade arvutamisepeale ei kulutata aega, ka teine ja üsna ebameeldiv tagajärg.

Akumulaator annab põhimõttelise võimaluse liitmisi jooksvalt sooritada, see tuleb ära kasutada.Haskellis on võimalik väärtustamist kohati agaraks pöörata, võttes appi agara rakendamise ope-raatori$! . Akumulaatorite kasutamine koos nende jooksva väärtustamisega annab olulise mälukokkuhoiu.

Infiksoperaator$! on paremassotsiatiivne prioriteediga0, nii nagu operaator$. Tema väärtu-seks on funktsioon, mis võtab argumendiks funktsioonif ja tema potentsiaalse argumendix: kuix 6= ⊥, siis annab väärtuseksf x, kui agax = ⊥, siis annab väärtuseks⊥. Kujul f $! e olevaavaldise arvutamisel väärtustatakse kõigepealt avaldiste senikaua, kuni saadakse selline aval-dis e′, mille kuju näitab, et väärtus on normaalne, misjärel väärtustataksef e′. Pangem tähele,et avaldiste ei väärtustata tingimata lõpuni, väärtuse normaalsuse selgumiseks piisab välimisekonstruktori ilmumisest.

Nimetame funktsioonitüüpi avaldistagaralt väärtustavaks, kui mistahes argumendile raken-dades väärtustatakse kõigepealt argumenti, kuni selgub, kas tema väärtus on normaalne. Kõikoperaatorid, mille definitsioonis on esimese argumendi kohal konstruktoriga ehitatud näidis, onagara väärtustamisega. Operaator$! annab võimaluse tekitada agaralt väärtustav variant ka aval-distest, mis muidu seda ei ole. Kui avaldis juba on agaralt väärtustav, siis pole operaatori$!lisamisel mõtet.

Näiteks avaldiseconst 5 $! undefined väärtus on ⊥, tema väärtustamine lõ-peb veateatega, mille annabundefined , samas kui avaldisteconst 5 $! 0ja const 5 $! (undefined , undefined) väärtus on 5. Avaldiseconst 5 $! length [1 .. ] väärtustamine jääb lõpmatusse tsüklisse. Aga näitekshead $! [1 .. ] ja head [1 .. ] annavad sama tulemuse1, sesthead rakendamiselväärtustatakse niikuinii kõigepealt argument (mitte täielikult, vaid ainult esimese kooloniilmumiseni, nii et lõpmatut arvutust ei teki).

Operaatori$! erinevust operaatorist$ näitab väga ilmekalt avaldiste

error "fun " $ error "arg " (134)

149

Page 150: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ja

error "fun " $! error "arg " (135)

väärtustamine. Kui (134) väärtustamisel tuleb välja funktsiooni viga, siis (135) väärtustamiselargumendi viga.

Meil on vaja definitsiooni (111) akumulaatorit jooksvalt väärtustama sundida. Selleks tuleb li-sada operaator$! koodi sellise koha peale, et tema teine argument oleks see akumulaator. Siinosutub mugavaks prefiksne rakendamine, mille korral piisab($! ) lisamisest rekursiivse pöör-dumise ette. Saame

suurSumma n| n >= 0

= letsuurSumma a i

| i <= n= ($! ) suurSumma (a + i ^ 4) (i + 1)

| otherwise= a

insuurSumma 0 1

| otherwise= error "suurSumma: negatiivne liidetavate arv "

. (136)

Tänu operaatorile$! väärtustab arvutusprotsess iga kord ennesuurSumma rekursiivset välja-kutset avaldisea + i ^ 4 , seega uuendatud akumulaator antakse igal rekursiivsel väljakut-sel edasi väärtustatud kujul, mis võtab vähem mälu. Definitsiooni (136) korral saab muutujatsuurSumma edukalt kasutada mitu suurusjärku suuremate argumentidega kui eelmiste definit-sioonide puhul.

Avaldist i + 1 see$! väärtustama ei sunni, kuid seda polegi vaja. See avaldis väärtustatakserekursiooni järgmisel tasemel niikuinii, sest tema väärtuse järgi on vaja valida haru.

Muutujafib defineeriva koodi (112) võime samasuguse lisandusega paremaks teha, saame

fib n| n >= 0

= letfib a _ 0

= afib a b i

= ($! ) fib b (a + b) (i - 1)infib 0 1 n

| otherwise= ( if odd n then 1 else -1) * fib (-n)

. (137)

150

Page 151: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Definitsioonis (137) ei mõju operaator$! küll liitmist sisaldavale avaldisele otse, kuid siin pii-sabki ainult esimese akumulaatori jooksvast väärtustamisest. Kuia on väärtustatud jab sunni-takse väärtustama rekursiivsel pöördumisel, siis argument a + b sisaldab ainult ühe liitmistehtening järgmisel rekursioonitasemel ona jällegi väärtustatud. Seega plahvatust ei toimu.

Ülal nägime, et andmestruktuuri väärtustamine ei tähenda tema lõpuni väljaarvutamist — näitekshead $! [1 .. ] ei arvuta avaldist[1 .. ] lõpuni (mida tal ei ole), vaid ainult esimesekooloni ilmumiseni. Kui tahame listi kohapeal lõpuni väärtustada, siis tuleb viia agar väärtusta-mine mööda lististruktuuri rekursiivselt alla.

Vaatame näiteks koodi

lõplik:: [a] -> [a]

lõplik (x : xs)= ($! ) (x : ) (lõplik xs)

lõplik _= []

. (138)

Kujul lõplik l oleva avaldise väärtustamine selle definitsiooni põhjal põhimõtteliselt kopee-rib avaldisel väärtuseks oleva listi struktuuri. Agaraks sundimise tõttu konstrueeritakse koopiatagant ettepoole: listi pead ei panda paika enne, kui saba onvalmis, sabas omakorda esimest ele-menti ei panda kohale enne, kui järgmiste elementide list onvalmis jne. Igal rekursioonitasemelon esimene asi uus rekursiivne pöördumine (naljaga poolekson operaatorilõplik definitsioonkoodis (138) “pearekursiivne”). Tulemusse ei ilmu ühtki konstruktorit enne, kui rekursioon onjõudnud põhjani ja ühtlasi argumentlisti struktuur on lõpuni välja arvutatud.

Seega niipea, kui on vaja mingilgi määral väärtustada avaldist kujul lõplik l, arvutatakse listilstruktuur kohe lõpuni välja. Kui argumentlist on lõpmatu, jääb see protsess midagi välja andmatalõpmatult tööle.

Ülesandeid

261. Kirjutada oma moodulisse deklaratsioonid (62), (111), (136) ja neid kahekaupa välja kom-menteerides kontrollida igaühe puhul, kui suure argumendiga on Hugs võimeline funkt-siooni väärtust arvutama ilma mälu ületäitumiseta.

262. Kirjutada definitsiooni (137) teisend, mille korral eijäetaks väärtustamata isegi mitte ühteliitmistehet.

263. Teisendada definitsioone (88) (107), (109) nii, et listi komponentide väärtustamine käiksjooksvalt.

264. Modifitseerida definitsiooni (75) selliselt, etfibs arvutamisel definitsiooni (93) järgi toi-muks jooksev summade arvutamine.

151

Page 152: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

265. Kirjutada ülesannete 240, 241, 242 lahendused nii, et arvutamise käigus pikki väärtustamataavaldisi ei tekiks.

266. Defineerida muutujasynna väärtusekscurried-kujul funktsioon, mis võtab täisarvuli-sed argumendidn ja k ning annab tulemuseks tõenäosuse, et kui tehak sõltumatut va-likut samastn elemendist, siis kõikk valitud väärtust on paarikaupa erinevad. Näitekssynna 365 20 väärtuseks peab olema tõenäosus, et juhuslikult valitud20 inimese hul-gas ei leidu ühtki kaht, kelle sünnipäevad langeksid kokku (eeldusel, et ükski pole sündi-nud 29. veebruaril). Arvutus peab andma veateate, kui argumendidn ja k on sellised, millekorral antud ülesandepüstitus on mõttetu, veateade peaks võimalikult täpselt kirjeldamaolukorda ja milles seisneb viga.

267. Demonstreerida interaktiivses keskkonnas, eterror ei ole agaralt väärtustav.

268. Kas iga agaralt väärtustava avaldise väärtus on agar funktsioon? Kas iga avaldis, mille väär-tus on agar funktsioon, on agaralt väärtustav?

269. Mis on koodiga (138) defineeritud muutujalõplik väärtus?

Laisa väärtustamise ärakasutamine

Matemaatikas juhtub mõnikord, et teoreemi tõestamist lihtsustab väite üldistamine. Teisi sõnu,lihtsam on tõestada nõutust rohkem, selle asemel et püüda tõestada parajasti nõutud väidet.

Laisk väärtustamine muudab sama taktika aktuaalseks ka programmeerimisel. Haskellis on ideekirjeldada rohkem kui ülesanne nõuab täiesti standardne. Oleme seda juba korduvalt ka kasuta-nud, kui mingi funktsiooni arvutamiseks kirjeldasime lõpmatu listi selle funktsiooni kõigi väär-tustega ja funktsiooni definitsiooni osaks jäi listist lugemine. Näiteks kasutas seda lähenemistFibonacci arvude definitsioon (94).

Eriti tihti tuleb nõutust rohkem kirjeldamise taktikat ette dünaamilisel programmeerimisel. Laisaväärtustamise korral pole vaja seada vahetulemuste struktuurile mingeid suuruspiiranguid läh-tuvalt sellest, kui palju on tegelikult neid vahetulemusi vaja arvutada. Väärtustusprotsess panebselle ise paika. Näiteks Hanoi ketaste tõstete jada kirjeldamisel definitsioonidega (132) ja (133)on osa vahetulemuste struktuuri komponente sellised, midaei kasutata. Selles veendumiseks pii-sab märgata, et järjekorrasn-ndast6-elemendilisest listist võetakse reaalselt ainult pea. Teisitema elemente välja ei arvutata, ehkki nad on kirjeldatud.

Toome veel mõned teistsugused näited samast lähenemisest.

Eelnevas vaatlesime ülesannet leida etteantud listi suurusjärjestuses kindlal kohal asuv element.Varem defineerisime selle tarvis operaatorisuuruseltNr koodiga (117), mille loetavus polejust kiita.

152

Page 153: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Oletame, et tarvis läheb vaid järjekorras üsna ees asetsevaid elemente, st arvuline argument onülalt tõkestatud. Siis viib praktiliselt sama keerukusegaarvutuseni kood

suuruseltNr n xs| 0 <= n && n < length xs

= järjestaPõim xs !! n| otherwise

= error "suuruseltNr: mõõtmetest väljas "

, (139)

mis käsib lihtsalt listi järjestada ja võtta siis nõutud positsioonilt elemendi, kusjuures põimimis-meetodi korral puuduvad ka varasemat definitsiooni kummitanud halvad juhud. Seda on raskekonkreetselt põhjendada, aga katsetamine seda näitab: kuigi järjestamine on keerukam protsesskui järjestuses kindla elemendi otsimine, tehakse laisa väärtustamise tõttu ka definitsiooni (139)korral tööd proportsionaalselt ühe elemendi otsimise protsessiga.

Olgu meil veel ülesanne kirjeldada funktsioon, mis võtaks argumendiks listi ja tooks selles mi-nimaalse elemendi algusse (kui minimaalne kordub, siis üheneist), jättes teiste elementide jär-jestuse muutmata, vastavalt signatuurile

minEtte:: (Ord a)=> [a] -> [a]

.

Agara väärtustamisega harjunud programmeerijale võib tunduda, et arvutamisel koodiga

minEtte xs @ (y : ys)| null ys || y <= z

= xs| otherwise

= z : y : zswhere

z : zs= minEtte ys

minEtte _= error "minEtte: tühi list "

(140)

tehakse tühja tööd: rekursiivse pöördumiseminEtte ys tulemust läheb vaja ainult juhul, kuikogu listi esimene element pole minimaalne, kui aga esimeneelement on vähim, siis visatakserekursiivse pöördumise tulemus minema ja antakse vastuseks algne list. Kuid katsetamine näitab,et juhul, kui minimaalne element on listi alguses, töötab kood (140) kiiremini. Laisk väärtustajaarvutab rekursiivse pöördumise tulemusest ainult niipalju kui vaja ehk parajasti listi pea, midaläheb vaja hargnemisel.

Ülesandeid

153

Page 154: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

270. Defineerida muutujarotatsioonid väärtuseks funktsioon, mis võtab argumendiks listija kui see on lõplik, siis annab tulemuseks tema kõik tsüklilised nihked listina, mille pikkusvõrdub argumentlisti pikkusega.

271. Defineerida muutujaotstestKeskele väärtuseks funktsioon, mis võtab argumendikslisti ja annab tulemuseks listi, kus argumentlisti elemendid on paigutatud ringi selliselt, etesimene element on jäänud esimeseks, viimane on läinud teiseks, teine kolmandaks, tagantteine neljandaks jne.

Olgu meil nüüd vaja defineerida muutujaüleÜheEtte väärtuseks funktsioon, mis võtab argu-mendiks listil ja annab tulemuseks listi, mille alguses on listil elemendid üle ühe alates peast jaseejärell ülejäänud elemendid.

Esmane võimalus seda teha on kasutada definitsiooniga (78) või (79) või (80) või (99) antudmuutujatkaheksLoe . Tema rakendamine argumentlistile teeb jaotamistöö ära jajääb üle vaidpaari komponendid ühte listi kokku konkateneerida. Selle ideega saame koodi

üleÜheEtte:: [a] -> [a]

üleÜheEtte xs= let

(us , vs)= kaheksLoe xs

inus ++ vs

.

Selle lahenduse puudus on, etkaheksLoe rakendamisel koostatava paari esimene komponent-list konkateneerimise käigus kopeeritakse. Tühja töö vältimiseks tulekskaheksLoe definit-sioon asendada sellisega, mis ehitab paari esimese listi kohe õigesse kohta — teise listi ette.

Võib tunduda, et funktsionaalse paradigma raames pole võimalik seda saavutada, sest mälu-haldus on otseselt kirjeldamatu. Kuid siiski — Haskelli väärtustamine on laisk ja seal töötavadrekursiooniskeemid, mis agaras keeles on kujuteldamatud.Lihtsalt rekursiooni baasjuhus tulebväljaantava väärtuse vasakus komponendis näidata funktsiooni rakendamisel algsele argumendi-le (mis see siis ka ei olnud) tulemuseks oleva paari teine komponent. Võttes alusekskaheksLoe

154

Page 155: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

definitsiooni (78), saame niimoodi ümber tehesüleÜheEtte definitsiooni

üleÜheEtte xs= let

kaheksLoe (x : y : ys)= let

(us , vs)= kaheksLoe ys

in(x : us , y : vs)

kaheksLoe xs= (xs ++ vs , [])

(us , vs)= kaheksLoe xs

inus

, (141)

mis tõepoolest ka töötab.

Koodis (141) defineeritakse lokaalselt paari(us , vs) väärtuseks operaatorikaheksLoetöö tulemus sellel argumendil, millegaüleÜheEtte välja kutsuti, kusjuureskaheksLoe de-finitsioon oma baasjuhus pöördub sellesama paari teise komponendi poole. Nii onkaheksLoeja vs definitsioonid vastastikku rekursiivsed.

Tähelepanu väärib asjaolu, etkaheksLoe baasjuhu töötlemise ajal pole lõpptulemuse teinekomponent üldiselt veel teada. Haskelli laiskuse tõttu väärtustaja seda ei “märka” ja viga eitule. Edasise väärtustamise käigus selgub kavs väärtus ja kokkuvõttes kujuneb lõpptulemusekssoovitud paar.

Analoogiline lahendus ei ole võimalik, kuikaheksLoe defineerimisel võtta aluseks definit-sioon (79), sest baasjuhu defineerimisel pole teada, kumba komponentidest kujuneb esimene,kumba teine list, ja nii ei ole võimalik üheselt otsustada, kumb komponent peab sisaldama muu-tujatvs .

Demonstreerime sama lahendusvõtet veel ühe näite peal. Olgu meil vaja saada muutujasumSees väärtuseks predikaat, mis võtab argumendiks arvulisti ja kontrollib, kas tema ele-mentide summa ise on selle listi element. Standardteegi operaatorite kaudu tehes on lahendusekskood

sumSees:: (Num a)=> [a] -> Bool

sumSees xs= elem (sum xs) xs

,

mille järgi arvutamisel läbitakse argumentlist kaks korda: üks kord operaatorisum rakendamiselja teine kord operaatorielem rakendamisel.

155

Page 156: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

On selge, et selle kohta, kas listi elementide summa listis esineb, ei ole võimalik saada vähimatkiinformatsiooni enne, kui summa on välja arvutatud ja seega list lõpuni käidud. Pärast summaselgumist, kui juhtumisi summa listis ei esine, on selle kindlakstegemiseks vaja veelkord kõikilisti elemente külastada.

Võtame aga kasutusele lokaalse abioperaatorisumSees, mille ülesandeks on arvutada otsi-tav tõeväärtus paaris listi elementide summa endaga. Selleoperaatori definitsioonis tähistagu(b , s) tema rekursiivse pöördumise tulemust listi sabal. Lokaalse operaatorisumSeesalgsele argumentlistile rakendamise tulemuse seome väljaspool tema definitsiooni paariga(bx , sx) . Siis saab lokaalse operaatori definitsioonis lõppsummatsx kasutada. Tekib kood

sumSees xs= let

sumSees (z : zs)= let

(b , s)= sumSees zs

in(z == sx || b , z + s)

sumSees _= (False , 0)

(bx , sx)= sumSees xs

inbx

, (142)

mis tõesti ka töötab. Seega ülesanne, kus on vaja listi elemendid kaks korda läbi vaadata, seejuu-res teine kord peale esimese korra lõppu, on realiseeritud listi ühekordse lugemisega.

Saamaks aru, kuidas see paradoks on võimalik, on vaja täpselt mõista, mida tähendab listi luge-mine. Selle väljendiga märgitud tegevus, mis koodi (142) puhul toimub ühekordselt, on täpsemaltöeldes listi struktuuri avamine. Listi elemente aga loetakse muidugi kaks korda.

Veel selgemaks saab asi, kui mõtleme arvutamise käigu peale. Oleme juba näinud, et definitsioo-nide nagu näiteks (62) korral tekitab rekursioonisügavustesse minek pika väärtustamata avaldise,reaalne arvutus toimub alles tagasipöördumisel. Nii on muidugi ka definitsiooni (142) korral. Agakui niikuinii koostatakse ainult väärtustamata avaldist,siis pole mingit tähtsust sellel, kas see võiteine komponent on hetkel teada olevate andmete järgi reaalselt arvutatav või mitte. Proovimaasutakse alles siis, kui ollakse jõudnud rekursiooni põhja, baasjuhuni. Kuid siis on kõik info,mille põhjal lõpptulemust arvutada, käes. Kõik listi elemendid esinevad seal pikas väärtustamataavaldises omal kohal sees. Seepärast ei olegi mingit tarvidust neid enam listist lugema hakata.Edasi toimub kõik täiesti loomulikult: väärtustatakse kõigepealt see osa avaldisest, mis puudutabelementide summat, sest seda on tõeväärtuse leidmiseks vaja, ning seejärel tõeväärtus.

Kõik see mahub laisa väärtustaja tavapärase töö raamidesse.

156

Page 157: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

272. Näidata definitsioonis (141) nimedeus ja vs kõigi esinemiste kohta nende tähendusulatus(skoop).

273. Kirjutada definitsioon (141) samaväärselt ümber ilma paarinäidisteta, kasutades muutujaidfst ja snd . Millised muutujad on nüüd defineeritud vastastikrekursiivselt ja mis on nendeväärtus?

274. Kirjutada muutujaleüleÜheEtte definitsiooni (141) eeskujul alternatiivne definitsioon,kus lokaalse operaatorikaheksLoe defineerimisel oleks eeskujuks võetud definitsioon(80). Millised muutujad on nüüd defineeritud vastastikrekursiivselt?

275. Kirjutada muutujaleüleÜheEtte definitsiooni (141) eeskujul alternatiivne definitsioon,kus lokaalne operaatorkaheksLoe oleks defineeritud koodi (99) eeskujul järjehoidjaga.

276. Defineerida muutujapaarisEtte väärtuseks funktsioon, mis võtab argumendiks täis-arvude listil ja annab tulemuseks listi, mille alguses onl paaris elemendid ja seejärellpaaritud elemendid. Vältida tühja tööd arvutusel.

277. Defineerida muutujakolmeJärgi väärtuseks funktsioon, mis võtab argumendiks täis-arvude listil ja annab tulemuseks listi, mille alguses onl elemendid, mis annavad3-gajagades jäägi0, nende järel elemendid, mis annavad jäägi1, ning lõpus elemendid, misannavad jäägi2. Vältida tühja tööd arvutusel.

278. Defineerida muutujakeskmista väärtuseks funktsioon, mis võtab argumendiks ujuko-maarvude listi ja annab tulemuseks tema igast elemendist kõigi elementide aritmeetilisekeskmise lahutamisel saadava listi. Arvutamisel peab listi loetama ainult üks kord.

279. Defineerida muutujailmaÜhetaSummad väärtuseks funktsioon, mis võtab argumendiksmingi arvulisti l ja annab välja listi, milles on samapalju elemente kuil-s ja mille elementpositsioonili võrdub summaga listil kõigist elementidest peale selle, mis on positsioonili.Arvutamisel peab listi loetama ainult üks kord.

280. Defineerida muutujaakeskSees väärtuseks predikaat, mis võtab argumendiks ujukoma-arvude listi ja kontrollib, kas tema elementide aritmeetiline keskmine on ise seal element.Arvutamisel peab listi loetama ainult üks kord.

281. Defineerida muutuja, mille väärtustamisel tekib lõpmata palju veateateid.

157

Page 158: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Abstraheerimine

Kõrgemat järku funktsioonid

Igasugune parameetrilisus tähendab abstraheerimist — mingi seoseskeemi väljaeraldamist, miskehtib ühtsena parameetri kõigi väärtuste jaoks. Selle asemel, et kirjeldada iga konkreetne objekteraldi, kirjeldatakse terve objektide pere ühtsel viisil.Seda eesmärki teenib programmeerimisesenamik funktsioone.

Eelnevas on korduvalt esinenud vihjeid, et Haskellis on võimalik kasutada kõrgemat järku funkt-sioone, st funktsioone, mis võtavad funktsioone argumendiks. Kõrgemat järku funktsioonid esin-davad abstraktsiooni kõrgemal tasemel. Vaadeldes funktsiooni esindavana teatavat käitumist,esindab funktsiooniargumendiga funktsioon mingit üldisemat käitumist parameetrilisena teisekäitumise suhtes. Pole raske mõista, et sellised funktsioonid võimaldavad ühekorraga kirjeldadamitmekesisemat arvutuskäikude valikut ja on laiema kasutusega kui funktsioonid, mille argu-mentide hulgas funktsioone pole.

Oleme mõnda kõrgemat järku funktsiooni ka möödaminnes puudutanud: muutujatemap ning$ ja $! väärtuseks on nimelt kõrgemat järku funktsioonid. Need üksikud näited moodustavadkogu Haskelli võimalustest kõrgemat järku funktsioonide alal märksa vähem kui jäämäest temaveepealne osa. Ainuüksi Haskelli peamooduliPrelude kaudu on kasutatavad kümned ja kümnedeeldefineeritud kõrgemat järku funktsioonid, lisaks neiletulevad paljud ka teistest moodulitest.

Programmeerijal on endal võimalik kõrgemat järku funktsioone lihtsasti defineerida. See ei nõuaühtegi lisateadmist võrreldes sellega, mis käesolevas materjalis on muutujate defineerimise kohtaeespool juba ära toodud.

Paljud kõrgemat järku funktsioonid abstraheerivad teatavaid tüüpilisi rekursiooniskeeme. See tä-hendab, et osates neid funktsioone kasutada, pääseb programmeerija rekursiooniskeemi ilmuta-tud kirjapanekust, rekursiooniskeem on peidetud kõrgematjärku funktsiooni sisse. Näiteks võiksprogrammeerija iga ülesande puhul, kus on vaja mingit funktsiooni listi igale elemendile ra-kendada, realiseerida lahenduse omaette rekursiooniskeemiga, kuid operaatorimapkasutaminepäästab ta sellest vaevast.

Haskellis on kõrgemat järku funktsioonid enamasti ühtlasipolümorfsed, mis tähendab, et nad on

158

Page 159: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kasutatavad paljude erinevate tüüpide jaoks. Polümorfism teeb funktsioonid veelgi universaalse-maks.

Kui konkreetne ülesanne või alamülesanne lahendub mõne standardse universaalse funktsiooniotsese rakendusena, on halb stiil minna mööda võimalusest see ära kasutada. Väärtuste esitamineuniversaalsete funktsioonide rakendusena aitab lugejal koodist kiiremini aru saada, võrreldessellega, kui see väärtus oleks defineeritud ilmutatud rekursiooniskeemiga.

Tähtsamad eeldefineeritud kõrgemat järku funktsioonid

Kõik selles jaotises õpitavad muutujad on kättesaadavad moodulistPrelude.

Alustame tutvust eeldefineeritud kõrgemat järku funktsioonidega kõige lihtsamatest, mille üles-andeks on funktsioonide argumente mingil viisil ümber paigutada või struktureerida või neidfunktsioone mingis kindlas omavahelises suhtes kasutada.

Muutuja flip väärtuseks on funktsioon, mis võtab argumendikscurried-kujulise funktsioonif ja annab tulemuseks samuticurried-kujulise funktsiooni, mis töötab naguf , kuid võtab kaksesimest argumenti vastupidises järjekorras.

Näiteks avaldiseflip div väärtus on täisarvulise jagamise funktsioon tavalisega võrreldesvahetatud argumentidega, st ta võtab jagaja enne jagatavat. Seegaflip div 3 17 väärtus onsama misdiv 17 3 väärtus ehk5.

Kui on teadaflip argumentfunktsiooni teine argument, siis saabflip asemel kasutada parem-sektsiooni. (Kompilaator tegelikult kirjutabki paremsektsioonid ümberflip kaudu, mis kajas-tub ka veateadetes. Tuum-Haskellis sektsioone ei ole.) Näiteksflip div 2 väärtus on funkt-sioon, mis jagab oma argumenti täisarvuliselt2-ga, st sama mis sektsioonil(‘div ‘ 2).

Muutujatecurry ja uncurry väärtuseks on teisendused funktsioonidecurried-kuju ja järjen-diargumendiga kuju vahel.

Näiteksuncurry (+) väärtus on funktsioon, mis võtab argumendiks arvupaari ja annab tule-museks selle paari komponentide summa. Niisiisuncurry (+) (2 , 3) väärtus on5, sest(+) 2 3 väärtus on5. Avaldiseuncurry (flip div) väärtus on aga funktsioon, mis võ-tab argumendiks täisarvupaari ja annab tulemuseks teise komponendi jagatise esimesega. Niisiisväärtustadesuncurry (flip div) (3 , 17) , saame tulemuseks5.

Oleme eelnevas näinud, et muutujatefst ja const väärtuseks on sisuliselt sama operatsioon,lihtsalt teisel juhul on tacurried-kujuline. Seega avaldisedcurry fst ja const on ekviva-lentsed, samuti avaldiseduncurry const ja fst .

Kui funktsiooni rakendamine seisneb oma argumendile mingite funktsioonide järjestrakenda-mises, kusjuures need funktsioonid sellest argumendist eisõltu, siis on tegemist funktsioonidekompositsiooniga (ingl composition) (matemaatilise analüüsi terminoloogiasliitfunktsioo-

159

Page 160: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

niga).

Infiksoperaatori. väärtuseks ongi parajasti funktsioonide komponeerimine kui operatsioonkahel funktsioonil. Kuna tegemist oncurried-kujuga, siis rangelt võttes saab see funktsioon ühefunktsioonif argumendiks ja annab tulemuseks funktsiooni, mis omakordavõtab argumendiksteise funktsioonig ja annab väljaf ja g kompositsiooni, st funktsiooni, mis oma argumendile ra-kendab kõigepealt funktsioonig ja saadud tulemusele funktsioonif . Siit võib teha tähelepaneku,et operaatori. väärtuseks on selline kõrgemat järku funktsioon, mis oma argumentfunktsioonilerakendades annab tulemuseks jälle kõrgemat järku funktsiooni. Muidugi on tüübikorrektsusekstarvilik, et esimesena rakendatava funktsiooni väärtusetüüp võrduks teisena rakendatava funkt-siooni argumenditüübiga.

Näiteks avaldise(+ 2) . (* 5) väärtuseks on funktsioon, mis igal oma argumendil ar-vutab väärtuse, korrutades argumenti5-ga ja liites saadud tulemusele2. Niisiis avaldise( (+ 2) . (* 5)) 7 väärtus on37, eelnevas vaadeldud avaldisuncurry (flip div)on aga samaväärselt operaatoriga. ümber kirjutatav kujul(uncurry . flip) div .

Ülesandeid

282. Testida interaktiivse interpretaatori käsurealt funktsiooni flip erinevate mittekommuta-tiivsete argumentfunktsioonidega.

283. Veenduda interaktiivse interpretaatori käsurealt testides, etcurry fst töötab naguconst ja uncurry const töötab nagufst .

284. Lambdaavaldise\ x y -> y väärtuseks oncurried-kujuline funktsioon, mis võtab jär-jest kaks argumenti ja annab välja neist teise. Kirjutada veel kaks põhimõtteliselt erinevatsellesama väärtusega avaldist, mis sisaldavad ülimalt10 sümbolit.

285. Kirjutada võimalikult lühike avaldis, mille väärtuseks on funktsioon, mis arvutab väärtusi,võttes oma argumendist siinuse ja saadud tulemusest koosinuse. Testida interaktiivses kesk-konnas.

286. Kirjutada kaks täisargumenteeritud tüübikorrektsetavaldist, milles muutujadflip jauncurry oleksid järjest rakendatud: ühel juhul ühes, teisel juhul teises järjekorras. Kum-malgi juhul kirjutada seejärel avaldis samaväärselt ümber, lisades ka kompositsiooni.

287. Küsida interaktiivses keskkonnas muutujateflip , curry , uncurry , $, $! , . tüübid jasaada neist aru.

Järgmisena vaatame funktsioone, mille ülesandeks on oma argumentfunktsiooni itereerimine.

Muutuja iterate väärtuseks on kõrgemat järku funktsioon, mis võtab argumendiks sellisefunktsiooni f , mille argumendi- ja väärtusetüüp on üks ja sama, ja annab tulemuseks funkt-siooni, mis võtab sama tüüpi väärtusex argumendiks ja annab välja lõpmatu listi, mis koosnebväärtustestx, f x, f (f x) jne.

160

Page 161: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Näiteks avaldiseiterate (* 2) 1 väärtuseks on lõpmatu list kõigi2 astmetega, st1 : 2 : 4 : 8 : 16 : . . ..

Selliselt koostatud listidega oleme eespool juba kokku puutunud; varem defineerisime neid kaslistirekursiooniga või akumulaatoritega. Muutujatiterate kasutades võiksime näiteks dekla-ratsiooniga (88) või (90) antud muutujageom samaväärselt defineerida koodiga

geom a q= iterate (* q) a

.

Ka Fibonacci jada on analoogiliselt kirjeldatav, ainult etkuna iga element määratakse kahe, mitteühe eelmise poolt, siis tuleb itereerida paaride peal ja pärast võtta neist esimesed komponendid.Seetõttu pole tegemist just kõige efektiivsema viisiga Fibonacci jada arvutamiseks, ehkki keeru-kus on lineaarne. Koodiks tuleb

fibs= [fst p | p <- iterate ( \ (a , b) -> (b , a + b)) (0 , 1) ]

.

(143)

Paarid listis, mille iteratsiooniprotsess tekitab, koosnevad parajasti kahest järjestikusest Fibo-nacci arvust. Kui(a, b) on selline paar, siis järgmises paaris on esimene arv muidugi b, tei-ne on Fibonacci jadas talle järgneva + b. Sellest tuleb itereeritava funktsiooni kohale just\ (a , b) -> (b , a + b) . Esimesed kaks Fibonacci arvu on0 ja 1, seetõttu on alg-paari kohal(0 , 1) .

Mõneti sarnane samuti iteratiivset käitumist abstraheeriv kõrgemat järku funktsioon on muutujauntil väärtuseks. Tema võtab järjest argumentideks predikaadip, selle predikaadi argument-tüübist samasse tüüpi töötava funktsioonif ning sama tüüpi argumendix ning annab välja esi-mese väärtuse jadasx, f x, f (f x) . . ., mis rahuldab predikaatip, kui seal selline leidub, vastaselkorral on väärtuseks⊥ (arvutus jääb lõpmatusse tsüklisse).

Näiteks avaldiseuntil (> 100) (* 2) 1 väärtus on128, sest128 on esimene arvu2 aste,mis on suurem100-st.

Ülesandeid

288. Kasutades erisüntaksi asemel muutujatiterate , kirjutada avaldis, mille väärtuseks onmingi aritmeetiline jada.

289. Kirjutada avaldis, mille väärtuseks on lõpmatu list, mille elemendid on järjest kõik ainultsuurtest A-tähtedest koosnevad lõplikud stringid alustades tühjaga.

290. Anda kõrgemat järku funktsioonide abil uued lahendused ülesannetele 212 ja 213.

291. Anda ülesandele 270 uus lahendus kõrgemat järku funktsioonide abil.

161

Page 162: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

292. Kasutades ülesandes 191 defineeritud muutujatsummad’ , kirjutada muutujapascalväärtuseks lõpmatu list, mille element nrn on Pascali kolmnurga rida nrn listi kujul. Niiloodava listi elemente kui ka Pascali kolmnurga ridu loendame alates0-st.

293. Arvutada interaktiivse interpretaatori käsurealt kirjutatud ühe avaldise abil esimene arvu18aste, mis jagub486-ga.

294. Muutujauntil abil defineerida muutujacollatzPikkus väärtuseks funktsioon, mispositiivsel täisarvulisel argumendiln annab tulemuseks elementide arvun-ga algavas Col-latzi jadas.

295. Küsida interaktiivses keskkonnas muutujateiterate ja until tüübid ja saada neist aru.

Omaette rühma moodustavad kõrgemat järku funktsioonid, mis võtavad argumendiks predikaadija listi, kontrollivad predikaadi kehtivust listi elementidel ja koostavad selle analüüsi põhjal ühe-või teistsuguse tulemusväärtuse.

MuutujatakeWhile väärtuseks on funktsioon, mis võtab järjest argumendiks predikaadi ja lis-ti, mille elemenditüüp võrdub predikaadi argumenditüübiga, ning annab tulemuseks listi pikimaalgusosa, mille kõik elemendid rahuldavad predikaati.

Näiteks avaldise takeWhile (<= 100) (iterate (* 2) 1) väärtus on1 : 2 : 4 : 8 : 16 : 32 : 64 : [], sest need elemendid2 astmete listi alguses on100-st väiksemad,järgmine element128 aga pole. AvaldisetakeWhile (> 100) (iterate (* 2) 1)väärtus on aga tühi list, sest juba esimene element1 pole suurem100-st, st predikaati ei rahulda;see, et kuskil tagapool on ka predikaati rahuldavaid elemente, enam määravaks ei saa.

Muutuja dropWhile väärtuseks on funktsioon, mis võtab samad argumendid nagu eelmi-ne, kuid annab neil tulemuseks argumentlisti selle osa, mistakeWhile puhul üle jääb.Niisiis avaldisedropWhile (<= 100) (iterate (* 2) 1) väärtus on lõpmatu list128 : 256 : 512 : . . ., dropWhile (> 100) (iterate (* 2) 1) väärtus aga kogu2 ast-mete list.

Ka muutujafilter väärtuseks on funktsioon, mis võtab samad argumendid nagu kaks eelne-valt vaadeldud funktsiooni, kuid tema annab välja listi kõigist argumentlisti elementidest, mispredikaati rahuldavad.

Avaldise filter (> 100) (iterate (* 2) 1) väärtus on lõpmatu list128 : 256 : 512 : . . ., sest predikaadi mitterahuldamine esimeste elementide poolt ei sun-ni otsimist lõpetama. Avaldisefilter isUpper "Tere, Haskell! " väärtus on“TH”; võrdluseks takeWhile isUpper "Tere, Haskell! " annab väärtuseks “T” jadropWhile isUpper "Tere, Haskell! " annab “ere, Haskell!”.

OperaatoritetakeWhile ja dropWhile rakendamisel vaadatakse listi läbi ainult nii-kaugele, kuni leitakse predikaati mitte rahuldav element.Operaatorfilter vaatab agaalati listi kõik elemendid läbi. See võib valmistada üllatuse lõpmatu listi puhul, mil-les teatud kohast alates predikaat enam ühelgi elemendil eikehti: siis jääb tulemuslisti

162

Page 163: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

arvutamine lõpmatusse tsüklisse, selle asemel et list normaalselt ära lõpetada. Näiteksfilter (<= 100) (iterate (* 2) 1) väärtus on 1 : 2 : 4 : 8 : 16 : 32 : 64 :⊥, stosaline, mitte lõplik list.

Operaatorspan moodulist Prelude ühendab endastakeWhile ja dropWhile tege-vuse. Näiteksspan (<= 100) (iterate (* 2) 1) väärtuseks on paar listidega1 : 2 : 4 : 8 : 16 : 32 : 64 : [] ja 128 : 256 : 512 : . . .. Moodulist Data.List tuleb analoogilineoperaatorpartition , mille aluseks ontakeWhile asemelfilter . Tema väärtusekson funktsioon, mis võtab samasugused argumendid ning annabtulemuseks listipaari, kusargumentlisti elemendid on vastavalt argumentpredikaadikehtivusele kaheks jaotatud: ele-mendid, millel predikaat kehtib, on esimeses ning ülejäänud elemendid teises listis. Näitekspartition isUpper "Tere, Haskell! " väärtus on paar(“TH” , “ere, askell!”); koo-diga (116) defineeritud muutujakaheksLahuta saaks selle abil samaväärselt, kuid lihtsamaltdefineerida reaga

kaheksLahuta x xs= partition (< x) xs

. (144)

Lisaks võiks mainida muutujaidall ja any , mis ka võtavad samasugused argumendid nagutakeWhile , dropWhile ja filter , kuid nemad annavad tulemuseks tõeväärtuse:all pu-hul tulebTrue siis, kui listi iga element rahuldab predikaati jaFalse, kui mõni ei rahulda;anypuhul tulebTrue siis, kui listi mõni element rahuldab predikaati jaFalse, kui ükski ei rahulda.Väärtustamisel vaadatakse listi alates algusest senikaua, kuni leitakse element, mis vastavalt kaspredikaati ei rahulda (all puhul) või rahuldab (any puhul).

Näiteks avaldiseall isSpace väärtus on funktsioon, mis võtab argumendiks stringi ja annabtulemuseksTrue, kui see string koosneb ainult tühisümbolitest, jaFalse vastasel korral. Avaldise

filter (not . all isSpace) (145)

väärtus on seega funktsioon, mis võtab argumendiks stringide listi l ja annab tulemuseks listineist stringidest listisl, mis ei koosne ainult tühisümbolitest. Tõepoolest, antud predikaat onrahuldatud parajasti neil stringidel, mille kõigi sümbolite tühisuse kontrolli tulemusele eitustrakendades saameTrue.

Pisut keerulisema näitena kirjutame nüüd koodi, mis arvutab kõik algarvud. Osutub, et definit-sioonike

algs:: [Integer]

algs= let

isAlg n= all ( (/= 0 ) . (n ‘mod‘))

(takeWhile ( (<= n) . sqr) algs)in2 : filter isAlg [3 .. ]

163

Page 164: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

annabki muutujaalgs väärtuseks kõigi algarvude listi. (Liiga pika rea vältimiseks on aval-dise all ( (/= 0 ) . (n ‘mod‘)) argumenttakeWhile ( (<= n) . sqr) algsviidud järgmisele reale; see on Haskelli koodipaigutusreeglite järgi lubatud.) Muutujasqr peabolema eraldi defineeritud ruutfunktsioonina, näiteks koodiga (33).

Idee on järgmine. Teame, et2 on algarv. Edasi kasutame tuntud fakti, et iga kordarvn jagub mingialgarvuga, mis pole suurem kui

√n. Seega võib sõelale jätta parajasti need2-st suuremad täis-

arvudn, mis ei jagu ühegi sellise algarvuga, mille ruut pole suuremkui n. Operaatorifilterargumendiks oleva muutujaisAlg väärtuseks on lokaalselt defineeritud just seda kontrollivpredikaat. Tõepoolest: argumendiln ta kontrollib, kas algarvude listi algusjupis, milles olevatearvude ruut on ülimaltn, rahuldab iga element tingimust, etn jagamisel temaga saadav jääk onnullist erinev. Tegemist on listirekursiooniga: muutujaalgs definitsioon pöördubalgs endapoole. Kood jookseb, kuna iga järgmise arvu kontrollimiselläheb vaja ainult neid algarve, mison selleks ajaks juba leitud.

Ülesandeid

296. Defineerida muutujailmaVäikestetaAlgus väärtuseks funktsioon, mis argument-tekstil annab välja tema pikima väiketähti mitte sisaldavaalgusjupi.

297. Arvutada interaktiivse keskkonna käsureale kirjutatud ühe avaldise abil, mitu arvu3 asteton ülimalt100-kohalised.

298. Kasutades ülesande 238 lahendust, kirjutada avaldis,mille väärtuseks on list paaritutestkolmnurkarvudest.

299. Defineerida muutujaelimTaanded väärtuseks funktsioon, mis võtab argumendiks mingiteksti stringide listi kujul ja annab tulemuseks samal kujul teksti, kus stringide algusest ontühisümbolite jorud ära kaotatud.

300. Kirjutada avaldis (145) samaväärselt ümber, nii etall asemel oleks kasutatudany .

301. Defineerida muutujaelimJärjKorduvad väärtuseks funktsioon, mis võtab argumen-diks listi ja annab tulemuseks listi, mis erineb argumentlistist parajasti selle poolest, etjärjestikused võrdsed elemendid on asendatud ühe esinemisega.

302. Defineerida muutujaelimKorduvad väärtuseks funktsioon, mis võtab argumendiks lis-ti ja annab tulemuseks listi, milles esinevad parajasti kõik argumentlisti elemendid, kuidigaüks vaid ühe korra.

303. Kirjutada tüübikorrektne avaldis, kus muutujafilter argumendiks olekssnd ja järg-miseks argumendiks mõni mittetühi list.

Lugeja on juba tuttav operaatorigamap, mille abil rakendati listi igale elemendile mingit ühtja sama funktsiooni. Lihtsamad listikomprehensiooniga avaldised on võimalik niisama lihtsalt

164

Page 165: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ümber kirjutadamap abil, näiteks avaldised (42) ja (43) on vastavalt samaväärsed avaldiste-ga map (\ x -> x * x) [0 .. ] ja map (\ x -> (x , x * x)) [1 .. 9].Operaatorimap kasutamisel on see eelis, et funktsioon, mis listile rakendatakse, on ilmutatudkujul alamavaldisena esitatud.

Toome veel näite operaatorimapkasutamisest rekursiooniskeemis, ehkki ka siin pole listikomp-rehensiooni tundmise puhul midagi põhimõtteliselt uut.

Listi l osalistiks nimetatakse listi, mille elementideks on mingi suvaline valik l elementidestilma nende järjekorda muutmata. Olgu meil vaja defineerida muutujaosalistid väärtuseksfunktsioon, mis võtab argumendiks listi ja annab tulemuseks tema kõigi osalistide listi. Eeldame,et eesmärgiks on läbi käia just kõik võimalikud positsioonide valikud ja koostada listid vastavatelpositsioonidel olevaist objektidest, nii et kui argumentlistis esineb kordusi, siis tekivad kordusedka tulemusse. Siis sobib signatuur

osalistid:: [a] -> [[a]]

,

mis elemenditüübile kitsendusi ei sea, kuna kordumisi kontrollida pole vaja.

Lahenduse aluseks on tähelepanekud, et mittetühja listil osalistid on parajastil saba osalistid jalisaks needsamad saba osalistid koos listil peaga, tühjal listil on aga parajasti üks osalist — temaise. Mittetühja argumentlisti puhul tuleb niisiis vastus koostada rekursiivse pöördumise tulemu-sest listi sabal, võttes tema elemendid üks kord niisama, teisel korral aga lisada kõigi nende etteargumentlisti pea. Viimatimainitud muudatuse tegemisekskasutamegi operaatoritmap. Et mit-te arvutada listi saba osalistide listi mitu korda välja, toome tema salvestamiseks sisse lokaalsemuutujapool . Pannes “peaga” listid vastusesse enne teisi, saame definitsiooni

osalistid (x : xs)= let

pool= osalistid xs

inmap (x : ) pool ++ pool

osalistid _= [[] ]

. (146)

Operaatorimap analoog, mis tegutseb korraga kahel listil, onzipWith . Tema väärtuseks onfunktsioon, mis võtab järjest argumentideks mingi kahe argumendigacurried-kujulise funkt-siooni ja kaks listi, tulemuseks aga annab ühe listi, mille elemendid on saadud argumentlistidevastavate elementide kokkuarvutamisel selle funktsiooniga. Kui üks list on teisest pikem, siisülejääv osa unustatakse lihtsalt ära.

NäitekszipWith ( ∗ ) [2, 3] [5, 7, 11] väärtus on10 : 21 : []. Tulemuslisti esime-ne element10 saadakse kui argumentlistide esimeste elementide2 ja 5 korrutis, tulemuse teine

165

Page 166: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

element21 saadakse kui argumentide teiste elementide3 ja 7 korrutis ning et esimeses argument-listis rohkem midagi pole, siis lõpeb siin ka tulemuslist.

OperaatorizipWith abil saab elegantselt realiseerida muuhulgas arvutusi, mis nõuavad listi ka-he järjestikuse elemendi vaatlemist igal rekursioonisammul. Sellist funktsiooni otse defineeridaon mõneti kohmakas, näiteks võiks tuua operaatorisummaddefinitsiooni (75). KuidzipWithabil onsummadlihtsasti defineeritav koodiga

summad xs= zipWith (+) xs (tail xs)

. (147)

Et tail xs tulemuseks on argumentlisti nihe ühe komponendi võrra, siis listide vastavad ele-mendid, mis kokku liidetakse, ongi parajasti algses listisjärjestikused.

Pangem tähele, et definitsioon (147) töötab ka juhul, kuixs väärtus on tühi list jatail xs see-ga vigane. Põhjus peitub selles, etzipWith arvutamisel uuritakse argumentliste ainult niikaua,kui üks neist ära lõpeb, kusjuures esimest listi kontrollitakse selles suhtes enne; kui esimene liston tühi, siis antakse kohe tühi list välja ja teine list jääb üldse uurimata.

Operaatoritsummadläks vaja Fibonacci jada arvutamiseks listirekursiooni abil definitsiooniga(93). Asendades seal pöördumise funktsioonisummadpoole definitsiooni (147) parema poolejärgi, saame Fibonacci jadale uue definitsiooni

fibs= 0 : 1 : zipWith (+) fibs (tail fibs)

,

mille põhimõte on sama mis koodil (93). Pisut teisendades saame elegantsema definitsiooni

fibs @ ( _ : fs)= 0 : 1 : zipWith (+) fibs fs

. (148)

Ülesandeid

304. Küsida interaktiivses keskkonnas muutujatemap ja zipWith tüübid ja saada neist aru.

305. Kirjutada avaldis, mille väärtus on funktsioon, mis võtab argumendiks stringi ja annab tu-lemuseks tema iga tähe suureks teisendamisel saadava stringi.

306. Komprehensioonsüntaksit kasutamata arvutada stringkoodide järjekorras kõigist sümboli-test, mis kooditabelis leiduvad.

307. Komprehensioonsüntaksit kasutamata defineerida muutujavahedeKorrutis väärtuseksfunktsioon, mis võtab argumendiks arvude listi ja annab väärtuseks tema mistahes kahesterinevast kohast võetud elementide vahede (eespoolsest lahutatakse tagapoolne) korrutise.

166

Page 167: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

308. Kirjutada avaldis, mille väärtuseks on lõpmatu list lõpmatutest listidest. Anda see avaldisinteraktiivses keskkonnas argumendiks sellisele avaldisele, mille tulemusena leitakse igastlistist mõned elemendid.

309. Defineerida muutujanurk väärtusekscurried-kujuline funktsioon, mis võtab argumendikstäisarvun ja listide listi l, mida interpreteerime kahemõõtmelise tabelina, ja annab tule-museks selle tabeli vasaku ülemise nurga mõõtmetegan × n samamoodi listide listi kujul.Näiteks kuinurk argumentideks panna10 ja ülesande 161 lahenduseks olev avaldis, peabtulemuseks olema10 × 10 korrutustabel.

310. Kirjutada definitsioon (147) samaväärselt ümber, võttes eeskuju definitsioonist (148):tailpoole pöördumise asemel tuleb argumentlisti saba saada näidisesobitusest.

311. Kasutades operaatoritzipWith , defineerida muutujaindeksid väärtuseks funktsioon,mis võtab argumendiks suvalise listi ja annab tulemuseks listi, mis on niisama pikk kuiargumentlist ja mille elemendid on järjestikused naturaalarvud alates0-st.

312. Defineerida muutujaselekt väärtusekscurried-kujuline funktsioon, mis võtab argumen-diks tõeväärtuste listi ja arvude listi ja annab tulemusekslisti, milles neil positsioonidel,kus tõeväärtuste listis onTrue, on algse arvude listi vastavad elemendid, mujal aga nullid.Võib eeldada, et argumentlistid on ühepikkused (vastasel korral võib pikema listi ülejäävatosa ignoreerida).

313. Lahendada kõrgemat järku funktsioonide abil ülesanded 189 ja 190.

314. Lahendada kõrgemat järku funktsioonide abil ülesanded 237 ja 238.

315. Defineerida muutujadiagPööre väärtuseks funktsioon, mis võtab argumendiks listidelisti l: kui l on lõpmatu list lõpmatutest listidest, siis annab väärtuseks lõpmatu listi lõpli-kest listidest, milles esimene sisaldab parajasti esimeselisti esimest elementi, teine sisaldabesimese listi teise ja teise listi esimese elemendi, kolmasesimese listi kolmanda, teise teiseja kolmanda esimese elemendi jne.

316. Kui zipWith argumendi väärtus on kommutatiivne funktsioon, kas siis teeb alati samavälja, ükskõik kummas järjekorras anda listid?

Muutujafoldl väärtuseks on funktsioon, mis võtab argumentideks järjestbinaarse operatsiooni⊕, objektie ja listi l ning annab välja objekti, mis tuleb lõppvastuseks, kui alustame väärtusesteja igal sammul arvutame jooksva väärtuse paremalt operatsiooni⊕ abil kokku listi l järjekordseelemendiga, läbides nii kogu listi elemendid suunaga algusest lõpu poole. See tähendab, et kuilelemendid ona1, . . . , an, siis on tulemuseks(. . . ((e⊕ a1) ⊕ a2) ⊕ . . .) ⊕ an. Erijuhul, kui list ontühi, on tulemuseks lihtsalte.

Näiteks avaldisefoldl (+) 0 [1, 2, 3] väärtus on6, sest0 + 1 + 2 + 3 = 6. Avaldisefoldl (- ) 0 [1, 2, 3] väärtus on− 6, sest nüüd tuleb arvutada0− 1− 2− 3. Avaldisefoldl (++) [] [[1, 3], [7], [] ] väärtus on analoogselt1 : 3 : 7 : [].

167

Page 168: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Sarnaselt muutujagafoldl on muutujafoldr väärtuseks funktsioon, mis võtab argumentideksjärjest operatsiooni⊕, objekti e ja listi l ning annab välja objekti, mis tuleb lõppvastuseks, kuialustame väärtuseste ja igal sammul arvutame jooksva väärtuse vasakult operatsiooni ⊕ abilkokku listi l järjekordse elemendiga, läbides nii kogu listi elemendid suunaga lõpust alguse poole.See tähendab, et kuil elemendid ona1, . . . , an, siis on tulemuseksa1⊕ (a2 ⊕ (. . .⊕ (an⊕e) . . .)).Erijuhul, kui list on tühi, on siingi tulemuseks lihtsalte.

Näiteks avaldisefoldr (+) 0 [1, 2, 3] väärtus on 6, sest 1 + 2 + 3 + 0 = 6,st tulemus on sama mis samade argumentidegafoldl puhul. Samas avaldisefoldr (- ) 0 [1, 2, 3] väärtus on mitte− 6 nagu foldl puhul, vaid hoopis2,sest arvutatakse1− (2− (3− 0)) = 1− (2− 3) = 1− (− 1) = 2.

On ilmne, et assotsiatiivse kommutatiivse operatsiooni (nagu liitmine) ja lõpliku listi puhul anna-vad foldl ja foldr alati sama tulemuse. Sama saab öelda juhul, kui operatsioonon assotsia-tiivne ja mitte tingimata kommutatiivne, kuide on selle operatsiooni ühikelement. Tõepoolest:listi elemendid on mõlemal juhul omavahel samas järjestuses ja ühikelemendi korral on ükspuha,kummas listi otsas tema asub. Näiteks ka avaldisefoldr (++) [] [[1, 3], [7], [] ]väärtus on1 : 3 : 7 : [].

Pangem aga tähele, et isegi võrdse lõppväärtuse puhul ei pruugi olla ükskõik, kas kasutada ope-raatorit foldl või foldr . Viimases näitesfoldr korral tehakse kõigepealt tühja listi kon-kateneerimisel0 ümbertõstmist, siis1-elemendilise listi konkateneerimisel1 ümbertõstmine ja2-elemendilise listi konkateneerimisel2 ümbertõstmist; kokku tuleb3 ümbertõstmist. Operaatorifoldl puhul ehitatakse listi vasakult paremale: alguses konkateneeritakse tühi list (foldl teiseargumendi väärtus)2-elemendilise ette, seejärel saadud2-elemendiline1-elemendilise ette ja lõ-puks konkateneeritakse saadud3-elemendiline list tühja listi ette. Siin tuleb kokku0 + 2 + 3 = 5ümbertõstmist. Esimese listi elemente tõstetakse ümber kaks korda. Kerge on mõista, et midarohkem liste listis on, seda rohkem kordi tõstetakse alguses paiknevate listide elemente ümber.Operaatorifoldr korral tühiümbertõstmisi pole, kõik elemendid satuvad kohe oma õigessekohta.

Operaatorifoldl rakendamisel toimub arvutus põhimõtteliselt akumuleerivalt: vahetulemus-teks on teise argumendi algväärtus, selle kokkuarvutaminelisti esimese elemendiga, omakordatulemuse kokkuarvutamine listi teise elemendiga jne. Näiteks koodiga (111) defineeritud muu-tuja suurSumma (väärtuseks funktsioon, mis argumendiln leiab arvude1 kuni n neljandateastmete summa) võiks samaväärselt defineerida ka koodiga

suurSumma n| n >= 0

= foldl ( \ a i -> a + i ^ 4) 0 [1 .. n]| otherwise

= error "suurSumma: negatiivne liidetavate arv "

. (149)

Samaväärsus ei tähenda siin ainult samu tulemusi samadel argumentidel, vaid ka sarnast akumu-leerivat arvutusskeemi. Ka muutujada ja i on koodides (149) ja (111) sama tähendusega.

168

Page 169: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Operaatorifoldl kasutamine muudab küll koodi lühemaks ja loetavamaks, kuidtemaga kaas-neb puudus, et vahetulemusi ei väärtustata jooksvalt. Kui akumulaatorit tahetakse jooksvalt väär-tustada, nagu see toimub näitekssuurSumma definitsiooni (136) korral, siis tulebfoldl ase-mel kasutada moodulistData.List saadavat operaatoritfoldl’ .

Seevastufoldr rakendamisel pole tegu akumuleeriva arvutusega. Asi on selles, et listi elemen-tide arvutusse kaasahaaramine toimub niifoldl kui ka foldr puhul samas järjekorras — listialgusest lõpu suunas. Esimese puhul on see selge. Kuid kafoldr puhul, kui argumentide väär-tused on näiteks⊕, e ja l, siis tehakse kindlaks, kasl on mittetühi, ja kui nii, siis püütakse kohearvutadaa1 ⊕ x1, kusa1 on tema esimese elemendi väärtus jax1 kõigi ülejäänud elementide jaekokkuarvutuse tulemus, mis üldiselt pole veel teada;x1 arvutamiseks omakorda vaadatakse, kaslistis elemente veel on, ja kui nii, siis püütakse arvutadaa2 ⊕ x2, kusa2 on teine element jax2

kõigi järgnevate elementide jae kokkuarvutuse tulemus, jne.

Selline järjekord mängib tähtsat rolli laisa väärtustamise tõttu. Kui juhtub, etai ⊕ xi leidmisekspole objektixi üldse vaja, siis listi edasi ei uurita. Tänu sellele võib operaator foldr andaka lõpmatul listil lõpliku ajaga tulemuse. Samuti võib juhtuda, etai ⊕ xi on andmestruktuur,mille algusosa saab leida ilmaxi-d teadmata — siis see algusosa antakse enne listi järgneva osauurimist juba välja. Nii võibfoldr arvutus lõpmatul listil anda välja lõpmatu andmestruktuuri.

Kuna const rakendamisel kahele argumendile teist argumenti arvutus ei kasuta, siis näi-teks foldr const 0 [1 .. ] väärtus on 1 (listi esimene element). Aritmeetilisedoperatsioonid on realiseeritud agaratena, mistõttu avaldised kujul foldr (* ) 1 l, mil-le väärtus võiks ollal väärtuseks oleva listi kõigi elementide korrutis, tegelikult tulemustei anna, kuil väärtus on lõpmatu list. Samas on loomulik soov, et kui list sisaldab nul-le, antaks tulemuseks0. See saab täidetud, kui panemefoldr argumendiks(* ) asemel\ x p -> if x == 0 then 0 else x * p.

Seevastu kui operaatorifoldl viimase argumendi väärtus on lõpmatu list, jääb arvutus alatilõpmatusse tsüklisse ilma midagi välja andmata. Põhjus on selles, et sulgude paigutamisel vasa-kult listi elementide ümber on kõik alamavaldised lõplikudja isegi kui laisa väärtustamise tõttusaaksime neist midagi välja jätta, jääks ikka järele lõpmata palju sooritamata tehteid.

Kõigest hoolimata on operaatorfoldr tihti abiks konkateneerimisel tekkiva vahetulemuste ko-peerimise vältimisel. Näitena sellest anname koodiga (146) defineeritud muutujaleosalistiduue, konkatenatsioonivaba definitsiooni

osalistid (x : xs)= let

pool= osalistid xs

infoldr ( \ as ass -> (x : as) : ass) pool pool

osalistid _= [[] ]

. (150)

169

Page 170: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Erinevus on ainult avaldises võtmesõnain järel, kus listi saba osalistide listi järgi pannakse kogulisti osalistide list kokku varasemast erinevalt. Lisatudpeaga listide listi omaette ei moodustata,vaid need listid pannakse kohe oma lõplikku asupaika saba osalistide listi ees.

Siiski, kuna tulemuslisti pikkus on algse listi pikkuse suhtes eksponentsiaalne, on arvutus niidefinitsiooni (146) kui ka (150) järgi eksponentsiaalse keerukusega, mingit kolossaalset võitukonkatenatsiooni elimineerimine siin ei anna.

Kood (150) näitab, et operatsioon, millega kokkuarvutus toimub, ei pea üldjuhul olema samatüüpi argumentidega. Seal on operatsiooni esimese argumendi tüüp võrdne teise argumendi ele-menditüübiga. Nõutav on ainult see, et operatsiooni väärtusetüüp peab võrduma vastavalt kasesimese argumendi tüübiga (foldl puhul) või teise argumendi tüübiga (foldr puhul). Koodis(150) on see täidetud ja sellest piisab, et kokkuarvutuse käigus ei ilmneks tüübisobimatust.

Muus osas operaatorifoldl moodi töötava, kuid akumulaatori vaheväärtusi listi salvestavarekursiooniskeemi abstraktsioon on operaatorscanl . See töötab normaalselt ka lõpmatu lis-ti peal, sest annab pidevalt vahetulemusi välja. Näiteks definitsiooniga (107) antud muutujasuuredSummad (väärtuseks lõpmatu list, kus positsiooniln on arvude1 kuni n neljandateastmete summa) saab samaväärselt defineerida kujul

suuredSummad= scanl ( \ a i -> a + i ^ 4) 0 [1 .. ]

.

Operaatorscanl võimaldab defineerida Fibonacci arvude listi koodiga

fibs= 0 : scanl (+) 1 fibs

, (151)

mis on elegantsem kõigist senivaadelduist.

On olemas ka operaatorscanr , mis vastab operaatorilefoldr samamoodi naguscanl ope-raatorilefoldl .

Ülesandeid

317. Küsida interaktiivses keskkonnas muutujatefoldl , foldr , scanl , scanr tüübid jasaada neist aru.

318. Anda koodiga (149) defineeritud muutujalesuurSumma samaväärne definitsioonfoldrkaudu.

319. Anda koodiga (131) defineeritud muutujalehanoiSeisud uus definitsioon, mis väldibtühiümbertõstmisi.

320. Leida sellised argumendidh ja e, et avaldisefoldr h e l väärtustamine lõpetab min-gi l korral, mille väärtus on lõpmatu list, töö normaalselt lõpliku ajaga, kuid avaldise

170

Page 171: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

foldr (flip h) e l väärtustamine jääb igal korral, mille väärtus on lõpmatu list,lõpmatusse tsüklisse ilma midagi välja andmata.

321. Võidakse arvata, et akumulaatorit saab panna jooksvalt väärtustama, kui kirjutadasuurSumma definitsioon (149) ümber kujule

suurSumma n| n >= 0

= foldl ( ($! ) ( \ a i -> a + i ^ 4)) 0 [1 .. n]| otherwise

= error "suurSumma: negatiivne liidetavate arv "

.

Miks see arvamus ei ole õige?

322. Lahendada ülesanded 237 ja 238 operaatoriscanl abil.

323. Kasutades ülesandes 315 defineeritud muutujatdiagPööre , defineerida muutujapaarid väärtuseks list, mille elementideks on kõik naturaalarvupaarid.

324. Anda ülesandes 292 defineeritud muutujalepascal uus definitsioon ilma muutujatasummad’ , kasutades ülesandes 315 defineeritud muutujatdiagPööre .

325. Defineerida operaatorihanoiInt väärtuseks funktsioon, mis võtab argumendiks operat-sioonide jada kujul, mille produtseerib kood (118) või (119), ja annab tulemuseks listi kõigiseisudega, mis nende tõstete käigus tekivad.

Kõrgemat järku funktsioonide defineerimine

Kõrgemat järku funktsiooni definitsioon ei erine väliselt millegi poolest tavaliste funktsioonidedefinitsioonidest. Mingeid erilisi süntaktilisi konstruktsioone vaja ei lähe. Tuleb teada, et funkt-sioonitüüpi argumendi näidiseks kõlbab ainult kas muutujanäidis või jokker. Konstruktoriteganäidised on keelatud, ehknäidis ja laisk näidis aga ei oma sellisel juhul mõtet.

Näiteks kood

kõrgem f= f 1

(152)

teeb muutujakõrgem väärtuseks kõrgemat järku funktsiooni, mis võtab argumendiks arvuliseargumenditüübiga funktsiooni ja annab tulemuseks selle funktsiooni väärtuse kohal1. Funkt-sioonitüüpi argumendi näidiseks on muutujaf .

Et argumentf just funktsioonitüüpi on, selgub sellest, et paremas pooles rakendatakse teda argu-mendile1. Samast tuleneb, et argumentfunktsiooni argumenditüüp peab olema arvuline. Argu-mentfunktsiooni väärtusetüüp võrdub defineeritava kõrgemat järku funktsiooni väärtusetüübiga,

171

Page 172: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

sestkõrgem rakendamise tulemuseks on argumentfunktsiooni teatava rakendamise tulemus.Selle tüübi konkreetne iseloom aga ei selgu, ta võib olla misiganes. Kokkuvõttes võib operaatorikõrgem tüübisignatuuriks kirjutada

kõrgem:: (Num a)=> (a -> b) -> b

.

Avaldise kõrgem log väärtus on arv0, sest log 1 = 0. Samuti onkõrgem (5 +) jakõrgem (5 - ) korrektsed avaldised: esimese väärtus on6, teise väärtus4. Korrektne avaldison kakõrgem properFraction , mille väärtus on paar(1 , 0), sestproperFraction 1on korrektne ja väärtuseks on arvu1 täisosa ja murdosa paarina ehk(1, 0). Seevastukõrgem fst on tüübivigane, sestfst väärtuseks oleva funktsiooni argumenditüüp polearvuline, see on paaritüüp.

Küll aga saab operaatoritkõrgem rakendada argumentidele, mille väärtuseks on mingicurried-kujul funktsioonf , kui vaidf võtab esimesena argumendiks arvulist tüüpi objekti. Sel juhul onkõrgem rakendamisel saadud avaldise väärtuseks funktsioon, mis jääb ülef rakendamisel ar-vule 1. Näiteks aritmeetikatehted kõik sobivad. Nii näiteks on korrektne avaldiskõrgem (- ),mille väärtus on sama mis avaldisel(- ) 1 ehk funktsioon, mis lahutab oma argumendi arvust1.

Argumentide arv muutuja definitsioonis ei määra, milline peab olema argumentide arv tema ka-sutuses. Argumentide arvule seavad piiranguid ainult tüübid. Kuna kõrgem (- ) väärtus onfunktsioon, siis saame avaldiselekõrgem (- ) tüübikorrektselt veel ühe argumendi anda, kuigidefinitsioonis (152) on muutujalkõrgem näha ainult üks argument. Näitekskõrgem (- ) 5on igati korrektne avaldis, mille väärtus on−4.

Ülesandeid

326. Otsida Hugsi teegist mooduliPrelude algtekstist üles operaatorite$, flip , curry ,uncurry , . definitsioonid ja saada neist aru.

327. Defineerida muutuja? väärtuseks postfiksne funktsioonirakendamine (st ta peab võtmafunktsiooni oma argumendi järel (vastandina operaatorile$)).

328. Olgu muutujakõrgem antud definitsiooniga (152). Milliseid avaldistest

(: [88]), take , takeWhile , foldr , foldr (+)

saab tüübikorrektselt andakõrgem argumendiks? Mis onkõrgem rakendamisel saadavateavaldiste väärtus neil argumentidel, kui see on tüübikorrektne?

Praktilisema näitena vaatame, kuidas defineeridamap-laadne operaatormapIgaTeine , mille

172

Page 173: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

väärtuseks oleks funktsioon, mis võtab argumendiks funktsiooni ja listi ning rakendab funktsioo-ni listi igale teisele elemendile. Signatuur oleks

mapIgaTeine:: (a -> a) -> [a] -> [a]

,

mis näitab, et argumentfunktsiooni argumendi- ja väärtusetüüp langevad kokku. See on vältima-tu, sest tulemuslisti jäävad vaheldumisi vanad ja uued väärtused, kuid listi kõik elemendid peavadolema sama tüüpi. Definitsiooniks tuleb

mapIgaTeine f (x : y : ys)= x : f y : mapIgaTeine f ys

mapIgaTeine _ xs= xs

.

Defineeritud operaatorit saab kasutada mitmeks otstarbeks, näiteks olukorras, kus arvulistiiga teine element tuleb asendada vastandarvuga, või olukorras, kus igale teisele elemen-dile tuleb 1 liita. Näiteks avaldisemapIgaTeine negate [0 .. 4] väärtus onlist 0 :−1 : 2 :−3 : 4 : [], avaldise mapIgaTeine (+ 1) [0 .. 4] väärtus aga list0 : 2 : 2 : 4 : 4 : []. AvaldisemapIgaTeine toLower "KERA" väärtus on aga “KeRa”.

Koodiga (73) ja alternatiivselt koodiga (74) defineerisimemuutujaeemaldaEsimene väär-tuseks funktsiooni, mis võtab argumendiks mingi objekti jasama tüüpi objektide listi ja annabtulemuseks paari, mille esimene komponent on tõeväärtus, mis väljendab, kas objekt leiti listist,ja teine komponent on list, mis tekib, kui objekti esimene esinemine listist eemaldada (mitte-esinemise korral jääb sinna algne list). On lihtne üldistada see funktsioon kõrgemat järku funkt-siooniks, mis võtab esimeseks argumendiks mitte konkreetse objekti, mida listist otsida, vaidpredikaadi ja otsida tuleb seda predikaati rahuldavat elementi. Signatuur tuleks

eemaldaSellineEt:: (a -> Bool) -> [a] -> (Bool , [a])

.

Lähtudes koodist (73), saaksime definitsiooniks

eemaldaSellineEt p (x : xs)| p x

= (True , xs)| otherwise

= let(tv , us)

= eemaldaSellineEt p xsin(tv , x : us)

eemaldaSellineEt _ _= (False , [])

173

Page 174: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Endisele operaatorileeemaldaEsimene saaksime uue operaatori kaudu anda üherealise defi-nitsiooni

eemaldaEsimene a xs= eemaldaSellineEt (== a) xs

. (153)

Lisaks aga on uus operaator kasutatav ka juhul, kui eraldatav element pole üheselt antud. Näiteksesimese paaritu arvu otsimiseks listist võib kasutada operaatoriteemaldaSellineEt oddjne.

Ülesandeid

329. Otsida Hugsi teegist mooduliPrelude algtekstist üles muutujateiterate , until ,takeWhile , dropWhile , filter , map, zipWith , foldr , foldl , scanl definit-sioonid ja saada neist aru.

330. Defineerida muutujajärgFilter väärtuseks funktsioon, mis võtab järjest argumendikspredikaadip ja listi l ning annab välja listi neist listil elementidest järjekorda muutmata,mis l-s vahetult järgnevad mõnele predikaatip rahuldavale elemendile. Testida erisugusteargumentpredikaatidega.

331. Defineerida muutujabinAll väärtuseks kõrgemat järku funktsioon, mis võtab argumen-diks binaarse predikaadip ja annab välja funktsiooni, mis võtab argumendiks listi ja annabvälja True või False vastavalt sellele, kas list on lõplik jap mistahes kahest järjestikusestelemendist on tõene või leidub listis koht, kusp kahest järjestikusest elemendist on väär.OperaatoritbinAll kasutades kirjutada avaldisi, mis kontrollivad mingite listide kohta,kas nad on kasvavalt järjestatud, mittekahanevalt järjestatud, kahanevalt järjestatud, mitte-kasvavalt järjestatud, vahelduvate märkidega.

332. Defineerida otserekursiooniga muutujaloeDropWhile väärtuseks funktsioon, mis võtabjärjest argumendiks predikaadip ja listi l ning annab välja paari, mille esimene komponenton list, milles onl elemendid alates esimesest sellisest, mis tingimustp ei rahulda (tühi list,kui sellist ei leidu), ja teine komponent näitabl algusest äravisatud elementide arvu.

333. Defineerida funktsioonpikkZipWith , mis töötab naguzipWith , aga ei viska ülejäävatlistisaba ära, vaid jätab selle tulemuslisti lõppu.

334. Defineerida muutujavärskenda väärtusekscurried-kujuline funktsioon, mis võtab argu-mentideks funktsioonif , listi l ja arvui ning annab tulemuseks listi, mille saab lististl temai-ndale komponendile (lugemine alates0-st) funktsioonif rakendamisel.

335. Defineerida muutujamapUntil väärtusekscurried-kujuline funktsioon, mis võtab argu-mentideks predikaadip, funktsioonif ja listi l ning annab tulemuseks listi, mille saab lististl, kui rakendab järjest igale elemendile funktsioonif kuni esimese selliseni, mis annab tu-lemuseks tingimustp rahuldava väärtuse (viimane kaasaarvatud), järelejääv listisaba jääbtulemuslisti lõppu.

174

Page 175: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

336. Defineerida muutujamapIf väärtuseks funktsioon, mis võtab argumentideks predikaadi p,funktsioonif ja listi l, mille elemenditüüp langeb kokku niip kui ka f argumenditüübiga,ja annab tulemuseks listi, mille saabl-st kõigilep-d rahuldavatele elementidelef rakenda-misega.

337. Defineerida muutujakahekaupaMap väärtuseks kõrgemat järku funktsioon, mis võtabargumentidekscurried-kujul funktsiooni⊕ ja listi l ning annab tulemuseks listi, mille saab,jaotades listil elemendid järjest kahestesse rühmadesse ja rakendades igale paarile operat-siooni⊕ (kui l elementide arv on paaritu, siis viimane element jääb niisama tulemuslistilõppu).

338. Kasutades ülesande 337 lahendust, defineerida muutujapuuFold väärtuseks kõrgematjärku funktsioon, mis võtab argumendiks operatsiooni⊕ ja listi l ning arvutab listil ele-mendid operatsiooniga⊕ kokku, asetades sulud selliselt, et avaldise struktuur oleks puu-laadne. Täpsemalt, avaldise struktuur peab olema kahendpuu, kus igal hargneval tipul onolemas kaks alluvat ning iga sellise tipu vasak alluv on balansseeritud ja sisaldab vähe-malt samapalju lehti kui parem alluv. Näiteks kui listi elemendid ona1, a2, a3, a4, on tu-lemuseks(a1 ⊕ a2) ⊕ (a3 ⊕ a4); kui listi elemendid ona1, a2, a3, a4, a5, on tulemuseks((a1 ⊕ a2) ⊕ (a3 ⊕ a4)) ⊕ a5; kui listi elemendid ona1, a2, a3, a4, a5, a6, on tulemuseks((a1 ⊕ a2) ⊕ (a3 ⊕ a4)) ⊕ (a5 ⊕ a6); kui listi elemendid ona1, a2, a3, a4, a5, a6, a7, on tule-museks((a1 ⊕ a2) ⊕ (a3 ⊕ a4)) ⊕ ((a5 ⊕ a6) ⊕ a7).

339. Realiseerida järjestamine põimimismeetodil ülesandes 338 defineeritud muutujapuuFoldabil.

Koodiga (128) defineerisime muutujaaste väärtuseks arvude astendamise täisarvuga. See ka-sutas asjaolu, et astendamine esitub ühe ja sama arvu paljukordse korrutamise kaudu.

Ühe fikseeritud objektiga sama operatsiooni paljukordse sooritamise näiteid on matemaatikaspalju — olgu siin ühe silmatorkavaimana mainitud korrutamine, mis esitub samal moel liitmisekaudu —, mistõttu on mõttekas see algoritmiskelett välja abstraheerida.

Defineerime muutujakõrgAste väärtuseks kõrgemat järku funktsiooni, mis suudab leida mis-tahes etteantud üht tüüpi argumentidega binaarse operatsiooni naturaalarv korda sooritamise tu-lemusi. Lisaks astme alusele ja astendajale võtab ta argumendiks binaarse operatsiooni, millegakokkuarvutamine toimub, ja objekti, mis tuleb vastuseks anda 0-ga astendamise puhul. Jätamevaatluse alt välja astendamise negatiivse täisarvuga, mille jaoks meil oleks vaja veel ka pöördo-peratsiooni.

175

Page 176: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Valdavalt koodi (128) järgi tehes saame definitsiooniks

kõrgAste:: (Integral b)=> (a -> a -> a) -> a -> a -> b -> a

kõrgAste (* ) e a n= case compare n 0 of

GT-> a * kõrgAste (* ) e a (n - 1)

EQ-> e

_-> error "kõrgAste: negatiivne astendaja "

, (154)

kus kõrgAste esimene argument(* ) tähistab operatsiooni jae arvuga0 astendamise tule-must. Argumendida ja n on, nagu ka definitsioonis (128), astme alus ja astendaja.

Pangem tähele, et muutuja* ei ole definitsioonis (154) oma tavatähendusega, sest ta esinebformaalses parameetris ja on seetõttu siin definitsioonis seotud lokaalselt. Tema väärtuseks tulebmilline iganes operatsioon, miskõrgAste väljakutsel talle argumendiks antakse.

Tavalise täisarvulisse astmesse tõstmise operaatori saabnüüd defineerida koodiga

aste:: (Num a, Integral b)=> a -> b -> a

aste a n= kõrgAste (* ) 1 a n

(155)

erijuhuna kõrgemat järku astendamisest. Argumentoperatsiooniks oleme sellega fikseerinud kor-rutamise ja0-ga astendamise tulemuseks arvu1. Definitsioonis (155) tähendab* tavalist korru-tamist, sest ta pole lokaalselt seotud muus tähenduses. Seetähendab, etkõrgAste väljakutseldefinitsioonist (155) saab ka definitsiooni (154) lokaalne muutuja* väärtuseks tavalise korruta-mise.

Soovi korral võime defineerida teisi analoogilisi operaatoreid, andeskõrgAste argumentideksmidagi muud. Näiteks saab täisarvuga korrutamise defineerida korduva liitmisoperatsiooni kaudukoodiga

korrutis:: (Num a, Integral b)=> a -> b -> a

korrutis a n= kõrgAste (+) 0 a n

(156)

176

Page 177: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ja superastendamise korduva tavaastendamise kaudu koodiga

super:: (Floating a , Integral b)=> a -> b -> a

super a n= kõrgAste (** ) 1 a n

. (157)

Kui kõrgAste on kutsutud välja definitsioonist (156), siis definitsiooni(154) lokaalse muutuja* väärtus on liitmine, kui aga definitsioonist (157), siis ujukomaarvude astendamine.

Koodi (154) korrektseks kasutamiseks on vajae väärtuseks valida* väärtuseks oleva operat-siooni parempoolne ühik. Näiteks astendamise esitamisel korduva korrutamise kaudu oli sellekskorrutamise ühik ehk1, korrutamise esitamisel korduva liitmise kaudu aga liitmise ühik ehk0.Superastendamise definitsioonis oli selleks jälle1, sest1 on astendamise parempoolne ühik (igaarv astmes1 on arv ise). Kui seda printsiipi mitte järgida, hakkabe väärtus tulemust mõjutama.

Tuntud on matemaatikas ka funktsioonide astendamine, kus korratavaks operatsiooniks on kom-positsioon. Kompositsiooni ühik on samasusfunktsioon, mis tuleb moodulistPrelude muutujasid . Seega võime kirjutada näiteks avaldisekõrgAste (. ) id tail 2 , mille väärtus onlisti saba võtmise funktsiooni teine aste ehk kompositsioon iseendaga. Teisi sõnu on see sabavõtmise funktsiooni kahekordne rakendamine ehk saba saba võtmine. Et tegu on funktsiooniga,saame tõrgeteta lisada argumendi, näiteks avaldise

kõrgAste (. ) id tail 2 [1, 2, 3, 4, 5]

väärtus on3 : 4 : 5 : [].

Arvude astendamisest olid meil olemas ka efektiivsemad variandid (129) ja (130), mis kasuta-sid “jaga ja valitse” metoodikat koos dünaamilise programmeerimisega. Ka need on üldistatavadsamamoodi nagu kood (128). Lähtudes koodist (129), saame muutujalekõrgAste uue definit-siooni

kõrgAste (* ) e a n= case compare n 0 of

GT-> let

(q , r)= n ‘divMod ‘ 2

x = kõrgAste (* ) e a qinif r == 0 then x * x else a * x * x

EQ-> e

_-> error "kõrgAste: negatiivne astendaja "

. (158)

177

Page 178: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Definitsiooni (158) korral töötavad koodiga (155) defineeritud operaatoraste ja koodiga (156)defineeritud operaatorkorrutis palju kiiremini kui endise definitsiooni (154) korral. Võibrääkida logaritmilisest keerukusest, kuid siis tuleb täpsustada, et keerukus tähendab operaatori*rakendamiste arvu. See ei lange asümptootiliselt kokku ajalise keerukusega, sest üldjuhul midasuurem on astendaja, seda suuremaks lähevad arvutuse käigus argumendid ja seda aeganõudvamon operatsiooni ühekordne sooritamine.

Äärmiselt oluline on arvestada, et “jaga ja valitse” tehnikaga on astet võimalik arvutada vaid tänukorrutamisoperatsiooni assotsiatiivsusele, sest selline astendamisalgoritm baseerub sulgude üm-berpaigutamisel. Seega annab taolise definitsiooniga nagu(158) arvutamine korrektseid tulemusivaid assotsiatiivsete argumentoperatsioonide korral.

Näiteks ei ole võimalik koodiga (158) defineeritud funktsiooni kõrgAste rakendusena saadasuperastendamist, sest astendamine ei ole assotsiatiivne. Kood (157) töötaks lihtsalt valesti.

Seevastu funktsioonide kompositsioon on assotsiatiivne,seega funktsioonide astmete arvutami-ne töötab korrektselt ka koodiga (158). Kuid funktsioonideastendamise juures ei anna astenda-miseks “jaga ja valitse” tehnika kasutamine reaalselt mingit võitu efektiivsuses. Kompositsioo-nide arv tuleb küll astendaja suhtes logaritmiline, kuid antud juhul pole see oluline näitaja, sestkasutajat ei huvita mitte funktsiooni aste ise, vaid tema rakendamise tulemus teatavale argumen-dile. Kui nüüd funktsiooni aste on esitatud kujulg ◦ g, siis erinevalt tavalise astendamise juhustpole siin mingit kasu asjaolust, et operatsiooni argumendid on võrdsed, sest seda kompositsioonimingile argumendile rakendades sooritatakse kõigepealt üksg sellel argumendil ja teineg saadudtulemusel, mitte samal argumendil, mis võimaldaks arvutusressurssi kokku hoida. Kokkuvõttessooritatakse mingi funktsioonif mingi astme rakendamisel argumendile ikka astendajaga võrdnearvf rakendamisi.

Kõrgemat järku funktsioonide kasutamisel on tihti kõige raskemaks osaks äratabamine, et üksvõi teine operatsioon on erijuht teatavast lihtsast kõrgemat järku funktsioonist. Selle iseloomus-tamiseks toome kaks näidet kõrgemat järku astendamise kasutamisest olukordades, millel esma-pilgul ei paista astendamisega midagi ühist olevat.

Kõigepealt vaatleme järjekordselt Fibonacci jada. Kuidasvõiks kõrgemat järku astendamine ai-data kirjeldada funktsiooni, mis võtab argumendiks täisarvu n ja annab tulemuseks Fibonaccijadan-nda liikme?

Võtmerolli mängib tähelepanek, et kõik Fibonacci arvud on võimalik kätte saada maatriksiF

astmetest, kus

F =

(

0 11 1

)

.

Täpsemalt, kehtib seaduspära

Fn =

(

Fn−1 Fn

Fn Fn+1

)

. (159)

178

Page 179: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Tõepoolest, see ilmselt kehtibn = 1 korral ning kui ta kehtib mingii jaoks, siis

Fi+1 = F · F n =

(

0 11 1

)

·(

Fi−1 Fi

Fi Fi+1

)

=

(

Fi Fi+1

Fi−1 + Fi Fi + Fi+1

)

=

(

Fi Fi+1

Fi+1 Fi+2

)

.

Seega tuleks realiseerida2×2-maatriksite korrutamine. Selle töö lihtsustamiseks tasub teha veelüks tähelepanek. Kirjutades võrduses (159)Fn−1 kohale samaväärseltFn+1 − Fn, me näeme, etF kõik astmed on kujul

(

y − x xx y

)

, (160)

kus x, y on mingid täisarvud. Järelikult on meisse puutuvate maatriksite ülemine rida alumisereaga üheselt määratud ja me võime kõik vajalikud tegevusedära kirjeldada maatriksite alumisteridade peal.

MaatriksistF jääb järele(

1 1)

. Arvuga0 astendamine annab ühikmaatriksi, mille alumine ridaon

(

0 1)

. Lõpuks, kui meil on kaks maatriksit kujul (160), mille alumised read on vastavalt(

a b)

ja(

c d)

, siis korrutismaatriksi alumise veeru vasak komponent tuleba(d−c)+bc ehkad+bc−acning parem komponent tulebac + bd.

Vastavalt valemile (159) on Fibonacci jada liige järjekorranumbrigan leitav maatriksiF n alu-mise rea vasaku komponendina. Esitades kahekomponendilisi ridu paarina, saame kõige sellepõhjal otsitavaks definitsiooniks

fib n| n >= 0

= let(a , b) * +* (c , d)

= (a * d + b * c - a * c , a * c + b * d)infst (kõrgAste (* +* ) (0 , 1) (1 , 1) n)

| otherwise= ( if odd n then 1 else -1) * fib (-n)

. (161)

Kuna maatriksite korrutamine on assotsiatiivne ja meie operatsioon paaridel peegeldab maatrik-site korrutamist üksühele, on ka see operatsioon paaridel assotsiatiivne ja on võimalik kasutadakõrgAste definitsiooni (158). Siis toimub Fibonacci arvude leidminekoodiga (161) logarit-milise keerukusega järjekorranumbri suhtes. See on kolossaalne hüpe kiiruses. Meenutagem, etmuutujafib varasematest definitsioonidest esimene, naiivsel matemaatilise seose ümberkirju-tamisel põhinev (66), oli eksponentsiaalse keerukusega, kõik ülejäänud ((67), (112) ja (137)) agalineaarse keerukusega. Kood (161) on parem isegi koodist (94), mis lasi Fibonacci arve valmis-arvutatud listist otsida, sest ka listist otsimine on lineaarse keerukusega.

On huvitav märkida, et kui kasutada siiskikõrgAste lineaarse keerukusega definitsiooni (154),siis arvutuse käigus läbitakse täpselt samad vahetulemused nagu näiteks definitsioonide (67) ja

179

Page 180: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

(112) korral, mis itereerisid operatsiooni, mis seab igalepaarile(a, b) vastavusse paari(b, a + b).Tõepoolest, kuna astme alus on(1 , 1), siis paarist(a, b) järgmine aste on parajasti(1b + 1a −1a, 1a + 1b) ehk(b, a + b).

Viimaks vaatame kõrgemat järku astendamist kombinatoorset laadi ülesande lahendamisel. Olgumeil vaja defineerida muutujabitistringid väärtuseks funktsioon, mis võtab argumendikstäisarvun ja kui see pole negatiivne, siis annab tulemuseks listi, mille elemendid on parajastikõik bitistringid pikkusegan.

Paneme tähele, et kui meil on õnnestunud leida list, mille elementideks on kõik bitistringid pik-kusegan, siis selleks, et leida kõigi ühe võrra pikemate bitistringide listi, tuleb lisada kõigilebitistringidele pikkusegan ette kord 0, kord 1, ja moodustada tulemustest üks list. Et vaadeldaseda tegevust teatava binaarse operatsiooni rakendamisena, mille argumendid oleksid üht tüüpi,et oleks mõtet rääkida korduvast iseendaga opereerimisest, moodustame ka üksikutest bittidestbitistringide listi “0” : “1” : []. Siis kindla pikkusega bitistringide lististl ühe võrra pikemate bitist-ringide listi saamise viis, mis ülal kirjeldatud, tähendablistidest “0” : “1” : [] ja l kõikvõimalikelviisidel kummastki ühe elemendi (stringi) väljavõtmist jaomavahel konkateneerimist ning kõiginii saadud stringide listi moodustamist.

Operatsioon stringide listidel, milleni jõudsime, on sarnane hulkade otsekorrutisega; vahe onselles, et hulkade rollis on listid, ja teiseks selles, et erinevaist kogumitest võetud objektid kon-kateneeritakse, mitte ei moodustata lihtsalt paare. Kuna konkatenatsiooni rollis võib kergesti ettekujutada muid operatsioone, siis defineerime kõigepealt operaatoriotsekorruta väärtusekskõrgemat järku funktsiooni, mis võtab argumentideks binaarse operatsiooni⊕ ja kaks listi ningannab tulemuseks “otsekorrutise”, kus argumentlistide kõikvõimalikel viisidel võetud esindajadopereeritakse tehtega⊕. Koos signatuuriga saame koodi

otsekorruta:: (a -> b -> c) -> [a] -> [b] -> [c]

otsekorruta (* ) xs ys= [x * y | x <- xs , y <- ys ]

.

Nüüd näiteksotsekorruta (, ) [1, 2, 3] "AB" annab listi paaridest(1, A), (1, B),(2, A), (2, B), (3, A), (3, B). Meil aga on vaja stringe mitte paaridesse panna, vaid konkateneeri-da. Seepärast läheb käiku rakendusotsekorruta (++). Muutujabitistringid otsitavdefinitsioon koos signatuuriga on

bitistringid:: (Integral a)=> a -> [String ]

bitistringid n= kõrgAste (otsekorruta (++)) [""] ["0", "1"] n

. (162)

Pole raske veenduda, et kui operatsioon on assotsiatiivne,siis vastav otsekorrutisoperatsioon onsamuti assotsiatiivne. Et konkatenatsioon on assotsiatiivne, siis sellest nähtub, et koodi (162)

180

Page 181: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

poolt välja kutsutav operaatorkõrgAste võib olla defineeritud “jaga ja valitse” metoodikaga.Siiski, sarnaselt funktsioonide astendamisega, ei anna see siin võitu efektiivsuses.

Kokkuvõttes oleme näinud kõrgemat järku astendamise kolmerakendust arvudel (korrutamine,tavaastendamine, superastendamine) ja lisaks kolme väga erinevat kasulikku rakendust, kus as-tendatavad objektid ei olnud arvud. Meisterlikkus funktsionaalses programmeerimises eeldabniisuguste ühiste arvutusskeemide läbinägemist ja väljatoomist.

Ülesandeid

340. Kasutades koodiga (154) või (158) defineeritud muutujat kõrgAste , kirjutada interaktiiv-se interpretaatori käsurealt avaldis, mille väärtus saadakse siinuse100-kordsel rakendamiselarvule1.

341. Defineerida muutujakõrgFact väärtuseks kõrgemat järku funktsioon, mis võtab järjestargumendiks operatsiooni, tema parempoolse ühiku ja naturaalarvun ning arvutab arvudn, n − 1, . . . , 1 selle operatsiooniga kokku. Näiteks avaldisekõrgFact (^) 1 n väär-

tus, kuin väärtus on naturaalrvn, peab oleman(n−1)···1

. OperaatorikõrgFact kaudu de-fineerida uuesti samaväärselt ülesandes 177 defineeritud muutujafact ning anda muutujakolmnurk väärtuseks funktsioon, mis võtab argumendiks täisarvun ja annab väljan-ndakolmnurkarvu (st ülesandes 238 defineeritud listi liikme nrn; negatiivse argumendi korralvõib töö lõppeda veateatega).

342. Defineerida muutujatabel väärtuseks funktsioon, mis võtab argumendiks binaarse ope-ratsiooni⊕ ja koostab lõpmatu⊕-tabeli naturaalarvudel. Listi nrm element nrn peabolemam ⊕ n.

343. Kasutades ülesannetes 302, 315 ja 342 defineeritud operaatoreid, defineerida muutujaposRats väärtuseks list, milles esinevad kõik positiivsed ratsionaalarvud parajasti ükskord.

344. Defineerida muutujalõiguPoolitamine väärtuseks kõrgemat järku funktsioon, misvõtab argumendiks funktsiooni ja leiab tema nullkoha lõigupoolitamise meetodiga. Koodpeab töötama juhul, kui argumentfunktsioon on kasvav ja talon nullkoht.

345. Kasutades ülesandes 344 defineeritud muutujatlõiguPoolitamine ,

• lahendada uuesti ülesanne 247;

• defineerida muutujapoolringiPoolitaja väärtuseks funktsioon, mis võtab ar-gumendiks ujukomaarvua: kui a on 0 ja 1 vahel, siis leiab nurgasuuruseα, millekorral poolringi segment, mis ühelt poolt piirneb diameetri ja teiselt poolt kõõluga,mille otspunkt on diameetri otspunktist kaart pidi kaugusel α, moodustab pindalaltaosa kogu poolringist.

181

Page 182: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

346. Defineerida muutujamängväärtusekscurried-kujuline funktsioon, mis võtab argumendikskaks protseduuri, mis mõlemad edastavad tõeväärtuse, ja annab tulemuseks protseduuri,mis paneb need protseduurid omavahel järgnevalt kirjeldatavat mängu mängima ja edastabvõitja.

Mäng algab sellega, et mõlemad protseduurid täidetakse ühekorra. Kui nende edastatudväärtused on erinevad, siis on tõese edastanud protseduur kohe võitnud. Vastasel korralmängitakse sama mängu algusest peale uuesti.

347. Kasutades ülesande 346 lahendust, defineerida muutujaprotseduuriOlümpia väärtu-seks funktsioon, mis võtab argumendiks selliste protseduuride listi, mis edastavad tõeväär-tuse, ja annab välja protseduuri, mis korraldab nende protseduuride vahel olümpiasüsteemisturniiri ja edastab võitja. Kui mõnel etapil on järele jäänud paaritu arv võistlejaid, siis üheneist võib võistlemata järgmisse vooru lasta. Turniiri igamäng seisneb selles, et kaks protse-duuri edastavad kumbki oma tõeväärtuse: kui need on erinevad, siis tõese edastanu võidab,vastasel korral proovitakse samamoodi uuesti, niikaua kuisaadakse erinevad tõeväärtused.

348. Anda koodiga (70) defineeritud muutujalesq2 samaväärne definitsioon, mille järgi arvu-tades oleks aritmeetiliste operatsioonide arv argumendi suhtes logaritmiline.

349. Kasutades koodiga (158) antud muutujatkõrgAste , defineerida muutujaposArvudväärtuseks funktsioon, mis võtab argumendiks numbrite listi ja täisarvun ning annab tu-lemuseks listi järjekorras kõigistn numbriga kirjutatud arvudest positsioonilisel kujul, kusnumbrid tulevad antud listist (arv võib alata ka nulliga). Võib eeldada, et numbrite listis eiole kordumisi jan > 0.

Argumendivaba stiil

Funktsionaalses paradigmas, kus funktsioonid on esimese klassi väärtused, on võimalik funkt-sioone kirjeldada otse teiste funktsioonide kaudu ilma argumente tähistamata. Seda abstrahee-rimisvõtet, kus funktsioonide argumendid n-ö kombineeritakse vajalike andmete hulgast välja,nimetatakseargumendivabaks stiiliks (ingl point-free style). Argumendivaba stiil on vägalaialt kasutatav nii funktsionaalses programmeerimises kui ka matemaatilistes distsipliinides.

Ekstensionaalsus

Argumendivaba stiili aluseks on matemaatiliste funktsioonide fundamentaalne omadus, mida ni-metatakseekstensionaalsuseks (ingl extensionality) — alati, kui funktsioonidf ja g onsama määramispiirkonnaga ja annavad igal sealt pärit argumendil sama tulemuse, onf = g.Funktsioonid on võrdsed, kui nad annavad samadel argumentidel sama tulemuse; matemaatilinefunktsioon ongi puhas seos argumentide ja väärtuste vahel,ta ei hõlma mitte mingit lisateavetnagu nimi, identiteet vms.

182

Page 183: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ekstensionaalsus võimaldab selle asemel, et kirjutada, etf x = g x iga võimaliku argumendixkorral, kirjutada lihtsaltf = g. Samal põhimõttel võib argumente ära jätta Haskelli definitsioo-nidest. Kui defineeritava funktsiooni töö seisneb lihtsaltoma argumendi ettesöötmises mingileteisele funktsioonile, võib selle argumendi mõlemalt poolt võrdusmärki ära “taandada”.

Vahetu näite saame, kui vaatleme põimimismeetodil järjestamise abioperaatorihakid definit-siooni koodist (127): jättes mõlemalt poolt ära argumendixs , saame samaväärse definitsiooni

hakid= segmendid

. (163)

Mõte on nii või teisiti see, et muutujatehakid ja segmendid väärtuseks on sama funktsioon,ning kood (163) väljendab seda mõneti otsesemalt kui varasem.

Seejuures tasub tähele panna, et kui koodis (127) pole signatuur kohustuslik, siis argumendi-vabale stiilile üleminekul muutub ta kohustuslikuks. Definitsioon (163) ilma signatuurita annabtüübivea.

Analoogiliselt võib definitsiooni pooltele ühiseid argumente lõpust ära jätta ka juhul, kui need ar-gumendid pole ainsad. Näiteks saab taandada ühise argumendi xs muutujaeemaldaEsimenedefinitsioonist (153), saades koodi

eemaldaEsimene a= eemaldaSellineEt (== a)

, (164)

kus siiski on mõlemale poole üks argument järele jäänud. Hoolimata sellest asjaolust on teguargumendivaba stiili näitega, sest ka ühe argumendi kaotamisel paljude seast on tegu väärtusekirjeldamisega abstraktsel funktsioonitasemel.

Samal põhimõttel saab funktsioonitasemele viia ruutjuureleidmisel lõigu poolitamise meetodigakasutatud abioperaatorialgpiirid definitsiooni (114). Selleks kirjutame kõigepealt paremapoole paari ümber prefiksse rakendamisega, saades definitsiooni

algpiirid n= (, ) 1 n

,

sellest aga saame juba tuttaval viisil argumendivabas stiilis definitsiooni

algpiirid= (, ) 1

. (165)

Argumenti saab vasaku ja parema poole lõpust kaotada ainultsiis, kui see argument ei esinedefinitsioonis mujal. See, kui definitsiooni parem pool sisaldab seda muutujat veel, tähendaks, eterinevatele argumentidele võiks seal rakenduda erinev funktsioon. Ekstensionaalsusseadus sellistolukorda ei käsitle.

183

Page 184: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Näiteks operaatorisqr definitsioon (33) omandab prefikssele rakendamisele üleminekul sama-väärse kuju

sqr x= (* ) x x

.

Selles aga mõlemalt poolt argumentix kaotada ei saa: etx esineb paremal pool kaks korda,jääb tema üks esinemine sinna alles, kuid vasakule poole mitte, mis tähendab, et tegu oleksdefineerimata muutujaga, tulemus poleks süntaktiliseltkikorrektne.

Kui definitsiooni mõlema poole lõpus on mitu ühist argumentisamas järjekorras, siis võib nadkõik ära jätta, sest samahästi võiksime nad kaotada ühekaupa.

Näiteks kõrgemat järku astendamise rakendused koodides (155), (156), (157) on argumendivabasstiilis samaväärselt vastavalt

aste= kõrgAste (* ) 1

,

korrutis= kõrgAste (+) 0

,

super= kõrgAste (** ) 1

.

Õpiku alguses sai möödaminnes mainitud, et Haskelli väärtuste maailmas ekstensionaalsus tege-likult rangelt võttes ei kehti. Nimelt funktsioonitüübi bottom ja sama tüüpi “päris” funktsioon,mis suvalisel argumendil annab tulemuseks väärtusetüübi bottomi, on käitumiselt eristamatud.Näiteks on selge, et avaldiste

undefined :: Int -> Int ,

( \ x -> undefined x) :: Int -> Int

rakendamine ükskõik millele lõpeb veateatega, rakendamisega pole nende avaldiste väärtusi või-malik eristada. Samas nad siiski on ilmutatud viidatavuse põhjal erinevad, sest leidub funktsioon,millele neid endid argumendiks andes on tulemused erinevad. Viimases võime lihtsalt veenduda,kasutades agara väärtustamise operaatorit$! :

const 0 $! (undefined :: Int -> Int)

väärtustamine lõpeb veaga, samal ajal kui

const 0 $! (( \ x -> undefined x) :: Int -> Int)

väärtustamine annab vastuse0.

Kuna kirjeldatud ekstensionaalse rikkumised on perifeerset laadi — reaalses programmeerimiseskonstantsest bottomist vaevalt et abstraheerida tuleb —, võib ekstensionaalsuse lugeda Haskellissiiski praktiliselt kehtivaks.

184

Page 185: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

350. Otsida Hugsi teegist üles muutujasubtract definitsioon ja saada temast aru.

351. Lahendada ülesanne 341 uuesti, defineerides muutujadfact ja kolmnurk argumendiva-bas stiilis.

352. Lahendada ülesanne 349 argumendivabas stiilis.

353. Anda koodiga (162) defineeritud muutujalebitistringid võimalikult lühike uus defi-nitsioon ülesandes 352 defineeritud muutujaposArvud kaudu.

354. Anda koodiga (104) defineeritud muutujalekordusteta samaväärne definitsioon argu-mendivabas stiilis.

Argumentide peitmine

Nagu nägime muutujaalgpiirid defineerimisel koodiga (165), võib argumendivaba stiili ra-kendamiseks olla vaja algset definitsiooni mudida, et parempool viia nõutavale kujule. Teinekordvõib argumendivaba stiili kasutamiseks teha definitsioonis suuremaidki ümberkorraldusi või ju-ba definitsiooni kirjutamisel sellega arvestada. Tüüpiline võte on kõrgemat järku funktsioonidesissetoomine, et esitada parem pool millegi rakendamisenaõigele argumendile.

Vaatame järjestamise kiirmeetodi realisatsiooni akumulaatoritehnikaga, operaatorijärjestaKiir definitsiooni (120). Defineeritava muutuja ainus argumentxs esinebküll paremas pooles — võtmesõnain järel avaldiseskiir xs [] —, kuid et ta pole viimaneargument, ei ole tema vahetu taandamine võimalik.

Kuna selles kohas on rakendatavaks operaatoriks sama definitsiooni lokaalne operaatorkiir ,on üks lahendusvõimalus lokaalses definitsioonis argumentide järjekord vahetada. Siis vahetuvadühtlasi argumendid ka väljakutsel,xs saab viimaseks ning üleminek argumendivabale stiililevõimalikuks.

See aga pole hea lahendus mitmel põhjusel. Kõigepealt halvendaks argumentide järjekorra va-hetamine lokaalse operaatori definitsiooni loetavust. Akumulaatoriga definitsioonid on tavaliseltkõige paremini jälgitavad just siis, kui akumulaator on viimane argument, sest siis satuvad jär-jestikku toimuvad tegevused oma õiges järjekorras ka definitsiooni. Teiseks on antud juhul tegun-ö passiivse akumulaatoriga, mis, nagu kohe ka näeme, võimaldab selle akumulaatori enda ar-gumendivaba stiili abil kergesti definitsioonist kaotada,selleks aga peab ta olema viimane.

Seepärast läheneme olukorrale teisiti: kirjutame avaldisekiir xs [] samaväärselt ümber ku-jul flip kiir [] xs . Selles avaldises onxs viimane argument, nii et tee argumendivabalestiilile on avatud.

185

Page 186: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Viime nüüd argumendivabale stiilile üle ka koodi (120) lokaalse definitsiooni. Esimese deklarat-siooni parem pool kirjeldab, mida akumulaatoriga tehakse:talle rakendubkiir vs , tulemuseette lähebx : , mida võib väljendada sektsiooni rakendamisena, ja tulemusele omakorda raken-dubkiir us . Kirjutades järjestikused rakendamised ümber kompositsiooni abil, tekib avaldis(kiir us . (x : ) . kiir vs) as . Teise deklaratsiooni paremaks pooleks on lihtsaltas . Ka selle saab samaväärselt rakendamiseks ümber teha, lisades operaatoriid , mille väärtuson samasusfunktsioon. Seejärel on võimalik mõlemas deklaratsioonis akumulaatori ilmutatudesinemised lihtsalt ära võtta. Tulemuseks on deklaratsioon

järjestaKiir= let

kiir (x : xs)= let

(us , vs)= kaheksLahuta x xs

inkiir us . (x : ) . kiir vs

kiir _= id

inflip kiir []

. (166)

Kui algse definitsiooni (120) kirjutasime loetavust ohvriks tuues akumulaatori abil, et vältidatühiümbertõstmisi, siis argumendivabas stiilis definitsioonis (166) õnnestus ühendada väljenduseselgus ja arvutuse efektiivsus. Viimases ütleb lokaalse muutuja kiir definitsioon tähelepanudetailidele viimata otse ära järjestamise kiirmeetodi põhimõtte!

Kirjutame nüüd muutujatõstaLõppu väärtuseks funktsiooni, mis võtab argumendiks täisarvun ja listi l ja tõstab listisl esimesedn elementi järjekorda muutmata lõppu. Juhud, kusn onindeksipiiridest väljas, pole huvitavad ja võime neid käsitleda nagu teevad standardoperaatoridtake , drop ja splitAt .

Püüame kasutada argumendivaba stiili, esitades vajalikudtegevused väiksemate operatsioonidekompositsioonina. Kui arvn ja list l on teada, siis kõigepealt tulebl jagadan-nda elemendikohalt kaheks jupiks. Et mõlemat juppi läheb pärast vaja, siis proovime kasutada operaatoritsplitAt argumendi väärtusegan. Sellega saame vahetulemuseks listijuppide paari. Edasi tu-leks nende järjekord vahetada ja nad siis jälle kokku panna.Kokkupanekuks on operaator++.Paari komponentide vahetamiseks standardoperaatorit pole, kuid paneme tähele, et ka++ tahabargumendiks mitte paari, vaid liste ühekaupa. Ühekaupa rakendamisel organiseerib argumentidevahetuse operaatorflip . Et paarile rakendamine oleks võimalik, tuleb rakendatavale funktsioo-

186

Page 187: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

nile teha kujuteisendus operaatorigauncurry . Nii kujuneb kood

tõstaLõppu:: Int -> [a] -> [a]

tõstaLõppu n= uncurry (flip (++)) . splitAt n

. (167)

Argumendivabale stiilile üleminek ei ole aga kindlasti mingi imerohi, mis alati vaieldamatultkoodi loetavamaks teeb.

Vaatame näiteks uuesti operaatoriteemaldaEsimene , mille definitsioonist (153) õnnestus ar-gumendivaba stiili kasutades ära kaotada üks argument kahest ja saada lühem definitsioon (164).Osutub, et pole probleem ka teine argument ära kaotada. Et võrduse argumentide järjekord poletähtis, võib definitsiooni (164) samaväärselt ümber kirjutada definitsiooniks

eemaldaEsimene a= eemaldaSellineEt ( (==) a)

.

Nüüd on aga kohe näha paremas pooles olev järjestrakendamine, mistõttu on saadud definitsioonsamaväärne definitsiooniga

eemaldaEsimene= eemaldaSellineEt . (==)

. (168)

Algajale definitsioon (168) vaevalt et loetavam on kui definitsioon (164).

Funktsionaalses programmeerimises kogenud tegijale definitsiooni (168) mõistmine esimesestpilgust raskusi ei valmista (muidugi eeldusel, et muutujaeemaldaSellineEt tähendus onselge), kuid üldjuhul on võimalik argumendivaba stiili ohjeldamatult viljeldes kood ükskõikkellele loetamatuks muuta. Niisiis, see võte nõuab teatavat mõõdukust.

Ülesandeid

355. Otsida Hugsi teegist üles muutujateodd , any , all , elem definitsioonid ja saada neistaru.

356. Defineerida argumendivabas stiilis muutujajäägid väärtuseks funktsioon, mis võtab ar-gumendiks täisarvun ja annab välja listi, mille elementideks on suurusjärjestuses kõik mit-tenegatiivsedn-st väiksemad täisarvud.

357. Defineerda võimalikult lühidalt muutujaflipInfiks väärtusekscurried-kujuline funkt-sioon, mis võtab järjest kolm argumentix, f , y, millest teine on isecurried-kujuline funkt-sioon, ja annab neil tulemuseksf y x. (Kui f kohal on infiksoperaator⊕, siis on mõnetitegemist infiksoperaatori argumentide vahetamisega, sesttulemuseks ony ⊕ x.)

187

Page 188: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

358. Kirjutada ülesandele 234 uus lahendus, mis kasutaks argumendivaba stiili nii põhioperaatorikui ka abioperaatori definitsioonis.

359. Lahendada ülesanne 309, kasutades argumendivaba stiili.

360. Anda ülesandes 138 defineeritud muutujaletakeLõpust uus samaväärne definitsioon,milles ilmutatult esineb vaid üks argument.

361. Anda ülesandes 138 defineeritud muutujaletakeLõpust selline samaväärne definitsioon,milles ükski argument pole ilmutatud.

362. Anda koodiga (144) ja (167) defineeritud muutujatelekaheksLahuta ja tõstaLõppusamaväärsed definitsioonid, kus ükski argument poleks ilmutatud.

363. Kirjutada muutujasuurSumma definitsioon (111) samaväärselt ümber nii, et lokaalse ope-raatori definitsioonis oleks kasutatud argumendivaba stiili.

364. Lahendada ülesanne 363, kui aluseks on definitsiooni (111) asemel definitsioon (136), starvutamisel peab akumulaatoris toimuma jooksev väärtustamine.

365. Kas on võimalik definitsioon (126) samaväärselt ümber kirjutada argumendivabas stiilis?

Vaatame lähemalt selliste argumendivabas stiilis definitsioonide koostamist, kus kirjeldatavfunktsioon väljendataksefoldr või foldl rakendamisega kahele argumendile.

Operaatorfoldr abstraheerib kõiki selliseid rekursiooniskeeme listidel, kus tulemus mittetühjallistil leitakse listi pea ja sama arvutuse tulemuse järgi listi sabal. See tuleb välja esitusesta1 ⊕(a2 ⊕ (. . . ⊕ (an ⊕ e) . . .)), kus väärtus on väljendatud listi peaa1 ja listi sabale vastava esitusea2 ⊕ (. . . ⊕ (an ⊕ e) . . .) kaudu.

Niisugune rekursiooniskeem esineb näiteks muutujanullideArv otserekursiivses definitsioo-nis (71). Tuletame sellest süstemaatilise analüüsiga argumendivabas stiilis definitsiooni operaa-tori foldr kaudu. Selguse mõttes kirjutame uue definitsiooni nii, etfoldr argumendid onseotud lokaalsete muutujategaop ja e.

Lihtne on kirjutadae definitsioon. Kuna see argument tulebfoldr op e väärtustamise käigustühja argumentlisti puhul lõppvastuseks, siis võtame definitsiooni paremaks pooleks originaal-definitsiooni tühja listi juhu parema poole. Antud näites (71) on selleks0.

Operaatori op definitsiooni kirjutamiseks kujutame ette, et tema argumentideks tulevadnullideArv mittetühja argumentlisti pea, mis originaalkoodis (71) seotakse muutujagax ,ja rekursiivse pöördumise tulemus argumentlisti sabal — koodis (71) on see seotud muu-tujaga ülejäänu . Kuna op läheb foldr argumendiks, siis teda kutsutakse välja ainultsellistes olukordades. Tulemus peab ses olukorras olema sama mis nullideArv raken-damisel kogu argumentlistile — seda aga väljendab originaaldefinitsioonis tingimusavaldisif x == 0 then 1 + ülejäänu else ülejäänu .

188

Page 189: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Selle analüüsi tulemuseks on definitsioon

nullideArv= let

op x ülejäänu= if x == 0 then 1 + ülejäänu else ülejäänu

e = 0infoldr op e

. (169)

Kõik originaaldefinitsiooniga ühised muutujad on kasutusel samas tähenduses.

Proovime sama teha ka operaatorigakaheksLoe . Tema senised definitsioonid on koodides(78), (79), (80), (99). Neist (79) on selline, kus rekursiooniskeem rahuldab ülaltoodud tingimust,seegafoldr abil üleminek argumendivabale stiilile on võimalik.

Tühjal listil tuleb (79) järgi välja anda([] , []) . Mittetühja argumentlisti pea on koodis(79) seotud muutujagax , rekursiivse pöördumise tulemus argumentlisti sabal aga näidisega(us , vs) . Tulemus ses olukorras on originaaldefinitsiooni järgi(x : vs , us) . Seegaeelmise näitega analoogiline analüüs viib definitsioonini

kaheksLoe= let

op x (us , vs)= (x : vs , us)

e = ([] , [])infoldr op e

. (170)

Jällegi on kõik originaaldefinitsiooniga ühised muutujad kasutusel samas tähenduses. Kaotadesmõneti tarbetud lokaalsed definitsioonid, saame definitsiooni

kaheksLoe= foldr ( \ x (us , vs) -> (x : vs , us)) ([] , [])

. (171)

Definitsioonid (170) ja (171) on küll samaväärsed omavahel,kuid pole samaväärsed originaalde-finitsiooniga (79). Vahe tekib lõpmatutel listidel, kus arvutus definitsiooniga (170) või (171) jääblõpmatusse tsüklisse ilma mingigi väljundita, samas kui definitsiooniga (79) arvutades saamepaari kahest listist (millest küll kumbki pole lõplik, kuidkõik nende elemendid saadakse kätte).

Viga definitsioonile (170) üleminekul tekkis sellest, etop teiseks argumendiks sattus agar näi-dis (us , vs) . Seetõttu tema sobitamine tegeliku argumendiga tehakse enneop rakendamist.Et antud kontekstis tuleb teiseks argumendiks rekursiivsepöördumise tulemus, siis ei saa ope-ratsiooni rakendada enne, kui rekursiivne pöördumine listi sabale on paari välja andnud — seeaga ei juhtu enne, kui ta on rakendanud operaatoritop. See nõiaring osutab, et esimene paar

189

Page 190: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

genereeritakse alles siis, kui list on lõpuni läbi käidud. Lõpmatu listi puhul aga ei juhtu sedakunagi.

Olukorra parandamiseks piisab probleemne paarinäidis laisaks muuta, et operatsiooni täitmisegaalustataks enne rekursiivse pöördumise lõpetamist. Saamedefinitsiooni

kaheksLoe= let

op x ~(us , vs)= (x : vs , us)

e = ([] , [])infoldr op e

või, samaväärselt ilma lokaalsete deklaratsioonideta,

kaheksLoe= foldr ( \ x ~(us , vs) -> (x : vs , us)) ([] , [])

.

See ongi laisa näidise tüüpilisim praktiline kasutus.

Algne mittevastavus originaaldefinitsiooniga (79) tekkissellest, et seal oli rekursiivse pöördu-mise tulemus seotud lokaalse deklaratsiooniga ja lokaalsete deklaratsioonide poolt seotavaidnäidiseid käsitletakse alati laisalt. Seda on eespool ka selgitatud: avaldiste (39) ja (40) väärtusta-mine käib erinevalt. Pärast näidise laisakssundimist saadud definitsioonid on definitsiooniga (79)täiesti samaväärsed.

Vaatame veel koodiga (146) või (150) defineeritud operaatorit osalistid . Ka need võibfoldr abil argumendivabas stiilis ümber teha; võtame siin aluseks koodi (150). Tulemus tüh-jal listil on [[] ], mittetühja listi pea ja rekursiivse pöördumise tulemus sabal seotakse sealvastavalt muutujategax ja pool . Kirjutame argumendivabas stiilis ümber ka lambdaavaldi-se\ as ass -> (x : as) : ass , kust saame(: ) . (x : ). Tulemuseks on definit-sioon

osalistid= foldr ( \ x pool -> foldr ( (: ) . (x : )) pool pool) [[] ]

.

Operaatorfoldl rakendus abstraheerib selliseid ühe akumulaatoriga arvutusi listil, kus akumu-laatori uus väärtus leitakse vanast, rakendades talle ja listi jooksvale elemendile mingit kindlatoperatsiooni, ja lõpus antakse akumulaator välja.

MuutujanullideArv definitsioon (97) annab just seda tüüpi arvutuse. Tuletame sellest süs-temaatilise mõttekäiguga argumendivabas stiilis definitsiooni operaatorifoldl kaudu. Jällegiseomefoldl argumendid selguse mõttes lokaalsete muutujategaop ja e.

Muutujae defineerimisel arvestame, et see onfoldl määratluse põhjal akumulaatori algväär-tus. Koodis (97) tuleb loendur-akumulaatorin algväärtus lokaalse operaatoriarv algsest välja-kutsest, mis annab tema kohale0.

190

Page 191: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Muutuja op defineerimiseks kujutame ette, et tema argumentideks on akumulaator ja mitte-tühja argumentlisti pea. Koodis (97) märgivad neid vastavalt muutujadn ja z lokaalse ope-raatori definitsioonis. Tulemus peab olema akumulaatori värskendatud väärtus ehk originaal-koodi mõttesn väärtus järgmisel rekursiivsel väljakutsel, mis on antud tingimusavaldisegaif z == 0 then n + 1 else n.

See analüüs annab tulemuseks koodi

nullideArv= let

op n z= if z == 0 then n + 1 else n

e = 0infoldl op e

,

mis erineb operaatorigafoldr kirjutatud koodist (169) märksa vähem kui originaaldefinitsioo-nid (97) ja (71) üksteisest. Erinevused on muutujate nimedes (x asemelz ja ülejäänu asemeln) ja plussi argumentide järjekorras, mis pole põhimõttelised. Lisaks onop argumentide järje-kord vastupidine. See tuleneb operaatoritefoldr ja foldl tüüpide erinevusest, mis on keeleloojate suvaotsus.

Kui akumulaator tuleb lõpus välja anda teisendatult, siis on operaatoritfoldl kasutav argumen-divabas stiilis definitsioon võimalik, kui lisada kompositsioon operaatori või (argumendivaba)avaldisega, mis seda teisendust väljendab. Kui akumulaatoreid on mitu või on akumulaatoritekõrval veel abiparameetreid, siis tuleb nad ühte andmestruktuuri kokku pakkida.

Kirjutame näiteks koodiga (100) antud muutujalenullsummadeArv argumendivabas stiilisdefinitsiooni. Selguse mõttes defineerimefoldl argumendid jälle lokaalsete muutujateop jae väärtuseks. Kuna originaalkoodi abifunktsioonis on kaks lisaparameetrits ja n, siis kasutamepaariakumulaatorit, mille komponendid vastavad neile parameetritele.

Muutujate s ja n algväärtused koodis (100) tulevad lokaalse operaatori algsest väljakut-sest, mis annab mõlema parameetri kohale0. Mittetühja argumentlisti pea seotakse muutu-jaga z . Argumentides ja n kohale järgneval rekursiivsel väljakutsel lähevad vastavalt s’ jaif s’ == 0 then n + 1 else n, kuss’ on defineeritud omakorda lokaalse deklarat-siooniga, mille võib lihtsalt üle võtta.

Kuna originaalkoodi abifunktsioon annab lõpus välja vaid parameetrin väärtuse, tuleb sama-väärse käitumise tekitamiseks nüüd lõpus akumulaatorist teine komponent võtta. Kogu analüüsi

191

Page 192: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

tulemusena tekib kood

nullsummadeArv= let

op (s , n) z= let

s’= s + z

in(s’ , if s’ == 0 then n + 1 else n)

e = (0 , 0)insnd . foldl op e

.

Ülesandeid

366. Otsida Hugsi teegist üles muutujateconcat , and , or , reverse definitsioonid ja saadaneist aru.

367. Kirjutadafoldr abil argumendivabas stiilis definitsioon koodiga (84) defineeritud muutu-jalekordaTähed .

368. Lahendada uuesti ülesanded 183, 184, 185, 186, 202, 302, kirjutades definitsioonid argu-mendivabas stiilisfoldr abil.

369. Lahendada uuesti ülesanne 299, kasutades argumendivaba stiili ja kõrgemat järku funkt-sioone.

370. Operaatori foldr abil argumendivabale stiilile üle minnes defineerida muutujaesimMittenull väärtuseks funktsioon, mis võtab argumendiks arvude listi: kuiselles leidub nullist erinevaid elemente, siis annab väljaesimese neist, vastasel korral kuilist on lõplik, siis annab väärtuseks0.

371. Operaatori foldr abil argumendivabale stiilile üle minnes defineerida muutujaloeFilter väärtuseks funktsioon, mis võtab järjest argumendiks predikaadi p jalisti l ning annab välja paari, mille esimene komponent on list neist l elementidest, misrahuldavad predikaatip, ja teine komponent on arv, kui palju on selles listis vähemelemente kui listisl.

372. Vaatleme definitsiooni

dropWhileLõpust p= reverse . dropWhile p . reverse

.

Anda muutujaledropWhileLõpust uus definitsioon argumendivabas stiilis operaato-ri foldr abil, nii et iga lõpliku argumentlisti korral töötab uus versioon samamoodi kuivarasem. Lisaks peab uus versioon töötama teatud olukorraska lõpmatu listi jaoks.

192

Page 193: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

373. Defineerida kahel viisil argumendivabas stiilis muutuja sgnSuhe väärtuseks funktsioon,mis võtab argumendiks arvude listi ja annab tulemuseksGT, LT või EQ vastavalt sellele,kas positiivseid elemente on rohkem kui negatiivseid, negatiivseid on rohkem kui positiiv-seid või on neid võrdselt.

374. Argumendivabas stiilis defineerida muutujakeera väärtuseks funktsioon, mis võtab argu-mendiks listide listi ja, interpreteerides seda tabelina (algse listi elemendid on tema read),annab tulemuseks90◦ päripäeva pööratud tabeli samal kujul. Võib eeldada, et tabel on lõp-lik, sisaldab vähemalt ühe rea ning kõik read on ühepikkused.

375. Operaatori foldl abil argumendivabale stiilile üle minnes defineerida muutujaosasummadSellisedEt väärtuseks funktsioon, mis võtab argumendiks arvutüü-bil töötava predikaadip ja arvude listil ja annab tulemuseks listi, mille elementideks onväiksemast suuremani need positiivsed arvudn, mille korral listi l esimesen elemendisumma rahuldab predikaatip.

376. Vaatame definitsiooni

concatLõpust= concat . map reverse . reverse

.

Anda muutujaleconcatLõpust uus samaväärne võimalikult argumendivabas stiilis de-finitsioon, mille korral arvutus vahestruktuure ei moodusta.

377. Operaatorifoldl abil defineerida argumendivabas stiilisviimaseNullsummani väär-tuseks funktsioon, mis võtab argumendiks arvude listi ja annab tulemuseks selle algusosakuni viimase sellise elemendinix, mille korral elementide summa algusest kuni elemendinix on 0 (kui sellist elementix ei leidu, siis annab tulemuseks tühja listi).

Erindid

Nagu tänapäeva programmeerimiskeeltes tüüpiline, on ka Haskellis olemaserindid (ingl excep-

tion) — spetsiaalsed väärtused erandolukordade tähistamiseks. Erindite kasutamine võimaldabkoodis tavajuhtude ja erandjuhtude töötlused üksteisest lahutada.

Haskell 98 on erindikäsitluse poolest üsna algeline. Ainsad erindid, mida ta tunneb, on need,mis tekivad protseduuride täitmisel keskkonnaga suheldes. Hugsi ja GHC laiendused on rikka-mad, võimaldades opereerida ka mujal tekkivate erinditega(kaasa arvatud sisseprogrammeeritudveaolukorrad nagu näidisesobituse ebaõnnestumine ja operaatorigaerror tekitatavad).

Protseduuride erindid

Protseduuride programmeerimise juures õppisime, et iga protseduur tüüpiIO A edastab väärtusetüübistA.

193

Page 194: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Täpsem olles tuleb nentida, et see on nii ainult juhul, kui protseduuri täitmine lõpeb normaalselt.Täitmine võib ka ebaõnnestuda — keskkonnaga suhtlemisel onsee tavaline, nt kui püütakse lu-geda faili, mida pole või mille lugemisõigus kasutajal puudub —, mispuhul väärtuse edastamiseasemel protseduur hoopiskiheidab (ingl throw) erindi.

Esmajärjekorras läheb erinditöötlust vaja failidega opereerimisel. MoodulistPrelude saab fai-litöötlusoperaatoridreadFile , writeFile ja appendFile . NeistreadFile väärtusekson funktsioon, mis võtab argumendiks failinime ja annab tulemuseks protseduuri, mis loeb antudnimega faili sisu ja edastab selle stringina. MuutujatewriteFile ja appendFile väärtusekson curried-kujuline funktsioon, mis võtab argumendiks failinime ja stringi ja annab tulemuseksprotseduuri, mis kirjutab selle stringi antud nimega faili; seejuures kui fail on juba olemas, siiswriteFile puhul tehakse fail enne tühjaks,appendFile puhul aga kirjutatakse string faililõppu olemasoleva sisu järele. (MoodulisPrelude on ainult kõige elementaarsemad failioperat-sioonid. Ülejäänud on koondatud moodulitesseSystem.IO ja System.Directory.)

Kõigil neil juhtudel heidab protseduur erindi, kui faili eiõnnestunud avada.

Kui do-avaldise mõne rea täitmine heidab erindi, siis lõpetab kogu do-avaldis kohe täitmise eba-normaalselt, heites sama erindi. See tagab, et kuskil protseduuride väljakutsete sügavuses heide-tud erind tuleb tasehaaval väljapoole läbi nende väljakutsete jada ühtlasi neid lõpetades, kuni tavõibolla kinni püütakse. Kui erindit kinni ei püüta, lõpeb täitmine süsteemse veateatega.

Vaatame näiteks koodi

lugemine:: IO ()

lugemine= do

putStr "Anna loetava faili nimi. "fn <- getLinesisu <- readFile fnputStrLn ( "Faili " ++ fn ++ " sisu: ")putStrLn sisuputStrLn "(Lõpp.) "

.

Kui sellega defineeritud muutujalugemine välja kutsuda, siis küsib ta faili nime. Kui sisestadaeksisteeriva lugemisõigusega faili nimi (see satub muutujassefn ), siis loeb ta selle faili sisu (seesatub muutujassesisu ) ja vormistab selle ekraanile. Kui aga faili avamine lugemiseks mingilpõhjusel ebaõnnestub, näiteks faili puudumise tõttu, siisheidetakse faili lugemisel erind, milletagajärjel kogu protseduur lõpetab töö veateatega.

Erindid, mis tekivad keskkonnaga suhtlemise käigus, nagu näiteks nõutud nimega faili puudumi-ne, kuuluvad tüüpiIOError.

Erindite püüdmiseks pakub moodulPrelude operaatoricatch . Tema väärtuseks oncurried-kujuline funktsioon, mis võtab argumendiks protseduuria ja püünise (ingl handler) h, miskujutab endast funktsiooni, mille argumendiks on suvalineerind tüübistIOError ja väärtuseks

194

Page 195: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

a-ga sama tüüpi protseduur. Neil argumentidel on tulemuseksprotseduur, mis täidab protseduuria: kui see lõpetab normaalselt, siis muud ei juhtugi ja edastatakse sama väärtus, mille edastaba; kui agaa heidab erindie, siis täidetakse lisaks protseduurh e, misjärel sõltuvalt tema õnnes-tumisest/ebaõnnestumisest kas edastatakse tema edastatud väärtus või heidetakse tema heidetuderind.

Operaatorcatch võimaldab niisiis hargnemist vastavalt sellele, kas protseduuri täitmine kul-geb normaalselt või tekib mingil etapil erind. Püünis peab välja andma protseduuriga sama tüüpiprotseduure sellepärast, et erindi tekkimisel asendab püünise antav protseduur erindi tõttu katke-nud põhiprotseduuri. See on analoogne olukorraga tavaliste hargnemiskonstruktsioonide puhul,kus harude tüübid peavad kokku langema.

Näiteks võiksime oma faililugemiskoodi täiendada deklaratsioonidega

lugemineE:: IO ()

lugemineE= lugemine ‘catch ‘ \ _ -> putStrLn "Lugemisel tekkis erind! "

.

Kutsudes nüüd välja muutujalugemineE , toimub õnnestunud failiavamise puhul kõik naguvarem, ebaõnnestumise puhul aga pärast failinime sisestamist ilmub ekraanile kiri “Lugemiseltekkis erind!”.

Tavaliselt soovib kasutaja erindi tekkimisel saada täpsemat infot erindi iseloomu kohta. Näiteksfaili lugemisel on kaks põhimõtteliselt erinevat võimalust erindi tekkimiseks: faili puudumineja tema lugemisõiguse puudumine. MoodulistSystem.IO.Error tulevad muutujad, mille väär-tuseks on sellised predikaadid, mis näitavad erindi iseloomu. Kasutades neid, võime kirjeldadapüünise, mille käitumine sõltub erindist. Sobib näiteks deklaratsioon

püünis:: IOError -> IO ()

püünis e| isDoesNotExistError e

= putStrLn "Pole faili! "| isPermissionError e

= putStrLn "Pole faili lugemisõigust! "| otherwise

= putStrLn "Tekkis tundmatu erind. "

.

Erinditöötlusega lugemise protseduuri kirjeldab nüüd eriti lihtne kood

lugemineE= lugemine ‘catch ‘ püünis

.

Mõnikord on vaja erinevaid erindeid töödelda erinevas kohas erinevate püünistega. Püünisedpeaksid siis erindeid valikuliselt töötlema: osa kinni püüdma, teised läbi laskma, et nad õigessepüünisesse jõuaksid.

195

Page 196: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Kirjutame näiteks meie püünise ümber kaheks erinevaks, millest üks püüab ainult õiguste puudu-misest tekkivaid erindeid, teine ainult faili puudumisesttekkivaid. Probleem tekib nüüd sellega,et erindi läbilaskmine on rangelt võttes võimatu: püünis kui funktsioon võtab argumendiks suva-lise erindi ja annab midagi tulemuseks. Kui kirjutaksime koodi

püüaÕigused:: IOError -> IO ()

püüaÕigused e| isPermissionError e

= putStrLn "Pole faili lugemisõigust! "

püüaMuud:: IOError -> IO ()

püüaMuud e| isDoesNotExistError e

= putStrLn "Pole faili! "| otherwise

= putStrLn "Tekkis tundmatu erind. "

lugemineE= lugemine ‘catch ‘ püüaÕigused ‘catch ‘ püüaMuud

,

siis lugemineE väljakutse ei töötaks enam nagu varem: faili puudumise korral satub vastaverind kõigepealt valesse püünisesse, kus hargnemiskonstruktsiooni sobiva juhu puudumise tõttutekib näidisesobituse viga, st väärtus⊥, mida ükski püünis enam ei võta.

Lahendus seisneb selles, et töötlemisele mittekuuluvad erindid uuesti heita. Selleks on olemasmuutujaioError , mille väärtuseks on funktsioon, mis võtab argumendiks erindi e ja annabtulemuseks protseduuri, mis midagi ei tee, aga heidab erindi e. Seega operaatorioError onanaloogne operaatorigareturn : esimene on paljas erindiseade, teine paljas normaalse väärtuseseade. Teiselt poolt onioError analoogne operaatorigaerror , sest ta on kasutatav sõltumatatüübikontekstist.

MuutujatepüüaÕigused ja püüaMuud õiged definitsioonid käiksid seega koodiga

püüaÕigused e| isPermissionError e

= putStrLn "Pole faili lugemisõigust! "| otherwise

= ioError e

püüaMuud e| isDoesNotExistError e

= putStrLn "Pole faili! "| otherwise

= putStrLn "Tekkis tundmatu erind. "

.

196

Page 197: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Kui aga juba erindiseadeks läheb, siis võib olla mugav luua hoopis oma erindeid ning seada japüüda neid. Oma erindi loomiseks on operaatoruserError . Tema väärtuseks on funktsioon,mis võtab argumendiks stringi ja annab tulemuseks erindi. Pangem tähele, et see funktsioonseda erindit ei sea; ta ei tegele sisendi-väljundiga üldse.Seadeks tuleb ikka kasutada operaatoritioError .

Püüniste järgmine versioon, mis kasutab omatekitatud erindeid, võiks olla

püüaÕigused e| isPermissionError e= putStrLn "Pole faili lugemisõigust! "

| otherwise= ioError (userError "Tekkis viga. Kas fail ikka olemas? ")

püüaMuud e= putStrLn (ioeGetErrorString e)

.

(172)

Kasutatud operaatoriioeGetErrorString väärtus on funktsioon, mis võtab argumendikserindi ja annab tulemuseks erindit iseloomustava stringi.Töötab ka tavalineshow, tema väärtusvõib olla mõnevõrra teistsugune.

OperaatoriteuserError ja ioError järjestrakendamise jaoks on olemas ka eraldi operaatorfail . Koodis (172) võiks niisiis avaldise

ioError (userError "Tekkis viga. Kas fail ikka olemas? ")

asemel kirjutada samaväärselt

fail "Tekkis viga. Kas fail ikka olemas? ".

Koodiga (81), (82), (83) ja (84) kirjutasime programmi, misharjutas kasutajat klaviatuuril viga-deta tippima. Erindite abil saab seda koodi lihtsasti täiendada nii, etescape-klahv lõpetab harju-tuse. Piisab tuua muutujakordaTähed definitsioonis (84) eraldi välja juht, kus kasutaja vajutasescape-klahvi, ja seada sel puhul erind, ning põhiprogrammi koodi(82) lisada püünis, mis sedaerindit tunneb ja selle peale normaalselt töö lõpetab. Uutmoodi oleksid need definitsioonid

kordaTähed (c : cs)= do

d <- getCharif d == cthen kordaTähed cselse if d == ’\ESC’

then fail "Harjutus katkestatud. "else return False

kordaTahed _= return True

, (173)

197

Page 198: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

harjutus= (

dohSetBuffering stdin NoBufferingkordaRead tekstputStrLn "Harjutus lõpuni läbitud. "

) ‘catch ‘\ e -> if isUserError e

then putStrLn (ioeGetErrorString e)else ioError e

. (174)

Muutuja kordaRead definitsioon võib jääda nii, nagu ta oli koodis (83), sest seal ei tegeldaüksikute sümbolite analüüsiga (koht, kus uus erind saab tekkida) ega töö lõpust teatamisega (misnõuaks erindi püüdmist).

Ülesandeid

378. Küsida interaktiivses keskkonnas muutujatecatch , ioError , userError tüübid jasaada neist aru.

379. Realiseerida ülal toodud koodinäited erindite püüdmise kohta ja selgitada erinevusi nendejooksutamisel.

380. Kirjutada muutujaharjutus definitsioon (174) ümber nii, et harjutamiseks esitatav teksttuleks reakaupa failist, mille nime programm töö algul küsiks. Kui nõutud faili lugemiseltekib tõrge, peab programm harjutuse katkestama ja eesti keeles põhjuse teatama. Muusosas peab töö käima nagu koodi (174) korral.

381. Kirjutada programm, mis küsib käivitudes kasutajalt faili nime ja kirjutab saadud nimegafaili sisu ekraanile. Kui faili lugemine ebaõnnestub, siisteatab veast ja alustab sama töödotsast peale.

Manipuleerimine kasutajaerinditega

Erinevad erindirühmad — õiguste puudumine, faili puudumine, kasutajaerind jne — on abiksolukordadele paindlikult reageeriva programmi loomisel.Kuid mahuka erinditöötluse korral te-kib olukordi, kus arvutuse erinevates faasides võidakse heita sama rühma erind, mida tulekstöödelda samas kohas. Siis ei piisa tekkepõhjustest sõltuva töötluse programmeerimiseks erindi-rühma teadmisest. Tekib erindi päritolu tuvastamise probleem.

Javas ja teistes objektorienteeritud keeltes lahendaks programmeerija selle probleemi lihtsalt,luues sellele erindiklassile erinevad alamklassid ja heites erinevates kohtades erineva alamklassi

198

Page 199: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

erindi. Hiljem saab täpse alamklassi järgi neid erindeid eristada. Haskellis, kus tüübid on jäigadja koosnevad fikseeritud hulgast objektidest, on selline lahendus võimatu.

Lisaks on Haskellis võimalik erindiiseloomustust — stringi, mida saab hiljem leida operaa-toriga ioeGetErrorString — programmeerija poolt ette anda vaid kasutajaerinditel, miskõik moodustavad ühe jagamatu erindirühma. Näiteks kui erind tekib õiguste puudumise tõttu,on tema iseloomustus “permission denied” ja muuta seda ei saa. Operaatorshow annab külldetailsema veateate, kuid ka see on kindlas formaadis. Kui kasutaja tahab veateate vormi ja si-su ise seada vastavalt vea tekkimise situatsioonile, peab ta erindid kohapeal kasutajaerinditegaasendama ja hiljem kasutajaerinditena töötlema.

Oletame, et meil on vaja kirjutada mingi programm, mille käigus tuleb erinevaid failitöötlusope-ratsioone mitmele failile rakendada, kusjuures failid teatab kasutaja programmile interaktiivselt.Pikkade failinimede sisestamise hõlbustamiseks tahame lisada faili küsimise protsessile võima-luse tabulaatoriga nime täiendamiseks, nagu on tavaline käsuridadel. Ütleme, et faili küsimineja osutatud failiga operatsiooni tegemine peaksid käima operaatorigategutse , mille signatuuron

tegutse:: (FilePath -> IO ()) -> IO ()

.

TüübisünonüümFilePath on defineeritud moodulisPrelude ja tähendab stringitüüpi. Niisiispeab kirjeldatav operatsioon võtma tegevuse, mis failiga teha, argumendiks.

Kasutame operaatoritegutse realiseerimisel eelkirjeldatud lähenemist, kus kõik huvitavaderindid interpreteeritakse ümber kasutajaerinditeks. Erindite rühmitamiseks lisame iseloomus-tuse algusse tunnuseid, milleks valime erindi soovitud töötlusviisi iseloomustavad tähtlühendid.Tunnuse ja ülejäänud osa eraldamiseks kasutame mingit sümbolit, mida tunnustes tõenäoliselt eiesine; võiks sobida näiteks definitsioon koodiga

eraldaja:: Char

eraldaja= ’>’

.

Kirjeldame kohe ka tunnuste lisamise, eemaldamise ja tunnuse järgi erindi püüdmise operatsioo-nid. Kirjutame selliselt, et nad kõik oleksid kasutatavad tavalise erindipüüdmise asemel, ainultpüünise asemel oleks tunnus (sisuline töötlus ei kuulu nende operatsioonide juurde, mistõttupüünist pole tarvis). Signatuurid oleksid

lisaT , eemaldaT:: IO a -> String -> IO a

,

püüaT:: IO () -> String -> IO ()

.

199

Page 200: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Tunnuse lisamine on lihtne: kasutajaerind tuleb püüda ja heita uuesti koos tunnusega, ülejäänuderindid tuleb niisama heita. Sobib kood

lisaT io t= let

lisa e| isUserError e

= fail (t ++ eraldaja : ioeGetErrorString e)| otherwise

= ioError einio ‘catch ‘ lisa

.

Tunnuse eemaldamine ja kinnipüüdmine on pisut keerulisemad, sest tunnus tuleb tuvastada. Sa-mas on mõlemal juhul tegu skemaatiliselt sama arvutusega. Erinevus on vaid selles, et eemal-damise juhul tuleb lühendatud iseloomustusega erind uuesti heita, püüdmise puhul aga tulebsoovitud erindi tuvastamisel lõpetada töö normaalselt. Seetõttu kirjeldame algul üldisema ope-ratsiooni, mis võtab jätkutegevuse lisaargumendiks. Sobib kood

töötleT:: IO a -> String -> (String -> IO a) -> IO a

töötleT io t f= let

töötle e| isUserError e && tun == t

= f str| otherwise

= ioError ewhere

(tun , _ : str)= break (eraldaja == ) (ioeGetErrorString e)

inio ‘catch ‘ töötle

. (175)

Koodi (175) lokaalne deklaratsioon kirjeldab püünise; muuhulgaswhere-konstruktsioon kirjel-dab tunnuse eraldamise erindi iseloomustuse eest. Püünis kontrollib, kas tegu on kasutajaerindigaja siis kas eraldatud tunnus on see, mida otsiti. Kui nii, siis käivitub jätkutegevus iseloomustusejärelejäänud osal, muidu heidetakse erind uuesti.

Eemaldamine ja kinnipüüdmine on nüüd defineeritavad selle operatsiooni kaudu deklaratsiooni-dega

eemaldaT io t= töötleT io t fail

,

200

Page 201: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

püüaT io t= töötleT io t (putStrLn . (’\n ’ : ))

.

Järgnevalt kirjeldame kataloogi uurimisel tekkivate erindite ümberkantimise kasutajaerinditeks.Formaalselt on tegu püünistega, mis alati heidavad uuesti kas sama või muudetud erindi.

Esimene rühm meid huvitavaid erindeid on sellised, mis tekivad jooksva kataloogi tuvastamisel.Vastava töötleja kirjeldame koodiga

vigaKataloogiga:: IOError -> IO a

vigaKataloogiga e| isPermissionError e

= fail "Jooksvat kataloogi ei õnnestu tuvastada. "| isDoesNotExistError e

= fail "Jooksev kataloog on vahepeal kadunud. "| otherwise

= ioError e

.

Teine rühm erindeid tekivad jooksva kataloogi sisu lugemisel. Neist uued on ainult õiguste puu-dumise erindid. Nende töötluse kirjeldab kood

vigaSisuga:: IOError -> IO a

vigaSisuga e| isPermissionError e

= fail "Jooksval kataloogil puudub lugemisõigus. "| otherwise

= vigaKataloogiga e

.

Tunnused tuleb aga määrata vastavalt soovitavale kinnipüüdmise kohale, mis annab erinditeleteistsuguse rühmituse. Kataloogi lugemisega seotud erindtuleb püüda jooksvalt ning programmitööga edasi minna. Nimetame seda jooksvaks erindiks ja omistame talle tunnuse “jo”. Kui aganäiteks tabulaatorijuhu analüüsis selgub, et nõutud tähtedega algavat faili ei eksisteeri, siis polemõtet püüdagi samast kohast jätkata. Samas tuleks kasutajat kindlasti olukorrast teavitada. Ni-metame seda katkestavaks erindiks ja tunnuseks paneme “ka”. Lisaks võib erindeid tekitada kafailitöötlus, mille kohta pole midagi teada. Võib juhtuda,et failitöötluse programmeerimisel onkasutatud samasugust erinditega opereerimist ja mõne erindi tunnuseks valitud näiteks “ka”. Ettagada, et seda erindit ei püütaks meie katkestava erindi pähe kinni, tuleb failitöötluse erinditelelisada kolmas tunnus. Valime selleks “võ” sõnast “võõras”.

Et vältida raskesti avastatavaid tippimisvigu, on mõistlik kolme tunnuse tarvis kasutusele võttaka muutujad, mitte kirjutada igale poole stringe sisse (vale muutuja tuleb palju kergemini välja

201

Page 202: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kui vale string). Näiteks kirjutame deklaratsioonid

joT , kaT, võT:: String

,

joT= "jo "

,

kaT= "ka"

,

võT= "võ"

.

Oleme jõudnud ülesande sisu juurde. On vaja kahte abioperaatorit. Esiteks stringide pikima ühisealgusosa ehkprefiksi (ingl prefix) leidmine; seda läheb vaja tabulaatorile reageerimisel, et pi-kendataks faili nime niikaugele kui võimalik. Teine on tuvastamine, kas stringil on etteantudprefiks, ja positiivse vastuse korral selle prefiksi ärajätmine. Realiseerime nad üldisemalt võrdu-sega tüüpi elementidega listide (stringide asemel) jaoks koodiga

püp:: (Eq a)=> [[a]] -> [a]

püp ss @ ( ~(c : _) : css)= if any null ss || any ( (c /= ) . head) css

then []else c : püp (map tail ss)

,

eemaldaPrefiks:: (Eq a)=> [a] -> [a] -> Maybe [a]

eemaldaPrefiks [] ds= Just ds

eemaldaPrefiks (c : cs)(d : ds)| c == d

= eemaldaPrefiks cs dseemaldaPrefiks _ _= Nothing

.

Edasi kirjeldame töölõigu, mis seondub kasutaja poolt sisestatud ühe sümboli analüüsiga. Pealemitmete veaolukordade tekib siin hargnemine ka normaalse kasutuse skeemis vastavalt sellele,kas sisend lõppes (näiteksenter-klahvi vajutamise tõttu) või tuleb veel sümboleid kuulata. De-fineerime operaatorianalüüsi väärtuseks funktsiooni, mis võtab argumendiks äsjakuulatudsümboli ja jooksva failinime algusosa ja annab tulemuseks protseduuri, mis töötleb selle klah-vivajutuse ja edastab paari failinime uuest jooksvast algusosast ja tõeväärtusest, mis näitab, kassisend on lõppenud.

202

Page 203: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Eraldi käsitlemeescape-sümbolit, mille puhul anname katkestava erindi. Kataloogi luge-miseks kasutame moodulistSystem.Directory tulevaid muutujaidgetCurrentDirectoryja getDirectoryContents , millest esimese väärtus on protseduur, mis õnnestumise korraledastab jooksva kataloogi täistee, ja teise väärtus on funktsioon, mis võtab argumendiks kata-loogi nime ja õnnestumise korral edastab selle kõigi failide nimed listina. Lisaks leiab kasutustmoodulistData.Maybe saadav operaatorcatMaybes , mille väärtuseks on funktsioon, mis võ-tab argumendiksMaybe-tüüpi objektide listi ja koostab tulemuseks kõigi (ühekordse)Just allesinevate väärtuste listi. Sellega on mugav esitada etapp,kus ebaõnnestunud prefiksieemaldusedjäetakse kõrvale, nii et alles jäävad vaid sobivate failinimede jätkuosad.

Koodiks tuleb

analüüsi:: Char -> FilePath -> IO (FilePath , Bool)

analüüsi c p| c == ’\ESC’

= fail "Programm katkestatud. " ‘lisaT ‘ kaT| c == ’\n ’

= return (p , True)| c == ’\t ’

= docd <- getCurrentDirectory

‘catch ‘ vigaKataloogiga‘lisaT ‘ joT

fns <- getDirectoryContents cd‘catch ‘ vigaSisuga‘lisaT ‘ joT

let gs = catMaybes (map (eemaldaPrefiks p) fns)let q = püp gscase gs of

_ : hs-> do

putStr qreturn (p ++ q , null hs)

_-> fail ( "Faili algusega " ++ p ++ " pole. ")

‘lisaT ‘ kaT| otherwise

= doputChar creturn (p ++ [c] , False)

.

Järgmiseks kodeerime ära kogu töölõigu, mis küsib kasutajalt faili nime. Teeme kaks operaatoritküsiNimi ja loeNimi , mis erinevad selle poolest, etküsiNimi puhul esitatakse kasutajale

203

Page 204: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ka sõnaline palve. Definitsioonid on vastastikku rekursiivsed (kui failinime sisselugemise käigustekib kataloogi lugemisel erind, siis selguse mõttes kolitakse üle järgmisse ritta, kus korratak-se palvet). Mõlemal kirjeldataval operatsioonil on üks argument, milleks on jooksev failinimealgusosa.

Saame deklaratsioonid

küsiNimi , loeNimi:: FilePath -> IO FilePath

,

küsiNimi p= do

putStr "Anna faili nimi. "putStr ploeNimi p

,

loeNimi p= (

dohSetEcho stdout Falsec <- getCharhSetEcho stdout True(q , b) <- analüüsi c pif b

then doputChar ’\n ’return q

else loeNimi q)‘catch ‘ \ e -> do

ioError e ‘püüaT‘ joTküsiNimi p

.

OperaatoriloeNimi definitsioonis oli vaja korraldada jooksvate erindite püüdmine, mille õn-nestumise korral järgneks operaatoriküsiNimi väljakutse. Selleks sai koodi lõpus esitatudpüünis, mis esimese asjana heidab erindi uuesti ja püüab selle operaatorigapüüaT , kus tunnu-seks on jooksvus. Jooksva erindi puhul lõpetab see normaalselt, mistõttu programmi täitminejätkub järgmise reaga. Muud erindid heidetakse veelkord jaselle koodi töö lõpeb.

204

Page 205: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

On jäänud kirjutada vaid põhioperaatori definitsioon

tegutse f= (

dohSetBuffering stdin NoBufferinghSetBuffering stdout NoBufferingfn <- kysiNimi ""f fn ‘lisaT ‘ võT

)‘püüaT‘ kaT‘eemaldaT ‘ võT

.

Argumentfunktsiooni rakendamisele on lisatud võõrerinditunnus, mis kõige lõpus eemaldatakse.Eemaldamine on vajalik, et jätta failioperatsiooni heidetav erind lõppkokkuvõttes puutumata, ettema jaoks mujal üles seatud püünised tunneksid erindi ära.

Ülesandeid

382. Kasutades ümberkantimist kasutajaerindiks, kirjutada ülesande 381 lahenduseks olev prog-ramm ümber nii, et tekkinud erindite kirjeldamisel mainitaks faili nime (nt kujul “Failil“k.txt” puudub lugemisõigus!”).

383. Kirjutada programm, mis küsib käivitudes kasutajalt kaks failinime ja kirjutab esimese failineed read, milles esineb mõni mittetühisümbol, teise faili. Kui sisendfaili lugemine eba-õnnestub, lõpetab programm kohe töö vastavasisulise teatega ekraanil. Kui sisendfaili lu-gemine õnnestub, kuid väljundfaili kirjutamine mitte, siis küsida väljundfaili nimi uuesti.Kui kasutaja sisestab tühja nime, siis lõpetada. Püüda tekkinud olukordi võimalikult täpseltdiagnoosida.

384. Kasutades moodulisSystem.Directory defineeritud operaatoreid, täiustada operaatoritegutse realisatsiooni nii, et oleks võimalik sisse lugeda ka faile, mis asuvad jooksvakataloogi alamkataloogides (mitte tingimata vahetutes).

205

Page 206: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Väärtuste maailma laiendamine

Polümorfism

Oleme praktiliselt algusest peale puutunud kokku polümorfismiga. Haskellis on kõik polümor-fism parameetriline (ingl parametric), st iga polümorfse objekti kõik tüübid on esitatavadparameetrite abil ühtsel viisil. Parameetriteks on siin tüübiparameetrid.

Kuid mitte ainult polümorfse avaldise tüüp, vaid ka tema väärtus on parameetriline nendesamadetüübiparameetrite suhtes. Polümorfsel avaldisel on iga oma tüübi kohta üks väärtus, mis kuulubsellesse tüüpi. Väärtuste kirjeldamisel tüübiparameetreid ei kirjutata, kuid nad on põhimõtteliseltolemas.

Näiteksmap tüüp on(a -> b) -> ( [a] -> [b]) , mis sisaldab kaht tüübimuutujat. Seetähendab, et operaatorilmapon lisaks funktsioonile ja listile veel ka kaks ilmutamata tüübiargu-menti. Seejuures tüübiargumendid määravad funktsiooniargumendi ja listiargumendi tüübi, sel-les mõttes on tüübiargumendid koguni esimesed. Näiteks kirjutades avaldisemap (== ’X’),on üks tüübiparameeterChar ja teineBool.

Operaatormapon näiteksuniversaalsest (ingl universal) polümorfismist, kus avaldis võtabküll väärtusi mitmest tüübist, kuid temaga ümberkäimine tüübist ei sõltu. Vastandub seeülelaa-

dimisele (ingl overloading), kus polümorfse avaldise väärtustamine kulgeb üht- või teistviisi,sõltuvalt tema tüübist. Üle laetud on näiteks aritmeetilised operatsioonid, sest nende sooritamiseviis sõltub sellest, millist laadi arvudega on tegu. Formaalselt tähistab ülelaadimist tüübikontekstsignatuuris — ülelaadimine ei saa kunagi toimuda üle kõigi tüüpide, vaid ainult üle klassidegapiiratud tüübiperede.

Universaalselt polümorfsete muutujate defineerimise juures pole mingit kunsti, sest seal prog-rammeeritav arvutus tüübist ei sõltu ja tüüp definitsioonisüldse ei esine. Sama olukord on ülelaetud muutujatega, kui nad defineerida olemasolevate üle laetud operaatorite kaudu ühtsel viisil.Selliseid definitsioone oleme näinud ja kirjutanud juba küll.

Teine lugu on ülelaadimise sissetoomisega otse “õhust”. Sisuliselt on tegemist hargnemiskonst-ruktsiooniga, kus haru valik toimub tüübi järgi, kuid süntaktiline struktuur erineb tavalistest harg-nemiskonstruktsioonidest kardinaalselt: erinevate tüüpide juhud on üle kogu koodi laiali. Selle

206

Page 207: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

mõte on võimaldada programmeerijal neid kirjeldada erinevates moodulites, et vajadusel saaksprogrammeerija laiendada ka standardteegist või muust mittemuudetavast teegist tulevate nimedekasutust.

Me ei vaatle ammendavalt kõiki piiranguid, mis Haskellis ülelaadimisele kehtivad. Neid on pal-ju ja võrreldes muude Haskelli reeglitega on nad üsna keerulised. Standardis on nad rangemad,realisatsioonide laiendustes leebemad. Huviline võib tingimused ise täpselt ametlikust keelekir-jeldusest järele uurida.

Klasside sissetoomine ja laiendamine

Et muutujat üldse saaks defineerida erinevate tüüpide jaokserinevalt, peab ta olema mingi klassimeetod. Klassimeetod (ingl method) on muutuja, mis on klassi definitsioonis vastavalt dekla-reeritud. Mujal seda määrata ega muuta ei saa.

Olemasolevate klasside meetodite nimekirja saab küsida interaktiivses keskkonnas. Näiteks klas-si Eq jaoks näitavad interaktiivsed keskkonnad, et tema meetodid on == ja /= ja esitavad kanende muutujate tüübid. KlassiNum jaoks annab GHCi teada, et tema meetodid on+, * , - ,negate , abs , signum ja fromInteger , samuti et klassiMonad meetodid on>>=, >>,return ja fail jne.

Klass defineeritakseklassideklaratsiooniga kujul

class h where

s1

. . .sl

d1

. . .dk

,

kush on klassideklaratsiooni päis,s1, . . . , sl on tüübisignatuurid ningd1, . . . , dk definitsioonid,kusjuuresl > 0 ja k > 0.

Klassideklaratsiooni päish on lihtsamal juhul kujulc a, kus c on uue klassi nimi jaa on tüü-bimuutuja, mis tähistab suvalist selle klassi potentsiaalset esindajat. Seda tüübimuutujat saab jatuleb kasutada kõigis signatuuridess1, . . . , sl, kusjuures tingimustc a konteksti kirjutada ei tohi,aga ta kehtib siiski. Signatuuride vasakud pooled peavad olema erinevad muutujad; need saavadkiklassi meedoditeks. Definitsioonidd1, . . . , dk on meetoditevaikedefinitsioonid. Vaikedefinit-sioone ei pea esitama kõigi meetodite kohta, vaid vabal valikul.

Oleme ehk märganud, et mitmed tuttavad tüübid on sümmeetrilised, nii et neis on mõtet rääkida“vastandväärtusest”: muidugi kuuluvad siia arvutüübid, kuid analoogilist rolli täidab tõeväärtus-

207

Page 208: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

te puhul eitus ning suurusvahekorratüübi puhul vastupidise vahekorra leidmise operatsioon. Onvõimalik, et erinevates olukordades, mis kasutavad mingite objektidevaheliste suhete iseloomus-tamiseks arve, tõeväärtusi ja suurusvahekordi, on kõikjalvaja kasutada suhte ümberpööramiseehk vastandväärtuse leidmise operatsiooni sarnasel viisil (näiteks listide kõigile elementidele ra-kendada).

Programmeerijale oleks mugav ja kood saaks ülevaatlikum, kui me ei peaks ühel tüübil kasutamaüht, teisel tüübil teist operaatorit. Standardteegis aga muutuja, mis oleks kasutatav kõigis neisolukordades, puudub. Selle võib sisse tuua klassideklaratsiooniga

class Sümm awhere

vastand:: a -> a

. (176)

Kood (176) defineerib klassi nimegaSümmja sätestab, et sellesse kuulumiseks peab tüübilAolema defineeritud muutujavastand , mille tüübiks onA → A. Meie vajadustega see klapib,sest vastandväärtust otsime tõepoolest samast tüübist kust on pärit etteantud objekt.

Pangem tähele, et meetodivastand tõeline tüüp, mida hakkavad deklaratsiooni (176) sisseluge-misel näitama ka interaktiivsed keskkonnad, on(Sümm a) => a -> a . TingimusSümm atuleneb signatuuri asukohast klassideklaratsioonis.

Kui deklaratsioon (176) on olemas, saab muutujatvastand definitsioonides juba kasutamahakata. Näiteks võib kirjutada koodi

vastandaKõik:: (Sümm a)=> [a] -> [a]

vastandaKõik= map vastand

. (177)

Muidugi ei saa operaatoritvastandaKõik , mis peaks argumentlisti järgi koostama vastand-väärtuste listi, kuidagi testida, kui aluseks oleva meetodi vastand kohta on teada vaid signa-tuur, ta pole ühegi tüübi jaoks defineeritud. Formaalselt onaga kõik korrektne, sestvastandpeab olema defineeritud parajasti sümmeetrilistel tüüpidel, kuid nende tüüpide klass on esialgutühi.

Klassi meetodeid saab defineerida ainult kas klassideklaratsioonis vaikedefinitsioonidena võiesindajadeklaratsioonides, mis laiendavad klassi uuteletüüpidele.Esindajadeklaratsioon

(ingl instance declaration) on üldjuhul kujul

instance h where

d1

. . .dl

. (178)

208

Page 209: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Siinh on esindajadeklaratsiooni päis ningd1, . . . , dl on meetodite definitsioonid, kusjuuresl > 0.

Päish on lihtsamal juhul kujulc t, kus c on klass jat on tüüp, mida tahetakse sinna klassipaigutada. Definitsioonidd1, . . . , dl annavad klassic meetodite tähenduse tüübit jaoks. Seejuu-res ei pea tingimata kõiki klassic meetodeid defineerima. Esindajadeklaratsioonis puuduvatedefinitsioonide aset täidavad klassideklaratsioonis olevad vaikedefinitsioonid ja kui ka klassi-deklaratsioonis meetodi definitsiooni pole, jääb meetodi väärtuseks bottom. Kui aga esindaja-deklaratsioon meetodi definitsiooni annab, siis tüübit jaoks kehtib selle meetodi see definitsioonisegi juhul, kui tal on klassideklaratsioonis ka vaikedefinitsioon. Teisi sõnu, vaikedefinitsiooneon võimalik esindajadeklaratsioonis üle katta.

Näiteks tüübiBool paneb uude klassi kuuluma kood

instance Sümm Bool where

vastand= not

,

kusjuures muutujavastand väärtuseks saab eitus. Analoogselt saab muutujavastand tüüpi-deInt ja Ordering jaoks defineerida deklaratsioonidega

instance Sümm Int where

vastand= negate

, (179)

instance Sümm Ordering where

vastand GT= LT

vastand LT= GT

vastand _= EQ

. (180)

Nüüd on lihtne interaktiivses keskkonnas veenduda, et näiteks avaldiste

vastandaKõik [0 .. 4] :: [Int ],vastandaKõik (map (compare 0) [-2 .. 1]) ,vastandaKõik (map ( \ v -> sin (v * pi / 4) > 0) [1, 3 .. 7])

väärtused on vastavalt

0 :−1 :−2 :−3 :−4 : [], LT : LT : EQ : GT : [], False : False : True : True : [].

MuutujavastandaKõik tüüp on neis kolmes avaldises vastavalt

[Int ] -> [Int ], [Ordering ] -> [Ordering ], [Bool ] -> [Bool ].

209

Page 210: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Esimesel juhul kasutatakse väärtustamisel meetodivastand definitsiooni tüübiInt jaoks, tei-sel juhul definitsiooniOrdering jaoks ja kolmandal juhul definitsiooniBool jaoks. (Esimesesavaldis on vajalik tüübisignatuur, sest vaikimisi loetakse täisarvkonstandid tüüpiInteger, millejaoks esindajadeklaratsioon puudub.)

Defineerime veel klassi, millesse võiksid kuuluda kõik tüübid, mille objektidel on mõtet rääki-da tsüklilisest nihkest ja objektide vahekaugusest selle nihutamise mõttes. Näiteks tüübilBoolvõiksime, võttes aluseks loomuliku järjestuseFalse < True, tuvastada, et tõeväärtusestFalsetõeväärtuseTrue saamiseks tuleb teha1 samm, tõeväärtusestTrue tõeväärtuseFalse saamiseksaga−1 samm, muudel juhtudel0 sammu. Sama võib teha mistahes loetelutüübi korral, ka onilmne, kuidas samasugust operatsiooni kirjeldada täisarvutüüpidel. Kuna mõtleme just tsükli-list liikumist üle tüübi kõigi objektide, siis erinevate objektide vahekaugusena võib käsitleda kaainult positiivset nihet. Kirjutame klassideklaratsiooni

class Tsükliline a where

nihe:: Integer -> a -> a

vahe:: a -> a -> Integer

posVahe:: a -> a -> Integer

vahe= posVahe

, (181)

mis võimaldab kasutada nii positiivset vahet kui lihtsalt vahet. Viimane osa sellest deklaratsioo-nist sätestab, et vaikimisi on tavaline vahe sama mis positiivne vahe.

Seda vaikeseost võiks programmeerida ka teisiti, viies tema signatuuri ja vaikedefinitsiooni klas-sideklaratsioonist välja. Signatuurile tuleb siis muidugi lisada klassikontekst. Sellega kaob muu-tujavahe klassi meetodite seast. See tähendaks, et muutujadvahe ja posVahe oleksid võrdseväärtusega klassi iga tüübi jaoks, puuduks võimalus mõnedetüüpide korral see definitsioon ülekatta.

210

Page 211: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Koodiga (181) defineeritud klassi võib laiendada tüübileBool deklaratsiooniga

instance Tsükliline Bool where

nihe n x= x == even n

vahe True False= 1

vahe False True= -1

vahe _ _= 0

posVahe x y= abs (vahe x y)

. (182)

Siin ei ole ära kasutatud, etvahe definitsioon on klassideklaratsioonis olemas, vaid ta on üle kae-tud definitsiooniga, mis võimaldab ka negatiivseid vahesid. Kui meid ei huvitaks, kummal poolobjektid üksteisest asetsevad, võiksime deklaratsiooni (182) ümber teha nii, et meetodivahedefinitsiooni seal ei esineks.

Antud juhul ei piisaks vaidvahe definitsiooni ärajätmisest, sestposVahe definitsioon on antudtema kaudu ja tagajärjena tekiks vastastikrekursioon klassideklaratsioonis antud definitsiooniga,mis viiks lõpmatusse tsüklisse. MeetodposVahe tuleks ümber defineerida näiteks koodiga

posVahe x y| x == y

= 0| otherwise

= 1

.

Interpreteerides ka fikseeritud pikkusega täisarvude tüüpi tsüklilisena, kus suurimale järgneb vä-

211

Page 212: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

him, saame vajalikud operatsioonid ka tüübiInt jaoks defineerida koodiga

neligiga= 2 ^ 32

instance Tsükliline Int where

nihe n i= let

c = (n + toInteger i) ‘mod‘ neligigainfromEnum ( if c >= 2 ^ 31 then c - neligiga else c)

posVahe i j= vahe i j ‘mod‘ neligiga

vahe i j= toInteger i - toInteger j

.

Laiendada saab ka juba olemasolevaid, teistes moodulites defineeritud klasse. Haskelli realisat-sioonidega kaasas olevais teekides on klassisNum vaid arvutüübid, nii et näiteks tõeväärtusipole võimalik arvudena käsitleda; kuid mõnikord võib programmeerija tahta sellest kitsendusestvabaneda. Siis võib ta kirjutada deklaratsiooni

instance Num Bool where

x + y= x || y

negate x= not x

x * y= x && y

abs x= x

signum x= x

fromInteger a= a /= 0

, (183)

212

Page 213: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

millega kuulutatakse tõeväärtustüüp klassiNum kuuluvaks ja defineeritakse enamik selle klassimeetodeist. Definitsioon puudub operaatoril- , kuid klassigaNum tuleb kaasa vaikedefinitsioon,mis ütleb, et lahutamine on samaväärne vastandarvu liitmisega. See võiks siin sobida küll.

Deklaratsioon (183) võimaldab tõeväärtustega sooritada aritmeetilisi operatsioone nagu arvu-degagi. Nii on nüüd tüübikorrektsed näiteks kirjutisedTrue + True , False * False ,True - False , abs (2 + 3 == 5) jne.

Töötavad isegi sellised näiliselt vigased avaldised naguTrue + 4 . Seda aktsepteeritakse tä-nu asjaolule, et täisarvulisi arvkonstante avaldistes interpreteeritakse operaatorifromIntegerabil, mistõttu antud juhul on konstandi4 väärtuseksTrue. Tegu on sama nähtusega, mis võimal-dab kirjutada5 + 0.2 jms.

Märgime, et klassi laiendamisel uuele tüübile jäävad meetodite prioriteet ja assotsiatiivsus keh-tima ka uue tüübi jaoks. Nii on operaatorite+ ja * prioriteetide vahekord ka tõeväärtustüübilsamasugune nagu tavalistel arvudel.

Ühe esindajadeklaratsiooniga võib sama klassi esindajaksmäärata terve tüüpide pere, mis onkirjeldatud tüübiparameetritega.

Teame, et funktsiooniväärtusega avaldised, nagu näitekssin , annavad interaktiivses keskkonnasväärtustamisel tüübivea, sest funktsioonitüübid ei kuuluklassiShow. Võime aga kõik funktsioo-nitüübid sinna klassi lükata deklaratsiooniga

instance Show (a -> b) where

show _= "See ju funktsioon! "

,

mis defineerib funktsioonitüüpide jaoks muutujashow väärtuseks funktsiooni, mis annab kons-tantselt välja teksti “See ju funktsioon!”. Pärast seda on interaktiivse keskkonna senine algajalepeavalu valmistav käitumine kui peoga pühitud; lastes väärtustada näitekssin , tuleb vastuseks“See ju funktsioon!”.

Ülesandeid

385. Otsida Hugsi teegist üles klassideEq, Enum, Show, Monad definitsioonid ja saada neistaru.

386. Viia läbi tsükliliste tüüpide klassi meetodivahe muutmine tavaliseks muutujaks, millesttekstis on juttu.

387. Deklareerida ühiktüüp sümmeetriliseks, defineeridessobivalt meetodivastand .

388. Deklareerida tüüpInteger tsükliliseks, defineerides sobivalt kõik vajalikud meetodid.

213

Page 214: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

389. Deklareerida suurusvahekorratüüp tsükliliseks, defineerides kõik vajalikud meetodid.

390. Deklareerida sümbolitüüp tsükliliseks, defineerideskõik vajalikud meetodid.

391. Deklareerida tõeväärtustüüp klassiNum esindajaks nii, et aritmeetilised operatsioonid vas-taksid arvutustelemodulo2.

392. Deklareerida suurusvahekorratüüp klassiNum esindajaks nii, etLT vastaks negatiivsuselevõi −1-le, GT positiivsusele või1-le ja EQ nullile.

393. Deklareerida sümbolitüüp klassiNum esindajaks nii, et iga sümbol oleks arvuna võrdneoma koodiga.

Pärimine

Et defineerida uus klass olemasolevate klasside alamklassiks, tuleb klassideklaratsiooni päisesselisada vastav tüübikontekst. Üldjuhul on päis kujul( k1 a, . . ., kl a) => c a, kusc on uueklassi nimi jaa tüübimuutuja nagu varemgi ja lisaksk1, . . . , kl on mingite olemasolevate klassidenimed. See tähendab, et defineeritav klassc on klassidek1, . . . , kl (ja ainult nende) alamklass.Ühtlasi pärib (ingl inherit) klassc kõik meetodid klassidestk1, . . . , kl koos signatuuride jaklassideklaratsioonidesse paigutatud vaikedefinitsioonidega.

Niisiis lubab Haskell mitmest pärimist, kusjuures tüübi ülemtüübid võivad ise olla mingi ühe jasama tüübi alamtüübid. Keelatud on aga tsükliline pärilus.

Vaatleme uuesti eelnevalt sisse toodud sümmeetriliste tüüpide klassi. Mitmel sellisel tüübil onolemas sümmeetriakeskpunkt: arvutüüpidel on selleks0, suurusvahekorratüübilEQ. Seepärastvõiks olla defineeritud ka meetodkese , mille väärtus sellistel tüüpidel oleks tüübi sümmeetria-keskpunkt. Sümmeetriakeset ei ole siiski kõigil sümmeetrilistel tüüpidel: tõeväärtustüübi jaokson raske üht või teist väärtust keskmeks pidada. Järelikultsobib sisse tuua tsentraalsümmeetri-liste tüüpide klass, mis on sümmeetriliste tüüpide klassi alamklass ja mille esindajatunnuseks onmeetodkese .

Selle jaoks kirjutame koodi

class (Sümm a) => TsentrSümm a where

kese:: a

.

Siis saame tüübidOrdering ja Int viia ka tsentraalsümmeetriliste tüüpide klassi tuttaval kujuldeklaratsioonidega

instance TsentrSümm Ordering where

kese= EQ

, (184)

214

Page 215: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

instance TsentrSümm Int where

kese= 0

. (185)

Interaktiivses keskkonnas on nüüd lihtne kontrollida, et annoteeritud avaldiskese :: Ordering annab väärtuseksEQ, agakese :: Int annab0. Meetodi keseannoteerimine mõne muu tüübiga, näiteks tõeväärtustüübiga, annab tüübivea.

Esindajatüüpe klassile, mis on deklareeritud mingite klasside alamklassiks, saab valida ainultselliste tüüpide seast, mis neisse ülemklassidesse juba kuuluvad. Deklaratsioonid (184) ja (185)töötavad ainult tingimusel, et leiduvad eraldi esindajadeklaratsioonid, mis näitavadOrdering jaInt kuuluvust sümmeetriliste tüüpide klassi, — näiteks (180) ja (179).

Hoolimata sellest on võimalik klassi meetodeid defineeridatema alamklassi meetodite kaudu.Näiteks deklareerime tüübiFloat sümmeetriliseks ja tsentraalsümmeetriliseks, määratledes vas-tandelemendi kui keskme suhtes sümmeetrilise objekti. Kese on loomulik võtta nulliks. Saamedeklaratsioonid

instance Sümm Float where

vastand x= kese + (kese - x)

, (186)

instance TsentrSümm Float where

kese= 0

, (187)

mis koos töötavad laitmatult.

Nüüd kui millegipärast on soov saada muutujavastand väärtuseks peegeldus mõne muu ar-vu, näiteks1 suhtes, siis võib jätta deklaratsiooni (186) muutmata ja asendada meetodikesedefinitsioon esindajadeklaratsioonis (187) koodiga

kese= 1

.

Olgu meil veel tahtmine defineerida klass selliste tüüpide jaoks, mida on võimalik ja realist-lik programmi töö käigus ammendavalt läbi vaadata. Klassispeaks olema meetodkõik , milleväärtuseks oleks list, mille elemendid on parajasti kõik konkreetsesse tüüpi kuuluvad objektid.

Loomulik on nõue, et sellised tüübid peavad ühtlasi kuulumaklassiEnum. Kuid sellest ei piisa— Enum sisaldab ka lõpmatuid tüüpe naguInteger ja isegi ujukomaarvutüübid. Nõuame li-saks, et meid huvitavad tüübid peavad kuuluma ka klassiBounded, milles on olemas meetodidminBound ja maxBound, mis tähendavad vastavalt vähimat ja suurimat objekti tüübis.

215

Page 216: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Nii saame definitsiooni

class (Enum a, Bounded a) => Fin a where

kõik:: [a]

kõik= [minBound .. maxBound]

. (188)

Selle tulemusel on uus klass korraga klassideEnum ja Bounded alamklass. Seega kõigil “lõp-likel” tüüpidel on olemasfromEnum , toEnum , aritmeetilise jada süntaks ning kaminBound ,maxBound. Praktiliselt saavutatakse see muidugi tänu nõudele, et esindajaks saab määrata vaidselliseid tüüpe, mis ülemklassideEnum ja Bounded esindajad juba on. Vaikedefinitsioon näitabühe võimaliku viisi kõigi väärtuste listi loomiseks: liikuda vähimast suurimani ühikulise sammu-ga.

Kui uus klass on deklareeritud mingite klasside alamklassiks, siis on vaikedefinitsioonidesvõimalik nende klasside meetodeid kasutada, sest nende olemasolu on garanteeritud. Koodis(188) on näiteks kasutust leidnud aritmeetilise jada süntaks, mis tuleb ülemklassistEnum, ningminBound ja maxBound ülemklassistBounded.

Nüüd saame näiteks tõeväärtustüübi ja suurusvahekorratüübi uue klassi esindajaks määrata liht-salt ridadega

instance Fin Bool where, (189)

instance Fin Ordering where, (190)

sest ainus meetodkõik on klassideklaratsioonis juba defineeritud.

Ülesandeid

394. Otsida Hugsi teegist üles klassideOrd, Num, Real, Integral, Fractional, Floating,RealFrac definitsioonid ja saada neist aru.

395. Kirjutada oma moodulisse deklaratsioonid (188), (189), (190) ja testida interaktiivses kesk-konnas meetoditkõik nii tõeväärtus- kui suurusvahekorratüübi jaoks.

396. Defineerida tüüpInt lõplike tüüpide klassi esindajaks, määrates meetodikõik väärtusekslisti, kus kõik4-baidised täisarvud on järjestatud absoluutväärtuse järgi alates0-st.

397. Defineerida klass, mis oleks nii tsentraalsümmeetriliste kui ka vahekaugustega tüüpide klas-si alamklass ja kus oleksid defineeritud meetodidkodeeri ja dekodeeri , mis tähendak-sid vastavalt kodeerimist täisarvuga tüübistInteger ja vastupidist operatsiooni. Anda neilemeetoditele vaikedefinitsioonid, mis kasutavad ülemklasside meetodeid. Viia tüüpInt uueklassi esindajaks tühja kehaga esindajadeklaratsioonigaja testida.

216

Page 217: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

398. Defineerida klass nimegaExp arvutüüpide klassi alamklassina, millesse kuuluks meetodaste , mille kavandatav tähendus oleks ujukomaarvu astendamineloodavasse klassi kuu-luvat tüüpi arvuga. Kirjutada esindajadeklaratsioonid vähemalt ühe täisarvutüübi ja üheujukomaarvutüübi lisamiseks uude klassi, nii et meetodiaste definitsioon vastaks kavan-datavale tähendusele.

Tingimuslikud esindajad

Ka esindajadeklaratsiooni päisesse võib lisada tüübikonteksti. See seab piirangud tüübimuutuja-tele, mis sisalduvad tüübiavaldises, mille väärtused see deklaratsioon klassi esindajaks paneb.

Näiteks võime ühe esindajadeklaratsiooniga kuulutada klassi Enum esindajaks kõik tüübidMaybe A, mille korral A ise kuulub klassiEnum. Täpsemalt, võime klassiEnum iga tüübiA jaoks antud selle klassi meetodid ühtsel viisil üle kanda vastavale tüübileMaybe A. Selle viibellu deklaratsioon

instance (Enum a) => Enum (Maybe a) where

fromEnum (Just x)= 1 + fromEnum x

fromEnum _= 0

toEnum a= case compare a 0 of

GT-> Just (toEnum (a - 1))

EQ-> Nothing

_-> error "toEnum: negatiivne argument "

. (191)

KlassisEnum on küll rohkem meetodeid, kuid ülejäänud meetodite jaoks onolemas vaikedefi-nitsioonid operaatoritefromEnum ja toEnum kaudu.

Deklaratsiooni (191) olemasolul on võimalik näiteks kasutada aritmeetilise jada süntaksit listidejaoks, mille elemendid onMaybe-tüüpi. Näiteks avaldise[Just ’A’ .. Just ’E’] väär-tus on listJust A : Just B : Just C : Just D : Just E : []. Samas pole deklaratsiooni (191) efekt alatiehk see, mida sooviti. See, kui väärtuseleNothing seatakse vastavusse0 ja ülejäänud väärtus-tele1 võrra suurem arv kui vastavaleJust argumendile, sobib juhul, kui originaaltüübi väärtustetäisarvkoodid on naturaalarvud, kuid negatiivsete koodide korral, mida tuleb ka ette, varjutabNothing midagi ära.

217

Page 218: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Tähelepanelikul lugejal tekib küsimus, kas meetodite definitsioonid koodis (191) on rekursiivsed.Esineb jufromEnum definitsioonisfromEnum ja toEnum definitsioonistoEnum .

Ühest küljest, tegu on ülelaadimisega. MuutujatefromEnum ja toEnum väärtused vasakus japaremas pooles on erinevad. Esimese definitsiooni vasakus pooles onfromEnum väärtus tüüpiMaybe A → Int, paremas pooles aga tüüpiA → Int; analoogiline vahe on teises definitsioonis.

Samas on tsüklilisus koodis (191) ikkagi peidus, sest igasttüübistA alustades saab arendadalõpmatu tüüpide jadaA, Maybe A, Maybe (Maybe A), . . . ja see deklaratsioon annab meetodi-tele fromEnum ja toEnum sisu kõigi nende tüüpide jaoks, kusjuures igaMaybe-tüübi korraldefineeritakse meetod sama meetodi kaudu selles jadas eelmisel tüübil. Arvestades ilmutamatatüübiparameetrit, on tõepoolest tegu rekursiivse definitsiooniga, nimelt struktuurse rekursioonigaüle tüübiparameetri ehituse.

Võime soovida näiteks laiendada oma sümmeetriliste tüüpide klassi üle listitüüpide, mille ele-menditüübid on sümmeetrilised; antud listi vastandlist võiks olla selline, kus kõik elemendid onasendatud vastandelementidega ja nende järjekord on algsega võrreldes vastupidine. Siis sobibkood

instance (Sümm a) => Sümm[a] where

sümm= foldl ( \ as x -> sümm x : as) []

.

Tulemusena saavad sümmeetriliseks kõik listitüübid, mille elemenditüüp juba oli sümmeetriline,siis kõik listitüübid, mille elemenditüübid on sellised listitüübid, jne lõpmatuseni. Jällegi kutsubmeetodi definitsioon välja meetodit ennast ja tegemist on rekursiooniga üle tüübi ehituse.

Siinkohal tasub märkida, et kui näiteks on tahtmine korragasümmeetriliseks kuulutada kõikarvutüübid (see oleks mõttekas, sest vastandarvu leidmineon igas arvutüübis olemas), siis sellegajätab Haskell meid hätta. Deklaratsioon kujul

instance (Num a) => Sümm awhere

d

on keelatud, sest üleminekul ei lisandu tüübile struktuuri.

Selline esindajadeklaratsioon sisuliselt ütleks, et sümmeetriliste tüüpide klass on arvutüüpideklassi ülemklass. Alam- ja ülemklassisuhete määramiseks saab kasutada ainult klassideklarat-sioone. Antud olukorras ei aita ka klassideklaratsioonid,sestNum on standardteegis juba defi-neeritud ja defineeritud klassidele saab lisada vaid alamklasse, mitte ülemklasse.

Ülesandeid

399. Otsida Hugsi teegist üles deklaratsioon, mis defineerib listide võrduse, ja saada sellest aru.

218

Page 219: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

400. Kirjutada esindajadeklaratsioon, mis laiendab klassi Num meetodid loomulikul viisil tüü-pideleMaybe A tingimusel, etA kuulub klassiNum.

401. Kirjutada esindajadeklaratsioon, mis laiendab klassi Num meetodid komponenthaaval lis-titüüpidele tingimusel, et elemenditüüp kuulub klassiNum. Neid meetodeid kasutades de-fineerida muutujaosasummad samaväärseks avaldisegascanl (+) 0 ja selle ja nendemeetodite abil anda muutujalefibs definitsiooniga (151) samaväärne veelgi lühem defi-nitsioon.

Ülal nägime, kuidas defineerida funktsioonitüüpe klassiShow esindajaks. Võib tekkida küsi-mus, kas funktsioonitüüpe ei saaks ka klassiEq kuuluma panna, et oleks võimalik funktsioonevõrrelda.

Funktsioonitüüpe ei ole pandud klassiEq kuuluma sellepärast, et funktsioonide sisulise võrdu-mise kontrollimine nõuab nende väärtuste võrdumise kontrolli kõigil võimalikel argumentidel,mis tähendab kõigi võimalike argumentide süstemaatilist läbivaatust, see aga pole üldjuhul reaal-selt võimalik (mõtleme näiteks juhule, kus argumenditüüp on protseduuritüüp). Kuid on selge,et mõnede argumenditüüpide puhul on funktsioonide võrdus algoritmiliselt täiesti realiseeritav:eelnevas defineerisime “lõplike” tüüpide klassi, kus meetod kõik tähendab kõigi tüüpi kuulu-vate objektide listi — piisab selle listi kõigile elementidele funktsioone rakendada ja kontrollida,kas tulemused on vastavalt võrdsed. Et tulemuste võrdsust saaks kontrollida, peab lisaks väärtu-setüüp kuuluma klassiEq.

Vastavalt neile tähelepanekutele võime kirjutada koodi

instance (Fin a , Eq b) => Eq (a -> b) where

f == g= all ( \ x -> f x == g x) kõik

.

Kui on olemas ka esindajadeklaratsioon (189), siis näiteksavaldise

fromEnum == \ b -> if b then 1 else 0

väärtustamine annab vastuseksTrue .

Muidugi võime lõplike argumenditüüpidega funktsioonide jaoks defineerida ka stringina esita-mise sisulisemalt kui oli varasem konstantne hüüatus. Stringis võiks ära tuua kõik võimalikudargumendid koos funktsiooni väärtusega. Selleks peab nii argumendi- kui väärtusetüüp kuulumaklassiShow.

Esindajadeklaratsiooni sees piisaks defineerida meetodshow, kuid siinkohal sobib tähelepa-nu juhtida ka teisele sama klassi meetodileshowsPrec , mis võimaldab stringiksteisendustprogrammeerida akumulaatoriga, et vältida tühiümbertõstmisi olukorras, kus kompleksse objek-ti stringikuju arvutamisel tuleb leida ja koostisse lülitada alamobjektide stringikujud. Muutuja

219

Page 220: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

showsPrec väärtus on funktsioon, mis võtab argumendiks (lühikese) täisarvu, stringiks teisen-datava objektia ja mingi juba valmis stringis ja annab tulemuseksa esituse stringina, millelesamas stringis järgnebs. String s on akumulaator. Esimene argument mängib rolli juhul, kuistringikujus tuleb märkida alamobjektide piire (nt sulgudega), tavaliselt võib seda argumenti ig-noreerida.

Kui showsPrec on defineeritud, siisshow definitsiooni pole vaja anda (meetodishow jaokstuleb koos klassiga vaikedefinitsioon meetodishowsPrec kaudu). Nii võiksime saada esinda-jadeklaratsiooni

instance (Fin a , Show a, Show b) => Show (a -> b) where

showsPrec n f as= let

lisa k= (++) ", " .

showsPrec n k .(++) " -> " .showsPrec n (f k)

_ : _ : s= foldr lisa ( ’} ’ : as) kõik

in’{ ’ : s

. (192)

Funktsiooni stringiesituseks saab sellega argument-väärtus-paaride komadega eraldatud loendlooksulgudes, kus argumenti ja väärtust eraldab noolemärk.

Nüüd lastes interaktiivses keskkonnas väärtustada näiteks operaatorinot , saame vastuseks strin-gi “{False -> True, True -> False}”.

Kui funktsioonitüüpA → B kuulub klassiShow ja mingi C on lõplik ja kuulub ka klas-si Show, siis tüüpC → A → B kuulub deklaratsiooni (192) järgi klassiShow — niisaame lõpmata palju funktsioonitüüpe, mis kuuluvad klassiShow. Järelikult saab edu-kalt väärtustada ka avaldisi, mille väärtuseks oncurried-kujulised funktsioonid. Näitekslastes väärtustada(&&), saame vastuseks sisuliselt konjunktsiooni tõeväärtustabeli. Kuiesineb ülelaadimist, tuleb seejuures annoteerida tüüp, muidu pole masinal selge, millisetüübi jaoks tabel koostada. Näitekscompare :: Bool -> Bool -> Ordering jacompare :: Ordering -> Ordering -> Ordering töötavad, samas kui lihtsaltcompare annab tüübivea.

Ülesandeid

402. Milliseid kitsendusi rahuldavad funktsioonitüübid saab panna klassiNum, defineeridesmeetodid komponenthaaval? Kirjutada esindajadeklaratsioon, mis seda teeb. Kõik meeto-did peavad saama definitsiooni.

220

Page 221: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

403. Olgu muutujaintress antud koodiga

intress:: Ordering -> Float

intress LT= 1.5

intress EQ= 4.5

intress _= 12

.

Mõte seisneb hoiuse kolme võimaliku aastaintressi korragaesitamises: argumendileLTvastab tavahoiuse intress, argumendileEQ tähtajalise hoiuse intress ja argumendileGTinvesteerimisriskiga hoiuse oodatav intress. Kõik intressid on protsentides.

Ülesande 402 lahendusele toetudes defineerida muutujatulu väärtuseks funktsioon, misvõtab argumendiks täisarvun ja ujukomaarvux ning annab tulemuseksx-krooniselt hoiu-seltn aasta jooksul saadud tulu samas vormis (st funktsioonina tüüpi Ordering → Float,kus argumendileLT vastab tulu tavahoiuse intressi korral jne).

404. Olgu antud signatuur

matKorruta:: (Num p, Fin a , Show a, Fin b , Show b, Fin c , Show c)=> (a -> b -> p) -> (b -> c -> p) -> a -> c -> p

.

Funktsioonem tüübistA → B → P interpreteerime maatriksitena, kus tüübiA iga objektia jaoks on üks rida, tüübiB iga objektib jaoks üks veerg jam ab ona-le vastava rea jab-levastava veeru ristumisel asuv arv.

Vastavalt sellele ja toetudes ülesande 402 lahendusele defineeridamatKorruta väärtu-seks maatriksite korrutamise operatsioon.

Algebralised tüübid

Kui uute klasside tekitamine kõrvale jätta, oleme seni oma tegemistes piirdunud Haskelli eelde-fineeritud väärtustega. Meie definitsioonid ei loonud uusi väärtusi, vaid kirjeldasid juba olemas-olevaid ja sidusid nendega uusi muutujaid.

Programmeerija saab aga kirjeldada ka täiesti uusi tüüpe jaandmeid. Tüüpide ja andmete maa-ilma laiendamine käib seejuures alati koos, pole võimalik näiteks uute andmete lisamine jubaolemasolevasse tüüpi.

Uute väärtuste sissetoomine tähendab alati uute konstruktorite defineerimist: selleks, et uusi väär-tusi vanadest eristada, tuleb neid koostada uute, endistest erinevate konstruktorite abil.

221

Page 222: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Vabadus uusi väärtusi konstrueerida võimaldab objekte, mis reaalses elus millegi olulise poolesterinevad, mugavalt ka koodis erinevat tüüpi andmetena kujutada. Kood, mis on kirjutatud üles-ande püstitusest tulenevate struktuuride terminites, on inimesele kergemini arusaadav. Samuti ontemasse sisse tehtud vigadest tavalisest suurem osa tüübivead ja ilmnevad kompileerimise käi-gus, mitte täitmise ajal täitmisaegsete vigadena või, veelhullem, arvutuste tulemustes, kus neidkaua aega keegi ei aimagi olevat.

Tüüpe, mida saab kirjeldada Haskelli uute tüüpide defineerimise võimaluste piires, nimetatak-sealgebralisteks (ingl algebraic) teoorias ilmneva analoogia tõttu sellega, kuidas esituvadüldises algebras nn algebralised arvud.

Tüüpide defineerimine

Enamasti sobib uute tüüpide ja neisse kuuluvate väärtuste sissetoomiseks deklaratsioon kujul

data t

= a1 | . . . | an

. (193)

Deklaratsioon (193) peab olema moodulis välistasemel, ta ei saa olla lokaalne. Lisaks peab oleman > 0.

Vaatleme algul lihtsamat juhtu, kus defineeritakse korragaainult üks tüüp. Siis kujutabt endastuue tüübi nime. Kuna süntaktiliselt on tegemist tüübikonstruktoriga, mitte muutujaga, peab nimialgama suurtähega.

Püstkriipsudega eraldatud osada1, . . . , an on alternatiivid. Iga alternatiiv esindab uude tüü-pi kuuluvate andmete või andmestruktuuride üht võimalikkukuju. Koos kirjeldavad nad kõiksellesse tüüpi kuuluvad normaalsed objektid; neile lisandub veel ebanormaalne väärtus⊥.

Täpsemalt, iga andmete kuju eristab teistest uus unikaalneandmekonstruktor, mille rakendamiseljust need andmed saadakse. Konstruktori argumendid on sisuliselt andmestruktuuriväljad (inglfield), mille konstruktor lihtsalt üheks andmestruktuuriks kokku seob, analoogiliselt näiteks sel-lega, kuidas paarikonstruktor oma kaks argumenti üheks paariks kokku seob. Alternatiiv dekla-ratsioonis (193) kujutab endast vastava andmekuju konstruktori täisargumenteeritud rakendusešablooni, kus reaalsete argumentide kohal on nende tüüpe väljendavad avaldised. Seega on igakonstruktori argumentide tüübid fikseeritud. Sellest saavad ka konstruktorid ise omale kindlaltmääratud tüübi. Konstruktoritele seetõttu tüübisignatuure andma ei pea ja Haskell 98 seda kaei võimalda. Laiendustes on olemas ka teistsugune süntaks uute tüüpide defineerimiseks, kusandmekonstruktorid tuuakse sisse signatuuridega.

Andmekonstruktorite nimed peavad olema reeglipärased. Nende rakendus võib ollacurried-kujuline ja seejuures vastavalt soovile prefiksne või infiksne. Andmekonstruktori argumentidearv võib olla ka0; see tähendab, et ühtegi rakendamist ei toimu, konstruktorise esindab väärtustuuest tüübist. Alternatiiv koosneb siis paljast andmekonstruktorist.

222

Page 223: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Kõige lihtsamal juhul pole ühelgi konstruktoril argumente. Siis on defineeritav tüüp loetelutüüp.

Näiteks deklaratsioon

data Kuu= Jaanuar| Veebruar| Märts| Aprill| Mai| Juuni| Juuli| August| September| Oktoober| November| Detsember

(194)

defineerib uue loetelutüübiKuu, milles on 12 väärtust. Need väärtused on seotud uutekonstruktoritegaJaanuar , Veebruar , Märts , Aprill , Mai , Juuni , Juuli , August ,September , Oktoober , November , Detsember .

Kuutüüpi objekte saab kasutada kalendrikuudega seotud arvutuste programmeerimiseks, kui onsoov eristada kalendrikuid muudest väärtustest ja mitte kasutada kuude tähistamiseks näitekstäisarve1 kuni 12. Inimestena võtame objekte tüübistKuu kui kalendrikuid; masina jaoks on te-gu lihtsalt mingite andmetega, mis eristuvad kõigisse teistesse tüüpidesse kuuluvatest andmetestoma unikaalsete nimede poolest.

Eraldi kalendrikuutüübi kasutamine kuudega opereerimiseks teeb koodi tunduvalt arusaadava-maks võrreldes sellega, kui kuid tähistavad täisarvud1 kuni 12. Arvud võivad sealsamas koodistähistada ka mingeid muid reaalse elu suurusi ja sel juhul oleks koodi lugejale kuude ja teistearvuliste suuruste eristamine vaevanõudev. Kui aga lugejanäeb koodis kuutüüpi andmekonst-ruktorit või mõnd muud kuutüüpi avaldist, siis see ütleb talle kohe, et mõeldud on teatavat ka-lendrikuud. Kuutüübi sihipärase kasutamise korral ei saa koodi sisse jääda viga, kus kalendrikuukohal on mõne muu tähendusega objekt, sest selline viga tuleb välja tüübikontrolli käigus ennekoodi jooksutamist. Arvude kasutamise puhul tekib ka oht, et kuu kohale satub arv, millele eivastagi ühtki kuud, näiteks13 või mõni negatiivne arv, ja seda viga võib olla raske avastada;kuutüüpi kasutades midagi sellist juhtuda ei saa.

Põhjused on sarnased sellega, miks võrdlemise puhul kasutatakse võrdlussuhete esitamiseksväärtusiLT, EQ, GT loetelutüübistOrdering, kuigi saaks kasutada ka arve, samuti miks tõe-väärtustel on oma tüüp jne.

Deklaratsiooni (194) olemasolul on kuutüüp oma konstruktoritega põhimõtteliselt kohe kasu-tatav. Kuid lastes interaktiivses keskkonnas väärtustadanäiteks konstruktoriJaanuar , saameoodatava vastuseJaanuar asemel hoopis tüübivea, mis tuleneb sellest, et uus tüüp ei kuulu

223

Page 224: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

klassiShow.

Et panna uusi tüüpe klassidesse kuuluma, võib muidugi kasutada eelnevalt käsitletud võimalusi.Haskellis on aga olemas võimalus lasta uute tüüpide sissetoomisel nende klassikuuluvusi auto-maatselt tuletada. Et kuutüüp läheks automaatselt klassiShow, piisab definitsiooni (194) lõppulisada rida

deriving (Show) . (195)

Kui siis anda interaktiivse keskkonna käsurealt näiteksJaanuar , saame ka vastuseksJaanuar . Niisugune variant rahuldab meid muidugi vaid juhul, kui meil pole uue tüübi objek-tide stringikuju suhtes erisoove. Automaatsel tuletamisel tehakse stringikujud otse konstruktoritenimede järgi.

Edasi võiksime kirjutada näiteks koodi

suvekuud:: [Kuu]

suvekuud= [Juuni , Juuli , August ]

,

mis defineerib muutujasuvekuud väärtuseks suvekuude listi. Kui aga on tarvis funktsiooni,mis võtab kalendrikuid argumendiks, siis võib kasutada kuukonstruktoreid näidistena. Näiteksfunktsiooni, mis võtab argumendiks aasta ja kuu ning arvutab päevade arvu selle aasta selles

224

Page 225: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kuus, saab esitada koodiga

päevi:: (Integer , Kuu) -> Int

päevi (y , m)= case m of

Aprill-> 30

Juuni-> 30

September-> 30

November-> 30

Veebruar| y ‘mod‘ 400 == 0

-> 29| y ‘mod‘ 100 == 0

-> 28| y ‘mod‘ 4 == 0

-> 29| otherwise

-> 28_-> 31

. (196)

Definitsioon (196) on silmatorkavalt kohmakas. Paremas pooles on paljuhargnev valikuavaldis,mille nelja juhu parem pool on ühesugune. Programmeerijaname tahaks need neli juhtu kirjel-dada ühekorraga, et vaeva kokku hoida.

Loomulik tee definitsiooni kohmakuse kaotamiseks oleks koondada ühesuguse päevade arvugakuud listidesse ja kontrollida argumentkuu kuuluvust neisse. Sellise ideega kood aga niisamatööle ei hakka, sest ta nõuab kontrollimist, kas mingi kuu võrdub kuude listi mingi elemendiga— selleks peaks võrdus olema kuutüübil defineeritud ehk kuutüüp kuuluma klassiEq. Jälle-gi aitab tüübiklassikuuluvuse automaatne tuletus: kui lisada täiendi (195) klasside loendisse kaEq, defineerub automaatselt kuutüübil võrduspredikaat, mis loeb iga väärtuse võrdseks parajastiiseendaga.

Samamoodi saab tekitada uute tüüpide kuuluvust ka klassiOrd. Kuutüübi puhul tekib klassiOrd kuuluvuse automaatsel tuletamisel võrdlusoperatsioon, mille järgi aastas eespool paiknevadkuud on tagapool paiknevatest väiksemad. Süsteem tuletab niisuguse võrdluse selle põhjal, milli-ses järjestuses konstruktorid definitsioonis (194) on esitatud. See näitab, et tüübiklassikuuluvuseautomaatse tuletamise puhul on alternatiivide järjekord definitsioonis oluline.

Loetelutüüpide puhul saab automaatselt tuletada ka kuuluvust klassiEnum. See annab võima-

225

Page 226: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

luse loetelutüüpi objektide puhul opereerida nende järjekorranumbritega. Esimene konstruktorsaab järjekorranumbri0, järgmine1 jne, nii et kuutüübi korral ollakse ühe võrra nihkes võrreldestraditsioonilise numeratsiooniga.

Edaspidi on oluline kuutüübi kuuluvus kõigisse mainitud klassidesse, nii et eeldame, et definit-siooni (194) on täiendatud reaga

deriving (Show, Eq, Ord, Enum).

Tänu kuulumisele klassiEnum saab muutujalepäevi anda varasemaga võrreldes üsna lühikesedefinitsiooni

päevi (y , m)| elem jrk [1, 3, 5, 7, 8, 10, 12]

= 31| elem jrk [4, 6, 9, 11]

= 30| y ‘mod‘ 400 == 0

= 29| y ‘mod‘ 100 == 0

= 28| y ‘mod‘ 4 == 0

= 29| otherwise

= 28where

jrk= fromEnum m + 1

, (197)

muutujalesuvekuud aga saab anda uue definitsiooni

suvekuud= [Juuni .. August ]

.

KlassiEnum kuuluvate tüüpide puhul on defineeritud ka muutujadsucc ja pred , mis tähen-davad vastavalt järgmise ja eelmise väärtuse võtmist. Neist esimese abil saame kirjeldada funkt-siooni, mis leiab etteantud kuule järgneva kuu, definitsiooniga

järgmKuu:: (Integer , Kuu) -> (Integer , Kuu)

järgmKuu (y , m)= case m of

Detsember-> (succ y , Jaanuar)

_-> (y , succ m)

. (198)

226

Page 227: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

405. Otsida Hugsi teegist üles tüüpideBool ja Ordering definitsioonid ja saada neist aru.

406. Kirjutada definitsioon (194) oma moodulisse ja viia kuutüüp klassiEnum. DefineeridamuutujakõikKuud väärtuseks list, milles on aastas esinemise järjekorras kõik kalendri-kuud.

407. Defineerid muutujaeelmKuu väärtuseks funktsioon, mis leiab etteantud kuule eelnevakuu. Tüüp peab olema sama mis koodiga (198) defineeritud muutujal järgmKuu .

408. Defineerida loetelutüüpNädalapäev , millesse kuuluvad väärtused vastavad nädalapäeva-dele. Seejärel defineerida muutujatööpäev väärtuseks predikaat, mis võtab argumendiksnädalapäeva ja annabTrue parajasti juhul, kui argument on tööpäev.

409. Defineerida loetelutüüpViker , mille väärtused vastavad värvidele värviringis. Defineeri-da muutujajärgm väärtuseks funktsioon, mis võtab argumendiks värvi ja annab tulemu-seks ringis järgmise värvi. Analoogselt defineerida ka muutuja eelm ringis eelmise värvileidmiseks.

Teine lihtne juht deklaratsioonist kujul (193) on selline,kus on ainult üks alternatiiv. Siis kõiknormaalsed väärtused uuest tüübist konstrueeritakse samakonstruktoriga. Selliste tüüpide hul-ka kuuluvad senituntutest näiteks paaritüüp, kus kõik normaalsed väärtused on konstrueeritudpaarikonstruktoriga.

Näiteks võib lisaks kuutüübile anda kuupäevatüübi deklaratsiooniga

data Daatum= Daatum Integer Kuu Intderiving (Show, Eq, Ord)

. (199)

Kuupäevatüübi nimeks on valitudDaatum , see nähtub deklaratsiooni (199) võrduse vasakustpoolest. Parem pool ütleb, et iga väärtus sellest tüübist konstrueeritakse konstruktorigaDaatum ,mis võtab kolm argumenti: täisarvu, kalendrikuu ja veel ühetäisarvu. Esimese täisarvu mõte onmängida aastaarvu rolli, viimase täisarvu mõte on mängida päeva numbri rolli.

Niisiis on nimi Daatum deklaratsiooni (199) erinevates pooltes kasutatud erinevas tähenduses:vasakul on ta tüüp ehk0 argumendiga tüübikonstruktor, paremal aga3 argumendiga andmekonst-ruktor. AndmekonstruktorDaatum , lähtuvalt tema argumentide tüüpidest ja sellest, et tulemu-seks onDaatum-tüüpi objekt, on tüüpiInteger -> Kuu -> Int -> Daatum (sellestüübiavaldises tähendabDaatum tüüpi). See, et tüübi nimi ja andmekonstruktori nimi langevadkokku, ei sega kuidagi, sest konteksti järgi on alati selge,kas konkreetne nimeDaatum esine-mine tähendab tüüpi või annet. Tavaliselt valitaksegi ühe alternatiiviga tüüpide puhul andme- jatüübikonstruktorile sama nimi, aga muidugi ei ole see kohustuslik.

227

Page 228: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Näiteks oleks Tartu rahu sõlmimise päevDaatum-tüüpi avaldisena üles kirjutatav kujulDaatum 1920 Veebruar 2 .

Kasutades ka varem defineeritud operaatoreidpäevi ja järgmKuu , saab nüüd funktsiooni, misleiab etteantud kuupäevale järgneva kuupäeva, kirjeldadakoodiga

järgmPäev:: Daatum -> Daatum

järgmPäev (Daatum y m d)| päevi (y , m) > d

= Daatum y m (d + 1)| otherwise

= let(y’ , m’)

= järgmKuu (y , m)inDaatum y’ m’ 1

. (200)

Jällegi on omadefineeritud konstruktorit kasutatud argumendinäidises. See näidis sobitub iganormaalse kuupäevatüüpi objektiga ja sobitamise tulemusel saaby väärtuseks kuupäeva esimesevälja, milleks on aastat tähistav arv,msaab väärtuseks teise välja ehk kuu ningd saab väärtusekspäeva numbri.

Päevatüübil on siiski juures samad vead, mille vastu kuutüübi sissetoomisega tegutsesime. Kõr-gema nõudlikkuse korral võiksime ka aasta- ja päevanumbrite tähistamiseks tuua sisse spetsiaal-sed tüübid, nagu tegime kuudega, näiteks deklareerides

data Aasta= A Integer

data Päev= P Int

(201)

ja kasutades definitsioonis (199) neid. TüüpidesseAasta ja Päev kuuluvad objektid on ühetäisarvulise väljaga andmestruktuurid. Siis ei ole võimalik kuupäevi ega aastaid koodis muu-de arvuliste andmetega segi ajada, sest tüübikontroll avastaks selle. Kuupäevadega on asi siiskihullem kui kuudega, sest ka koodi (201) kontekstis saab kirjutada olematuid kuupäevi, kus päe-va number on negatiivne või suurem kui antud kuus päevi. Sedaolukorda on aga juba tülikasparandada, sest erinevates kuudes on päevade arv erinev.

Kasutame järgnevas deklaratsiooniga (199) antud kuupäevatüüpi edasi. Viidatud puuduse kom-penseerimiseks toome sisse predikaadi, mis kontrollib, kas etteantud kuupäevatüüpi objekt väl-jendab reaalset kuupäeva. Seda teeb näiteks kood

olemas:: Daatum -> Bool

olemas (Daatum y m d)= 0 < d && d <= päevi (y , m)

.

228

Page 229: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Päeva legaalsust tuleks kontrollida enne selliste operaatorite nagujärgmPäev väljakutset, mui-du võime saada ootamatuid tulemusi.

Kuna deklaratsiooniga (199) tuletasime ühtlasi kuupäevatüübi kuuluvuse klassidesseEq ja Ord,saab ka kuupäevade võrdust kontrollida ja neid omavahel võrrelda. Võrduse automaatsel tule-tamisel loetakse võrdseteks ainult eristamatud objektid.Selline võrdus kuupäevatüübil vastabpäevade reaalsele võrdusele, sest üht ja sama päeva esitab ainult üks kuupäevatüüpi objekt.

Automaatselt tuletatav järjestus on olemuselt leksikograafiline. Struktuure loetakse kui sõnu, kustähtedeks on struktuuri väljad. Kui struktuurid on koostatud sama konstruktoriga, siis on “sõ-nad” ühepikkused ja vastavad “tähed” on sama tüüpi. Sellisel juhul otsustab esimene erinevus,kumb sõna on teisest eespool. Kui tegelda ainult legaalsetekuupäevadega, siis vastab see järjes-tus ilmselt päevade ajalisele järgnevusele. Siin mängib otsustavat rolli asjaolu, et kuupäevatüübidefinitsioonis on esimese väljana antud aastanumber, siis kuu ja lõpuks päevanumber.

Pangem tähele, et kuupäevatüübi viimine automaatselt klassidesseShow, Eq ja Ord on võimalikvaid tänu sellele, et kõik andmekonstruktoriDaatum argumenditüübid (Integer, Kuu ja Int)kuuluvad samuti klassidesseShow, Eq ja Ord, sest nii stringiksteisendamisel kui ka võrdlemiseltuleb alamülesandena teisendada stringiks või võrrelda välju. Kui väljatüübid neisse klassidesseei kuuluks, tekitaks automaatse tuletuse nõudmine deklaratsioonis (199) tüübivea.

Ülesandeid

410. Defineerida muutujaiseseisvus1 väärtuseks funktsioon, mis võtab argumendiks kuu-päeva ja kontrollib, kas see kuulub Eesti esimesse iseseisvusaega.

411. Defineerida muutujasünnipäevad väärtuseks funktsioon, mis võtab argumendiks kuu-päevad ja annab tulemuseks lõpmatu listi kuupäevadest, mil tähistatakse päevald sündinudinimese sünnipäeva — eeldame, et 29. veebruaril sündinud inimese sünnipäeva tähistatakse29. veebruaril, kui see kuupäev antud aastas on, muidu 28. veebruaril.

412. Defineerida muutujavaheAastates väärtuseks funktsioon, mis võtab argumendiks ob-jektid d1, d2 tüübist Daatum ja annab väärtuseks arvu, mitu täisaastat on kuupäevald2

möödas kuupäevastd1.

413. Defineerida muutujavahePäevades väärtuseks funktsioon, mis võtab argumendiks ob-jektid d1, d2 tüübistDaatum ja annab väärtuseks päevade arvu kuupäevaded1 ja d2 vahel.

414. Automaatse tüübituletuse asemel defineerida kuupäevatüüp klassiShow esindajaks nii, etkuupäevade stringikuju vastaks ISO standardile (n-ö “YYYY-MM-DD”).

415. Defineerida aasta- ja päevanumbritüübid koodiga (201)ja teha päevatüübi definitsioon(199) ümber nii, et uued tüübid oleksid sobivalt kasutatud.Teha ümber definitsioonid (197)ja (198), nii et nad sobivalt kasutavad aastanumbritüüpi. Vajadusel teha ümber ka definit-sioon (200), nii et ta töötaks muutunud päevatüübil.

229

Page 230: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

416. Kasutades ülesandes 408 defineeritud nädalapäevatüüpi, defineerida muutujanädalapäev väärtuseks funktsioon, mis võtab argumendiks kuupäeva ja annabtulemuseks nädalapäeva, millele see kuupäev satub.

417. Nädalakalendris jaotatakse päevad mitte kuudesse, vaid nädalatesse; nädalad on samad na-gu tavalises kalendris. Päevad identifitseeritakse aastaarvu, nädala numbri ja nädalapäevakaudu. Nädal kuulub sellise numbriga aastasse, millise numbriga aastasse kuulub Gregoo-riuse kalendris tema neljapäev. Ühe aasta piires tähistatakse nädalaid järjestikuste täisarvu-dega alates1-st.

Kasutades ülesande (416) lahendust ja ülesandes 408 defineeritud nädalapäevatüüpi, defi-neerida nädalakalendripäevatüüp. Defineerida muutujaolemas väärtuseks predikaat, misvõtab argumendiks sellist nädalakalendripäevatüüpi objekti ja annab välja tõeväärtuse vas-tavalt sellele, kas selline päev on nädalakalendris olemasvõi mitte.

Näitena tüübidefinitsioonist, mis sisaldab mitu alternatiivi, mille konstruktoritel on argumendid,anname deklaratsiooni

data Kalendripäev= Gregooriuse Daatum| Juuliuse Daatumderiving (Show)

. (202)

Väärtused selles tüübis väljendavad kuupäevi konkreetseskalendris — kas gregooriuse või juu-liuse ehk, nagu öeldakse, vastavalt uues või vanas kalendris. Sellist tüüpi võib vaja minna juhul,kui soovitakse päevi talletada selle kalendri järgi, mis sel päeval kehtis. Definitsioon (202) onmõttekas tänu sellele, et nii juuliuse kui gregooriuse kalendris kasutatakse sama kuupäevakuju,mis on kodeeritud tüübisDaatum .

Seda tüüpi avaldisena kirjutatult oleks Tartu rahu sõlmimise päev kujul

Gregooriuse (Daatum 1920 Veebruar 2) .

Deklaratsiooni (193) paremas pooles võib esineda ka keerulisemaid tüübiavaldisi. Olgu meilnäiteks vaja tegelda olukordadega, kus kuupäev ei pruugi olla täpselt teada, kuid meil võib ollatema kohta teatavat informatsiooni. Siis võib olla sobiv kasutada definitsiooni

data EbaDaatum= Teadmata| Piirid Daatum Daatum| Valik [Daatum]

. (203)

See deklaratsioon määratleb tüübi, mille elemendid on kolme sorti. Esimene variant onTeadmata , millel välju pole ja mis näitab, et meil puudub kuupäeva kohta igasugune in-formatsioon. Teiseks on konstruktorigaPiirid kahest kuupäevatüüpi väljast kokku pandud

230

Page 231: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

objektid, kus kaht kuupäeva võib mõista kui alumist ja ülemist ajalist tõket. Kolmandaks onkonstruktorigaValik ühest kuupäevade listi tüüpi väljast ehitatud objektid, kus listis onkõik võimalikud kuupäevad ükshaaval üles loetud. Sellist laadi väärtust esitab näiteks avaldisValik [Daatum 1918 Veebruar 24 , Daatum 1991 August 20 ].

Kirjeldame kasutusnäitena predikaadi, mis võtab argumendiks osalise info ehkEbaDaatum -tüüpi objektip ja kuupäevad tüübistDaatum ning kontrollib, kas osaline infop lubab kuupäevad. Eeldame, et olematuid kuupäevi ei kasutata. Sobib kood

lubab:: EbaDaatum -> Daatum -> Bool

lubab ed d= case ed of

Valik ds-> elem d ds

Piirid a b-> a <= d && d <= b

_-> True

. (204)

Valikuavaldise teise juhu käsitlus kasutab ära ülal selgitatud asjaolu, et päevade ajaline järgnevusvastab automaatselt tuletatud järjestusele.

Kuigi Haskell ei luba olemasolevaid tüüpe laiendada ega kasutada tüübina olemasolevate tüüpideühendit, võimaldavad algebralised tüübid seda kunstlikult lavastada. Näiteks tüüpEbaDaatumon ehituse poolest kuupäevade listi tüübi ja kuupäevade paari tüübi ühend, millesse on lisatudveel üks väärtus. TüüpKalendripäev on aga kuupäevatüübi ühend oma koopiaga.

Ülesandeid

418. Defineerida muutujasordi väärtuseks funktsioon, mis võtab argumendiks koodiga (202)defineeritud tüüpi objektide listi ja annab tulemuseks listi, kus argumentlistis esinevad gre-gooriuse kalendri kuupäevad on ees ja juuliuse kalendri kuupäevad taga. Sama kalendrikuupäevade omavaheline järjestus peab olema sama mis originaallistis.

419. Muuta tüüpKalendripäev klassideEq ja Ord esindajaks selliselt, et sama päeva esitu-sed erinevates kalendrites loetaks võrdseks. ning järjestus oleks vastavalt aja kulgemisele.

420. Defineerida muutujavalikuna väärtuseks funktsioon, mis võtab argumendiksEbaDaatum -tüüpi objekti ja annab tulemuseks sama tüüpi objekti, mis kirjeldabsama päevade hulka, kuid on esitatud valikuna kuupäevade listist.

421. Defineerida uus tüüpÜksliige , millesse kuuluvate objektidega saaks esitada astme ja

231

Page 232: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

eksponendi kujul üksliikmeid, st kujulxa või ax mingi arvua ja muutujax korral. Kons-tandid olgu tüüpiDouble , muutujad tüüpiString .

Deklaratsiooniga (193) saab defineerida mitte ainult üksikuid tüüpe, vaid korraga terveid tüüpideperesid. Selleks peab selle deklaratsiooni vasakus poolesolema uus tüübikonstruktor koos argu-mendinäidistega. Haskell 98 lubab tüübinäidistena kasutada vaid muutujaid (seda nägime jubatüübisünonüümide defineerimisel). Parem pool on ehituseltselline nagu varem kirjeldatud, ainultet ta võib sisaldada neid muutujaid.

Vasaku poole muutujate iga väärtustuse jaoks saame ühe uue tüübi. Täpsemalt, uue tüübikonst-ruktori tähenduseks oncurried-kujuline tüübifunktsioon, mis võtab samapalju argumente, kui-palju vasakus pooles on argumendinäidiseid, ja annab neil tulemuseks tüübi. Tulemustüüp koos-neb objektidest, mis on kirjeldatud parema poolega samamoodi nagu varem, kusjuures lokaalsetemuutujate väärtusteks on vastavad argumendid.

Suures osas on põhimõte sama nagu funktsionaalse vasaku poolega deklaratsioonide ja tüübisü-nonüümide deklaratsioonide puhul. Erinevalt neist deklaratsioonidest on uue tüübi definitsioonivasak pool alati täisargumenteeritud, sest parem pool on kirjeldatud väärtuste kogumina, ta eisaa väljendada tüübifunktsiooni.

Enamasti ongi uusi tüüpe mõistlik sisse tuua perede kaupa, et võimaldada polümorfsust. Kuidefinitsiooni (193) vasak pool sisaldab muutujaid, on alternatiive tähistavad andmekonstrukto-rid automaatselt polümorfsed, nad on kasutatavad tüübimuutujate kõigi väärtuste korral. Konst-ruktorite polümorfsusega oleme varasemast tuttavad, sestsamamoodi polümorfsed on näitekslistikonstruktorid[] ja : ningMaybe-tüüpi väärtuste konstruktoridNothing ja Just .

Näiteks võib deklaratsioonis (203) kirjeldatud tüübigaEbaDaatum sarnase põhimõtte aluselkoostatud tüüpi vaja minna peale kuupäevade ka teistlaadi väärtuste järgnevusvahemike ja va-likute esitamiseks. Seepärast on mõistlik abstraheerida kuupäevatüüp definitsioonis (203) väljasuvaliseks tüübiks. Saame deklaratsiooni

data Eba d= Teadmata| Piirid d d| Valik [d]

, (205)

mis defineerib ühe parameetriga tüübipereEba. Konstruktori Piirid tüüp ond -> d -> Eba d , konstruktoriValik tüüp[d] -> Eba d ja konstruktoriTeadmatatüüp lihtsaltEba d.

Nüüd saame kirjutada näiteks avaldisePiirid 0 5 , mille tüübiks võib kirjutadaEba Integer või üldisemalt (Num d) => Eba d, avaldisePiirid (-3.5) 8.9 ,mille tüübiks Eba Double või üldisemalt (Floating a) => Eba d, avaldiseValik "Aa" tüübiga Eba Char jne. Tüüp Eba Daatum on aga sisuliselt sama misendineEbaDaatum . Muidugi ei saa tüüpeEba Daatum ja EbaDaatum kasutada samas

232

Page 233: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

moodulis. Definitsioonid (203) ja (205) ei saa samas moodulis esineda, sest defineerivad samadandmekonstruktorid.

Kui nüüd kirjeldame muutujaonTeadmata koodiga

onTeadmata Teadmata= True

onTeadmata _= False

, (206)

siis see muutuja on polümorfne, kasutatav objektide jaoks tüüpidestEba d tüübimuutujadsuvalise väärtuse korral. Signatuuriks deklaratsiooni (206) juurde sobib

onTeadmata:: Eba d -> Bool

.

Muutujalubab definitsioon koodist (204) on aga ilma muutusteta interpreteeritav polümorfselt,tuleb vaid tüübisignatuur ära jätta või asendada signatuuriga

lubab:: (Ord d)=> Eba d -> d -> Bool

.

Ülesandeid

422. Otsida Hugsi teegist tüübiperedeMaybe ja Either definitsioonid ja saada neist aru.

423. Defineerida muutujavõimalik väärtuseks predikaat, mis võtab argumendiks objekti dek-laratsiooniga (205) defineeritud tüübist ja kontrollib, kas leidub väärtus, mis rahuldab selleobjekti esitatud piiranguid.

424. Defineerida kahe parameetriga tüübipere, mille iga konkreetne tüüp sisaldab parameetrite-ga määratud tüüpide esindajate paarid ja esimese parameetriga määratud tüüpide esinda-jad üksikuna. Uute konstruktorite nimed valida vastavalt tähendusele. Defineerida muutujapaarina väärtuseks funktsioon, mis võtab sellist tüüpi objekti argumendiks, üksikele-mendid teisendab dubleerimise teel paarikujule ja paarid jätab puutumata.

425. Defineerida ülesandes 421 defineeritud tüüp ümber polümorfseks, kus konstanditüüp võiksolla suvaline ja samuti muutujatüüp.

426. Ülesandes 425 kirjutatud tüübidefinitsiooni kontekstis defineerida muutujaisExp väärtu-seks funktsioon, mis annabTrue, kui argument on eksponendikujul, jaFalse, kui argumenton astmekujul.

233

Page 234: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

427. Ülesandes 425 kirjutatud tüübidefinitsiooni kontekstis defineerida muutujadoominokuju väärtuseks funktsioon, mis võtab argumendiks üksliikmetelisti jaannab tulemuseksTrue või False vastavalt sellele, kas iga kaks järjestikust liiget omavadühesugust argumenti (muutujat või konstanti), mis on eespool seisvas üksliikmes astendajaja tagapool seisvas astendatav, või ei ole see nii.

Rekursiivselt defineeritud tüübid

Väga tihti läheb vaja andmestruktuure, mille ehitus on tsükliline selles mõttes, et tema mingialamstruktuur on sama tüüpi kui tervik. Tüüpilised näited on mitmesugused kahendpuud ja üldsehierarhilised puud, mille alampuudeks on omakorda samalaadsed puud. Senikäsitletud andmest-ruktuuridest on listid sellised: iga mittetühja listi sabaon omakorda list.

Sellise andmestruktuuri konstruktor peab vähemalt üheks argumendiks võtma väärtuse, mis si-saldab endas konstrueeritava struktuuriga sama tüüpi struktuuri. Nii näiteks on konstruktori:teine argument alati sama tüüpi list nagu tulemuslist.

Kui tahame sellise tüübi definitsiooni kirja panna kujul (193), peame paremas pooles konstruk-tori argumendi kohal kasutama defineeritavat tüüpi ennast.Seega on tegemist rekursiivse de-finitsiooniga. Arvestades funktsionaalsete keelte rekursioonilembust, on see muidugi võimalikja nii tehaksegi. Tasub märkida, et siinkohal on rekursioonainuvõimalik tee isegi tavapärastesimperatiivsetes keeltes.

Mis puutub listitüüpidesse, siis need on Haskelli sisse ehitatud, seega näidet nende reaalsest de-fineerimisest ei ole võimalik leida. Lähtudes aga deklaratsiooni (193) mõttest ja listide ehitusestvastab listitüüpide perele tinglik definitsioon

data [a]= []| a : [a]

.

Interpreteerides seda pseudodefinitsiooni vastavaltdata-deklaratsioonide mõttele, tõdeme, et kir-jeldatav tüübipere sõltub ühest tüübiparameetrist, kusjuures tüübileA vastav tüüp selles peressisaldab parajasti objekti[] ning kõik sellised objektid, mis ehitatakse konstruktoriga : ühestA-tüüpi objektist ja kirjeldatavaga sama tüüpi objektist.

Defineerime sarnase, kuid juba legaalse näitena tüübid, millesse kuuluvad struktuurid on omaehituselt samuti elementide järjendid nagu listid, kuid mille hulgas puudub tühi objekt. Alter-natiivideks oleksid ühekomponendiline struktuur ja mitmest objektist koostatud järjend. Definit-siooniks sobib

data Joru a= Üks a| Mitu a (Joru a)deriving (Show)

. (207)

234

Page 235: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Parema poole teise alternatiivi ehitus on sama mis listitüüpide definitsioonil, kuid esimeses al-ternatiivis on konstruktoril üks argument. Kui paneksime esimeseks alternatiiviks palja konst-ruktori, saaksime listitüüpidega struktuurilt samaväärsed tüübid, esimese alternatiivi konstruk-tor mängiks tühja listi rolli ja konstruktorMitu kooloni rolli. Definitsiooni (207) puhul agaon võimalikud avaldised tüübistJoru Int näiteksÜks 0 , Üks 1 , Mitu 1 (Üks 2) ,Mitu 3 (Mitu 5 (Üks 15)) jne, mis kõik esitavad struktuuri, kus vähemalt üks kom-ponent.

Kui tüüp sisaldab väärtusi, mille pärisosana esinevad samatüüpi väärtused, tuleb sellel tüübiltöötavate funktsioonide kirjeldamisel loomuldasa kasutada rekursiooni. Nii on see listide puhulja samuti äsjadefineeritud tüübi puhul. Näiteks funktsiooni, mis leiab kogu jorustruktuuri kom-ponentide arvu, defineerib muutujajoruPikkus väärtuseks rekursiivne kood

joruPikkus:: Joru a -> Int

joruPikkus (Mitu _ xj)= 1 + joruPikkus xj

joruPikkus _= 1

,

komponentide summa arvutamise aga võib programmeerida koodiga

joruSumma:: (Num a)=> Joru a -> a

joruSumma (Mitu x xj)= x + joruSumma xj

joruSumma (Üks x)= x

.

Vaatame siin veel näitena kolme sagedasti vajaminevat puutüüpi, millest kaks on kahendpuud,üks aga paljuhargnev puu.

Kahendpuu (ingl binary tree) on puu, mis kas on tühi (ei sisalda ühtki tippu) või sisaldabjuur-tipu ning temavasaku (ingl left) ja parema (ingl right) alampuu, mis kumbki on omakordakahendpuu. See määratlus näitab vaid struktuuri ilma andmeteta; andmete hoidmiseks puustruk-tuuris on mitu mõistlikku võimalust, nt andmed kõigis tippudesvsandmed ainult lehtedes.

Kui andmeid hoitakse puu kõigis tippudes, siis sobib kahendpuutüüp defineerida koodiga

data Kahend a= Tühi| Tipp a (Kahend a) (Kahend a)

. (208)

Tüübiparameeter definitsioonis (208) vastab puus hoitavate andmete tüübile. Esimene alternatiivkirjeldab tühja kahendpuu, teine mittetühja. Teise alternatiivi konstruktori esimese argumendi

235

Page 236: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kohal on tüübiparameeter, mis vastab andmele selles tipus.Ülejäänud kaks argumenti on rekur-siivsed pöördumised, mis tähendab, et puu need väljad on isesama tüüpi puud. Loeme näiteksesimese neist vasakuks ja teise paremaks alampuuks.

Näiteks on korrektsed järgmisedKahend -tüüpi avaldised:Tühi , Tipp 0 Tühi Tühi ,Tipp 5 (Tipp 2 Tühi (Tipp 3 Tühi Tühi)) (Tipp 7 Tühi Tühi) .

Standardne valik puhuks, kus andmed paiknevad ainult kahendpuu lehtedes, on definitsiooniga

data Kahend’ a= Leht a| Harud (Kahend’ a) (Kahend’ a)

(209)

antud tüübid. Tüübiparameetri roll on sama misKahend -tüübi korral. Esimene alternatiiv kir-jeldab ühetipulised puud, neis on igaühes üks väärtus. Teine alternatiiv vastab vahetippudelenagu definitsioonis (208), kuid nüüd puudub komponenditüüpi väli. Seega vahetippudesse ei saaandmeid paigutada.

Definitsioon (209) kitsendab muuski mõttes kahendpuude hulka. Nimelt on välistatud tühi puu,mis omakorda tingib selle, et igal tipul on parajasti kas0 või 2 alluvat.

Kahendpuu komponentide kokkuarvutamine pole sugugi keerulisem kui lineaarse struktuuri kor-ral. Näiteks koodiga (208) defineeritud tüüpide jaoks võiksime komponentide arvu ja summaarvutamise programmeerida koodijuppidega

kahendSuurus:: Kahend a -> Int

kahendSuurus (Tipp _ ut vt)= 1 + kahendSuurus ut + kahendSuurus vt

kahendSuurus _= 0

, (210)

kahendSumma:: (Num a)=> Kahend a -> a

kahendSumma (Tipp x ut vt)= x + kahendSumma ut + kahendSumma vt

kahendSumma _= 0

, (211)

koodiga (209) sissetoodud tüüpide jaoks aga komponentide summa arvutamise programmeerida

236

Page 237: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

koodiga

kahend’Summa:: (Num a)=> Kahend’ a -> a

kahend’Summa (Harud ut vt)= kahend’Summa ut + kahend’Summa vt

kahend’Summa (Leht x)= x

. (212)

Pangem tähele, et definitsioonid (210), (211), (212) järgivad “jaga ja valitse” tehnikat, kusjuuressee on dikteeritud andmestruktuuri ehituse poolt.

Paljuharulise puu tüübina kasutatakse funktsionaalses programmeerimises tavaliselt nnRose’i

puud, mis sisaldab juurtipu ning tema0 või enam kindlas järjekorras alampuud, millest igaükson omakorda Rose’i puu. Alampuude järjend esitatakse listina. Siit tuleneb definitsioon

data Rose a= Juur a [Rose a]

. (213)

See, et definitsioonis (213) puudub rekursiivse pöördumiseta alternatiiv, ei tähenda, et Rose’ipuud alati lõpmatuseni hargnevad. Kui alampuude list on tühi, siis alampuid järelikult po-le ja tegemist on lehega, alluvateta tipuga. Nii on korrektsed avaldised näiteksJuur 0 [] ,Juur 1 [Juur 2 [] , Juur 3 [] ] jpt. Hargnemine ja mittehargnemine on kirjeldatudühtsel viisil.

Rose’i puu komponentide summa arvutamine on ka peaaegu niisama lihtne kui kahendpuul,sobib kood

roseSumma:: (Num a)=> Rose a -> a

roseSumma (Juur x rs)= x + sum (map roseSumma rs)

.

Rose’i puu alampuud moodustavad kokkuRose’i metsa. Tihti on Rose’i puudega tegelemiselmugav Rose’i metsa tüübile eraldi nimi anda. Selleks võiksime sisse tuua tüübisünonüümiMetskoodiga

type Mets a= [Rose a]

. (214)

Seda nime kasutades võib lihtsustada definitsiooni (213), kirjutades

data Rose a= Juur a (Mets a)

. (215)

237

Page 238: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Nüüd on uue tüübi definitsioon (215) tüübisünonüümi definitsiooniga (214) vastastikku rekur-siivne.

Vastastikku tohivad rekursiivsed olla ka uue tüübi definitsioonid omavahel. Haskell ei luba agatüübisünonüüme isekeskis rekursiivselt defineerida. Tüübitaseme definitsioonide rekursiivsetepöördumiste tsüklites peab alati sees olema uue tüübi definitsioon.

Ülesandeid

428. Defineerida muutujajoruElem väärtuseks funktsioon, mis võtab argumendiks objektija sama tüüpi objektide joru (definitsiooni (207) mõttes) jaannab tulemuseks tõeväärtusevastavalt sellele, kas objekt esineb jorus elemendina või mitte.

429. Kirjutada avaldis tüüpiKahend Int , mis väljendab joonisel

0

1 2

4 7

kujutatud kahendpuud.

430. Kirjutada mõned avaldised, mis väljendavad erineva suurusegaKahend’ -tüüpi kahend-puid.

431. Defineerida muutujakahend’Suurus väärtuseks funktsioon, mis võtab argumendiks ka-hendpuu deklaratsiooniga (209) defineeritud tüübist ja annab tulemuseks tema komponen-tide arvu. Defineerida sarnaselt muutujaroseSuurus Rose’i puude jaoks.

432. Defineerida muutujakõrgus väärtuseks funktsioon, mis võtab argumendiks kahendpuudeklaratsiooniga (208) defineeritud tüübist ja annab tulemuseks tema kõrguse (tiputasemetearvu).

433. Defineerida puutüüp, mille igal puul on juurtipp, iga puu igal tipul on kas0 või 2 alluvatning igas tipus on andmekirje.

434. Kirjutada operaatorid ülesandes 433 defineeritud kahendpuu andmeväljade arvu ja summaleidmiseks.

435. Defineerida Rose’i puu ja Rose’i metsa tüübid nii, et metsatüüp oleks uus ja puutüüp süno-nüüm. Üldine struktuur peab säilima, liigendkohtade konkreetne ehitus võib olla teistsugu-ne kui ülaltoodud definitsioonide puhul.

238

Page 239: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

436. Kirjutada operaatorid ülesandes 435 defineeritud puude andmeväljade arvu ja summa leid-miseks.

Puu konstrueerimise esmase näitena defineerime muutujatäielik väärtuseks funktsiooni, misvõtab argumendiks täisarvu ja annab tulemuseks täieliku kahendpuu koodiga (208) defineeri-tud tüübist kõrgusegan, kus andmeteks tippudes on arvud, mis näitavad vastavast tipust lähtuvaalampuu kõrgust. Kahendpuud nimetataksetäielikuks, kui tema lehed on juurest kõik ühe-kaugusel ja kõigil ülejäänud tippudel on täpselt kaks alluvat. Näitekstäielik 2 peaks väär-tustamisel andmaTipp 2 (Tipp 1 Tühi Tühi) (Tipp 1 Tühi Tühi) . Ilmselt onmittetühja täieliku kahendpuu mõlemad alampuud täielikudja 1 võrra väiksema kõrgusega.

Kui argument on negatiivne, siis sobib anda veateade, kuna ülesanne on mõttetu. Kui argumenton 0, siis on tulemuseks tühi puu. Paneme tähele, et positiivse argumendi korral on otsitava puualampuud ühesugused. Seega saab abimuutujat kasutades läbi ühe rekursiivse pöördumisega.Koodiks tuleb

täielik:: Int -> Kahend Int

täielik n= case compare n 0 of

GT-> let

t = täielik (n - 1)inTipp n t t

EQ-> Tühi

_-> error "täielik: negatiivne argument "

. (216)

Arvutus koodiga (216) on lineaarse keerukusega argumendi suhtes — rekursiivsete pöördumistearv on lineaarne ja igal rekursioonitasemel tehakse ühepalju tööd. Samas on selge, et tulemusväljendab puud, mille suurus on sellesama argumendi suhteseksponentsiaalne. Interaktiivseskeskkonnas saadav stringikujul väljund on eksponentsiaalse suurusega ja selle esitamine võtabeksponentsiaalsel hulgal aega, aga see töömaht tuleb sissealles stringikujule viimisel, mitte puukonstrueerimisel.

Erinevate andmestruktuuritüüpide paralleelsel kasutamisel on tüüpiliseks ülesandeks nende oma-vaheline konverteerimine. Kuna list on kõige kasutatavam struktuur, on eriti tihti vaja erinevaidmittelineaarseid struktuurelamendada (ingl flatten), nendes hoitavad komponendid listi üm-ber paigutada, kaotades algse struktuuri, aga samuti vastupidist teisendust, mittelineaarse and-mestruktuuri sisselugemist listi pealt.

Lamendamine seisneb sisuliselt andmestruktuuri läbimiseviisi määramises. On olemas kaksklassikalist rekursiivset puuläbimisviisi: eesjärjestus ja lõppjärjestus.Eesjärjestuse (ingl pre-

239

Page 240: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

order) puhul võetakse algul ette puu juur, seejärel järjest iga alampuu tipud eesjärjestuses.Lõpp-

järjestuse (ingl post-order) puhul läbitakse kõigepealt järjest iga alampuu tipud lõppjärjestusesja lõpuks juur. Mõlemal juhul võetakse alampuud ette samas järjestuses. Kahendpuu juures tun-takse kolmanda klassikalise läbimisviisinakeskjärjestust (ingl in-order), mille korral juurläbitakse vasaku ja parema alampuu läbimise vahel, kumbki alampuu läbitakse omakorda kesk-järjestuses. Realiseerime kõik need läbimisviisid Haskellis Kahend -tüüpide peal.

Otse määratluse järgi kirjutades saaksime näiteks eesjärjestuse puhul naiivse definitsiooni

läbiEes:: Kahend a -> [a]

läbiEes (Tipp x ut vt)= x : läbiEes ut ++ läbiEes vt

läbiEes _= []

. (217)

See aga on ilmselt ebaefektiivne, sest konkatenatsiooni vasakus argumendis esineb rekursiivnepöördumine. Selle asemel tuleb ehitada tulemuslist akumulaatoris.

Olukord on analoogiline listi järjestamisega kiirmeetodil, kus samuti on kaks rekursiivset pöör-dumist ja tulemusena peab valmima list; selle jaoks saime varasemas efektiivsed koodid (120) jaargumendivabas stiilis (166). Andmestruktuuridega, millel on rohkem kui üks kogu struktuurigasama tüüpi väli, tekib taoline olukord tihti, tegu on akumulaatori kasutamise ühe standardjuhuga.

Definitsiooni (166) eeskujul tehes saame eesjärjestuses läbimise jaoks koodi

läbiEes= let

läbi (Tipp x ut vt)= (x : ) . läbi ut . läbi vt

läbi _= id

inflip läbi []

.

Samal põhimõttel kirjutatud koodid lõppjärjestuse ja keskjärjestuse leidmiseks on

läbiLõpp:: Kahend a -> [a]

läbiLõpp= let

läbi (Tipp x ut vt)= läbi ut . läbi vt . (x : )

läbi _= id

inflip läbi []

,

240

Page 241: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

läbiKesk:: Kahend a -> [a]

läbiKesk= let

läbi (Tipp x ut vt)= läbi ut . (x : ) . läbi vt

läbi _= id

inflip läbi []

.

Kõige täpsemalt järgib järjestamise kiirmeetodi eeskuju just läbimine keskjärjestuses, sest üksikelement paigutatakse siin rekursiivsete pöördumiste tulemuste vahele nagu kiirmeetodis.

Kahendpuu konstrueerimise näitena toome vastupidise operatsiooni, täpsemalt listina esitatudandmete kogumi organiseerimise kahendotsimispuuna. Kahendpuu onkahendotsimispuu, kuitema iga tipur kirje on suuremr vasaku alluva suvalise tipu kirjest ja väiksem-võrdner paremaalluva suvalise tipu kirjest.

Algoritmi koostamise eeskujuks võtame jällegi listi järjestamise kiirmeetodil. Nagu seal, eralda-me listi esimese elemendi; siin paneme ta loodava puu juureks. Ülejäänud elemendid jaotamekaheks vastavalt sellele, kas nad on esimesest elemendist väiksemad või ei ole — selle jaoks onvarasemast olemas koodiga (116) või (144) defineeritud operaatorkaheksLahuta , mida kasu-tasime samamoodi ka järjestamisel. Edasi moodustame rekursiivselt väiksematest elementidestvasaku alampuu ja teistest parema alampuu. Definitsioonikstuleb

kahendOtsimispuu:: (Ord a)=> [a] -> Kahend a

kahendOtsimispuu (x : xs)= let

(us , vs)= kaheksLahuta x xs

inTipp x (kahendOtsimispuu us) (kahendOtsimispuu vs)

kahendOtsimispuu _= Tühi

.

Nägime, et kahendpuu lamendamine listiks (läbimine) ja kahendotsimispuu ehitamine listi põh-jal realiseerivad mõlemad listi kiirmeetodil järjestamise algoritmi osi. Seejuures kiirmeetodiljärjestamise “jagamise” etapile vastab kahendotsimispuuehitamine, “valitsemisele” ehk tulemu-se kokkupanemisele aga läbimine keskjärjestuses. Pannes need kaks tegevust kokku, saame listijärjestamise kiirmeetodile uue realisatsiooni koodiga

järjestaKiir= läbiKesk . kahendOtsimispuu

.

241

Page 242: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Tegu on põhimõttelt sama algoritmiga, kuid selle järgi arvutamisel luuakse vahestruktuur — ka-hendpuu. Varasem abstraktne alamülesannete puu, mis väljendus vaid rekursiivsete väljakutsetestruktuuris, on nüüd ilmutatult andmeobjektina kasutusel.

“Kehastame” nüüd kahendpuudel ka selle skeemi, millega saieespool defineeritud listi järjesta-mine põimimismeetodil. Kasutame siin deklaratsiooniga (209) sissetoodud kahendpuutüüpe, kusandmed asuvad ainult lehtedes.

Kõigepealt on vaja programmeerida operatsioon, mis listi pealt sellise kahendpuu üles ehitab, niiet listi elemendid jääksid tema lehtedesse. Teeme seda kahes osas, mis vastavad koodiga (124) ja(125) defineeritud operaatoritelepõimiTase ja põimiPuu eespool.

Esimeses osas on vaja kirjeldada funktsioon, mis lisab ehitatavasse struktuuri ühe taseme. Va-hestruktuurina sobib kasutada puude listi: see sisaldab elemendina need puud, mis on parasjagukäsutada, ja operatsioon seisneb nende kahekaupa kokkusidumises suuremateks puudeks, misvähendab puude arvu ümmarguselt kaks korda. Saame koodi

seoTase:: [Kahend’ a] -> [Kahend’ a]

seoTase (xt : yt : yts)= Harud xt yt : seoTase yts

seoTase xts= xts

.

Võrreldes koodiga (124) on vahe ainult see, et põimimist ei toimu, selle asemel seotakse põimi-mist vajavad osad ühte andmestruktuuri (kahendpuusse) kokku.

Teise osana peame kirjeldama funktsiooni, mis ühe taseme lisamist itereerib, nii et lõpuks üks-ainuke puu kõik elemendid endasse haarab. Lisame lõppu ka operatsiooni, mis selle puu lististvälja võtab, analoogiliselt sellega, kuidas arvutamisel koodi (125) järgi lõpus listide listist temaainus elementlist välja võetakse. Saame koodi

seoPuu:: [Kahend’ a] -> Kahend’ a

seoPuu xts @ ( _ : _ : _)= seoPuu (seoTase xts)

seoPuu [xt ]= xt

.

Nüüd on vaja programmeerida ka puu järgi järjestatud listi tegemine. Selle käigus tuleb sooritadakõik ärajäänud põimimised. Puu, mis põimimise asemel sai ehitatud, väljendab parajasti operat-sioonide järjekorra struktuuri, mis varasemas lähenemises põimimiste vahel oli. Seega järele-jäänud tegevus on ülesehituselt sama mis koodiga (212) defineeritud elementide kokkuliitmine,

242

Page 243: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ainult liitmisoperatsiooni rollis on põim. Sobib kood

kahend’Põim:: (Ord a)=> Kahend’ [a] -> [a]

kahend’Põim (Harud ust vst)= põimi (kahend’Põim ust) (kahend’Põim vst)

kahend’Põim (Leht xs)= xs

,

kus muutujapõimi on defineeritud koodiga (121).

Nüüd on jäänud vaid järjestamine kirjeldada, ühendades sobivalt kahendpuu ehitamise ja lamen-damise. Et saaks rakendada operaatoritseoTase , peab alglisti esitama puude listi kujul; selleksteememap Leht rakendamisega iga elemendi ühetipuliseks puuks. Analoogselt koodiga (126)tuleb ka siin tühja listi juht eraldi käsitleda, sest operaator seoPuu pole tühja listi jaoks definee-ritud. Tulemuseks on kood

järjestaPõim []= []

järjestaPõim xs= kahend’Põim (seoPuu (map Leht (hakid xs)))

,

kushakid on defineeritud koodiga (123) või (127).

Kood, mis kasutab vahestruktuuri, töötab muidugi mõnevõrra aeglasemalt kui ilma läbi saav, kuidpõhimõttelt sama algoritmi realiseeriv kood. Selles mõttes on äsjatoodud näited kontraillustra-tiivsed. Tegelikkuses püütakse koodi muuta just vastupidises suunas — vahestruktuure vähemkasutama. Selle jaoks on käibel isegi spetsiaalne terminmetsatustamine (ingl deforestra-

tion). Paljudel juhtudel on vahestruktuuridega arvutus programmeeritav lühemalt ja loetavamaltkui otsene, seetõttu uuritakse teaduslikult võimalusi metsatustamist automatiseerida, et program-meerija poleks sunnitud efektiivsuse nimel koodi raskemini jälgitavaks tegema, vaid kompilaatoroskaks ise selle sammu teha.

Siin toodud näidete eesmärk on eelkõige illustreerida andmestruktuuride kasutamist ja tähendust.

Ülesandeid

437. Defineerida muutujajoruListiks väärtuseks funktsioon, mis võtab argumendiks jorudeklaratsiooniga (207) defineeritud tüübist ja annab tulemuseks selle järjendi listina.

438. Defineerida muutujatäielik’ väärtuseks funktsioon, mis võtab argumendiks täisarvun ja kui see on mittenegatiivne, siis annab tulemuseks täieliku kahendpuu koodiga (208)defineeritud tüübist kõrgusegan, kus andmed tippudes on arvud, mis näitavad tipu kaugustpuu juurest.

243

Page 244: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

439. Defineerida muutujanummerdus väärtuseks funktsioon, mis võtab argumendiks täisar-vu n ja kui see on mittenegatiivne, siis annab tulemuseks kahendpuu koodiga (209) defi-neeritud tüübist, mille kõik lehed asuvad juurest kauguseln ja kus andmed näitavad lehejärjekorranumbrit vasakult lugedes.

440. Defineerida muutujapügaKahend väärtuseks funktsioon, mis võtab järjest argumendikstäisarvun ja kahendpuut koodiga (208) defineeritud tüübist ja annab tulemuseks kahend-puu, mille saab puustt nende osade mahalõikamisel, mis jäävad juurest kaugemale kui ntaset.

441. Lahendada ülesandega 440 analoogiline ülesanne Rose’i puude jaoks.

442. Kirjutada operaatorid, millega saaks teisendada Rose’i puude ja metsade kahe esituse vahel:üks, mis antud deklaratsioonidega (214) ja (215), ja teine,mis kirjutatud ülesandes 435.

443. Defineerida muutujaonKahendOtsimis väärtuseks funktsioon, mis võtab argumendikskahendpuu koodiga (208) defineeritud tüübist ja annab tulemuseksTrue või False vastavaltsellele, kas tegemist on kahendotsimispuuga või mitte.

444. Defineerida muutujaotsi väärtuseks funktsioon, mis võtab argumentideks kahendpuutkoodiga (208) defineeritud tüübist ja temas hoitavate väärtustega sama tüüpi objektix: eel-dusel, ett on kahendotsimispuu, annab funktsioon tulemuseksJust u, kusu on t alamst-ruktuur, mille juurtipu kirje onx, kui x esineb puust, vastasel korral annab funktsioontulemuseksNothing.

445. Defineerida muutujaroseEes väärtuseks funktsioon, mis võtab argumendiks Rose’i puudeklaratsiooniga (215) defineeritud tüübist ja annab tulemuseks listi, millesse on salvesta-tud selles puus olevad elemendid eesjärjestuses. Defineerida sarnane muutujaroseLõpplõppjärjestuse jaoks.

446. Lahendada ülesandega 445 analoogne ülesanne Rose’i puude jaoks, mis on defineeritudülesandes 435.

447. Defineerida muutujateed väärtuseks funktsioon, mis võtab argumendiks Rose’i puu jaan-nab tulemuseks sama tüüpi komponentidega listide listi, kus listid vastavad teedele juurestlehte, listi elementideks teel kohatud andmed.

448. Defineerida muutujarose väärtuseks funktsioon, mis võtab argumendiks võrdusega tüüpielementide listide listi ja annab tulemuseks sama tüüpi elementidega Rose’i puu, milleleülesandes 447 kirjeldatud operatsiooni rakendamisel saaksime algse listi tagasi. LeitavaRose’i puu ühegi tipu vahetutes alluvates olevate andmete seas ei tohi olla kordumisi.

244

Page 245: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Agarad väljad

Uued konstruktorid on vaikimisi laisa väärtustamisega. See tähendab, et väärtustamise käiguson võimalik olukord, kus struktuur on määratud (st väliminekonstruktor olemas), samal ajalkui väljad on väärtustamata. Kui kuskil on vaja andmestruktuuri väärtustamist — näiteks agarafunktsiooni argumendis —, siis ei too see kaasa tema komponentide väärtustamist. Struktuurvõib tervikuna olla normaalne, st mitte-⊥, samal ajal kui mõni tema komponent on⊥.

Näiteks kui meil on operaatorihead analoog koodiga (207) defineeritud jorutüüpidel kirjeldatudkoodiga

joruPea (Üks x)= x

joruPea (Mitu x _)= x

,

siis operaatorijoruPea väljakutsel piirdub väärtustaja joru struktuuri väljaselgitamisega ainultkuni esimese elemendini, täpselt nagu operaatorihead rakendamisel listitüüpi avaldisele. See,mis tagapool on, ei mängi rolli.

Kui välju on vaja agaralt väärtustada, siis ühe võimaluse selleks annab muidugi operaator$! .Kuid selle operaatori tihe lisamine koodi on tülikas ja muudab koodi raskemini loetavaks.

Mõnikord on andmetüübi defineerimisel juba teada, et seda tüüpi objektide sihipärasel kasu-tamisel on mõne välja väärtust alati vaja. Siis tasub kasutada Haskelli võimalust lisada süm-bol ! selle välja tüübi ettedata-deklaratsioonis. Vajadusel tuleb välja tüüp asetada sulgudesse.Hüüumärgiga varustatud väljad väärtustatakse andmestruktuuri loomisel alati enne konstruktorit.Seega niisuguse andmestruktuuri väärtustamine toob paratamatult kaasa selliste väljade väärtus-tamise. Neid välju nimetatakseagaraks (ingl strict).

Näiteks võib arvata, et koodiga (199) defineeritud kuupäevatüübi väljad on kõik alati olulised,niipea kui kuupäev kui tervik on oluline. Osaliselt väärtustatud kuupäeval ei ole mõtet. Seegavõib definitsiooni (199) asendada definitsiooniga

data Daatum= Daatum !Integer !Kuu !Intderiving (Show, Eq, Ord)

, (218)

kus kõik väljad on deklareeritud agaraks.

Märk ! ei muuda välja tüüpi, vaid ainult konstruktori väärtustamisstrateegiat selle välja suhtes.Hüüumärk esineb ainult uue tüübi definitsioonis, mitte näiteks seda tüüpi sisaldavates tüübian-notatsioonides ega signatuurides. Välja muutmisel agaraks pole mujal koodis vaja mingeid muu-datusi teha, kui just kood tema laiskust sisuliselt kasutada ei püüa. Näiteks senine kuupäevatüüpikasutav kood läheb ühtviisi läbi nii definitsiooni (199) kuika definitsiooni (218) puhul.

Agaraks võib märkida vabal valikul suvalise hulga välju. Agarad väljad võivad esineda ka mitmealternatiiviga tüüpides.

245

Page 246: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Hugsi teegis on agarate väljadega realiseeritud näiteks ratsionaal- ja kompleksarvutüübid.

Objektihulgana vaadeldes on tüübid, mille definitsioonis on mõni väli märgitud aga-raks, pisut teistsuguse ehitusega kui üdini laiskade konstruktoritega tüübid: neis puu-duvad struktuurid, kus agara välja väärtus on bottom. Näiteks kuupäevatüüpi avaldiseDaatum 2008 undefined 1 väärtus ei erine avaldiseundefined väärtusest, mõlemadon võrdsed bottomiga.

Kui agaraks deklareerimist ei kasuta, siis kuuluvad rekursiivselt defineeritud tüüpi automaatseltka lõpmatud ja osalised struktuurid. Deklaratsiooni (207)olemasolul võiksime kirjutada näiteksdefinitsiooni

nullijoru:: Joru Int

nullijoru= Mitu 0 nullijoru

,

mis annab muutujanullijoru väärtuseks lõpmatu jorutüüpi struktuuri, kus kõik komponendidon nullid.

Deklareerides struktuuritüübi definitsioonis rekursiivset pöördumist sisaldava tüübiga välja aga-raks, on sealt hargnev lõpmatu struktuur välistatud.

Näiteks saame nii defineerida tüübi, mis koosneb listilaadsetest struktuuridest, mis on kõik lõp-likud. Sobib deklaratsioon

data LõpList a= Kõik| a ‘Ja‘ !(Lõplist a)

. (219)

Tühja listi märgib konstruktorKõik , ühe elemendi lisamist konstruktorJa . See näide illustree-rib ühtlasi konstruktori infiksset rakendust oma definitsioonis.

Et infikskuju‘Ja‘ oleks konstruktoriga: sarnaste prioriteedi ja assotsiatiivsusega, lisame kainfiksdeklaratsiooni

infixr 5 ‘Ja‘.

Nüüd saame seda tüüpi “liste” kirja panna avaldistega nagu näiteks

1 ‘Ja‘ 2 ‘Ja‘ 3 ‘Ja‘ Kõik (220)

jms.

Lisades deklaratsioonile (219) rea

deriving (Show) ,

võib juhtuda, et paljastame Hugsi ja GHC loojate erineva tõlgenduse klassiShow kuuluvuseautomaatse tuletuse detailidest Haskelli keeledefinitsioonis. Et tulemus oleks ühtmoodi loetav

246

Page 247: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

oodataval viisil, st sidesõna “ja” esineks listi elementide vahel, võiks loobuda automaatsest tule-tusest ja kirjutada oma esindajadeklaratsiooni

instance (Show a) => Show (LõpList a) where

showsPrec n l as= let

loend (x ‘Ja‘ xs)= showsPrec n x . (" ja " ++) . loend xs

loend _= ("kõik) " ++)

in’( ’ : loend l as

.

Nüüd annab avaldise (220) väärtustamine interaktiivses keskkonnas vastuse “(1 ja 2 ja 3 jakõik)”.

Lõpmatut ega osalist listi koodiga (219) defineeritud tüübist kirja panna ei saa. Avaldis, mis ilmaagarate väljadeta annaks väärtustamisel lõpmatu struktuuri, annab nüüd tühja lõpmatu tsükli —struktuuri ei teki.

Näiteks kirjutame koodiga (138) defineeritud operaatorilõplik definitsiooni ümber selliselt,et argumendiks oleks küll tavaline list, kuid tulemus oleksdeklaratsiooniga (219) sisse toodudtüüpi. Saame koodi

lõplik:: [a] -> LõpList a

lõplik (x : xs)= x ‘Ja‘ lõplik xs

lõplik _= Kõik

.

Nüüd kui mingi avaldisel väärtus on lõplik list, siis avaldiselõplik l väärtustamise tule-museks on sama listi väljendav struktuur uuest listitüübist, lõpmatu listi puhul aga jääb arvutuslõpmatusse tsüklisse midagi välja andmata.

Struktuure koodiga (219) defineeritud tüüpidest saab väärtustada ainult kas lõpuni või üld-se mitte. Näiteks avaldiseconst 0 $! (lõplik [1 .. 10000 ]) väärtustamine ehitab10000-elemendilise struktuuri (võrdluseks,const 0 $! [1 .. 10000 ] väärtustamiselväärtustatakse list ainult esimese koolonini).

Ülesandeid

449. Anda koodiga (202) defineeritud tüübile uus definitsioon, mille korral konstruktorid oleksagara väärtustamisega.

247

Page 248: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

450. Defineerida muutuja, mille väärtuseks on selline lõpmatu kahendpuu, et rakendades sellelemuutujale ülesandes 440 defineeritud operaatoritpügaKahend , saame lahendada ülesan-de 216.

451. Defineerida kahendpuutüüp, millesse kuuluvad parajasti kõik vasakult lõplikud (st kõikvasakute alluvate jadad on lõplikud) kahendpuud.

452. Defineerida lõplike Rose’i puude tüüp.

Mähistüübid

Lisaks deklaratsioonile võtmesõnagadata saab Haskellis uusi tüüpe defineerida ka võtmesõna-ganewtype. Deklaratsioon on kujul

newtype t

= n u, (221)

kus t koosneb uue tüübi nimest koos argumente tähistavate muutujatega (peab olema täisargu-menteeritud),n on uus andmekonstruktor jau on tüübiavaldis, mis võib sisaldadat-s sisse toodudmuutujaid. Lisada võib kaderiving-klausli klassikuuluvuste automaatseks tuletuseks.

Tegu on mõneti vaheastmegadata- ja type-deklaratsiooni vahel. Süntaktiliselt on deklaratsioon(221) nagudata-deklaratsioon (193), mille paremal pool on täpselt üks alternatiiv ja selles al-ternatiivis on konstruktoril täpselt üks argument (lisaksmuidugi erinevus võtmesõnas). Samutivõib öelda, et deklaratsioon (221) on nagu tüübisünonüümi definitsioon, kuid erinevalt viimasestesineb paremas pooles uus andmekonstruktor ja ka klassikuuluvuse tuletus on sünonüümi puhulvõimatu.

Kõik deklaratsiooniga (221) defineeritud tüübid on n-ömähistüübid: uus andmekonstruktormähib ühe olemasolevat tüüpi väärtuse uut tüüpi väärtuseks. Deklaratsiooniga (221) antakse va-nale tüübileu uus vormt, mis oleks justkui täiesti uus tüüp. Selle põhjal nimetame deklaratsiooni(221)mähistüübideklaratsiooniks (ingl type renaming).

Mähistüüpi kui sellist võib luua ka tavalisedata-deklaratsiooniga, kuid eraldi mähistüübidekla-ratsiooniga loodud tüüp on semantiliselt teistsugune kui ta oleks juhul, kui võtmesõnanewtypeasemel oleks kasutatud võtmesõnadata.

Erinevus seisneb selles, et mähistüübideklaratsiooni puhul on uus andmekonstruktor masinalenähtav vaid tüübianalüüsi etapil, kuid andmetega opereerimise ajaks (st programmi jooksuta-misel) heidetakse need minema. Programmeerija peab vana jauue tüübi vahel vahet tegema sa-mamoodi nagudata-deklaratsiooni korral, ka tüübikorrektsuse kontroll teeb neil tüüpidel vahet,kuid edasi on mähistüübideklaratsiooniga (221) sisse toodud tüübid juba samaväärsed mähitudtüübiga, otsekui selle asemel oleks olnud sünonüümideklaratsioon

type t

= u.

248

Page 249: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Tulenevalt mähiste äraheitmisest väärtustamise etapil onmähistüübideklaratsiooniga sisse too-dud andmekonstruktoril mõned üllatavad omadused, mida võib olla kasulik juba programmeeri-misel arvestada.

Esiteks, selline andmekonstruktor on alati agar; teisiti öeldult, selle konstruktori argument onagar väli. Kuna täitmisajal seda konstruktorit näha pole, ei ole mähistüüpi avaldise väärtustamiselvõimalik väärtuse normaalsuse üle otsustada välimise konstruktori väljailmumise järgi, mistõttuväärtustamine jätkub sujuvalt avaldise selle osaga, mis jääb konstruktori alla.

Selle tõttu pole Haskellis lubatud mähistüübideklaratsioonis välja agaraks märkida. Väli on agarniikuinii, sel märkel poleks mõtet.

Teiseks, sellise andmekonstruktoriga moodustatud näidise sobitamisel ei toimu otsustamine, kasavaldist on vaja väärtustada, selle konstruktori juures, vaid see delegeeritakse näidise siseosale.Näidisesobitusel reaalselt üldse ei kontrollita, kas väärtus on konstrueeritud sama konstruktoriga.Kui tüüp klapib (ja kuna väärtustamine juba käib, siis tüüp on klappinud), siis peab klappimaka välimine konstruktor, sest mähistüübis muid konstruktoreid pole ja agaruse tõttu on isegi⊥põhimõtteliselt selle konstruktoriga ehitatud.

See pole sama mis laisa näidise käitumine: laisk näidis ei delegeeri otsustust sissepoole, vaidmusta kastina sobitub iga avaldisega. Võib isegi öelda, et näidisesobitus töötab põhimõtteliseltsamamoodi nagu tavalise konstruktoriga koostatud näidiste korral. Erinevust käitumises võibseletada väärtustamise lõppkujude erineva käsitlusega, millest lähtub struktuuri klappimine võimitteklappimine. Kui tavaliste tüüpide puhul on bottomi lõppkuju ilma konstruktorita ja seda kaagarate väljade korral, siis mähistüübideklaratsioonigaloodud tüüpidel loeme bottomi lõppku-juks konstruktoriga kuju.

Olgu meil mähistüübideklaratsioon

newtype Mähis a= Mähis a

.

Siis näiteks definitsioonid

(x , Mähis y)= (1 , undefined)

ja

(x , Mähis y)= (1 , Mähis undefined)

annavad mõlemad muutujax väärtuseks1, sestundefined ja Mähis undefined on sa-maväärsed, mõlemal juhul sobitatakse näidisy avaldisegaundefined ja see õnnestub. Samasdefinitsioonid

(x , Mähis [])= (1 , undefined)

249

Page 250: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ja

(x , Mähis [])= (1 , Mähis undefined)

annavadx väärtuseks⊥. Mõlemal juhul sobitatakse näidis[] avaldisegaundefined ja seeebaõnnestub.

Võrdluseks kui toome mähistüübi sissedata-deklaratsiooniga, milles väli on märgitud agaraks,siis tuleb teistsugune efekt. Olgu meil deklaratsioon

data Mähis’ a= Mähis’ !a

.

Siis definitsioonid

(x , Mähis’ y)= (1 , undefined)

ja

(x , Mähis’ y)= (1 , Mähis’ undefined)

annavad mõlemadx väärtuseks⊥. Kuigi ka siin onundefined ja Mähis’ undefinedsamaväärsed, on näidises konstruktor alles ja seetõttu hakatakse avaldist väärtustama kohe, mis-tõttu viga tuleb välja, sobitus ebaõnnestub.

Mähistüübideklaratsioonid on vajalikud selleks, et võimaldada efektiivsemat mähistüüpidessekuuluvate objektidega opereerimist võrreldes sellega, kui see toimuksdata-deklaratsiooniga sissetoodud tüüpide puhul. Võib tekkida küsimus, milleks mähistüübid ise vajalikud on. Haskellisvõib neil eristada mitut eesmärki.

Üks neist on lihtsalt rollivahetus, mida võib täheldada ka üldisemalt, mitte ainult mähistüüpidel.Näiteks kood (202), mis defineeris kuupäevad seoses kindla kalendriga, annab kuupäevatüüpiobjektidele kaks rolli, mida tähistavad uued konstruktorid Gregooriuse ja Juuliuse . Spet-siifilist rolli märkiv uus konstruktor tasub kasutusele võtta ka juhul, kui mitut alternatiivi ei ole, etvältida erineva tähendusega struktuuride segiminekut, kuigi põhimõtteliselt oleks võimalik läbiajada ka vanade tüüpidega.

Rollivahetuse tüüpolukord on teatavate abstraktsete andmestruktuuride mooduli või teegi loomi-ne, kus andmestruktuuri esitus on mingi teine, konkreetne andmestruktuur.

Näiteks võib juhtuda, et tahame mingeid andmeid hoida järjestatud listides, sest mõni tihedalt ka-sutatav operatsioon on järjestatud listidel efektiivsem kui üldjuhul. Kuna andmestruktuuriks onreaalselt vana hea list, siis võiksime läbi saada ilma uute tüüpideta. Samas kui järjestatud listidehoidmiseks defineerida omaette tüüp ja kirjutada moodul, mis ekspordib vaid sellised operat-sioonid järjestatud listidega, mis garanteerivad vormi jasisu vastavuse, st et uut tüüpi objektina

250

Page 251: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

esitatakse tõepoolest vaid järjestatud liste, siis saab mooduli kasutaja olla kindel, et järjestatudlistid ja teised listid ei lähe segi — kohe tekiks tüübiviga.

Viime selle plaani ellu. Alustame mähistüübideklaratsioonist

newtype JList a= JList [a]deriving (Eq , Show)

.

Järjestatud listi loomiseks tavalisest listist peame listi järjestama ja mähkima konstruktoriga.Selleks sobib kood

looJ:: (Ord a)=> [a] -> JList a

looJ= JList . järjestaPõim

,

mis kasutab varem defineeritud operaatoritjärjestaPõim .

Järjestatud listide kasutamiseks võime nüüd defineerida spetsiaalsed operaatorid näiteks koodiga

tühi:: (Ord a)=> JList a

tühi= JList []

,

üksik:: (Ord a)=> a -> JList a

üksik x= JList [x]

,

ühenda:: (Ord a)=> JList a -> JList a -> JList a

ühenda (JList xs) (JList ys)= JList (põimi xs ys)

,

eraldaVähim:: (Ord a)=> JList a -> (a , JList a)

eraldaVähim (JList (x : xs))= (x , JList xs)

eraldaVähim _= error "eraldaVähim: tühi järjestatud list "

,

251

Page 252: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

mis kasutab koodiga (121) defineeritud operaatoritpõimi . Et järjestatud listi tüüpi objektistlistiesitust kätte saada ja seda tavalise listina edasi kasutada, kirjutame operaatorilist koodiga

list:: (Ord a)=> JList a -> [a]

list (JList xs)= xs

.

Üksiku elemendi lisamiseks järjestatud listi võib nüüd kirjutada koodi

lisa:: (Ord a)=> a -> JList a -> JList a

lisa x xj= ühenda (üksik x) xj

,

mis enam järjestatud listi reaalset esitust ei kasuta.

Et välistada suvalise listi sattumist järjestatud listi tüüpi objekti rolli, mis meie eesmärgi nul-liks, ei tohi see moodul eksportida konstruktoritJList . Väljaspool moodulit tohib järjestatudlisti saada luua vaid muutujagalooJ ja teiste selle mooduli operaatoritega, mis garanteerivadtulemuse järjestatuse. Selleks piisab kirjutada moodulile ekspordiloendiga päis

module JList(

JList , looJ , tühi , üksik , ühenda , eraldaVähim , list , lisa) where

.

(222)

Haskelli reeglite kohaselt ei saa andmekonstruktorid ekspordiloendi välistasemel esineda, see-ga nimi JList tähistab tüüpi, mitte andmekonstruktorit. Konstruktori eksportimiseks tulnukssee lisada tüübiJList järele sulgudesse alamekspordiloendisse, nii et tekkinuks ekspordiüksuskujul JList (JList) .

Eri rollides kasutatavate andmestruktuuride eristamine tüübitasemel on vajalik ka juhul, kui prog-rammeerija soovib mõne klassi meetodeid eri rollide puhul erinevalt defineerida. Haskellis ei saasama tüüp olla sama klassi esindaja mitmes eksemplaris. Kuna näiteks listide stringiksteisen-damine on juba standardteegis defineeritud, siis kui programmeerija tahab, et järjestatud listeesitataks stringina teisiti, siis peab ta nende jaoks uue tüübipere kasutusele võtma või loobumastandardse meetodishow kasutamisest nende korral üldse, mis kasvõi interaktiivses keskkonnastoob kaasa ebamugavusi ja alandab koodi loetavust.

Järjestatud listi tüüpide peale võib nüüd analoogsel viisil omakorda ehitada hulgatüübid, missamuti moodustavad ühe tüübiparameetriga pere. Tüübiparameeter näitab, millisesse tüüpi kuu-luvatest objektidest moodustatud hulkadega on tegu. Hulgaesituseks oleks järjestatud list, milleselemendid ei kordu.

252

Page 253: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Vaatame siin aga teist võimalikku hulkade esitust karakteristlike funktsioonide abil. Hulgaka-

rakteristlik funktsioon on predikaat, mis ütleb, kas objekt kuulub hulka või mitte. Niisiis,kui jutuks on tüüpiA kuuluvatest objektidest moodustatud hulgad, siis nende karakteristlikudfunktsioonid on tüüpiA → Bool. Toome nad sisse koodiga

newtype Hulk a= Kar (a -> Bool)

ja anname ka mõned põhilised hulgaoperatsioonid koodiga

tühi:: (Eq a)=> Hulk a

tühi= Kar (const False)

,

täis:: (Eq a)=> Hulk a

täis= Kar (const True)

,

ainult:: (Eq a)=> a -> Hulk a

ainult x= Kar (x ==)

,

täiend:: (Eq a)=> Hulk a -> Hulk a

täiend (Kar f)= Kar (not . f)

,

ühend:: (Eq a)=> Hulk a -> Hulk a -> Hulk a

ühend (Kar f) (Kar g)= Kar ( \ z -> f z || g z)

jne.

Tahame nüüd hulgatüüpe saada klassideEq ja Show esindajaks. Kui järjestatud listide tüüpidepuhul polnud see mingi probleem, sest töötas automaatne tuletus, siis nüüd see läbi ei läheks,kuna siseesituses on funktsioonitüüp, millele pole võimalik automaatselt võrduse kontrolli egastringiksteisendamist tuletada.

253

Page 254: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Eeldusel, et alustüüp (st hulga elemenditüüp) on lõplik selles mõttes, et kuulub koodiga (188)defineeritud klassi, saame hulkade võrduse ja stringiksteisenduse käsitsi defineerida. Siin näemeveel üht põhjust, miks mähistüübid Haskellis on vajalikud.Kui hulgatüüp elemenditüübigaAoleks lihtsalt funktsioonitüüpA → Bool, siis poleks neid võimalik klasside esindajaks defineeri-da, sest tüübiavaldisesa -> Bool esineb argumendinaBool , mis pole muutuja — sellist asjaHaskell esindajaks defineeritava tüübipere kirjelduses eiluba.

Mähkimine avab tee esindajaks defineerimisele. Sobivad deklaratsioonid

instance (Fin a) => Eq (Hulk a) where

Kar f == Kar g= f == g

,

instance (Fin a , Show a) => Show (Hulk a) where

showsPrec n (Kar p) as= let

pKõik= filter p kõik

lisa x= (++) ", " . showsPrec n x

in’{ ’ :( if null pKõik then id else drop 2)

(foldr lisa ( ’} ’ : as) pKõik)

.

Ka siin võib pahategemisvõimaluste vähendamiseks moodulile kirjutada valikulise ekspordigapäise

module Hulk(

Hulk , Tühi , Täis , ainult , täiend , ühend)

.

Esindajadeklaratsioonid eksporditakse niikuinii.

Märgime, et kumbki jutuks olnud hulkade esitusviis — järjestatud listidega ega karakteristli-ke funktsioonidega — pole optimaalne. Hugsi teegis on mittestandardne moodulData.Set, kusantakse hulk kui andmestruktuur palju efektiivsemalt teatavate kahendpuude kujul. Tõsi, seeesitusviis eeldab, et alustüübil oleks olemas järjestus. Sama nõude esitab järjestatud listidega lä-henemine, kuid karakteristlikud funktsioonid töötavad ilma järjestuseta, nende puhul on ainsakstingimuseks võrduse olemasolu.

254

Page 255: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Ülesandeid

453. Täiendada järjestatud listide moodulit ja ekspordilisti muutujagaonTühi , mille väärtusekson predikaat, mis kontrollib, kas argumendiks saadud järjestatud list on tühi või mitte.

454. Väljaspool järjestatud listide moodulit, kuid kasutades ülesandes 453 defineeritud muutu-jat onTühi , defineerida muutujalahuta väärtuseks funktsioon, mis võtab argumendikskaks järjestatud listi ja annab tulemuseks järjestatud listi, mis saadakse esimesest listist teiselisti kõigi elementide eemaldamisel.

455. Väljaspool järjestatud listide moodulit defineerida muutujasisaldub väärtuseks binaar-ne predikaat, mis võtab argumendiks kaks järjestatud listija annab tulemuseTrue, kuiesimese listi kõik elemendid on vähemalt sama kordsusega teises listis, muiduFalse.

456. Kirjutada hulgaoperatsioonide moodul, kus alusstruktuuriks oleks järjestatud list, milleselemendid ei kordu. Realiseerida tühi hulk, üheelemendiline hulk etteantud elemendiga,ühend, lõige, vahe ja võrdus. Kirjutada päis, mille ekspordilist välistab sobimatud alus-struktuurid mooduli kasutamisel väljast.

457. Täiendada karakteristlike funktsioonide alusele ehitatud hulkade moodulit ja tema ekspor-dilisti muutujagalõige , mille väärtuseks on hulkade binaarse lõike operatsioon.

458. Täiendada karakteristlike funktsioonide alusele ehitatud hulkade moodulit ja tema ekspor-dilisti muutujategaonTühi ja onTäis , mille väärtuseks on vastavalt predikaadid, misvõtavad argumendiks lõplikku tüüpi elementide hulga ja kontrollivad, kas tegu on tühjahulgaga või hoopis universaalhulgaga, mis sisaldab kõik alustüübi objektid.

459. Väljaspool hulkade moodulit defineerida muutujateüldühend ja üldlõige väärtuseksfunktsioonid, mis võtavad argumendiks hulkade listi ja annavad tulemuseks vastavalt kõiginende hulkade ühendi ja lõike.

Nimelised väljad

Haskellis on võimalus anda andmestruktuuri väljadele nimesid. Väljanimed ehkselektorid

tuuakse sisse uue tüübi definitsiooni paremas pooles. Igas alternatiivis tuleb nimed anda kaskõigile väljadele või mitte ühelegi. Nimede panekul kirjutatakse väljanimed oma tüübiga anno-teeritult konstruktori järele looksulgudesse ja eraldatakse üksteisest komadega, nii et alternatiivjääb kujule

c

{s1 :: t1,. . . . . . . .,sl :: tl

}

, (223)

255

Page 256: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kusc on uus andmekonstruktor,si on väljade nimed ningti on nende vastavad tüübid.

Näiteks isikuandmete kirjete jaoks võime defineerida tüübiIsik koodiga

data Isik= Isik

{eesnimi :: String ,perenimi :: String ,sünniaeg :: Daatum,sugu :: Sugu

}deriving (Show)

, (224)

kus tüüpDaatum tuuakse sisse koodiga (199) või (218), tüübiSugu aga võime defineeridakoodiga

data Sugu= Naine| Mees| Segaderiving (Eq , Show)

,

et keegi end kõrvalejäetuna ei tunneks.

Kirjeid nimeliste väljadega tüübist saab luua sarnase süntaksiga, lihtsalt signatuuride asemel onvõrdused. Näiteks Eesti esimese presidendi isikuandmete kirje saab anda koodiga

president:: Isik

president= Isik

{eesnimi = "Konstantin ",perenimi = "Päts ",sünniaeg = Daatum 1874 Veebruar 23 ,sugu = Mees

}

. (225)

Seejuures pole nimeliste väljade omavaheline järjekord oluline. Muutujapresident definit-

256

Page 257: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

siooni koodis (225) võiks samaväärselt ümber kirjutada näiteks kujul

president= Isik

{perenimi = "Päts ",eesnimi = "Konstantin ",sugu = Mees,sünniaeg = Daatum 1874 Veebruar 23

}

,

kus perekonnanimi on eesnime ees ja sünniaeg on nihkunud lõppu. Väljade nimed välistavadsegimineku.

Samas töötab ka tavaline andmestruktuuri moodustamine, kus väljad identifitseeritakse järje-korra alusel. Kui tüübiu definitsioonis on alternatiiv kujul (223), siis konstruktori c tüüp ont1 -> . . . -> tl -> u. Seega kui soovitakse kirjet moodustada ilma nimesid kasutamata,siis tuleb lihtsalt väljad konstruktori argumentidena õiges järjekorras esitada. Seostamine nime-dega toimub automaatselt. Näiteks muutujapresident definitsioon koodis (225) on täiestisamaväärne ka definitsiooniga

president= Isik "Konstantin " "Päts " (Daatum 1874 Veebruar 23) Mees

.

Nimeliste väljadega definitsiooni puhul on tavapärases mõttes avaldised mitte ainult uued konst-ruktorid, vaid ka kõik väljanimed. Kui tüübiu definitsioonis on alternatiiv kujul (223), siisiga i = 1, . . . , l korral on selektorsi samal ajal muutuja tüüpiu -> ti ja tema väärtuseksolev funktsioon annab kirje järgi väljai-nda välja väärtuse. Nii on korrektne näiteks avaldisperenimi president , mille väärtus on string “Päts”.

See tähendab, et koodiga (224) defineeritud isikutüüp oleksalternatiivselt samaväärselt sissetoodav koodiga

data Isik= Isik String String Daatum Sugu

eesnimi (Isik en _ _ _ )= en

perenimi (Isik _ pn _ _ )= pn

sünniaeg (Isik _ _ sünd _ )= sünd

sugu (Isik _ _ _ sugu)= sugu

,

257

Page 258: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

ainult et see oleks kohmakas ja programmeerija kaotaks võimaluse muutujaideesnimi ,perenimi , sünniaeg , sugu kasutada väljanimedena. Ka automaatselt tuletatav stringiks-teisendus töötaks teisiti.

Kui selektor esineb ainult ühes alternatiivis, siis tema väärtuseks olev funktsioon on ainult sellealternatiivi konstruktoriga ehitatud andmestruktuuridel määratud. Teistel argumentidel on tule-museks⊥ (väärtustamine lõpeb veateatega). Sama väljanime võib kasutada mitmes alternatiivis,kuid sel juhul peab välja tüüp olema sama, et selektor oleks korrektselt tüübitav.

Näiteks võiksime kalendripäevatüübi definitsioonis (202)sisse tuua selektoridaatum , mis oleksühtviisi kasutatav nii gregooriuse kui juuliuse kalendri kuupäevade jaoks. Sobib definitsioon

data Kalendripäev= Gregooriuse { daatum :: Daatum }= Juuliuse { daatum :: Daatum }

.

Näitena polümorfsest nimeliste väljadega tüüpide perest anname koodiga (208) sisse toodud ka-hendpuutüüpidele uue definitsiooni

data Kahend a= Tühi| Tipp

{juur :: a,vasak :: Kahend a ,parem :: Kahend a

}

, (226)

mis toob mittetühjade puude juurtipu elemendi ning vasaku ja parema alampuu jaoks sisse se-lektorid juur , vasak , parem . Et nimelised väljad on ainult süntaktiline suhkur ja sellisedandmestruktuuritüübid on põhimõtteliselt võrdsed selektorideta tüüpidega, kus väljad identifit-seeritakse järjekorra alusel, siis kõik varasem kahendpuutüüpe kasutav kood jätkuvalt töötab.

Samas võib varasemaid definitsioone samaväärselt ümber kirjutada, et ka selektorid leiaksid ka-sutust. Haskell lubab selektore kasutada ka näidistes. Sellise näidise moodustamine käib sama-moodi nagu avaldise moodustamine konstruktori ja komadegaeraldatud võrduste loeteluga look-sulgudes, kuid võrduste paremates pooltes on vastavaid välju märkivad näidised. Nagu avaldi-seski, ei pea väljanimede kasutamisel argumentide järjekord olema see, mis on antud tüübidefi-nitsiooni alternatiivis.

Näiteks muutujalekahendSummasaaks koodis (211) oleva definitsiooni asemel kirjutada defi-nitsiooni

kahendSumma (Tipp { juur = x, vasak = ut , parem = vt })= x + kahendSumma ut + kahendSumma vt

kahendSumma _= 0

, (227)

258

Page 259: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

kus muutujadx , ut , vt esinevad samas tähenduses kui koodis (211).

Võib tekkida küsimus, miks muutujaidx , ut , vt on üldse vaja sisse tuua, kui väljanimed võiksidnende osa suurepäraselt ära täita. Haskell 98 ei luba nimelisi välju kasutavas näidises loobudavõrdustest, kuid Hugsi ja GHC laiendused võimaldavad definitsiooni (227) asemel kirjutada ka

kahendSumma (Tipp { juur , vasak , parem })= juur + kahendSumma vasak + kahendSumma parem

kahendSumma _= 0

.

Siis on võrdusi vaja vaid juhul, kui soovitakse siduda keerulisemat näidist kui muutuja.

Küll aga on Haskelli standardis lubatud terve võrdus ära jätta, kui ta ei seo ühtki vajalikku muu-tujat. Näiteks muutujakahendSuurus definitsiooni (210) saaks kirjutada selektoridega sama-väärse definitsiooni

kahendSuurus (Tipp { vasak = ut , parem = vt })= 1 + kahendSuurus ut + kahendSuurus vt

kahendSuurus _= 0

,

kus nimeliste väljadega näidises välijuur puudub. Pole vajadust (kuid tohib) lisada võrdustjuur = _.

Haskell pakub kõigele sellele lisaks veel ühe süntaktilisekonstruktsiooni nimeliste väljadegaandmestruktuuridega opereerimiseks —värskendamise (ingl update). Kui e on avaldis, milleväärtus on andmestruktuur nimeliste väljadega tüübist, siis saab sama konstruktoriga moodusta-tud andmestruktuurid, mis sellest mõne välja poolest erinevad, kirjeldada avaldisega kujul

e { si1= a1, . . ., sik = ak }, (228)

kussi1, . . . , sik on avaldisegae kirjeldatud andmestruktuuri mingid väljanimed jaa1, . . . , ak on

avaldised, mis kirjeldavad nende väljade väärtusi uues andmestruktuuris. Ülejäänud väljade väär-tused on uues andmestruktuuris samad mis originaalses.

Näiteks kui kahendpuutüübid on defineeritud koodiga (226) ja muutujatäielik koodiga (216),siis avaldise

(täielik 3) { juur = 0, parem = Tühi } (229)

väärtus on puu

0

2

1 1.

259

Page 260: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Avaldises (228) esinev avaldisee ja looksulgudes oleva värskenduste nimekirja järjestkirjutuson kõrgema prioriteediga isegi funktsioonirakendamisest. Seetõttu on näites (229) vaja avaldisetäielik 3 ümber sulge.

Nimelisi välju võib märkida ka agaraks. Samuti võib nime anda mähistüüpi objekti ainsale väl-jale.

Ülesandeid

460. Täiendada kahendpuutüüpide definitsiooni (226) nii, et kõigil kahendpuudel oleks lisaksüks lühikese täisarvu tüüpi väli nimegah. Kirjutada teek, milles oleksid elementaarsedkahendpuude ehitamise operatsioonid antud muutujate väärtuseks nii, et nende muutujatekaudu oleks võimalik koostada mistahes kahendpuid, kusjuures väljah väärtus võrduksalati kahendpuu kõrgusega.

461. Anda kuupäevatüübile mõistlike nimeliste väljadega definitsioon, mille korral loodav tüüpoleks võrdne definitsiooniga (218) sisse toodud tüübiga.

260

Page 261: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

Indeks

“jaga ja valitse”, 130curried-

kuju, 10let-

avaldis, 77where-

konstruktsioon, 78

adresseeriminenime, 23

agarfunktsioon, 11väli, 245väärtustamine, 4

agaralt väärtustavavaldis, 149

ahelmurd, 116ahelmurru

element, 116akumulaator, 124alam-

ekspordiloend, 252klass, 13list, 8

alampuuparem, 235vasak, 235

algebralinetüüp, 222

alternatiiv, 222andme-

avaldis, 24konstruktor, 25muutuja, 25struktuur, 7

anne, 7

annoteeritudavaldis, 26

argumendivabastiil, 182

argumentfunktsiooni, 9

arvFibonacci, 94Stirlingi 1. liiki, 148Stirlingi 2. liiki, 148

assotsiatiivsus, 28mitte-, 29parem-, 29vasak-, 29

avaldis, 24let-, 77agaralt väärtustav, 149andme-, 24annoteeritud, 26lambda-, 59monomorfne, 24polümorfne, 24tingimus-, 63tüübi-, 24valiku-, 64

avaldisetüüp, 24väärtus, 49väärtustamine, 50

baasrekursiooni, 91

bottom, 7

Cantori tolm, 124Collatzi

261

Page 262: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

jada, 113

definitsioonrekursiivne, 91vaike-, 207

deklaratiivneprogrammeerimine, 1

deklaratsioonesindaja-, 208impordi-, 22infiks-, 62klassi-, 207mähistüübi-, 248

deklaratsiooni-süsteem, 69

dünaamilineprogrammeerimine, 143

edastusväärtuse-, 11

eesjärjestus, 239ehk-

näidis, 54ekspordi-

loend, 23eksport, 21ekstensionaalsus, 182element

ahelmurru, 116erind, 193erindi

iseloomustus, 199erindi-

heide, 194püünis, 194

esimese kategooriaväärtus, 2

esindaja-deklaratsioon, 208

Fibonacciarv, 94jada, 94

funktsionaalne

programmeerimine, 1vasak pool, 69

funktsioon, 9agar, 11karakteristlik, 253kõrgemat järku, 10laisk, 11tüübi-, 12

funktsiooniargument, 9järk, 10rakendamine, 9väärtus, 9

generaator, 80GHC, 17GHCi, 17

Haskell 98, 20heide

erindi-, 194hi, 19hmake, 19Hugs, 16

ilmutatud viidatavus, 2imperatiivne

programmeerimine, 1impordi-

deklaratsioon, 22import, 21infiks-

deklaratsioon, 62kuju, 39operaator, 28

iseloomustuserindi, 199

jadaCollatzi, 113Fibonacci, 94Lucas’, 96

jokker, 55järjehoidja, 119

262

Page 263: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

järjend, 8tüübi-, 14

järkfunktsiooni, 10

kahend-otsing, 130puu, 235

kaja, 87karakteristlik

funktsioon, 253keskjärjestus, 240klass, 13

alam-, 13ülem-, 13

klassi-deklaratsioon, 207

klassikuuluvusetuletus, 224

kollisioonnime-, 23

kolmik, 9kolmnurkarv, 128kompositsioon, 159komprehensioon

listi-, 80monaadi-, 84

komprehensioon-süntaks, 80

konkatenatsioon, 83binaarne, 40

konstruktor, 25andme-, 25tüübi-, 25

konstruktsioonwhere-, 78valvuri-, 73

konteksttüübi-, 27

korrutis-tüüp, 8

kujuinfiks-, 39

üld-, 39kõrgemat järku

funktsioon, 10kõrvalefekt, 3

laiskfunktsioon, 11näidis, 55väärtustamine, 4

lambda-avaldis, 59

lamendamine, 239Leibnizi seadus, 2liik, 13liitfunktsioon, 160list, 7

alam-, 8lõpmatu, 8osaline, 8tühi, 7

listielement, 7pea, 8saba, 8segment, 100

listi-komprehensioon, 80

loendekspordi-, 23

alam-, 252loendur, 117loetelu-

tüüp, 7loogiline

programmeerimine, 1Lucas’

jada, 96lõigu poolitamine, 130lõpmatu

list, 8lõppjärjestus, 240

meetod, 207

263

Page 264: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

metsRose’i, 237

metsatustamine, 243mitte-

assotsiatiivsus, 29modulaarne, 1monaadi-

komprehensioon, 84monomorfne

avaldis, 24muutuja, 25

andme-, 25tüübi-, 25

mähis-tüüp, 248

mähistüübi-deklaratsioon, 248

NHC, 18nime

adresseerimine, 23nime-

kollisioon, 23nimeline

väli, 255näidis, 50

ehk-, 54laisk, 55

näidisesobitamine, 51sobitumine, 50väärtus, 50

objekt, 6operaator, 28

infiks-, 28prefiks-, 32

osalinelist, 8

osalist, 165osastring, 148otse-

rekursioon, 117

otsingkahend-, 130

paar, 9parameetriline

polümorfism, 206parem

alampuu, 235parem pool, 51parem-

assotsiatiivsus, 29sektsioon, 41

polümorfism, 24parameetriline, 206universaalne, 206

polümorfneavaldis, 24

prefiks, 202prefiks-

operaator, 32prioriteet, 28programmeerimine

deklaratiivne, 1dünaamiline, 143funktsionaalne, 1imperatiivne, 1loogiline, 1

protseduur, 11puhverdamine, 86puu

kahendtäielik, 239

kahend-, 235Rose’i, 237

pärimine, 214põimimine, 112

rakendamine, 24rekursiivne

definitsioon, 91vastastikku, 91

rekursioon, 91otse-, 117

264

Page 265: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

saba-, 117struktuurne, 91vastastik-, 103

rekursioonibaas, 91samm, 91

Rose’imets, 237puu, 237

saba-rekursioon, 117

sammrekursiooni, 91

segmentlisti, 100

sektsioon, 41parem-, 41vasak, 41

selektor, 255signatuur

tüübi-, 57sobitamine

näidise, 51sobitumine

näidise, 50staatiline

viga, 15stiil

argumendivaba, 182Stirlingi

1. liiki arv, 1482. liiki arv, 148

struktuurandme-, 7

struktuurnerekursioon, 91

summa-tüüp, 9

suurusvahekord, 7sümbol, 7sünonüüm

tüübi-, 39

süntakskomprehensioon-, 80

süsteemdeklaratsiooni-, 69

tingimus-avaldis, 63

täielikkahendpuu, 239

täisargumenteeritud, 44täisarv, 7täitmisaegne

viga, 16tõeväärtus, 7tühi

list, 7tüübi-

avaldis, 24funktsioon, 12järjend, 14konstruktor, 25kontekst, 27muutuja, 25signatuur, 57sünonüüm, 39süsteem, 4

tüüp, 7, 12algebraline, 222avaldise, 24korrutis-, 8loetelu-, 7mähis-, 248summa-, 9ühik-, 9

ujukomaarv, 7universaalne

polümorfism, 206

vaike-definitsioon, 207

valiku-avaldis, 64

valvur, 73

265

Page 266: Sissejuhatuskodu.ut.ee/~nestra/mat/inf/p/d/f/fpm/08s/pr/pr.pdfarvestada ja nende ilmutamata olekul on see raskem. Programmeerija peab saama aru, kuidas te-ma kirjutatud koodijärgi

valvuri-konstruktsioon, 73

vasakalampuu, 235

vasak pool, 51funktsionaalne, 69

vasak-assotsiatiivsus, 29sektsioon, 41

vastastik-rekursioon, 103

vastastikkurekursiivne, 91

vigastaatiline, 15täitmisaegne, 16

väli, 222agar, 245nimeline, 255

värskendamine, 259väärtus, 6

avaldise, 49esimese kategooria, 2näidise, 50

väärtustamineagar, 4avaldise, 50laisk, 4

üld-kuju, 39

ülelaadimine, 206ülem-

klass, 13

266