Cláusula With en Elixir

Lo bueno de un lenguaje que está principalmente guiado por la comunidad, es que va evolucionando según las necesidades que se van encontrando sus usuarios. Un ejemplo es el que nos encontramos con la cláusula with de Elixir, que fue introducido en la versión 1.2 del lenguaje, y mejorado en la 1.3 para incluir la cláusula else.

El problema

Primero vamos a ver el problema que soluciona. Supongamos que tenemos una estructura del tipo %User{}. La estructura tendrá los campos típicos que tiene un usuario de una aplicación.

defmodule Example.With.User do
  defstruct name: "", age: 0, mail: "", accept_privacy: false, mail_sucriber: false
end

Ahora vamos a definir una serie de validaciones que realizará nuestro programa para gestionar este usuario. Algunas de las validaciones serán comprobar que es mayor de 18, que el nombre es válido, o que ha aceptado las políticas de privacidad.

defmodule Example.With.Program1 do

  alias Example.With.User

  def verify(user = %User{}) do
    users
    |> validate_name
    |> validate_age
    |> accepted_privacy
  end

  defp validate_name(user = %User{ name: n}) when length(n) == 0 do
    {:error, "Name is mandatory"}
  end

  defp validate_name(user = %User{}) do
    n = user.name

    case Regex.match?(~r/^[A-Z]\w{5,}/, n) do
      true -> user
      false -> {:error, "Name is not correct"}
    end
  end

  defp validate_age(user = %User{ age: a}) when a >= 18 do
    user
  end

  defp validate_age(%User{ age: a}) when a < 18 do
    {:error, "You are not 18 years old"}
  end

  defp accepted_privacy(%User{ accept_privacy: p}) when p == false do
    {:error, "You have to accept our privacy policy"}
  end

  defp accepted_privacy(user = %User{}) do
    user
  end
end

¿Veis cuál es el problema? El operador pipe |> es un instrumento genial para enlazar funciones. Pero claro, estas funciones tienen que ser consistentes y devolver siempre un mismo tipo de resultado. El problema es que por ejemplo validate_name puede devolver un :error, haciendo que recibamos un error en tiempo de ejecución, ya que una función envía una tupla, pero la siguiente espera recibir una estructura %User{}.

En este caso no nos sirve el pipe, así que en definitiva, necesitamos algo que sirva para concatenar funciones y que permita gestionar los errores. Y para eso entre otras cosas se utiliza with.

With

Como el movimiento se muestra andando, vamos a reescribir el ejemplo anterior, pero usando with. Ahí va:

 def verify(user = %User{}) do
    with %User{} <- validate_name(user),
         %User{} <- validate_age(user),
         %User{} <- accepted_privacy(user)
      do
        user 
      else
        {:error, e}->  IO.puts(e)
     end
  end

Como veis solo he cambiado la función verify, ya que el resto puede seguir igual. Con with iremos ejecutando las diferentes funciones, siempre que el pattern matching se vaya cumpliendo. En el caso de que no se cumpla en algún caso se pasará al bloque else. En este bloque también podemos tener pattern matching, y tener varias cláusulas. Yo solo he puesto un {:error, e}, pero podríamos ponero distintos tipos de error según su mensaje y para realizar distintas operaciones según el error devuelto.



¿Quiéres que te avisemos cuando se publiquen nuevas entradas en el blog?

Suscríbete por correo electrónico o por RSS