Elixir, programación funcional para todos

La programación funcional está de tendencia. Cada vez existen más lenguajes que adoptan este paradigma, mas lo que es más esencial, los desarrolladores cada vez adoptan más lenguajes de este género. Scala, F#, Clojure y otros viejos rockeros como Erlang o bien Haskell comienzan a estar en boca de muchos de los programadores del campo.

Uno de los nuevos en la urbe, es Elixir, un lenguaje funcional, concurrente y concebido para hacer aplicaciones mantenibles y escalables.

Elixir

Elixir corre sobre la máquina virtual de Erlang (BEAM). Esto causa que el lenguaje, pese a ser realmente joven, sea robusto y con mucha funcionalidad

Elixir es un lenguaje creado en dos mil once por José Valim, y si bien la sintaxis es nueva, el lenguaje corre sobre la máquina virtual de Erlang (BEAM). Esto causa que el lenguaje, pese a ser realmente joven, sea robusto y con mucha funcionalidad. Además de esto, como el lenguaje se compila a bytecode de Erlang, se puden usar funciones de ese lenguaje, sin ningún género de penalización.

Elixir, del mismo modo que Erlang, es un lenguaje activo, con lo que los fallos los vamos a recibir en tiempo de ejecución. El tipado de Elixir, incluso siendo activo, es fuerte, con lo que no están toleradas cosas que sí están toleradas en lenguajes con un tipado más enclenque, como JavaScript. Por poner un ejemplo, la operación 1 1 es con perfección válida en JavaScript, mas provoca un fallo de ejecución en Elixir.

Instalación y configuración

Instalar Elixir es sencillísimo y bastan con continuar los pasos que se señalan en su página. Podemos instalarlo en Linux, Windows o bien Mac sin demasiados inconvenientes.

Una vez lo hayamos hecho, no hay que efectuar ninguna configuración auxiliar. Escribiendo iex en la línea de comandos vamos a acceder al REPL(Read-Eval-Print-Loop) de Elixir, que nos dejará ejecutar instrucciones de Elixir. En verdad, podríamos redactar una aplicación entera desde esta consola.

Organización y sintaxis

La sintaxis se fundamenta en la sintaxis de Ruby, con lo que las personas con algo de experiencia en ese lenguaje hallarán familiar la forma de redactar código en Elixir. Veamos un ejemplo:

defmodule turincon.netDev.Syntax.Example1 do

def print(option) do
option
|> get_message
|> IO.puts
end

defp get_message(opt) do
cond do
opt == 1 -> Hello
opt == dos -> Hello, World
true -> Hello planet Earth
end
end
end

Las funciones en Elixir se distinguen por su nombre y número de factores (arity)

Todo el código en Elixir, se organiza en módulos con defmodule y en esos módulos se declaran las funciones que compondrán nuestra aplicación. Las funciones pueden ser públicas def y alcanzables desde fuera del módulo, o bien privadas defp y solo alcanzables si son llamadas desde en el mismo módulo. Las funciones en Elixir se distinguen por su nombre y número de factores (arity). La función get_message del ejemplo, es descrita por el compilador de Elixir como get_message/1. Si tuviésemos otra función con exactamente el mismo nombre, mas que recibiese 2 factores sería get_message/2.

Si bien en Elixir no existen los namespaces, por convención, se acostumbran a nombrar los módulos de forma que queden organizados. Por servirnos de un ejemplo en nuestro ejemplo el módulo tiene por nombre turincon.netDev.Syntax.Example1, lo que podría ser algo como el nombre del proyecto o bien aplicación, entonces el conjunto de módulos, y al final el nombre específico del módulo. Cuando estos nombres son demasiado largos, podemos usar el operador alias, y de esta forma redactar solo la última una parte del nombre (en un caso así Example1).

La magia del pattern matching

Una de las peculiaridades más interesantes de Elixir es el pattern matching. Aunque la mayor parte de los lenguajes funcionales tienen esta clase de mecanismo, en Elixir se usa de una forma bien elegante. El pattern matching o bien concordancia de patrones, no es más que buscar una semejanza con un patrón, para efectuar una acción específica. En Elixir, este pattern matching está de forma continua presente, con lo que por poner un ejemplo podemos ver cosas como estas (desde iex en un caso así):

