PHP 5.3: Closures en Lambda functies
Het voorstel voor Closures en Lambda functies waar ik vorige maand over schreef is inmiddels zover doorontwikkeld dat de feature is opgenomen in PHP 5.3. Sinds het voorstel zijn er de nodige wijzigingen geweest in hoe lambda's en closures gebruikt worden, dus werd het tijd voor een compleet overzicht.
Wat zijn lambda's en closures ?
Lambda functies
Laten we beginnen met lambda functies. Dit zijn functies die geen naam hebben, ofwel anonieme functies, maar welke worden toegekend aan een (evt. tijdelijke) variabele. Via deze variabele zijn deze lambda functies dan aan te roepen. Dat ziet er dan zo uit:
-
$func();
Dit lijkt een klein beetje op de PHP callbacks die je wellicht al kent. Je kunt ze dan ook gebruiken op alle plekken waar je ook callbacks kunt gebruiken, maar lambda functies en callbacks verschillen behoorlijk.
Waar een PHP callback een verwijzing (met een string, of een object en een string) naar een functie is, bevat $func uit het voorbeeld hierboven een daadwerkelijke representatie van de gedefinieerde functie. Verderop in het artikel zal ik hier nog iets verder op in gaan.
Je gebruikt lambda functies vooral wanneer de functie die je wilt bouwen wel nodig is maar eigenlijk niet belangrijk genoeg is om een eigen plaats in de namespace op te eisen. Dit gebeurt vooral met, maar niet uitsluitend met, callbacks die je maakt voor bijv. usort, array_map, preg_replace_callback, enzovoorts.
Een mooi voorbeeld voor het gebruik van een lambda functie is het sorteren van een multi-dimensionele array aan de hand van een van de elementen. Neem de volgende array:
Om deze array oplopend op leeftijd te sorteren gebruik je de functie usort() met een eigen callback waarin je het element 'leeftijd' vergelijkt:
-
function sort_leeftijd_asc($ar1, $ar2)
-
{
-
if ($ar1['leeftijd'] == $ar2['leeftijd'])
-
return 0;
-
-
return ($ar1['leeftijd']> $ar2['leeftijd']) ? 1 : -1;
-
}
-
Deze functie is natuurlijk enorm specifiek. Deze kun je alleen gebruiken voor arrays met daarin arrays met daarin een element 'leeftijd'. Daarom is het een beetje jammer dat daar een functie naam aan moet hangen, daarbij zou je misschien de functie in een ander bestand zetten omdat dat de 'netste' plek zou zijn, wat het geheel weer niet veel duidelijker maakt.
Met een lambda functie zou je deze als volgt kunnen herschrijven:
-
$array,
-
function($value1, $value2) {
-
if ($value1['leeftijd'] == $value2['leeftijd'])
-
return 0;
-
-
return ($value1['leeftijd']> $value2['leeftijd']) ? 1 : -1;
-
}
-
);
In deze versie definieren we als 2e argument aan usort een nieuwe lambda functie. Een functie die alleen op het moment van de aanroep aan usort bestaat en bereikbaar is. Daarna bestaat de functie niet meer en kan ook geen onduidelijkheid meer veroorzaken. Doordat we de functie definieren precies op de plek waar we hem willen gebruiken, wordt ook direct duidelijk wat de bedoeling is van de usort aanroep.
Closures
Normaal gesproken definieer je een functie specifiek voor 1 doel met een specifiek aantal stappen om dat doel te bereiken. Het wordt compile-time verwerkt en een functie is dus een statisch element binnen je webapplicatie. Een closure verandert dat principe.
Een closure is eigenlijk hetzelfde als een lambda functie, met het verschil dat deze gebruik maakt van de scope waarin hij gedefinieerd wordt. Dit heeft als gevolg dat je runtime functies kunt gaan samenstellen, aan de hand van runtime gegevens wat enorme flexibiliteit met zich mee brengt:
-
-
$array,
-
function($element) use ($filteredElements)
-
{
-
}
-
);
-
In dit voorbeeld definieer ik 2 lijsten van namen. $filteredElements bevat de namen die ik uit de eerste lijst wil filteren. Vervolgens is daar de aanroep naar array_filter met als 2e argument een closure definitie.
Met het use keyword geef je aan welke variabelen je uit de bovenliggende scope wilt gebruiken. Deze variabelen worden als kopie geimporteerd in de scope van de closure. Als je de variabele als reference wilt importeren kan dat ook, je plaatst dan simpelweg een & teken voor de variabele zoals normaal bij references.
Maar het kan nog leuker! Omdat een closure runtime gedefinieerd wordt en toegekend wordt aan een variabele, kan je dus afhankelijk van runtime informatie verschillende closures gebruiken:
-
function buildArrayFilter($filteredElements, $caseInsensitive)
-
{
-
if ($caseInsensitive) {
-
-
$function = function($element) use ($filteredElements) {
-
-
};
-
} else {
-
$function = function($element) use ($filteredElements)
-
{
-
};
-
}
-
-
return $function;
-
}
-
-
-
Dit voorbeeld gebruikt een aparte closure voor het case-insensitive en case-sensitive filteren van gegevens.
Lambda's en closures zijn objecten
Zoals ik al eerder aanhaalde bevat de variabele waarin je een lambda of closure definieert de representatie van de functie. In feite is deze representatie een object van het type Closure:
Echter, ze zijn niet objecten in de stricte zin van het woord. Daarmee bedoel ik dat hoewel de code van de functie wordt uitgevoerd via/binnen de context van de methode Closure::__invoke dit niet inhoudt dat je een volwaardig object hebt. Zo kun je geen properties toewijzen aan een Closure object en is $this binnen de closure geen referentie naar het huidige Closure object.
De reden voor het gebruik van Closure objecten is volgens mij voornamelijk technisch. Het gebruik van een object voor closures maakt het volgens mij voor de php ontwikkelaars mogelijk om closures gemakkelijk uit het geheugen te halen op het moment dat ze niet meer nodig zijn.
Het biedt ons ook de nodige mogelijkheden. Zo is het nu mogelijk om met type-hints te forceren dat een functie ook echt een lambda functie of closure moet krijgen:
Een tweede 'voordeel' is dat er een nieuwe magic method geintroduceerd wordt: __invoke en deze kunnen we natuurlijk eventueel gebruiken in een eigen klasse - al moet het nut daarvan nog eens bepaald worden, zeker aangezien de klasse Closure als final gedefinieerd is en je er dus niet van kunt overerven:
-
class MyFakeClosure
-
{
-
public function __invoke($name)
-
{
-
}
-
}
-
-
$func = new MyFakeClosure();
-
$func();
De oplettende lezer heeft misschien gemerkt dat, hoewel de magic method __call een array van argumenten ontvangt __invoke de argumenten lijst zelf definieert. Dit is op het tijd van schrijven nog een discussie punt en het is goed mogelijk dat dit verandert naar hoe __call werkt.
Lambda's, Closures en references
Lambda functies en closures zijn volwaardige functies en dat betekent dat ze alle functionaliteit bieden die gewone functies ook hebben, dus ook references. Mocht je een variabele als referentie willen doorgeven, dan kun je dit op de gebruikelijke manier doen:
Wil je een waarde by-reference retourneren, dan is de syntax wellicht wat verwarrender. Om het voorbeeld dat PHP.net geeft voor return-by-reference iets aan te passen:
-
class ReferenceTest {
-
public $value = 10;
-
}
-
-
$obj = new ReferenceTest();
-
$refBuilder = function &() use ($obj) { return $obj->value; };
-
-
$value =& $refBuilder();
-
-
$value += 5;
-
Lambda en closure gebruik binnen objecten
Tot nu toe hebben we het vooral gehad over lambda's en closures in de procedurele vorm. Maar ze zijn natuurlijk ook te gebruiken binnen objecten. Laten we de filter functie die we hierboven al eens procedureel hebben ontwikkeld eens in object-vorm gieten:
-
class MyArrayHandler {
-
private $source;
-
-
public function __construct($source) {
-
$this->source = $source;
-
}
-
-
public function buildFilterClosure($filtered) {
-
return function() use ($filtered) {
-
$this->source,
-
);
-
};
-
}
-
-
}
-
-
-
$filter();
-
-
$handler->dump();
De grote truc zit hem natuurlijk in de buildFilterClosure methode die 2 closures definieert. De eerste is een kleine functie die we kunnen gebruiken binnen als callback voor array_filter - deze checked simpelweg of het element in de opgegeven $filtered array voorkomt. De bovenliggende closure definieert de aanroep naar array_filter en maakt gebruik van de private eigenschap source.
Er zijn twee opmerkelijke dingen aan deze code.
Ten eerste blijkt dat we $this niet via use() constructie hoeven door te geven. Dat klopt, deze wordt automatisch doorgegeven door PHP. Zelfs op het moment dat je een genestte closure maakt, zoals wij hier doen.
Ten tweede benaderen we in de closure een private eigenschap die we uiteindelijk vanuit het publieke domein benaderen. Dat lijkt misschien wat vreemd, maar aangezien de closure vanuit een publieke methode wordt gemaakt is de aanroep-semantiek in feite hetzelfde als die voor de buildFilterClosure methode. Het wordt haast een dynamisch toegevoegde methode.
Uitbreidingen op Reflection
Hoewel er geen echte uitbreidingen zijn voor lambda's en closures zelf, ze zijn immers anoniem, hebben de Reflection klassen ReflectionFunction en ReflectionMethod wel een nieuwe methode erbij gekregen: getClosure. Met deze functie kun je van een normale functie alsnog een lambda functie maken:
-
-
$x = new ReflectionFunction('ReflectionTest');
-
$y = $x->getClosure();
-
-
$y();
Conclusie
De nieuwe closures en lambda functionaliteit in PHP 5.3 biedt ons PHP ontwikkelaars enorm veel nieuwe flexibiliteit. Aangezien dit soort functionaliteit voor PHP ontwikkelaars, myself included, toch wel nieuw is zal het waarschijnlijk even duren voordat iedereen het nut ervan ziet en het zal zeker even duren voordat lambda's en closures tot hun volledige nut gebruikt zullen worden.
Volg Scriptorama via RSS!
Reageer ook!
Mooi artikel! PHP 5.3 lijkt een nuttige update te worden. Zelf kijk ik er zeker ook naar uit om __callStatic te gaan gebruiken, een andere toevoeging.
Ik vraag me alleen nog wat af wat nu precies het verschil of voordeel is van een closure met use keyword ten opzichte van het global keyword binnen de functie zelf.
Door Geert
op 08.11.08 @ 9:33 am | Permalink
Ik denk dat het hem er hier in zit:
"Met het use keyword geef je aan welke variabelen je uit de bovenliggende scope wilt gebruiken."
Met global kan het eigenlijk overal vandaan komen.
Overigens een zeer duidelijk, en goed uitgewerkt, artikel!
Door Gerard Klomp
op 08.11.08 @ 9:57 am | Permalink
Er zit een fout in één van de codes :)
"if ($caseInsensitive) {
array_map($filteredElements, function($el) { return strtolower($el); } );
"
De array_map() word niet opnieuw toe gewezen dus de verwerking heeft geen nut ;)
Door Sebastiaan Stok
op 08.11.08 @ 10:55 am | Permalink
Je hebt gelijk, fixed. Thanks!
Door Mathieu Kooiman
op 08.11.08 @ 11:17 am | Permalink
Mooi! Deze mogelijkheden ben ik in JavaScript erg gaan waarderen en ik kan niet wachten dit ook in PHP te gaan gebruiken.
Door Edwin Martin
op 08.11.08 @ 10:35 pm | Permalink
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>