PHP Lambda functions y Closures, lo que es bullshit y lo que realmente es de provecho

Agosto 3, 2008 at 8:25 pm (Evangelios de Dioses de Silicio, Libro de Hechizos PHP) (, , , , , , )

Bueno, hace unos días que lo ví en el twitter de phpsenior y tenía ganas de explicar realmente como viene la mano con este tema algo controversial.

Primero tienen que entender que hace años que el tema se propone incontables veces en la lista de internas de PHP y el argumento de rechazo a las lambda functions siempre fue el mismo: PHP no es lenguaje funcional, no hay nada de provecho en implementar lambda functions, y el eterno purismo que dice que las lambda functions son solo indicio de un mal diseñador de sistemas.

En general esto último tiene algo de cierto. Las lambda functions son en esencia funciones anónimas o con nombres aleatorios. Este tipo de construcciones son en general indicadores de un mal diseño. Si algo carece de una denominación apropiada es porque no debería estar ahí o no se encuentra en el lugar correcto del sistema.

Ahora, esto cambio cuando alguien trajo a la mesa la posibilidad de incluir closures y con esto una nueva utilidad a las funciones anonimas o lambda functions. Pero primero voy a tratar de explicar porque y para que es que ciertos usuarios piden que se agregen las lambda functions.

Como saben hay ciertas funciones en la librería de PHP que utilizan otra función indicada por parametros, por ejemplo array_map, array_filter, array_reduce, array_walk, usort y sus variantes. La queja de los que piden las lambda functions es que a veces no resulta práctico tener que crear una función adicional para poder pasarsela a este tipo de funciones, por ejemplo, si quisieramos filtrar todos los elementos de un array que sean mayores a 2:

function greaterThan2($number) {
	return $number > 2;
}
$array = array_filter($array, 'greaterThan2');

La función que creamos es muy ad-hoc, carente de un propósito general que justifique su presencia salvo para este caso particular. Usando lambda functions:

$array = array_filter($array, function ($number) {
	return $number > 2;
});

En general esto puede a simple vista resultar una manera práctica y elegante de resolver esta situación, a contraste de otras opciones que uno tiene a disposición en PHP, tal como se argumenta en el RFC. Esto no es cierto. Abusar de estas facilidades resulta en un embrollo en donde se pierde fácilmente la noción de cual es el scope donde actualmente estamos parados, sumado al anonimato de los componentes se vuelve una pasta inentendible y difícil de depurar. Si no me creen, dense una vuelta por el mundo Java a ver que tal les parece las clases anónimas, sumado al AOP y los stack traces quilómetros…

Lo que uno tiene que darse cuenta como programador es que lo que esta bueno de un lenguaje no es lo que te deja hacer, sino lo que no te deja hacer. En el momento en que empiecen a ver lambda functions anidadas vamos a ver que tan elegante les parece.

Y lo cierto es que sí hay una opción elegante, reusable y que no hace uso de funciones anónimas, algo no muy utilizado del OOP, functor objects. Un functor es basicamente un objeto que abstrae el comportamiento de una función. Mejor explicado con un ejemplo:

class GreaterThan {
	private $limit;
	public function __construct($limit) { $this->limit = $limit; }
	public function call($number) { return $number > $this->limit; }
}
$array = array_filter($array, array(new GreaterThan(2), 'call'));

En este caso el functor tiene un propósito general que puede reutilizarse en otras partes del sistema.

Ahora, volviendo al tema de los closures, esta adición hizo que el parche de las lambda functions resultara interesante. Para entender porque volvamos al ejemplo, suponiendo ahora que estamos construyendo una función genérica para seleccionar solo los elementos que superen cierto valor, usando array_filter. Esto NO lo podemos hacer en PHP:

function filterGreaterThan($array, $limit) {
	$array = array_filter($array, function ($number) {
		return $number > $limit;
	});
}

