Elixir: Primeras impresiones

Elixir: Primeras impresiones

NOTA: Este artículo originalmente lo escribí para La Cara Oscura del Software, un blog colectivo dedicado a desarrollo de software.

Durante el segundo hangout de @rubyVE escuché a @edgar comentar sobre Elixir y en verdad me llamó la atención lo que indicaba, siempre me inquieta conocer al menos un poco sobre otros lenguajes de programación, siempre terminas aprendiendo algo, una buena lección es seguro, sobre todo por aquello de la filosofía del programador pragmático y la necesidad de invertir regularmente en tu portafolio de conocimientos.

Ahora bien, después de leer un artículo de Joe Armstrong, padre de Erlang, en donde afirmaba que tras una semana de haber usado Elixir estaba completamente entusiasmado por lo visto. Con esto era claro que se estaba presentando para mi una gran oportunidad para retomar la programación funcional con Elixir, inicié con Haskell en la Universidad en la materia de Compiladores y la verdad es que no lo he vuelto a tocar.

José Valim es un brasileño, parte del equipo core committer de Rails. Después de sufrir RSI, en su afán por encontrar qué hacer en su reposo se puso a leer el libro: Seven Languages in Seven Weeks: A Pragmatic Guide to Learning Programming Languages y allí conoció Erlang y su EVM (Erlang Virtual Machine), cierto tiempo después creo este nuevo lenguaje llamado Elixir, en donde uno de sus mayores activos es la EVM, tanto es así que de hecho no existe un costo de conversión al invocar Erlang desde Elixir y viceversa. Todo esto es seguramente es la respuesta de Valim a las limitantes físicas actuales en los procesadores o lo que se conoce también como: “se nos acabó el almuerzo gratis”, sobre todo ahora con recientes anuncios de Parallella y de Intel con los procesadores Xeon Phi.

A pesar de la horrible sintaxis de Erlang, o al menos después de leer a Damien Katz, autor original de CouchDB en What Sucks About Erlang y a Tony Arcieri, autor de Reia (otro lenguaje basado en BEAM), en su artículo The Trouble with Erlang (or Erlang is a ghetto) es fácil concluir que la sintaxis no es la más amenas de todas. Sin embargo, las inmensas habilidades que brinda Erlang para establecer sistemas concurrentes (distribuidos, tolerantes a fallas y code swapping) ha permitido llegar a mantener hasta 2 millones de conexiones TCP en un solo nodo. Es por ello que compañías como Whatsapp, Facebook, Amazon, Ericsson, Motorola, Basho (Riak) y Heroku por mencionar algunas están usando Erlang para desarrollar sus sistemas.

Rápidamente quisiera compartirles mi felicidad por haber iniciado a explorar este lenguaje. Para iniciar tu proyecto tienes un magnífico utilitario llamado mix (inspirado en Leiningen de Clojure). Mix también permite manejar las tareas más comunes como administración de dependencias de tu proyecto, compilación, ejecución de pruebas, despliegue (pronto), entre otras. Incluso puedes programar nuevas tareas, simplemente asombroso, en fin, vamos a jugar:

$ mix help
mix                 # Run the default task (current: mix run)
mix archive         # Archive this project into a .ez file
mix clean           # Clean generated application files
mix cmd             # Executes the given command
mix compile         # Compile source files
mix deps            # List dependencies and their status
mix deps.clean      # Remove the given dependencies' files
mix deps.compile    # Compile dependencies
mix deps.get        # Get all out of date dependencies
mix deps.unlock     # Unlock the given dependencies
mix deps.update     # Update the given dependencies
mix do              # Executes the tasks separated by comma
mix escriptize      # Generates an escript for the project
mix help            # Print help information for tasks
mix local           # List local tasks
mix local.install   # Install a task or an archive locally
mix local.rebar     # Install rebar locally
mix local.uninstall # Uninstall local tasks or archives
mix new             # Creates a new Elixir project
mix run             # Run the given file or expression
mix test            # Run a project's tests

Procedamos con la creación de un nuevo proyecto:

$ mix new demo
* creating README.md
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/demo.ex
* creating test
* creating test/test_helper.exs
* creating test/demo_test.exs

Your mix project was created with success.
You can use mix to compile it, test it, and more:

    cd demo
    mix compile
    mix test

Run `mix help` for more information.

La estructura del proyecto creado por mix es como sigue:

