Funktionaalista ohjelmointia tavalliselle koodaajalle, osa 3: map, flatMap, mitä eroa niillä on ja miksi se on kiinnostavaa?

Orangit
Orangit
Published in
6 min readOct 4, 2021

--

Heippa hei taas kaikki funktionaalisesta ohjelmoinnista kiinnostuneet lapset ja aikuiset! Tällä kertaa jutellaan mapista ja flatMapista.

Jos aihe tuntuu tylsältä jo etukäteen, niin voin antaa lisämotivaatiota: tämän artikkelin lukeminen auttaa sinua askeleen verran eteenpäin tiellä, jonka varrella odottaa pahamaineisten monadien ymmärtäminen!

Alun perin minun ei ollut edes tarkoitus tarinoida mapista ja flatMapista. Mutta kohtalo puuttui peliin Juuson muodossa. Juuso on oivallinen koodaaja ja muutenkin mukava veikko. Yritin viime viikonloppuna selittää Juusolle mikä on monadi.

Kuten kaikki tietävät, se on lähes mahdotonta, mutta sain Juuson hahmottamaan hieman paremmin, mistä on kyse, kun selitin hänelle mapin ja flatMapin yhtäläisyyksistä ja erilaisuuksista. Niinpä kokeilen samaa pedagogista strategiaa teihin, hyvät lukijat.

Muistatte ehkä map-funktion edellisestä jaksosta? Jos ette, niin tässä pieni kertaus.

Funktio maalaaPunaiseksi maalaa mitä tahansa punaiseksi.

Funktio map, joka ottaa syötteeksi toisen funktion, suorittaa syötteenä saadun toisen funktion jokaiselle FUNKTORIN alkiolle. Esimerkissä yllä kananmunakennosto on funktori.

Mietitäänpä sitten, mitä map-funktiolla ei voi tehdä.

Hmm. Se voi tehdä mitä tahansa yksittäiselle funktorin alkiolle. Jos meillä on vaikka funktio maalaaSiniseksi, niin komento map maalaaSiniseksi kananmunakennosto muuttaa kaikki munat sinisiksi.

Ei tarvitse olla kyse maalaamisesta, tarkkavainuisimmat ymmärtävät. Jos meillä on funktio riko, niin map riko kananmunakennosto rikkoo kaikki kennoston 6 munaa.

Noin, siinä oikein kuva niin näette miten kananmunia rikotaan.

Funktorissahan oli sellainen sääntö, että kun map suoritetaan, niin se ei saa muuttaa funktorin RAKENNETTA. Yksittäiselle alkiolle (kananmunalle) se voi tehdä mitä tahansa. Mutta se ei voi esimerkiksi muuttaa kennoston muotoa. Se ei voi lisätä tai poistaa alkioita. Kennosto (rakenne) on ja pysyy, vain yksittäiset kananmunat (alkiot) muuttuvat.

Mitä teet, jos haluat vaikkapa tuplata munien määrän? Silloin kehiin astuu funktio flatMap.

flatMap — yksi kovimmista funktioista

Nyt astutaan sellaiselle abstraktiotasolle, että heikompaa voi alkaa hirvittää, mutta pysykää mukana kuitenkin, kuvia katsomalla ymmärrätte. Funktio flatMap on ikään kuin mapin isoveli. Se voi tehdä melkein mitä tahansa. Muuttaa alkioiden määrää. Muuttaa rakenteen muotoa. Mutta kaikella on hintansa! FlatMap ei nimittäin kelpuuta syötteekseen mitä tahansa. Se tarvitsee syötteekseen aivan erikoisen tyyppisen funktion, joka tekee yksittäisestä alkiosta (muna) rakenteen (kennosto)!

Siinäpä vasta onkin eriskummallinen funktio. tuplaaJaLuoKennosto tekee kaksi asiaa:

  1. Se tuplaa yksittäisen alkion (muna).
  2. Se upottaa tuloksen rakenteeseen (kennosto).

Mihin näin outoa funktiota tarvitaan?

Yksinään tuplaaJaLuoKennosto saattaa näyttää melko järjettömältä. Mutta entäs… kun syötetään se flatMapille?

Otetaan esimerkiksi kennosto, jossa on vain kaksi munaa. Näin homma on helpompi hahmottaa.

Lue yllä olevaa kuvaa näin:

  1. Vasemmalla on lähtötila eli input.
  2. Keskellä on välivaihe, johon ei pääse kiinni ulkopuolelta mitenkään. On siis aivan sama vaikka sitä ei olisi olemassakaan, mutta piirsin sen kuvaan, jotta hahmottaisitte, mitä tapahtuu.
  3. Oikealla on lopputila eli output.

Kun flatMap-funktiota kutsutaan ja annetaan sille syötteeksi funktio tuplaaJaLuoKennosto sekä kaksi (2) munaa sisältävä kennosto, niin tapahtuu seuraavia asioita:

  1. Jokainen muna muuttuu kennostoksi, joka sisältää kaksi munaa.
  2. Näin syntyneet kennostot sulautuvat yhteen yhdeksi kennostoksi, jossa on yhteensä neljä munaa.

Varoitus: JOS YMMÄRRÄT FLATMAPIN, OLET JO VAARALLISEN LÄHELLÄ MONADIN YMMÄRTÄMISTÄ! Hehee.

Katso kuvaa uudestaan. Peitä välivaihe sormella. Katso, ota sormi pois, katso uudestaan.

Munasta tulee kennosto. Kennostot sulautuvat yhteen.

Tarkista ymmärsitkö homman oikein.

Jos lähtötilanne olisi se, että meillä olisi yksi (1) kennosto, jossa on aiemmista esimerkeistä tutut kuusi (6) munaa, niin mitä meillä olisi sen jälkeen, kun kutsumme flatMap tuplaaJaLuoKennosto kennosto?

