Pensamiento funcional: Recursos funcionales en Groovy, Parte 1

Tesoros que se esconden en Groovy

Con el tiempo, los lenguajes y los tiempos de ejecución han manejado más y más detalles mundanos por nosotros. Los lenguajes funcionales ejemplifican esta tendencia, pero los lenguajes dinámicos modernos también han incorporado muchos recursos funcionales para hacer que las vidas de los desarrolladores sean mucho más fáciles. Esta entrega investiga algunos de los recursos funcionales que ya se esconden en Groovy, mostrando cómo la recursión oculta estado y cómo construir listas perezosas.

Neal Ford, Software Architect / Meme Wrangler, ThoughtWorks Inc.

Photo of Neal FordNeal Ford es arquitecto de software y Meme Wrangler en ThoughtWorks, una consultora global en TI. También diseña y desarrolla aplicaciones, materiales instructivos, artículos para revistas, courseware, y presentaciones en video/DVD, y es el autor o editor de libros que abarcan una variedad de tecnologías, incluyendo las más recientes The Productive Programmer. Él se enfoca en el diseño y construcción de aplicaciones corporativas a gran escala. También es un conferencista aclamado internacionalmente en conferencias de desarrolladores en todo el mundo. Visite su sitio web.



19-03-2012

Sobre esta serie

Esta serie tiene como objetivo reorientar su perspectiva hacia una forma de pensar funcional, ayudándole a observar problemas comunes de nuevas formas y a encontrar formas para mejorar su codificación del día a día. Este explora conceptos de programación funcional, infraestructuras que permiten programación funcional dentro del lenguaje Java™ lenguajes de programación funcional que se ejecutan en la JVM, y algunas indicaciones de cara al futuro sobre diseño de lenguaje. Esta serie está articulada para desarrolladores que conozcan Java y cómo funcionan sus abstracciones, pero que tengan poca o ninguna experiencia usando un lenguaje funcional.

Confesión: Nunca quise trabajar de nuevo en un lenguaje donde no se recolectara la basura. Ya pagué mis deudas en lenguajes como C++ por muchos años y no deseo rendirme a las conveniencias de los lenguajes modernos. Esa es la historia sobre cómo progresa el desarrollo de software. Construimos capas de abstracción para que administran (y oculten) detalles mundanos. A medida que las capacidades de los computadores han crecido, hemos descargado más tareas a lenguajes y tiempos de ejecución. Hace tan solo una década los desarrolladores rehuían de los lenguajes interpretados por ser demasiado lentos para aplicaciones de producción, pero hoy son comunes. Muchos de los recursos de los lenguajes funcionales eran prohibitivamente lentos, hace una década, pero actualmente tienen total sentido porque optimizan el tiempo y el esfuerzo del desarrollador.

Muchos de los recursos que cubro en esta serie de artículos muestran cómo los lenguajes e infraestructuras funcionales manejan detalles mundanos. No obstante, usted no necesita ir a un lenguaje funcional para beneficiarse de las construcciones funcionales. En esta entrega y en la siguiente, mostraré cómo algo de programación funcional ya se ha filtrado a Groovy.

Listas funcionales de Groovy

Groovy aumenta significativamente las bibliotecas de colección Java, incluyendo construcciones funcionales. El primer favor que Groovy le hace es proporcionarle diferentes perspectivas de listas, lo cual parece trivial el principio, pero que ofrece algunos beneficios interesantes.

Observando las listas de forma diferente

Si su formación principalmente es en C o lenguajes tipo C (incluyendo Java), probablemente usted conceptualizará las listas como colecciones indexadas. Esta perspectiva facilita la iteración sobre una colección, incluso si usted no usa explícitamente el índice, como se muestra en el código Groovy del Listado 1:

Listado 1. Lista transversal usando índices (ocultos)
def perfectNumbers = [6, 28, 496, 8128]

def iterateList(listOfNums) {
  listOfNums.each { n ->
    println "${n}"
  }
}

iterateList(perfectNumbers)