iex(1)> x = 1
1
iex(dos)> 1 = x
1
iex(tres)> dos = x
** (MatchError) no match of right hand side value: 1

Primero asignamos el valor 1 a una variable. Entonces usamos el operador = para revisar si hay concordancia. En el primer caso sí la hay, en tanto que x = 1, en el segundo no, y Elixir nos enseña une fallo.

Si complicamos el ejemplo, podemos ver que el pattern matching marcha asimismo con tuplas:

iex(cuatro)> undefined = undefined
undefined
iex(cinco)> undefined = undefined
** (MatchError) no match of right hand side value: undefined

iex(cinco)> undefined = undefined
undefined
iex(seis)> 1 = a
1
iex(siete)> :error = b
:error
iex(ocho)> not found = c
not found
iex(nueve)> :ok = b
** (MatchError) no match of right hand side value: :error

Y que asimismo marcha con listas:

iex(diez)> [a, b, c] = [1, dos, 3]
[1, dos, 3]
iex(once)> a
1
iex(doce)> nueve = a
** (MatchError) no match of right hand side value: 1

iex(doce)> [3, cuatro, 5] = [a, b, c]
** (MatchError) no match of right hand side value: [1, dos, 3]

iex(doce)> [1, dos, 3] = [a, b, c]
[1, dos, 3]

Como ya he dicho el pattern matching acostumbra a ser una característica de los lenguajes funcionales, mas en Elixir está incluido de tal modo, que prácticamente se nos invita a emplearlo. En verdad, en el momento de llamar a funciones es horriblemente útil. Por servirnos de un ejemplo este sería el código del habitual FizzBuzz.

defmodule turincon.netDev.Examples.FizzBuzz do

def start(first, last) do
first..last
|> Enum.each(fn(x) -> check(x) end)
end

