Muchas veces me pasa que me pongo a leer sobre programación funcional y me echo a temblar por el hecho de que en ocasiones sus términos pueden ser confusos. Lo curioso es que en el día a día podemos estar usando funciones funcionales sin darnos cuenta y no resultan tan complicadas de entender.
Cuando intentamos irnos a la Wikipedia y ver, por definición, qué es algo que vemos todos los días y tenemos interiorizado podemos echarnos a llorar directamente. Esto justo ocurre con las monads, o mónadas, así que vamos a intentar dar una explicación sencilla sobre qué son, cómo podemos usarlas y qué ventajas nos aportan.
Definición de monads
Cómo hemos comentado, si vamos a la Wikipedia y buscamos que es una mónada seguramente no entendamos nada. Si buscamos por internet nos puede pasar más de lo mismo a no ser que demos con una definición que nos encaje con la forma de trabajar que usemos en nuestros proyectos.
Sin entrar en muchos tecnicismos, podemos decir que las mónadas son un patrón de diseño que nos permite componer funciones con un retorno extendido. Y ahora la pregunta es, ¿qué es un retorno extendido? Podemos usar un retorno extendido para hacer que una operación en nuestro código retorne un valor exitoso o erróneo y nos sirva para continuar o detener la ejecución.
Ejemplo
En nuestro ejemplo, vamos a usar Ruby junto a una gema llamada dry/monads
con la que vamos a poder usar monads. Pongamos un ejemplo sencillo:
#monad_example.rb
require 'dry/monads'
class ValidationExample
include Dry::Monads[:result]
def call(valid = true)
puts 'Some example call'
if valid
puts Success('this is a valid monad')
else
puts Failure('this is a failure monad')
end
end
end
Primero necesitamos requerir la gema dry/monads
que es la que nos va a aportar las mónadas, y después hacemos el include indicando result
porque queremos que las monads nos retornen ese tipo extendido, el de result, que nos devolverá Success
cuando sea correcto y Failure
cuando no. Si miramos en la documentación veremos que hay una llamada Maybe
que puede retornar nil en algún momento. Como nosotros queremos una monadas categóricas, que retornen éxito o fracaso, usamos result
.
Tenemos una clase que llamamos ValidationExample
que recibe un parámetro. Si ese parámetro es true
, la función call
nos mostrará una monad exitosa y si es false
nos mostrará una errónea.
Si enviamos como parámetro true
tendremos una salida como ésta:
Some example call
Success("this is a valid monad")
Como podemos ver, la Monad engloba a nuestro string. Aquí nos es fácil ver que todo ha ido bien y que tenemos un mensaje de confirmación. En un código real nos puede servir para hacer una búsqueda en base de datos y retornar el registro dentro de Success
.
Si en cambio enviamos false
tendremos esto:
Some example call
Failure("this is a failure monad")
Aquí vemos como la monad Failure engloba a un mensaje de error. Esto puede ser muy parecido a lo que veamos en una aplicación real. Si seguimos con el ejemplo de antes, el de recuperar un registro de base de datos, en caso de que no exista podemos retornar un error y cortar la ejecución.
Simpleza al gestionar errores
Y es justo aquí donde creo que nos podemos beneficiar mucho del uso de mónadas. Pongamos por ejemplo que usamos una arquitectura adecuadamente montada y queremos que cuando falle algo a nivel repositorio, ese error se vaya propagando por capas superiores hasta retornar un error al usuario. En este caso, el uso de monadas nos simplifica los retornos.
En el segundo ejemplo podemos ver cómo, en Ruby, usando la instruccion yield
podemos hacer que un método retorne directamente el error hacia arriba.
Para simplificar hemos modularizado el código. Hemos creado una clase FakePresenter, que va a ser la encargada de retornarnos el resultado de la operación:
#fake_presenter.rb
class FakePresenter
def initialize(service = FakeService)
@service = service
end
def call
data = @service.new.call
puts data
end
end
Es muy simple, solo tiene como dependencia inyectada nuestro FakeService que será el que va a tener la lógica de negocio de nuestra aplicación (o script en este caso).
#fake_service.rb
require 'dry/monads'
require 'dry/monads/do'
class FakeService
include Dry::Monads[:result]
include Dry::Monads::Do.for(:call)
def initialize(repository = FakeRepository)
@repository = repository
end
def call(valid = false)
yield @repository.search
end
end
La línea «mágica» de este servicio es la que contiene el yield
. Con yield
nosotros pasamos una función, que en este caso es una función del repositorio. Cuando la validación se compruebe, este yield
revisa la monad y corta la ejecución en caso de Failure
o pasa a la siguiente línea en caso de Success
.
Y por ultimo tenemos un FakeRepository
con un método search
que siempre va a retornar Failure
porque queremos comprobar que, retornando el error en el nivel más bajo, éste llega hasta el presenter.
#fake_repository.rb
require 'dry/monads'
class FakeRepository
class << self
include Dry::Monads[:result]
def search
return Failure('no record')
end
end
end
Si ahora ejecutamos nuestro código, veremos como nos muestra una Failure
:
David.Luque in ~/Documents/secture/ruby-monads > ruby monad_example.rb
Failure("no record")
Como decíamos al principio, las monads son un patrón de diseño que nos ayudan a componer funciones. En este caso, tenemos funciones que puede retornar un error que se va a propagar hacia arriba y será devuelto al usuario. O, incluso, podemos usar ese error para retornar un estado 400 de HTTP a nivel presenter.