Funciones anónimas en PHP

Desde la versión 5.3, PHP nos ofrece funciones anónimas (o una suerte de) a las que casi casi podemos considerar First-class citizens.
Luego de experimentar un poco en un pequeño proyecto, estas son mis pequeñas conclusiones.

Lo básico

Las funciones anónimas son (como es de esperar) objetos, más particularmente instancias de la clase Closure. Como tales, pueden tener métodos y propiedades (en la versión 5.4 tienen un único método: bindTo, además de los magic methods).

Los ejemplos clásicos de uso de funciones anónimas se pueden ver con los clásicos map y reduce:

$list = array(1, 2, 3, 4);

$sum = function($memo, $current) {
    return $memo + $current;
};

echo array_reduce($list, $sum, 0); // 10

$square = function($current) {
    return $current * $current;
};

echo array_reduce(array_map($square, $list), $sum); // 30

Como otros valores, pueden ser definidas en distintos contextos así como tambien ser ejecutadas. En este aspecto pareciera ser que los “Closures” no se corresponden con otros objetos*:

// Ok
$myFn = function() {
    // Ok
    $anotherFn = function() { echo 'Hello World'; };

    return $anotherFn;
};

// Ok
var_dump($myFn()); // object(Closure)#2 (0) { }

// Parse error: syntax error, unexpected '('
$myFn()();

// Ok
$myFn()->__invoke(); // Hello World

// Ok
$myFn->__invoke()->__invoke(); // Hello World

// Ok
$result = $myFn();
$result(); // Hello World

// Ok
$inArray = array(function() { echo 'Hello World'; });

// Ok
$inArray[0](); // "Hello World"

// Constants may only evaluate to scalar values
define('MY_CONSTANT', function() {});

class Foo {
	// Parse error: syntax error, unexpected T_FUNCTION
	const IS_GET = function() {};

	// Parse error: syntax error, unexpected T_FUNCTION
	private $foo = function() {};

	// Parse error: syntax error, unexpected T_FUNCTION
	private $fns = array(
		'foo' => function() {}
	);
}

$myObject = new stdClass();

// Ok
$myObject->fn = function() { echo 'Hello World'; };

// Fatal error: Call to undefined method stdClass::fn()
//$myObject->fn();

// Ok
$myObject->fn->__invoke(); // "Hello World"

// Parse error: syntax error, unexpected '('
($myObject->fn)();

// Ok
$holder = $myObject->fn;
$holder(); // "Hello World"

*Test realizados sobre la versión 5.4.0beta2-dev

Closures

Los closures propiamente dichos están disponibles, pero (gran sorpresa) requieren explícitar las referencias deseadas:

$foo = 1;
$bar = 2;

$fn = function() use($foo, $bar) {
  echo $foo + $bar;
};

$fn(); // 3

Como dato curioso, podemos referenciar las variables, para que el valor que toma la función (en caso de que sean primitivas) siempre sea el actual y no el inicial:

$foo = 1;
$bar = 2;

$fn = function() use(&$foo, $bar) {
  echo $foo + $bar;
};

$foo = 10;

$fn(); // 12

Obviamente, las referecias a $foo y $bar quedan ligadas sin importar si están disponibles en el scope donde se ejecute $fn.

Binding

Desde PHP 5.4, esta disponible el método Closure#bindTo (y su versión estática Closure::bind) que nos permiten cambiar el thisValue de la función anónima, definiendo inclusive la visibilidad que tendrá dentro del objeto/clase en cuestión.

La firma del método consta de dos argumentos, siendo el primero el objeto al cual se bindeará la función y el segundo la clase que definirá su visibilidad. En caso de no definir este último argumento, la función tendrá disponible únicamente los métodos y propiedades públicos. En caso de definir la clase o el objeto (del cual tomará la clase).

Para clarificar esto, dada una clase Foo y una función anónima $fn:

class Foo {
	static private $staticPrivate = 'staticPrivate';
	static protected $staticProtected = 'staticProtected';
	static public $staticPublic = 'staticPublic';

	private $private = 'private';
	protected $protected = 'protected';
	public $public = 'public';
}

$foo = new Foo;

// Nuestra funcion anonima
$fn = function($name, $isStatic = false) {
    echo  ($isStatic ?
            Foo::$$name :
            $this->{$name}) . '<br/>';
};

Podemos bindear la función de la siguiente manera:

$bindedFn = $fn->bindTo($foo);

// Binding con scope de clase Foo
$bindedWithScope = $fn->bindTo($foo, 'Foo');
// Que es similar a
$bindedWithScope = $fn->bindTo($foo, $foo);

Lo que resultará de la siguiente manera:

$bindedFn('public'); // public
$bindedFn('protected'); // Fatal error: Cannot access protected...
$bindedFn('private'); // Fatal error: Cannot access private...

$bindedFn('staticPublic', true); // staticPublic
$bindedFn('staticProtected', true); // Fatal error: Cannot access protected...
$bindedFn('staticPrivate', true); // Fatal error: Cannot access private...

$bindedWithScope('public'); // public
$bindedWithScope('protected'); // protected
$bindedWithScope('private'); // private

$bindedWithScope('staticPublic', true); // staticPublic
$bindedWithScope('staticProtected', true); // staticProtected
$bindedWithScope('staticPrivate', true); // staticPrivate

ReflectionFunction

A la hora de manipular nuestras funciones anónimas, tenemos la posibilidad de aplicar reflection sobre las mismas utilizando la clase ReflectionFunction*, obteniendo así información sobre la misma (nombre, argumentos, archivo donde fue definida, etc).

*Es una lástima que la documentación oficial sea tan deplorable.

Esta clase es especialmente útil a la hora de generar llamadas on the fly, permitiendo manipular los argumentos con los que se llamará a la función.

Un ejemplo de esto se puede ver en este fragmento de código de del pequeño proyecto en cuestión:

private function fillArguments($fn, $arguments = array()) {
	if(!is_callable($fn)) {
		return $arguments;
	}

	$reflection = new \ReflectionFunction($fn);
	$names = $reflection->getParameters();

	array_shift($names);

	foreach ($names as $arg) {
		$arguments[] = $this->request->value(
			$arg->getName(),
			$arg->isOptional() ? $arg->getDefaultValue() : null
		);
	}

	return $arguments;
}

public function exec($fn) {
	$arguments = $this->fillArguments($fn, array($this->memo));

	return call_user_func_array(\Closure::bind(
		$fn,
		$this->executionContext,
		$this->executionContext
	), $arguments);
}

Resumiendo

Lo bueno

  • La sintaxis es consistente con las funciones y métodos.
  • Soportan closures.
  • Soportan binding.
  • Y reflection!

Lo malo

  • El lookup de métodos y propiedades de los objetos hace imposible emular métodos con funciones anónimas (aunque, se puede hackear mediante el método mágico __call).
  • Requiere la invocación (excesivamente) explícita (_invoke) en algunos casos.
  • No se puden declarar funciones anónimas como atributos de clases.
  • A la clase Closure le falta bastante azúcar.
  • La documentación oficial deja bastante que desear.


2 views shared on this article. Join in...

  1. joseanpg dice:

    Magnífico artículo Valentin.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Comment

You may use these tags : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>