Groovy también incluye un iterador eachWithIndex() , el cual proporciona el índice como parámetro para el bloque de código para casos en los que se necesita acceso explícito. Aunque no utilizo un índice en el método iteraterList() en el Listado 1, todavía considero que es una colección ordenada de ranuras, como se muestra en la Figura 1:

Figura 1. Listas i ranuras indexadas
Listas i ranuras indexadas

Muchos lenguajes funcionales tienen una perspectiva ligeramente funcional sobre las listas, y afortunadamente Groovy comparte esta perspectiva. En lugar de pensar en una lista como ranuras indexadas, piense en ellas como la combinación del primer elemento de la lista (la cabeza) más el resto de la lista (la cola), como se muestra en la Figura 2:

Figura 2. Una lista como su cabeza y su cola
Una lista como su cabeza y su cola

Pensar en una lista como una cabeza y su cola me permite iterar la usando la recursión, como se muestra en el Listado 2:

Listado 2. Lista transversal usando recursión
def recurseList(listOfNums) {
  if (listOfNums.size == 0) return;
    println "${listOfNums.head()}"
    recurseList(listOfNums.tail())
}

recurseList(perfectNumbers)

En el método recurseList() del Listado 2, primero verifico para ver si la lista que es pasada como el parámetro no tiene elementos en ella. Si ese es el caso, entonces he terminado y puedo retornar. Si no lo es, imprimo el primer elemento de la lista disponible mediante el método head() de Groovy y luego llamo de forma recursiva al método recurseList() sobre el resto de la lista.

La recursión tiene límites técnicos incorporados a la plataforma (vea Recursos), así que no es la panacea. Pero debe ser seguro para listas que contengan un número pequeño de elementos. Estoy más interesado en investigar el impacto de la estructura del código, como anticipación para el día cuando los límites se ablanden o desaparezcan. Dadas las insuficiencias, el beneficio de la versión recursiva puede no ser obvio de inmediato. Para que lo sea un poco más, considere el problema de filtrar una lista. En el Listado 3, mostré un ejemplo de un método de filtrado que acepta una lista y un predicado (una prueba booleana) para determinar si un elemento pertenece a la lista:

Listado 3. Filtrado imperativo con Groovy
def filter(list, p) {
  def new_list = []
  list.each { i -> 
    if (p(i))
      new_list << i
  }
  new_list
}

modBy2 = { n -> n % 2 == 0}

l = filter(1..20, modBy2)

El código del Listado 3 es simple: creo una variable contenedora para los elementos que deseo conservar, itero la lista, verifico cada elemento con el predicado de inclusión y retorno la lista de elementos filtrados. Cuando llamo filter(), suministro un bloque de código que especifica los criterios de filtrado.

Considere una implementación recursiva del método de filtrado del Listado 3, mostrado en el Listado 4:

Listado 4. Filtrado recursivo con Groovy
def filter(list, p) {
  if (list.size() == 0) return list
  if (p(list.head()))
    [] + list.head() + filter(list.tail(), p)
  else 
    filter(list.tail(), p)
}

l = filter(1..20, {n-> n % 2 == 0})

En el método filter() del Listado 4, primero verifiqué el tamaño de la lista pasada y retorné si no tenía elementos. En caso contrario, revisé la cabeza de la lista contra mi predicado de filtro; si pasaba, la añadía a la lista (con una lista inicial vacía para garantizar que siempre retorne el tipo correcto); en caso contrario, filtraba la cola recursivamente.

La diferencia entre el Listado 3 y el Listado 4 resalta una pregunta importante: ¿quién está pendiente del estado? En la versión imperativa, Yo lo estoy. Yo debo crear una nueva variable llamada new_list, Yo debo añadirle cosas y Yo debo retornarla cuando haya terminado. En la versión recursiva, el lenguaje administra el valor de retorno, construyendo sobre la pila como el retorno recursivo para cada invocación de método. Observe que cada ruta de salida del método filter() en el Listado 4 es un llamado de retorno, el cual construye el valor intermedio de la pila.

Aunque no es una mejora de vida dramática como la recolección de basura, esto ilustra una tendencia importante en los lenguajes de programación: la descarga de partes móviles. Si nunca se me permite tocar los resultados inmediatos de la lista, no puedo introducir errores en la forma en que interactúo con ella.

