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ä:

… code-block:: plaintext

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 (lähde). 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 toimintoja, 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.

Tässä selkeytetty ja täydennetty versio tekstistäsi:

### 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.

### Toiminnallisen ohjelmoinnin tärkeimmät ominaisuudet

#### 1. 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-objekti, joka sisältää tiedon sen nykyisestä sijainnista. Jos haluat muuttaa tuolin sijaintia, et muuta olemassa olevan objektin arvoa. Sen sijaan luot uuden objektin, jossa on päivitetty sijainti.

…code-block:: javascript

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.

#### 2. 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ä:


Puhdas funktio (pure function):

  • ei aiheuta sivuvaikutuksia, kuten console.log(), API-kutsuja tai yleisiä datamuutoksia,

  • palauttaa aina saman lähdön samalle tulolle

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-sivusto ei voi rakentaa pelkästään puhtailla funktiolla. Jos käyttäisit vain puhtaita toimintoja et voisi:

manipuloida DOM:ta,

noutaa tietoja palvelimelta tai

lähettää tietoja palvelimelle.

Siten molemmilla toimintotyypeillä on tärkeä rooli verkkokehityksessä. Silti erossa toiminnallisuus puhtaaseen “liiketoimintalogiikkaan” ja epäpuhtaisiin toimintoihin (tietojen nouto, DOM-manipulaatio jne.) saattaa olla hyvä idea. Näin voit varmistaa, että sinun Liiketoimintalogiikka on helposti testattavissa ja uudelleen laskettavissa. Liikelogiikka sekoitettuna sivuvaikutukset tekevät sovellusten muokkaamisesta paljon vaativampaa.

Vinkkejä:

  • Kun toiminto 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 toiminnoilleen


Korkeamman asteen funktiot: JavaScriptin Functions ovat objekteja. Tämä tarkoittaa käytännössä sitä, että voit määrittää toiminnon muuttuja, 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

In the example above, we pass a string as a parameter to the function greet. Instead of a string, we could also pass a function as a parameter.

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. Toimintojen välittäminen parametrien mukaan myös, että funktiot ovat ensiluokan kansalaisia kielessä.

[Syntaksi ? Edellisessä esimerkissä käytettyä : kutsutaan kolmiooperaattori. Se on pohjimmiltaan lyhyempi versio sanasta “jos-else”.]

Attention

Kun annat funktioita parametreina, et kutsu funktiota ja siksi jätät pois sulkeet (ja mahdolliset argumentit) ja ohitat vain funktion nimi. Funktion kutsuminen todella palauttaa sen, mitä funktio on 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); // pass in functions
greet("Hello", sayHi(), sayHello()); // pass in results of the functions "Hi!" and "Hello"

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 koostumus: Toimintojen koostumusta käytetään useiden yksinkertaisten yhdistämiseen toimii monimutkaisemmaksi. Kunkin funktion tulos välitetään muodossa parametri seuraavaan. Määrittelemme kaksi funktiota: ensimmäinen funktio lisää 2 parametriin, ja toinen kertoo sillä 2.

const add2        = (n) => n + 2;
const times2      = (n) => n * 2;

Nyt näitä toimintoja voidaan käyttää ja käyttää uudelleen useissa paikoissa ja voisi jopa Käytä toimintoja eri järjestyksessä tai useita kertoja tuottaaksesi erilaisia lopputulokset.

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ä toimintokoostumukset ovat niin pitkiä kuin haluamme että edellisen funktion lähtö on seuraavan funktion odotettu tulo ketjussa.

Meillä voi olla myös aputoiminto compose(). compose():lla voimme kirjoittaa sävellyksiä toisessa muodossa. (Huomaa: että tässä tapauksessa arviointijärjestys on vasemmalta oikealle verrattuna edellisen esimerkin oikealta vasemmalle):

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 kirjoitustoimintoa itse, vaan käytät kirjastoa kuten Lodash.

Harjoitukset

Seuraavissa lyhyissä harjoituksissa tavoitteena on erottaa puhtaat funktiot epäpuhtaista.

Tutki seuraavaa koodiesimerkkiä:

let numbers = [1, 2, 3, 4];

function addFive(numberArray) {
  console.log(numberArray);
  numberArray.push(5);
  console.log(numberArray);
}

addFive(numbers);

Mitä epäpuhtaiden funktioiden ominaisuuksia voit tunnistaa?

Tutki seuraavaa koodiesimerkkiä:

function printEmployees() {

  const url = "http://dummy.restapiexample.com/api/v1/employees";

  fetch(url)
    .then((response) => {
      return response.json();
    })
    .then((myJson) => {
      console.log(myJson.data);
    });
}

Mitä ominaisuuksia epäpuhtaalla funktiolla on?

Harkitse seuraavaa koodiesimerkkiä:

const numbers = [1, 2, 3, 4];

const addFive = (numbers) => [...numbers, 5]

addFive(numbers);

Mitä ominaisuuksia epäpuhtaalla toiminnalla on?

Tutki seuraavaa koodiesimerkkiä:

const render = (elem, text) => document.getElementById(elem).textContent = text;

render("header", "Hello World!");

Mitä ominaisuuksia epäpuhtaalla toiminnalla on?

Harkitse seuraavaa koodiesimerkkiä:

const numbers = [1, 2, 3, 4];

function addDate(numberArray) {
  const date = new Date(Date.now());
  return [...numberArray, date.getDate()];
};

const modified = addDate(numbers);

Mitä ominaisuuksia epäpuhtaalla toiminnalla on?