No podemos hacerlo porque $limit no existe dentro de la lambda function, no puede “ver” esa variable desde ahí dentro. Para eso se requiere implementar closures. En la propuesta que se implemento para PHP5.3 sería de esta manera:

function filterGreaterThan($array, $limit) {
	$array = array_filter($array, function ($number) use ($limit) {
		return $number > $limit;
	});
}

De esta manera la función anónima “copia” la variable a su scope.

Ahora vuelvan a mirar el ejemplo de functor que les mostre antes y vuelvan a mirar el ejemplo de lambda function con closure. Si no tuvieron un momento “AHA!!” cierren la página y vayan a estudiar más analisis y diseño orientado a objetos porque les está haciendo falta.

En efecto, los functor objects no solo son una muy buena opción para no tener que usar lambda functions, también son una muy buena opción para no tener que usar closures. Todo esto sin andar ensuciando el sistema con funciones y/o clases anónimas. Lo más interesante de todo es que cuando usas lambda functions con closures internamente PHP está creando un functor object. La clase especial “Closure” creada con este parche no es más que un Functor que contiene a la función anónima y en sus propiedades contiene a las variables pasadas mediante use(…)

Ahora, mi consejo es: un buen diseño orientado a objetos debería evitar las funciones anónimas. Estan los functor objects y varios patrones de diseño como opciones para no tener que usarlos. Pero en todo el meollo de lo que implementaron hay algo muy interesante y realmente útil para agregar a la “bolsa de trucos”. Ironicamente esto que considero lo más importante esta visto como un “extra”, un adicional, un condimento: la función __invoke(). Esto es lo que faltaba para que usar functors con PHP sea práctico y elegante.

Mediante __invoke podemos hacer que un objeto se comporte como una función, lo cual es justamente lo que queremos para un functor. Reescribiendo el primer ejemplo de functor:

class GreaterThan {
	private $limit;
	public function __construct($limit) { $this->limit = $limit; }
	public function __invoke($number) { return $number > $this->limit; }
}
function filterGreaterThan($array, $limit) {
	$array = array_filter($array, new GreaterThan($limit));
}

Hora de buscar mi vieja implementación de functors en PHP y ver si la publico como open source de una buena vez…

Permalink 5 comentarios

Mitos de la programación – ¿OOP? III

Julio 19, 2008 at 10:33 pm (Filosofia de Toilette) (, , , , )

Retomando el tema de la reusabilidad y OOP. En la discusión en ADVA que mencione antes escribí un ejemplo con pseudocódigo para explicar la diferencia entre reusabilidad a caja abierta y reusabilidad a caja cerrada. Un ejemplo bastante burdo de como pensar en objetos el juego del tetris (como es un foro de desarrolladores de videojuegos…): Leer el resto de esta entrada »

Permalink 2 comentarios

Mitos de la programación – ¿OOP? II

Julio 19, 2008 at 1:53 pm (Filosofia de Toilette) (, , , , )

Siguiendo con el post anterior. Esto me recuerda uno de los mitos más difíciles de desterrar de la programación orientada a objetos, y uno de los más polémicos. En general un buen diseño en objetos te asegura un buen grado de reusabilidad, pero no es una garantía. Decir que trabajar con objetos te asegura la reusabilidad es una exageración, una falacia. No alcanza con usar programación orientada a objetos para hacer tu código reusable, y tampoco un lenguaje que no provee features del OOP está menos predispuesto a la reusabilidad solo por el hecho de no soportar OOP.

Por ahí acá resulta medio contradictorio lo que digo, siendo que es inevitable para mi hablar de reusabilidad cuando hablo del paradigma de objetos. En parte esto es porque no considero como un uso apropiado del paradigma aquello que no busque una buena reusabilidad de los algoritmos creados. Sin embargo hay que evitar enseñar a los que introducen en este paradigma que la reusabilidad se obtiene sin costo alguno. En particular hay que evitar dar a entender la reusabilidad como una característica del OOP. Se puede hacer tanto código reusable como pasta-code en OOP como en paradigma procedural o en el paradigma funcional.