Aivan oikein, 12 munaa yhdessä kennostossa. Tämä oli helppo. Nyt tulee vaikea.

Mitä jos käyttäisimme flatMapin sijasta tavallista mapia?

Aivan. Kun mapia käyttäen suoritetaan tuplaaJaLuoKennosto jokaiselle munalle, se tekee kuuliaisesti jokaisesta munasta kennoston, jossa on kaksi munaa. Ja sitten se laittaa nämä syntyneet kennostot sen alkuperäisen kennoston sisään, alkuperäisten munien paikalle.

Tämähän on loogista. Kuten sanottu, map ei voi muuttaa rakennetta (funktoria) erilaiseksi. Map voi muuttaa vain yksittäisiä alkioita. Tässä tapauksessa se muuttaa yksittäisen alkion (munan) uudeksi funktoriksi (kennostoksi), joka sisältää kaksi munaa, ja lopuksi tunkee näin syntyneet kennostot alkuperäiseen rakenteeseen (kennostoon).

Huomaatko, mitä tapahtuu?

Syntyy sisäkkäisiä kennostoja.

Miten niistä pääsee eroon? Ei mitenkään. Map ei sellaiseen pysty. Mutta flatMap pystyy, koska se sulauttaa syntyneet välirakenteet toisiinsa. FlatMap pystyy tekemään kaikki samat temput kuin tavallinen map, mutta lisäksi paljon lisää. FlatMap on siis voimakkaampi kuin map. Mutta tämä tarkoittaa sitä, että se on myös rajoitetumpi — sitä ei voi käyttää kaikissa tilanteissa, joissa yksinkertaista mapia voi.

Mapia voi käyttää kaikille funktoreille. FlatMapia ei. FlatMapinkin käsittelemän rakenteen pitää olla funktori, mutta siltä vaaditaan muutakin. Pelkkä funktorius ei riitä. Palataan tähän aiheeseen seuraavassa osassa.

Nyt, kun olet ymmärtänyt asian käytännössä, voit ymmärtää sen teoriassa.

Ensin esittelen pseudotieteellisen tyyppinotaation. Haskellissa tyypit ilmaistaan suunnilleen näin.

Funktion maalaaPunaiseksi tyyppi on “A -> B”. Se tarkoittaa sitä, että se muuttaa yhdentyyppisen asian (A, eli valkoinen esine) toisentyyppiseksi (B, eli punainen esine). Eli A otetaan syötteenä ja B palautetaan.

Funktion map tyyppi on “Functor f => (A -> B) -> f A -> f B”. Näyttääkö kamalalta? Yksi askel kerrallaan ymmärrät kaavan. Functor f tarkoittaa sitä että f on funktori, eli kun myöhemmin luet kaavan loppuosaa niin pidät vain tämän mielessä. Voit kuvitella tässä vaiheessa että f on esimerkiksi kananmunakennosto. Se helpottaa ymmärtämistä alkuvaiheessa.

Jäljelle jää “(A -> B) -> f A -> f B”. Eli funktio, joka ottaa kaksi parametria. Ensimmäinen parametri “(A -> B)” on siis funktio joka muuttaa jotain joksikin. Esimerkiksi maalaaPunaiseksi. Se muuttaa valkoisen kananmunan punaiseksi kananmunaksi.

Toinen parametri “f A” tarkoittaa funktorillista A-tyyppisiä olioita. Jos nyt kuvitellaan, että kyseessä oleva funktori on kananmunakennosto, niin meillä on kananmunakennostollinen A:ta, vaikkapa 6 kananmunaa.

Ja viimeinen osa tuota yhtälöä on se mitä funktio palauttaa, eli “f B”. Eli sama funktori, jonka sisällä on erilaisia asioita kuin ennen! 6 munaa vetävä kennosto, jossa on sisällä 6 punaista kananmunaa!

Puretaanpa siis tämä osiin: map maalaaPunaiseksi kennosto. Ensimmäisenä on map, eli funktio, jota kutsutaan. Sitten tulee funktion ensimmäinen parametri, joka on tyyppiä (A -> B), kuten maalaaPunaiseksi (ottaa yhden, antaa toisen). Ja kennosto on se funktori, jolle tämä tehdään.

Jos muistatte vielä viime jakson esimerkin: map maalaaPunaiseksi rekkalastillinenKananmunia toimisi yhtä hyvin.

flatMapin tyyppi tällä notaatiolla on “(A -> f B) -> f A -> f B”. Eli: funktio, joka ottaa ensimmäisenä parametrinaan funktion, joka muuttaa yhden esineen A täyteen lastatuksi funktoriksi esineitä B. Toinen parametri ja paluuarvo ovat täsmälleen samat kuin mapissakin. Eli “(A -> f B)” on funktio, joka muuttaa yhden valkoisen kananmunan kokonaiseksi kennostoksi munia. Funktio tuplaaJaLuoKennosto muutti yhden kananmunan kaksi munaa sisältäväksi kennostoksi. Ja oleellista tässä on se, että flatMap heittää kaikki syntyneet välikennostot pois ja luo yhden ison kennoston, johon kaikki munat asetetaan.

On olemassa toinenkin funktio, jonka tyyppi on sama “(A -> f B) -> fA -> fB” kuin flatMapilla. Se on bind-funktio, tuttavallisemmin (>>=). Tuo merkki on myös Haskell-kielen logo. Sen verran merkittävästä funktiosta on kysymys. Voin paljastaa, että bind on osa maailmankuulua MONADIA! Ja tähän paljastukseen päätämme tämän jakson.

Pureskelkaapa näitä, ensi kerralla sitten se monadi. Pysykää kuulolla!

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

--

--