El secreto de Ruby: Las «clausuras»

Uno de los mecanismos más potentes provistos por el lenguaje Ruby es la posibilidad de manipular bloques de código que pueden ser pasados como argumentos en la invocación a funciones.

Este mecanismo se denomina «clausura» («closure«, en inglés), y aunque no es exclusivo de Ruby (es un viejo conocido en Smalltalk y Lisp, encontrándose aún en Perl y Python), la simplicidad de su uso lo transforma en una herramienta muy poderosa y flexible.

Quienes hemos programado en lenguajes funcionales (como ML y Haskell), siempre hemos extrañado en los lenguajes imperativos la posibilidad de pasar funciones como parámetros de forma «natural» (no a la manera en que puede hacerse en Java o Pascal, sino incluyendo las ligaduras al scope actual).

Un ejemplo simple

Supongamos que deseamos escribir una función que tome como parámetro un arreglo de valores enteros y retorne otro arreglo, producto de multiplicar por 2 cada uno de los elementos del parámetro.

En Java podríamos escribir algo así:

public static ArrayList double(ArrayList x)
{
  ArrayList y = new ArrayList();
  for (Iterator i = x.iterator(); i.hasNext(); )
    y.add(i.next() * 2);
  return y;
}

(Obviamente, olvidándonos de algunos casts para simplificar la lectura.)

O, en PHP:

function double($x) {
  $y = array();
  foreach ($x as $e)
    array_push($y, $e*2);
  return $y;
}

En tanto que en un lenguaje funcional, como Haskell podríamos escribir lo siguiente:

double x = map (*2) x

O, simplemente:

double = map (*2)

«map» toma como parámetro una función (en este caso, la función parcial «*2«) y un arreglo, devolviendo el resultado de aplicar dicha función a cada elemento del arreglo.

Veamos un ejemplo de la evaluación de double:

double [1, 2, 3]
[1*2, 2*2, 3*2]
[2, 4, 6]

Como podemos ver, la principal ventaja de los lenguajes funcionales es su expresividad, en el sentido de que el código del programa expresa de forma mucho más clara qué es lo que este hace (multiplicar por 2 cada elemento del arreglo).

En Ruby, aunque no es un lenguaje funcional, la posibilidad de pasar funciones (o, en general, bloques de código) como parámetro se presenta de forma igualmente sencilla:

def double(a)
  a.map {|e| e*2}
end

«map» es un método de la clase «Array» que toma un bloque de código y lo ejecuta para cada elemento del arreglo, devolviendo la colección de los resultados producidos. El fragmento «|e|» establece la ligadura entre el identificador «e» y el elemento actual.

Explotando la potencia de las clausuras

Supongamos ahora que tenemos una clase «Person«, cuyo método «age» calcula la edad de la persona. Si tenemos una colección de personas en el arreglo «people» y deseamos obtener sólo aquellas que son mayores de 18 años, en Ruby podríamos escribir lo siguiente:

people.select{|p| p.age > 18}

(El método «select» aplica una función lógica (booleana) a cada elemento y devuelve sólo aquellos para los cuales resultó verdadera.)

En tanto que si, además, quisiéramos mostrar el nombre de cada una de ellas, podríamos escribir:

people.select{|p| p.age > 18}.each{|p| puts p.name}

(El método «each» aplica el bloque a cada elemento del arreglo.)

O bien, de forma más compacta (sin generar un segundo arreglo):

people.each{|p| puts p.name if p.age > 18}

Comparación con otros lenguajes

Problema 1

Convertir las cadenas de un arreglo a mayúsculas.

En PHP:

for ($i = 0; $i < count($a); $i++)
  $a[$i] = strtoupper($a[$i]);

En Ruby:

a.map!{|e| e.upcase}

(El método «map!«, a diferencia de «map«, modifica el arreglo, en vez de retornar uno nuevo.)

Problema 2

Dado un arreglo de la forma [["nombre", "apellido"]], obtener un arreglo de la forma ["apellido, nombre"]. Por ejemplo, dado:

