Lær funktionel programmering og bliv en bedre udvikler!

I løbet af de seneste år er funktionel programmering blevet mere og mere synlig. Flere og flere af de funktionelle byggesten sniger sig ind alle steder - ikke mindst i OO-sprogene, og grunden til det, er at vi får bedre, simplere og mere letlæselig kode af det.

En lille historie-lektion

Langt de fleste programmeringssprog som er udbredte i dag bygger i store træk på den samme model som Fortran lagde ud med tilbage i 1957, men har gradvist tilføjet flere og flere koncepter fra den funktionelle verden.

Fortran var i sin originale udformning ikke meget mere end en pæn indpakning af rå maskinkode, men med tiden har Fortran udviklet sig og lånt mere og mere fra den funktionelle verden; navnligt fra Lisp.

Lisp er det første reelle funktionelle programmeringssprog. Lisp blev opdaget af John McCarthy i 1958, og var til at starte med en akademisk øvelse i at finde en matematisk repræsentation hvorved man kunne beskrive et stykke softwares opførsel. Resultatet af denne akademiske øvelse var, at al software kunne brydes ned og beskrives ved blot 7 primitive funktioner. Disse 7 funktioner fungerer som en slags naturlov for software, og derfor er der mange som siger at lisp ikke blev opfundet, men i stedet opdaget af John McCarthy.

Den Lisp som John McCarthy opdagede, var dog kun en beskrivelse, og kunne ikke bruges i praksis som et programmeringssprog. Det var først efter at Steve Russell havde læst John McCarthys udgivelse, at det gik op for Steve at man kunne implementere Lisps eval funktion i maskinkode. Denne implementation blev den allerførste Lisp interpreter, og dermed var Lisp pludselig et ægte programmeringssprog.

Funktionel indflydelse

Flere og flere steder ser man de funktionelle koncepter dukke op. Mange af de sprog som følger Fortran-modellen får gradvist flere og flere funktionelle features, eksempelvis har C# fået LINQ, og en af Javas medforfattere har endda udtalt at Java var et forsøg på at trække C++ programmørerne halvvejs over mod Lisp, version 8 af Java får også både lambda-funktioner og closures, som har sine rødder i Lisp.

Mange nyere sprog har en masse funktionelle features medfødt; Ruby har de fleste funktionelle funktioner indbygget, Python har List Comprehensions og Javascript har også Higher-Order Functions. Alle disse features stammer fra den funktionelle programmerings verden, og mange flere ting som du sikkert ikke anede, kommer også derfra, så som conditionals, funktioner som typer, rekursion, dynamiske typer, garbage collection og symbols.

Hvad er funktionel programmering?

Funktionel programmering er modstykket til imperativ og struktureret programmering som vi kender den fra C eller Java, og er en fundamentalt anderledes måde at programmere på. Hvor man i imperativ programmering vil udtrykke sin kode som en sekvens af operationer som modificerer programmets tilstand (ofte objekter), vil man i funktionel programmering i stedet arbejde på et højere abstraktionsniveau, og udtrykke sin kode som en slags matematiske transformationer.

For at forstå nærmere hvad jeg mener, vil jeg give et eksempel i javascript og clojure:

Imperativ form:

var items = [1, 2, 3, 4];
var doubledItems = [];
for(var i = 0; i < items.length; i++) {
   doubledItems.append(items[i] * 2);
}

Funktionel form:

