• Ei tuloksia

3. Funktionaalinen ohjelmointi

3.4 Haasteet

Funktionaalisessa ohjelmoinnissa on monia ominaisuuksia, jotka tekevät ohjelmista toimintavarmempia ja helpommin ymmärrettäviä. Funktionaalinen ohjelmointi

saat-3.4. Haasteet 9 taa kuitenkin tuntua hankalalta proseduraaliseen ohjelmointiin tottuneen ohjelmoi-jan näkökulmasta. Uuden ohjelmointikielen opetteluun liittyy lähes aina uuden syntaksin opettelu. Syntaksin lisäksi uuden ohjelmointiparadigman opettelu vaatii, että ohjelmoija omaksuu uuden tavan lähestyä ongelmia. Tässä luvussa käsitellään muutamia funktionaaliselle ohjelmoinnille tyypillisiä käsitteitä, jotka saattavat olla aloittelevalle ohjelmoijalle haastavia.

Tähän työhön on valittu tarkasteltaviksi rekursio, korkeamman asteen funktiot sekä monadit. Valinta perustuu kirjoittajan omiin kokemuksiin funktionaalisen ohjel-moinnin opettelusta. Nämä konseptit tulevat kuitenkin usein esille myös muiden kokemuksissa [17, 3, 13], ja erityisesti rekursion haastavuutta on tutkittu aikaisem-min [5, 4].

3.4.1 Rekursio

Koska funktionaalisessa ohjelmoinnissa ei ole mahdollista muuttaa muuttujien ar-voja, ei proseduraalisesta ohjelmoinnista tuttuja silmukkarakenteita voida käyttää.

Funktionaalisissa ohjelmointikielissä toisto tulee toteuttaa rekursion avulla. Monissa kielissä on tarjolla funktioita, kuten map, foldl ja zip, listojen käsittelyyn, mutta nekin on toteutettu rekursion avulla. Yksinkertainen esimerkki rekursiosta on ko-konaislukulistan summan laskeminen, joka voidaan toteuttaa seuraavasti:

1 sum :: [I n t] - > I n t

2 sum [] = 0

3 sum ( n : ns ) = n + sum ns

Summausoperaatio on määritelty rekursiivisesti. Rivit 2 ja 3 määrittelevät funktion paluuarvon eri parametreilla. Rivillä 2 on määritelty rekursion lopetusehto: tyhjän listan summa on 0. Jos lista ei ole tyhjä, siirrytään riville 3, jossa listan summa on määritelty rekursiivisesti: summa on ensimmäisen alkion n ja listan loppuosan ns summa.

Rekursion ymmärtäminen on kuitenkin vaikeaa proseduraalista ohjelmointia opiskel-leille. Rekursiota opetetaan ohjelmoinnin peruskursseilla yleensä melko myöhäisessä vaiheessa. Opetus on konkreettista ja perustuu rekursion suorituksen ymmärtämi-seen, jolloin abstraktimpi ongelman rekursiivinen hahmottaminen jää vähemmälle huomiolle. [4] Rekursiivisia funktioita määriteltäessä on tärkeää määritellä rekursi-olle lopetusehto, jotta suoritus ei jatku loputtomiin. Tällaisen lopetusehdon tun-nistaminen on monelle rekursioon tottumattomalle haastavaa. Huono lopetusehdon valinta voi myös johtaa tarpeettoman monimutkaiseen toteutukseen. [5]

3.4. Haasteet 10

3.4.2 Korkeamman asteen funktiot

Kuten luvussa 3.3 todettiin, funktiot ovat tyypillisesti samanarvoisia kuin muutkin datatyypit, mikä mahdollistaa tehokkaiden abstraktioiden luomisen. Funktioiden parametrit sekä paluuarvot voivat siis olla myös toisia funktioita. Uusien funk-tioiden määritteleminen soveltamalla osittain (engl. partial application) olemassa olevaa funktiota on tärkeä työkalu. Esimerkiksi Haskell-kielessä funktio voi ottaa maksimissaan yhden parametrin. Useita parametreja vastaanottavat funktiot otta-vatkin todellisuudessa vastaan yhden parametrin ja palauttavat funktion, joka ottaa parametrinaan yhden parametrin ja palauttaa funktion, joka ottaa parametrinaan yhden parametrin ja niin edelleen. [12] Esimerkiksimin-funktion tyyppi

1 min :: (Ord a ) => a - > a - > a

voidaan lukea: “min ottaa parametrinaan kaksi järjestettävän tyyppistä arvoa ja palauttaa yhden samantyyppisen arvon”. Sama tyyppimäärittely voidaan kirjoittaa ekvivalentisti muodossa

1 min :: (Ord a ) => a - > ( a - > a )

jolloin siitä nähdään helpommin, että todellisuudessaminottaa vastaan vain yhden parametrin ja palauttaa funktion. Kun funktiota kutsutaan seuraavasti

1 min 2 4

todellisuudessa kutsu tapahtuukin seuraavasti

1 (min 2) 4

Ensimmäiseksi siis kutsutaan min-funktiota vain parametrilla 2. Tämä palaut-taa funktion, joka vertailee parametrina annettua lukua lukuun 2 ja palautpalaut-taa pienemmän. Tätä funktiota kutsutaan parametrilla 4 ja palautetaan tulos. Funk-tioiden osittainen soveltaminen saattaa tuntua aluksi vaikealta konseptilta, mutta se kuitenkin auttaa jakamaan ohjelmaa pienempiin uudelleenkäytettäviin osiin. Esi-merkiksi funktio, joka kertoo listan kaikki luvut kahdella, voidaan määritellä seu-raavasti:

