- COMP.CS.200
- 7. JavaScript: FP & async (harjoitustyö)
- 7.1 Funktionaalinen JavaScript
Funktionaalinen JavaScript¶
Mitä on funktionaalinen ohjelmointi?¶
Ohjelmointiparadigmat voidaan jakaa kahteen pääluokkaan: imperatiivisiin ja deklaratiivisiin.
Imperatiivinen paradigma voidaan ajatella koneena, joka ratkaisee ongelman askel askeleelta, tarkasti määritellen, mitä tehtäviä suoritetaan ja missä järjestyksessä. Imperatiivisia ohjelmointikieliä ovat esimerkiksi C, Fortran, Pascal ja Ada. Näissä kielissä ohjelmoija ohjaa suoraan koneen toimintaa, kuten muistinhallintaa ja loogisten operaatioiden suorittamista. Olio-ohjelmointi voidaan nähdä yhtenä imperatiivisen paradigman laajennuksena tai “mausteena”, joka korostaa ohjelman rakennetta ja modulaarisuutta käyttämällä olioita ja niiden välisiä vuorovaikutuksia.
Deklaratiivinen ohjelmointiparadigma puolestaan keskittyy kuvaamaan, mitä halutaan saavuttaa, eikä niinkään miten se toteutetaan. Ohjelmoija antaa tulkille tai kääntäjälle yleiskuvauksen toivotusta lopputuloksesta ilman yksityiskohtaista ohjeistusta toimintatavoista. Esimerkkejä deklaratiivisista kielistä ovat SQL, HTML ja XML. SQL:ssä määritellään, millaiset tiedot halutaan kyselyllä saada, kun taas HTML:ssä määritellään verkkosivun rakenne ja ulkoasu.
Tämän vertailun tarkoituksena ei ole todistaa toisen paradigman paremmuutta toiseen nähden. Imperatiiviset ja deklaratiiviset paradigmat palvelevat eri tarkoituksia, ja molemmat ovat erinomaisia, kun niitä käytetään tarkoituksenmukaisesti ja oikeissa tilanteissa. Esimerkiksi imperatiivinen ohjelmointi voi olla parempi valinta suorituskykykriittisissä sovelluksissa, kun taas deklaratiivinen ohjelmointi yksinkertaistaa monimutkaisten prosessien kuvaamista.
Esimerkki imperatiivisesta ohjelmoinnista¶
Imperatiivinen ohjelmointi soveltuu hyvin sulautettuihin järjestelmiin, joissa järjestelmän toiminta perustuu tarkasti määriteltyihin askelmiin. Esimerkiksi pesukoneen ohjelmointi on hyvä esimerkki tällaisesta sovelluksesta. Pesukoneen ohjelma koostuisi erilaisista vaiheista, jotka suoritetaan tietyssä järjestyksessä:
Kun vedenpinta on alle 23 cm
ota vettä sisään
Kun lämpötila on alle 40 astetta
kuumenna vesi
20 minuutiksi pesuohjelma päälle
Poista vesi
Toista vaiheet kahdesti
Avaa lukko
Pesukonetta ohjaava järjestelmä on jatkuvasti tietoinen pesukoneen tilasta, kuten vedenpinnasta, veden lämpötilasta ja suoritetuista vaiheista. Tämä mahdollistaa järjestelmän reaaliaikaisen reagoinnin olosuhteiden muutoksiin. Imperatiivinen lähestymistapa sopii erityisen hyvin tällaisiin tilanteisiin, joissa ohjelman suoritus riippuu jatkuvasti muuttuvasta tilasta.
Esimerkki deklaratiivisesta ohjelmoinnista¶
Deklaratiivisessa ohjelmoinnissa esitetään mitä halutaan saavuttaa, ei niinkään miten se saavutetaan. Pesukoneen ohjelmointia ei ole yhtä intuitiivista kuvata deklaratiivisessa muodossa, koska tällaiset järjestelmät harvoin toimivat puhtaasti deklaratiivisella lähestymistavalla.
Sen sijaan voimme tarkastella esimerkkinä Facebookin roskapostisuojausta (Facebooks spam protection). Facebookin roskapostin, tietojenkalastelun ja haittaohjelmien havaitseminen täytyy tapahtua reaaliaikaisesti. Tällainen ongelma on haastava ratkaista imperatiivisella ohjelmoinnilla, koska se vaatisi valtavan määrän if-else -ehtoja, mikä tekisi koodista hitaan ja vaikeasti ylläpidettävän.
Sen sijaan Facebook hyödyntää toiminnallista ohjelmointia, joka on yksi deklaratiivisen paradigman alalajeista. He kirjoittivat funktioita, joita voidaan yhdistellä ja käyttää uudelleen. Näitä yhdistelmiä kutsutaan “korkeamman asteen funktioiksi”. Esimerkiksi jokainen käyttäjän toiminto Facebookissa (tykkäykset, jaot, päivitykset jne.) tarkistetaan näiden sääntöjen mukaisesti. Jos haitallinen toiminta havaitaan, se poistetaan välittömästi muiden käyttäjien uutisvirrasta.
Deklaratiivisen ohjelmoinnin merkitys¶
Toiminnallinen ohjelmointi, joka on yleinen deklaratiivinen lähestymistapa, on erityisen tehokas suurten tietomäärien reaaliaikaisessa analysoinnissa ja käsittelyssä. Aiemmin tietomäärät olivat hallittavampia, ja big data mainittiin lähinnä raskaita laskentatehtäviä, kuten sääennusteita, kryptografiaa tai talouslaskelmia, varten. Internetin ja sosiaalisen median myötä datan määrä on kuitenkin kasvanut räjähdysmäisesti.
Nykyään jokainen klikkaus verkossa luo dataa, jota analysoidaan esimerkiksi käyttäjäkokemuksen parantamiseksi, markkinointitarkoituksiin tai turvallisuuden takaamiseksi. Tällaisissa tapauksissa deklaratiivinen ohjelmointi mahdollistaa monimutkaisten sääntöjen joustavan ja tehokkaan toteuttamisen, mikä olisi imperatiivisella lähestymistavalla huomattavasti hankalampaa.
Funktionaalinen ohjelmointi¶
Funktionaalinen ohjelmointi (FP) kuuluu deklaratiiviseen ohjelmointiparadigmaan, jossa keskitytään kuvaamaan mitä ohjelman tulee tehdä, eikä niinkään miten se tehdään. Funktionaalisen ohjelmoinnin keskiössä ovat funktiot, joiden avulla dataa muunnetaan. Ohjelmointi tapahtuu luomalla ja yhdistelemällä funktioita niin, että ne muodostavat ketjuja tai koostumuksia, jotka kuvaavat datan kulkua ja muokkauksia.
On olemassa erityisesti funktionaalista ohjelmointia varten suunniteltuja “puhtaita” ohjelmointikieliä, kuten Haskell <https://www.haskell.org/> ja Elm <https://guide.elm-lang.org/>. Vaikka JavaScript ei ole puhtaasti funktionaalinen kieli, siinä on monia toiminnallisia ominaisuuksia, joita voidaan hyödyntää. Tässä oppaassa keskitytään tarkastelemaan, miten JavaScriptin toiminnallisia ominaisuuksia voidaan käyttää tehokkaasti.
Funktionaalisen ohjelmoinnin tärkeimmät ominaisuudet¶
Muuttumaton data (immutability) Funktionaalisessa ohjelmoinnissa muuttujien arvoja ei koskaan muuteta suoraan. Sen sijaan data käsitellään luomalla uusia kopioita alkuperäisistä olioista tai rakenteista.
Esimerkki: Kuvitellaan, että sinulla on tuoli-olio, joka sisältää tiedon sen nykyisestä sijainnista. Jos haluat muuttaa tuolin sijaintia, et muuta olemassa olevan olion arvoa. Sen sijaan luot uuden olion, jossa on päivitetty sijainti.
const chair = { position: "corner" };
// Muutoksen sijaan luodaan uusi objekti
const movedChair = { ...chair, position: "center" };
console.log(chair); // { position: "corner" }
console.log(movedChair); // { position: "center" }
Tämä saattaa aluksi tuntua ylimääräiseltä työltä, mutta muuttumattomuus tekee koodista luotettavampaa ja virheiden jäljittämisestä helpompaa. Koska data ei muutu suoraan, sivuvaikutukset (side effects) vähenevät, mikä parantaa ohjelman ylläpidettävyyttä ja ennustettavuutta.
Funktioiden yhdistely ja uudelleenkäytettävyys Funktionaalisessa ohjelmoinnissa pyritään kirjoittamaan pieniä, yksittäisiä tehtäviä suorittavia funktioita, joita voidaan yhdistellä suurempien ongelmien ratkaisemiseksi. Tämä mahdollistaa koodin uudelleenkäytön ja parantaa ohjelman selkeyttä.
Funktionaalinen ohjelmointi tarjoaa selkeän ja modulaarisen lähestymistavan ohjelmointiin, erityisesti tilanteissa, joissa käsitellään monimutkaista datavirtaa. Vaikka se voi vaatia hieman erilaista ajattelutapaa, sen hyödyt virheiden vähentämisessä ja koodin ylläpidettävyydessä ovat merkittäviä. Vinkkejä:
käytä
let
taivar
sijastaconst
.Immutable isoille projekteille
Puhdas funktio (pure function):
ei aiheuta sivuvaikutuksia, kuten
console.log()
, API-kutsuja tai yleisiä datamuutoksia,palauttaa aina saman outputin samalle inputille
Missä tilanteissa funktio voi palauttaa eri arvon samoilla parametreilla?
Esimerkiksi jos jotkin ehdot riippuvat globaaleista muuttujista, jotka voidaan mykistää ulkoisessa laajuudessa.
Jos käytät
Math.random
.
Vaikka “puhdas” kuulostaa paremmalta kuin “epäpuhdas”, epäpuhtaus on välttämätöntä vuorovaikutuksessa epäpuhtaan maailman kanssa, esimerkiksi käyttäjän kanssa. Toimivaa Web-sivustoa ei voi rakentaa pelkästään puhtailla funktiolla. Jos käyttäisit vain puhtaita funktioita et voisi:
manipuloida DOM:ta,
noutaa tietoja palvelimelta tai
lähettää tietoja palvelimelle.
Näin ollen molemmilla funktiotyypeillä on tärkeä rooli web-kehityksessä. Silti toiminnallisuuden jakaminen puhtaaseen “liiketoimintalogiikkaan” ja epäpuhtaisiin funktioihin (tietojen nouto, DOM-manipulaatio jne.) voi olla hyvä idea. Näin voit varmistaa, että liiketoimintalogiikkasi on helposti testattavissa. Kun liiketoimintalogiikka on sekoitettuna sivuvaikutuksiin, se tekee sovellusten muokkaamisesta paljon vaativampaa.
Vinkkejä:
Kun funktio on testattu, poista
console.logs
.Älä muuntele suoraan funktiollesi välitettyjä tietoja. Luo kopio, muokkaa sitä ja palauta muokattu kopio.
Älä käytä funktiosi ulkopuolisia arvoja
Palauta aina se, mitä funktiossa on muokattu
Erilliset sivuvaikutukset omille funktioilleen
Korkeamman asteen funktiot: JavaScriptin funktiot
ovat olioita
.
Tämä tarkoittaa käytännössä sitä, että voit määrittää funktion
muuttujaan, taulukon elementteihin tai objektiin. Lisäksi esineenä
funktiolla on oma prototyyppiominaisuus, jonka arvo on “Function.prototype”.
Function.prototype määrittelee kolme funktiota: call()
, bind()
, apply()
.
const greet = (name) => console.log(`Greetings, ${name}`);
greet("Teemu Teekkari"); // Greetings, Teemu Teekkari
Esimerkissä yllä annamme merkkijonon (string) greet-funktiolle parametrina. Merkkijonon sijasta voisimme myös välittää funktion parametrina.
const sayHi = () => console.log("Hi!");
const sayHello = () => console.log("Hello");
const greet = (type, sayHi, sayHello) => type === "Hi" ? sayHi() : sayHello();
greet("Hi", sayHi, sayHello); // Hi!
greet("Hello", sayHi, sayHello); // Hello
Tässä voisi sanoa, että funktio greet()
on korkeamman asteen funktio,
koska se ottaa funktioita parametreina. Funktioiden välittäminen parametreina tarkoittaa
myös, että funktiot ovat first-class citizens
kielessä.
[Syntaksi ? :
, jota käytettiin edellisessä esimerkissä, kutsutaan
ternary operaattoriksi.
Se on pohjimmiltaan lyhyempi versio “if-else”:stä.]
Attention
Kun annat funktioita parametreina, et kutsu funktiota ja siksi jätät pois sulkeet (ja mahdolliset argumentit) ja välität vain funktion nimen. Funktion kutsuminen taas palauttaa sen, mitä funktio palauttaa, ja siirtää sitten palautusarvon korkeamman asteen funktiolle.
const sayHi = () => console.log("Hi!");
const sayHello = () => console.log("Hello");
const greet = (type, sayHi, sayHello) => type === "Hi" ? sayHi() : sayHello();
greet("Hi", sayHi, sayHello); // välitetään funktiot
greet("Hello", sayHi(), sayHello()); // välitetään funktioiden "Hi!" ja "Hello" tulokset
Meillä voisi olla myös korkeamman asteen funktio, joka palauttaisi funktion:
const printGreetings = () => console.log("Greetings");
const getGreetingsFunction = () => printGreetings;
const greet = getGreetingsFunction();
greet(); // Greetings
Funktion yhdistäminen: Funktioiden yhdistämistä käytetään, kun halutaan yhdistää useita yksinkertaisia funktioita monimutkaisemmaksi kokonaisuudeksi. Kunkin funktion tuoma arvo välitetään seuraavalle funktiolle parametrina. Määrittelemme kaksi funktiota: ensimmäinen funktio lisää 2 parametrin arvoon, ja toinen kertoo parametrin arvon 2:lla.
const add2 = (n) => n + 2;
const times2 = (n) => n * 2;
Nyt näitä funktioita voidaan käyttää ja uudelleenkäyttää useissa yhteyksissä, ja niitä voitaisiin jopa soveltaa eri järjestyksessä tai useita kertoja, jotta saataisiin erilaisia lopputuloksia.
console.log(add2(times2(5))); // 12
console.log(times2(add2(5))); // 14
console.log(add2(times2(add2(5)))); // 16
console.log(times2(add2(times2(5)))); // 24
Mitä jos meidän pitäisi kirjoittaa jotain tällaista?
const accessRights = a(b(c(d(e(f(g("Teemu Teekkari")))))));
Puhtaat funktiot mahdollistavat sen, että funktioketjut voivat olla niin pitkiä kuin haluamme, kunhan edellisen funktion tuotos on seuraavan funktion odotettu syöte ketjussa.
Voimme käyttä myös compose()
-apufunktiota. compose()
:lla voimme kirjoittaa
yhdistelmät toisessa muodossa. (Huomaa: että tässä tapauksessa suoritusjärjestys on
vasemmalta oikealle verrattuna edellisen esimerkin oikealta vasemmalle -suoritusjärjestykseen):
const compose = (...fns) => (initialVal) => fns.reduceRight((val, fn) => fn(val), initialVal);
const accessRightsFn = compose(a, b, c, d, e, f, g);
const accessRights = accessRightsFn("Teemu Teekkari");
Vinkkejä:
Todellisessa skenaariossa et kirjoittaisi compose-funktiota itse, vaan käytät kirjastoa kuten Lodash.
Harjoitukset¶
Seuraavissa lyhyissä harjoituksissa tavoitteena on erottaa puhtaat funktiot epäpuhtaista.