fn: Lazy loading, currying, etc.

Door Wouter J, 6 jaar geleden, 9.056x bekeken

Met deze kleine library heb ik wat van Haskell's (en functioneel programmeren) beste features naar PHP willen halen.

Currying
De eerste feature is currying. Dat betekend dat je maar een gedeelte van een functie invult en dat je dan een functie terugkrijgt waarbij de overige argumenten nog kunnen worden ingevuld.

Dat klinkt heel moeilijk, dus laat ik maar snel een voorbeeldje geven:

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
<?php
use Wj\fn as f;

$f = f\curry(' ', 'implode');

echo $f(array('hello', 'world'));
//> 'hello world'
?>

Wat we hier doen is een functie maken die van implode het eerste argument op ' ' instelt. We hebben dan dus eigenlijk implode(' ', ???). Vervolgens roepen we die functie aan met een array die op de plek van die vraagtekens komt, we krijgen dan dus implode(' ', array(...)) en dat wordt vervolgens uitgevoerd.

We kunnen dit ook met operators gebruiken, dan moeten we ze alleen wel even omzetten in een functie met de operator functie:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
<?php
use Wj\fn as f;

$f = f\curry(f\operator('*'), 2);

echo $f(10); //> 20
?>

Hier maken we dus een functie ??? * 2 die we vervolgens aanroepen met 10, wat resulteert in 10 * 2 = 20.

Waarom is dit handig? zul je je afvragen. Nou, neem eens een simpele array filter actie:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
<?php
// zonder currying
$newArray = array_map(function ($a) {
    return $a < 10;
},
$oldArray);

// met currying
use Wj\fn as f;

$newArray = array_map(f\curry(f\operator('<'), 10), $oldArray);
?>


Merk op dat de volgende van de argumenten uitmaakt: curry(callable, argument) maakt een functie waarbij argument 1 onbekend is. En curry(argument, callable) maakt een functie waarbij argument 2 onbekend is.

Lazy loading
De library heeft ook heel veel functies die niks anders doen dan iterator over iterator heen plakken. Hierdoor krijg je een lazy loading effect, de volgende waarde wordt pas opgehaald wanneer daar om gevraagd wordt.
Begin je bijvoorbeeld eerst met 200 items en stop je in de loop al naar 10 items, dan worden er maar 10 items opgevraagd ipv 200, als je een array gebruikt is dit niet het geval.

Er zijn 2 manieren om lazy loading te beginnen: een range van getallen opstellen met range of een eigen lazy iterator initialiseren.

We bespreken hier alleen de eerste methode. De range functie heeft een begin en eind en zal daartussen lopen doormiddel van de lazy SuccessiveIterator:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
<?php
use Wj\fn as f;

foreach (f\range(1, 10) as $num) {
    echo $num, ', ';
}

//> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
?>


Door 1 stap voor te doen kun je de stapgrootte aangeven:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
use Wj\fn as f;

foreach (f\range(1, 3, 10) as $num) {
    echo $num, ', ';
}

//> 1, 3, 5, 7, 9

foreach (f\range(1, -1, -10) as $num) {
    echo $num, ', ';
}

//> 1, -1, -3, -5, -7, -9
?>

Merk op dat je voor een negatieve stapgrootte altijd de stap moet geven: range(1, 0, -10).

Je kan ook beginnen met een array en die omzetten naar een iterator met to_iterator. Merk op dat het dan niet meer lazy loaded is, aangezien arrays dat niet zijn in PHP.


Deze iterator kunnen we vervolgens in elke andere iterator stoppen. De library komt hiervoor met 4 functies: (callbable is een functie (kan currying zijn) en traversable is de iterator)

map(callable, traversable)
Deze zal de callable voor elke waarde van de iterator aanroepen (wanneer hier om gevraagd wordt uiteraard):
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php
use Wj\fn as f;

var_dump(f\map(
    f\curry(f\operator('*'), 2),
    range(1, 5)
));

//> Array(2, 4, 6, 8, 10)
?>


reduce(callable, traversable, accumulator = null)
Deze functie zal de iterator terugbrengen tot 1 waarde. De callable returnd de waarde voor de volgende functie, de accumulator is de start waarde.
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
use Wj\fn as f;

echo f\reduce(
    f\curry(f\operator('+')),
    f\range(1, 5)
);

//> 30

echo f\reduce(
    function (
$acc, $value) {
        return $acc.', '.$value;
    },

    f\to_iterator(array('wouter', 'nienke', 'oscar'))
);

//> 'wouter, nienke, oscar'
?>


filter(callable, traversable)
Deze plaatst de iterator in een CallbackFilterIterator:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php
use Wj\fn as f;

