Erlang es un lenguaje de programación diseñado para desarrollar sistemas de comunicación grandes en tiempo real con alta disponibilidad, que sean escalables y tolerantes a fallos. Es un lenguaje de programación funcional, cuya característica principal es disponer de concurrencia. Otras propiedades del lenguaje son la inmutabilidad de los datos, el encaje de patrones, la evaluación impaciente, el tipado dinámico, la computación distribuida o el cambio en caliente, entre muchas otras.
Recomiendo leer el libro Learn You Some Erlang, que recoge con bastante acierto las principales herramientas de las que dispone el lenguaje. El IDE que recomiendo es el IntelliJ IDEA, que tiene la versión Community que es gratuita y permite instalar un plugin para trabajar con Erlang. Por último, estos apuntes han sido probados en la versión 23 de Erlang/OTP, aunque deberían funcionar la mayoría de los ejemplos con las versiones posteriores.
Los programas en Erlang se dividen en módulos, ficheros con la extensión .erl
, que contienen conjuntos de funciones. También existen ficheros de cabecera, con la extensión .hrl
, para incluir macros en diferentes módulos.
Una vez codificados los módulos del programa, estos son compilados a ficheros .beam
para convertir el código fuente del programa al byte code que será ejecutado por la máquina virtual de Erlang.
Para compilar y ejecutar programas necesitamos entrar en la terminal de Erlang, con los comandos erl
y werl
. El primero ejecuta Erlang desde la consola del sistema y el segundo lo hace desde una ventana de la interfaz gráfica del sistema operativo.
Dentro de la terminal existe una lista de comandos del sistema y de la terminal para poder trabajar. Aquí destacamos una lista de los más fundamentales para el día a día:
Comando | Descripción | Parámetros |
---|---|---|
c(M) |
Compilar un módulo | M : Nombre del módulo |
l(M) |
Cargar un módulo | M : Nombre del módulo |
cd(D) |
Cambia de directorio | D : Ruta del directorio |
pwd() |
Muestra el directorio actual | |
f() |
Libera las variables asignadas | |
q() |
Cierra la terminal | |
help() |
Muestra la ayuda de la terminal |
Podemos también compilar un módulo con el comando erlc
, de cara a automatizar la compilación de un proyecto mediante scripts.
Como ejemplo inicial vamos a realizar el clásico hola mundo:
% hello.erl
-module(hello).
-export([world/0]).
world() -> io:format("Hello, world!~n").
Los comentarios en Erlang se escriben usando %
, como muestra la primera línea del ejemplo. Un módulo está compuesto por una serie de definiciones que terminan con un punto. Las definiciones pueden ser atributos o funciones. Los atributos describen propiedades del módulo, que pueden ser consultadas por la máquina virtual. En el ejemplo tenemos dos: el nombre del módulo con -module
y las funciones públicas del mismo con -export
. Estos dos atributos son los mínimos que necesitamos para construir un módulo y hay que tener en cuenta que el nombre del módulo y del fichero tienen que ser el mismo. Después de los atributos viene la función world
que no tiene parámetros e invoca la función format
del módulo io
, que muestra una cadena de texto en la consola del sistema.
Con esto tenemos entonces nuestro fichero hello.erl
, que contiene el módulo hello
. Para compilarlo podemos utilizar erlc
de la siguiente manera:
erlc hello.erl
Esto compila el módulo hello
y genera un fichero hello.beam
dentro del mismo directorio que contiene a hello.erl
. Si queremos compilar el módulo desde la terminal de Erlang, tenemos que ejecutar el comando erl
y dentro de la terminal usar los comandos:
c(hello).
l(hello).
El primero compila el módulo hello
y el segundo carga el módulo en la máquina virtual, siempre y cuando se haya generado el fichero .beam
correspondiente. Si estamos muy seguros, de que se va a compilar sin problemas, podemos también usar:
c(hello), l(hello).
Una vez está cargado el módulo en la máquina virtual, ejecutamos el programa con:
hello:world().
Para ejecutarlo desde la consola del sistema directamente, como si fuera un programa normal, usaríamos erl
de la siguiente manera:
erl -eval hello:world(). -s init stop -noshell
Esto arranca la máquina virtual sin usar la terminal, para evaluar la expresión hello:world().
mediante la opción -eval
y después ejecuta la función init:stop()
, que finaliza la ejecución del entorno de ejecución.
Esta forma de trabajar hace que los ficheros .beam
terminen en el mismo directorio que nuestro código, por lo que sería buena idea tener un directorio de salida para la compilación. Para ello tenemos que utilizar la opción -o
con erlc
y -pa
con erl
:
erlc -o Release Source/hello.erl
erl.exe -pa Release -eval hello:world(). -s init stop -noshell
Este escenario presupone que tenemos un directorio Release
para el resultado de la compilación y Source
para el código fuente del programa. Entonces, tras compilar hello.erl
, se genera hello.beam
en el directorio Release
y es ejecutado con gracias a que hemos indicado a la máquina virtual un directorio adicional en el que buscar ficheros .beam
.
En Erlang podemos tener secuencias de expresiones separadas por comas, que terminarán en un punto para cerrar la definición dentro del módulo. Por ejemplo, la siguiente función:
hi_bye() ->
io:format("Hello~n"),
io:format("Goodbye~n").
También dentro de la terminal se puede introducir una secuencia de expresiones, como hemos visto antes para compilar y cargar un módulo. Tomando el último ejemplo:
1> io:format("Hello~n"), io:format("Goodbye~n").
Hello
Goodbye
ok
Para este ejemplo mostramos la salida por pantalla de la terminal, viendo que nos muestra el texto que queremos enviar a la salida de la consola, y podemos observar que termina con un ok
, que es el valor que devuelve la función io:format
al terminar.
La notación que utiliza la biblioteca estándar de Erlang/OTP para los nombres de módulos y funciones es de tipo snake case, es decir, que las palabras se separan con un guion bajo para que sea el nombre más legible.
La sintaxis para definir números enteros es la siguiente:
La base puede ser un valor dentro del intervalo y por defecto es 10
. Por ejemplo: 4
, 8
, 15
, 2#10000
, 8#27
, 16#2A
.
También podemos definir números reales o de coma flotante:
Por ejemplo: 0.64341054629
, 2.718281828459045
, 3.141592653589793
.
Enteros y reales tienen una representación interna en Erlang distinta, por ello a la hora de realizar una operación matemática, si fuera necesario, la máquina virtual transforma un valor entero en uno real automáticamente.
La sintaxis de Erlang permite usar el símbolo
_
como separador visual entre los dígitos de un número, de modo que se puede usar como separador de millares. Por ejemplo:1_234
o1_2_3
. Al ser un elemento meramente estético, son eliminados a la hora de evaluar la expresión numérica.
Los átomos son valores literales representados por un nombre. Para definirlos hay dos maneras. La primera es aquellas cadenas compuestas por letras, números, el guion bajo (_
) y la arroba (@
), que empiezan por una letra minúscula. La segunda es aquellas cadenas delimitadas por comillas simples ('
), que contienen cualquier cadena de texto. Por motivos obvios, la segunda forma no puede contener una comilla simple, salvo que vaya precedida que la barra invertida, es decir: \'
. Por ejemplo: plastic_love
, 'Mariya Takeuchi'
.
Como curiosidad, para representar valores booleanos se utilizan los átomos true
y false
.
Existen algunas palabras claves reservadas del lenguaje que no pueden ser utilizadas como átomos:
after
,and
,andalso
,band
,begin
,bnot
,bor
,bsl
,bsr
,bxor
,case
,catch
,cond
,div
,end
,fun
,if
,let
,not
,of
,or
,orelse
,query
,receive
,rem
,try
,when
,xor
.
Se puede utilizar caracteres y cadenas de texto en Erlang como valores literales. Para los caracteres hay que utilizar el símbolo $
seguido de la letra en cuestión. Para las cadenas hay que delimitarlas con las comillas dobles ("
). Por ejemplo: $ñ
, "La letra eñe."
.
Realmente, como pasa en tantos otros lenguajes, la representación de cada letra es un valor numérico y por extensión el de una cadena es una lista de números. Es decir, que esta sintaxis es azúcar sintáctico, pero no por ello deja de ser útil su uso.
Se puede dividir la definición de una cadena en varios segmentos si es necesario, sin que ello altere el resultado final. Por lo tanto, es lo mismo la cadena
"abcdefgh"
que la cadena"abcd" "efgh"
.
Las variables son valores asociados a un nombre identificador. Para definir un nombre de variable, se necesita una cadena compuesta por una serie de letras, números, guiones bajos (_
) y/o arrobas (@
), que empieza por una letra mayúscula o un guion bajo. A diferencia de los átomos, que son valores por sí mismos, las variables necesitan ser inicializadas. Una de las formas posibles es utilizar el operador igual (=
) de la siguiente manera:
Year = 1984,
Name = george_orwell.
Otra de las formas, para asignar valores a una variable, es invocar una función con una serie de valores utilizados como argumentos de la aplicación de dicha función. Al invocarla, los valores de los argumentos son asignados a las variables que conforman los parámetros de la función.
Hay que tener en cuenta, que una vez que se asigna un valor a cualquier variable, no se puede volver a asignar otro valor nuevo a la misma, ya que estas son inmutables. De modo que lo siguiente daría un fallo en ejecución:
N = 1,
N = 2.
Es común en lenguajes funcionales que el operador igual no es un operador de asignación, sino de igualdad matemática. Al evaluar el primer uso, N = 1
, al no estar inicializada la variable N
se asume que su valor ha de ser 1
. Pero al evaluar el segundo uso, N = 2
, la variable ya está inicializada y es falsa la igualdad, por lo tanto nuestro programa fallará.
Manejar variables inmutables puede parecer al principio un escollo insalvable, pero ya iremos viendo cómo superar esta aparente dificultad, con el uso de funciones recursivas.
El guion bajo a solas (
_
) es una variable especial que se utiliza en el lenguaje para cuando no nos interesa el valor que tiene asignado. Internamente, al compilarlo, genera una variable fresca para evitar la colisión de nombres entre las diferentes apariciones de esta variable comodín.
Las tuplas son estructuras de datos que agrupan información de forma ordenada con un tamaño fijo. Siguen la siguiente sintaxis:
Por ejemplo: {}
, {0, a}
, {{data, 3.14}, Foo, {}, 8}
.
Las listas son estructuras de datos que agrupan información de forma ordenada con un tamaño variable. Su sintaxis es la siguiente:
Este es el constructor de listas, que podemos hacer una lista vacía con []
, o podemos hacer una lista no vacía donde la primera expresión es el valor en la cabecera de dicha lista y la segunda la continuación. Por ejemplo: [1, [2 | []]]
, que es la lista con los valores 1
y 2
en orden. La continuación, también denominada cola de la lista, puede ser cualquier valor posible, por lo que podemos tener una lista tal que: [z | 80]
.
Que la cola pueda ser cualquier valor puede provocar que la terminación de la lista no sea una lista vacía, en cuyo caso se le denomina como lista impropia, frente a las listas propias. Esta distinción es importante a la hora de analizar el tipo de una expresión, pues hay funciones que sólo funcionan con listas propias, por lo que fallarían al recibir una impropia ya que nunca llegarían a la condición final de parar con la lista vacía.
A diferencia de los lenguajes fuertemente tipados, como es el caso de Haskell, las listas en Erlang pueden ser heterogéneas en relación a los tipos de los valores que contiene, gracias a su naturaleza de lenguaje con tipado dinámico.
Como definir listas más complejas, con el constructor de listas, puede llegar a ser costoso y confuso, existe una sintaxis alternativa para definir listas:
De este modo, la lista [1, [2 | []]]
se puede definir como [1, 2]
, siendo más legible para el programador. Internamente, para la máquina virtual, son la misma cosa porque esta forma de sintaxis es azúcar sintáctico.
Dentro de la biblioteca estándar existe el módulo lists
, con una buena colección de funciones que permiten consultar y transformar listas, algunas de ellas bastante avanzadas.
Los mapas son estructuras de datos que relacionan una clave con un valor. Aunque también se le conoce como diccionarios en otros lenguajes, la biblioteca estándar de Erlang tiene otro tipo de estructura nativa que se llama diccionario (módulo dict
), por lo que usaremos el término mapa para evitar confusiones innecesarias. Los mapas se añadieron como sustitución de los diccionarios implementados, para corregir algunas carencias que tenía el lenguaje.
La sintaxis para crear un mapa es la siguiente:
Tanto las claves, como los valores, pueden ser de cualquier tipo. Para actualizar un mapa previo, usaremos esta sintaxis:
De este modo se devuelve un nuevo mapa, donde se asigna un valor a la clave indicada, existiera esta previamente o no. Existe otra variante para actualizar un mapa previo con:
Con esta versión, si la clave no existe previamente, no se actualizará el contenido del nuevo mapa creado y será simplemente una copia idéntica del mapa que hemos intentado modificar.
El módulo maps
contiene una serie de funciones que permite trabajar con mapas, para poder consultar su contenido o realizar transformaciones avanzadas.
Debido a que Erlang fue diseñado para construir sistemas de telecomunicaciones, existía la necesidad de tener las herramientas para poder procesar protocolos e información a nivel de bytes e incluso de bits. Para ello se tiene en Erlang la siguiente sintaxis:
Los segmentos tienen la siguiente sintaxis:
Los descriptores es una lista de propiedades que definen al dato y que se separan con guiones (-
). Los descriptores disponibles son:
Categoría | Valores | Defecto | Descripción |
---|---|---|---|
Tipo | integer , float , binary , bytes , bitstring , bits , utf8 , utf16 , utf32 |
integer |
Tipo del dato del segmento. El valor bytes equivale a binary y el valor bits a bitstring . |
Signo | signed , unsigned |
unsigned |
Esta propiedad sólo importa cuando el tipo es integer , para indicar si tiene signo o no su formato. |
Orden | big , little , native |
big |
Indica cómo están orientados los bytes, si se trata de una arquitectura big-endian o little-endian. Esta propiedad solamente es relevante para los tipos integer , float , utf16 y utf32 . |
Unidad | unit:Literal (Literal = 1 ..256 ) |
1 (integer , float ,bitstring )8 (binary ) |
Indica el número de bits que se va a usar como unidad al definir el tamaño del segmento. Con los tipos utf8 , utf16 y utf32 no se indica unidad alguna. |
El dato dependerá del tipo indicado en el descriptor y podremos usar números, cadenas de texto o bloques binarios, siempre y cuando se indique el tipo adecuado, de lo contrario obtendremos un error.
El tamaño, dentro del segmento, indica en número de unidades el espacio que ocupa. Por ejemplo, si se trata de un dato de tipo integer
y el tamaño es 16
, como por defecto la unidad de 1 bit, el tamaño final del segmento son 16 bits. Si no se indica el tamaño, por defecto será de 8
unidades cuando sea un integer
, de 64
unidades para float
, y en el caso de binary
y bitstring
se tomará el resto del bloque como tamaño.
La única diferencia relevante entre el tipo
binary
ybitstring
, es que el primero por defecto maneja unidades de 8 bits, mientras que el segundo maneja unidades de 1 bit, por ello conbinary
es requisito que la información que se vaya a definir o encajar sea múltiplo de 8.
Veamos algunos ejemplos:
1> <<_,A/binary>> = <<1, 2, 257:16>>.
<<1,2,1,1>>
2> A.
<<2,1,1>>
3> <<_,B/bitstring>> = <<1, 2, 257:12>>.
<<1,2,16,1:4>>
4> B.
<<2,16,1:4>>
5> <<C,D>> = <<256:16/big>>, {C,D}.
{1,0}
6> <<E,F>> = <<256:16/little>>, {E,F}.
{0,1}
7> <<1024:32, 3.14/float, <<"abc">>/bytes>>.
<<0,0,4,0,64,9,30,184,81,235,133,31,97,98,99>>
8> io:format("~w~n", [<<"Año"/utf8>>]).
<<65,195,177,111>>
9> io:format("~w~n", [<<"Año"/utf16>>]).
<<0,65,0,241,0,111>>
10> io:format("~w~n", [<<"Año"/utf32>>]).
<<0,0,0,65,0,0,0,241,0,0,0,111>>
Los registros es un mecanismo que Erlang tiene para definir estructuras de datos cuyas componentes tienen nombre. Para simplificarlo, sería equivalente a una tupla donde cada componente de la misma tiene un nombre propio con el que acceder a ella.
La ventaja principal, frente a las tuplas, es su mayor flexibilidad a la hora de modificar el tamaño de definiciones previas. Modificar de tamaño una tupla implica tener que revisar todo el código, para actualizar todas las tuplas que deben encajar con la modificada. Sin embargo, con registros se puede aumentar el número de componentes sin romper el código anterior. Además, el uso de registros añade una mayor claridad para entender el acceso a los datos.
Para definir un registro usamos la siguiente sintaxis:
Esto crea un registro con el nombre indicado para el módulo donde ha sido definido. De forma opcional se puede indicar valores por defecto de inicialización cuando se crea un valor. La sintaxis para crear un valor es el siguiente:
En caso de dejar campos sin definir en el constructor, se les asignará el valor undefined
. También se puede usar como campo la variable comodín _
para inicializar todos aquellos que no hayan sido explícitamente indicados en el constructor. Para modificar un valor de registro previo se usa la siguiente sintaxis:
Esto devuelve un nuevo calor con la información modificada. Para pode acceder a un campo se usa la sintaxis:
Esto devuelve el valor asociado al campo. Es perfectamente posible anidar registros dentro de otros registros. Para acceder al contenido simplemente hay que usar la sintaxis anterior aplicada al campo en cuestión.
En versiones anteriores a la
R14
, para acceder al contenido de un registro anidado en otro registro, era necesario usar paréntesis para ayudar al compilador con el análisis. Por suerte, se eliminó esa limitación.
Si necesitamos saber cuál es la posición del campo dentro de la tupla usaremos:
Por ejemplo:
-record(state,{first=1, second=2, third=3}).
foo() ->
S1 = #state{},
S2 = #state{first="ichi", _=null},
S3 = S2#state{second="ni", third="san"},
io:format("S1 = ~p~n", [S1]),
io:format("S2 = ~p~n", [S2]),
io:format("S3 = ~p~n", [S3]),
io:format("first = ~p~n", [#state.first]),
io:format("second = ~p~n", [#state.second]),
io:format("third = ~p~n", [#state.third]),
io:format("S2.first = ~p~n", [S2#state.first]).
Obtenemos por consola el siguiente resultado:
S1 = {state,1,2,3}
S2 = {state,"ichi",null,null}
S3 = {state,"ichi","ni","san"}
first = 2
second = 3
third = 4
S2.first = "ichi"
Como se puede observar, internamente es una tupla la estructura. Es el compilador el que se encarga de analizar el código y traducirlo, sin tener que cambiar la implementación interna de Erlang.
Operador | Descripción |
---|---|
== |
Igualdad |
/= |
Desigualdad |
=< |
Menor o igual que |
< |
Menor que |
>= |
Mayor o igual que |
> |
Mayor que |
=:= |
Igualdad estricta |
=/= |
Desigualdad estricta |
El resultado de las operaciones de comparación son los valores booleanos true
y false
, que pertenecen al conjunto de los átomos. La única particularidad, frente a otros lenguajes, es la distinción entre las versiones de la igualdad y la desigualdad. La igualdad estándar, como la desigualdad estándar, realizan una conversión de tipos cuando se comparan enteros y reales entre sí, de modo que el número entero será convertido a formato de coma flotante. En las versiones estrictas de estos dos operadores no se realiza la conversión, por lo que nunca un entero será igual a un real dentro del lenguaje Erlang. Por ejemplo:
1 == 1.0, % true
1 =:= 1.0. % false
Según la documentación de Erlang, la relación entre los diferentes tipos del lenguaje es la siguiente:
number < atom < reference < fun < port < pid
< tuple < map < nil < list < bit string
Si bien no es necesario conocer esta relación para programar, es importante que un lenguaje esté bien definido y mantenga un orden fijo para la coherencia de las operaciones.
Operador | Descripción | Tipo |
---|---|---|
+ |
Positivo | Número |
- |
Negativo | Número |
+ |
Suma | Número |
- |
Resta | Número |
* |
Multiplicación | Número |
/ |
División | Número |
div |
División entera | Entero |
rem |
Resto de la división | Entero |
Estos operadores funcionan con números y devuelven números como resultado. Salvo en el caso del resto y la división entera, que sólo admiten números enteros, el resto de operadores devolverá como resultado un número entero, salvo que alguno de los operandos sea de coma flotante y por lo tanto se devolverá como resultado un número real.
Operador | Descripción |
---|---|
not |
Negación |
and |
Conjunción |
or |
Disyunción |
xor |
Disyunción exclusiva |
El resultado de estos operadores son valores booleanos, que son los átomos true
y false
. También hay que tener en cuenta que sólo admiten como operandos valores booleanos. Para poder entender mejor estas operaciones, veamos sus tablas de la verdad:
A |
B |
not A |
not B |
A and B |
A or B |
A xor B |
---|---|---|---|---|---|---|
true |
true |
false |
false |
true |
true |
false |
false |
true |
true |
false |
false |
true |
true |
true |
false |
false |
true |
false |
true |
true |
false |
false |
true |
true |
false |
false |
false |
Estos operadores evalúan toda la expresión, aunque se alcance el resultado final tras terminar de evaluar el operando izquierdo. Por ello el lenguaje dispone de orelse
y andalso
, que son versiones de la conjunción y la disyunción con cortocircuito. Es decir, si el operando izquierdo en la conjunción es false
, se devolverá false
sin evaluar el operando derecho. De modo similar, si el operando izquierdo en la disyunción es true
, se devolverá true
sin evaluar el operando derecho.
Operador | Descripción |
---|---|
bnot |
Negación |
band |
Conjunción |
bor |
Disyunción |
bxor |
Disyunción exclusiva |
bsl |
Desplazamiento de bits a la izquierda |
bsr |
Desplazamiento de bits a la derecha |
Todos estos operadores trabajan con números enteros exclusivamente. Los cuatro primeros operadores funcionan igual que los operadores lógicos booleanos, la diferencia es que, en lugar de usar true
y false
, usan los valores binarios 1
y 0
respectivamente. En cuanto a los operadores de desplazamiento de bits, el operando izquierdo es desplazado tantas posiciones como indique el operando derecho:
1> 1024 bsr 5.
32
2> 2 bsl 10.
2048
La representación de los enteros en Erlang no se limita a un tamaño fijo como en otros lenguajes. Para enteros pequeños se utilizan 28 bits en arquitecturas de 32 bits y 60 bits en las de 64 bits. Pero para enteros largos se reservan bloques de memoria divisibles por el ancho de palabra de la arquitectura donde se esté ejecutando. De esta manera se pueden representar números enteros extraordinariamente largos, al punto que la expresión
1 bsl (1 bsl 24)
es ejecutable en arquitecturas de 64 bits.
Operador | Descripción |
---|---|
++ |
Concatenación |
-- |
Eliminación |
Estos dos operadores sólo permiten listas como operandos y devuelven listas. Con ++
se obtiene una nueva lista que concatena dos listas. Con --
se obtiene una nueva lista en la que se ha ido eliminando los elementos de la lista en el operando derecho. Por ejemplo:
1> [1,2,3] ++ [4,5].
[1,2,3,4,5]
2> [1,2,3,2,1,2] -- [2,1,4,2].
[3,1,2]
Como se puede ver, para la eliminación, no salta ningún error si el elemento que se busca eliminar no existe. Con este operador tenemos una forma básica de filtrar valores.
Operador | Descripción |
---|---|
: |
Acceso a módulos |
# |
Modificación de estructuras |
= |
Encaje de patrones |
! |
Envío de mensajes |
catch |
Captura de excepciones |
Estos operadores se pueden ver en más detalle en otras secciones, para explicar mejor los conceptos que manejan.
La precedencia de los operadores es la siguiente:
Operadores | Asociatividad |
---|---|
: |
|
# |
|
Unarios: + , - , bnot , not |
|
/ , * , div , rem , band , and |
Izquierda |
+ , - , bor , bxor , bsl , bsr , or , xor |
Izquierda |
++ , -- |
Derecha |
== , /= , =< , < , >= , > , =:= , =/= |
|
andalso |
|
orelse |
|
= , ! |
Derecha |
catch |
La asociatividad de un operador indica cómo se va evaluando las expresiones, si es de izquierda a derecha o al revés. Los operadores aritméticos se evalúan de izquierda a derecha, por lo que primero se resuelven aquellas operaciones que están a la izquierda de otra operación con el mismo nivel de prioridad.
Si necesitamos indicar de forma explícita la precedencia de una operación sobre otra, podemos usar el operador de paréntesis (
)
de forma idéntica al uso que le damos al operar en matemáticas.
Una de las propiedades del lenguaje Erlang es el encaje de patrones, este se realiza en múltiples circunstancias, la más obvia de ellas es usando el operador =
, que vimos en la sección sobre las variables. El encaje de patrones sirve para dos cometidos:
Veamos el siguiente ejemplo para entenderlo mejor:
A = 5,
B = {A, A * 2},
{_, C} = B,
10 = C.
La primera expresión asigna el valor 5
a la variable A
. La segunda expresión asigna a la variable B
una tupla de dos componentes, la primera con el valor de A
y la segunda con el valor de A
multiplicado por 2
. En la tercera expresión, se realiza un ajuste de patrón, para asignar en la variable C
el valor de la segunda componente de la tupla que está asignada a B
. Como la primera componente no nos interesa, usamos la variable comodín _
para descartar dicha información. Por último, comprobamos que el contenido de C
es el valor 10
usando la expresión 10 = C
en el ejemplo, aunque también podríamos haber usado C = 10
porque actúan de forma idéntica las dos.
Este mismo comportamiento, que ocurre con el operador =
, veremos que también se aplica con las cláusulas al llamar una función o al utilizar las expresiones case
, receive
y try
.
Las cláusulas en Erlang son una construcción que permite ajustar un valor a un patrón determinado, siempre que se cumplan una serie de condiciones que denominaremos guardas. De modo que su sintaxis sería algo tal que:
Para la declaración de funciones, la sintaxis varía ligeramente porque el ajuste se realiza sobre cero o más parámetros, recibidos en la invocación de dicha función.
Para ser más precisos, un patrón es una expresión que define una estructura de datos y que contiene variables y valores literales. Si las variables están ya ligadas a un valor, se comprobará que se ajusten los valores con lo que se intenta encajar, si no están ligadas se asignará el valor que se está encajando. Por ejemplo, cuando se utiliza el operador =
, el lado izquierdo ha de ser un patrón, mientras que el derecho es la expresión que nos da el valor que se va a intentar encajar.
En cuanto a las guardas, estas son expresiones booleanas. Si no se indica ninguna guarda, por defecto se utiliza el valor true
internamente. Una condición que debe cumplir las guardas, es que no debe tener ningún efecto colateral al evaluarse. Para ello, están limitados los elementos que pueden formar parte de una guarda a los siguientes:
Expresión#Nombre.Campo
y #Nombre.Campo
.andalso
y orelse
.is_atom/1
, is_binary/1
, is_bitstring/1
, is_boolean/1
, is_float/1
, is_function/1
, is_function/2
, is_integer/1
, is_list/1
, is_map/1
, is_number/1
, is_pid/1
, is_port/1
, is_record/2
, is_record/3
, is_reference/1
, is_tuple/1
.abs/1
, bit_size/1
, byte_size/1
, element/2
, float/1
, hd/1
, is_map_key/2
, length/1
, map_get/2
, map_size/1
, node/0
, node/1
, round/1
, self/0
, size/1
, tl/1
, trunc/1
, tuple_size/1
.En la sección sobre las funciones, se habla en más detalle sobre las funciones nativas del lenguaje que hay en Erlang. Volviendo a las guardas, podemos tener una secuencia de ellas utilizando una de estas dos formas:
Usando la coma (,
) es requisito que todas las guardas den como resultado true
, mientras que con el punto y coma (;
) sólo es necesario que una de las guardas sea cierta. Esta sintaxis vendría a ser un equivalente de usar andalso
para el caso de la coma y orelse
para el caso del punto y coma, la principal diferencia es que usando operadores no se capturan las excepciones cuando se producen, es decir, hd(1) orelse true
fallaría, pero hd(1); true
tendría éxito.
Podemos usar la siguiente sintaxis como patrón de encaje con mapas:
Como requisito, para que funcione correctamente, las claves tienen que cumplir los mismos requisitos que cumplen las guardas de las cláusulas, lo cual implica que todas las variables internas han de estar previamente ligadas. Si las claves son encontradas, los valores de estas son ajustados a los patrones definidos.
En caso de no encontrar alguna de las claves indicadas, se lanzará una excepción de tipo badmatch
si el encaje se realiza mediante el operador =
. Si el encaje se está realizando en el patrón de una cláusula, en caso de fallar el ajuste se pasará a la siguiente cláusula.
Podemos usar la siguiente sintaxis como patrón de encaje con registros:
Funciona parecido a las tuplas, a la hora de hacer un ajuste de patrones, pero con algo más de flexibilidad en cuanto a la posición de los componentes.
Además de poder crear listas mediante literales y con el uso de operadores, podemos utilizar las listas intensionales para crear nuevas listas a partir de otra, realizando filtrados y transformaciones. Para ello existe la siguiente sintaxis:
Donde la expresión generadora puede ser una de las siguientes:
El primer tipo de generador ajusta un patrón con cada elemento de la lista. El segundo hace lo mismo que el primero pero con cada elemento dentro de un bloque binario. Finalmente, podemos usar guardas como predicados para filtrar los elementos de la lista de entrada, de modo que se usarán aquellos elementos que den como resultado true
con el predicado, y aquellos que den false
serán descartados. Por ejemplo:
1> L=[3,e,4.5,f,{1,2},7].
[3,e,4.5,f,{1,2},7]
2> [X || X <- L, is_integer(X)].
[3,7]
La lista intensional [X || X <- L, is_integer(X)]
nos devuelve sólo los números enteros de L
. Obviamente 4.5
, aunque es un número, no es un entero y por ello queda descartado.
De forma análoga a las listas, con los bloques binarios podemos también crear bloques binarios intensionales con la siguiente sintaxis:
Los generadores que se usan son los mismos que usamos con las listas.
Las funciones son bloques de código que realizan diferentes tareas. Para realizar programas y algoritmos necesitamos descomponer el problema en diferentes funciones.
Para poder ejecutar una función tenemos que invocarla usando la siguiente sintaxis:
Indicando el módulo podemos llamar a funciones que están en otros módulos. Si se omite el módulo, se asume que estamos usando funciones del módulo actual.
Hay que tener en cuenta que las funciones son valores para el lenguaje, por lo que podemos usarlas como parámetros de otras funciones y devolverlas. Por lo tanto, Erlang es un lenguaje con funciones de orden superior. Entonces, si queremos referenciar a una función con nombre como un valor, usaremos la siguiente sintaxis:
De este modo, con fun hello:world/0
tendríamos el valor que representa a la función world
dentro del módulo hello
. Si no indicamos el módulo, se asume que se trata del módulo actual que estemos codificando. Una vez está una variable ligada a una función, podemos usar la variable para invocar la función, pasando entre paréntesis los parámetros que necesita. Por ejemplo:
1> l(hello).
{module,hello}
2> Hi = fun hello:world/0.
fun hello:world/0
3> Hi().
Hello, world!
Para poder definir funciones se utiliza la siguiente sintaxis:
Como podemos ver, lo que tenemos aquí es una secuencia de cláusulas que componen la función. Los patrones y expresiones son secuencias separadas por comas de patrones y expresiones respectivamente. Cada patrón representa los argumentos de la función y las expresiones son el cuerpo de la función, es decir:
La diferencia es que, mientras que podemos tener una función sin argumentos, el cuerpo de la función requiere al menos una expresión. El caso de que no se indique la guarda para la cláusula funcional, se asume por defecto como guarda el valor true
. Por ejemplo:
fact(N) when N > 0 ->
N * fact(N - 1);
fact(0) ->
1.
La función fact
calcula el factorial, para ello tiene la cláusula recursiva primero y segundo el caso base. Hay que entender que el orden de las cláusulas es importante, porque para evaluar cual hay que seleccionar se hace en orden de definición, escogiendo la primera que permita ajustar los parámetros de entrada con sus patrones y su guarda sea cierta. Por ejemplo:
foo(X) when X >= 0 -> up;
foo(X) when X =< 0 -> down.
bar(X) when X =< 0 -> down;
bar(X) when X >= 0 -> up.
test() -> foo(0) =:= bar(0).
El resultado de test()
es el valor false
, ya que aplicar el valor 0
a foo
y bar
da resultados distintos aunque el código parezca el mismo. Esto es porque hay superposición de casos entre las cláusulas y se escogerá la primera que se pueda usar con éxito. Por ello, cuando usemos la variable comodín _
, como patrón de ajuste, es importante usarla en una cláusula que no bloquee el acceso a las siguientes salvo que haya una muy buena razón.
También se pueden definir funciones anónimas, también conocidas como funciones lambda. Para ello se utiliza la siguiente sintaxis:
La sintaxis es muy similar a la declaración de funciones normales, pero las lambdas no tienen nombre propio, por ello para poder realizar lambdas recursivas se puede utilizar una variable para invocar a la función anónima desde dentro. Por ejemplo:
foo() ->
fun Fact(N) when N > 0 ->
N * Fact(N - 1);
Fact(0) ->
1
end.
La función foo
nos devuelve una función que contiene la función factorial.
Las expresiones lambda crean un ámbito nuevo para las variables, por lo tanto, si definimos una variable
X
como parámetro de entrada y existe la misma variable fuera de la lambda, la variable interna ocultará el acceso a la exterior.
Otro aspecto importante al diseñar funciones, es la recursión de cola. En programación funcional la recursión es esencial, porque la iteración se realiza mediante la recursión.
Si el resultado de la llamada recursiva se tiene que utilizar para realizar más cálculos, se tiene que almacenar en la pila de llamadas la información que contiene la llamada actual, para que no se pierda al evaluar las siguientes iteraciones recursivas. Aunque dispongamos de muchos recursos en cuanto a memoria, en determinadas circunstancias se puede provocar un desbordamiento de pila por realizarse una cantidad grande de llamadas a función anidadas.
La recursión de cola se produce cuando la expresión final a devolver es la llamada recursiva a la función, por lo que todos los parámetros de la llamada se evalúan antes de la llamada y no hace falta guardar en la pila ninguna información. La ventaja es que este tipo de recursión no puede desbordar la pila y nos sirve, por ejemplo, para hacer bucles infinitos cuando necesitamos un servidor que recibe y envía mensajes. Para entenderlo mejor, vamos a ver el ejemplo del factorial con recursión de cola:
fact(N) ->
ifact(N,1).
ifact(N, R) when N > 0 ->
ifact(N - 1, R * N);
ifact(0, R) ->
R.
La primera expresión de control es el case
que tiene la siguiente sintaxis:
Las expresiones case
sirven para ramificar la ejecución en base al resultado de una expresión dada como discriminante. Una vez evaluada la expresión, se toma el valor final y se intenta ajustar con las cláusulas definidas. Como pasaba con las funciones, las cláusulas van siendo probadas en el orden en el que están definidas y la primera que logre ajustar el valor, y pasar su guarda, será la que se ejecute finalmente. Por ejemplo:
fact(N) ->
case N of
(N) when N > 0 ->
N * fact(N - 1);
(0) ->
1
end.
Aquí vemos la implementación del factorial usando un case
. Erlang, internamente, convierte las cláusulas funcionales en expresiones case
al compilar los módulos, pero por comodidad y limpieza es mejor usar cláusulas funcionales.
La expresión if
tiene la siguiente sintaxis:
Las expresiones if
también sirven para ramificar la ejecución, pero esta ramificación se hace en base al cumplimiento de una serie de condiciones descritas en las guardas de cada rama. Hay que señalar que las guardas en la expresión if
no son tan restrictivas como las guardas originales, en estas sí podemos usar funciones de propias. Esto es posible porque internamente al compilar el módulo se transforma en una expresión case
.
Podemos tener bloques de expresiones usando la siguiente sintaxis:
El resultado de la expresión, igual que ocurre con el cuerpo de una cláusula, es el valor de evaluar la expresión final del bloque. Esto puede ser útil si uno quiere anidar una secuencia de expresiones en una posición de la sintaxis que sólo permite una única expresión (por ejemplo, las componentes de una tupla).
Erlang dispone de mecanismos para gestionar errores durante la ejecución, como ocurre con otros lenguajes modernos. Sin embargo, los creadores del lenguaje recomiendan la filosofía del “let it crash”, basada en dejar morir un proceso si falla y crear uno nuevo en su lugar. Dicho lo cual, en algunas ocasiones puede ser útil lanzar y gestionar excepciones.
La sintaxis para capturar excepciones es la siguiente:
Aunque son opcionales, las secciones of
, catch
y after
, es necesario que exista al menos una sección catch
o after
. La sección of
funciona como una expresión case
. La sección catch
trata de ajustar las excepciones a unos patrones y ejecutará una serie de expresiones siempre que encaje el valor y la guarda se cumpla. La sección after
es un bloque de código que se ejecutará independientemente de si se ha producido una excepción o no en tiempo de ejecución. Si la excepción no es gestionada, por ninguna cláusula de la sección catch
, se lanzará fuera de la expresión try
.
Para ajustar una excepción, tenemos que hacer el encaje con tres elementos: la clase, un patrón y la información de pila. En Erlang hay tres clases de excepciones:
error
: Producidas por las funciones error/1
o error/2
.exit
: Producidas por la función exit/1
.throw
: Producidas por la función throw/1
.Estas son funciones nativas del lenguaje del módulo erlang
. El primer argumento en todas es el valor que representa cuál es el motivo de la excepción, este valor es el que tiene que encajar con el patrón en la cláusula del catch
. El valor que tiene que encajar con pila en la cláusula es la información de pila para la depuración que acompaña a las excepciones de clase error
.
La primera sección de la cláusula de captura de excepciones Clase:Patrón:Pila
tiene partes opcionales. Si no es de clase error
, podemos prescindir de :Pila
. Si se omite Clase:
se asume por defecto el valor throw
y, por lo tanto, sólo podremos usar Patrón
para el ajuste.
También existe la función
erlang:raise/3
para lanzar excepciones, donde el primer parámetro es la clase, el segundo el motivo y el tercero la información de pila para la depuración.
Otro mecanismo para capturar excepciones es:
Esta expresión es azúcar sintáctico de la anterior y lo que hace es capturar toda excepción y devolverla como un valor, en caso de producirse un fallo. Si no hay error alguno, devuelve el valor al que se evalúa la expresión. Usar catch
sería lo mismo que usar el siguiente código:
try
expresión
catch
throw:Motivo ->
Motivo;
exit:Motivo ->
{'EXIT', Motivo};
error:Motivo:Pila ->
{'EXIT', {Motivo, Pila}}
end
Los módulos en Erlang son la unidad en la que se organiza el código de nuestros proyectos. Todo módulo se compone en una secuencia de atributos y declaración de funciones, terminadas con punto cada una de ellas.
Aunque Erlang es un lenguaje donde las variables obtienen su tipo de forma dinámica, el lenguaje nos permite definir tipos para documentar los módulos usando la especificación de tipos. Que un lenguaje no requiera indicar el tipo de sus variables, no quiere decir que este lenguaje no tenga un sistema de tipos, por ello también es importante conocer cuáles son los tipos con los que trabaja el lenguaje.
Todo atributo en Erlang tiene la siguiente sintaxis:
Donde la etiqueta es un átomo y los valores son expresiones literales. Esta es la lista de atributos básicos que se puede definir para un módulo:
Etiqueta | Parámetros y Tipos | Descripción |
---|---|---|
module |
Nombre: atom() |
Declara cuál es el nombre del módulo. Por requisitos técnicos, el nombre del fichero y del módulo han de ser el mismo, exceptuando por la extensión .erl . |
export |
Funciones: [atom()/integer()] |
Declara cuáles son las funciones públicas del módulo, aquellas que pueden ser accesibles desde otros módulos. El parámetro funciones es una lista con los identificadores de las funciones, que tienen la sintaxis Nombre/Aridad . |
import |
Nombre: atom() Funciones: [atom()/integer()] |
Importa una lista de funciones dentro del módulo actual, para no necesitar usar el operador : al invocar dichas funciones, usando únicamente el nombre de las mismas. |
compile |
Opciones: option() o [option()] |
Añade opciones de compilación extras al compilar el módulo. El parámetro opciones puede ser una sola opción o una lista de ellas, las cuales están descritas en la documentación del módulo compile . |
vsn |
Versión: any() |
Declara la versión del módulo. La versión es cualquier literal y se puede obtener con la función version/1 del módulo beam_lib . |
on_load |
Función: atom()/integer() |
Indica qué función, dentro del módulo, ha de ser invocada al cargarse. |
behaviour |
Nombre: atom() |
Indica que el módulo implementa los callbacks que definen a un comportamiento. |
Cuando en Erlang se usa la forma
Nombre/Aridad
, el compilador lo traduce a la expresión{Nombre,Aridad}
.
module_info
Todo módulo contiene dos funciones generadas por el compilador que son module_info/0
y module_info/1
, que devuelven información relativa al módulo en cuestión. El resultado de module_info/0
devuelve una lista de tuplas {Clave,Valor}
, mientras que module_info/1
recibe como parámetro la clave y te devuelve el valor asociado. Estas son las claves disponibles:
Clave | Tipo | Descripción |
---|---|---|
module |
atom() |
Devuelve el nombre del módulo. |
attributes |
[atom(),any()] |
Devuelve los atributos del módulo mediante una lista de tuplas {Etiqueta, Valores} . |
compile |
[option()] |
Devuelve una lista con las opciones usadas para compilar el módulo. |
exports |
[{atom(),integer()}] |
Devuelve una lista con las funciones públicas del módulo. |
functions |
[{atom(),integer()}] |
Devuelve una lista con todas las funciones del módulo. |
md5 |
binary() |
Devuelve un bloque binario que representa la suma de verificación MD5 del módulo. |
native |
boolean() |
Devuelve si el módulo contiene funciones nativas. |
nifs |
[{atom(),integer()}] |
Devuelve una lista con todas las funciones nativas del módulo. |
El preprocesador en Erlang nos permite realizar operaciones de sustitución durante la compilación de un módulo. Una de las operaciones es el incluir ficheros externos dentro del módulo actual, para insertar definiciones que necesitemos. Para ello usaremos:
Habitualmente los ficheros que se insertan son ficheros .hrl
, que son ficheros de cabecera con macros y definiciones de registros de uso compartido entre varios módulos de nuestro proyecto. Existe una variante que es -include_lib(fichero)
, que sirve para incluir cabeceras de la biblioteca estándar de Erlang. Por ejemplo:
-include_lib("kernel/include/file.hrl").
La otra operación importante del preprocesador son las macros, que realizan sustituciones dentro del módulo. Para definir macros la sintaxis es la siguiente:
Lo primero es indicar el nombre identificador de la macro, que por convención se suele usar un nombre en mayúsculas. Después, dependiendo de si queremos parametrizar o no la macro, podemos poner una secuencia de variables como argumentos de entrada para la marco. Finalmente, tendremos una expresión que será usada como resultado final, después de sustituir las variables definidas como parámetros de entrada.
Para invocar una macro se usa la siguiente sintaxis:
Por ejemplo:
-define(VERSION, "1.0").
-define(PRINTLN(V), io:format("~s := ~p~n", [??V, V])).
foo() ->
Victim = ?VERSION,
?PRINTLN(Victim).
Si invocamos foo
obtendremos como salida Victim := "1.0"
. Esto es porque hemos asignado a la variable Victim
el valor que representa la macro ?VERSION
y luego hemos invocado una macro con parámetros para mostrar una información por pantalla.
Nótese que dentro de la macro PRINTLN
se utiliza ??V
con la variable de entrada V
de la macro. Este mecanismo, de poner ??
delante de una variable de entrada, hace que se tome la expresión de entrada usada con la macro y se convierta a una cadena de texto. De ese modo, como la expresión de entrada de PRINTLN
era la variable Victim
, el resultado nos muestra eso mismo al aplicar la función io:format/2
.
Se puede sobrecargar un mismo identificador de macro, a excepción de las macros predefinidas del lenguaje, para poder tener diferentes macro parametrizadas con distinto número de argumentos de entrada.
Estas son algunas de las macros predefinidas por el lenguaje:
Nombre | Descripción |
---|---|
?MODULE |
Nombre del módulo actual. |
?MODULE_STRING |
Nombre del módulo actual como cadena. |
?FILE |
Nombre del fichero del módulo actual. |
?LINE |
Número de línea actual. |
?MACHINE |
Nombre de la máquina: 'BEAM' . |
?FUNCTION_NAME |
Nombre de la función actual. |
?FUNCTION_ARITY |
Número de argumentos de la función actual. |
?OTP_RELEASE |
Versión actual de Erlang/OTP. |
También podemos controlar parte del flujo del preprocesador con:
Comando | Descripción |
---|---|
-undef(Macro). |
Borra la definición de la macro para el módulo actual a partir de esa posición dentro del fichero. |
-ifdef(Macro). |
Permite acceder a las líneas siguientes si la macro indicada ha sido definida. |
-ifndef(Macro). |
Permite acceder a las líneas siguientes si la macro indicada no ha sido definida. |
-if(Condición). |
Permite acceder a las líneas siguientes si la condición indicada se cumple, dando como resultado true . |
-elif(Condición). |
Sólo se pueden usar después de un bloque -if o -elif . Permitirá acceder a las líneas siguientes si la condición se cumple y no se ha cumplido ninguna de las condiciones de los bloques anteriores. |
-else. |
Sólo se pueden usar después de un bloque -ifdef o -ifndef . Si no se ha cumplido ninguna de las condiciones previas, permite acceder a las líneas siguientes. |
-endif. |
Cierra el último bloque de control condicional del preprocesador. |
Estos comandos no se pueden utilizar en el interior de la definición de una función. Normalmente se utilizan para tener diferentes versiones de código, dependiendo de alguna condición o de la existencia de una definición de macro previa.
Existen dos comandos adicionales para interactuar con la compilación de un módulo. Con -error(Expresión)
podemos emitir un mensaje de error y con -warning(Expresión)
podemos emitir un mensaje de aviso. Ambos comandos mostrarán dichos mensajes en tiempo de compilación.
La concurrencia es una de las características principales del lenguaje Erlang, para ello podemos crear procesos que sean ejecutados aparentemente a la vez que otros. La arquitectura de Erlang permite que estos procesos sean ligeros y se puedan crear y destruir rápido.
Dentro de cada instancia iniciada de la máquina virtual, hay una serie de planificadores (scheduler) que tienen una cola de ejecución (run queue), para organizar qué procesos gestiona y su orden de ejecución. Por cada núcleo del procesador se tiene un planificador (salvo que se desactive el SMP), que organiza la ejecución de los procesos a su cargo de forma concurrente pero no paralela.
También es posible hacer aplicaciones distribuidas, para ello hay que configurar una red de instancias de la máquina virtual como nodos. Cada nodo tendrá sus propios planificadores y además cualquier nodo puede encargar la ejecución de un proceso a otro nodo.
Para crear procesos tenemos la función spawn
:
Ambas funciones devuelven como resultado un identificador de proceso o PID, cuyo tipo es pid()
. El PID obtenido nos permitirá poder comunicarnos con el proceso creado. El parámetro función de spawn/1
tiene que ser un valor funcional, mientras que en spawn/3
los dos primeros parámetros han de ser un átomo y a continuación una lista con los argumentos necesarios para invocar la función. Por ejemplo:
1> spawn(fun() -> erlang:system_time() end).
<0.81.0>
2> spawn(fun erlang:system_time/0).
<0.86.0>
3> spawn(erlang, system_time, []).
<0.88.0>
Vemos varias formas de crear un proceso con la función erlang:system_time/0
, que también podría haber incluido usar una variable que esté ligada a un valor funcional. Los resultados que vemos en la consola de Erlang, al usar spawn
, es el PID devuelto que representa al proceso creado.
La comunicación entre procesos se realiza mediante el paso de mensajes. Para enviar uno se usa la siguiente sintaxis:
Mientras que el mensaje puede ser cualquier expresión, el proceso puede ser identificado mediante un PID (pid()
), una referencia (reference()
), un puerto de comunicación (port()
), un nombre registrado (atom()
), o una tupla {Nombre, Nodo}
, en la que ambas componentes son nombres que identifican al proceso y al nodo donde se encuentra. El resultado, de la expresión de envío, es la propia expresión enviada.
La expresión
!
es azúcar sintáctico de la funciónerlang:send/2
, siendo esta la función primitiva que realmente se usa al ejecutar el programa.
Para recibir mensajes necesitamos la sintaxis siguiente:
Esencialmente receive
es como case
aplicado a los valores de la cola de mensajes del proceso. Se comportan igual en cuanto al funcionamiento de las cláusulas, la diferencia es que receive
dispone de una cláusula after
, que requiere de un valor que representa el máximo tiempo de espera en milisegundos para recibir un mensaje. Si el tiempo de espera se ha agotado, se ejecuta el cuerpo de expresiones y se sale de la expresión receive
. Los valores que admite son enteros entre 0
y 4294967295
. Si se omite la cláusula after
, por defecto se asigna al tiempo el átomo infinity
, indicando así que se ha de esperar indefinidamente hasta recibir un mensaje.
foo() ->
PID = spawn(
fun Loop() ->
receive
stop ->
stop;
X ->
io:format("~p~n", [X]),
Loop()
end
end
),
PID ! "Hello",
PID ! {data, [a,b,c]},
PID ! stop,
PID ! "Bye".
Al ejecutar esta función veremos los mensajes "Hello"
y {data, [a,b,c]}
, pero no veremos "Bye"
porque el proceso ya finalizó.
Erlang permite poner asociar a un PID un átomo como nombre. Para gestionarlo tenemos las siguientes funciones nativas:
Función | Descripción |
---|---|
register(Nombre, PID) |
Asocia un átomo como nombre para un PID. |
registered() |
Da la lista de todos los nombres registrados. |
whereis(Nombre) |
Da el PID asociado a un nombre registrado o undefined si no está registrado. |
Además podemos crear un alias para un proceso con la función alias()
, que devuelve una referencia asociada al proceso que invoca la función. Con unalias(Referencia)
se desactiva el alias registrado.
Erlang permite dos modos de controlar la muerte prematura de procesos. El primero es con enlaces, que conecta dos procesos entre sí y cuando uno muere el otro también lo hace recibiendo una excepción. Podemos crear un proceso enlazado al actual con:
Si queremos enlazar un proceso ya creado al actual usaremos link(PID)
, pudiendo revertir el enlace con unlink(PID)
. También se puede cambiar el comportamiento por defecto, para capturar la excepción como si fuera un mensaje recibido con process_flag(trap_exit, true)
, mensajes que tendrían la forma:
El pid es el identificador de proceso que ha muerto y el motivo es la información relativa a la excepción, que dependiendo del tipo tendrá la siguiente forma:
Tipo | Forma |
---|---|
exit(Valor) |
Valor |
error(Valor) |
{Valor, Pila} |
throw(Valor) |
{{nocatch, Valor}, Pila} |
Al activar el flag
trap_exit
, cuando termine un proceso de forma normal se recibirá un mensaje con la forma{'EXIT', PID, normal}
, siendo PID el identificador del proceso que acaba de terminar. Esto es importante, porqueexit(normal)
es considerada una terminación normal del proceso, por lo que usarlo no finalizará al proceso enlazado si no tiene el flagtrap_exit
activado.
El segundo modo es con monitores, que conecta dos procesos entre sí, donde uno es el monitor y el otro el monitorizado. Cuando el proceso monitorizado muere, el proceso monitor recibe un mensaje con la forma:
Podemos crear un proceso monitorizado por el actual con:
El otro método, para monitorizar un proceso, es usando la función monitor(process, PID)
para activarlo, que nos devuelve una referencia para identificar la relación, y demonitor(Referencia)
para desactivarlo.
Es posible finalizar la ejecución de un proceso con la función exit(PID, Motivo)
. Al usar el átomo kill
, se asume que se está matando al proceso de forma abrupta, obteniendo killed
como motivo de la excepción al capturarla con un mensaje. Se pueden usar otros valores para terminar un proceso, pero intentarlo con normal
no funcionará. Si un proceso termina desde dentro con exit(kill)
, el motivo que se capturará como mensaje es kill
en lugar de killed
.
Todo proceso tiene asociado al mismo un diccionario interno. Lo podemos manejar con las siguientes funciones:
Función | Descripción |
---|---|
put(Clave, Valor) |
Asigna un valor a una clave. |
get(Clave) |
Obtiene el valor de una clave. Si no existe se devuelve undefined . |
get() |
Devuelve el contenido como una lista {Clave, Valor} . |
get_keys(Valor) |
Obtiene una lista con las claves que tienen el valor indicado. |
erase(Clave) |
Borra una clave del diccionario. Si la clave existe devuelve el valor asociado y si no undefined . |
erase() |
Borra el contenido del diccionario, devolviéndolo como una lista {Clave, Valor} . |
Para crear un nodo hace falta iniciar la máquina virtual de Erlang dándole un nombre de nodo. Las opciones para configurar un nodo con el comando con erl
son:
Opción | Descripción |
---|---|
-sname Nombre |
Activa el nodo con un nombre corto. |
-name Nombre |
Activa el nodo con un nombre largo. |
-setcookie Cookie |
Configura la cookie del nodo. |
-setcookie Nodo Cookie |
Configura la cookie del nodo. |
-hidden |
El nodo será invisible al resto de la red de nodos. |
-connect_all false |
Sólo se permiten las conexiones explícitas. |
Un nombre de nodo suele ser un átomo con la forma Nombre@Máquina
, donde máquina varía dependiendo de si se elige un nombre corto o largo. La red de nodos de Erlang no está preparada para temas de seguridad, pues el sistema fue diseñado en los años 80, cuando el acceso a las redes era más rudimentario. Para separar las redes de nodos se puede usar una cookie, que es un átomo, para que sólo puedan conectarse aquellos nodos que comparten la misma cookie.
Algunas funciones nativas para gestionar los nodos son:
Función | Descripción |
---|---|
node() |
Da el nombre del nodo actual. |
node(Valor) |
Da el nombre del nodo que encaja con el valor indicado, que puede ser un PID, una referencia o un puerto. |
nodes() |
Da la lista de nodos actual. |
nodes(Valor) |
Da la lista de nodos actual en base a la opción indicada como valor. |
is_alive() |
Devuelve si el nodo actual puede conectarse al resto de nodos. |
monitor_node(Nodo, Bool) |
Monitoriza el estado de un nodo, recibiendo {nodedown, Nodo} como mensaje si se pierde la conexión. |
erlang:get_cookie() |
Da la cookie actual del nodo. |
erlang:get_cookie(Nodo) |
Da la cookie actual de un nodo. |
erlang:set_cookie(Cookie) |
Cambia la cookie actual del nodo. |
erlang:set_cookie(Nodo, Cookie) |
Cambia la cookie actual de un nodo. |
erlang:disconnect_node(Nodo) |
Desconecta un nodo de la red. |
Además podemos crear un proceso en un nodo que queramos con:
También se puede aplicar esto para el caso de spawn_link
y spawn_monitor
.
Lo comportamientos en Erlang es un tipo de interfaz que un módulo puede implementar. Esta interfaz tienen que tener una serie de funciones determinadas, para que el módulo, que define la interfaz de comportamiento, pueda operar con el módulo que la implementa. Estas funciones que ha de tener un módulo para implementar un comportamiento se llaman callbacks. Declaramos la implementación de un comportamiento con el siguiente atributo:
El nombre es un átomo con el nombre del módulo que define el comportamiento en cuestión, ya sea uno definido por el usuario o uno de los siguientes de la biblioteca estándar de OTP: gen_server
, gen_statem
, gen_event
, supervisor
. Erlang también permite usar behavior
como etiqueta del atributo.
Para crear una interfaz de comportamiento propia, dentro de nuestro módulo tendremos que indicar una lista de atributos que definan los callbacks a implementar, usando una sintaxis similar a la especificación de tipos:
Además tenemos el atributo de módulo optional_callbacks
, que tiene como valor una lista de los callbacks (Nombre/Aridad
) que son opcionales para implementar la interfaz de comportamiento.
Por ejemplo, creamos la siguiente interfaz de comportamiento:
-module(foobeh).
% Otros atributos
-callback ping() -> boolean().
-callback pong() -> boolean().
% Otras funciones
Para implementarla tendremos que:
-module(foo).
-behaviour(foobeh).
-export([ping/0, pong/0]).
% Otros atributos
ping() -> true.
pong() -> false.
% Otras funciones
gen_server
Este comportamiento se utiliza para crear servidores genéricos que ofrecen una serie de servicios mediante peticiones. Los eventos que se han de implementar son:
Función | Parámetros | Resultados | Descripción |
---|---|---|---|
init |
(Argumentos) |
{ok,Estado} {ok,Estado,Extra} {stop,Motivo} ignore |
Inicialización del servidor. |
handle_call |
(Petición, Origen, Estado) |
{reply,Resultado,Estado} {reply,Resultado,Estado,Extra} {noreply,Estado} {noreply,Estado,Extra} {stop,Motivo,Estado} {stop,Motivo,Resultado,Estado} |
Peticiones con respuesta. |
handle_cast |
(Petición, Estado) |
{noreply,Estado} {noreply,Estado,Extra} {stop,Motivo,Estado} |
Peticiones sin respuesta. |
handle_info |
(Mensaje, Estado) |
{noreply,Estado} {noreply,Estado,Extra} {stop,Motivo,Estado} |
Mensajes recibidos que no son peticiones para el servidor. |
code_change |
(Versión, Estado, Extra) |
{ok,Estado} {error,Motivo} |
Cambio en caliente. |
terminate |
(Motivo, Estado) |
- | Terminación del servidor. |
Sólo son obligatorias init
, handle_call
y handle_cast
, el resto son opcionales. En el caso de terminate
podemos devolver cualquier valor que necesitemos. En el caso de handle_info
, uno de sus usos es gestionar los mensajes que recibe cuando está enlazado o monitoriza otro proceso.
Las opciones extra en la respuesta pueden ser Timeout
e hibernate
. El primero es un número entero de tiempo de espera en milisegundos o el átomo infinity
, que es el valor por defecto como opción. El segundo envía al servidor a un estado de hibernación, a la espera de recibir un mensaje para reactivarse.
El cambio en caliente (hot swapping) se refiere a cuando se carga en la máquina virtual otra versión de un módulo ya cargado. Comportamientos como el
gen_server
reaccionan ante esta eventualidad invocando acode_change
, donde la versión puede ser o bien un átomo que identifica la nueva versión, o tener la forma{down, Versión}
cuando se trata de cargar una versión anterior. Con este evento podemos transformar la información de estado del servidor, para adaptarla a la siguiente versión cargada.
Las operaciones que gestionan el comportamiento están en el módulo gen_server
, entre las que tenemos las siguientes funciones:
Función | Parámetros | Descripción | Evento |
---|---|---|---|
start |
(Módulo, Argumentos, Opciones) (Nombre, Módulo, Argumentos, Opciones) |
Crea un proceso que ejecuta el servidor. | init |
start_link |
(Módulo, Argumentos, Opciones) (Nombre, Módulo, Argumentos, Opciones) |
Crea un proceso enlazado que ejecuta el servidor. | init |
start_monitor |
(Módulo, Argumentos, Opciones) (Nombre, Módulo, Argumentos, Opciones) |
Crea un proceso monitorizado que ejecuta el servidor. | init |
stop |
(Identificador) (Identificador, Motivo, Timeout) |
Detiene un servidor creado. | terminate |
call |
(Identificador, Petición) (Identificador, Petición, Timeout) |
Envía una petición que espera una respuesta de forma síncrona. | handle_call |
cast |
(Identificador, Petición) |
Envía una petición que no espera una respuesta de forma asíncrona. | handle_cast |
El parámetro nombre, cuando se inicia un servidor, sirve para registrar el proceso con un átomo, para ello se puede usar {local,Nombre}
o {global,Nombre}
, entre otras opciones. Al indicar local
se registra el proceso sólo en el nodo actual, mientras que con global
se registra en la red de nodos.
gen_statem
Este comportamiento se utiliza para crear máquinas de estados y sustituye al módulo gen_fsm
. Los eventos que se han de implementar son:
Función | Parámetros | Resultados | Descripción |
---|---|---|---|
init |
(Argumentos) |
{ok,Estado,Datos} {ok,Estado,Datos,Acciones} {stop,Motivo} ignore |
Inicialización de la máquina de estados. |
callback_mode |
() |
state_functions handle_event_function [state_functions,state_enter] [handle_event_function,state_enter] |
Configuración del modo de la máquina de estados. |
Estado |
(enter, EstAnt, Datos) (Evento, Mensaje, Datos) |
{next_state,Estado,Datos} {next_state,Estado,Datos,Acciones} {keep_state,Datos} {keep_state,Datos,Acciones} keep_state_and_data {keep_state_and_data,Acciones} {repeat_state,Datos} {repeat_state,Datos,Acciones} repeat_state_and_data {repeat_state_and_data,Acciones} stop {stop,Motivo} {stop,Motivo,Datos} {stop_and_reply,Motivo,Respuestas} {stop_and_reply,Motivo,Respuestas,Datos} |
Gestión del estado Estado . |
handle_event |
(enter, EstAnt, Estado, Datos) (Evento, Mensaje, Estado, Datos) |
{next_state,Estado,Datos} {next_state,Estado,Datos,Acciones} {keep_state,Datos} {keep_state,Datos,Acciones} keep_state_and_data {keep_state_and_data,Acciones} {repeat_state,Datos} {repeat_state,Datos,Acciones} repeat_state_and_data {repeat_state_and_data,Acciones} stop {stop,Motivo} {stop,Motivo,Datos} {stop_and_reply,Motivo,Respuestas} {stop_and_reply,Motivo,Respuestas,Datos} |
Gestión de los estados. |
code_change |
() |
{ok,Estado,Datos} Motivo |
Cambio en caliente. |
terminate |
() |
- | Terminación de la máquina de estados. |
A diferencia del anterior comportamiento, el estado aquí se refiere al nombre del estado que se está ejecutando dentro de la máquina virtual, por lo que los datos es la información interna que equivale al estado de un servidor genérico.
Sólo son obligatorias init
y callback_mode
, más las funciones Estado
o handle_event
, dependiendo de la configuración. El resto de funciones son opcionales.
Una vez se ha inicializado la máquina, se invoca el evento para configurar el modo de funcionamiento con callback_mode
, que tiene que devolver como mínimo una de las dos siguientes opciones:
state_functions
: Los eventos se gestionan con las funciones Estado/3
, requiriendo una función particular para cada estado de la máquina.handle_event_function
: Los eventos se gestionan con la función handle_event/4
.De añadir el valor state_enter
, se indicará que además habrá un evento de entrada para cada estado de la máquina.
Al gestionar los eventos que reciben los estados tenemos dos situaciones:
enter
, es que tenemos activada la configuración de entrada a los estados, por lo que EstAnt
será el estado anterior del que viene la máquina, salvo en el primer estado que entrará, que al no haber habido uno previo el valor será el propio estado.{call,Origen}
, cast
, info
, timeout
, {timeout,Nombre}
, state_timeout
o internal
. El parámetro mensaje es la información que acompaña al evento generado para el estado.Luego, al devolver la respuesta al comportamiento, Respuestas
puede ser la tupla {reply,Origen,Respuesta}
o una lista de varias de ellas. Algo similar ocurre con Acciones
que puede ser uno de los siguientes elementos, así como una lista de varios de ellos:
Acciones | Descripción |
---|---|
hibernate {hibernate,Bool} |
Manda a hibernar al proceso. |
postpone {postpone,Bool} |
Pospone el evento actual. |
{state_timeout,Timeout,Mensaje} {state_timeout,Timeout,Mensaje,Opciones} {state_timeout,update,Mensaje} {state_timeout,cancel} |
Inicia, actualiza o cancela el timeout de un estado. |
{{timeout,Nombre},Timeout,Mensaje} {{timeout,Nombre},Timeout,Mensaje, Opciones} {{timeout,Nombre},update,Mensaje} {{timeout,Nombre},cancel} |
Inicia, actualiza o cancela el timeout genérico. |
{timeout,Timeout,Mensaje} {timeout,Timeout,Mensaje,Opciones} Timeout |
Inicia un evento de tipo timeout. |
{reply,Origen,Respuesta} |
Responde al proceso que activó el evento. |
{next_event,Evento,Mensaje} |
Genera un evento. |
{change_callback_module,Módulo} |
Cambia el módulo encargado de gestionar los estados. |
{push_callback_module,Módulo} |
Añade un módulo a la pila de módulos que gestionan los estados. |
pop_callback_module |
Saca un módulo a la pila de módulos que gestionan los estados. |
Las operaciones que gestionan el comportamiento están en el módulo gen_statem
, entre las que tenemos las siguientes funciones:
Función | Parámetros | Descripción | Evento |
---|---|---|---|
start |
(Módulo, Argumentos, Opciones) (Nombre, Módulo, Argumentos, Opciones) |
Crea un proceso que ejecuta la máquina. | init |
start_link |
(Módulo, Argumentos, Opciones) (Nombre, Módulo, Argumentos, Opciones) |
Crea un proceso enlazado que ejecuta la máquina. | init |
start_monitor |
(Módulo, Argumentos, Opciones) (Nombre, Módulo, Argumentos, Opciones) |
Crea un proceso monitorizado que ejecuta la máquina. | init |
stop |
(Identificador) (Identificador, Motivo, Timeout) |
Detiene una máquina creada. | terminate |
call |
(Identificador, Mensaje) (Identificador, Mensaje, Timeout) |
Envía un mensaje que espera una respuesta de forma síncrona. | Estado handle_event |
cast |
(Identificador, Mensaje) |
Envía un mensaje que no espera una respuesta de forma asíncrona. | Estado handle_event |
El parámetro nombre, cuando se inicia una máquina de estados, sirve para registrar el proceso con un átomo, para ello se puede usar {local,Nombre}
o {global,Nombre}
, entre otras opciones. Al indicar local
se registra el proceso sólo en el nodo actual, mientras que con global
se registra en la red de nodos.
gen_event
Este comportamiento se utiliza para la gestión de eventos en un programa. Los eventos que se han de implementar son:
Función | Parámetros | Resultados | Descripción |
---|---|---|---|
init |
(Argumentos) |
{ok,Estado} {ok,Estado,hibernate} {error,Motivo} |
Inicialización del gestor de eventos. |
handle_call |
(Petición, Estado) |
{ok,Resultado,Estado} {ok,Resultado,Estado,hibernate} {swap_handler,Result,Args1,Estado,Gest2,Args2} {remove_handler,Resultado} |
Peticiones con respuesta. |
handle_event |
(Evento, Estado) |
{ok,Estado} {ok,Estado,hibernate} {swap_handler,Args1,Estado,Gest2,Args2} remove_handler |
Peticiones sin respuesta. |
handle_info |
(Mensaje, Estado) |
{ok,Estado} {ok,Estado,hibernate} {swap_handler,Args1,Estado,Gest2,Args2} remove_handler |
Mensajes recibidos que no son peticiones. |
code_change |
(Versión, Estado, Extra) |
{ok,Estado} |
Cambio en caliente. |
terminate |
(Motivo, Estado) |
- | Terminación del gestor. |
Sólo son obligatorias init
, handle_call
y handle_event
, el resto son opcionales. Los valores de Gest2
son indicar un módulo con un átomo o la tupla {Módulo,Id}
.
Las operaciones que gestionan el comportamiento están en el módulo gen_event
, entre las que tenemos las siguientes funciones:
Función | Parámetros | Descripción | Evento |
---|---|---|---|
start |
(Opciones) (Nombre, Opciones) |
Crea un proceso que procesa eventos. | - |
start_link |
() (Opciones) (Nombre, Opciones) |
Crea un proceso enlazado que procesa eventos. | - |
start_monitor |
() (Opciones) (Nombre, Opciones) |
Crea un proceso monitorizado que procesa eventos. | - |
add_handler |
(Identificador, Gestor, Argumentos) |
Añade un gestor de eventos al proceso. | init |
add_sup_handler |
(Identificador, Gestor, Argumentos) |
Añade un gestor de eventos al proceso, cuyas peticiones estarán supervisadas mediante enlace. | init |
swap_handler |
(Identificador, {Gestor1, Args1}, {Gestor2, Args2}) |
Intercambia un gestor de eventos en el proceso. | terminate init |
swap_sup_handler |
(Identificador, {Gestor1, Args1}, {Gestor2, Args2}) |
Intercambia un gestor de eventos en el proceso, por uno nuevo supervisado. | terminate init |
delete_handler |
(Identificador, Gestor, Argumentos) |
Elimina un gestor de eventos del proceso. | terminate |
stop |
(Identificador) (Identificador, Motivo, Timeout) |
Detiene un gestor creado. | terminate |
call |
(Identificador, Gestor, Petición) (Identificador, Gestor, Petición, Timeout) |
Envía una petición síncrona que espera una respuesta. | handle_call |
notify |
(Identificador, Evento) |
Envía un evento asíncrono. | handle_event |
sync_notify |
(Identificador, Evento) |
Envía un evento síncrono. | handle_event |
El parámetro nombre, cuando se inicia un procesador de eventos, sirve para registrar el proceso con un átomo, para ello se puede usar {local,Nombre}
o {global,Nombre}
, entre otras opciones. Al indicar local
se registra el proceso sólo en el nodo actual, mientras que con global
se registra en la red de nodos. El nombre se puede usar como una opción con las funciones start/1
. El parámetro gestor de nuevo es el nombre del módulo que gestiona los eventos o una tupla {Módulo,Id}
.
El funcionamiento del gestor de eventos consiste en crear un proceso, que va a administrar los gestores encargados de procesar los eventos que se produzcan, esto se hace con las funciones start
. Una vez creado, se irán añadiendo gestores con add_handler
. Configurado ya los gestores, se procederá a notificar los eventos con notify
. El uso de sync_notify
se reserva para cuando necesitamos que nuestro programa espere a que se termine de procesar el evento notificado. Si necesitamos hacer una petición con respuesta, usaremos la función call
, para obtener información interna del gestor, por ejemplo.
supervisor
Este comportamiento se utiliza para supervisar la ejecución de procesos. Los eventos que se han de implementar son:
Función | Parámetros | Resultados | Descripción |
---|---|---|---|
init |
(Argumentos) |
{ok,{Config,[Hijo]}} ignore |
Inicialización del supervisor. |
La inicialización debe devolver una configuración del modo de funcionamiento del supervisor, así como una especificación de cada hijo, cuyas estructuras son:
Variable | Valores |
---|---|
Config |
{Estrategia, Intensidad, Periodo} |
Config |
#{strategy => Estrategia, intensity => Intensidad, period => Periodo, auto_shutdown => Apagado} |
Hijo |
{Identificador, Llamada, Reinicio, Apagado, Tipo, Módulos} |
Hijo |
#{id => Identificador, start => Llamada, restart => Reinicio, significant => Significativo, shutdown => Apagado, type => Tipo, modules => Módulos} |
Los valores que para la configuración son:
Variable | Defecto | Descripción |
---|---|---|
Estrategia |
one_for_one |
Estrategia de reinicio ante la terminación de los hijos: - one_for_one = Sólo el proceso terminado.- one_for_all = Todos los procesos.- rest_for_one = El proceso terminado y los siguientes.- simple_one_for_one = Sólo el proceso terminado. |
Intensidad |
1 |
Número de reinicios máximos durante el periodo configurado. Superar la cifra aquí configurada finalizará la ejecución del supervisor y la de sus hijos. El valor será un entero mayor o igual que cero. |
Periodo |
5 |
Tiempo en segundos máximos para el límite de reinicios máximos. El valor será un entero mayor que cero. |
Apagado |
never |
Estrategia de finalización ante la finalización de los hijos significativos del supervisor: - never = Nunca.- any_significant = Al morir alguno.- all_significant = Al morir todos. |
Los valores que para la especificación de un hijo son:
Variable | Defecto | Valores |
---|---|---|
Identificador |
- | Nombre identificador del proceso. |
Llamada |
- | Función de llamada para crear el proceso:{Módulo, Función, Argumentos} |
Reinicio |
permanent |
Estrategia ante la terminación del proceso: - permanent = Reiniciar siempre.- transient = Reiniciar cuando falla.- temporary = Reiniciar nunca. |
Significativo |
false |
Indica si el proceso es un hijo significativo con un true o false . |
Apagado |
infinity |
Estrategia para terminar el proceso: - brutal_kill = Muerte inmediata.- Timeout = Muerte después de un tiempo.- infinity = Esperar a que termine. |
Tipo |
worker |
Tipo de proceso: - worker = Trabajador.- supervisor = Supervisor. |
Módulos |
[Módulo] |
Si el proceso implementa un comportamiento como supervisor , gen_server o gen_statem , con [Módulo] se indica cuál es el módulo con los callbacks, mientras que el valor dynamic se usa para los procesos de gen_event . Si no se indica su valor, es tomado del módulo indicado en la llamada. |
Las operaciones que gestionan el comportamiento están en el módulo supervisor
, entre las que tenemos las siguientes funciones:
Función | Parámetros | Descripción |
---|---|---|
start_link |
(Módulo, Argumentos) (Nombre, Módulo, Argumentos) |
Crea un proceso enlazado de un supervisor. |
start_child |
(IdSup, Hijo) |
Añade una especificación de un hijo del supervisor e inicia su proceso. |
terminate_child |
(IdSup, IdHijo) |
Termina un proceso hijo del supervisor. |
restart_child |
(IdSup, IdHijo) |
Reinicia un proceso hijo del supervisor. |
delete_child |
(IdSup, IdHijo) |
Borra una especificación de un hijo del supervisor. |
get_childspec |
(IdSup, IdHijo) |
Devuelve la especificación de un hijo del supervisor. |
count_children |
(IdSup) |
Devuelve el estado actual de los hijos del supervisor. |
which_children |
(IdSup) |
Devuelve una lista de todos los hijos del supervisor. |
check_childspecs |
(Hijos) (Hijos, Apagado) |
Comprueba si una lista de especificaciones es correcta. |
Con start_link
se inicia el proceso de supervisión, que invoca la función init
. Se pueden gestionar hijos de forma dinámica usando start_child
, terminate_child
, restart_child
y delete_child
.
El parámetro nombre, cuando se inicia un supervisor, sirve para registrar el proceso con un átomo, para ello se puede usar {local,Nombre}
o {global,Nombre}
, entre otras opciones. Al indicar local
se registra el proceso sólo en el nodo actual, mientras que con global
se registra en la red de nodos.
Cuando el supervisor está configurado como simple_one_for_one
, sólo se podrá tener una única especificación de hijo para supervisar, porque todos los hijos que se supervisen van a ser instancias dinámicas de esta especificación. Por ello, el parámetro hijo de start_child
, en lugar de ser una especificación, es una lista de argumentos que se concatena a la llamada indicada en la especificación única del supervisor.
application
Este comportamiento se utiliza para controlar aplicaciones de Erlang. Los eventos que se han de implementar son:
Función | Parámetros | Resultados | Descripción |
---|---|---|---|
start |
(Tipo, Argumentos) |
{ok,PID} {ok,PID,Estado} {error,Motivo} |
Inicio de la aplicación. |
prep_stop |
(Estado) |
Estado |
La aplicación va a finalizar pero todavía no lo ha hecho. |
stop |
(Estado) |
- | Fin de la aplicación. |
El tipo en la inicialización habitualmente es normal
, pero en aplicaciones distribuidas nos podemos encontrar con {takeover,Nodo}
y {failover,Nodo}
. Los argumentos corresponden con los valores definidos en la clave mod
del fichero de configuración de la aplicación.
Las operaciones que gestionan el comportamiento están en el módulo application
, entre las que tenemos las siguientes funciones:
Función | Parámetros | Descripción |
---|---|---|
start |
(Aplicación) (Aplicación, Modo) |
Inicia una aplicación. |
stop |
(Application) |
Detiene una aplicación. |
unload |
(Aplicación) |
Quita una aplicación cargada. |
loaded_applications |
() |
Devuelve las aplicaciones cargadas. |
which_applications |
() (Timeout) |
Devuelve las aplicaciones que están ejecutándose. |
get_all_env |
() (Aplicación) |
Devuelve los valores definidos en el entorno de la aplicación. |
get_env |
(Clave) (Aplicación, Clave) (Aplicación, Clave, Defecto) |
Devuelve un valor definido en el entorno de la aplicación. |
set_env |
(Configuración) (Configuración, Opciones) (Aplicación, Clave, Valor) (Aplicación, Clave, Valor, Opciones) |
Modifica un valor definido en el entorno de la aplicación. |
get_all_key |
() (Aplicación) |
Devuelve las claves usadas en la configuración de la aplicación. |
get_key |
(Clave) (Aplicación, Clave) |
Devuelve un valor de la configuración de la aplicación. |
Para poder aplicar este comportamiento es necesario que el proyecto siga la siguiente estructura durante el desarrollo:
my_app
├── doc
│ ├── internal
│ ├── examples
│ └── src
├── include
├── priv
├── src
│ └── my_app.app.src
└── test
El directorio src
es obligatorio. Son opcionales priv
e include
. Son recomendados doc
y test
. Para la versión de lanzamiento esta es la estructura necesaria:
my_app-version
├── bin
├── doc
│ ├── examples
│ ├── html
│ ├── internal
│ ├── man [1-9]
│ └── pdf
├── ebin
│ └── my_app.app
├── include
├── priv
│ ├── bin
│ └── lib
└── src
El directorio ebin
es obligatorio. Son opcionales src
, priv
, include
, bin
y doc
. Son recomendados priv/lib
y priv/bin
. Pero sobre todo es importante tener el fichero .app
que configura la aplicación y que tiene la siguiente forma:
{application, Nombre,
[{description, Descripción},
{id, Identificador},
{vsn, Versión},
{modules, Módulos},
{maxP, Procesos},
{maxT, Tiempo},
{registered, Nombres},
{included_applications, Aplicaciones},
{optional_applications, Aplicaciones},
{applications, Aplicaciones},
{env, Entorno},
{mod, {Módulo, Argumentos}},
{start_phases, Fases},
{runtime_dependencies, Dependencias}]}.
Cuyos tipos y valores son:
Elemento | Tipo | Defecto | Descripción |
---|---|---|---|
Nombre |
atom() |
- | Nombre de la aplicación. |
description |
string() |
"" |
Identificador del producto. |
id |
string() |
"" |
Identificador del producto. |
vsn |
string() |
"" |
Versión de la aplicación. |
modules |
[atom()] |
[] |
Módulos que introduce la aplicación. |
maxP |
int() |
infinity |
Número máximo de procesos. |
maxT |
int() |
infinity |
Tiempo máximo de ejecución. |
registered |
[atom()] |
[] |
Nombres registrados por la aplicación. |
included_applications |
[atom()] |
[] |
Aplicaciones incluidas que serán cargadas pero no iniciadas automáticamente. |
optional_applications |
[atom()] |
[] |
Aplicaciones opcionales de las que depende la aplicación. |
applications |
[atom()] |
[] |
Aplicaciones de las que depende y que serán cargadas e iniciadas. |
env |
[{atom(), term()}] |
[] |
Entorno con información necesaria para la aplicación. |
mod |
{atom(), list()} |
[] |
Llamada inicial para arrancar la aplicación. |
start_phases |
{atom(), list()} |
undefined |
Fases para arrancar la aplicación. |
runtime_dependencies |
[string()] |
[] |
Dependencias que tiene la aplicación para ser ejecutada. |
Por ejemplo:
{application, my_app,
[{description, "My App"},
{vsn, "1.0"},
{modules, [my_app, my_sup, my_worker]},
{registered, [my_worker]},
{applications, [kernel, stdlib]},
{mod, {my_app, []}}
]}.
Tenemos una aplicación con tres módulos, un nombre que se va a registrar y unas dependencias. Con esto llamaríamos a la función start
de application
, indicando que la aplicación es my_app
y eligiendo uno de los siguientes modos:
permanent
: Si termina normal se cierran las otras aplicaciones y se apaga la máquina virtual. Si termina abruptamente ocurre lo mismo que al terminar normal.transient
: Si termina normal no ocurre nada. Si termina abruptamente se informa del fallo, se cierran las otras aplicaciones y se apagar la máquina virtual.temporary
: Este es el valor por defecto. Si termina normal no ocurre nada. Si termina abruptamente se informa del fallo, y la aplicación termina sin reiniciarse.Cuando queramos acceder a función del entorno definido en la configuración se puede usar la función get_env
, que salvo que le hayamos indicado un valor de retorno por defecto, si no se encuentra la clave nos devolverá undefined
, en caso contrario nos devuelve {ok,Valor}
. Y para apagar la aplicación se utiliza la función stop
.
Estas son las aplicaciones que conforma la plataforma Erlang/OTP:
Categoría | Aplicación | Descripción |
---|---|---|
Básico | compiler |
Compilador de Erlang. |
Básico | erts |
Entorno de ejecución de Erlang. |
Básico | kernel |
Núcleo de ejecución de Erlang. |
Básico | sasl |
Sistema para soporte de bibliotecas. |
Básico | stdlib |
Bibliotecas básicas de Erlang. |
Datos | mnesia |
Base de datos distribuida NoSQL. |
Datos | odbc |
Interfaz para bases de datos SQL. |
Interfaces | asn1 |
Soporte para ASN.1 (notación de sintaxis abstracta). |
Interfaces | crypto |
Soporte para criptografía. |
Interfaces | diameter |
Soporte para el protocolo Diameter. |
Interfaces | eldap |
Soporte para el protocolo LDAP. |
Interfaces | erl_interface |
Interfaz de bajo nivel con C. |
Interfaces | ftp |
Soporte para el protocolo FTP. |
Interfaces | inets |
Soporte para servidores HTTP. |
Interfaces | jinterface |
Interfaz de bajo nivel con Java. |
Interfaces | megaco |
Soporte para el protocolo Megaco/H.248. |
Interfaces | public_key |
Soporte para claves públicas. |
Interfaces | ssh |
Soporte para el protocolo SSH. |
Interfaces | ssl |
Soporte para el protocolo SSL. |
Interfaces | tftp |
Soporte para el protocolo TFTP. |
Interfaces | wx |
Soporte para wxWidgets. |
Interfaces | xmerl |
Soporte para el formato XML 1.0. |
Herramientas | debugger |
Depurador de Erlang. |
Herramientas | dialyzer |
Analizador de tipos. |
Herramientas | et |
Trazador de eventos. |
Herramientas | observer |
Inspector de sistemas distribuidos. |
Herramientas | parsetools |
Parser y análisis léxico de código. |
Herramientas | reltool |
Gestor de aplicaciones para su lanzamiento final. |
Herramientas | runtime_tools |
Herramientas para la ejecución. |
Herramientas | syntax_tools |
Soporte para árboles sintácticos abstractos de Erlang. |
Herramientas | tools |
Herramientas auxiliares del sistema. |
Tests | common_test |
Testing automático para aplicaciones. |
Tests | eunit |
Test unitarios para módulos. |
Documentación | edoc |
Genera documentación tomando las etiquetas en los comentarios de un módulo. |
Documentación | erl_docgen |
Genera documentación para la OTP. |
Mantenimiento | os_mon |
Monitor de recursos del sistema operativo. |
Mantenimiento | snmp |
Gestiona el protocolo SNMP. |
erts
Los módulos principales son:
Módulo | Descripción |
---|---|
atomics |
Soporte para operaciones atómicas. |
counters |
Soporte para operaciones de conteo. |
erlang |
Funciones nativas del lenguaje. |
erl_driver |
Interfaz para drivers de Erlang. |
erl_nif |
Interfaz para funciones nativas de usuario. |
erl_prim_loader |
Cargador de bajo nivel de Erlang. |
erl_tracer |
Comportamiento de trazado en Erlang. |
init |
Gestor del arranque del sistema Erlang. |
persistent_term |
Persistencia de datos. |
zlib |
Interfaz para ficheros .zip . |
kernel
Los módulos generales del sistema son:
Módulo | Descripción |
---|---|
application |
Soporte para aplicaciones OTP genéricas. |
code |
Servidor de código Erlang. |
erl_boot_server |
Servidor de arranque para otras máquinas Erlang. |
erl_ddll |
Carga y enlace dinámica de drivers en Erlang. |
erl_epmd |
Interfaz para el epmd . |
error_handler |
Gestor por defecto de errores del sistema. |
file |
Operaciones con ficheros. |
global |
Sistema para registrar nombres globales. |
global_group |
Grupos de nodos para los grupos de registro de nombres globales. |
heart |
Sistema para monitorizar el proceso heart , que controla qué nodos Erlang de la red siguen vivos. |
os |
Operaciones del sistema operativo. |
pg |
Grupos de procesos con nombre distribuidos. |
seq_trace |
Trazado secuencial de transferencias de información. |
Los módulos de comunicaciones son:
Módulo | Descripción |
---|---|
erpc |
Llamadas a rutinas remotas (RPC) mejoradas. |
gen_sctp |
Comunicación con sockets usando SCTP. |
gen_tcp |
Comunicación con sockets usando TCP. |
gen_udp |
Comunicación con sockets usando UDP. |
inet |
Soporte para el protocolo TCP/IP. |
inet_res |
Cliente DNS básico. |
net |
Soporte para la interfaz de red. |
net_adm |
Rutinas para administrar la red de nodos Erlang. |
net_kernel |
Núcleo de la red de nodos Erlang. |
rpc |
Llamadas a rutinas remotas (RPC). |
socket |
Interfaz para manejar sockets. |
Los módulos de registro de eventos son:
Módulo | Descripción |
---|---|
disk_log |
Sistema de registro de eventos (logs) con ficheros. |
logger |
Interfaz para el registro de eventos (logs). |
logger_filters |
Filtrado del registro de eventos (logs). |
logger_formatter |
Formato para el registro de eventos (logs). |
logger_std_h |
Gestor estándar del registro de eventos (logs). |
logger_disk_log_h |
Registro de eventos (logs) con ficheros. |
wrap_log_reader |
Servicio para leer registros de disco de tipo wrap formateados internamente. |
stdlib
Los módulos generales son:
Módulo | Descripción |
---|---|
base64 |
Codificación y decodificación con Base64 (RFC 2045). |
c |
Interfaz de la consola Erlang. |
calendar |
Manejo de fechas y horas. |
erl_error |
Utilidades para informar de errores. |
erl_tar |
Manejo de ficheros .tar . |
file_sorter |
Ordena el contenido de ficheros. |
filelib |
Utilidades para manejar ficheros. |
filename |
Manipulación de nombres de ficheros. |
gen_event |
Gestor de eventos genérico. |
gen_server |
Servidor genérico. |
gen_statem |
Máquina de estados genérica. |
io |
Interfaz estándar de entrada y salida. |
io_lib |
Funciones de entrada y salida. |
log_mf_h |
Gestor de eventos que registra eventos en ficheros. |
math |
Funciones matemáticas. |
peer |
Inicia y controla nodos enlazados. |
pool |
Gestor de distribución de carga con procesos. |
proc_lib |
Funciones para la creación de procesos. |
rand |
Generación de números pseudo-aleatorios. |
re |
Manejo de expresiones regulares para Erlang. |
shell |
Consola de comandos de Erlang. |
shell_docs |
Visualización de la documentación en la consola de Erlang. |
slave |
Inicia y controla nodos esclavos. |
supervisor |
Supervisor genérico de procesos. |
supervisor_bridge |
Puente supervisor genérico de procesos. |
sys |
Interfaz para mensajes de sistema. |
timer |
Manejo de temporizadores. |
unicode |
Conversión de caracteres Unicode. |
uri_string |
Procesado de URIs. |
win32reg |
Manejo del registro de Windows. |
zip |
Manejo de ficheros .zip . |
Los módulos de estructuras de datos son:
Módulo | Descripción |
---|---|
array |
Manejo de arrays. |
binary |
Manejo de binarios. |
dict |
Manejo de diccionarios clave-valor. Las claves se comparan con =:= . |
digraph |
Manejo de grafos dirigidos. |
digraph_utils |
Algoritmos para grafos dirigidos. |
gb_sets |
Manejo de conjuntos implementados con árboles balanceados. Los elementos se comparan con == . |
gb_trees |
Manejo de árboles balanceados. Los elementos se comparan con == . |
lists |
Manejo de listas. |
maps |
Manejo de mapas clave-valor. |
orddict |
Manejo de diccionarios clave-valor implementados con listas ordenadas. Las claves se comparan con == . |
ordsets |
Manejo de conjuntos implementados con listas ordenadas. Los elementos se comparan con == . |
proplists |
Manejo de listas de propiedades clave-valor. Las claves se comparan con =:= . |
queue |
Manejo de colas. |
sets |
Manejo de conjuntos. Los elementos se comparan con =:= . |
sofs |
Manejo de conjuntos de conjuntos. Los elementos se comparan con == . |
string |
Manejo de cadenas de texto. |
Los módulos de bases de datos son:
Módulo | Descripción |
---|---|
dets |
Base de datos NoSQL en ficheros. |
ets |
Base de datos NoSQL en memoria. |
ms_transform |
Transformación de sintaxis para crear especificaciones de ajuste de patrones. |
qlc |
Interfaz de consultas a Mnesia, ETS, DETS y demás. |
Los módulos de gestión del lenguaje son:
Módulo | Descripción |
---|---|
beam_lib |
Interfaz del formato de ficheros BEAM. |
epp |
Preprocesador de código Erlang. |
erl_anno |
Tipo de datos abstracto para las anotaciones del compilador de Erlang. |
erl_eval |
Meta-interprete de Erlang. |
erl_expand_records |
Transforma formas abstractas de código Erlang. |
erl_features |
Manejo de características del lenguaje. |
erl_id_trans |
Transformación de parseado identidad. |
erl_internal |
Definiciones internas de Erlang. |
erl_lint |
Linter para el lenguaje Erlang. |
erl_parse |
Parser del lenguaje Erlang. |
erl_pp |
Representación legible de Erlang. |
erl_scan |
Generador de tokens del lenguaje Erlang. |
El módulo erlang
contiene la mayor parte de las funciones nativas que hay en el lenguaje. Algunas de estas funciones no requieren indicar su módulo para invocarlas. Algunas de las funciones generales son:
Función | Descripción |
---|---|
error |
Lanza un error de ejecución. |
garbage_collect |
Invoca al recolector de basura. |
halt |
Para el entorno de ejecución de Erlang. |
make_ref |
Crea una referencia única. |
memory |
Da información sobre la memoria usada por Erlang. |
nif_error |
Lanza un error de ejecución. |
raise |
Lanza una excepción en ejecución. |
statistics |
Devuelve estadísticas sobre el sistema. |
system_flag |
Modifica el comportamiento del sistema. |
system_info |
Devuelve información sobre el sistema. |
system_monitor |
Obtiene/modifica la configuración para monitorizar el sistema. |
system_profile |
Obtiene/modifica la configuración del sistema. |
throw |
Lanza una excepción en ejecución. |
Estas son las funciones para comprobar tipos:
Función | Descripción |
---|---|
is_atom |
Indica si es de tipo átomo. |
is_binary |
Indica si es de tipo binario. |
is_bitstring |
Indica si es de tipo binario. |
is_boolean |
Indica si es de tipo booleano. |
is_float |
Indica si es de tipo coma flotante. |
is_function |
Indica si es de tipo función. |
is_integer |
Indica si es de tipo entero. |
is_list |
Indica si es de tipo lista. |
is_map |
Indica si es de tipo mapa. |
is_number |
Indica si es de tipo número. |
is_pid |
Indica si es de tipo PID. |
is_port |
Indica si es de tipo puerto. |
is_record |
Indica si es de tipo registro. |
is_reference |
Indica si es de tipo referencia. |
is_tuple |
Indica si es de tipo tupla. |
Estas son las funciones para convertir entre tipos:
Función | Descripción |
---|---|
atom_to_binary |
De átomo a texto en binario. |
atom_to_list |
De átomo a texto en lista. |
binary_to_atom |
De texto en binario a átomo. |
binary_to_existing_atom |
De texto en binario a átomo. |
binary_to_float |
De texto en binario a coma flotante. |
binary_to_integer |
De texto en binario a entero. |
binary_to_list |
De binario a lista. |
binary_to_term |
De binario a valor literal. |
bitstring_to_list |
De binario a lista. |
float_to_binary |
De coma flotante a texto en binario. |
float_to_list |
De coma flotante a texto en lista. |
fun_to_list |
De valor funcional a texto en lista. |
integer_to_binary |
De entero a texto en binario. |
integer_to_list |
De entero a texto en lista. |
iolist_to_binary |
De lista de entrada a binario. |
iolist_to_iovec |
De lista de entrada a vector de entrada. |
list_to_atom |
De texto en lista a átomo. |
list_to_binary |
De lista a binario. |
list_to_bitstring |
De lista a binario. |
list_to_existing_atom |
De texto en lista a átomo. |
list_to_float |
De texto en lista a coma flotante. |
list_to_integer |
De texto en lista a entero. |
list_to_pid |
De texto en lista a PID. |
list_to_port |
De texto en lista a puerto. |
list_to_ref |
De texto en lista a referencia. |
list_to_tuple |
De lista a tupla. |
pid_to_list |
De PID a texto en lista. |
port_to_list |
De puerto a texto en lista. |
ref_to_list |
De referencia a texto en lista. |
term_to_binary |
De valor literal a binario. |
term_to_iovec |
De valor literal a vector de entrada. |
tuple_to_list |
De tupla a lista. |
Estas son las funciones para temas numéricos:
Función | Descripción |
---|---|
abs |
Devuelve el valor absoluto de un número. |
adler32 |
Calcula un checksum Adler-32. |
adler32_combine |
Calcula un checksum Adler-32. |
ceil |
Devuelve el menor entero igual o mayor que el número actual. |
crc32 |
Calcula un checksum CRC-32. |
crc32_combine |
Calcula un checksum CRC-32. |
external_size |
Calcula el tamaño en bytes de un valor literal. |
float |
Transforma un número en coma flotante. |
floor |
Devuelve el mayor entero igual o menor que el número actual. |
max |
Devuelve el valor mayor. |
md5 |
Calcula un MD5. |
md5_final |
Finaliza el cálculo de un MD5. |
md5_init |
Inicia el cálculo de un MD5. |
md5_update |
Actualiza el cálculo de un MD5. |
min |
Devuelve el valor menor. |
phash2 |
Devuelve un hash para un valor literal. |
round |
Redondea un número. |
trunc |
Trunca un número quitando los decimales. |
unique_integer |
Genera un número entero único. |
Estas son las funciones para manejar fechas y horas:
Función | Descripción |
---|---|
convert_time_unit |
Transforma marcas de tiempo. |
date |
Devuelve la fecha actual. |
localtime |
Devuelve la fecha y hora actual. |
localtime_to_universaltime |
Transforma a horario UTC. |
monotonic_time |
Devuelve el tiempo actual de ejecución. |
system_time |
Devuelve el tiempo actual del sistema. |
time |
Devuelve la hora actual. |
time_offset |
Devuelve la diferencia entre el tiempo monótono y el del sistema. |
timestamp |
Devuelve una marca de tiempo. |
universaltime |
Devuelve la fecha y hora actual en UTC. |
universaltime_to_localtime |
Transforma a horario local. |
Estas son las funciones para manejar temporizadores:
Función | Descripción |
---|---|
cancel_timer |
Cancela un temporizador. |
read_timer |
Consulta un temporizador. |
start_timer |
Inicia un temporizador. |
Estas son las funciones para manejar listas:
Función | Descripción |
---|---|
hd |
Devuelve la cabeza de la lista. |
length |
Devuelve el número de elementos de una lista. |
tl |
Devuelve la cola de la lista. |
Estas son las funciones para manejar tuplas:
Función | Descripción |
---|---|
append_element |
Añade una componente adicional a una tupla. |
delete_element |
Borra una componente en una tupla. |
element |
Devuelve una componente en una tupla. |
insert_element |
Inserta una componente adicional a una tupla. |
make_tuple |
Crea una tupla con todas las componentes iguales. |
setelement |
Modifica una componente en una tupla. |
tuple_size |
Devuelve el número de componente de una tupla. |
Estas son las funciones para manejar mapas:
Función | Descripción |
---|---|
is_map_key |
Indica si la clave está en el mapa. |
map_get |
Obtiene el valor para una clave en el mapa. |
map_size |
Devuelve el número de elementos de un mapa. |
Estas son las funciones para manejar funciones o módulos:
Función | Descripción |
---|---|
apply |
Invoca una función con una serie de argumentos. |
check_old_code |
Comprueba si se está ejecutando código viejo. |
check_process_code |
Comprueba si se está ejecutando código viejo. |
function_exported |
Comprueba si existe una función. |
fun_info |
Devuelve información relativa a una función. |
is_builtin |
Indica si es de una función nativa. |
load_nif |
Carga código nativo para un módulo. |
loaded |
Devuelve la lista de todos los módulos cargados. |
pre_loaded |
Devuelve la lista de todos los módulos pre-cargados al iniciar el entorno de ejecución de Erlang. |
Estas son las funciones para manejar otras estructuras de datos:
Función | Descripción |
---|---|
binary_part |
Devuelve un fragmento de un binario. |
bit_size |
Devuelve el tamaño en bits de un binario. |
byte_size |
Devuelve el tamaño en bytes de un binario. |
decode_packet |
Decodifica un paquete binario. |
iolist_size |
Devuelve el tamaño en bytes de una lista de entrada. |
match_spec_test |
Comprueba una especificación de encaje. |
size |
Devuelve el número de elementos en un binario o tupla. |
split_binary |
Parte un binario en dos fragmentos. |
Estas son las funciones para manejar procesos:
Función | Descripción |
---|---|
alias |
Devuelve una referencia como alias del proceso actual. |
demonitor |
Elimina una relación de monitor. |
erase |
Borra elementos del diccionario del proceso. |
exit |
Termina la ejecución de un proceso. |
get |
Consulta elementos del diccionario del proceso. |
get_keys |
Devuelve las claves del diccionario del proceso. |
group_leader |
Obtiene/modifica el líder de grupo. |
hibernate |
Hiberna un proceso en ejecución. |
is_process_alive |
Indica si el proceso está vivo. |
link |
Enlaza el proceso actual con otro. |
monitor |
Establece una relación de monitor. |
process_display |
Muestra información sobre un proceso. |
process_flag |
Modifica el comportamiento de un proceso. |
process_info |
Devuelve información sobre un proceso. |
processes |
Devuelve los procesos actuales del nodo. |
put |
Modifica elementos del diccionario del proceso. |
self |
Devuelve el PID del proceso actual. |
send |
Envía un mensaje a un proceso o puerto. |
send_after |
Envía un mensaje a un proceso o puerto. |
send_nosuspend |
Envía un mensaje a un proceso o puerto. |
spawn |
Crea un proceso. |
spawn_link |
Crea un proceso enlazado. |
spawn_monitor |
Crea un proceso monitorizado. |
spawn_opt |
Crea un proceso avanzado. |
spawn_request |
Pide crear un proceso. |
spawn_request_abandon |
Descarta la petición de crear un proceso. |
unalias |
Elimina una referencia como alias del proceso actual. |
unlink |
Elimina un enlace de un proceso con otro. |
yield |
Cede a otro proceso en espera la ejecución. |
Estas son las funciones para manejar nodos:
Función | Descripción |
---|---|
disconnect_node |
Fuerza la desconexión de un nodo. |
get_cookie |
Devuelve la cookie de un nodo. |
is_alive |
Indica si el nodo actual está vivo. |
monitor_node |
Modifica una relación de monitor con un nodo. |
node |
Devuelve el nombre de un nodo. |
nodes |
Devuelve nombres de nodos. |
register |
Registra un nombre en el nodo. |
registered |
Devuelve los nombres registrados. |
set_cookie |
Modifica la cookie de un nodo. |
unregister |
Elimina un nombre registrado en el nodo. |
whereis |
Devuelve el proceso o puerto asociado a un nombre. |
Estas son las funciones para manejar puertos de comunicación:
Función | Descripción |
---|---|
open_port |
Abre un puerto. |
port_call |
Realiza una petición síncrona a un puerto. |
port_close |
Cierra un puerto. |
port_command |
Envía datos a un puerto. |
port_connect |
Cambia el dueño de un puerto. |
port_control |
Ejecuta una operación de control. |
port_info |
Devuelve información sobre un puerto. |
ports |
Devuelve los puertos usados por el nodo. |
El módulo io
se encarga de la gestión de entrada y salida estándar. Para mostrar información en la terminal tenemos la función format
y para leer una cadena de texto la función get_line
, que siguen la siguiente sintaxis:
Tanto formato como mensaje son cadenas de texto que se van a mostrar. El parámetro datos es una lista con los valores que se van a insertar en la cadena de formato. Y por último, format
devuelve siempre ok
, mientras que get_line
devuelve una cadena de texto, la tupla {error, Motivo}
o el átomo eof
.
Hay que señalar que la cadena de formato de format
tiene diferentes marcadores para representar información. Los marcadores siguen la forma ~A.P.RMC
, donde A
es el ancho mínimo (alineado a la izquierda, salvo que sea un número negativo para alinearlo a la derecha), P
es la precisión que se va a mostrar, R
es el carácter de relleno, M
es un modificador de cómo se interpretará el dato a representar, y C
es el código de representación. Se puede usar un *
para A
, P
y R
, utilizando los siguientes elementos en la lista de datos que haya para definir estos valores. La mayoría de estos indicadores son opcionales, siendo obligatorio como mínimo la forma ~C
. Los códigos de representación son:
Código | Descripción |
---|---|
~ |
Muestra el carácter ~ . |
c |
Muestra un código ASCII como un carácter. |
f |
Muestra número de coma flotante con formato [-]ddd.ddd . |
e |
Muestra número de coma flotante con formato [-]d.ddde+-ddd . |
g |
Muestra número de coma flotante, usando f para valores entre 0.1 y 10000.0 , y usando e para el resto. |
s |
Muestra una cadena de texto. |
w |
Muestra un valor Erlang de forma simple. |
p |
Muestra un valor Erlang de forma legible. |
W |
Como w pero toma un dato adicional que indica el nivel de profundidad máximo para la representación. |
P |
Como p pero toma un dato adicional que indica el nivel de profundidad máximo para la representación. |
B |
Muestra números enteros en bases entre 2 y 36 , siendo 10 el valor por defecto. Por ejemplo, ~.16B nos muestra enteros hexadecimales. |
X |
Como B pero toma un dato adicional que indica el prefijo a usar para la representación. |
# |
Como B pero usa la notación # de Erlang para representar los números enteros. |
b |
Como B pero las letras son en minúsculas. |
x |
Como X pero las letras son en minúsculas. |
+ |
Como # pero las letras son en minúsculas. |
n |
Muestra una nueva línea. |
i |
Ignora el siguiente dato en la lista. |
La biblioteca estándar tiene diferentes módulos para manejar algunos tipos de estructuras de datos. El módulo más completo disponible es lists
que sirve para manipular listas. Dentro de todas las funciones que hay, vamos a centrarnos en las de orden superior, que son aquellas que aceptan como argumentos de entrada otras funciones:
Función | Tipo | Descripción |
---|---|---|
all(P,L) |
((T) -> Bool, [T]) -> Bool |
Comprueba que todos los elementos de L cumplan P . |
any(P,L) |
((T) -> Bool, [T]) -> Bool |
Comprueba que algún elemento de L cumplan P . |
dropwhile(P,L) |
((T) -> Bool, [T]) -> [T] |
Elimina elementos de L mientras se cumpla P . |
filter(P,L) |
((T) -> Bool, [T]) -> [T] |
Mantiene aquellos elementos de L que cumplan P . |
filtermap(F,L) |
((T) -> BOT, [T]) -> [T] |
Mantiene y/o transforma elementos de L con F , donde BOT puede ser {true, Valor} , true , false . Con el primero se sustituye el elemento por Valor , con el segundo se mantiene el elemento y con el tercero se descarta. |
flatmap(F,L) |
((A) -> B, [A]) -> [B] |
Aplana la lista L y aplica F a cada elemento de la lista aplanada. |
foldl(F,A,L) |
((T,R) -> R, R, [T]) -> R |
Reduce la lista L , usando un valor acumulador inicial A , mediante la función F , cuyo primer parámetro es el elemento de la lista y el segundo el valor acumulado actual. Se recorre la lista de izquierda a derecha y por ello tiene recursión de cola, siendo más eficiente que foldr . |
foldr(F,A,L) |
((T,R) -> R, R, [T]) -> R |
Reduce la lista L , usando un valor acumulador inicial A , mediante la función F , cuyo primer parámetro es el elemento de la lista y el segundo el valor acumulado actual. Se recorre la lista de derecha a izquierda y por ello no tiene recursión de cola, siendo menos eficiente que foldl . |
foreach(F,L) |
((T) -> R, [T]) -> ok |
Aplica la función F a cada elemento de la lista L sin importar el resultado. |
map(F,L) |
((A) -> B, [A]) -> [B] |
Aplica la función F a cada elemento de la lista L . |
mapfoldl(F,A,L) |
((T,R) -> {U,R}, R, [T]) -> {[U],R} |
Realiza las funciones map y foldl en un recorrido. |
mapfoldr(F,A,L) |
((T,R) -> {U,R}, R, [T]) -> {[U],R} |
Realiza las funciones map y foldr en un recorrido. |
merge(F,L1,L2) |
((A,B) -> Bool, [A], [B]) -> [A+B] |
Fusiona dos listas usando F para determinar si A <= B . |
partition(P,L) |
((T) -> Bool, [T]) -> {[T],[T]} |
Devuelve una tupla donde la primera componente son los elementos de L que cumplen P y la segunda los que no. |
search(P,L) |
((T) -> Bool, [T]) -> R |
Busca un elemento en L que cumpla P , donde R puede ser {value, Valor} o false . |
sort(F,L) |
((A,B) -> Bool, [T]) -> [T] |
Devuelve una lista ordenada, con los elementos de L , usando F para determinar si A <= B . |
splitwith(P,L) |
((T) -> Bool, [T]) -> {[T],[T]} |
Devuelve una tupla con dos listas a partir de L , la primera son elementos que cumplen P hasta que deja de cumplirse, para devolver el resto de elementos en la segunda. |
takewhile(P,L) |
((T) -> Bool, [T]) -> [T] |
Devuelve elementos de L mientras se cumpla P . |
umerge(F,L1,L2) |
((A,B) -> Bool, [A], [B]) -> [A+B] |
Fusiona dos listas ordenadas usando F para determinar si A <= B , si A == B descarta el segundo elemento. |
usort(F,L) |
((A,B) -> Bool, [T]) -> [T] |
Devuelve una lista ordenada eliminando los duplicados, con los elementos de L , usando F para determinar si A <= B . |
zipwith(F,L1,L2) |
((X,Y) -> R, [X], [Y]) -> [R] |
Combina dos elementos en una sola lista usando F . |
zipwith3(F,L1,L2,L3) |
((X,Y,Z) -> R, [X], [Y], [Z]) -> [R] |
Combina tres elementos en una sola lista usando F . |
uniq(F,L) |
((T) -> Any, [T]) -> [T] |
Elimina los duplicados de L usando F para seleccionar el valor que representa a cada elemento. |
Algunas de estas funciones se pueden encontrar en otros módulos como array
, gb_sets
, gb_trees
, maps
, queue
o sets
.
La biblioteca de Erlang dispone de una serie de módulos que nos permite tener una base de datos NoSQL a nuestra disposición. Estos módulos son: ets
, dets
y mnesia
. El primero gestiona la base de datos en memoria, el segundo en disco y el tercero es una versión extendida que engloba a las dos anteriores.
El funcionamiento consiste en crear las tablas que necesitemos, para insertar, actualizar, borrar y consultar datos con forma de tupla. Dentro de la tupla, una de las componentes actuará como clave de la tabla. Internamente las tablas funcionan con cuatro tipos de estructuras:
set
: No permiten claves duplicadas dentro de la tabla.ordered_set
: No permiten claves duplicadas dentro de la tabla y los elementos estarán ordenados a la hora de recorrer la tabla.bag
: Permite claves duplicadas dentro de la tabla, pero no permite dos tuplas exactamente iguales.duplicate_bag
: Permite incluso tuplas duplicadas dentro de la tabla.El tipo
ordered_set
utiliza la igualdad simple==
para la comparación, por lo que1
es igual que1.0
. El resto de tipos de tablas usan el operador=:=
. Además,ordered_set
no está disponible como tipo de tabla paradets
y por ello tampoco lo estará paramnesia
cuando se quiera trabajar sólo con el disco duro.
Las funciones principales del módulo ets
son:
Función | Parámetros | Descripción |
---|---|---|
all |
() |
Devuelve todas las tablas del nodo actual. |
delete |
(Tabla) (Tabla, Clave) |
Borra tuplas de una tabla. |
delete_object |
(Tabla, Tupla) |
Borra la misma tupla de una tabla. |
file2tab |
(Tabla, Fichero) (Tabla, Fichero, Opciones) |
Vuelca en una tabla el contenido de un fichero. |
first |
(Tabla) |
Devuelve la primera clave en una tabla de tipo ordered_set . |
foldl |
(Función, Accumulador, Tabla) |
Reduce el contenido de una tabla. |
foldr |
(Función, Accumulador, Tabla) |
Reduce el contenido de una tabla. |
from_dets |
(Tabla, TabDETS) |
Rellena una tabla ETS con los datos de una tabla DETS. |
fun2ms |
(Lambda) |
Toma una función lambda y la transforma en una especificación de ajuste para realizar operaciones de selección. |
give_away |
(Tabla, PID, Extra) |
Cambia el proceso que es dueño de una tabla. |
info |
(Tabla) (Tabla, Opción) |
Devuelve información sobre una tabla. |
insert |
(Tabla, Tuplas) |
Añade una o varias tuplas. En las tablas que no permiten duplicados de clave, si ya existe una tupla con la misma clave, se sustituye por la nueva tupla. |
insert_new |
(Tabla, Tuplas) |
Añade una o varias tuplas que no existieran previamente. |
last |
(Tabla) |
Devuelve la última clave en una tabla de tipo ordered_set . |
lookup |
(Tabla, Clave) |
Devuelve una tupla o varias con una clave. |
lookup_element |
(Tabla, Clave, Posición) |
Devuelve la componente de una tupla o varias con una clave. |
match |
(Tabla, Patrón) |
Devuelve los elementos indicados de aquellas tuplas que se ajustan a un patrón. |
match_delete |
(Tabla, Patrón) |
Borra aquellas tuplas que se ajustan a un patrón. |
match_object |
(Tabla, Patrón) |
Devuelve aquellas tuplas que se ajustan a un patrón. |
member |
(Table, Clave) |
Indica si la clave existe en una tabla. |
new |
(Nombre, Opciones) |
Crea una nueva tabla. |
next |
(Tabla, Clave) |
Devuelve la clave siguiente en una tabla de tipo ordered_set . |
prev |
(Tabla, Clave) |
Devuelve la clave anterior en una tabla de tipo ordered_set . |
rename |
(Tabla, Nombre) |
Renombra una tabla. |
select |
(Tabla, Ajuste) |
Devuelve una serie de tuplas, que se ajustan a una especificación de ajuste. |
select_count |
(Tabla, Ajuste) |
Cuenta cuantas tuplas se ajustan a una especificación de ajuste que devuelva true . |
select_delete |
(Tabla, Ajuste) |
Borra una serie de tuplas, que se ajustan a una especificación de ajuste que devuelva true . |
select_replace |
(Tabla, Ajuste) |
Reemplaza una serie de tuplas, que se ajustan a una especificación de ajuste, con el resultado de la especificación. |
select_reverse |
(Tabla, Ajuste) |
Devuelve una consulta en orden inverso cuando se usa una tabla de tipo ordered_set . |
tab2file |
(Tabla, Fichero) (Tabla, Fichero, Opciones) |
Vuelca en un fichero el contenido de una tabla. |
tab2list |
(Tabla) |
Devuelve en una lista el contenido de una tabla. |
take |
(Tabla, Clave) |
Devuelve y elimina las tuplas con una clave. |
to_dets |
(Tabla, TabDETS) |
Rellena una tabla DETS con los datos de una tabla ETS. |
Las opciones a la hora de crear una tabla nueva son las siguientes:
Opción | Descripción |
---|---|
set , ordered_set , bag o duplicate_bag |
Tipo de la tabla. Por defecto es set . |
private , protected o public |
Acceso de escritura y lectura a la tabla desde otros procesos. Por defecto es protected . Las tablas privadas sólo son accesibles al proceso que la creó, las protegidas permiten la lectura a otros procesos y las públicas además permiten la escritura. |
named_table |
Utiliza el nombre pasado como argumento para dar nombre interno a la tabla. |
{keypos, Posición} |
Posición de la clave dentro de las componentes de la tupla. Por defecto es la posición 1 . |
{heir, PID, Extra} o {heir, none} |
Define un proceso supervisor para eliminar la tabla cuando este muera. Por defecto es {heir, none} . Esta opción se puede cambiar con la función setopts/2 . |
{read_concurrency, Bool} |
Con true optimiza las lecturas a la tabla a costa del rendimiento de las escrituras, con false pasa lo contrario. |
{read_concurrency, Bool} |
Con true permite que se puedan realizar lecturas de forma concurrente a las escrituras, sin afectar a las propiedades ACID de la tabla. Con false se trabaja de forma secuencial. |
compressed |
Permite que las componentes que no sean la clave sean comprimidas para ahorrar tamaño a costa del rendimiento. |
Para hacer consultas a una tabla, con lookup/2
buscamos los elementos de una clave. Con match/2
definimos un patrón de ajuste que nos devuelva una selección de tuplas en una lista. El patrón será una tupla con una serie de valores, que se pueden combinar con una serie de átomos especiales que ejercen de variables, teniendo estos la forma: '$0'
, '$1'
, '$2'
, etcétera. Adicionalmente, se usa '_'
para indicar que nos es indiferente el valor de esa posición dentro del patrón. Por ejemplo:
1> ets:new(foo, [named_table]).
foo
2> ets:insert(foo, [{1,a,a}, {2,b,c}, {4,d,d}]).
true
3> ets:match(foo, {'_','$0','$0'}).
[[a],[d]]
4> ets:match_object(foo, {'_','$0','$0'}).
[{1,a,a},{4,d,d}]
Sin embargo, este mecanismo no es todo lo flexible que gustaría y por ello existen las funciones select/2
y compañía, para utilizar una especificación de ajuste que permita consultas más elaboradas. Por suerte, existe una función auxiliar que el compilador detecta para construir especificaciones usando expresiones escritas en Erlang. Para ello tenemos que incluir la siguiente cabecera:
-include_lib("stdlib/include/ms_transform.hrl").
Esto permite al compilador detectar que vamos a usar la función fun2ms/1
, que recibe como argumento una expresión lambda, que el compilador traducirá a una especificación de ajuste. Por ejemplo:
5> ets:fun2ms(fun({K,A,_}) when A > 3 -> K end).
[{{'$1','$2','_'},[{'>','$2',3}],['$1']}]
Que aplicado al ejemplo anterior de la tabla foo
:
6> ets:select(foo, ets:fun2ms(fun({K,V,V}) -> {ok,K,V} end)).
[{ok,1,a},{ok,4,d}]
Cuando una tabla ETS deja de ser necesaria, basta con llamar a delete/1
para borrarla de la memoria por completo.
Las funciones principales del módulo dets
son:
Función | Parámetros | Descripción |
---|---|---|
all |
() |
Devuelve todas las tablas del nodo actual. |
close |
(Tabla) |
Cierra una tabla previamente abierta. |
delete |
(Tabla, Clave) |
Borra una tupla de una tabla. |
delete_object |
(Tabla, Tupla) |
Borra la misma tupla de una tabla. |
first |
(Tabla) |
Devuelve la primera clave en una tabla. |
foldl |
(Función, Accumulador, Tabla) |
Reduce el contenido de una tabla. |
foldr |
(Función, Accumulador, Tabla) |
Reduce el contenido de una tabla. |
from_ets |
(Tabla, TabETS) |
Rellena una tabla DETS con los datos de una tabla ETS. |
info |
(Tabla) (Tabla, Opción) |
Devuelve información sobre una tabla. |
insert |
(Tabla, Tuplas) |
Añade una o varias tuplas. En las tablas que no permiten duplicados de clave, si ya existe una tupla con la misma clave, se sustituye por la nueva tupla. |
insert_new |
(Tabla, Tuplas) |
Añade una o varias tuplas que no existieran previamente. |
lookup |
(Tabla, Clave) |
Devuelve una tupla o varias con una clave. |
match |
(Tabla, Patrón) |
Devuelve los elementos indicados de aquellas tuplas que se ajustan a un patrón. |
match_delete |
(Tabla, Patrón) |
Borra aquellas tuplas que se ajustan a un patrón. |
match_object |
(Tabla, Patrón) |
Devuelve aquellas tuplas que se ajustan a un patrón. |
member |
(Table, Clave) |
Indica si la clave existe en una tabla. |
next |
(Tabla, Clave) |
Devuelve la clave siguiente en una tabla. |
open_file |
(Fichero) (Fichero, Opciones) |
Abre una tabla almacenada en un fichero. Si no existe el fichero, crea el fichero con una tabla vacía. |
select |
(Tabla, Ajuste) |
Devuelve una serie de tuplas, que se ajustan a una especificación de ajuste. |
select_delete |
(Tabla, Ajuste) |
Borra una serie de tuplas, que se ajustan a una especificación de ajuste que devuelva true . |
sync |
(Tabla) |
Actualiza cualquier cambio pendiente de realizar en el disco duro para una tabla. |
to_ets |
(Tabla, TabETS) |
Rellena una tabla ETS con los datos de una tabla DETS. |
traverse |
(Tabla, Función) |
Aplica una función a cada tupla de una tabla. |
Las opciones principales a la hora de abrir una tabla son las siguientes:
Opción | Descripción |
---|---|
{access, Tipo} |
Acceso de escritura y lectura a la tabla, donde Tipo puede ser read o read_write . Por defecto es read_write . |
{auto_save, Intervalo} |
Intervalo de autoguardado de la tabla, donde Intervalo es un número entero de milisegundos o el átomo infinity . |
{file, Fichero} |
Ruta del fichero con la tabla. |
{keypos, Posición} |
Posición de la clave dentro de las componentes de la tupla. Por defecto es la posición 1 . |
{ram_file, Bool} |
Con true se trabaja sobre la memoria RAM, guardando los cambios de esta en disco cuando se cierra la tabla. Con false se trabaja directamente sobre disco. |
{repair, Valor} |
Se indica si se debe aplicar el algoritmo de reparación del fichero con la tabla, donde Valor es true , false o force . |
{type, Tipo} |
Tipo de la tabla, donde Tipo puede ser set , bag o duplicate_bag . Por defecto es set . |
Dejando a un lado funciones como close/1
o sync/1
, el resto de funcionalidad para tablas DETS es muy similar al de las tablas ETS.
Mnesia es una base de datos distribuida, que por debajo utiliza ETS y DETS para gestionar la información en cada nodo. Como es una aplicación aparte, será necesario utilizar application:start/1
y application:stop/1
para cargar el sistema, pero antes de arrancar la aplicación es necesario invocar a la función mnesia:create_schema/1
, pasando la lista de nodos que van a encargarse de la base de datos:
ok = mnesia:create_schema(nodes()),
application:start(mnesia),
% Operaciones con la base de datos...
application:stop(mnesia).
Aquí configuramos la base de datos para que se ejecute en la lista de nodos visibles de la red actual, para iniciar la aplicación, hacer las operaciones que hagan falta y parar la aplicación.
Hay que tener en cuenta que se usan registros, en lugar de tuplas, para trabajar en Mnesia. Además, el nombre del tipo de registro se utiliza como nombre de la tabla. Entonces, las funciones principales del módulo mnesia
son:
Función | Parámetros | Descripción |
---|---|---|
activity |
(Modo, Función) |
Realiza un bloque de operaciones encapsuladas en una función. |
clear_table |
(Tabla) |
Borra le contenido de una tabla. |
create_table |
(Nombre, Opciones) |
Crea una tabla en la base de datos. |
delete |
({Tabla, Clave}) |
Borra entradas de una tabla con una clave. |
delete_table |
(Tabla) |
Borra una tabla de la base de datos. |
match_object |
(Patrón) |
Devuelve una lista de entradas que ajustan a un patrón de registro. |
read |
({Tabla, Clave}) (Tabla, Clave) |
Devuelve entradas de una tabla con una clave. |
select |
(Tabla, Ajuste) |
Devuelve entradas que se ajustan a una especificación de ajuste. |
table |
(Tabla) |
Devuelve un manejador para consultas con el módulo qlc . |
wait_for_tables |
(Tablas, Timeout) |
Espera un tiempo a que estén disponibles una serie de tablas. |
write |
(Entrada) |
Escribe una entrada en una tabla. |
Las opciones principales a la hora de crear una tabla son las siguientes:
Opción | Descripción |
---|---|
{access_mode, Modo} |
Acceso de escritura y lectura a la tabla, donde Modo puede ser read_only o read_write . Por defecto es read_write . |
{attributes, Lista} |
Atributos de la tabla, donde Lista son los nombres de los campos del registro. Se puede usar la función record_info(fields, Nombre) para obtener dicha lista. |
{disc_copies, Nodos} |
Indica los nodos que trabajan con tablas DETS (disco duro) y ETS (memoria). Por defecto es [] . |
{disc_only_copies, Nodos} |
Indica los nodos que trabajan sólo con tablas DETS (disco duro). |
{ram_copies, Nodos} |
Indica los nodos que trabajan sólo con tablas ETS (memoria). Por defecto es [node()] . |
{index, Índices} |
Lista de índices de la tabla, ya se indicando el nombre de los campos o sus posiciones. |
{record_name, Nombre} |
Modifica el nombre del registro a usar como elementos de la tabla, para permitir que el nombre de la tabla y del tipo de registro sea diferente. Por defecto se usa el valor usado como nombre de la tabla. |
{type, Tipo} |
Tipo de la tabla, donde Tipo puede ser set , ordered_set o bag . Por defecto es set . No se puede usar ordered_set con disc_only_copies . |
{local_content, Bool} |
Con true el contenido local de cada nodo no se compartirá, mientras que con false cada nodo tendrá la misma información para cada tabla. Por defecto es false . |
Supongamos que tenemos el siguiente tipo de dato:
-record(contact, {name, phone, birthday}).
Para crear una tabla con Mnesia haríamos lo siguiente:
mnesia:create_table(contact, [
{attributes, record_info(fields, contact)},
{index, [#contact.name]},
{disc_copies, nodes()}
])
Una vez creada las tablas, cuando se ejecuta nuestro programa y se inicia Mnesia, se puede utilizar wait_for_tables/2
para que el sistema esté preparado para trabajar con las tablas previamente creadas. Entonces, para trabajar con las tablas, usaremos la función activity/2
, que tiene los siguientes modos:
Modo | Descripción |
---|---|
transaction |
Realiza una transacción sobre la base de datos, que consiste en ejecutar una serie de operaciones como un bloque funcional atómico. Esto nos garantiza que o se aplican todas las operaciones con éxito, o no se aplica ninguna en absoluto en caso de fallar alguna. |
sync_transaction |
Realiza una transacción sobre la base de datos, pero la diferencia es que se sincroniza con todos los nodos, en lugar de hacerlo con el local únicamente. |
async_dirty |
Realiza una serie de operaciones de forma no transaccional, sincronizándose sólo con el nodo local. |
sync_dirty |
Realiza una serie de operaciones de forma no transaccional, sincronizándose con todos los nodos. |
ets |
Realiza una serie de operaciones directamente sobre las tablas ETS de forma no transaccional. |
{transaction, Intentos} |
Realiza una transacción, con un número de intentos máximos en caso de fallo. |
{sync_transaction, Intentos} |
Realiza una transacción síncrona, con un número de intentos máximos en caso de fallo. |
Lo recomendable es utilizar transacciones para realizar operaciones sobre la base de datos, para ello se usa habitualmente el modo transaction
. Por ejemplo:
mnesia:activity(transaction,
fun() ->
mnesia:write(#contact{
name = "Junko",
phone = "56709",
birthday = "April 26"
}),
mnesia:read({contact, "Junko"})
end
)
Insertamos un elemento en la base de datos y luego lo recuperamos, devolviéndolo dentro de una lista. Para hacer consultas se puede usar match_object/1
y select/2
, de la misma manera que se hacía con las tablas ETS. Otra forma de hacer consultas a las tablas es usando el módulo qlc
junto a la función table/1
. Para poder realizar estas consultas, hay que incluir la siguiente cabecera:
-include_lib("stdlib/include/qlc.hrl")
Esto nos permite usar la función qlc:q/1
, para que traduzca una expresión de lista intensional al formato interno del módulo, que podremos usar con qlc:eval/1
para ejecutar la consulta. Por ejemplo:
mnesia:activity(transaction,
fun() ->
qlc:eval(qlc:q(
[{N,P,B} ||
#contact{name=N,
phone=P,
birthday=B} <- mnesia:table(contact),
string:slice(B,0,5) =:= "April"]
))
end
)
Con esto obtenemos una lista de tuplas, de aquellas entradas cuyo cumpleaños es en abril. El módulo qlc
tiene funciones como fold/3
o sort/1
entre otras.
Erlang tiene dos sistemas para realizar pruebas: EUnit y Common Test. El primero sirve para hacer pruebas unitarias sencillas y el segundo es para sistemas de pruebas más avanzadas. Para poder utilizar EUnit hay que incluir el siguiente fichero:
-include_lib("eunit/include/eunit.hrl").
Se puede incluir dentro del módulo que queremos probar o en un fichero adicional cuyo nombre tenga la forma nombre_tests.erl
, siendo nombre el del módulo que queremos probar. Dentro del módulo para pruebas tendremos una serie de funciones cuyos hombres terminen en _test
o _test_
, que serán usadas para la ejecución de las pruebas. Por ejemplo:
-module(foo).
-export([op/2]).
op(A, B) -> A / B.
-module(foo_tests).
-include_lib("eunit/include/eunit.hrl").
ok_test() ->
2.0 = foo:op(4, 2),
1.5 = foo:op(3, 2).
fail_test() ->
1.0 = foo:op(3, 1).
Para ejecutar las pruebas usaremos la siguiente función del módulo eunit
:
Donde módulo es el nombre del mismo y opciones es una lista, en la que podemos tener verbose
para mostrar más detalle sobre las pruebas. Una vez invocada la función test
, ejecutará las pruebas y nos mostrará un informe con los fallos encontrados. Siguiendo con el ejemplo anterior:
1> eunit:test(foo).
foo_tests: fail_test...*failed*
**error:{badmatch,3.0}
output:<<"">>
=======================================================
Failed: 1. Skipped: 0. Passed: 1.
error
Existe una serie de macros para ayudar a implementar las pruebas:
Macro | Descripción |
---|---|
?assert(E) |
La expresión E debe valer true . |
?assertNot(E) |
La expresión E debe valer false . |
?assertMatch(P, E) |
La expresión E debe encajar en el patrón P . |
?assertNotMatch(P, E) |
La expresión E no debe encajar en el patrón P . |
?assertEqual(E1, E2) |
El valor de las expresiones debe ser igual. |
?assertNotEqual(E1, E2) |
El valor de las expresiones no debe ser igual. |
?assertException(C, P, E) |
La expresión E produce una excepción P de la clase C . |
?assertError(P, E) |
La expresión E produce un error P . |
?assertExit(P, E) |
La expresión E produce una salida P . |
?assertThrow(P, E) |
La expresión E produce una excepción P . |
También hay macros para ejecutar comandos en la terminal del sistema operativo:
Macro | Descripción |
---|---|
?assertCmd(C) |
Ejecuta el comando C y comprueba que su valor de salida es 0 . |
?assertCmdStatus(N, C) |
Ejecuta el comando C y comprueba que su valor de salida es N . |
?assertCmdOutput(T, C) |
Ejecuta el comando C y comprueba que su salida es igual a T . |
?cmd(C) |
Ejecuta el comando C . |
Además tenemos una serie de macros para la salida estándar:
Macro | Descripción |
---|---|
?capturedOutput |
Devuelve el contenido de la salida estándar. |
?debugHere |
Muestra por consola el fichero y el número de línea actual. |
?debugMsg(T) |
Muestra por consola una cadena de texto. |
?debugFmt(F, Args) |
Muestra por consola una cadena de texto. |
?debugVal(E) |
Muestra por consola un valor. |
?debugVal(E, N) |
Muestra por consola un valor con una profundidad máxima de N . |
?debugTime(T, E) |
Muestra por consola una cadena de texto T y el tiempo que se ha tardado en evaluar E , devolviendo el valor obtenido como resultado de la macro. |
Las funciones que terminan con _test_
se denominan generadores de pruebas. Estas funciones han de devolver funciones o listas de funciones que serán tomadas para ejecutar la batería de pruebas. Para ello existe la macro ?_test(E)
que encapsula la expresión en una lambda. Las macros para realizar asertos, como ?assert(E)
, tienen su equivalente que empieza por guion bajo para la generación de funciones de pruebas, por ejemplo ?_assert(E)
, que equivale a ?_test(?assert(E))
. Tomando el ejemplo anterior:
all_test_() ->
[?_assertMatch(2.0, foo:op(4, 2)),
?_assertMatch(1.5, foo:op(3, 2)),
?_assertMatch(1.0, foo:op(3, 1))].
Cuyo resultado de la prueba sería:
1> eunit:test({generator, fun foo_tests:all_test_/0}).
foo_tests:13: all_test_...*failed*
**error:{assertMatch,[{module,foo_tests},
{line,13},
{expression,"foo : op ( 3 , 1 )"},
{pattern,"1.0"},
{value,3.0}]}
output:<<"">>
=======================================================
Failed: 1. Skipped: 0. Passed: 2.
error
Como se puede ver, podemos indicar con la tupla {generator, Función}
la función concreta que queremos probar, si sólo queremos ejecutar una prueba en concreto.
Dentro de las funciones generadoras de pruebas, se puede devolver lo que Erlang denomina fixture, que es una tupla con una configuración para realizar pruebas un poco más complejas con las pruebas unitarias. Para ello tenemos como opciones:
El significado de cada componente es el siguiente:
local
, spawn
y {spawn, Nodo}
.foreach
. Para cada función de esta lista se ejecutará la función inicio, luego prueba y por último final.Se puede para la función prueba devolver una tupla en lugar de una lista, siguiendo las siguientes opciones:
{spawn, Lista}
: Ejecuta las pruebas de la lista en un proceso aparte.{timeout, Segundos, Lista}
: Ejecuta las pruebas de la lista con un tiempo máximo de ejecución.{inorder, Lista}
: Ejecuta las pruebas de la lista en el orden que están.{inparallel, Lista}
: Ejecuta las pruebas de la lista en paralelo si es posible.Por último, se puede añadir una cadena de texto como comentario a una configuración de pruebas devolviendo como resultado {Cadena, Configuración}
.
Podemos incluir en nuestros proyectos un fichero llamado Emakefile
, que nos permitirá automatizar la compilación. El contenido del fichero es una secuencia de definiciones con la forma {Módulos, Opciones}.
, donde módulos son átomos que indican la ubicación relativa de los módulos a compilar y las opciones determinan cómo se va a compilar cada módulo. Por ejemplo:
{'sources/*', [debug_info,
{i, "sources"},
{i, "include"},
{outdir, "ebin"}]}.
El fichero indica que nuestros módulos están en el directorio sources
y luego una serie de opciones para la compilación. Entre las opciones de compilación que existen tenemos:
Opción | Descripción |
---|---|
debug_info |
Incluye información útil para la depuración. |
{i,Dir} |
Añade un directorio a la lista de búsqueda cuando se quiere incluir una cabecera con el preprocesador. |
{outdir,Dir} |
Establece el directorio de salida donde se guardará el resultado de la compilación. |
export_all |
Hace que todas las funciones del módulo sean exportadas y por lo tanto públicas al exterior. |
{d,Macro} |
Define una macro. |
{d,Macro,Valor} |
Define una macro con un valor asignado. |
Una vez configurado el fichero, para compilar el proyecto podemos usar el comando:
erl -make
O también desde la consola de Erlang invocar a:
make:all().