Este cambio de perspectiva con respecto a las listas le permite explorar otros aspectos, como el tamaño y el ámbito de una lista

Listas perezosas en Groovy

Uno de los recursos comunes de los lenguajes funcionales es la lista perezosa: una lista cuyo contenido solo es generado a medida que usted lo necesita. La lista perezosa le permite diferir la inicialización de recursos costosos hasta que usted los necesite de forma absoluta. También pueden permitir la creación de secuencias infinitas: listas que no tengan límite superior. Si no se le solicita que diga de antemano qué tan grande puede ser la lista, puede dejar que sea tan grande como necesite ser.

Primero, le mostraré un ejemplo del uso de una lista perezosa en Groovy en el Listado 5, y luego le mostraré la implementación:

Listado 5. Usando listas perezosas en Groovy
def prepend(val, closure) { new LazyList(val, closure) }

def integers(n) { prepend(n, { integers(n + 1) }) }

@Test
public void lazy_list_acts_like_a_list() {
  def naturalNumbers = integers(1)
  assertEquals('1 2 3 4 5 6 7 8 9 10', 
      naturalNumbers.getHead(10).join(' '))
  def evenNumbers = naturalNumbers.filter { it % 2 == 0 }
  assertEquals('2 4 6 8 10 12 14 16 18 20', 
      evenNumbers.getHead(10).join(' '))
}

El primer método en el Listado 5, prepend(), crea una nueva LazyList, permitiéndole anteponer los valores. El siguiente método, integers(), retorna una lista de enteros usando el método prepend() . Los dos parámetros que envío al método prepend() son el valor inicial de la lista y un bloque de código que incluye código para generar el siguiente valor. El método integers() actúa como una fábrica y retorna la lista perezosa de enteros con un valor en el frente y una forma para calcular valores adicionales en la cola.

Para recuperar valores de la lista, llamo al método getHead() , el cual retorna el número de argumento de valores desde la parte superior de la lista. En el Listado 5, naturalNumbers es una secuencia perezosa de todos los enteros. Para obtener algunos de ellos, llamo al método getHead() , especificando cuántos enteros deseo. Como lo indica la afirmación, recibo una lista de los primeros 10 números naturales. Usando el método filter() , recupero una lista perezosa de números pares y llamo al método getHead() para capturar los primeros 10 números pares.

La implementación de LazyList aparece en el Listado 6:

Listado 6. LazyList implementación
class LazyList {
  private head, tail

  LazyList(head, tail) {
    this.head = head;
    this.tail = tail
  }

  def LazyList getTail() { tail ? tail() : null }

  def List getHead(n) {
    def valuesFromHead = [];
    def current = this
    n.times {
      valuesFromHead << current.head
      current = current.tail
    }
    valuesFromHead
  }

  def LazyList filter(Closure p) {
    if (p(head))
      p.owner.prepend(head, { getTail().filter(p) })
    else
      getTail().filter(p)
  }
}

Una lista perezosa contiene cabeza y cola, especificadas por el constructor. El método getTail() garantiza que lacola no sea nula y la ejecuta. El método getHead() reúne los elementos que deseo retornar, uno a la vez, tomando el elemento existente de la cabeza de la lista y solicitando a la cola que genere un nuevo valor. El llamado a n.times {} realiza esta operación para el número de elementos solicitados, y el método retorna los valores cosechados.

El método filter() en el Listado 5 usa el mismo enfoque recursivo que el Listado 4 pero lo implementa como parte de la lista en lugar de una función autónoma.

La lista perezosa existe en Java (vea Recursos) pero son mucho más fáciles de implementar en lenguajes que tengan recursos funcionales. Las listas perezosas funcionan muy bien en situaciones donde generar recursos es costoso, como obteniendo listas de números perfectos.

Lista perezosa de números perfectos