1 d o u b l e L i s t :: (Num a ) => [ a ] - > [ a ]

2 d o u b l e L i s t = map ( ( * ) 2)

Määrittelyssä on hyödynnetty funktion osittaista kutsumista kahteen kertaan. Kut-sumalla kertolaskufunktiota vain parametrilla 2 luodaan funktio, joka kertoo paramet-rina annetun luvun kahdella. Funktiota map kutsutaan antaen parametrina tämä

3.4. Haasteet 11 funktio, jolloin se palauttaa funktion, joka kertoo kaikki annetun listan luvut kahdella.

(Lausekkeen (*) 2 ympärillä olevat sulut voidaan jättää pois käyttämällä funk-tionsoveltamisoperaattoria $, joka olisi kenties Haskellille tyypillisempi tapa. Sulut on jätetty esimerkkiin selvyyden vuoksi.) Ilman funktion osittaista soveltamista doubleList-funktion määrittely voisi näyttää seuraavalta:

1 d o u b l e L i s t xs = map (\ x - > 2 * x ) xs

Funktion tyyppimäärittely on jätetty toistamatta. Määrittely on hieman pidempi kuin osittaista soveltamista käytettäessä. Parametrina annettu listaxs on tarpeet-tomasti kirjoitettu auki, ja myös kertolaskuoperaatio on avattu lambdafunktioksi.

Esimerkin mukainen toteutus saattaa tuntua luontevammalta funktionaalista ohjel-mointia aloittelevalle.

3.4.3 Sivuvaikutuksien esittäminen

Kuten aikaisemmin on todettu, puhtailla funktioilla ei voi olla sivuvaikutuksia. Sivu-vaikutuksia tarvitaan kuitenkin moniin tavallisiin operaatioihin, jotka käsittelevät jotain ohjelman ulkopuolisia resursseja. Tällaisia operaatioita ovat esimerkiksi teks-tin tulostus tai levyllä olevan tiedoston lukeminen. Tarvitaan siis jokin tapa esittää sivuvaikutuksia, jotta funktionaalisella ohjelmointikielellä voidaan toteuttaa käytän-nöllisiä ohjelmia. Seuraavassa perehdytään hieman Haskellin tapaan esittää sivu-vaikutuksia.

Haskellissa sivuvaikutusten esittäminen on toteutettu monadien jaIO a-tietotyypin avulla. IO aon tietotyyppi, joka kuvaa jotakin toimintoa, joka suoritettaessa aiheut-taa sivuvaikutuksen ja tuotaiheut-taaa-tyyppisen tuloksen [9]. Esimerkiksi tiedostonluku-funktion tietotyyppi

1 r e a d F i l e :: FilePath - > IO S t r i n g

tarkoittaa, että kun sitä kutsutaan, se palauttaa toiminnon, joka suoritettaessa tuot-taa tuloksenaan merkkijonon, tässä tapauksessa tiedoston sisällön. Itse funktion kutsuminen ei siis vielä aiheuta sivuvaikutuksia, sillä se vain palauttaa toimin-non, jolla on sivuvaikutuksia. IO-toimintoja voi Haskellissa suorittaa vain main-funktio. [9]

IO-tietotyypin taustalla on monadi. Monadit ovat tapa liittää arvoon jokin kon-teksti sekä soveltaa funktioita näille arvoille konkon-teksti huomioon ottaen [12]. Puh-taasti funktionaalisissa ohjelmointikielissä kaiken datan välityksen täytyy tapah-tua eksplisiittisesti, sillä sivuvaikutuksia ei sallita. Tällä on, kuten luvussa 3.3

3.4. Haasteet 12 todettiin, hyviä puolia, mutta se saattaa myös toisinaan vaikeuttaa ohjelman toteu-tusta. Esimerkiksi, jos haluttaisiin pitää kirjaa jonkin operaation suorituskertojen lukumäärästä, tulisi tämän arvo välittää ja palauttaa aina eksplisiittisesti funktiosta toiseen. Proseduraalisessa ohjelmointikielessä laskuri voisi olla globaali muuttuja, jonka arvoa muutetaan tarvittaessa. Monadit mahdollistavat muun muassa arvo-jen kuljettamisen funktiosta toiseen. [20] HaskellissaIO a on monadi, jonka avulla ohjelman ulkopuolista tilaa kuljetetaan ohjelman suorituksen mukana. Ohjelma saa tilan main-funktiota kutsuttaessa, jonka jälkeen se kulkee IO-toimintojen mukana läpi ohjelman. [10]

Monadien avulla voidaan ketjuttaa operaatioita, jotka saattavat tuottaa virheitä il-man, että edellisen operaation virheellisyyttä tarvitsee erikseen tarkistaa [20]. Mona-dit ovat hyödyllisiä työkaluja funktionaalisessa ohjelmoinnissa, ja niiden ymmärtämi-nen helpottaa muun muassa io-operaatioiden käyttämistä. Erilaisia oppaita, jotka pyrkivät selittämään monadien käsitettä ja käyttöä, on internetissä paljon [15], joten se vaikuttaisi olevan monille haastavaa. Tämä johtuu todennäköisesti siitä, että vastaavaa rakennetta ei tavallisesti ole olemassa proseduraalisessa tai olio-ohjelmoinnissa, sillä sille ei ole tarvetta.

13

LIITTYVÄT TIEDOSTOT