Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

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:

PHP:
  1. $func = function($name) { echo 'Oh, hi! ', $name, '!', PHP_EOL; };
  2. $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:

PHP:
  1. $array = array (
  2.   array('naam' => 'Mathieu', 'leeftijd' => 25),
  3.   array('naam' => 'Joost', 'leeftijd' => 26),
  4.   array('naam' => 'Lotje', 'leeftijd' => 24)
  5. );

Om deze array oplopend op leeftijd te sorteren gebruik je de functie usort() met een eigen callback waarin je het element 'leeftijd' vergelijkt:

PHP:
  1. function sort_leeftijd_asc($ar1, $ar2)
  2. {
  3.   if ($ar1['leeftijd'] == $ar2['leeftijd'])
  4.     return 0;
  5.  
  6.  return ($ar1['leeftijd']> $ar2['leeftijd']) ? 1 : -1;
  7. }
  8.  
  9. usort($array, 'sort_leeftijd_asc');

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:

PHP:
  1.     $array,
  2.     function($value1, $value2) {
  3.         if ($value1['leeftijd'] == $value2['leeftijd'])
  4.             return 0;
  5.        
  6.         return ($value1['leeftijd']> $value2['leeftijd']) ? 1 : -1;
  7.     }
  8. );

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:

PHP:
  1. $array = array ('lotje', 'mathieu', 'Jaap', 'Kees');
  2. $filteredElements = array('Jaap', 'Kees');
  3.  
  4. $result = array_filter(
  5.         $array,
  6.         function($element) use ($filteredElements)
  7.         {
  8.                 return !in_array($element, $filteredElements);
  9.         }
  10. );
  11.  
  12. var_dump($result);

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:

PHP:
  1. function buildArrayFilter($filteredElements, $caseInsensitive)
  2. {
  3.     if ($caseInsensitive) {
  4.         $filteredElements = array_map($filteredElements, function($el) { return strtolower($el); } );
  5.  
  6.         $function = function($element) use ($filteredElements) {
  7.                 $element = strtolower($element);
  8.  
  9.                 return !in_array($element, $filteredElements);
  10.         };
  11.     } else {
  12.         $function = function($element) use ($filteredElements)
  13.         {
  14.                 return !(in_array($element, $filteredElements));
  15.         };
  16.     }
  17.  
  18.     return $function;
  19. }
  20.  
  21. $array = array ('Lotje', 'mathieu', 'Jaap', 'Kees');
  22.  
  23. $result = array_filter($array, buildArrayFilter(array('jaap', 'kees'), true));
  24. var_dump($result);
  25.  
  26. $result = array_filter($array, buildArrayFilter(array('jaap', 'kees'), false));
  27. var_dump($result);

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:

PHP:
  1. $lambda = function($name) { echo 'Oh, hi! ', $name; };
  2. var_dump($lambda);
  3.  
  4. // object(Closure)#1 (0) {
  5. // }

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:

PHP:
  1. function myLambdaCaller(Closure $target)
  2. {
  3.   return $target();
  4. }
  5.  
  6. $func = function() { echo "Hello, world!"; };
  7. echo myLambdaCaller($func);
  8. // Hello, world!
  9.  
  10. echo myLambdaCaller("test");
  11. // Catchable fatal error: Argument 1 passed to myLambdaCaller() must be an instance of Closure, string given.

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:

PHP:
  1. class MyFakeClosure
  2. {
  3.   public function __invoke($name)
  4.   {
  5.     echo 'Hello ', $name, '!';
  6.   }
  7. }
  8.  
  9. $func = new MyFakeClosure();
  10. $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:

PHP:
  1. $closure = function(&$myVar) { $myVar--; };
  2. $myVar = 10;
  3.  
  4. $closure($myVar);
  5. echo $myVar; // 9
  6.  
  7. $closure($myVar);
  8. echo $myVar; // 8

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:

PHP:
  1. class ReferenceTest {
  2.   public $value = 10;
  3. }
  4.  
  5. $obj = new ReferenceTest();
  6. $refBuilder = function &() use ($obj) { return $obj->value; };
  7.  
  8. $value =& $refBuilder();
  9.  
  10. $value += 5;
  11.  
  12. echo $obj->value;

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:

PHP:
  1. class MyArrayHandler {
  2.     private $source;
  3.  
  4.     public function __construct($source) {
  5.         $this->source = $source;
  6.     }
  7.  
  8.     public function buildFilterClosure($filtered) {
  9.         return function() use ($filtered) {
  10.             $this->source = array_filter (
  11.                 $this->source,
  12.                 function($element) use ($filtered) { return !in_array($element, $filtered); }
  13.             );
  14.         };
  15.     }
  16.  
  17.     public function dump() { var_dump($this->source); }
  18. }
  19.  
  20. $handler = new MyArrayHandler(array('lotje', 'mathieu', 'Kees', 'Jaap'));
  21. $filter     = $handler->buildFilterClosure(array('Kees', 'Jaap'));
  22.  
  23. $filter();
  24.  
  25. $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:

PHP:
  1. function ReflectionTest() { echo 'Hi'; }
  2.  
  3. $x = new ReflectionFunction('ReflectionTest');
  4. $y = $x->getClosure();
  5.  
  6. $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.

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.

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!

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 ;)

Je hebt gelijk, fixed. Thanks!

Mooi! Deze mogelijkheden ben ik in JavaScript erg gaan waarderen en ik kan niet wachten dit ook in PHP te gaan gebruiken.

Leave a comment
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>