Noticias de Stack Builders

Ideas y notas de nuestro equipo

La fórmula para un Elixir más seguro


Elixir es un lenguaje de programación funcional y dinámico que trabaja sobre la máquina virtual de Erlang. Su objetivo es construir aplicaciones confiables, escalables y de alto rendimiento.

En esta publicación nos vamos a enfocar en los siguientes temas con respecto al lenguaje:

  • Programación funcional: Es un paradigma de programación que permite a los desarrolladores crear código corto y conciso por medio de funcionalidades como guardia y búsqueda de patrones.
  • Tipado dinámico: Es una característica de un lenguaje de programación que evalúa los tipos en tiempo de ejecución.

Si te preguntas: ¿Cómo me va a ayudar Elixir a construir una aplicación confiable y escalable si es un lenguaje dinámico? Podría tener un error de tipos en cualquier momento. No te preocupes, vamos a resolver esta duda. Pero primero, necesitamos hablar sobre algunas herramientas y conceptos.

Mix

Mix es la herramienta de construcción de Elixir que nos permite crear proyectos, administrar dependencias, ejecutar tareas y mucho más. Mix también puede compilar las dependencias y código fuente de nuestro proyecto con los siguientes comandos:

mix deps.compile && mix compile

Ahora que ya sabemos como compilar nuestro código, creemos un módulo pequeño con una función.

defmodule MyModule do
  def greet(pet), do: "Hey #{pet}!"
end

Bastante sencillo, no hay errores de compilación y funciona como se espera. Sin embargo, a diario (y después de unas pocas docenas o cientos de líneas de código) siempre existe la posibilidad de cometer un error tipográfico que lleve a nuestro código a errores en tiempo de ejecución.

Casi todos (si no todos) los desarrolladores web se han encontrado con un error de referencia, generalmente escrito como “yourVariable” is not defined. Esto es algo común en lenguajes dinámicos y, por supuesto, hay formas de evitar estos errores (por ejemplo usando TypeScript en lugar de Vanilla JavaScript).

Por suerte para nosotros, compilar código de Elixir ya nos ayuda a prevenir este problema de forma inmediata. Introduzcamos un error tipográfico en nuestro ejemplo anterior:

defmodule MyModule do
  def greet(not_used_variable), do: "Hey #{pet}!"
end

Al compilar el proyecto nuevamente, se presentaría el siguiente resultado:

mix compile
Compiling 1 file (.ex)
warning: variable "pet" does not exist and is being expanded to "pet()", please use parentheses to remove the ambiguity or change the variable pet
  lib/barebones.ex:2: MyModule.hello/1

warning: variable "not_used_variable" is unused (if the variable is not meant to be used, prefix it with an underscore)
  lib/barebones.ex:2: MyModule.hello/1

== Compilation error in file lib/barebones.ex ==
** (CompileError) lib/barebones.ex:2: undefined function pet/0

Con estos mensajes, ya podemos ver algunos beneficios del compilador:

  • La compilación falla cuando hay un error tipográfico.
  • Nos advierte de las variables no utilizadas, por lo que no hay código muerto. Esto se puede lograr usando la opción de línea de comando --warnings-as-errors.
mix compile --warnings-as-errors

O si deseas habilitar eso de forma predeterminada para un proyecto, puedes agregar esa opción dentro de elixirc_options en la configuración de tu proyecto.

defmodule Example.MixProject do
  use Mix.Project

  def project do
    [
      app: :example,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      elixirc_options: [warnings_as_errors: true]
    ]
  end
end

Al usar esta opción, la compilación fallará en caso de que haya alguna advertencia en tu código.

Búsqueda de patrones y guardias

La búsqueda de patrones nos permite definir patrones para verificar cómo se conforman los datos.

En Elixir se puede usar la búsqueda de patrones en cualquier lugar, inclusive al declarar funciones y como resultado, tener un código más limpio y legible. Actualicemos nuestro ejemplo con un simple patrón.

defmodule MyModule do
  def greet("dog"), do: "Woof woof!"

  def greet(pet), do: "Hey #{pet}, we haven't meet before!"
end

Ahora nuestra función tiene dos posibles rutas de ejecución basadas en el argumento que recibe. Pero esto todavía tiene un defecto. ¿Qué pasaría si el argumento dado no es una cadena de caracteres? Esto es más conocido en lenguajes como Haskell como una función parcial o, en otras palabras, una función que no toma en cuenta todas las posibles rutas de ejecución que puede tener un argumento.

Debemos recordar que Elixir es un lenguaje dinámico y un argumento puede tener cualquier tipo en tiempo de ejecución. Dado que no tenemos verificación de tipos durante la compilación, es posible que deseemos agregar verificaciones más complejas en nuestra función y ahí es donde entran los guardias (o guards). Agreguemos una guardia a nuestro ejemplo para que devuelva un mensaje predeterminado cuando la función greet reciba solo cadena de caracteres.

defmodule MyModule do
  def greet("dog"), do: "Woof woof!"

  def greet(pet) when is_binary(pet), do: "Hey #{pet}, we haven't meet before!"

  def greet(_), do: "Oops! That's not a string"
end