En pocas palabras, la reusabilidad no es una característica del OOP, no hay garantías de ello prácticamente en ningún paradigma. PERO, diseñar a partir de simular los procesos reales (característico de OOP), sumado a un cuidado diseño orientado a la reusabilidad, generalmente concluyen en un sistema fácilmente mantenible.

En el foro de ADVA tuvimos con algunos usuarios una discusión bastante acalorada sobre el lenguaje C++ y en general sobre ciertos aspectos del paradigma de objetos. Uno de los temas tocados fue sobre la reusabilidad. Quiero extraer algunos fragmentos de esta discusión. Por ejemplo acá comento sobre la crítica academica al OOP en temas de reusabilidad y la propuesta del GoF y los patrones de diseño:

Algo en lo que insisto siempre es en destronar el mito del OOP como facilitador de la reusabilidad. Usar OOP no garantiza reusabilidad, por el contrario, una de las críticas académicas más fuertes al OOP es que la reusabilidad por herencia resulta en malas prácticas que afecta la mantenibilidad (esa palabra es un trabalenguas) del sistema. En contraposición, el paradigma procedural tiene un modelo de reusabilidad mucho más organizado, que genera una mayor independencia entre componentes.

Sobre esto, el “Grupo de los Cuatro” (Gang of Four) popularizo los términos de “white-box reuse” (reusabilidad a caja abierta) y “black-box reuse” (reusabilidad a caja cerrada). La reusabilidad a caja abierta, o reusabilidad por herencia, es la que es criticada por generar dependencias entre los componentes a la hora de hacer tareas de mantenimiento. La reusabilidad a caja cerrada, o reusabilidad por interfases, es la usada en el paradigma procedural y la que el GoF propone recuperar en el paradigma OO mediante patrones de diseño.

Ahí tengo que admitir que elegí mal las palabras. Puntualmente el OOP, bien usado, puede resultar un facilitador de la reusabilidad. El punto importante es que la reusabilidad no es una garantía, en ningún paradigma de la programación. En particular es una crítica académica que el OOP favorece practicas incorrectas de reusabilidad, pero esto está originado en el mismo ámbito académico que enseña mal el paradigma de objetos.

Acá hay otro extracto de esa discusión donde observo los comentarios de Stroustrup sobre el lenguaje C++ y el paradigma de objetos:

Me tome el tiempo de releer los capítulos iniciales del libro de Stroustrup. En ningún momento plantea el paradigma de objetos como una mejora a la reusabilidad, lo cual era de esperarse, sería una promesa ridícula. En el prefacio habla del OOP como una técnica de abstracción de datos más “flexible y eficiente”, lo cual es cierto. Dice “Cuando se les utiliza bien, estas técnicas producen programas más cortos y más fáciles de comprender y mantener”. Esto último es en general cierto con el paradigma de objetos. El punto clave es el “buen uso”. Después habla del type safety sobre todo.

En el primer capítulo hace una reseña filosófica sobre el lenguaje, donde dice que el lenguaje C está pensado como un “lenguaje cercano a la máquina”, y los agregados que se hicieron en C++ son para hacer un “lenguaje cercano al problema a resolver”. Hace una reseña bastante interesante que quiero compartir:

“El lenguaje proporciona al programador un conjunto de herramientas conceptuales; si éstas resultan inadecuadas para una tarea, sencillamente se las ignorara. (…) No se puede garantizar el buen diseño y la ausencia de errores sólo por la presencia o ausencia de características específicas del lenguaje.”

Es rescatable esto último que dice Stroustrup, y sobre como la intención de C++ es acercar al programador al problema a resolver, esto es en realidad cierto no solo para C++ sino para el paradigma de objetos en general.

Continuo más adelante sobre el tema para no alargar innecesariamente el post.

Permalink Dejar un comentario

Siguiente Página »