$ cd demo
$ tree
.
|-- README.md
|-- lib
|   `-- demo.ex
|-- mix.exs
`-- test
    |-- demo_test.exs
    `-- test_helper.exs

2 directories, 5 files

En mix.exs encontramos la configuración del proyecto así como sus dependencias en caso de aplicar, en lib/demo.ex ubicamos la definición del módulo que nos ayudará a estructurar posteriormente nuestro código, en test/test_demo.exs encontramos un esqueleto base para los casos de pruebas asociadas al modulo. Finalmente en test/test_helper.exs radica inicialmente el arranque del framework ExUnit.

Creemos un par de pruebas sencillas primero:

$ vim test/demo_test.exs
defmodule DemoTest do
  use ExUnit.Case

  test "factorial base case" do
    assert Demo.factorial(0) == 1
  end

  test "factorial general case" do
    assert Demo.factorial(10) == 3628800
  end

  test "map factorial" do
    assert Demo.map([6, 8, 10], fn(n) -> Demo.factorial(n) end) == [720, 40320, 3628800]
  end
end

Evidentemente al hacer “mix test” todas las pruebas fallaran, vamos a comenzar a subsanar eso:

$ vim lib/demo.ex
defmodule Demo do

def factorial(0) do
    1
end

def factorial(n) when n > 0 do
    n * factorial(n - 1)
end

end

En el par de bloques de código mostrado previamente se cubren los dos casos posibles del factorial.

Volvamos a correr las pruebas:

$ mix test       
..

  1) test map factorial (DemoTest)
     ** (UndefinedFunctionError) undefined function: Demo.map/2
     stacktrace:
       Demo.map([6, 8, 10], #Function<0.60019678 in DemoTest.test map factorial/1>)
       test/demo_test.exs:13: DemoTest."test map factorial"/1



Finished in 0.04 seconds (0.04s on load, 0.00s on tests)
3 tests, 1 failures

De las 3 pruebas programadas hemos superado dos, nada mal, continuemos, volvamos a editar nuestro módulo:

$ vim lib/demo.ex
defmodule Demo do
    def factorial(0) do
        1
    end

    def factorial(n) when n > 0 do
        n * factorial(n - 1)
    end

    def map([], _func) do
        []
    end

    def map([head|tail], func) do
        [func.(head) | map(tail, func)]
    end
end

En esta última versión se ha agregado la función map, básicamente esta función recibe una colección de datos y una función que se aplicará sobre cada uno de los elementos de la colección, para nuestros efectos prácticos la función que será pasada a map será el factorial.

Como nota adicional, los bloques de código vistos en el ejemplo anterior prefiero expresarlos de manera sucinta así, cuestión que también es posible en Elixir:

$ vim lib/demo.ex
defmodule Demo do
    @moduledoc """
    Demo module documentation, Python *docstrings* inspired.
    """
    def factorial(0), do: 1

    def factorial(n) when n > 0, do: n * factorial(n - 1)

    def map([], _func), do: []

    def map([head|tail], func), do: [func.(head) | map(tail, func)]
end

Acá se pueden apreciar conceptos como pattern matching, guard clauses, manejo de listas y docstrings (inspirado en Python). Atención, los docstrings soportan MarkDown, junto a ExDoc es posible producir sitios estáticos que extraen los docstrings a partir del código fuente.

Comprobemos los casos desde la consola interactiva iex antes de pasar de nuevo al caso automatizado:

$ iex lib/demo.ex
Erlang R16B01 (erts-5.10.2) [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (0.10.2-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> import Demo
nil
iex(2)> h(Demo)
# Demo

Demo module documentation, Python *docstrings* inspired.

iex(3)> Demo.factorial(10)
3628800
iex(4)> Demo.map([6, 8, 10], Demo.factorial(&1))
[720, 40320, 3628800]

Lo previo es una consola interactiva, vimos la documentación e hicimos unas pruebas manuales.

Seguro notaron que al final del ejemplo previo, al hacer el map he cambiado la forma en la que invoco a la función anónima la cual originalmente fue definida en las pruebas como fn(n) -> Demo.factorial(n) end, solamente he recurrido a un modo que permite Elixir y otros lenguajes funcionales para expresar este tipo de funciones de manera concisa, se le conoce como Partials.

Ahora corramos las pruebas automatizadas de nuevo:

$ mix test
Compiled lib/demo.ex
Generated demo.app
...

Finished in 0.04 seconds (0.04s on load, 0.00s on tests)
3 tests, 0 failures

Con eso hemos pasado los casos de pruebas.

En este caso particular, prefiero que las pruebas sean autocontenidas en el módulo, además, no recurrimos a fixtures ni nada por el estilo, así que vamos a cambiar el código para que soporte doctest

$ vim lib/demo.ex
defmodule Demo do
    @moduledoc """
    Demo module documentation, Python *docstrings* inspired.
    """

    @doc """
    Some examples

    iex> Demo.factorial(0)
    1

    iex> Demo.factorial(10)
    3628800

    iex> Demo.map([6, 8, 10], Demo.factorial(&1))
    [720, 40320, 3628800]
    """
    def factorial(0), do: 1

    def factorial(n) when n > 0, do: n * factorial(n - 1)

    def map([], _func), do: []

    def map([head|tail], func), do: [func.(head) | map(tail, func)]
end

Dado lo anterior ya no es necesario tener las pruebas aparte, por lo que reduzco:

$ vim test/demo_test.exs
defmodule DemoTest do
  use ExUnit.Case
  doctest Demo
end

Comprobamos la equivalencia:

$ mix test              
...

Finished in 0.06 seconds (0.06s on load, 0.00s on tests)
3 tests, 0 failures

Simplemente hermoso, cabe resaltar que lo mencionado es solo rascar un poco la superficie de Elixir :-)

Ah, por cierto, ya para finalizar, José Valim está apuntando el desarrollo de Elixir y Dynamo (framework) a la Web, lo ha dejado claro, por eso he visto que algunos programadores Rails están “echándole un ojo” a Elixir, al menos eso es lo que concluyo de los elixir-issues en Github, el reciente screencast de Peepcode (vale la pena comprarlo) y los libros que se avecinan de Dave Thomas y Simon St. Laurent.

Quizá en una nueva oportunidad hablemos de Macros, pase de mensajes entre procesos, Protocolos (inspirados en Clojure protocols), Reducers (inspirados también en Clojure Reducers), HashDict, el hermoso y *nix like operador pipeline (|>), mejorar nuestra implementación de la función map para que haga los cálculos de manera concurrente, entre otros.

Espero hayan disfrutado la lectura, que este artículo sirva de abreboca y les anime a probar Elixir.