Si usted ha estado siguiendo esta serie de artículos, usted conoce mi código de conejillo de indias favorito, encontrar números perfectos (vea ""Pensando funcionalmente, Parte 1"). Uno de los inconvenientes de todas las implementaciones hasta ahora ha sido la necesidad de especificar el número para clasificación. En lugar de ello, deseo una versión que retorne una lista perezosa de números perfectos. Para esa meta, he escrito un buscador de números perfectos bastante funcional y muy compacto que soporta listas perezosas, mostrado en el Listado 7:

Listado 7. Versión recortada de un clasificador de números, incluyendo el método nextPerfectNumberFrom() .
class NumberClassifier {
  static def factorsOf(number) {
      (1..number).findAll { i -> number % i == 0 }
  }

  static def isPerfect(number) {
      factorsOf(number).inject(0, {i, j -> i + j}) == 2 * number
  }

  static def nextPerfectNumberFrom(n) {
    while (! isPerfect(++n)) ;
    n
  }
}

Si el código en los métodos factorsOf() y isPerfect() parece oscuro, usted puede ver la derivación de estos métodos en la última entrega. El nuevo método, nextPerfectNumber(), usa el método isPerfect() para encontrar el siguiente número perfecto después de un número pasado como el parámetro. Este llamado de método tardará bastante en ejecutarse incluso para valores pequeños (especialmente dado lo poco optimizado es el código); simplemente no hay tantos números perfectos.

Usando esta nueva versión de NumberClassifier, puedo crear una lista de números perfectos, como se muestra en el Listado 8:

Listado 8. Lista inicializada perezosamente de números perfectos
def perfectNumbers(n) { 
  prepend(n, 
    { perfectNumbers(nextPerfectNumberFrom(n)) }) };

@Test
public void infinite_perfect_number_sequence() {
  def perfectNumbers = perfectNumbers(nextPerfectNumberFrom(1))
  assertEquals([6, 28, 496], perfectNumbers.getHead(3))
}

Usando el método prepend() que definí en el Listado 5, construí una lista de números perfectos con el valor inicial como la cabeza y el bloque de cierre que sabe cómo calcular el siguiente número perfecto como la cola. Yo inicio mi lista con el primer número perfecto después de 1 (usando una importación estática de manera que pueda llamar a mi método NumberClassifier.nextPerfectNumberFrom() más fácilmente), y luego le pido a mi lista retornar los tres primeros números perfectos.

Calcular nuevos números perfectos es costoso, por lo que prefiero hacerlo lo menos posible. Al construir la lista perezosa puedo diferir los cálculos hasta el momento óptimo.

Es más difícil pensar en secuencias infinitas si su abstracción de "lista" es "casillas numeradas". Pensar en una lista como un "primer elemento" y en el "resto de la lista" le anima a pensar en los elementos en la lista en lugar de la estructura, lo cual a su vez le permite pensar sobre cosas como listas perezosas.


Conclusión

Una de las formas en las que los desarrolladores hacen saltos gigantes en la productividad es construyendo abstracciones efectivas para ocultar detalles. No podríamos llegar a ningún lado si todavía estuviéramos codificando con unos y ceros. Uno de los aspectos atractivos de los lenguajes funcionales es el intento por abstraer más detalles de los desarrolladores. Lenguajes dinámicos modernos en la JVM ya le ofrecen muchos de estos recursos. En esta entrega mostré cómo cambiar su perspectiva un poco sobre cómo las listas se construyen permitiendo al lenguaje manejar estados durante la iteración. También cuando usted piensa en las listas como "cabeza" y "cola", esto le permite construir cosas como listas perezosas y secuencias infinitas.

En la siguiente entrega, usted verá cómo la meta programación Groovy puede hacer que sus programas sean más funcionales — y le permite aumentar bibliotecas funcionales para hacer que funcionen mejor en Groovy.

Recursos

Aprender

Obtener los productos y tecnologías

Comentar

  • Participe en la comunidad developerWorks. Conéctese con otros usuarios developerWorks mientras explora los blogs, foros, grupos y wikis dirigidos a desarrolladores.

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=tecnologia Java
ArticleID=806145
ArticleTitle=Pensamiento funcional: Recursos funcionales en Groovy, Parte 1
publish-date=03192012