Durante los últimos meses he estado trabajando en un proyecto utilizando Scala como lenguaje de programación principal. La mezcla de programación orientada a objectos y programación funcional que permite Scala resulta interesante, pero cada vez estoy más convencido de que la verdadera potencia del mismo se consigue cuando se aplica un estilo puramente funcional. Al fin y al cabo, si únicamente queremos un lenguaje con algunas características funcionales que hagan nuestro código un poco más claro y limpio, hay otras opciones que nos dan eso (véanse la última versión de Java o Kotlin), sin las complejidades con las que hay que lidiar en Scala.

Supongo que escribiré en el futuro un artículo más elaborado sobre el tema, pero dejadme que hoy me centre en algo que llevo viendo bastante cuando examino código en Scala, casi siempre cuando éste ha sido escrito por desarrolladoras que tienen amplia experiencia en algún lenguaje orientado a objetos (como Java) y empiezan a escribir código en Scala: cómo hacer inyección de dependencias.

Empezaré proponiendo un escenario muy básico para analizar el problema y algunas de las soluciones que proporciona el lenguaje. Supongamos que queremos crear un servicio de informes que nos devuelva los balances del último mes de todas las cuentas bancarias abiertas por un cliente. Nuestros clientes están relacionados con todas cuentas que tienen abiertas (serían nuestro Aggregate Root si hablásemos de Domain Driven Design) y se identifican por una secuencia alfanumérica de longitud diez. Dichas cuentas, mantienen todos los movimientos ordenados por su fecha de ejecución. Un balance contiene, en definitiva, el número que identifica a una cuenta, la fecha hasta donde el balance ha contabilizado y una cantidad de dinero.

De esta forma definimos nuestro servicios con la siguiente operación:

type Id = String

case class Balance(acNo: String, balanceAt: Date, amount: Money)

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int): List[Balance]
   ...
}

Los clientes son accessibles dados sus identificadores vía un repositorio.

trait CustomerRepository {
   def retrieve(customerId: Id): Option[Customer]
   ...
}

Nuestra implementación del servicio de informes necesitará una instancia válida del repositorio de clientes para poder tener acceso al cliente solicitado y hacer el cáculo de los balances de sus cuentas. Hasta el momento, lo que he visto es que un porcentaje bastante algo de desarrolladores que vienen de otros lenguajes como Java harán algo así para implementar dicho servicio:

class ReportingInterpreter(repo: CustomerRepository) implements Reporting {
   override def monthlyBalance(customerId: Id, year: Int, month: Int): List[Balance] = //do something here
   ...
}

A muchos desarrolladores acostumbrados al paradigma orientado a objetos, esta implementación le parecerá bastante natural (y acertada). Sin embargo, desde el punto de vista de la programación funcional, tiene el principal inconveniente de que en ninguna parte se hace explícito que la operación monthlyBalance necesita de un repositorio de clientes. Alguien que utilice esta función nunca sabrá que, sin un repositorio de clientes, dicha operación no podrá ser ejecutada. El repositorio de clientes es parte del contexto de ejecución de la clase ReportingInterpreter, a quien se lo estamos inyectando a través de su constructor.

Esta situación puede mejorarse un poco aplicando un diseño algo más funcional a nuestra función monthlyBalance. ¿Por qué no pasarle explícitamente el repositorio de clientes como un párametro de entrada a dicha función? De esta forma tendríamos:

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int, repo: CustomerRepository): List[Balance]
   ...
}

Ahora ya hemos hecha explícita la dependencia de la función monthlyBalance con el repositorio de clientes y podemos decir que nuestro diseño es más funcional. Sin embargo, todavía hay algunas desventajas con esta solución. Una de ellas es que añadimos un parámetro extra a nuestra función, lo que hace que ésta sea más difícil de ser llamada. Si nuestro módulo Reporting crece y ofrece muchas otras funciones que también dependen de CustomerRepository (y es probable que dichas funciones también dependan de este repositorio), siempre tendremos que pasar éste como último parámetro en todas nuestras llamadas a funciones del módulo Reporting. Esto resulta especialmente engorroso si queremos componer funciones de dicho módulo para ofrecer nuevas funciones.

