functional-programmingAan de slag met functioneel programmeren


Opmerkingen

Functioneel programmeren is een programmeerparadigma dat berekeningen (en dus programma's) modelleert als de evaluatie van wiskundige functies. Het heeft zijn wortels in lambda-calculus, die werd ontwikkeld door Alonzo Church in zijn onderzoek naar berekenbaarheid.

Functioneel programmeren heeft enkele interessante concepten:

  • Hogere orderfuncties
  • Zuiverheid
  • Herhaling
  • Luiheid
  • Referentiële transparantie
  • Currying
  • functoren
  • monaden
  • Memoisatie & Tail-call-optimalisatie
  • Testen van functionele eenheden

Enkele voorbeelden van functionele programmeertalen zijn Lisp , Haskell , Scala en Clojure , maar ook andere talen, zoals Python , R en Javascript, laten toe om (delen van) uw programma's in een functionele stijl te schrijven. Zelfs in Java heeft functioneel programmeren zijn plaats gevonden met Lambda Expressions en de Stream API die werden geïntroduceerd in Java 8.

Currying

Currying is het proces van het transformeren van een functie waarbij meerdere argumenten worden omgezet in een reeks functies die elk slechts één parameter hebben. Currying is gerelateerd aan, maar niet hetzelfde als, gedeeltelijke toepassing.

Laten we de volgende functie in JavaScript bekijken:

var add = (x, y) => x + y
 

We kunnen de definitie van currying gebruiken om de add-functie te herschrijven:

var add = x => y => x + y
 

Deze nieuwe versie heeft een enkele parameter, x , en retourneert een functie die een enkele parameter, y , nodig heeft, die uiteindelijk het resultaat zal opleveren van het toevoegen van x en y .

var add5 = add(5)
var fifteen = add5(10) // fifteen = 15
 

Een ander voorbeeld is wanneer we de volgende functies hebben die haakjes om strings plaatsen:

var generalBracket = (prefix, str, suffix) => prefix + str + suffix
 

Nu moeten we elke keer dat we generalBracket gebruiken tussen de haakjes plaatsen:

var bracketedJim = generalBracket("{", "Jim", "}") // "{Jim}"
var doubleBracketedJim = generalBracket("{{", "Jim", "}}") // "{{Jim}}"
 

Trouwens, als we de tekenreeksen doorgeven die geen haakjes zijn, geeft onze functie nog steeds een verkeerd resultaat. Laten we dat oplossen:

var generalBracket = (prefix, suffix) => str => prefix + str + suffix
var bracket = generalBracket("{", "}")
var doubleBracket = generalBracket("{{", "}}")
 

Merk op dat zowel bracket als doubleBracket nu functies zijn die wachten op hun laatste parameter:

var bracketedJim = bracket("Jim") // "{Jim}"
var doubleBracketedJim = doubleBracket("Jim") // "{{Jim}}"
 

Hogere-orde functies

Hogere-orde functies nemen andere functies als argumenten en / of retourneren ze als resultaten. Ze vormen de bouwstenen van functioneel programmeren. De meeste functionele talen hebben bijvoorbeeld een vorm van filterfunctie. Dit is een functie van een hogere orde, waarbij een lijst en een predikaat (functie die waar of onwaar retourneert) als argumenten worden gebruikt.

Functies die geen van beide uitvoeren, worden vaak first-order functions .

function validate(number,predicate) {
    if (predicate) {    // Is Predicate defined
        return predicate(number);
    }
    return false;
}
 

Hier is "predicaat" een functie die zal testen op een voorwaarde waarbij de argumenten betrokken zijn en waar of onwaar retourneert.

Een voorbeeld van het bovenstaande is:

validate(someNumber, function(arg) {
    return arg % 10 == 0;
    }
);
 

Een veel voorkomende vereiste is om nummers binnen een bereik toe te voegen. Door functies van een hogere orde te gebruiken, kunnen we deze basismogelijkheden uitbreiden door een transformatiefunctie op elk nummer toe te passen voordat deze in de som wordt opgenomen.

U wilt alle gehele getallen binnen een bepaald bereik toevoegen (met Scala)

def sumOfInts(a: Int, b: Int): Int = {
  if(a > b) 0
  else a + sumOfInts(a+1, b)
}
 

U wilt vierkanten van alle gehele getallen binnen een bepaald bereik toevoegen

def square(a: Int): Int = a * a

def sumOfSquares(a: Int, b: Int): Int = {
  if(a > b) 0
  else square(a) + sumOfSquares(a + 1, b)
}
 

Merk op dat deze dingen één ding gemeen hebben, dat je een functie op elk argument wilt toepassen en ze vervolgens wilt toevoegen.

Laten we een hogere-orde functie maken om beide te doen:

def sumHOF(f: Int => Int, a: Int, b: Int): Int = {
  if(a > b) 0
  else f(a) + sumHOF(f, a + 1, b)
}
 

Je kunt het zo noemen:

def identity(a: Int): Int = a

def square(a: Int): Int = a * a
 

Merk op dat sumOfInts en sumOfSquare kunnen worden gedefinieerd als:

def sumOfInts(a: Int, b: Int): Int = sumHOF(identity, a, b)

def sumOfSquares(a: Int, b: Int): Int = sumHOF(square, a, b)
 

Zoals u in dit eenvoudige voorbeeld kunt zien, bieden functies van een hogere orde meer algemene oplossingen en verminderen ze codeduplicaties.

Ik heb Scala By Example - door Martin Odersky als referentie gebruikt.

Onveranderlijkheid

In traditionele objectgeoriënteerde talen is x = x + 1 een eenvoudige en juridische uitdrukking. Maar bij functionele programmering is het illegaal .

Variabelen bestaan niet in functionele programmering. Opgeslagen waarden worden vanwege de geschiedenis nog steeds variabelen genoemd. In feite zijn ze constanten . Als x eenmaal een waarde aanneemt, is het die waarde voor het leven.

Dus, als een variabele een constante is , hoe kunnen we dan de waarde ervan veranderen?

Functioneel programmeren behandelt wijzigingen in waarden in een record door een kopie van het record te maken met de gewijzigde waarden.

In plaats van bijvoorbeeld:

var numbers = [1, 2, 3];
numbers[0] += 1; // numbers = [2, 2, 3];
 

Je doet:

var numbers = [1, 2, 3];
var newNumbers = numbers.map(function(number) {
    if (numbers.indexOf(number) == 0)
        return number + 1
    return number
});
console.log(newNumbers) // prints [2, 2, 3]
 

En er zijn geen lussen in functionele programmering. We gebruiken recursie- of hogere-ordefuncties zoals map , filter en reduce om lussen te voorkomen.

Laten we een eenvoudige lus maken in JavaScript:

var acc = 0;
for (var i = 1; i <= 10; ++i)
    acc += i;
console.log(acc); // prints 55
 

We kunnen het nog steeds beter doen door de levensduur van acc veranderen van wereldwijd naar lokaal:

function sumRange(start, end, acc) {
    if (start > end)
        return acc;
    return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0)); // 55
 

Geen variabelen of loops betekenen eenvoudiger, veiliger en beter leesbare code (vooral bij het debuggen of testen - u hoeft zich geen zorgen te maken over de waarde van x na een aantal statements, deze zal nooit veranderen).

Pure functies

Pure functies staan op zichzelf en hebben geen bijwerkingen. Gegeven dezelfde set ingangen, retourneert een pure functie altijd dezelfde uitgangswaarde.

De volgende functie is puur:

function pure(data) {
    return data.total + 3;
}
 

Deze functie is echter niet puur omdat deze een externe variabele wijzigt:

function impure(data) {
    data.total += 3;
    return data.total;
}
 

Voorbeeld:

data = {
    total: 6
};

pure(data);   // outputs: 9
impure(data); // outputs: 9 (but now data.total has changed)
impure(data); // outputs: 12