var_dump(f\filter(
    f\curry(f\operator('<'), 10),
    f\to_iterator(array(1, 30, 2, 40, 23, 10, 44))
));

//> Array(1, 2, 10)
?>


takeWhile(callable, traversable)
Deze pakt alle elementen totdat de callable false returned:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php
use Wj\fn as f;

var_dump(f\takeWhile(
    f\curry(f\operator('<'), 10),
    f\to_iterator(1, 2, 3, 4, 20, 5, 6, 7, 8, 9)
));

//> Array(1, 2, 3, 4)
?>


Lazy loading in de praktijk
Wanneer gebruiken we lazy loading nou in de praktijk? Het mooie is dat we nu zonder zorgen Infinity (INF in php) kunnen gebruiken, als we maar een takeWhile erin stoppen of als we maar de loop zelf een keer stoppen.

Een vraagstuk als: Hoeveel kwadraten onder de 200 zijn er? Kunnen we op deze manier simpel oplossen.
Eerst maken we een range van 1 tot oneindig:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
<?php
use Wj\fn as f;

f\range(1, INF);
?>

Vervolgens nemen we van de opgevraagde elementen uit deze lijst de kwadraten:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
<?php
use Wj\fn as f;

f\map(
    f\curry(f\operator('^'), 2),
    f\range(1, INF)
);

?>

En daarna pakken we ze totdat we de 200 overschrijden:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
<?php
use Wj\fn as f;

f\takeWhile(
    f\curry(f\operator('<='), 200),
    f\map(
        f\curry(f\operator('^'), 2),
        f\range(1, INF)
    )
);

?>

En als laatst converteren we dit tot een array en tellen we het aantal elementen:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
use Wj\fn as f;

echo count(f\to_array(
    f\takeWhile(
        f\curry(f\operator('<='), 200),
        f\map(
            f\curry(f\operator('^'), 2),
            f\range(1, INF))
        )
    )
);

?>

Het antwoord is 14!

Met dank aan
Deze library is geïnspireerd door Haskell en nikic/iter (die dit op een PHP 5.5+ manier aanpakt).

Voor de geïnteresseerden, in haskell zou het bovenstaande voorbeeld er zo uitzien:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
length $ takeWhile (<= 200) $ map (^2) [1..]

Gesponsorde koppelingen

PHP script bestanden

  1. fn.php

 

Er zijn 5 reacties op 'Fn lazy loading currying etc'

PHP hulp
PHP hulp
0 seconden vanaf nu
 

Gesponsorde koppelingen
Wouter J
Wouter J
6 jaar geleden
 
0 +1 -0 -1
De curry functie is nu bewerkt, zodat hij ook met meer dan 2 argumenten om kan gaan. Momenteel is het alleen nog ondersteund om de gegeven argumenten aan het begint of het eind te plaatsen.
Pim -
Pim -
5 jaar geleden
 
1 +1 -0 -1
Leuk gedaan! Mooi gebruik gemaakt van de SPL iterators. Misschien goed om even aan te geven dat je minimaal PHP 5.4 nodig hebt. f\range() is alleen vrij lelijk. Ook is het f\operator niet zo mooi/veilig, ik zou daar gewoon een lange switch van maken.
Wouter J
Wouter J
5 jaar geleden
 
0 +1 -0 -1
Bedankt Pim! (en leuk dat je hier weer eens bent trouwens!)

Ik had f\operator eerst inderdaad met een lange switch, maar toen had ik het naar dit vervangen, zodat de code forewards compatible werd. Maar het is inderdaad wel onveilig nu, ik ga er wat op verzinnen :)

En je punt van f\range() begrijp ik niet helemaal. Wat is er precies lelijk aan? (maw, weet jij een betere methode?)
Pim -
Pim -
5 jaar geleden
 
0 +1 -0 -1
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
<?php
function range($start, $end)

// ->
function range($start, $end, $step = 1)
?>

Gewoon zo toch?
Dan heb je die hele switch niet nodig.
PHP hulp
PHP hulp
0 seconden vanaf nu
 

Gesponsorde koppelingen
Wouter J
Wouter J
5 jaar geleden
 
0 +1 -0 -1
hmm, ja dat kan ook. Maar ik vind die haskell methode wel mooi, vandaar dat ik die gewoon naar PHP code heb omgezet:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
[1..5] => [1, 2, 3, 4, 5]
[1,1.1..1.5] => [1.1, 1.2, 1.3, 1.4, 1.5]

Om te reageren heb je een account nodig en je moet ingelogd zijn.

 
 

Om de gebruiksvriendelijkheid van onze website en diensten te optimaliseren maken wij gebruik van cookies. Deze cookies gebruiken wij voor functionaliteiten, analytische gegevens en marketing doeleinden. U vindt meer informatie in onze privacy statement.