Funksjonsmoro og frustrasjoner

For noen år siden ble funksjonell programmering hot igjen. Ikke for det, i mange miljøer har det alltid vært hot – på samme måte som Lisp, som har vært hot i over seksti år. Nå til dags er det veldig mange flere som utvikler i JavaScript, Scala og Java enn i Lisp. Vi gjør størstedelen av utviklingen vår i JavaScript og Java. Det er blitt gradvis mer JavaScript og gradvis mindre Java ettersom vi har gått over til JavaScript-baserte frontender. JavaScript har alltid hatt funksjonelle egenskaper. På tross av dette gikk det flere år før vi satte oss ned og pratet skikkelig gjennom hvilke gevinster funksjonelle egenskaper og teknikker kunne gi oss.

For oss som driver med produktutvikling av både eksisterende og nye produkt, er høyest mulig endringsdyktighet den viktigste egenskapen til koden vår. Vi mener koden vår er endringsdyktig når den:

  • er konsis og enkel å forstå
  • er testbar
  • har høy kohesjon
  • har lite koblinger
  • har god verktøystøtte for verifikasjon og refaktorering

Hvordan passer funksjonelle egenskaper og teknikker inn i dette? Hvordan kan funksjonelle egenskaper og teknikker hjelpe oss med å tilføre disse egenskapene til koden vår?

Konsis og enkel å forstå?

Hva er enklest å forstå av

og

Det kommer an på hvem du spør.

De fleste er enige i at den funksjonelle koden er penere. Men mange føler den er vanskeligere å forstå. Vi tror dette primært forklares med hva en er vant med. Mange er vant med imperative språk. Selv om de funksjonelle egenskapene er en del av det samme programmeringsspråket, er det i praksis et nytt delspråk. Et nytt språk med annen gramatikk og setningsoppbygning er alltid vanskeligere enn språket en kan fra før – inntil en har lært det.

De funksjonelle konseptene er mer abstrakte. «If» er ord med lavere kognitiv overhead enn «Filter». De krever litt mer innsats å bli kjent med og forstå.

En pipeline ala Filter-Map-Filter-Reduce dropper mellomstegene i beregningen av resultatet. Disse må en holde i hodet for å forstå hvordan resultatet blir beregnet. I en imperativ variant av den samme beregningen, vil mellomstegene gjerne være eksplisitt representert som variabler med beskrivende navn i koden. Dette kan gjøre det enklere å forstå – inntil en har fått de funksjonelle språkkonseptene under huden. – Kanskje en pipeline som består av mange steg kan bli enklere å forstå ved å dele den opp, og gi mellomvariablene beskrivende navn for de aktuelle datastrukturene?

På en annen side abstraherer en pipeline bort kode som ikke er viktig for intensjonen til koden. Det skal ikke mange nivå med for looper og if-er til, før en ikke ser skogen for bare trær i vanlig imperativ kode.

Et annet aspekt som er kan være fremmedgjørende i starten, er bruk av lambdauttrykk direkte i pipelinen. Enkle / trivielle lambdauttrykk vil kunne forsvares å ha i pipelinen. Er de kompliserte, økes lesbarheten betraktelig av å trekke dem ut i egne metoder. Å tenke at kode må leve i én og samme funksjon for å være funksjonell, er en vanlig feiloppfatning. Extract method er god refaktorering også når en koder funksjonelt.

Vår erfaring er at når en først lærer seg de funksjonelle mekanismene, så vil problemer som egner seg for løsning via pipelines

  • være kodemessig minst like enkle å forstå som den imperative varianten
  • gi mindre sannsynlighet for feil, da operasjonene i pipelinen gjerne har høyere kohesjon
  • gi mer konsis kode

Testbar?

Funksjonell kode fokuserer på funksjoner. Objektorientert kode fokuserer på klasser. Det faller minst like naturlig, om ikke mer, å lage små funksjoner når en koder funksjonelt. Små funksjoner er enklere å teste enn store funksjoner.

Med dependency injection også av funksjonalitet, er det enklere å skrive tester uten bruk av mock-rammeverk.

Vi forsøker unngå sideeffekter. Vi streber etter rene funksjoner. Det betyr at vi i størst mulig grad vil ha funksjoner som returnerer et resultat basert på inputen. Rene funksjoner er enklere å teste enn funksjoner som baserer seg på delvis global state og som har heslige sideeffekter.

Høy kohesjon?

I og med at det sentrale konseptet er funksjoner og ikke klasser, får vi høyere kohesjon på kjøpet ved bruk av funksjonelle konsepter.

Færre koblinger?

Rene funksjoner har ingen sideeffekter. Det betyr at det eneste de gjør er å returnere et resultat basert på inputen de har fått. Samtidig blir det en temmelig trist applikasjon hvis en ikke utfører sideeffekter som å kalle en REST-tjeneste eller oppdatere et UI i ny og ne. Med et funksjonelt tankesett fokuserer vi på å isolere og tydeliggjøre sideeffekter.

Verktøystøtte for verifikasjon og refaktorering?

Vi benytter verktøy og teknikker som gir oss størst mulig grad av automatisk verifikasjon under utviklingen. For JavaScript får vi hjelp av ESLint, og for Java-utviklingen vår får vi hjelp av IntelliJ, også for de funksjonelle språkkonseptene.

Veien videre

Vi har fokusert på å utnytte de gode funksjonelle egenskapene de siste par årene. Den største gevinsten har vi fått i JavaScript kodebasen vår med innføringen av React og Redux. Redux sine kodemønstre med godt skille mellom state, UI og sideeffekter har økt vedlikeholdbarheten på JavaScript-koden vesentlig.

I Java-koden har vi valgt å fokusere på enkle grep. Dette for å kunne utnytte funksjonelle egenskaper også ved mindre kodeendringer og feilrettinger, og ikke bare ved nyskriving av kode:

  • alltid lage immutable objekter
  • bruke static metoder der vi kan
  • slutte med void metoder
  • iterere og operere over collections med streams
  • bruke Optional

Vi har også startet jobbe med Kotlin, siden en del av de funksjonelle konseptene er bedre støttet, og det med adskillig mindre sermoni og syntaktisk overhead enn i Java.

Å skrive mer funksjonell kode er gøy, men kan være frustrerende i starten. Frustrasjonen går over med trening. Den funksjonelle koden som kommer ut vil være mer vedlikeholdbar og med færre feil enn den imperative varianten, men det krever at en fortsatt følger klassiske programmeringsprinsipper som god navngivning og refaktorering.