Al usar la palabra reservada when seguida de una expresión booleana, podemos asegurarnos de que nuestro código solo funciona cuando la expresión resultante es verdadera. La función is_binary es un contenedor para la función de Erlang is_binary que devuelve verdadero si el argumento dado es una cadena de caracteres. Usando ambas funcionalidades, esto significa lo siguiente: solo ejecutar esta función cuando el argumento es una cadena de caracteres.

Pero eso no es todo. Si quisiéramos tener una API más extensible, también podríamos proporcionar patrones opcionales específicos usando [palabras clave o mapas]. Actualicemos nuestro ejemplo a continuación.

defmodule MyModule do
  def greet("dog"), do: "Woof woof!"

  def greet(pet) when is_binary(pet), do: "Hey #{pet}, we haven't meet before!"

  @doc "Match a keyword list which contains `:pet` string element"
  def greet(pet: pet) when is_binary(pet), do: greet(pet)

  @doc "Match a map which the `:pet` key"
  def greet(%{pet: pet}) when is_binary(pet), do: greet(pet)

  def greet(_), do: "Oops! That's not a string"
end

Con esto, nuestra función greet trabajará correctamente para cualquier cadena de caracteres en diferentes tipos de datos y también tiene un mensaje de respaldo en caso de que el argumento recibido no sea válido. ¡Y listo! ¡Ahora tenemos una función total que cubre todas las rutas y no da lugar a errores de tiempo de ejecución!

Estructuras

Ahora que hemos utilizado búsqueda de patrones y guardias, probablemente nos gustaría agregar seguridad adicional a nuestra función. Podemos hacerlo usando las structs (o estructuras) de Elixir. Antes veamos la documentación en su sitio:

Structs are extensions built on top of maps that provide compile-time checks and default values.

¡Justo lo que estábamos buscando, más comprobaciones en tiempo de compilación! Agreguemos una estructura Pet a nuestro ejemplo.

defmodule MyModule do
  defmodule Pet do
    defstruct [:kind]
  end

  def greet("dog"), do: "Woof woof!"

  def greet(pet) when is_binary(pet), do: "Hey #{pet}, we haven't meet before!"

  @doc "Match a keyword list which contains `:pet` string element"
  def greet(pet: pet) when is_binary(pet), do: greet(pet)

  @doc "Match a map which the `:pet` key"
  def greet(%{pet: pet}) when is_binary(pet), do: greet(pet)

  def greet(%Pet{origin: kind}) when is_binary(kind), do: greet(kind)

  def greet(_), do: "Oops! That's not a string"
end

Al hacer este cambio en nuestro ejemplo y compilarlo, los siguientes mensajes serían mostrados en nuestra consola:

Compiling 1 file (.ex)

== Compilation error in file lib/barebones.ex ==
** (CompileError) lib/barebones.ex:14: unknown key :origin for struct MyModule.Pet
    lib/barebones.ex:14: (module)

El mensaje de error al compilar es realmente útil. Dice que tenemos un error tipográfico en nuestro código al hacer una búsqueda de patrones en la estructura Pet. Funcionará correctamente después de cambiar el nombre de origin a kind.

Ahora que tenemos una comprensión más profunda de búsqueda de patrones, guardias y estructuras, tenemos una mejor idea de cómo prevenir errores en tiempo de ejecución en Elixir y también cómo recibir comprobaciones adecuadas en tiempo de compilación.

Las búsqueda de patrones en estructuras nos permiten ser más expresivos en los objetos de negocio en cualquier proyecto. Un gran ejemplo de esto sería el DSL de base de datos Ecto. Al usarlo en nuestro código, crea estructuras sobre las entidades de la base de datos y eso hace que sea más fácil realizar la búsqueda de patrones con ellas.

Advertencias menores

El uso de estructuras en nuestro código nos ayudará a obtener mejores comprobaciones en tiempo de compilación, pero, como siempre, hay un inconveniente. Cada vez que usamos una estructura combinada con búsqueda de patrones, creamos una dependencia en nuestro módulo, lo que lleva a mayores tiempos de compilación.

Esto no es malo por sí solo, pero podría afectar el ciclo de desarrollo. Usar más tiempo en compilar que ejecutar código después de realizar cambios es una tarea frecuente y en proyectos grandes podría tomar unos minutos compilar todo nuevamente (en el peor de los casos).

Comentarios finales

Al usar las características principales de Elixir, podremos introducir más seguridad en nuestro código. Pero no se trata solo de conocer y utilizar sus características principales. También significa que debemos cambiar nuestra forma de pensar.

Tener una formación técnica es el primer paso hacia una mentalidad funcional. Después de eso, necesitamos aplicar esos conceptos a diario para convertir su uso en un hábito, lo que nos permite comprender sus beneficios, como hacer que nuestro código sea declarativo, reutilizable y más. Pero eso no significa que solo tengamos que limitarnos con esta mentalidad para Elixir. También podemos aplicar estos conceptos a cualquier otro lenguaje de programación.

En Stack Builders, fomentamos el uso de programación funcional. Al aplicar esta mentalidad, podemos entregar código de alta calidad a nuestros clientes. Si estás interesado en la programación funcional, asegúrate de revisar nuestras publicaciones de programación.

¿Tienes lo necesario para ser un Stack Builder?