sábado, 6 de marzo de 2010

Eliminando boilerplate con HOF (higher order functions)

Esta es una pequeña demostración de cómo las HOF permiten eliminar boilerplate. Para iniciados será algo muy obvio, pero puede ser interesante para los que sólo usan lenguajes sin HOF, como Java. Para empezar, definamos:

Boilerplate: secciones de código que es necesario escribir (o copiar y pegar) en diferentes contextos o aplicaciones, casi sin variaciones. Esto puede ser por limitación de un lennguaje o framework. El boilerplate es indeseable porque agranda el código sin aportar nada nuevo, y como todo código está sujeto a errores y es otra fuente de bugs. Idealmente el boilerplate debe ser abstraído.

Higher order functions: son las funciones que pueden recibir funciones como argumento (y eventualmente invocarlas), o retornar funciones como resultado. Es característica básica de todo lenguaje funcional.

El caso que voy a mostrar viene de una aplicación en Scala con SWT; hice un mini wrapper ad-hoc de SWT que no se justifica detallar acá, pero la situación es: tengo un trait TabView para ciertos componentes visuales.
trait TabView {
  def init: Unit
  def reset: Unit = /* reinicializa el TabView */
  def redisplay: Unit = /* despliega el contenido del TabView */
  // etc...
}
El método init debe ser implementado por las vistas concretas, definiendo el contenido visual. En algunas vistas tenía la necesidad de hacer cambios en el contenido visual, pero reflejar los cambios en pantalla no era algo tan directo: lo resolví implementando los métodos reset y redisplay en el trait, y haciendo los cambios de esta manera:
class MyView extends TabView {
  def init: Unit = /* definir contenido visual en base a un modelo */
 
  def changeStuff: Unit = {
    reset
    /* hacer cambios en el modelo */
    init
    redisplay
  }
}
En otras palabras, cada vez que necesitaba cambiar alguna cosa en el modelo que debería reflejarse en la vista, debía antes invocar a reset, y después a init y a redisplay. Eso que siempre se hace de la misma manera y que lleva más de una línea de código, es boilerplate.

Usando HOF podemos resolver esto aunque el código relevante del cambio esté en el medio del código reutilizable. Lo hacemos enviando nuestro código relevante como argumento a un método que sabe cómo renovar la vista, asi nos olvidamos de los pasos necesarios. Este es el método que agregué al trait:
  protected def doWithRefresh(task: => Unit): Unit = {
    reset
    task
    init
    redisplay
  }
Y así es como lo invoco:
  def changeStuff: Unit = doWithRefresh {
    /* hacer cambios en el modelo */
  }
Por si no queda claro, el bloque entre llaves es el argumento de tipo => Unit que pasamos al método doWithRefresh. Éste método ejecuta los pasos necesarios en el orden correcto, incluyendo la invocación a nuestra función anónima.

Para una solución equivalente en Java, tendríamos que definir una interfaz como la siguiente, y que sea éste el tipo aceptado por doWithRefresh():
  interface Task {
      public void execute();
  }
Luego lo usaríamos con una clase anónima que implementa Task y por consiguiente el método execute():
  void changeStuff() {
    doWithRefresh(new Task() {
      public void execute() {
  /* hacer cambios en el modelo */
      }
     });
  }
Esto es lo más cercano que tenemos en Java a HOFs: estamos obligados a definir al menos una interfaz, y a instanciarla con una clase anónima. En Java no existe la función como ciudadano de primera, sólo existe como método en un objeto: estamos condenados a pasar un objeto como argumento. Y esta solución, como se ve, agrega su propio boilerplate. El patrón es bueno y es el que se usa por ejemplo en Spring JDBC para los RowMappers. Pero, como dicen, la necesidad de un patrón suele indicar una falencia del lenguaje, y este es un buen ejemplo de eso. Donde hay soporte de HOF, este patrón no tiene razón de ser.