¿Cómo podemos mejorar esta situación? Teniendo en cuenta que intentamos utilizar un estilo de programación cada vez más funcional, podemos hacer uso de la idea de que en un lenguaje funcional las funciones son objetos de primera clase que pueden ser utilizados como cualquier otro tipo de objeto. Por ejemplo, pueden ser pasados como parámetros de entrada a otras funciones (creando de esta forma funciones de orden superior). Otra cosa que a veces olvidamos quienes venimos de lenguajes orientados a objetos y no tenemos demasiada experiencia en programación funcional, es que las funciones pueden devolver otras funciones. ¿Qué pasaría si hacemos que nuestra función monthlyBalance devolviera otra función que toma como único parámetro de entrada un repositorio de clientes y devuelve los balances mensuales de las cuentas de un cliente? Vemos cómo podríamos definir esto en Scala:

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int): CustomerRepository => List[Balance]
   ...
}

Ahora podemos utilizar nuestra función para obtener el balance mensual sin tener que proporcionar el repositorio de clientes:

val reporter: Reporting = ...
val balance = reporter.monthlyBalance("ad82-fdad9-2429-a23bc-2b9c0", 2017, 6)

Por supuesto, lo que obtenemos no es el balance todavía pues, para obtener el balance, necesitamos proporcionar el repositorio de clientes, sino una función capaz de devolver los balances cuando se le pasa un repositorio adecuado. De esta forma, hemos conseguido obtener una definición de nuestro balance sin tener que resolver la dependencia todavía y, de momento, no hemos ejecutado nada aún. Estamos aplicando un principio bastante interesante y repetido en programación funcional: define pronto, ejecuta tarde.

Supongamos que del balance queremos obtener la cantidad total, podemos aplicar composición de funciones para obtenerlo:

val amount = reporter.monthlyBalance("ad82-fdad9-2429-a23bc-2b9c0", 2017, 6) _ andThen (map(_.amount))

Y, de nuevo, todavía no hemos proporcionado el repositorio de clientes ni hemos ejectuado nada. Cuando tengamos disponible una instancia válida de CustomerRepository, podemos hacer:

val repo: CustomerRepository = ...

print("this is the monthly balance amount: " + amount(repo)) //amount is a function

Por último, podemos valernos de una herramienta común en programación funcional: las mónadas. Más concretamente, la mónada Reader resuelve nuestro problema de una forma puramente funcional y muy elegante.

En primer lugar, debemos cambiar la definición de nuestra función para que devuelva una instancia de Reader en lugar de una función (podemos decir que, en cierta forma, Reader encapsula una función que, a partir de un contexto que se le pasa como parámetro de entrada, nos devuelve un resultado). En este ejemplo, utilizaré la implementación de Reader proporcionada en por la librería scalaz.

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int): Reader[CustomerRepository, List[Balance]]
   ...
}

Ahora, nuestro método monthlyBalance nos devolverá una instancia de Reader. ¿Qué podemos hacer con ella? Muchas cosas, como, por ejemplo, extraer las cantidades de los balances de una forma más limpia que como hicimos anteriormente gracias a la función map:

val amount = reporter.monthlyBalance("ad82-fdad9-2429-a23bc-2b9c0", 2017, 6).map(b => b.map(_.amount))

Como se trata de una mónada, podemos componer instancias de Reader dentro de una estructura for comprehension, por ejemplo, para obtener la evolución de los balances en los primeros tres meses del año:

val thisYear = ...
val firstQuarterReport = for {
   balances1 <- monthlyBalance(customerId, thisYear, 1)
   balances2 <- monthlyBalance(customerId, thisYear, 2)

   balances3 <- monthlyBalance(customerId, thisYear, 3)
} yield balances1 ++ balances2 ++ balances3