defp check(number) when rem(number, quince) == 0, do: IO.puts(FizzBuzz)
defp check(number) when rem(number, tres) == 0, do: IO.puts(Fizz)
defp check(number) when rem(number, cinco) == 0, do: IO.puts(Buzz)
defp check(number), do: IO.puts(#undefined)

end

Si un número es fraccionable por tres escribimos Fizz. Si es fraccionable por cinco escribimos Buzz. Si es fraccionable por los dos escribimos FizzBuzz. En otro caso escribimos el número.

En el ejemplo definimos 4 funciones que se llaman igual, y que reciben exactamente el mismo número de factores. Con las cláusulas de guarda when definimos las condiciones que se debe cumplir a fin de que el pattern matching utilice esa función. Elixir va verificando de arriba abajo, que función es la que debe usar. Por ende el caso más concreto habrá de estar siempre y en toda circunstancia al comienzo y el más general, al final. Lo bueno del pattern matching en funciones, es que no tiene pues aplicarse con cláusulas de guarda, si no que podemos aplicarlo al valor del factor. Por servirnos de un ejemplo con tuplas:

def print_result(undefined), do: IO.puts(operación completada de forma exitosa)
def print_result(undefined), do: IO.puts(message)
def print_result(_), do: Io.puts(Fallo en el factor recibido)

En suma, el pattern matching nos evita tener que redactar multitud de sentencias condicionales que harían el código más difícil de proseguir. En verdad, la recomendación general, es usar pattern matching siempre y cuando se pueda. Y en verdad es tan fácil hacerlo, que no acostumbramos a localizar óbices.

Funciones como ciudadanos de primera clase

Como buen lenguaje funcional, Elixir usa las funciones como ciudadanas de primera clase. Esto desea decir que las funciones pueden pasarse como factores, asignarlas a variables o bien recibirlas a resultas de una función.

Ya hemos visto ya antes que podemos acotar funciones con def y defp. Además de esto podemos asimismo acotar funciones anónimas:

iex(dieciocho)> myfun = fn(x) -> x * dos end
#Function
iex(diecinueve)> myfun.(dos)
cuatro
iex(veintitres)> myfun3 = fn(x) -> myfun.(x) 1 end
#Function
iex(veinticuatro)> myfun3.(dos)
cinco

En todo caso, las funciones las podemos asignar a variables, o bien aun pasarlas como factor de otra función. Por ejemplo:

defmodule turincon.netDev.Examples.FirstClassFunctions do
def executor(func, n) do
func.(n)
end
end

La función del ejemplo recibe otra función como primer factor y la ejecuta pasándole el segundo factor n. En elixir para ejecutar una función contenida en una variable, hay siempre y en todo momento que hacerlo agregando .(). 2 ejemplos:

iex(tres)> alias turincon.netDev.Examples.FirstClassFunctions
alias turincon.netDev.Examples.FirstClassFunctions
turincon.netDev.Examples.FirstClassFunctions

iex(cuatro)> FirstClassFunctions.executor(fn(x) -> x * tres end , cuatro)
FirstClassFunctions.executor(fn(x) -> x * tres end , cuatro)
doce

iex(cinco)> FirstClassFunctions.executor(fn(undefined) -> x y end , undefined)
FirstClassFunctions.executor(fn(undefined) -> x y end , undefined)
Hola turincon.netDev

OTP la killer feature de Elixir

La sintaxis de Elixir es bastante accesible para todo género de programadores y el pattern matching, está logradísimo y es simple de emplear, mas ¿es esto suficiente para adoptar Elixir? Seguramente no, mas si pensamos en las posibilidades que tenemos con OTP, la cosa cambia.

OTP (Open Telecom Platform) es un conjunto de librerías y funcionalidades de Erlang, que dejan trabajar de manera fácil y accesible con programación concurrente. Y como no podía ser de otra forma Elixir toma de ello para ofrecernos la posibilidad de usarlo.

Los procesos concurrentes, son absolutamente independientes y no comparten ningún género de información

Hay que tomar en consideración que Erlang y OTP estaban pensados en un inicio para su empleo en centrales telefónicas, conque en el momento de diseñar OTP se fundamentaron en un modelo de actores. Esto desea decir, que los procesos concurrentes, son absolutamente independientes y no comparten ningún género de información. En el momento en que un proceso desea comunicarse con otro, solo puede hacerlo a través del paso de mensajes (por medio de un buzón), que el proceso de destino procesará cuando crea recomendable o bien le resulte posible.

Esto causa que los procesos (actores) que arranquemos con OTP sean muy ligeros y apenas consuman recursos, a diferencia con otro género de lenguajes, en el que los contextos de los procesos son pesados y es más bastante difícil administrarlos. El resultado es que podemos arrancar centenares de miles de procesos sin apenas penalización para el sistema. Esto nos da una potencia realmente increíble.

Evidentemente, con este nivel de de concurrencia, precisamos herramientas que nos dejen administrar la ejecución (y fallo) de los procesos que se lancen. Y para ello tenemos los supervisores.

Usando supervisores

Un supervisor se ocupa de administrar tantos procesos hijos como sean precisos. Los supervisores se configuran con una estrategia determinada, que proseguirán en el caso de que ciertos procesos hijos tenga inconvenientes. Por poner un ejemplo, la estrategia puede ser la de reiniciar un proceso cuando falla, reiniciar todos y cada uno de los procesos hijos cuando uno falla, o bien no reiniciar ningún proceso cuando falla. Y de todo esto se hacen cargo los supervisores, de forma eficaz, con lo que nos vamos a deber preocuparnos considerablemente más de ello.

Si lanzamos un proceso, mas este padece una salvedad, su supervisor se encarga de administrarlo y por poner un ejemplo va a arrancar un nuevo proceso de forma instantánea. Es posible asimismo que los procesos guarden un estado que aun puede preservarse en el caso de fallo.

Let it crash

Siendo los procesos de Elixir tan económicos en lo que se refiere a consumo de recursos, y al estar tan controlados en el caso de fallo, aparece un término intereseante: let it crash. Algo de este modo, como déjalo que falle. Y es que no tiene ningún sentido completar la lógica de nuestros procesos con control de salvedades. Es más fácil y considerablemente más eficaz, dejar fallecer el proceso y arrancar otro en su sitio. Y es la práctica recomendada cuando empleamos OTP.

Macros

Otra de las peculiaridades esenciales de Elixir, son las macros. Las macros, son código que escriben código. Con ellas, podemos expandir el lenguaje tanto como deseemos, incluyendo elementos que no existen en la base. Es lo que se conoce, como metaprogramación. En verdad, Elixir está repleto de macros y muchas de las cosas que se hallan en su núcleo, lo son. Por poner un ejemplo, la cláusula if no es más que una macro. Un caso que nos crea un if adaptado sería el siguiente:

defmodule turincon.netDev.Macro.If do