Introducción a las monads

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.

que_es _monads

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.

Backend

David Luque Quintana

David Luque Quintana

La mejor forma de aprender a programar es programando. Actualmente trabajo con Ruby y React.
David Luque Quintana

David Luque Quintana

La mejor forma de aprender a programar es programando. Actualmente trabajo con Ruby y React.
2023 ©Secture Labs, S.L. Created by Madrid x Secture