Funktionaalista ohjelmointia tavalliselle koodaajalle, osa 2: Funktorit

Orangit
Orangit
Published in
5 min readMay 25, 2021

--

No niin, arvoisat lapset ja aikuiset! Sarjamme “Funktionaalista ohjelmointia tavalliselle koodaajalle” jatkuu. Viimeksi juteltiin monoideista. Ne olivat kivoja ja helppoja. Tämä toinen osa kertoo toisesta funktionaalisesta perusluokasta eli funktoreista. Tässä on vielä jännä pääsiäisteema, vaikka pääsiäinen menikin jo ajat sitten. Pääsiäismunat on niin mainio analogia funktoreiden selittämiseen.

Varoituksen sana: kun puhumme luokista, tarkoitamme ns. funktionaalisen ohjelmoinnin luokkia (engl. typeclass). Luokat tarkoittavat funktionaalisessa ohjelmoinnissa eri asiaa kuin tavallisessa olio-ohjelmoinnissa. Eli jos olet koodannut vaikka Javaa, niin ole tarkkana, etteivät mene käsitteet sekaisin. Jos et ymmärtänyt tästä kappaleesta mitään, vaara ohi, voit jatkaa lukemista.

Funktori tarkoittaa karkeasti ottaen jonkinlaista astiaa tai säiliötä, jonka sisällä oleville esineille voi tehdä asioita. Vaikka kuulostaisi epämääräiseltä, lue silti rohkeasti eteenpäin. Pääsiäismunavertausten kautta tulet ymmärtämään funktorit tuossa tuokiossa.

Funktori on vähän niin kuin kananmunakennosto. Tämä on nyt muuten sitä pääsiäisteemaa — kananmunista voi tehdä pääsiäismunia maalaamalla ne, mutta siihen päästään myöhemmin.

Tyhjä kananmunakennosto

Tässä on tyhjä kananmunakennosto. Olet varmaan joskus nähnyt tällaisen kotona, sen jälkeen kun kaupasta ostetusta kananmunakennostosta on kaikki munat syöty. Tässä kennossa on paikka kuudelle munalle. Tiedän, että nykyään on olemassa myös nelipaikkaisia kennostoja, mutta nyt puhutaan vanhanaikaisista kuusipaikkaisista.

Tältä se kennosto näyttää kaupassa, kun siinä on vielä kaikki munat tallella:

Täysi kennosto

Leikitään että on pääsiäinen. Otetaan kennostosta yksi muna. Se näyttää tältä, ihan tavalliselta valkoiselta kananmunalta:

Tavallinen valkoinen kananmuna

Haluamme tehdä tästä munasta iloisen punaisen pääsiäismunan.

Oletetaan, että meillä on funktio nimeltä maalaaPunaiseksi. Se ottaa syötteenä jonkin esineen ja palauttaa punaiseksi maalatun version samasta esineestä.

Haluamme siis maalata pääsiäismunan. Punaiseksi, tietysti. Komennamme siis tietokonetta: maalaaPunaiseksi kananmuna.

Punaiseksi muuttunut kananmuna

TADAA! Kananmuna, joka oli aluksi valkoinen, muuttui punaiseksi. Tietokone totteli ihmistä, aivan kuten tietokoneet yleensä tottelevat.

Munien maalaaminen yksitellen on kuitenkin aika tylsää puuhaa. Haluaisimme maalata kokonaisen kennoston kerralla.

Maalaamattomat kananmunat kennostossa

Onneksi meillä on käytössämme mainio maalaaPunaiseksi-funktio, jota käytimme jo äsken menestyksekkäästi yhden munan maalaamiseen.

Komennamme tietokonetta: maalaaPunaiseksi kananmunakennosto!

Kananmunakennosto maalattuna

ÄH! Mikä pettymys! Tietokone maalasi itse kennoston, eli siis sen kellertävän pahvin. Eihän se ollut tarkoitus. Tarkoitus oli maalata ne munat sieltä kennon sisältä. Tietokoneilla ei ole maalaisjärkeä, vaan ne tekevät vain juuri sen mitä käsketään.

Onneksi kananmunakenno on FUNKTORI.

Funktoreilla on operaatio, jonka nimi voisi olla suomeksi “tee asia X kaikille säiliön sisältämille alkioille”. Tämä olisi kuitenkin vähän turhan pitkä nimi, joten tätä operaatiota kutsutaan Haskellissa nimellä fmap ja muissa kielissä, esimerkiksi JavaScriptissä, yleensä nimellä map. Koska Haskellia osaavat vain harvat, käytämme tästä eteenpäin useimmille koodaajille tutumpaa nimeä map. map siis ottaa KAKSI parametriä:

1) Funktion, joka halutaan suorittaa jokaiselle säiliön alkiolle.

2) Itse säiliön, joka sisältää ne alkiot.

Komennamme tietokonetta jälleen: map maalaaPunaiseksi kananmunakennosto!

Tanner tömisee ja maa järisee, ja lopputulos on tämä:

Maalatut kananmunat keltaisessa kennostossa

Jippi jei! Voi pääsiäinen sentään! Saimme kaikki kuusi munaa maalattua kauniisti — tavallaan yhden munan maalaamisen vaivalla (eli yhdellä komennolla)! Näin automatisointi ja tietotekniikka helpottavat jokapäiväistä elämää! Ja kennostokin pysyi kauniin pahvinvärisenä eikä sotkeutunut operaatiossa.

