Book-cover

INTRODUKTION TILL FUNKTIONELL PROGRAMMERING

I denna artikel kommer jag gå igenom grundläggande koncept och tekniker man använder i funktionella programmeringsparadigmen. Dessa koncept och tekniker kommer göra din kod mer robust och enklare att resonera om. Även om artikeln är riktad till utvecklare som jobbar med JavaScript kan principerna tillämpas i andra språk också.

Omuterbar data

I funktionell programmering muterar man inte data. Skälet är att muterbar data är en stor källa till buggar och att det leder till ökad kognitiv belastning då man måste komma ihåg eller leta reda på var värden muteras. Ett exempel på detta:


let myVariable = 'lorem'
let mySecondVariable = [{ myKey: 'lorem ipsum' }]

// senare:

myVariable = 1 // vi muterar myVariable

let myThirdVariable = mySecondVariable.pop() // pop raderar sista elementet från listan och returnerar det

// senare:

myVariable.concat(' ipsum') // Uncaught TypeError: myVariable.concat is not a function
mySecondVariable[0].myKey // Uncaught TypeError: Cannot read property 'myKey' of undefined

Med omuterbar data behöver vi inte tänka på värdenas historia.


const last = xs => xs[xs.length - 1]

const myVariable = 'lorem'
const mySecondVariable = [{ myKey: 'lorem ipsum' }]

// senare:

const myThirdVariable = 1 // istället för att mutera myVariable skapar vi en ny variabel
const myFourthVariable = last(mySecondVariable) // vi använder en icke-destruktiv funktion för att ta sista elementet

// senare:

myVariable.concat(' ipsum') // 'lorem ipsum'

last(mySecondVariable).myKey // 'lorem ipsum'

Rena funktioner

En funktion som inte interagerar med omvärlden på något sätt och alltid returnerar samma värde för argumenten den kallas med är en ren funktion. Skälet till varför man använder rena funktioner är densamma som med omuterbar data — att minska kognitiva belastningen och eliminera buggar.

Ett exempel med en oren funktion:


let myVariable = 1
let myFunction = () => myVariable = 'lorem ipsum' // funktionen muterar en icke-lokal variabel

// senare:

myFunction()

// senare:

myVariable * 2 // NaN

Ett till exempel:


let myVariable = 1
let doubleMyVariable = () => myVariable * 2 // funktionen är beroende av en icke-lokal variabel

// senare:

myVariable = 'lorem ipsum'

// senare:

doubleMyVariable() // NaN

Exempel på en ren funktion:


const double = x => x * 2

Funktionens enda uppgift är att beräkna ett värde baserad på argumenten och returnera det. Med rena funktioner kan du vara säker på att ett anrop inte kommer resultera i oönskade sidoeffekter och att returvärdet alltid kommer vara det samma oavsett hur många gånger eller när du kallar på funktionen.

Deklarativt vs imperativt

Funktionell programmering är en deklarativ paradigm till skillnad från en imperativ. I imperativa paradigm ger man datorn specifika steg för steg instruktioner medan man i deklarativa paradigm använder sig av abstraktioner för att beskriva resultatet man är ute efter.

Exempel på imperativ kod:


let myList = [1, 2, 3]
let myIncrementedList = []

for (let i = 0; i < 3; i += 1) {
 myIncrementedList.push(myList[i] + 1)
}

Deklarativ alternativ:


const increment = x => x + 1

const myList = [1, 2, 3]
const myIncrementedList = myList.map(increment)

Imperativa koden har mycket lägre signal-brusförhållande, dessutom finns det mer utrymme för buggar att krypa in, speciellt i konditionen. Vad vi vill göra är att skapa en ny lista som innehåller värdena från myList inkrementerade, och det är exakt vad vi beskriver med deklarativa koden — vi mappar myList värdena med increment funktionen.

Ofullständiga funktioner och partiell tillämpning

En ofullständig(Curried) funktion kan ta sina argument en i taget. När vi kallar på en ofullständig funktion med färre argument än vad funktionen kräver får vi tillbaka en funktion som tar resten av argumenten.

Denna teknik att kalla på ofullständiga funktioner med bara en del av argumenten de tar kallas för partiell tillämpning. Med partiell tillämpning kan man återanvända befintliga funktioner för att skapa nya funktioner.


const add = x => y => x + y
const multiply = x => y => x * y
const divide = x => y => y / x
const increment = add(1) // y => 1 + y
const double = multiply(2) // y => 2 * y
const half = divide(2) // y => y / 2

increment(1) // 2
double(1) // 2
half(4) // 2

Funktionskomposition

Funktionskomposition är en teknik där du skapar funktioner som är en kombination av andra funktioner.


const doubleAndIncrement = x => increment(double(x))

doubleAndIncrement(1) // 3

När man komponerar mer än två funktioner börjar koden bli svårläst.


const halfDoubleIncrement = x => increment(double(half(x)))

halfDoubleIncrement(1) // 2

De flesta funktionella bibliotek har funktioner som underlättar funktionskomposition. Ramda är ett av biblioteken. Ramdas compose fungerar på följande sätt:


compose(increment, double, half)(1) // 2

Ett exempel för att visa funktionskomposition med partiellt tillämpade funktioner:


const divide = x => y => y / x
const multiply = x => y => y * x
const subtract = x => y => y - x
const add = x => y => y + x

compose(
 add(2), // y => y + 2
 subtract(2), // y => y - 2
 multiply(2), // y => y * 2
 divide(2) // y => y / 2
)(1) // 1

Med compose kan man enkelt se flödet samt ändra ordningen på funktionerna.

Sammanfattning

Om funktionell programmering är något du vill testa skulle jag rekommendera att du börjar med att behandla data som omuterbar och att du undviker orena funktioner samt byter ut loopar mot JavaScripts array funktioner — map, filter, reduce, some, every, find o.s.v. Undvik push, pop, splice och andra funktioner som muterar data. Med dessa ändringar får du mycket av fördelarna med funktionell programmering.

När du känner dig bekväm med array funktionerna och vill ta abstraktionen till en högre nivå kan du kolla på bibliotek som t.ex. Ramda eller Sanctuary. Efter det kan du börja kolla på monader som är användbara abstraktioner för att hantera t.ex. icke-existerande värden eller fel i kod.

JAN LONDÉN webbutvecklare || frontend guru