Aprendiendo erlang durante la cuarentena
Durante estos días de encierro forzoso en casa y sin más obligaciones que distraer la mente en algo para que el techo no nos caiga encima sin haberse movido, decidí aprender otro lenguaje funcional.
Mi primera intención fue volver a intentarlo con Haskell
, un
lenguaje que viene resistiéndose desde hace tiempo, pero me encontré
que cuando fui a instalarlo en mi sistema, pedía 1,8Gb de espacio... y
me pareció una exageración. Caber cabía, pero se me hace mucho
desperdicio para un mero aprendizaje.
Descartado Haskell
me acordé de erlang
. Hace unos años ya le eché
un ojo a ese lenguaje, pues hacia él me llevó la curiosidad de
encontrar una magnífica herramienta creada con él: wings 3D, en una
época en que le eché muchas horas al tema del 3D. En aquella época el
código que yo escribía solía ser C/C++
o Java
. Empezaba a trastear
con Python
y me divertía con Smalltalk
, e incluso durante un año y
medio en el que trabajé como programador con Delphi/Pascal
, nunca
había tratado con un lenguaje de los llamados funcionales. Entonces,
con aquellos conocimientos, o limitaciones ─dirán algunos─, erlang
me pareció un lenguaje muy marciano y después de echarle un vistazo
me olvidé de él.
Hace poco, salió un proyecto en el que podría colaborar y se
realizaría en erlang
, base de datos couchDB
, ─otra de las
herramientas punteras hechas con ese lenguaje─. El caso es que aquél
proyecto no salió: se lo adjudicaron a alguna consultora más fuerte
que cuatro animosos frikis. Comencé en aquellos días a repasar
erlang
pero cuando se torció el proyecto lo dejé de nuevo y casi me
olvidé de su existencia.
Estos días, pensando en qué hacer y habiendo descartado las 1,8Gb que
pedía Haskell
, me acordé de nuevo de ese lenguaje: funcional,
marciano y friki que promete muchas cosas buenas:
- Concurrencia.
- Distribución.
- Escalado.
- Resistencia a errores.
- Sustitución de código en caliente.
Es un lenguaje que, como se puede apreciar, promete muchas buenas cosas pero que, también supongo, me tropezará con algún problema o pega.
Un vistazo general
Sin ánimo de ser exhaustivo quiero compartir una visión general sobre
el lenguaje y los pasos que he venido dando en mi proceso de
aprendizaje. Lo primero que hice, como es de suponer es instalar
erlang
en mi ordenador:
sudo zypper install erlang
En otros sistemas, cada uno sabrá cómo hacerlo... pero lo que me llamó
la atención es que iba a descargar algunas dependencias y que entre
todo el tamaño de la descarga no superaba los 30Mb. Vale que una vez
descomprimido el erlang
en mi sistema ocupa unas 50Mb, pero no son
la 1,8Gb que pedía Haskell
sólo en la descarga, que instalado, sería
aún más. Este es el primer punto a su favor, no es un sistema
demasiado pesado. Tanto, que ya puestos, instalé algunas herramientas
para el desarrollo, como la documentación o el debugger o el
observer, que aún no sé utilizar pero que prometen ser útiles. Con
esos paquetes se instalaron también otras dependencias y en total,
entre unas cosas y otras, tengo ocupadas 98Mb de disco duro, que a
estas alturas de la vida, tampoco son un gasto significativo. Otros
lenguajes como Python, Ruby o Java ocupan bastante más que eso.
Un ejemplo sencillo
No es un lenguaje funcional puro según dice la misma documentación de
erlang
, sin embargo, es lo más funcional que he visto hasta ahora.
De hecho, ni siquiera tiene instrucciones para hacer bucles; nada de
while
ni de for
. Las tareas repetitivas se realizan mediante la
recursión sobre listas, por ejemplo. Para no hablar en vacío, pongo
un ejemplo sencillo, pero que muestra varias características del
lenguaje. Obsérvese el siguiente código:
-module(factorial). -export([factorial/1]). factorial(1) -> 1; factorial(N) -> N * factorial(N-1).
Lo explicaré a continuación, pero vamos a ver cómo trabaja erlang
compilando el código y ejecutándolo desde su herramienta interactiva
de shell: erl
.
$ erl Erlang/OTP 22 [erts-10.7] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] Eshell V10.7 (abort with ^G) 1> c(factorial). {ok,factorial} 2> factorial:factorial(5). 120 3> factorial:factorial(-5). Terminado (killed) $ _
Vemos que al intentar calcular el factorial de un número negativo,
llega un momento en que bloquea y arrastra consigo al prompt
interactivo de erlang
.
Si observamos el código vemos que los comandos de las primeras líneas
comienzan por -
. Podríamos considerarlos como la cabecera del
módulo. En nuestro caso -module(factorial).
lo que hace es definir
el fichero de código como un módulo de nombre factorial
. También
es importante el .
al final, pues es lo que delimita las expresiones
de erlang
: toda expresión en este lenguaje va a terminar con un
punto. La segunda línea exporta una lista de funciones del módulo. En
nuestro caso sólo tenemos una función factorial/1
, pero podemos
observar que aún así debemos delimitarlo como una lista [...]
. La
otra cosa que nos puede llamar la atención es la manera de referirse a
una función: el nombre de la misma y separado con una barra el número
de argumentos de la misma. Eso nos permite, por ejemplo, si tienen
distinto número de parámetros, trabajar con dos funciones con el mismo
nombre. Por último, tenemos la expresión que define la función:
comienza con factorial(1)
y acaba donde está el punto, tres líneas
más abajo. Como vemos, está dividida en dos partes según el argumento
recibido: si es un 1
devuelve 1
y si no, devuelve el resultado de
multiplicar el argumento N
por el factorial de N-1
... es decir, la
típica función recursiva llamándose a sí misma. Pero vemos, que las
dos partes de la función están separadas por un carácter ;
.
Entorno interactivo
El entorno interactivo que nos presenta erlang
es erl
, como hemos
visto antes. Ahí podemos cargar los módulos que necesitemos y hacer
muchas más cosas, pero vamos poco a poco.
En el ejemplo anterior hemos visto que compilábamos el módulo que
habíamos creado mediante el comando c(factorial).
antes de poder
utilizarlo con el comando factorial:factorial(5).
y que nos permite
analizar cómo son las llamadas en este lenguaje. Para empezar es el
nombre del módulo y de la función separado por el carácter :
.
Pero de momento, vamos a centrarnos en el eshell:
$ erl Erlang/OTP 22 [erts-10.7] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] Eshell V10.7 (abort with ^G) 1> _
En la segunda línea nos dice abort with ^G
, y puede parecer que
C-g
(en notación de emacs) lo que haga solamente es parar el
proceso de Eshell
, pero no es cierto. Si pulsamos esa combinación de
teclas nos aparecerá un prompt tal que -->
:
$ erl Erlang/OTP 22 [erts-10.7] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] Eshell V10.7 (abort with ^G) 1> User switch command --> _
Antes del prompt vemos el mensaje User switch command. Si pulsamos
h
, o ?
, y enter
nos mostrará una lista de posibles comandos:
1> User switch command --> h c [nn] - connect to job i [nn] - interrupt job k [nn] - kill job j - list all jobs s [shell] - start local shell r [node [shell]] - start remote shell q - quit erlang ? | h - this message --> _
Vamos a utilizar j
y enter
para ver la lista de trabajos, aunque
de momento sólo tenemos nuestro eshell
funcionando, así que
contestará algo parecido a:
--> j 1* {shell,start,[init]} --> _
Y si tenemos nuestra consola ocupada con cálculos complejos y tarda en
contestar, por poner un ejemplo, podemos necesitar otra consola, la
lanzaremos con s
... paralelismo desde la propia línea de comandos:
--> s --> j 1 {shell,start,[init]} 2* {shell,start,[]} --> _
Bien, vemos que ahora mismo tenemos 2 procesos shell
corriendo y
estamos enlazados al 2, porque está marcado con el *
. Si queremos
volver a la consola anterior utilizaremos el comando c 1
. Si
utilizamos sólo la c
conectaremos con la shell a la que estemos
enganchados. También, como se puede ver en el listado de comandos se
pueden hacer más cosas, como matar un proceso (k
), levantar un
«nodo» para conexión remota (r
), o salir del shell
(q
).
Un poco de programación defensiva
Como vimos antes, el calcular el factorial de un número negativo producía un bloqueo. Vamos a proteger un poco esa ejecución:
-module(factorial). -export([factorial/1]). factorial(1) -> 1; factorial(N) when N > 0 -> N * factorial(N-1).
El resultado será el siguiente:
$ erl Erlang/OTP 22 [erts-10.7] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] Eshell V10.7 (abort with ^G) 1> c(factorial). {ok,factorial} 2> factorial:factorial(5). 120 3> factorial:factorial(-5). ** exception error: no function clause matching factorial:factorial(-5) (factorial.erl, line 4) 4> _
Como vemos la cláusula when
establece que se haga el cálculo cuando
N > 0
, es decir, cuando es un número positivo y cuando es negativo
se lanza una excepción porque no hay ninguna cláusula que tenga en
cuenta ese caso. Sin embargo, esa excepción no se lleva consigo la
ejecución del shell, lo cual es de agradecer y entraría en la
filosofía de erlang
que dice eso de déjalo que falle.
Tipos de datos
Están los típicos valores numéricos: enteros y de coma flotante que se ven limitados al máximo que pueda determinar la máquina, pero luego hay otros tipos que datos que vamos a ver resumidos.
Los primeros son los átomos o atoms
, que algunos vienen definidos
en el propio lenguaje, como son true
, false
, undefined
, ok
,
error
... Sin embargo, podemos definir los nuestros propios según los
necesitemos.
En los números cabe destacar que podemos trabajar en distintas bases,
como en otros lenguajes, pero en este caso la base se define con el
operador #
y no sólo soporta las clásicas bases 2, 8 y 16. Se
pueden especificar bases entre 2 y 32. con la notación siguiente:
7> 2#1011. 11 8> 8#123. 83 9> 30#12a0e. 873014 10>
Como en todos los lenguajes funcionales, las listas son el tipo de
datos. En erlang
ocurre lo mismo, sin embargo, las listas vienen
cargadas con otro punto importante a tener en cuenta, por ejemplo:
1> [104, 111, 108, 97]. "hola" 2> _
¿Eh? ¿Cómo?... Pues eso, si es una lista de números y todos los números equivalen a caracteres imprimibles, la lista y la cadena unifican criterios. Sin embargo:
1> [104, 111, 108, 97]. "hola" 2> [104, 111, 108, 97, 0]. [104,111,108,97,0]. 3> _
Cuando se encuentra con algún valor que no pertenece a ningún carácter
imprimible, como es el 0
, la lista se comporta como en cualquier
otro lenguaje. Como vemos se definen con los caracteres []
y se
separan por comas. Si en lugar de los corchetes utilizamos las llaves
{}
, lo que estamos definiendo es una tupla
, que básicamente es una
lista con características especiales. Por ejemplo, cuando compilábamos
en el punto anterior el código del módulo factorial
el resultado que
nos devolvía era {ok,factorial}
. Es decir, una tupla que contenía
dos atoms
uno que nos decía que todo ha ido bien en la compilación
(ok
) y otro con el nombre del módulo. Esto es algo muy habitual en
erlang
, que el retorno de una función sea un tipo compuesto que
proporciona mucha más información que un simple valor numérico.
Además podemos definir nuestros propios datos gracias a los
records
. Es decir, podemos definir un tipo tal que:
-record(listin, {nombre, apellido1, apellido2, numero}).
Desde la shell
la sintaxis se reduce y en lugar de la forma
-record
se utiliza la función rd
:
rd(listin, {nombre, apellido1, apellido2, numero}).
Por ejemplo:
1> rd(listin, {nombre, apellido1, apellido2, telefono}). listin 2> A = {listin, "Fulanito", "de Tal", "y Tal", 987654321}. #listin{nombre = "Fulanito",apellido1 = "de Tal", apellido2 = "y Tal",telefono = 987654321} 3> A#listin.nombre. "Fulanito" 4> _
Vemos que se utiliza la forma general de variable#registro.campo
para acceder a un dato determinado.
He leído por encima, que la instalación básica de erlang
proporciona
una base de datos llamada Mnesia
, que aún no sé cómo funciona pero
que supongo que será toda una ayuda en aplicaciones grandes. Y tampoco
he tocado el tema de conexión a través de red.
Conclusiones
De momento, el lenguaje erlang
me está sorprendiendo y gustando.
Ahora mismo estoy en plena fase de aprendizaje, apenas he leído un
poco y más bien por encima, un libro para hacerme una idea general de
cómo funciona. Me he encontrado una sintaxis bastante simple, no hay
que aprenderse largos listados de comandos ─con sus excepciones─ que
nos compliquen la vida. Es un lenguaje expresivo y aunque de primeras,
si vienes acostumbrado de otros lenguajes, puede parecer un poco
marciano, como ya dije antes que me pasó a mí, pero con muy poco la
práctica hace que todo el código sea claro y fácil de entender.
Puesto en el camino, y para aprender realmente el lenguaje hay que utilizarlo, así que estoy pensando alguna aplicación que utilice las características del lenguaje con profusión y poner a prueba esa capacidad de resistencia a fallos, recambio en caliente, concurrencia, etc. Se me había ocurrido alguna especie de MUD, aunque aún no tengo muy claro qué o cómo... en estos días de encierro forzoso espero delinear un poco mejor la idea y ya veremos si la puedo poner en marcha. De momento estoy utilizando algunos libros que he encontrado por ahí para aprender cuantos más detalles mejor, que aún me falta un poco para dar el salto hacia un proyecto.