fn: Lazy loading, currying, etc.
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: 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: 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: 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 : Door 1 stap voor te doen kun je de stapgrootte aangeven: 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): 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. filter(callable, traversable) Deze plaatst de iterator in een CallbackFilterIterator : takeWhile(callable, traversable) Deze pakt alle elementen totdat de callable false returned: 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: Vervolgens nemen we van de opgevraagde elementen uit deze lijst de kwadraten: En daarna pakken we ze totdat we de 200 overschrijden: En als laatst converteren we dit tot een array en tellen we het aantal elementen: 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:
<?php
namespace Wj\fn;
function operator($operator)
{
if ('^' === $operator) {
return function ($a, $b) { return pow($a, $b); };
}
return eval('return function ($a, $b) { return $a '.$operator.' $b; };');
}
function curry()
{
$boundArgs = func_get_args();
if (is_callable(current($boundArgs))) {
// left curry
$func = current($boundArgs);
array_shift($boundArgs);
return function () use ($func, $boundArgs) {
$callArgs = func_get_args();
return call_user_func_array($func, array_merge($callArgs, $boundArgs));
};
}
if (is_callable(end($boundArgs))) {
// right curry
$func = end($boundArgs);
array_pop($boundArgs);
return function () use ($func, $boundArgs) {
$callArgs = func_get_args();
return call_user_func_array($func, array_merge($boundArgs, $callArgs));
};
}
}
class SuccessiveIterator implements \Iterator
{
private $acc;
private $initialValue;
private $step;
private $end;
public function __construct($initialValue = 0, $step = 1, $end = false)
{
$this->initialValue = $initialValue;
$this->step = $step;
$this->end = $end;
}
public function current()
{
return $this->acc;
}
public function next()
{
$this->acc += $this->step;
}
public function prev()
{
$this->acc -= $this->step;
}
public function valid()
{
if (false !== $this->end) {
return ($this->step > 0 ? $this->acc <= $this->end : $this->acc >= $this->end);
}
return true;
}
public function rewind()
{
$this->acc = $this->initialValue;
}
public function key()
{
return null;
}
}
abstract class CallableIterator extends \IteratorIterator
{
protected $callback;
public function __construct(\Traversable $iterator, $callback)
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException('CallbackIterator::__construct() expects second argument to be callable.');
}
$this->callback = $callback;
parent::__construct($iterator);
}
}
class CallbackIterator extends CallableIterator
{
public function current()
{
$f = $this->callback;
return $f(parent::current());
}
}
class TakeIterator extends CallableIterator
{
public function valid()
{
if (!parent::valid()) {
return false;
}
$f = $this->callback;
return $f($this->current());
}
}
function range($start, $end)
{
switch (func_num_args()) {
case 2:
return new SuccessiveIterator($start, 1, $end);
case 3:
$args = func_get_args();
return new SuccessiveIterator($start, $end - $start, $args[2]);
default:
throw new \InvalidArgumentException(sprintf('Range expected two or three arguments, %s given.', func_num_args()));
}
}
function map($callback, \Traversable $subject)
{
return new CallbackIterator($subject, $callback);
}
function reduce($callback, \Traversable $subject, $acc = null)
{
foreach ($subject as $s) {
$acc = $callback($acc, $s);
}
return $acc;
}
function filter($callback, \Traversable $subject)
{
return new \CallbackFilterIterator($subject, $callback);
}
function takeWhile($callback, \Traversable $subject)
{
return new TakeIterator($subject, $callback);
}
function to_iterator(array $subject)
{
return new \ArrayIterator($subject);
}
function to_array(\Traversable $subject)
{
$array = array();
foreach ($subject as $s) {
$array[] = $s;
}
return $array;
}
// som van de eerste 5 even getallen.
// 2 + 4 + 6 + 8 + 10 = 30
// reduce(operator('+'), map(curry(operator('*'), 2), range(1,5)));
// eerste tien tientallen
// to_array(map(curry(operator('*'), 10), range(1, 10)));
// aantal even getallen kleiner of gelijk aan 100
// count(to_array(takeWhile(curry(operator('<='), 100), map(curry(operator('*'), 2), range(1, INF)))));
// aantal kwadraten onder de 100
// count(to_array(takeWhile(curry(operator('<='), 100), map(curry(operator('^'), 2), range(1, INF)))));
Reacties
0