De nuevo, todavía no hemos ejecutado nada, solamente hemos proporcionado una definición de los balances del primer trimestre. ¿Cómo resolvemos la dependencia para tener el resultado final? Para ello, utilizamos el método run proporcionado por la mónada Reader:

val balancesQ1 = firstQuarterReport.run(customerRepository)

Es justo en ese momento cuando se ejecuta nuestro código y todo queda resuelto.

Hemos visto tres alternativas funcionales a hacer inyección de dependencias mediante el constructor de clase:

  • Proporcionar la dependencia a través de un parámetro extra añadido a nuestra función
  • Dividir la lista de parámetros para poder hacer currying, obteniendo una función a que devuelve otra función
  • Utilizar la mónada Reader como tipo de retorno

Todas ellas, alternativas mucho más funcionales que, además, nos permiten diferir la resolución de nuestra dependencia hasta justo el momento en el que resulta imprescindible.


El pasado sábado tuve la oportunidad de organizar un code retreat con la comunidad Scala de Madrid donde, además, hice de facilitador. La idea de un code retreat es tomarse un tiempo para hacer una práctica deliberada y mejorar nuestras habilidades de desarrollo de software en un entorno sin el estrés cotidiano que tenemos en nuestro trabajo. El principal ingrediente para conseguir esto es que, durante el code retreat, no existe ningún objetivo excepto el probar cosas nuevas, practicar y aprender, sin ninguna necesidad de tener algo “terminado” al final del día.

El code retreat en sí está organizado de la siguiente forma:
  • Se trabaja en un único problema durante todo el día (normalmente, el code retreat tiene un formato de ocho horas con una pausa para comer)
  • El trabajo se organiza en sesiones de 45 minutos seguidos de una discusión de unos quince minutos sobre cómo ha ido la sesión
  • Se trabaja siempre en parejas (esto es de suma importancia ya que facilitará que aprendamos unos de otros y hará más difícil que alguien se quede bloqueado)
  • Antes de cada sesión se organizan nuevas parejas
  • Al final del code retreat, se hace una sesión retrospectiva general sobre todo el día
En sí, el formato del code retreat es sumamente interesante y ha sido puesto en práctica muchas veces, habiendo incluso un día global del code retreat, en el que se celebra este evento simultáneamente en diferentes partes del mundo (participé en unos de ellos cuando estaba en Berlín y me resultó de lo más interesante). Ciertamente, recomiendo participar y, si puedes, incluso, organizar alguno, para lo que puedes tomar como punto de partida la información disponible en coderetreat.org.

En nuestro caso, decidimos trabajar sobre el Juego de la Vida, que es posiblemente el problema que más se ha utilizado en un code retreat. Si te gusta la programación y nunca lo has probado, te recomiendo que intentes hacer tu propia implementación del juego de la vida. Una vez tienes cierta familiaridad con el problema, te puede servir también para practicar cuando quieres aprender un lenguaje de programación nuevo, por ejemplo.

Aquí van algunas de mis notas finales una vez el code retreat hubo terminado.

Ten en cuenta el nivel de programación de los asistentes

Es recomendable que, antes de empezar a trabajar en el problema por primera vez, hagas una pequeña encuesta entre los asistentes y sepas qué nivel de programación tienen. Esta información me resultó útil para emparejar en la primera sesión a gente con más conocimiento de programación con gente que está empezando. Esto resulta de ayuda para que nadie se quede atascado en cosas triviales al principio, lo que puede desanimar bastante.

Conforme el día fue transcurriendo, fue también un acierto el emparejar a gente con un nivel de programación más alto, sobre todo en las últimas sesiones. Esto hizo que algunas parejas tuvieran tiempo de implementar soluciones más creativas y sofisticadas, lo que elevó el nivel de la conversación después de cada sesión de trabajo. Cuando los asistentes avanzados explicaban sus soluciones, todos aprendimos bastante.

No te preocupes si no todo el mundo consigue terminar el problema

