Naar inhoud springen

Fold

Uit Wikipedia, de vrije encyclopedie

In functionele programmeertalen is een fold (of reduce) een hogere-ordefunctie waarmee een recursieve datastructuur geanalyseerd kan worden. In veel gevallen wordt een lijst in een bepaalde volgorde doorlopen en wordt gaandeweg een bepaalde waarde berekend. Een fold maakt gebruik van twee zaken: een functie die elementen van de datastructuur combineert en de datastructuur zelf. Een voorbeeld:

fold (+) [1,2,3,4,5]

wat resulteert in 1 + 2 + 3 + 4 + 5 = 15. In dit voorbeeld is '+' een associatieve operatie waardoor het irrelevant is hoe de haakjes geplaatst worden. Als benadering van de werking van fold zou men de komma's in de lijst kunnen vervangen door de meegegeven functie.

foldr en foldl

[bewerken | brontekst bewerken]

Doorgaans is de functie niet associatief waardoor de volgorde waarin men de berekening uitvoert er wel toe doet. In lijsten zijn er twee manieren waarop dit kan gebeuren: men begint aan de rechterkant met het plaatsen van de haakjes en voert recursief de berekening uit (een right fold) of men begint aan de linkerkant (een left fold). Vaak gebruikt men ook een initiële waarde om de berekening uit te voeren. Dit kan, afhankelijk van wat men wil berekenen, een neutraal element zijn maar ook een andere waarde. Een voorbeeld van een right fold in Haskell (dit gebeurt met de functie foldr):

foldr (+) 0 [1,2,3,4]

Deze berekening is gelijk aan (1 + (2 + (3 + (4 + 0)))) = 10. Een voorbeeld van een left fold:

foldl (-) 100 [1,2,3,4]

Dit is gelijk aan ((((100 - 1) - 2) - 3) - 4) = 90.

De functies foldl en foldr kunnen in Haskell als volgt gedefinieerd worden (in Haskell is [] een lege lijst en (x:xs) een lijst met x als eerste element en xs de rest van de lijst):

foldr f z []     = z
     -- als de lijst leeg is dan is de opgeleverde waarde de beginwaarde
foldr f z (x:xs) = f x (foldr f z xs)
     -- anders, pas f toe op het eerste element en het resultaat van foldr op de rest
foldl f z []     = z
     -- als de lijst leeg is dan is de opgeleverde waarde de beginwaarde
foldl f z (x:xs) = foldl f (f z x) xs
     -- anders, bereken een nieuwe beginwaarde door f toe te passen op de beginwaarde en
     -- het eerste element en pas foldl met deze waarde toe op de rest van de lijst

Eén belangrijk punt is dat foldr bij luie evaluatie of normal-order evaluatie direct f toepast op de recursieve aanroep van foldr op de rest van de lijst. Als f een deel van het resultaat kan produceren zonder de recursieve aanroep te gebruiken en als de rest van het resultaat nooit nodig is dan wordt de recursieve aanroep niet uitgevoerd. Op deze manier is foldr in staat om met oneindige lijsten om te gaan. foldl roept zichzelf echter aan met nieuwe parameters totdat het einde van de lijst is bereikt. Deze staartrecursie kan efficiënt gecompileerd worden als een lus maar het kan niet omgaan met oneindige lijsten (dit zal resulteren in een oneindige lus).

Een ander technisch punt is dat de nieuwe initiële waarde bij foldl in zulke programmeertalen niet wordt uitgerekend voordat de recursieve aanroep is uitgevoerd. Dit kan leiden tot stack overflows wanneer het einde van de lijst is bereikt en het een gigantische expressie moet uitrekenen. Om deze reden bezitten zulke programmeertalen ook een strikte versie van foldl die eerst de nieuwe initiële waarde uitrekent voordat de recursieve aanroep kan plaatsvinden. In Haskell is dit foldl' (met apostrof, te vinden in de Data.List bibliotheek). Gecombineerd met de snelheid van staartrecursie zijn zulke 'folds' zeer efficiënt wanneer een luie manier van uitrekenen onmogelijk of ongewenst is.

In Scheme kunnen foldr en foldl geschreven worden als:

(define (foldr f z xs)
  (if (null? xs)
      z
      (f (car xs) (foldr f z (cdr xs)))))
(define (foldl f z xs)
  (if (null? xs)
      z
      (foldl f (f z (car xs)) (cdr xs))))

De C++ Standard Template Library bibliotheek implementeert fold als "accumulate" (in de header <numeric>).

foldl1 en foldr1