var items = [1, 2, 3, 4];
var doubledItems = items.map(function(item) {
   return item * 2;
};

Map er en klassisk funktion fra den funktionelle verden, som tager alle elementer i en liste og udfører en operation for hvert element, og returnerer listen af resultaterne. Som det kan ses er det allerede væsentligt mindre støj i koden. Der er en del mindre tvungen syntax, og man kan nemmere få øje på forretningslogikken.

Hvis man ville skrive det samme i et rigtigt funktionelt sprog så som Clojure vil koden dog tage en meget anderledes form:

(def items [1 2 3 4])
(def doubledItems (map (partial * 2) items))

Hvis man så pludselig står og har brug for en funktion der i stedet lægger 10 til alle elementerne, bliver det igen bøvlet i den Imperative verden:

var plusTenItems = [];
for(var i = 0; i < items.length; i++) {
   plusTenItems.append(items[i] + 1);
}

Men i den funktionelle form skal man blot udskifte den indre operation:

var plusTenItems = items.map(function(item) {
   return item + 10;
};

eller hvis det var Clojure:

(def plusTenItems (map (partial + 10) items))

Hvis man så pludselig står en dag og skal bruge en kombination af de to operationer, altså at tallene først skal fordobles og derefter lægges 10 til, så vil den imperative form enten kræve at man kopierer loopet en gang mere og ændrer indholdet, eller at man pakker hvert loop ind i hver deres funktion og kalder dem i rækkefølge, hvilket kræver at man looper over listen en gang for meget.

var doublePlusTenItems = [];
for(var i = 0; i < items.length; i++) {
   doublePlusTenItems.append(items[i] * 2 + 10);
}

Hvis man derimod tænker funktionelt, går det hurtigt op for en at man er nået til et godt sted at lave en refaktorering af sin kode, så hele koden kommer til at se sådan her ud:

(def items [1 2 3 4])
(def double (partial * 2))
(def addTen (partial + 10))
(def doubledItems (map double items))
(def plusTenItems (map addTen items))
(def doublePlusTenItems (map (comp double addTen) items))

Læg særligt mærke til den nederste linje, hvor funktionen comp bliver brugt til at lave en ny funktion som først kalder double og derefter kalder addTen.

Idealer i funktionel programmering

Når man skriver funktionel kode, er der en del mønstre man næsten bliver tvunget til at følge, og herunder er nok de 2 vigtigste.

Uforanderlighed

En meget vigtig faktor i funktionel programmering er at ens datastrukturer og variable er uforanderlige (“immutable” på engelsk). Det vil sige at man ikke kan ændre i datastrukturen eller i sine variable, men i stedet laver man en ny udgave af datastrukturen som indeholder de ændrede data.

Immutability giver også mulighed for at man kan vente med at evaluere de resulterende data indtil de rent faktisk skal bruges, hvilket kan spare én en masse hukommelse, og endda gøre det muligt at arbejde med data som er uendelige, eftersom det kun er de data som rent faktisk er i brug, som bliver evalueret og derfor fylder i hukommelsen. Immutability har også den bivirkning at parallel-programmering bliver meget nemmere, eftersom race-conditions ikke kan forekomme.

Idempotens

Langt de fleste funktioner i funktionel programmering vil også opfylde et begreb som kaldes “Idempotens”. Idempotens går ud på at ens funktioner altid vil give det samme resultat, hvis man giver den de samme parametre; det tvinger en til ikke at have nogen lokal tilstand som kan påvirke funktionen.

En anden enormt vigtig ting ved idempotente funktioner er at de under ingen omstændigheder må ændre værdien af deres parametre; det eneste de må er at returnere en ny værdi som resultat. Det gør det utroligt nemt at teste sin kode og samtidig bliver det meget nemmere at finde frem til fejl, da det vil være langt nemmere at overskue, hvad der rent faktisk sker, når en funktion bliver kaldt.

Et godt eksempel på idempotente funktioner er de sædvanlige matematisk funktioner som plus og minus; uanset hvordan tilstanden af systemet er, vil to specifikke parametre altid give det samme resultat, og samtidig kan man være sikker på at de to parametre ikke bliver ændret af funktionen.

Hvordan kan funktionel programmering gøre mig bedre til at udvikle software?

Når man prøver at udforme sin kode ud fra funktionelle principper, uanset om man bruger et funktionelt sprog, bliver ens kode næsten altid en hel del simplere. Hvis man prøver at følge mønstre såsom idempotens og uforanderlighed, er der en masse grimme overraskelser, der pludselig forsvinder, og kompleksiteten af ens kode falder betydeligt.

Min personlige rejse gennem den funktionelle verden startede med, at jeg arbejdede med et projekt skrevet i C#, og jeg samtidig benyttede JetBrains’ populære ReSharper værktøj, som jævnligt fortalte mig at mine komplicerede loops kunne simplificeres til et LINQ-udtryk. Ved at kigge nærmere på hvad ReSharper rent faktisk gjorde ved min kode begyndte jeg at forstå mange af de normale funktionelle transformationer. Senere hen har jeg skrevet en del kode i Javascript, Ruby, Python og Scala som alle tilbyder en masse funktionelle features, som kan gøre ens kode langt simplere og lettere at teste.

Senest har jeg bestemt mig for at lære et rigtigt funktionelt programmeringssprog, nemlig Clojure, som er en nutidig dialekt af Lisp, som har vundet et ret stort og loyalt community, og er under aktiv udvikling.

Under hele denne rejse er det gået op for mig igen og igen, hvor meget lettere det rent faktisk er at skrive kode som jeg er sikker på virker, når jeg blot prøver at skrive min kode så funktionelt som muligt.

Bruger funktionel programmering ikke langt flere ressourcer?

Både ja og nej, hvis man rent faktisk bruger et funktionelt programmeringssprog vil hukommelsesforbruget ofte være lavere, og samtidig er CPU forbruget ikke voldsomt højere.

Hvis man benytter funktionelle mønstre i et ikke-funktionelt sprog, så kan man dog godt opleve at koden er marginalt langsommere og bruger mere hukommelse, men jeg har aldrig oplevet det som et problem, da langt det meste ikke-funktionelle kode i forvejen ikke er skrevet for at være lynende hurtig.

Hvordan kan jeg komme i gang med at lære noget funktionel programmering?

Hvis jeg har givet dig blod på tanden, så kan jeg anbefale at man rent faktisk sætter sig ned og lærer et funktionelt programmeringssprog, og ikke blot bruger de funktionelle aspekter af et imperativt sprog, da det vil tvinge en til at skrive koden i en funktionel form. Derved undgår man at lære sig selv nogle unoder.

Clojure, Scheme, Haskell, F# eller OCaml er alle gode kandidater til steder at starte med noget rigtig funktionel programmering. Det skal dog siges at man nemt kommer ud hvor man ikke kan bunde, da rigtig mange af de ting man er vant til at gøre, pludselig skal gøres på en helt anden måde. Derfor vil jeg anbefale at man finder sig en rigtig god lærebog, og så begynder helt fra bunden af.

Personligt vil jeg særligt anbefale at tage et kig på Clojure, da det er et sprog som har opnået en ret stor popularitet for nyligt med et aktivt community, og derfor er det også nemmere at finde hjælp, hvis man løber ind i problemer.

En anden fremgangsmåde til at komme igang med funktionel programmering er at finde et bibliotek til det sprog man foretrækker, som tilbyder de gængse funktionelle funktioner, så som “map”, “reduce” og “filter”, og så forsøge at udtrykke sine loops og funktioner ved hjælp af disse, og samtidig undgå at ændre i sine variable og datastrukturer undervejs. Desværre løber man ofte ind i at sproget ikke understøtter nogle af de lidt mere avancerede funktionelle features, og man ender derfor nogle gange i en blindgyde og må falde tilbage på imperativ programmering.

Held og lykke!

-Frederik Sabroe