Como ya dije, éste no es el objetivo de las sesiones y, de hecho, lo más normal es que no se termine el problema, sobre todo, durante las primeras sesiones, en las que los asistentes están familiarizándose con el mismo. Mientras la gente trabaja en el problema, lo normal es que estés paseando y viendo qué hacen, dando alguna explicación si alguien pregunta o incluso ayuda.

Pero no te apresures en dar demasiadas pistas ni ideas sobre cómo hacer el problema porque influirás demasiado en cómo trabajan las parejas. Llevado al extremo, podrías encontrarte con que todas las soluciones que se discuten tras la sesión se parecen demasiado entre sí porque, básicamente, se parecen a tu propia solución, así que no lo hagas.

No te cortes al introducir variantes

Existe un catálogo amplio de variantes que puedes utilizar en cada sesión y normalmente tiene bastante sentido utilizarlo como guía, ya que estas variantes están muy bien pensadas para conseguir que la gente interiorice mejor algunos conceptos. Pero no es necesario que te ciñas únicamente a ellas. De hecho, si no lo haces, tendrás la satisfacción de que le has dado a los asistentes algo más especial si introduces algo que se salga de las variantes más comunes.

En nuestro caso, como queríamos centrarnos en utilizar Scala (normalmente en un code retreat se puede utilizar cualquier lenguaje de programación que los asistentes quieran, pero éste era un evento organizado para la comunidad Scala por lo queríamos utilizar únicamente Scala para resolver el problema) decidimos usar una variante que consistía en resolver el problema haciendo el código puramente funcional. Esto resultó un reto para muchos de los asistentes y, al mismo tiempo, les permitió explorar algunas de las posibilidades del lenguaje que normalmente no utilizaban en su práctica diaria.

Además, en nuestra última sesión, decidimos improvisar respecto a lo que teníamos originalmente preparado, ya que había habido asistentes que tuvieron dificultades y no se terminaban de encontrar cómodos haciendo TDD (esto ocurrió, sobre todo, en las primeras sesiones). Así que decidimos probar en la última sesión lo siguiente:
  • Durante los quince primeros minutos, escribir todo el código posible para terminar el problema sin hacer ningún test
  • Cambiar de parejas: uno continuaría trabajando su propio código y otro trabajaría con una nueva pareja sobre un código que no conoce
  • Terminar el problema, esta vez haciendo TDD
La idea es que, en los primeros quince minutos, generemos algo de legacy code y ver cómo la gente reacciona al cambiarlo sin tener la red de seguridad de los tests. Tengo que decir que, para cuando hicimos esta sesión, la gente ya estaba más cómoda con TDD e incluso protestaron un poco al sugerirles que escribiese la máxima cantidad de código que pudieran sin ningún test.

Al final, cuando tuvimos la discusión después de esta sesión, el tema más repetido por quienes llegaron a un código nuevo fue lo difícil que había sido modificar el código y continuar con el problema sin tener ningún test, incluso teniendo la ayuda de otra persona que sí conocía dicho código al lado.

Así es como resultó ser nuestro code retreat:
  • Introducción al formato de code retreat y al problema sobre el que trabajaríamos (25 minutos)
  • Primera sesión: resolver el problema sin ningún tipo de restricción (una hora)
  • Segunda sesión: no usar estructuras de control condicionales ni pattern matching y mantener todos los métodos/funciones lo más reducidos posibles (no más de 3 líneas de código) (una hora)
  • Pausa para almorzar
  • Tercera sesión: hacer el código puramente funcional (una hora)
  • Cuarta sesión: trabajo sobre código hecho por otros y sin tests (una hora)
  • Discusión final acerca de todo lo que hicimos durante el día, lo que habíamos aprendido y lo que nos había sorprendido más (45 minutos)
En resumen, fue una experiencia de lo más gratificante y enriquecedora. Al final, creo que todos aprendimos algo y pudimos conocer a gente muy interesante. Espero poder repetir en el futuro, ya sea como asistente o facilitador. En cualquier caso, será divertido. ¡Anímate tú también!