[bewerken | brontekst bewerken]

In bepaalde situaties wil men foldl of foldr maar is er geen geschikte initiële waarde te bedenken, bijvoorbeeld bij het berekenen van de maximale waarde in de lijst met behulp van een functie die de grootste van z'n twee parameters teruggeeft. Er zijn hiervoor varianten van foldl en foldr die geen initiële waarde gebruiken maar het eerste of laatste element van de lijst als startwaarde gebruiken. In Haskell en in een aantal andere talen heten deze foldl1 en foldr1 waarbij de 1 verwijst naar het feit dat het eerste element (vanaf links of vanaf rechts) als startwaarde wordt gebruikt en ook dat de lijsten waarop ze worden toegepast minimaal één element moeten bevatten.

De functie maximum die het grootste element van een lijst oplevert, zal op deze manier een foutmelding geven wanneer deze wordt toegepast op een lege lijst:

maximum = foldl1 max

Standaardfuncties

[bewerken | brontekst bewerken]

Veel standaardfuncties in functionele programmeertalen kunnen worden geschreven met behulp van een foldfunctie:

and = foldr (&&) True (plaatst logische conjunctie tussen de elementen in een lijst booleaanse waarden)
or = foldr (||) False (plaatst logische disjunctie tussen de elementen in een lijst booleaanse waarden)
sum = foldr (+) 0 (optellen, telt elementen van een lijst bij elkaar op)
product = foldr (*) 1 (vermenigvuldigen, vermenigvuldigt de elementen van een lijst met elkaar)
concat = foldr (++) [] (concatenatie, namelijk: concat [[1], [2], [3]] = [1,2,3])
(++) xs ys = foldr (:) ys xs (concatenatie, namelijk: [1,2] ++ [3,4] = [1,2,3,4])
maximum = foldl1 max (levert het grootste element in de lijst)
minimum = foldl1 min (levert het kleinste element in de lijst)
map f = foldr ((:).f) [] (de functie map; zie ook hieronder)
reverse = foldr (\a b -> b ++ [a]) [] (geeft de lijst met de elementen in omgekeerde volgorde)

Een andere benadering

[bewerken | brontekst bewerken]

Foldfuncties kunnen worden gezien als een manier om de structurele componenten van een datastructuur te vervangen door functies en waarden. In veel talen is een lijst of een lege lijst (in het Engels nil genoemd) of een lijst die is opgebouwd door een element toe te voegen aan een bestaande lijst.In Haskell wordt het opbouwen van een lijst gedaan met de (:) functie, deze wordt ook wel cons of 'constructor' genoemd omdat er een lijst mee geconstrueerd wordt. In Scheme en Lisp heet deze operator cons. Een voorbeeld hiervan is de lijst [1,2,3,4,5], dit is eigenlijk een makkelijkere notatie voor 1:2:3:4:5:[]. Een lijst wordt opgebouwd door eerst het element 5 toe te voegen aan de lege lijst ([5]), vervolgens wordt 4 toegevoegd aan die lijst ([4,5]), enzovoort.

Men kan een right fold beschouwen als het vervangen van de lege lijst door een specifieke waarde (de initiële waarde die wordt meegegeven aan de foldfunctie) en elke (:) door de meegegeven functie:

De werking van foldl is iets minder natuurlijk maar het heeft wel een regelmatige structuur:

De werking van foldl1 en foldr1 is op deze manier ook te visualiseren:

Naast het feit dat deze afbeeldingen de right en left werking van respectievelijk foldr en foldl weergeven is ook te zien dat foldr (:) [] een identiteitsfunctie voor een lijst is: het uitvoeren van foldr (:) [] l levert dezelfde lijst op voor elke lijst l.

Het foldl-diagram suggereert een manier om de elementen van een lijst in omgekeerde volgorde op te leveren: foldl (flip (:)) []. De functie flip draait de volgorde van de parameters om. Dit is nodig aangezien het element dat toegevoegd moet worden aan de lijst nu de rechterparameter is van de (:) functie.

Op deze manier is ook te zien dat de hogere-ordefunctie map geschreven kan worden met behulp van foldr:

map f = foldr ((:).f) []

waarbij (.) (een punt) de operator voor functie-compositie voorstelt.

Deze aanpak maakt het mogelijk om ook foldfuncties te ontwerpen voor algebraïsche datatypes, zoals bomen. Men schrijft een functie die recursief de constructoren van de datastructuur vervangt door functies en de constante waarden door meegegeven waarden. Dit principe wordt catamorfisme genoemd.