[["Javier","Smaldone"],["Santiago","Lobos"],["Bon","Scott"]]

obtener:

["Smaldone, Javier", "Lobos, Santiago", "Scott, Bon"]

En PHP:

$b = array();
foreach ($a as $e)
  array_push($b, $e[1] . ', ' . $e[0]);

En Ruby:

b = a.map{|e| e.reverse.join(', ')}

Problema 3

Calcular la edad promedio de las personas mayores de edad (según el ejemplo planteado anteriormente).

En PHP:

$sum = 0;
$count = 0;
foreach ($people as $p) {
  if ($p->age > 18) {
    $sum += $p->age;
    $count++;
  }
}
if ($count) print $sum/$count;

Una solución posible en Ruby sería:

adults = people.select{|p| p.age > 18}
sum = 0
adults.each {|p| sum += p.age }
puts sum.to_f/adults.size if adults.size > 0

Pero usando todo el poder de las clausuras y las funciones «map» e «inject«, podríamos escribir:

adults = people.select{|p| p.age > 18}
sum = adults.map{|p| p.age}.inject{|ac,e| ac+e}
puts sum.to_f/adults.size if sum

( «inject» ejecuta el bloque, asignando la segunda variable a cada elemento del arreglo y asignando el resultado de la ejecución a la primera, manteniendo su valor a través de cada iteración.)

Problema 4

Dado un arreglo de cadenas, obtener la de mayor longitud.

En PHP:

$max = '';
foreach ($a as $e)
  if (strlen($e) > strlen($max))
    $max = $e;

En Ruby:

max = a.inject {|m, e| e.length > m.length ? e : m }

(Adicionalmente, hemos usado el operador de asignación condicional «cond ? val1 : val2«, que retorna «val1» si «cond» es verdadero y «val2» en caso contrario.)

Conclusión

La posibilidad de utilizar clausuras nos acerca a la simplicidad y la expresividad de la programación funcional. Este es uno de los puntos fuertes del lenguaje Ruby (y la envidia de otros lenguajes, como Java, cuya comunidad está debatiendo sobre la posibilidad de incorporarlo en la versión 7).

(Y sí, se lo que pensará mi amigo Ricardo cuando lea este artículo. Es cierto, los lenguajes tienden a Lisp.)

Algunas referencias:

17 comentarios sobre “El secreto de Ruby: Las «clausuras»

  1. Gracias por el post, después de tanto tiempo posteas algo realmente interesante, sobretodo para los desarrolladores de software, lenguajes como Ruby, Python y perl facilitan mucho la tarea del desarrollador, yo personal mente aprendí Python y estoy comenzando con una pequeña aplicación (proyecto final de la Uni) y me ha facilitado mucho el trabajo, ahora es cosa de comenzar con Ruby que segun puedo ver en tus ejemplos es muy muy util.

    gracias por el tiempo.

  2. Patricio:

    Sí, parece que dijera otra cosa. Pero como hace mucho que no posteo nada, no tengo por qué tomarlo a mal ;)

    Santi:

    Por algo no te avisé del post. Tardaste poco en descubrirlo (y saltaste como yo imaginaba) :P

  3. Ya que alguien preguntó por ahí, aquí va la definición de ‘map’ en el lenguaje Haskell:

    map f [] = []
    map f (x:xs) = (f x):(map f xs)

    Explicación:

    – Al aplicar una función a una lista (arreglo) vacía, se obtiene una lista vacía.

    – Al aplicar una función a una lista no vacía (que tiene un elemento ‘x’ seguido de una lista ‘xs’), se obtiene una lista cuyo primer elemento es el resultado de aplicar la función al primer elemento original (‘x’), y cuya cola (el resto de la lista) es hacer ‘map’ con la misma función a la cola de la lista original (‘xs’).

    Nota: El operador ‘:’ añade un elemento a la cabeza de una lista.

  4. Las que siguen son soluciones en Python de los ejemplos que planteaste, algunas usan clausuras, otros puede que no :)

    El ejemplo simple:

    def f(a):
    return [2*x for x in a]

    El ejemplo de people:

    [p.name for p in people if p.age > 18]

    Problema 1:

    a = [x.upper() for x in a]

    Problema 2:

    [«, «.join([y,x]) for [x,y] in a]

    Problema 3:

    Este no quedó tan bien como los otros, pero te dejo mi interacción en REPL de Python:

    >>> p1 = Persona()
    >>> p1.age = 19
    >>> p1.name = «Juan Hewr»
    >>> p2 = Persona()
    >>> p2.age = 2
    >>> p2.name = «Baby Jones»
    >>> p3 = Persona()
    >>> p3.age = 30
    >>> p3.name = «Sussan Ahoria»
    >>> people = [p1, p2, p3]

    >>> s = 0.0
    >>> adults = [p for p in people if p.age > 18]
    >>> for p in adults:
    print p.name
    s += p.age
    Juan Hewr
    Sussan Ahoria
    >>> print s/len(adults)
    24.5

    Problema 4:

    max(cadenas)

  5. Muchas gracias Juanjo por las soluciones.

    Con respecto al problema 4, un detalle:

    En Ruby también puedes obtener el máximo elemento de un arreglo ‘a’ usando

    a.max

    Sin embargo, la idea era mostrar como aplicar una función al estilo del ‘fold’ de los lenguajes funcionales. ¿Podrías reescribir la solución en esos términos?

  6. Primero una aclaración de algo que me pasé por alto: max(a) en Python cuando a es una lista de strings no da el string más largo sino el que está más al final en un ordenamiento creciente alfabético estando las minúsculas antes que las mayúsculas. Es así también en ruby?

    >>> max(«hola», «juanjo»)
    ‘juanjo’
    >>> max(«hola», «Juanjo»)
    ‘hola’
    >>> max(«a», «A»)
    ‘a’
    >>> max(«a», «AAAAAAAAAAAAAAAAAAAAAa»)
    ‘a’
    >>> max(«a», «AAAAAAAAAAAAAAAAAAAAA», «a»)
    ‘a’
    >>> max(«AAAAAAAAAAAAAAAAAAAAA», «a»)
    ‘a’
    >>> max(«b», «a»)
    ‘b’
    >>> max(«B», «a»)
    ‘a’

  7. Así es, Juanjo. Yo también me equivoqué. max, aplicado a strings devuelve el máximo según el orden lexicográfico, y no según la longitud.

    Me sigue quedando la duda. ¿Hay algo similar a ‘inject’ o ‘fold’ en Python?

  8. Ahora si vamos a intentar resolver el ejemplo 4 en Python:

    Mmm inject está bueno para resolver ese problema.. pero creo que no tenemos algo así en Python. Intentemos de todas formas:

    Tenemos una lista de strings:

    a = [«hola», «juAnjO», «Argentina», «8»]

    Una función ‘criterio’:

    def longer(a, b):
    if len(a) > len(b):
    return a
    else:
    return b

    And this magic functions called ‘mas’:

    def mas(l, c, v):
    if l:
    return mas(l[1:], c, c(l[0], v))
    else:
    return v

    Que nos va a permitir hacer algo como esto:

    >>> mas(a, longer, «»)
    ‘Argentina’

    Mirando desde más cerca.. esta función ‘mas’, es muy parecida a ‘inject’, salvo por el nombre :)

    >>> inject = mas
    >>> numeros = [1,2,3,4,5,6]
    >>> def suma(a, b):
    return a + b
    >>> inject(numeros, suma, 0)
    21

    Mmm parece que funciona, comprobemoslo:

    >>> sum(numeros)
    21

    Si!

    Nos leemos!

  9. como puedo sumar los elementos de dos listas que se ven de esta manera:
    lista=[1,2,3,4,5]
    lista2=[7,7,5,3,1]
    donde la lista 3 sera el resultado de el primer elemento de la lista uno y el último de la lista 2, y el ultimo elemento de la lista uno con el primer elemento de la lista 2 quedando asi
    list3[2,5,8,11,12] espero sus respuestas desde ya GRACIAS!!!!!!!!!!!!

Deja una respuesta

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