Mitä jos meillä olisikin REKKALASTILLINEN kananmunia? Ja haluaisimme maalata ne kaikki punaisiksi? Yksitellen siihen menisi ikä ja terveys. Mutta — ehkä jo arvasittekin? Rekan perävaunukin on funktori!

Siispä käskemme tietokonetta: map maalaaPunaiseksi rekanPerävaunu!

Kuinkahan monta kananmunaa rekan perävaunuun mahtuu? Monta joka tapauksessa. Yksi komento, ja ne kaikki säteilevät kauniin punaisina! Vieläkö joku väittää ettei funktoreista ole hyötyä?

Miten tämä kaikki sitten liittyy tavallisen koodaajan käytännön työhön? Ehkä jo arvaatkin. Melkein kaikki tuntemasi tietorakenteet ovat funktoreita.

Lista on funktori. Voimme suorittaa minkä tahansa operaation sen jokaiselle alkiolle yhdellä map-kutsulla:

map (* 2) [1, 2, 3, 4, 5] => [2, 4, 6, 8, 10]

JavaScriptissä syntaksi on hieman erilainen, mutta ajatus on sama:

[1, 2, 3, 4, 5].map(x => x * 2) =>[2, 4, 6, 8, 10]

Esimerkiksi binääripuu on myös funktori. Senkin jokaiselle lehdelle voidaan suorittaa jokin operaatio. Oikeastaan tietorakenteen pitää olla melko eksoottinen, jotta se EI olisi funktori.

Funktoreilla on kuitenkin tietyt säännöt, joita pitää noudattaa. Jos haluat tieteellisen kuvauksen niistä, niin lue Wikipediasta: https://en.wikipedia.org/wiki/Functor. Jos sinulle kuitenkin riittää hieman vähemmän matemaattinen selitys, niin säännöt menevät näin:

Funktorien laki 1:

Jos suoritat funktorille funktion, joka ei tee mitään, lopputuloksen pitää olla sama kuin jos ei oltaisi tehty mitään.

Siis näin: äläTeeMitään kananmunakennosto == map äläTeeMitään kananmunakennosto

Esimerkki sellaisesta tietorakenteesta, joka EI ole funktori, olisi vaikkapa sellainen kananmunakennosto, josta putoaa yksi muna pois aina kun sille suoritetaan mikä tahansa operaatio. Silloin komennon map äläTeeMitään kananmunakennosto lopputuloksena olisi kennosto, josta puuttuu munia. Se ei ole sallittua! Tai voit sinä tietysti sellaisen tietorakenteen koodata, mutta se ei sitten olekaan enää laillinen funktori.

Abstraktimpi esimerkki olisi lista, josta putoaa pois yksi alkio aina kun sille tehdään jotain. Eli:

map (* 2) [1, 2, 3, 4, 5] => [2, 4, 6, 8]

Huomaa, että lopputuloksessa on vain neljä alkiota, kun lähtiessä oli viisi.

Funktorien laki 2:

Jos kaksi funktiota komposoidaan (eli koodaajaslangilla putkitetaan) keskenään ja suoritetaan tämä tulos funktorille, saadun lopputuloksen pitää olla sama kuin jos nämä kaksi funktiota suoritettaisiin erikseen peräkkäin.

Menikö yli hilseen? Menisi varmaan minultakin ilman esimerkkiä.

Komposoimme aiemmin tapaamamme funktiot maalaaPunaiseksi ja äläTeeMitään, jolloin syntyy uusi funktio maalaaPunaiseksiÄläkäSenJälkeenTeeMitään.

Jos sovellamme näitä funktioita sellaiseen tietorakenteeseen, joka on laillinen funktori eli noudattaa sääntöjä, sillä ei ole väliä teetkö näin:

map maalaaPunaiseksiÄläkäSenJälkeenTeeMitään kananmunakennosto

vai näin:

map äläTeeMitään (map maalaaPunaiseksi kananmunakennosto)

Lopputulos on sama: kananmunakennosto, jonka kaikki kuusi munaa on maalattu punaisiksi.

Jos nyt taas otetaan se huonompi kananmunakennosto, jota käytin esimerkkinä laki 1:ssä, eli sellainen josta katoaa yksi muna aina kun sille suoritetaan jokin operaatio, niin näinpä ei käykään.

map maalaaPunaiseksiÄläkäSenJälkeenTeeMitään kananmunakennosto

=> seuraus: kennostosta puuttuu yksi muna.

map äläTeeMitään (map maalaaPunaiseksi kananmunakennosto)

=> seuraus: kennostosta puuttuu kaksi munaa, koska map:ia kutsuttiin kahdesti.

Kananmunakennosto, josta katoaa muna aina kun sille suoritetaan jokin operaatio, rikkoo siis molempia funktorien lakeja.

Koodaamisessakin sellainen tietorakenne, josta katoaa yksi alkio aina kun sille tehdään jotain, kuulostaa (ainakin minusta) aika hyödyttömältä. Ainakaan minä en äkkiseltään keksi sellaiselle juuri mitään käyttöä.

Nyt sinä osaat funktorit. Nyt menee hyvin!

Muistakaa ostaa luomukananmunia, jos ostatte kananmunia ylipäätään! Hyvää pääsiäistä 2022!

Haluaisitko oppia kanssamme? Etsimme uusia koodaajia, joille lisää oppiminen on intohimo.

--

--