Erlang/OTP - Volumen I: Un Mundo Concurrente

Transcripción

Erlang/OTP - Volumen I: Un Mundo Concurrente
Erlang/OTP
Volumen I: Un Mundo Concurrente
Manuel Angel Rubio Jiménez
Erlang/OTP
Volumen I: Un Mundo Concurrente
Manuel Angel Rubio Jiménez
Resumen
El lenguaje de programación Erlang nació sobre el año 1986 en los laboratorios
Ericsson de la mano de Joe Armstrong. Es un lenguaje funcional con base
en Prolog, tolerante a fallos, y orientado al trabajo en tiempo real y a la
concurrencia, lo que le proporciona ciertas ventajas en lo que a la declaración
de algoritmos se refiere.
Como la mayoría de lenguajes funcionales Erlang requiere un análisis del
problema y una forma de diseñar la solución diferente a como se haría en un
lenguaje de programación imperativo. Sugiere una mejor y más eficiente forma
de llevarlo a cabo. Se basa en una sintaxis más matemática que programática
por lo que tiende más a la resolución de problemas que a la ordenación y
ejecución de órdenes.
Todo ello hace que Erlang sea un lenguaje muy apropiado para la programación
de elementos de misión crítica, tanto a nivel de servidor como a nivel de
escritorio, e incluso para el desarrollo de sistemas embebidos o incrustados.
En este libro se recoge un compendio de información sobre lo que es el
lenguaje, cómo cubre las necesidades para las que fue creado, cómo sacarle
el máximo provecho a su forma de realizar las tareas y a su orientación a la
concurrencia. Es un repaso desde el principio sobre cómo programar de una
forma funcional y concurrente en un entorno distribuido y tolerante a fallos.
Erlang/OTP, Volumen I: Un Mundo Concurrente por Manuel Ángel Rubio
1
Jiménez se encuentra bajo una Licencia Creative Commons Reconocimiento2
NoComercial-CompartirIgual 3.0 Unported .
1
2
http://erlang.bosqueviejo.net/
http://creativecommons.org/licenses/by-nc-sa/3.0/
Tabla de contenidos
Prólogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii
Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix
1. Acerca del autor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix
2. Acerca de los Revisores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . x
3. Acerca del libro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi
4. Objetivo del libro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xii
5. ¿A quién va dirigido este libro? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii
6. Estructura de la colección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii
7. Nomenclatura usada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiv
8. Agradecimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xv
9. Más información en la web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvi
1. Lo que debes saber sobre Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1. ¿Qué es Erlang? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2. Características de Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
3. Historia de Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4. Desarrollos con Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
4.1. Sector empresarial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
4.2. Software libre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5. Erlang y la Concurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
5.1. El caso de Demonware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.2. Yaws contra Apache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2. El lenguaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1. Tipos de Datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.1. Átomos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.2. Números Enteros y Reales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3. Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.4. Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.5. Tuplas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.6. Registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2. Imprimiendo por pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3. Fechas y Horas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3. Expresiones, Estructuras y Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1. Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.1. Expresiones Aritméticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.2. Expresiones Lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.3. Precedencia de Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2. Estructuras de Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.1. Concordancia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.2. Estructura case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.3. Estructura if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.4. Listas de Comprensión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3. Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.1. Recoger excepciones: catch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
iii
Erlang/OTP
3.2. Lanzar una excepción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.3. La estructura try...catch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.4. Errores de ejecución más comunes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4. Las funciones y módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
1. Organización del código . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2. Ámbito de las funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3. Polimorfismo y Concordancia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4. Guardas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5. Clausuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6. Programación Funcional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
7. Recursividad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
7.1. Ordenación por mezcla (mergesort) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
7.2. Ordenación rápida (quicksort) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8. Funciones Integradas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5. Procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
1. Anatomía de un Proceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2. Ventajas e inconvenientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3. Lanzando Procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4. Bautizando Procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5. Comunicación entre Procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
6. Procesos Enlazados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
7. Monitorización de Procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
8. Recarga de código . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
9. Gestión de Procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
10. Nodos Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
11. Procesos Remotos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
12. Procesos Locales o Globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
13. RPC: Llamada Remota a Proceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
14. Diccionario del Proceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6. ETS, DETS y Ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
1. ETS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
1.1. Tipos de Tablas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
1.2. Acceso a las ETS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
1.3. Creación de una ETS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
1.4. Lectura y Escritura en ETS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
1.5. Match: búsquedas avanzadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
1.6. Eliminando tuplas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
1.7. ETS a fichero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
2. DETS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
2.1. Tipos de Tablas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
2.2. Crear o abrir una DETS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
2.3. Manipulación de las DETS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
2.4. De ETS a DETS y viceversa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
3. Ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.1. Abriendo y Cerrando Ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.2. Lectura de Ficheros de Texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
3.3. Escritura de Ficheros de Texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
iv
Erlang/OTP
3.4. Lectura de Ficheros Binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5. Escritura de Ficheros Binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.6. Acceso aleatorio de Ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.7. Lecturas y Escrituras por Lotes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4. Gestión de Ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1. Nombre del fichero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2. Copiar, Mover y Eliminar Ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3. Permisos, Propietarios y Grupos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5. Gestión de Directorios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1. Directorio de Trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2. Creación y Eliminación de Directorios . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.3. ¿Es un fichero? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.4. Contenido de los Directorios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7. Comunicaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1. Conceptos básicos de Redes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1. Direcciones IP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2. Puertos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2. Servidor y Cliente UDP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3. Servidor y Cliente TCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4. Servidor TCP Concurrente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5. Ventajas de inet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8. Ecosistema Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1. Iniciar un Proyecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1. Instalar rebar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2. Escribiendo el Código . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2. Compilar y Limpiar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3. Creando y lanzando una aplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4. Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5. Liberar y Desplegar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6. Actualizando en Caliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7. Guiones en Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8. El camino a OTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Apéndices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
A. Instalación de Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1. Instalación en Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2. Instalación en sistemas GNU/Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1. Desde Paquetes Binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2. Compilando el Código Fuente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3. Otros sistemas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B. La línea de comandos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1. Registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2. Módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3. Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4. Histórico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5. Procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6. Directorio de trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7. Modo JCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
v
103
105
105
106
107
107
108
109
110
111
111
112
113
114
114
115
117
118
123
125
127
131
131
132
133
135
136
137
140
146
150
152
153
154
154
155
156
156
157
158
158
159
160
160
161
162
162
Erlang/OTP
8. Salir de la consola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
C. Herramientas gráficas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1. Barra de herramientas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2. Monitor de aplicaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3. Gestor de procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4. Visor de tablas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5. Observer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6. Depurador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
vi
163
164
164
165
167
168
169
170
Prólogo
Conocí a Manuel Angel cuando me explicó su idea de escribir un libro
sobre Erlang en Castellano, no sólo me pareció una idea apasionante,
sino un hito imprescindible para llevar este lenguaje de programación a
donde se merece entre la comunidad castellano-parlante.
Tras intercambiar algunos emails, enseguida me di cuenta de la similitud
de nuestras ideas y objetivos: escribir programas eficientes y escalables.
Y aunque no lo conocía personalmente, simplemente con ver su dilatada
experiencia en un abanico tan amplio de tecnologías, ya intuí que el
material que saldría de su cabeza sería de ayuda para todo tipo lectores.
En un mundo donde predomina la programación imperativa, los
lenguajes funcionales vuelven a cobrar importancia por su potencia
y sencillez. La necesidad de sistemas que sean capaces de gestionar
millones de usuarios concurrentes de manera eficiente, ha provocado
que Erlang sea relevante dos décadas después de su creación.
Mi primera experiencia con Erlang fue como ver Matrix, teniendo en
cuenta que todos los conocimientos que tenía estaban basados en
lenguajes orientados a objetos, el primer instinto fue extrapolarlos a
aquel primer reto al que me enfrentaba (iterar sobre una lista). Con el
paso de los días empecé a comprender que el salto que estaba realizando
no era como aprender otro lenguaje más (saltar entre PHP, Java o Ruby),
estaba aprendiendo una nueva forma de pensar y resolver problemas la
esencia de los lenguajes funcionales.
Cabe destacar, que los conceptos y herramientas que proporciona
de manera nativa Erlang, te permiten diseñar y desarrollar desde el
inicio sistemas robustos, evitando tener que resolver problemas de
escalabilidad y operaciones complejas en las siguientes fases de un
proyecto (capas de cache complejas, despliegues en producción sin
interrupciones, optimización de la máquina virtual, ...).
La introducción al lenguaje propuesta por Manuel Angel, desde la base,
pasando por los tipos de datos y expresiones, y terminando con las
funcionalidades nativas que lo diferencian, ayudarán tanto a lectores
noveles, como a lectores con experiencia en programación funcional.
Cuando alguien me comenta que quiere aprender Erlang suelo decir tres
cosas:
1. Intenta con todas tus fuerzas olvidar todo lo que sepas de
programación imperativa;
2. Lee un buen libro, completo, desde la introducción hasta las reseñas;
vii
Prólogo
3. Ten siempre una consola a mano para ir poniendo en práctica los
conocimientos adquiridos.
Hasta el momento de la publicación de este libro, ese consejo estaba
muy condicionado al conocimiento de inglés de la persona que lo recibía;
y si sumamos todos los nuevos conceptos al que el lector se enfrenta, el
resultado no siempre era el esperado. Gracias a este libro, con un estilo
claro y directo, ejemplos útiles y el reflejo de la experiencia del autor,
hará que aprender este lenguaje sea una experiencia productiva, de la
que espero nazcan desde simples algoritmos rápidos y eficientes, hasta
sistemas distribuidos altamente escalables.
—José Luis Gordo Romero
viii
Introducción
Sorprendernos por algo es el primer paso de la
mente hacia el descubrimiento.
—Louis Pasteur
1. Acerca del autor
La programación es un tema que me ha fascinado desde siempre. A partir
del año 2002, a la edad de 22 años, me centré en perfeccionar mis
conocimientos sobre C++, el paradigma de la orientación a objetos y sus
particularidades de implementación en este lenguaje.
El lenguaje C++ me abrió las puertas de la orientación a objetos y ese
mismo año ya comencé a interesarme por Java. Al año siguiente, en 2003,
aprendí SQL, Perl y PHP, comenzando así una aventura que me ha llevado
al aprendizaje de nuevos lenguajes de programación regularmente,
siempre con el interés de analizar sus potencias y debilidades. Así es
como experimenté también con lenguajes clásicos como Basic, Pascal,
Modula-2 y otro tipo de lenguajes de scripting para la gestión de sistemas
informáticos como Perl o lenguajes de shell.
En los siguientes 8 años, después de haber tratado con lenguajes
imperativos, tanto estructurados como orientados a objetos, con sus
particularidades y ecosistemas como son C/C++, Java, Perl, Python, PHP,
Ruby, Pascal y Modula-2 entre otros, descubrí Erlang.
En Erlang encontré un mundo en el que es posible desarrollar estructuras
complejas cliente-servidor, en el que los procesos son concurrentes,
distribuidos, robustos y tolerantes a fallos. Por si fuera poco, estas
estructuras se crean mediante un código compacto y con una sintaxis
clara, elegante y fácilmente comprensible.
En el año 2006, encabecé algunos desarrollos en una oficina de I+D
en Córdoba que no resultaron del todo favorables. En aquella época, el
desarrollo mediante lenguajes imperativos, y las estructuras propias de
concurrencia y distribución, hicieron que la creación de soluciones fuese
excesivamente costosa y se terminase desechando.
En 2008 volví a retomar el desarrollo de sistemas del área de voz,
principalmente en telefonía. Con el bagaje de la experiencia anterior
y dispuestos a aplicar las mejores soluciones que proporciona Erlang,
encabecé una serie de proyectos para entornos de telecomunicaciones.
Estos proyectos los desarrollamos con éxito con una escasa cantidad
de código, en unos tiempos y con una calidad y robustez que parecería
ix
Introducción
imposible en otros lenguajes. Igualmente comprobamos la simpleza,
efectividad y la capacidad de escalado que brinda el lenguaje y su
máquina virtual.
Con todo lo aprendido y hecho en torno a este lenguaje, me he dado
cuenta de que hace falta llenar el hueco que deja el no tener literatura
sobre Erlang en nuestro idioma, y de paso tratar los temas desde otro
punto de vista.
Puedo lanzarme a esta tarea no sin antes recomendar la literatura
existente que me ha servido como referencia durante mi propio proceso
de aprendizaje y paso a enumerar. El libro de Joe Armstrong sobre Erlang,
completísimo y centrado en el lenguaje base que he releído decenas de
veces. El de Francesco Cesarini, igualmente recomendable, aunque más
orientado al desarrollo de proyectos en Erlang. Incluso otro gran libro
1
que he conocido recientemente del equipo que mantiene Erlware , muy
orientado al framework OTP y la metodología que propone.
2. Acerca de los Revisores
Este libro ha sido revisado por dos personas, sin las cuales, de seguro
que sería mucho más complejo de seguir. En esta sección hacemos una
pequeña presentación de ambos.
José Luis Gordo Romero
Apasionado de la tecnología y del software libre. Durante mi
carrera profesional he recorrido distintas áreas tecnológicas, lo
que me ha permitido afrontar proyectos teniendo una perspectiva
global. Empecé por la administración y automatización de sistemas,
pasando por el desarrollo hasta llegar al diseño de arquitectura (en
entornos web).
Trabajar en startups me ha permitido explorar y profundizar en
diferentes tecnologías, además de poder colaborar en varios
proyectos de software libre (de los cuales disfruto aprendiendo y
aportando todo lo que puedo).
Actualmente estoy centrado en varios proyectos donde Erlang es
la base, así que haber podido ayudar en la revisión y escribir el
prólogo, ha sido todo un placer.
Juan Sebastián Pérez Herrero
Soy experto en diversas tecnologías, con abultada experiencia en
entornos web, plataformas de movilidad, integración y gestión
1
http://erlware.com/
x
Introducción
de proyectos internacionales y entornos open source de lo más
diversos.
He sido compañero de trabajo de Manuel, lo que ha servido
para un enriquecimiento mutuo, tanto en conocimientos como en
estrategia. Su amplio espectro de conocimiento aporta muchos
patrones y antipatrones, su carácter templado hace que siempre
se pueda llegar a acuerdos y sus indicaciones, especialmente en
el procesamiento a tiempo real y de sistemas con alto número de
transacciones me han sido de gran interés.
Me gusta participar en proyectos estimulantes y la edición de
este libro junto con el aprendizaje de Erlang, era una oportunidad
de divertirme haciendo una tarea nueva como es la edición de
literatura técnica en español que no estaba dispuesto a dejar pasar.
He realizado la edición de varios capítulos y ha resultado ser una
experiencia más que interesante. Ponerse en la piel de un lector
y facilitarle las cosas sin bajar demasiado el nivel técnico es un
reto. Espero que el resultado, probablemente mejorable, facilite la
lectura y comprensión de la obra y por ende su difusión.
El presente libro permite formarse en programación concurrente
en Erlang de una forma entretenida. Ya estoy deseando leer la
segunda parte sobre OTP, ya que intuyo que para proyectos de
cierta envergadura se requieren unas directrices claras y el uso
de buenos patrones de diseño, sobre todo si se busca la robustez
que normalmente requieren proyectos críticos que en muchas
ocasiones manejan transacciones monetarias.
3. Acerca del libro
Durante el año 2008, estuvimos trabajando en proyectos de fuerte
concurrencia para el tratamiento de llamadas telefónicas. Buscábamos
que los tiempos de respuesta, la robustez y la agilidad en los desarrollos
fuera la necesaria para este tipo de sistemas. Tras haber empleado
diversas herramientas y técnicas de replicación y compartición en base
de datos sin obtener resultados totalmente satisfactorios nos decidimos
a introducir Erlang. Con ello pudimos observar las capacidades de
este lenguaje y lo bien que se adaptaba a nuestras necesidades de
rendimiento. Fue entonces cuando nos dimos cuenta de que había muy
poca cantidad de información acerca del lenguaje (aunque poco a poco
se vaya subsanando), y mucho menos en castellano.
Es complejo adentrarse en un lenguaje nuevo que nada tiene que
ver con lenguajes con los que se haya trabajado anteriormente (salvo
excepciones como Lisp, Scheme o Prolog), por lo que me decidí a escribir
xi
Introducción
el libro que me hubiese gustado encontrar. Un libro con las palabras
justas y los diagramas apropiados para poder entender más rápidamente
todos los conceptos nuevos que se ponen delante del programador de
Erlang y OTP.
Por último, el hecho de que el texto esté en castellano hace que, sin duda,
sea más asequible para el público de habla hispana. El nivel y densidad
de ciertas explicaciones son más bajos cuando se tratan en el idioma
nativo, lo que hace que sea más fácil de entender.
4. Objetivo del libro
Con este libro pretendo cubrir principalmente los aspectos más
importantes dentro del ámbito de aprendizaje de un nuevo lenguaje de
programación:
• Explicar los aspectos básicos del lenguaje para comenzar a programar.
Ya que Erlang no es un lenguaje imperativo, puede ocurrir que su
sintaxis sea paradójicamente más fácil para el que no sabe programar
que para desarrolladores avanzados de lenguajes como C, Java o PHP.
• Conocer las fortalezas y debilidades del lenguaje. Como en el uso de
cualquier tecnología es importante tener la capacidad de seleccionar
un lenguaje o entorno frente a otro dependiendo del trabajo que se
vaya a realizar. En este texto analizamos qué es Erlang y en qué se
puede emplear, con lo que se obtendrá una idea clara de posibles
casos de uso cuando tenga que acometer un nuevo desarrollo.
Hay muchos casos en los que una mala elección tecnológica ha forzado
a reescribir, versión a versión, el desarrollo inicial. La motivación a la
hora de seleccionar una tecnología no puede ser nunca una moda o la
inercia. Aunque cada año haya un nuevo lenguaje que ofrece versatilidad
y gran cantidad de facilidades, hay que tener siempre en mente que
un lenguaje puede estar orientado a resolver un problema determinado
más adecuadamente que otros. En este punto hay que ser mucho más
pragmáticos que fanáticos.
Hay desarrollos que en una versión temprana se han abandonado
completamente y se han recomenzado de otra forma. Ya sea con otros
lenguajes, herramientas, librerías o frameworks. El hecho de toparse con
impedimentos tan grandes de salvar ha provocado que una reescritura
desde cero sea con frecuencia lo más simple y rápido.
Para ampliar el conocimiento y la posibilidad de elección, sobre
todo ahora que se incrementa el número de sistemas concurrentes,
de alta disponibilidad, tolerantes a fallos y que deben prestar un
servicio continuo en la red de redes, el presente libro proporciona el
conocimiento de lo que es Erlang, lo que es OTP, y lo que significan estas
xii
Introducción
nuevas herramientas que va ganando cada vez más relevancia en los
entornos mencionados.
5. ¿A quién va dirigido este libro?
Este libro está dirigido a todo aquél que quiera aprender a programar
en un lenguaje funcional con control de concurrencia y distribuido.
Permite igualmente ampliar el vocabulario de programación del lector
con nuevas ideas sobre el desarrollo de programas y la resolución de
problemas. Esto se aplica tanto a los que comienzan a programar como a
los que ya saben programar, y a aquellos que quieren saber qué puede
hacer este lenguaje para tomarlo en consideración en las decisiones
tecnológicas de su empresa o proyecto.
Para los programadores neófitos ofrece una guía de aprendizaje base, una
forma rápida de adentrarse en el conocimiento del lenguaje que permite
comenzar a desarrollar directamente. Propone ejemplos, ejercicios y
preguntas que el programador puede realizar, resolver y responder.
Para el programador experimentado ofrece un nexo hacia un lenguaje
diferente, si el lector proviene del mundo imperativo, o bien
relativamente similar a otros vistos (si se tienen conocimientos de
Lisp, Scheme, Prolog o Haskell). Provee un acercamiento detallado a las
entrañas de un sistema desarrollado con una ideología concreta y para
un fin concreto. Incluso si ya se conoce Erlang supone un recorrido por lo
que ya se sabe, pero desde otro enfoque y con características o detalles
que probablemente no se conozcan.
Para el desarrollador, analista o arquitecto, ofrece el punto de vista
de una herramienta, un lenguaje y un entorno, en el que se pueden
desarrollar un cierto abanico de soluciones de forma rápida y segura.
Erlang es un lenguaje con muchos años de desarrollo, probado en
producción por muchas empresas conocidas y desconocidas. Permite
realizar un recorrido por las potencias del lenguaje y obtener el
conocimiento de sus debilidades. Lo suficiente como para saber si es una
buena herramienta para desarrollar una solución específica.
6. Estructura de la colección
Al principio pensé en escribir un único libro orientado a Erlang, pero
viendo el tamaño que estaba alcanzando pensé que mejor era dividirlo
por temática y darle a cada libro la extensión apropiada como para ser
leído y consultado de forma fácil y rápida.
La colección, por tanto, consta de dos volúmenes. Cada volumen tiene
como misión explorar Erlang de una forma diferente, desde un punto de
vista diferente, y con un objetivo diferente. Los volúmenes son:
xiii
Introducción
• Un mundo concurrente. En esta parte nos centraremos en conocer la
sintaxis del lenguaje, sus elementos más comunes, sus estructuras, los
tipos de datos, el uso de los ficheros y comunicaciones a través de la
red. Será el bloque más extenso, ya que detalla toda la estructura del
lenguaje en sí.
• Las bases de OTP. Nos adentramos en el conocimiento del sistema OTP,
el framework actualmente más potente para Erlang y que viene con su
instalación base. Se verán los generadores de servidores, las máquinas
de estados, los supervisores y manejadores de eventos, entre otros
elementos.
Nota
Recomiendo que, para poder hacer los ejemplos y practicar lo
que se va leyendo, se tenga a mano un ordenador con Erlang
instalado, así como acceso a su consola y un directorio en el
que poder ir escribiendo los programas de ejemplo. En este
caso será de bastante ayuda revisar los apéndices donde explica
cómo se descarga, instala y usa la consola de Erlang, así como la
compilación de los ejemplos y su ejecución de forma básica.
7. Nomenclatura usada
A lo largo del libro encontrarás muchos ejemplos y fragmentos de código.
Los códigos aparecen de una forma visible y con un formato distinto al
del resto del texto. Tendrán este aspecto:
-module(hola).
mundo() ->
io:format("Hola mundo!~n", []).
Además de ejemplos con código Erlang, en los distintos apartados del
libro hay diferentes bloques que contienen notas informativas o avisos
importantes. Sus formatos son los siguientes:
Nota
Esta es la forma que tendrán las notas informativas. Contienen
detalles o información adicional sobre el texto para satisfacer la
curiosidad del lector.
Importante
Estas son las notas importantes que indican usos específicos
y detalles importantes que hay que tener muy en cuenta. Se
recomienda su lectura.
xiv
Introducción
8. Agradecimientos
Manuel Ángel Rubio
Agradecer a mi familia, Marga, Juan Antonio y Ana María, por ser
pacientes y dejarme el tiempo suficiente para escribir, así como
su amor y cariño. A mis padres por enseñarme a defenderme en
esta vida, así como a competir conmigo mismo para aprender y
superarme en cada reto personal y profesional.
Respecto al libro, he de agradecer al equipo con el que estuve
trabajando en Jet Multimedia: Guillermo Rodríguez, María Luisa de
la Serna, Jonathan Márquez, Margarita Ortiz y Daniel López; el que
cada desarrollo que nos planteasen pudiésemos verlo como un
desafío a nosotros mismos y sacar lo mejor de nosotros mismos,
así como aprender de cada situación, de cada lenguaje y de cada
herramienta. Aprendí mucho con ellos y espero que podamos seguir
aprendiendo allá donde nos toque estar y, si volvemos a coincidir,
muchísimo mejor.
También agradecer a José Luis Gordo por su revisión, el prólogo
escrito y sus buenos consejos así como su crítica constructiva, ha
sido un aliado inestimable en esta aventura y un balón de oxígeno
en momentos arduos.
A Juan Sebastián Pérez, por brindarse también a aprender el
lenguaje de manos de este manuscrito, así como corregir también
mi forma de expresarme en algunos puntos que confieso fueron
complicados.
Por último pero no por ello menos importante, agradecer a mi
hermano Rafael y a Luz (Bethany Neumann) el diseño de la portada
y contraportada del libro, así como el logotipo de BosqueViejo.
José Luis Gordo
Agradecer a Manuel Angel su confianza por haberme dejado aportar
mi pequeño granito de arena a este proyecto. Además de ampliar
conocimientos, me ha dado la oportunidad de conocer mejor su
trabajo y a él personalmente, descubriendo su increíble energía y
motivación, sin la cual este libro nunca hubiera visto la luz.
Juan Sebastián Pérez
Gracias a Manuel por compartir tantos cafés (descafeinados) e ideas.
A otros compañeros en lo profesional y personal, especialmente al
departamento de movilidad de Jet Multimedia por compartir fatigas
y éxitos. Y como no, a mi familia, amigos y pareja que me han
xv
Introducción
apoyado en el desarrollo de mis habilidades en otros aspectos de
la vida.
9. Más información en la web
Para obtener información sobre las siguientes ediciones, fe de erratas
y comentarios, contactos, ayuda y demás sobre el libro Erlang/OTP he
habilitado una sección en mi web.
El sitio web:
http://erlang.bosqueviejo.net
xvi
Capítulo 1. Lo que debes saber
sobre Erlang
Software para un mundo concurrente.
—Joe Armstrong
Erlang comienza a ser un entorno y un lenguaje de moda. La existencia
creciente de empresas orientadas a la prestación de servicios por
internet con un elevado volumen de transacciones (como videojuegos
en red o sistemas de mensajería móvil y chat) hace que en sitios como
los Estados Unidos, Reino Unido o Suecia proliferen las ofertas de trabajo
que solicitan profesionales en este lenguaje. Existe una necesidad
imperiosa de desarrollar entornos con las características de la máquina
de Erlang, y la metodología de desarrollo proporcionada por OTP.
En este capítulo introducimos el concepto de Erlang y OTP. Su significado,
características e historia. La información de este primer capítulo se
completa con las fuentes que lo han motivado y se provee información
precisa sobre dónde se ha extraído cada sección.
1. ¿Qué es Erlang?
Para comprender qué es Erlang, debemos entender que se trata de un
entorno o plataforma de desarrollo completa. Erlang proporciona no sólo
el compilador para poder ejecutar el código, sino que posee también una
colección de herramientas, y una máquina virtual sobre la que ejecutarlo,
por lo tanto existen dos enfoques:
Erlang como lenguaje
Hay muchas discusiones concernientes a si Erlang es o no un
lenguaje funcional. En principio, está entendido que sí lo es, aunque
tenga elementos que le hagan salirse de la definición pura. Por
ello Erlang podría mejor catalogarse como un lenguaje híbrido, al
tener elementos de tipo funcional, de tipo imperativo, e incluso
algunos rasgos que permiten cierta orientación a objetos, aunque
no completa.
Donde encaja mejor Erlang, al menos desde mi punto de vista, es
como un lenguaje orientado a la concurrencia. Erlang tiene una gran
facilidad para la programación distribuida, paralela o concurrente y
además con mecanismos para la tolerancia a fallos. Fue diseñado
desde un inicio para ejecutarse de forma ininterrumpida. Esto
significa que se puede cambiar el código de sus aplicaciones sin
detener su ejecución. Más adelante explicaremos cómo funciona
esto concretamente.
1
Lo que debes
saber sobre Erlang
Erlang como entorno de ejecución
Como hemos mencionado antes Erlang es una plataforma de
desarrollo que proporciona no sólo un compilador, sino también
una máquina virtual para su ejecución. A diferencia de otros
lenguajes interpretados como Python, Perl, PHP o Ruby, Erlang se
pseudocompila y su máquina virtual le proporciona una importante
capa de abstracción que le dota de la capacidad de manejar y
distribuir procesos entre nodos de forma totalmente transparente
(sin el uso de librerías específicas).
La máquina virtual sobre la que se ejecuta el código pseudocompilado de Erlang, que le proporciona todas las características de
distribución y comunicación de procesos, es también una máquina
1
que interpreta un pseudocódigo máquina que nada tiene que ver,
a ese nivel, con el lenguaje Erlang. Esto ha permitido la proliferación
de los lenguajes que emplean la máquina virtual pero no el lenguaje
en sí, como pueden ser: Reia, Elixir, Efene, Joxa o LFE.
Erlang fue propietario hasta 1998 momento en que fue cedido como
código abierto (open source) a la comunidad. Fue creado inicialmente por
Ericsson, más específicamente por Joe Armstrong, aunque no sólo por él.
Recibe el nombre de Agnus Kraup Erlang. A veces se piensa que el
nombre es una abreviación de ERicsson LANGuage, debido a su uso
intensivo en Ericsson. Según Bjarne Däcker, jefe del Computer Science
Lab en su día, esta dualidad es intencionada.
2. Características de Erlang
Durante el período en el que Joe Armstrong y sus compañeros estuvieron
en los laboratorios de Ericsson, vieron que el desarrollo de aplicaciones
basadas en PLEX no era del todo óptimo para la programación de
aplicaciones dentro de los sistemas hardware de Ericsson. Por esta razón
comenzaron a buscar lo que sería un sistema de desarrollo óptimo
basado en las siguiente premisas:
Distribuido
El sistema debía de ser distribuido para poder balancear su carga
entre los sistemas hardware. Se buscaba un sistema que pudiera
lanzar procesos no sólo en la máquina en la que se ejecuta, sino
que también fuera capaz de hacerlo en otras máquinas. Lo que en
lenguajes como C viene a ser PVM o MPICH pero sin el uso explícito
de ninguna librería.
1
O trozos de código nativo si se emplea HiPE.
2
Lo que debes
saber sobre Erlang
Tolerante a fallos
Si una parte del sistema tiene fallos y tiene que detenerse, que esto
no signifique que todo el sistema se detenga. En sistemas software
como PLEX o C, un fallo en el código determina una interrupción
completa del programa con todos sus hilos y procesos. Hay otros
lenguajes como Java, Python o Ruby que manejan estos errores
como excepciones, afectando sólo a una parte del programa y
no a todos sus hilos. No obstante, en los entornos con memoria
compartida, un error puede dejar corrupta esta memoria por lo
que esa opción no garantiza tampoco que no afecte al resto del
programa.
Escalable
Los sistemas operativos convencionales tenían problemas en
mantener un elevado número de procesos en ejecución. Los
sistemas de telefonía que desarrolla Ericsson se basan en tener
un proceso por cada llamada entrante, que vaya controlando los
estados de la misma y pueda provocar eventos hacia un manejador,
a su vez con sus propios procesos. Por lo que se buscaba un sistema
que pudiese gestionar desde cientos de miles, hasta millones de
procesos.
Cambiar el código en caliente
También es importante en el entorno de Ericsson, y en la mayoría
de sistemas críticos o sistemas en producción de cualquier índole,
que el sistema no se detenga nunca, aunque haya que realizar
actualizaciones. Por ello se agregó también como característica el
hecho de que el código pudiese cambiar en caliente, sin necesidad
de parar el sistema y sin que afectase al código en ejecución.
También había aspectos íntimos del diseño del lenguaje que se quisieron
tener en cuenta para evitar otro tipo de problemas. Aspectos tan
significativos como:
Asignaciones únicas
Como en los enunciados matemáticos la asignación de un valor a
una variable se hace una única vez y, durante el resto del enunciado,
esta variable mantiene su valor inmutable. Esto nos garantiza un
mejor seguimiento del código y una mejor detección de errores.
Lenguaje simple
Para rebajar la curva de aprendizaje el lenguaje debe de tener pocos
elementos y ninguna excepción. Erlang es un lenguaje simple de
comprender y aprender, ya que tiene nada más que dos estructuras
de control, carece de bucles y emplea técnicas como la recursividad
y modularización para conseguir algoritmos pequeños y eficientes.
3
Lo que debes
saber sobre Erlang
Las estructuras de datos se simplifican también bastante y su
potencia, al igual que en lenguajes como Prolog o Lisp, se basa en
las listas.
Orientado a la Concurrencia
Como una especie de nueva forma de programar, este lenguaje se
orienta a la concurrencia de manera que las rutinas más íntimas
del propio lenguaje están preparadas para facilitar la realización de
programas concurrentes y distribuidos.
Paso de mensajes en lugar de memoria compartida
Uno de los problemas de la programación concurrente es la
ejecución de secciones críticas de código para acceso a porciones
de memoria compartida. Este control de acceso acaba siendo un
cuello de botella ineludible. Para simplificar e intentar eliminar
el máximo posible de errores, Erlang/OTP se basa en el paso
de mensajes en lugar de emplear técnicas como semáforos
o monitores. El paso de mensajes hace que un proceso sea
el responsable de los datos y la sección crítica se encuentre
sólo en este proceso, de modo que cualquiera que pida
ejecutar algo de esa sección crítica, tenga que solicitárselo
al proceso en cuestión. Esto abstrae al máximo la tarea de
desarrollar programas concurrentes, simplificando enormemente
los esquemas y eliminando la necesidad del bloque explícito.
Hace no mucho encontré una presentación bastante interesante sobre
2
Erlang , en la que se agregaba, no sólo todo lo que comentaba Armstrong
que debía de tener su sistema para poder desarrollar las soluciones
de forma óptima, sino también la contraposición, el porqué no lo pudo
encontrar en otros lenguajes.
En principio hay que entender que propósito general se refiere al uso
generalizado de un lenguaje a lo más cotidiano que se suele desarrollar.
Como es obvio, es más frecuente hacer un software para administración
de una empresa que un sistema operativo. Los lenguajes de propósito
general serán óptimos para el desarrollo general de ese software de
gestión empresarial, seguramente no tanto para ese software del sistema
operativo. PHP por ejemplo, es un fabuloso lenguaje de marcas que
facilita bastante la tarea a los desarrolladores web y sobre todo a
maquetadores que se meten en el terreno de la programación. Pero es
algo completamente desastroso para el desarrollo de aplicaciones de
scripting para administradores de sistemas.
En sí, los lenguajes más difundidos hoy en día, como C# o Java, presentan
el problema de carecer de elementos a bajo nivel integrados en sus
2
http://www.it.uu.se/edu/course/homepage/projektDV/ht05/uppsala.pdf
4
Lo que debes
saber sobre Erlang
sistemas que les permitan desarrollar aplicaciones concurrentes de
forma fácil. Esta es la razón de que en el mundo Java comience a hacerse
cada vez más visible un lenguaje como Scala.
3. Historia de Erlang
Joe Armstrong asistió a la conferencia de Erlang Factory de Londres, en
2010, donde explicó la historia de la máquina virtual de Erlang. En sí,
3
es la propia historia de Erlang/OTP. Sirviéndome de las diapositivas que
proporcionó para el evento, vamos a dar un repaso a la historia de Erlang/
OTP.
La idea de Erlang surgió por la necesidad de Ericsson de acotar un
problema que había surgido en su plataforma AXE, que estaba siendo
desarrollada en PLEX, un lenguaje propietario. Joe Armstrong junto a dos
colegas, Elshiewy y Robert Virding, desarrollaron una lógica concurrente
de programación para canales de comunicación. Esta álgebra de telefonía
permitía a través de su notación describir el sistema público de telefonía
(POTS) en tan sólo quince reglas.
A través del interés de llevar esta teoría a la práctica desarrollaron
modelos en Ada, CLU, Smalltalk y Prolog entre otros. Así descubrieron
que el álgebra telefónica se procesaba de forma muy rápida en sistemas
de alto nivel, es decir, en Prolog, con lo que comenzaron a desarrollar un
sistema determinista en él.
La conclusión a la que llegó el equipo fue que, si se puede resolver
un problema a través de una serie de ecuaciones matemáticas y
portar ese mismo esquema a un programa de forma que el esquema
funcional se respete y entienda tal y como se formuló fuera del
entorno computacional, puede ser fácil de tratar por la gente que
entiende el esquema, incluso mejorarlo y adaptarlo. Las pruebas
realmente se realizan a nivel teórico sobre el propio esquema, ya
que algorítmicamente es más fácil de probarlo con las reglas propias
de las matemáticas que computacionalmente con la cantidad de
combinaciones que pueda tener.
Prolog no era un lenguaje pensado para concurrencia, por lo que
se decidieron a realizar uno que satisfaciera todos sus requisitos,
basándose en las ventajas que habían visto de Prolog para conformar
su base. Erlang vió la luz en 1986, después de que Joe Armstrong se
encerrase a desarrollar la idea base como intérprete sobre Prolog, con
un número reducido de instrucciones que rápidamente fue creciendo
gracias a su buena acogida. Básicamente, los requisitos que se buscaban
cumplir eran:
3
http://www.erlang-factory.com/upload/presentations/247/erlang_vm_1.pdf
5
Lo que debes
saber sobre Erlang
• Los procesos debían de ser una parte intrínseca del lenguaje, no una
librería o framework de desarrollo.
• Debía poder ejecutar desde miles a millones de procesos concurrentes
y cada proceso ser independiente del resto, de modo que si alguno de
ellos se corrompiese no dañase el espacio de memoria de otro proceso.
Este requisito nos lleva a que el fallo de los procesos debe de ser
aislado del resto del programa.
• Debe poder ejecutarse de modo ininterrumpido, lo que obliga a que
para actualizar el código del sistema no se deba detener su ejecución,
sino que se recargue en caliente.
En 1989, el sistema estaba comenzando a dar sus frutos, pero surgió
el problema de que su rendimiento no era el adecuado. Se llegó a la
conclusión de que el lenguaje era adecuado para la programación que
se realizaba, pero tendría que ser, al menos unas 40 veces más rápido.
Mike Williams se encargó de escribir el emulador, cargador, planificador y
recolector de basura (en lenguaje C) mientras que Joe Armstrong escribía
el compilador, las estructuras de datos, el heap de memoria y la pila; por
su parte Robert Virding se encargaba de escribir las librerías. El sistema
desarrollado se optimizó a un nivel en el que consiguieron aumentar su
rendimiento en 120 veces de lo que lo hacía el intérprete en Prolog.
En los años 90, tras haber conseguido desarrollar productos de la
gama AXE con este lenguaje, se le potenció agregando elementos como
distribución, estructura OTP, HiPE, sintaxis de bit o compilación de
patrones para matching. Erlang comenzaba a ser una gran pieza de
software, pero tenía varios problemas para que pudiera ser adoptado de
forma amplia por la comunidad de programadores. Desafortunadamente
para el desarrollo de Erlang, aquél periodo fue también la década de Java
y Ericsson decidió centrarse en lenguajes usados globalmente por lo que
prohibió seguir desarrollando en Erlang.
Nota
HiPE es el acrónimo de High Performance Erlang (Erlang de Alto
Rendimiento) que es el nombre de un grupo de investigación
sobre Erlang formado en la Universidad de Uppsala en 1998. El
grupo desarrolló un compilador de código nativo de modo que la
máquina (BEAM) virtual de Erlang no tenga que interpretar ciertas
partes del código si ya están en lenguaje máquina mejorando así
su rendimiento.
Con el tiempo, la imposición de no escribir código en Erlang se fue
olvidando y la comunidad de programadores de Erlang comenzó a crecer
fuera de Ericsson. El equipo OTP se mantuvo desarrollando y soportando
6
Lo que debes
saber sobre Erlang
Erlang que, a su vez, continuó como sufragador del proyecto HiPE y
aplicaciones como EDoc o Dialyzer.
Antes de 2010 Erlang agregó capacidad para SMP y más recientemente
para multi-core. La revisión de 2010 del emulador de BEAM se ejecuta
con un rendimiento 300 veces superior al de la versión del emulador en
C, por lo que es 36.000 veces más rápido que el original interpretado en
Prolog. Cada vez más sectores se hacen eco de las capacidades de Erlang
y cada vez más empresas han comenzado desarrollos en esta plataforma
por lo que se augura que el uso de este lenguaje siga al alza.
4. Desarrollos con Erlang
Los desarrollos en Erlang cada vez son más visibles para todos sobre todo
en el entorno en el que Erlang se mueve: la concurrencia y la gestión
masiva de eventos o elementos sin saturarse ni caer. Esto es un punto
esencial y decisivo para empresas que tienen su nicho de negocio en
Internet y que han pasado de vender productos a proveer servicios a
través de la red.
En esta sección veremos la influencia de Erlang y cómo se va asentando
en el entorno empresarial y en las comunidades de software libre y el
tipo de implementaciones que se realizan en uno y otro ámbito.
4.1. Sector empresarial
Debido a las ventajas intrínsecas del lenguaje y su entorno se ha hecho
patente la creación de modelos reales MVC para desarrollo web. Merecen
mención elementos tan necesarios como ChicagoBoss o Nitrogen, cuyo
uso pueden verse en empresas como la española Tractis.
También es conocido el caso de Facebook que emplea Erlang en su
implementación de chat para soportar los mensajes de sus 70 millones
de usuarios. Al igual que Tuenti que también emplea esta tecnología.
4
La empresa inglesa Demonware , especializada en el desarrollo
y mantenimiento de infraestructura y aplicaciones servidoras para
videojuegos en Internet, comenzó a emplear Erlang para poder soportar
el número de jugadores de títulos tan afamados como Call of Duty.
Varias empresas del sector del entretenimiento que fabrican
aplicaciones móviles también se han sumado a desarrollar sus
aplicaciones de parte servidora en Erlang/OTP. Un ejemplo de este tipo
5
de empresas es Wooga .
4
5
http://www.erlang-factory.com/conference/London2011/speakers/MalcolmDowse
http://es.slideshare.net/wooga/erlang-the-big-switch-in-social-games
7
Lo que debes
saber sobre Erlang
WhatsApp, la aplicación actualmente más relevante para el intercambio
y envío de mensajes entre smartphones emplea a nivel de servidor
sistemas desarrollados en Erlang.
Una de las empresas estandarte de Erlang ha sido Kreditor, que cambió
6
su nombre a Klarna AB . Esta empresa se dedica al pago por Internet y
pasó en 7 años a tener 600 empleados.
En el terreno del desarrollo web comienzan a abrirse paso también
7
empresas españolas como Mikoagenda . Es un claro ejemplo de
desarrollo de aplicaciones web íntegramente desarrolladas con Erlang a
nivel de servidor.
Desde que surgió el modelo Cloud, cada vez más empresas de software
están prestando servicios online en lugar de vender productos, por lo
que se enfrentan a un uso masificado por parte de sus usuarios, e incluso
a ataques de denegación de servicio. Estos escenarios junto con servicios
bastante pesados e infraestructuras no muy potentes hacen cada vez más
necesarias herramientas como Erlang.
8
En la web de Aprendiendo Erlang mantienen un listado mixto de
software libre y empresas que emplean Erlang.
4.2. Software libre
Hay muchas muestras de proyectos de gran envergadura de muy diversa
índole creados en base a Erlang. La mayoría de ellos se centra en
entornos en los que se saca gran ventaja de la gestión de concurrencia y
distribución que realiza el sistema de Erlang.
Nota
Aprovechando que se ha comenzado a hacer esta lista de software
libre desarrollado en Erlang se ha estructurado y ampliado la
página correspondiente a Erlang en Wikipedia (en inglés de
momento y poco a poco en castellano), por lo que en estos
momentos será más extensa que la lista presente en estas
páginas.
El siguiente listado se muestra como ejemplo:
• Base de Datos Distribuidas
6
7
8
https://klarna.com/
https://mikoagenda.com/es
http://aprendiendo-erlang.blogspot.com/p/donde-se-usa-erlang.html
8
Lo que debes
saber sobre Erlang
9
• Apache CouchDB , es una base de datos documental con acceso a
datos mediante HTTP y empleando el formato REST. Es uno de los
proyectos que están acogidos en la fundación Apache.
10
• Riak , una base de datos NoSQL inspirada en Dynamo (la base
de datos NoSQL de Amazon). Es usada por empresas como Mozilla
y Comcast. Se basa en una distribución de fácil escalado y
completamente tolerante a fallos.
11
• SimpleDB , tal y como indica su propia web (en castellano) es un
almacén de datos no relacionales de alta disponibilidad flexible que
descarga el trabajo de administración de las bases de datos. Es decir,
un sistema NoSQL que permite el cambio en caliente del esquema
de datos de forma fácil que realiza auto-indexación y permite la
distribución de los datos. Fue desarrollada por Amazon.
12
• Couchbase , es una base de datos NoSQL para sistemas de
misión crítica. Con replicación, monitorización, tolerante a fallos y
compatible con Memcached.
• Servidores Web
13
• Yaws . Como servidor web completo, con posibilidad de instalarse
y configurarse para ello, sólo existe (al menos es el más conocido en
la comunidad) Yaws. Su configuración se realiza de forma bastante
similar a Apache. Tiene unos scripts que se ejecutan a nivel de
servidor bastante potentes y permite el uso de CGI y FastCGI.
• Frameworks Web
14
• ErlyWeb , no ha tenido modificaciones por parte de Yariv desde
hace unos años por lo que su uso ha decaído. El propio Yariv lo
empleó para hacer un clon de twitter y se empleó inicialmente para
la interfaz de chat para facebook.
15
• BeepBeep , es un framework inspirado en Rails y Merb aunque sin
integración con base de datos.
16
• Erlang Web , es un sistema desarrollado por Erlang Solutions que
trata igualmente las vistas y la parte del controlador pero tampoco
la parte de la base de datos.
9
http://couchdb.apache.org
http://wiki.basho.com/Riak.html
http://aws.amazon.com/es/simpledb/
12
http://www.couchbase.com/
13
http://yaws.hyber.org/
14
https://github.com/yariv/erlyweb
15
https://github.com/davebryson/beepbeep/
16
http://www.erlang-web.org/
10
11
9
Lo que debes
saber sobre Erlang
17
• Nitrogen , es un framework pensado para facilitar la construcción
de interfaces web. Nos permite agregar código HTML de una forma
simple y enlazarlo con funcionalidad de JavaScript sin necesidad de
escribir ni una sola línea de código JavaScript.
18
• ChicagoBoss , quizás el más activo y completo de los frameworks
web para Erlang a día de hoy. Tiene implementación de vistas,
plantillas (ErlyDTL), definición de rutas, controladores y modelos a
19
través de un sistema ORM .
• CMS (Content Management System)
20
21
• Zotonic , sistema CMS que permite el diseño de páginas web de
forma sencilla a través de la programación de las vistas (DTL) y la
gestión del contenido multimedia, texto y otros aspectos a través del
interfaz de administración.
• Chat
22
• ejabberd , servidor de XMPP muy utilizado en el mundo Jabber.
Este servidor permite el escalado y la gestión de multi-dominios. Es
usado en sitios como la BBC Radio LiveText, Ovi de Nokia, KDE Talk,
Chat de Facebook, Chat de Tuenti, LiveJournal Talk, etc.
• Colas de Mensajes
23
• RabbitMQ , servidor de cola de mensajes muy utilizado en sistemas
de entornos web con necesidad de este tipo de sistemas para
conexiones de tipo websocket, AJAX o similar en la que se haga
necesario un comportamiento asíncrono sobre las conexiones
síncronas. Fue adquirido por SpringSource, una filial de VMWare en
abril de 2010.
5. Erlang y la Concurrencia
Una de las mejores pruebas de que Erlang/OTP funciona, es mostrar las
comparaciones que empresas como Demonware o gente como el propio
Joe Armstrong han realizado. Sistemas sometidos a un banco de pruebas
17
http://nitrogenproject.com/
http://www.chicagoboss.org/
Object Relational Mapping, sistema empleado para realizar la transformación entre objetos y tablas
para emplear directamente los objetos en código y que la información que estos manejen se almacene
en una tabla de la base de datos.
20
http://zotonic.com/
21
Content Management System, Sistema de Administración de Contenido
22
http://www.ejabberd.im/
23
http://www.rabbitmq.com/
18
19
10
Lo que debes
saber sobre Erlang
para comprobar cómo rinden en producción real o cómo podrían rendir
en entornos de pruebas controlados.
Comenzaré por comentar el caso de la empresa Demonware, de la que
ya comenté algo en la sección de uso de Erlang en el Sector empresarial,
pero esta vez lo detallaré con datos que aportó la propia compañía a
través de Malcolm Dowse en la Erlang Factory de Londrés de 2011.
Después veremos el banco de pruebas que realizó Joe Armstrong sobre
un servicio empleando un par de configuraciones de Apache y Yaws.
5.1. El caso de Demonware
En la conferencia de Erlang Factory de Londrés, en 2011, Malcolm Dowse,
de la empresa Demoware (de Dublín), dictó una ponencia titulada Erlang
and First-Person Shooters (Erlang y los Juegos en Primera Persona).
Decenas de millones de fans de Call of Duty Black Ops testearon la carga
de Erlang.
Demonware es la empresa que trabaja con Activision y Blizzard dando
soporte de los servidores de juegos multi-jugador XBox y PlayStation.
La empresa se constituyó en 2003 y desde esa época hasta 2007 se
mantuvieron modificando su tecnología para optimizar sus servidores,
hasta llegar a Erlang.
En 2005 construyeron su infraestructura en C++ y MySQL.
Su concurrencia de usuarios no superaba los 80 jugadores,
afortunadamente no se vieron en la situación de superar esa cifra.
Además, el código se colgaba con frecuencia, lo que suponía un grave
problema.
En 2006 se reescribió toda la lógica de negocio en Python. Se seguía
manteniendo a nivel interno C++ con lo que el código se había hecho
difícil de mantener.
Finalmente, en 2007, se reescribió el código de los servidores de C++ con
Erlang. Fueron unos 4 meses de desarrollo con el que consiguieron que
el sistema ya no se colgase, que se mejorase y facilitase la configuración
del sistema (en la versión C++ era necesario reiniciar para reconfigurar,
lo que implicaba desconectar a todos los jugadores). También se dotó
de mejores herramientas de log y administración y se hacía más fácil
desarrollar nuevas características en muchas menos líneas de código.
Para entonces habían llegado a los 20 mil usuarios concurrentes.
A finales de 2007 llegó Call of Duty 4, que supuso un crecimiento
constante de usuarios durante 5 meses continuados. Se pasó de 20 mil
a 2,5 millones de usuarios. De 500 a 50 mil peticiones por segundo. La
empresa tuvo que ampliar su nodo de 50 a 1850 servidores en varios
11
Lo que debes
saber sobre Erlang
centros de datos. En palabras de Malcolm: fue una crisis para la compañía,
teníamos que crecer, sin el cambio a Erlang la crisis podría haber sido un
desastre.
Demonware es una de las empresas que ha visto las ventajas de Erlang.
La forma en la que implementa la programación concurrente y la gran
capacidad de escalabilidad. Gracias a estos factores, han podido estar a la
altura de prestar el servicio de los juegos en línea más usados y jugados
de los últimos tiempos.
5.2. Yaws contra Apache
24
Es bastante conocido ya el famoso gráfico sobre la comparativa que
realizaron Joe Armstrong y Ali Ghodsi entre Apache y Yaws. La prueba es
bastante fácil, de un lado, un servidor, de otro, un cliente para medición
y 14 clientes para generar carga.
La prueba propuesta era generar un ataque de denegación de servicio
(DoS), que hiciera que los servidores web, al recibir un número de
peticiones excesivo, fuesen degradando su servicio hasta dejar de darlo.
Es bien conocido que este hecho pasa con todos los sistemas, ya que los
recursos de un servidor son finitos. No obstante, por su programación,
pueden pasar cosas como las que se visualizan en el gráfico:
En gris oscuro (marcando el punto con un círculo y ocupando las líneas
superiores del gráfico) puede verse la respuesta de Yaws en escala de
24
http://www.sics.se/~joe/apachevsyaws.html
12
Lo que debes
saber sobre Erlang
KB/s (eje Y) frente a carga (eje X). Las líneas que se cortan a partir de las 4
mil peticiones corresponden a dos configuraciones diferentes de Apache
(en negro y gris claro).
En este caso, pasa algo parecido a lo visto con Demonware en la sección
anterior, Apache no puede procesar más de 4000 peticiones simultáneas,
en parte debido a su integración íntimamente ligada al sistema operativo,
que le limita. Sin embargo, Yaws se mantiene con el mismo rendimiento
hasta llegar a superar las 80 mil peticiones simultáneas.
Erlang está construido con gestión de procesos propia y desligada del
sistema operativo. En sí, suele ser más lenta que la que proporciona el
sistema operativo, pero sin duda la escalabilidad y el rendimiento que se
consigue pueden paliar ese hecho. Cada nodo de Erlang puede manejar
en total unos 2 millones de procesos.
13
Capítulo 2. El lenguaje
Sólo hay dos tipos de lenguajes: aquellos de los que
la gente se queja y aquellos que nadie usa.
—Bjarne Stroustrup
Erlang tiene una sintaxis muy particular. Hay gente a la que termina
gustándole y otras personas que lo consideran incómodo. Hay que
entender que es un lenguaje basado en Prolog y con tintes de Lisp por lo
que se asemeja más a los lenguajes funcionales que a los imperativos.
La mayoría de personas comienzan programando en lenguajes como
Basic, Modula-2 o Pascal, que tienen una sintaxis muy parecida entre
ellos. Lo mismo pasa con la rama de C/C++, Java y Perl o PHP, que tienen
una sintaxis, el uso de los bloques condicionales, iterativos y declaración
de funciones y clases también semejantes.
En los lenguajes imperativos la sintaxis se basa en la consecución de
mandatos que el programador envía a través del código a la máquina. En
Erlang y demás lenguajes funcionales, la sintaxis está diseñada como si
se tratara de la definición de una función matemática o una proposición
lógica. Cada elemento dentro de la función tiene un propósito: obtener
un valor; el conjunto de todos esos valores, con o sin procesamiento,
conforma el resultado. Un ejemplo básico:
area(Base, Altura) -> Base * Altura.
En este ejemplo puede verse la definición de la función area. Los
parámetros requeridos para obtener su resultado son Base y Altura. A
la declaración de parámetros le sigue el símbolo de consecución (->),
como si se tratase de una proposición lógica. Por último está la operación
interna que retorna el resultado que se quiere obtener.
Al tratarse de funciones matemáticas o proposiciones lógicas no existe
una correlación entre imperativo y funcional. Para un código imperativo
común como el que sigue:
para i <- 1 hasta 10 hacer
si clavar(i) = 'si' entonces
martillea_clavo(i)
fsi
fpara
No existe en Erlang un equivalente que pueda transcribir una acción
imperativa como tal. Para desarrollar en Erlang hay que pensar en el qué
se quiere hacer más que en el cómo. Si en un lenguaje funcional lo que se
quiere es clavar los clavos que seleccione la función clavar martilleando,
se podría hacer a través de una lista de comprensión:
14
El lenguaje
[ martillea_clavo(X) || X <- Clavos, clavar(i) =:= 'si' ].
Hay que entender que para resolver problemas de forma funcional
muchas veces la mentalidad imperativa es un obstáculo. Tenemos que
pensar en los datos que tenemos y qué datos queremos obtener como
resultado. Es lo que nos conducirá a la solución.
Erlang es un lenguaje de formato libre. Se pueden insertar tantos
espacios y saltos de línea entre símbolos como se quiera. Esta función
area es completamente equivalente a la anterior a nivel de ejecución:
area(
Base,
Altura
) ->
Base * Altura
.
A lo largo de este capítulo revisaremos la base del lenguaje Erlang.
Veremos lo necesario para poder escribir programas básicos de propósito
general y entender esta breve introducción de una forma más detallada
y clara.
1. Tipos de Datos
En Erlang se manejan varios tipos de datos. Por hacer una distinción
rápida podemos decir que se distinguen entre: simples y complejos;
otras organizaciones podrían conducirnos a pensar en los datos como:
escalares y conjuntos o atómicos y compuestos. No obstante, la forma
de organizarlos no es relevante con el fin de conocerlos, identificarlos
y usarlos correctamente. Emplearemos la denominación simples y
complejos (o compuestos), pudiendo referirnos a cualquiera de las otras
formas de categorización si la explicación resulta más clara.
Como datos simples veremos en esta sección los átomos y los números.
Como datos de tipo complejo veremos las listas y tuplas. También
veremos las listas binarias, un tipo de dato bastante potente de Erlang
y los registros, un tipo de dato derivado de las tuplas.
1.1. Átomos
Los átomos son identificadores de tipo carácter que se emplean como
palabras clave y ayudan a semantizar el código.
Un átomo es una palabra que comienza por una letra en minúscula y va
seguido de letras en mayúscula o minúscula, números y/o subrayados.
También se pueden emplear letras en mayúscula al inicio, espacios y lo
15
El lenguaje
que queramos, siempre y cuando encerremos la expresión entre comillas
simples. Algunos ejemplos:
> is_atom(cuadrado).
true
> is_atom(a4).
true
> is_atom(alta_cliente).
true
> is_atom(bajaCliente).
true
> is_atom(alerta_112).
true
> is_atom(false).
true
> is_atom('HOLA').
true
> is_atom('
eh??? ').
true
Los átomos tienen como única finalidad ayudar al programador a
identificar estructuras, algoritmos y código específico.
Hay átomos que se emplean con mucha frecuencia como son: true, false
y undefined.
Los átomos junto con los números enteros y reales y las cadenas de texto
componen lo que se conoce en otros lenguajes como literales. Son los
datos que tienen un significado de por sí, y se pueden asignar a una
variable directamente.
Nota
Como literales se pueden especificar números, pero también
valores de representaciones de la tabla de caracteres. Al igual
que en otros lenguajes, Erlang permite dar el valor de un carácter
específico a través el uso de la sintaxis: $A, $1, $!. Esto retornará el
valor numérico para el símbolo indicado tras el símbolo del dólar
en la tabla de caracteres.
1.2. Números Enteros y Reales
En Erlang, los números pueden ser de dos tipos, tal y como se ve en este
ejemplo de código en la consola:
> is_float(5).
false
> is_float(5.0).
true
> is_integer(5.0).
false
> is_integer(5).
16
El lenguaje
true
Otra de las cosas que sorprende de Erlang es su precisión numérica.
Si multiplicamos números muy altos veremos como el resultado sigue
mostrándose en notación real, sin usar la notación científica que
muestran otros lenguajes cuando una operación supera el límite de
cálculo de los números enteros (o valores erróneos por overflow):
> 102410241024 * 102410241024 * 1234567890.
12947972063153419287126752624640
Esta característica hace de Erlang una plataforma muy precisa y adecuada
para cálculos de intereses bancarios, tarificación telefónica, índices
bursátiles, valores estadísticos, posición de puntos tridimensionales, etc.
Nota
Los números se pueden indicar también anteponiendo la base
en la que queremos expresarlos y usando como separador la
almohadilla (#). Por ejemplo, si queremos expresar los números
en base octal, lo haremos anteponiendo la base al número que
queremos representar 8#124. Análogamente 2#1011 representa
un número binario y 16#f42a un número hexadecimal.
1.3. Variables
Las variables, como en matemáticas, son símbolos a los que se enlaza un
valor y sólo uno a lo largo de toda la ejecución del algoritmo específico.
Esto quiere decir que cada variable durante su tiempo de vida sólo puede
contener un valor.
El formato de las variables se inicia con una letra mayúscula, seguida de
tantas letras, números y subrayados como se necesiten o deseen. Una
variable puede tener esta forma:
> Pi = 3.1415.
3.1415
> Telefono = "666555444".
"666555444"
> Depuracion = true.
true
Sobre las variables se pueden efectuar expresiones aritméticas, en
caso de que contenga números, operaciones de listas o emplearse
como parámetro en llamadas a funciones. Un ejemplo de variables
conteniendo números:
> Base = 2.
2
17
El lenguaje
> Altura = 5.2.
5.2
> Base * Altura.
10.4
Si en un momento dado, queremos que Base tenga el valor 3 en lugar del
valor 2 inicialmente asignado veríamos lo siguiente:
> Base = 2.
2
> Base = 3.
** exception error: no match of right hand side value 3
Lo que está ocurriendo es que Base ya está enlazado al valor 2 y que
la concordancia (o match) con el valor 2 es correcto, mientras que si lo
intentamos encajar con el valor 3 resulta en una excepción.
Nota
Para nuestras pruebas, a nivel de consola y para no tener que salir
y entrar cada vez que queramos que Erlang olvide el valor con el
que se enlazó una variable, podemos emplear:
> f(Base).
ok
> Base = 3.
3
Para eliminar todas las variables que tenga memorizadas la
consola se puede emplear: f().
La ventaja de la asignación única es la facilidad de analizar código
aunque muchas veces no se considere así. Si una variable durante toda
la ejecución de una función sólo puede contener un determinado valor
1
el comportamiento de dicha función es muy fácilmente verificable .
1.4. Listas
Las listas en Erlang son vectores de información heterogénea, es decir,
pueden contener información de distintos tipos, ya sean números,
átomos, tuplas u otras listas.
Las listas son una de las potencias de Erlang y otros lenguajes
funcionales. Al igual que en Lisp, Erlang maneja las listas como lenguaje
de alto nivel, en modo declarativo, permitiendo cosas como las listas
de comprensión o la agregación y eliminación de elementos específicos
como si de conjuntos se tratase.
1
Muestra de ello es dialyzer, una buena herramienta para comprobar el código escrito en Erlang.
18
El lenguaje
1.4.1. ¿Qué podemos hacer con una lista?
Una lista de elementos se puede definir de forma directa tal y como se
presenta a continuación:
> [ 1, 2, 3, 4, 5 ].
[1,2,3,4,5]
> [ 1, "Hola", 5.0, hola ].
[1,"Hola",5.0,hola]
A estas listas se les pueden agregar o sustraer elementos con los
operadores especiales ++ y --. Tal y como se presenta en los siguientes
ejemplos:
> [1,2,3] ++ [4].
[1,2,3,4].
> [1,2,3] -- [2].
[1,3]
Otro de los usos comunes de las listas es la forma en la que se puede ir
tomando elementos de la cabecera de la lista dejando el resto en otra
sublista. Esto se realiza con esta sencilla sintaxis:
> [H|T] = [1,2,3,4].
[1,2,3,4]
> H.
1
> T.
[2,3,4]
> [H1,H2|T2] = [1,2,3,4].
[1,2,3,4]
> H1.
1
> H2.
2
> T2.
[3,4]
De esta forma tan sencilla la implementación de los conocidos
algoritmos de push y pop de inserción y extracción en pilas resultan tan
triviales como:
> Lista = [].
[]
> Lista2 = [1|Lista].
[1]
> Lista3 = [2|Lista2].
[2,1]
> [Extrae|Lista2] = Lista3.
[2,1]
> Extrae.
2
> Lista2.
19
El lenguaje
[1]
No obstante, el no poder mantener una única variable para la pila
dificulta su uso. Este asunto lo analizaremos más adelante con el
tratamiento de los procesos y las funciones.
1.4.2. Cadenas de Texto
Las cadenas de texto son un tipo específico de lista. Se trata de una
lista homogénea de elementos representables como caracteres. Erlang
detecta que si una lista en su totalidad cumple con esta premisa, es una
cadena de caracteres.
Por tanto, la representación de la palabra Hola en forma de lista, se puede
hacer como lista de enteros que representan a cada una de las letras o
como el texto encerrado entre comillas dobles ("). Una demostración:
> "Hola" = [72,111,108,97].
"Hola"
Como puede apreciarse, la asignación no da ningún error ya que ambos
valores, a izquierda y derecha, son el mismo para Erlang.
Importante
Esta forma de tratar las cadenas es muy similar a la que se emplea
en lenguaje C, en donde el tipo de dato char es un dato de 8
bits en el que se puede almacenar un valor de 0 a 255 y que
las funciones de impresión tomarán como representaciones de
la tabla de caracteres en uso por el sistema. En Erlang, la única
diferencia es que cada dato no es de 8 bits sino que es un
entero lo que conlleva un mayor consumo de memoria pero mejor
soporte de nuevas tablas como la de UTF-16 o las extensiones del
UTF-8 y similares.
Al igual que con el resto de listas, las cadenas de caracteres soportan
también la agregación de elementos, de modo que la concatenación se
podría realizar de la siguiente forma:
> "Hola, " ++ "mundo!".
"Hola, mundo!"
Una de las ventajas de la asignación propia de que dispone Erlang es
que si encuentra una variable que no ha sido enlazada a ningún valor,
automáticamente cobra el valor necesario para que la ecuación sea
cierta. Erlang intenta hacer siempre que los elementos a ambos lados del
signo de asignación sean iguales. Un ejemplo:
20
El lenguaje
> "Hola, " ++ A = "Hola, mundo!".
"Hola, mundo!"
> A.
"mundo!"
Esta notación tiene sus limitaciones, en concreto la variable no asignada
debe estar al final de la expresión, ya que de otra forma el código para
realizar el encaje sería mucho más complejo.
1.4.3. Listas binarias
Las cadenas de caracteres se forman por conjuntos de enteros, es
decir, se consume el doble de memoria para una cadena de caracteres
almacenada en una lista en Erlang que en cualquier otro lenguaje. Las
listas binarias permiten almacenar cadenas de caracteres con tamaño de
byte y permite realizar trabajos específicos con secuencias de bytes o
incluso a nivel de bit.
La sintaxis de este tipo de listas es como sigue:
> <<"Hola">>.
<<"Hola">>
> <<72,111,$l,$a>>.
<<"Hola">>
La lista binaria no tiene las mismas funcionalidades que las listas vistas
anteriormente. No se pueden agregar elementos ni emplear el formato
de anexión y supresión de elementos tal y como se había visto antes.
Pero se puede hacer de otra forma más potente.
Por ejemplo, la forma en la que tomábamos la cabeza de la lista en una
variable y el resto lo dejábamos en otra variable, se puede simular de la
siguiente forma:
> <<H:1/binary,T/binary>> = <<"Hola">>.
<<"Hola">>
> H.
<<"H">>
> T.
<<"ola">>
La concatenación en el caso de las listas binarias no se realiza como
con las listas normales empleando el operador ++. En este caso debe
realizarse de la siguiente forma:
> A = <<"Hola ">>.
<<"Hola ">>
> B = <<"mundo!">>.
<<"mundo!">>
> C = <<A/binary, B/binary>>.
21
El lenguaje
<<"Hola mundo!">>
Para obtener el tamaño de la lista binaria empleamos la función
byte_size/1. En el caso anterior para cada una de las variables
empleadas:
> byte_size(A).
5
> byte_size(B).
6
> byte_size(C).
11
Esta sintaxis es un poco más elaborada que la de las listas, pero se debe
a que nos adentramos en la verdadera potencia que tienen las listas
binarias: el manejo de bits.
1.4.4. Trabajando con Bits
En la sección anterior vimos la sintaxis básica para simular el
comportamiento de la cadena al tomar la cabeza de una pila. Esta sintaxis
se basa en el siguiente formato: Var:Tamaño/Tipo; siendo opcionales
Tamaño y Tipo.
El tamaño está ligado al tipo, ya que una unidad de medida no es nada sin
su cuantizador. En este caso, el cuantizador (o tipo) que hemos elegido
es binary. Este tipo indica que la variable será de tipo lista binaria, con lo
que el tamaño será referente a cuántos elementos de la lista contendrá
la variable.
En caso de que el tamaño no se indique, se asume que es tanto como
el tipo soporte y/o hasta encajar el valor al que debe de igualarse (si es
posible), por ello en el ejemplo anterior la variable T se queda con el
resto de la lista binaria.
Los tipos también tienen una forma compleja de formarse, ya que se
pueden indicar varios elementos para completar la definición de los
mismos. Estos elementos son, en orden de especificación: Endian-SignoTipo-Unidad; vamos a ver los posibles valores para cada uno de ellos:
• Endian: es la forma en la que los bits son leídos en la máquina, si es en
formato Intel o Motorola, es decir, little o big respectivamente. Además
de estos dos, es posible elegir native, que empleará el formato nativo
de la máquina en la que se esté ejecutando el código. El valor por
defecto se prefija big.
> <<1215261793:32/big>>.
<<"Hola">>
> <<1215261793:32/little>>.
22
El lenguaje
<<"aloH">>
> <<1215261793:32/native>>.
<<"Hola">>
En este ejemplo se ve que la máquina de la prueba es de tipo big u
ordenación Intel.
• Signo: se indica si el número indicado se almacenará en formato con
signo o sin él, es decir, signed o unsigned, respectivamente.
• Tipo: es el tipo con el que se almacena el dato en memoria. Según el
tipo el tamaño es relevante para indicar precisión o número de bits,
por ejemplo. Los tipos disponibles son: integer, float y binary.
• Unidad: este es el valor de la unidad, por el que multiplicará el tamaño.
En caso de enteros y coma flotante el valor por defecto es 1, y en
caso de binario es 8. Por lo tanto: Tamaño x Unidad = Número de bits;
por ejemplo, si la unidad es 8 y el tamaño es 2, los bits que ocupa el
elemento son 16 bits.
Si quisiéramos almacenar tres datos de color rojo, verde y azul en 16
bits, tomando para cada uno de ellos 5, 5 y 6 bits respectivamente,
tendríamos que la partición de los bits se podría hacer de forma algo
dificultosa. Con este manejo de bits, componer la cadena de 16 bits (2
bytes) correspondiente, por ejemplo, a los valores 20, 0 y 6, sería así:
> <<20:5, 0:5, 60:6>>.
<<" <">>
Nota
Para obtener el tamaño de la lista binaria en bits podemos
emplear la función bit_size/1 que nos retornará el tamaño de
la lista binaria:
> bit_size(<<"Hola mundo!").
88
1.5. Tuplas
Las tuplas son tipos de datos organizativos en Erlang. Se pueden crear
listas de tuplas para conformar conjuntos de datos homogéneos de
elementos individuales heterogéneos.
Las tuplas, a diferencia de las listas, no pueden incrementar ni
decrementar su tamaño salvo por la redefinición completa de su
estructura. Se emplean para agrupar datos con un propósito específico.
23
El lenguaje
Por ejemplo, imagina que tenemos un directorio con unos cuantos
ficheros. Queremos almacenar esta información para poder tratarla y
sabemos que va a ser: ruta, nombre, tamaño y fecha de creación.
Esta información se podría almacenar en forma de tupla de la siguiente
forma:
{ "/home/yo", "texto.txt", 120, {{2011, 11, 20}, {0, 0, 0}} }.
Las llaves indican el inicio y fin de la definición de la tupla, y los
elementos separados por comas conforman su contenido.
Nota
En el ejemplo se puede ver que la fecha y hora se ha introducido
de una forma un tanto peculiar. En Erlang, las funciones de los
módulos de su librería estándar, trabajan con este formato, y si se
emplea, es más fácil tratar y trabajar con fechas. Por ejemplo, si
ejecutásemos:
> {date(), time()}.
{{2011,12,6},{22,5,17}}
Este tipo de dato también se emplea para emular los arrays asociativos
(o hash). Estos arrays almacenan información de forma que sea posible
rescatarla mediante el texto o identificador específico que se usó para
almacenarla. Se usa en aquellos casos en que es más fácil que acceder
al elemento por un identificador conocido que por un índice que podría
ser desconocido.
1.5.1. Listas de Propiedades
Una lista de propiedades es una lista de tuplas clave, valor. Se gestiona
mediante la librería proplists. Las listas de propiedades son muy usadas
para almacenar configuraciones o en general cualquier información
variable que se requiera almacenar.
Supongamos que tenemos la siguiente muestra de datos:
> A = [{path, "/"}, {debug, true}, {days, 7}].
Ahora supongamos que de esta lista, que se ha cargado desde algún
fichero o mediante cualquier otro método, queremos consultar si
debemos de realizar o no la depuración del sistema, es decir, mostrar
mensajes de log si la propiedad debug es igual a true:
> proplists:get_value(debug, A).
24
El lenguaje
true
Como es muy posible que no se sepan las claves que existen en un
determinado momento dentro de la lista existen las funciones is_defined,
o get_keys para poder obtener una lista de claves de la lista.
Un ejemplo de posible uso como tabla hash sería:
> Meses = [
{enero, 31}, {febrero, 28}, {marzo, 31},
{abril, 30}, {mayo, 31}, {junio, 30},
{julio, 31}, {agosto, 31}, {septiembre, 30},
{octubre, 31}, {noviembre, 30}, {diciembre, 31}
].
> proplists:get_value(enero, Meses).
31
> proplists:get_value(junio, Meses).
30
El empleo de las listas de propiedades de esta forma nos facilita el acceso
a los datos que sabemos que existen dentro de una colección (o lista) y
extraer únicamente los que queramos obtener.
Nota
El módulo de proplists contiene muchas más funciones útiles para
tratar este tipo de colección de datos de forma fácil. No es mala
idea dar un repaso al mismo para ver el partido que podemos
sacarle en nuestros programas.
1.6. Registros
Los registros son un tipo específico de tupla que facilita el acceso a los
datos individuales dentro de la misma mediante un nombre y una sintaxis
de acceso mucho más cómoda para el programador. Internamente para
Erlang, los registros realmente no existen. A nivel de preprocesador son
intercambiados por tuplas. Esto quiere decir que los registros en sí son
una simplificación a nivel de uso de las tuplas.
Como los registros se emplean a nivel de preprocesador, en la consola
sólo podemos definir registros empleando un comando específico de
consola. Además, podemos cargar los registros existentes en un fichero
y emplearlos desde la propia consola para definir datos o para emplear
los comandos propios de manejo de datos con registros.
La definición de registros desde la consola se realiza de la siguiente
forma:
> rd(agenda, {nombre, apellidos, telefono}).
25
El lenguaje
Para declarar un registro desde un archivo el formato es el siguiente:
-record(agenda, {nombre, apellidos, telefono}).
Nota
Los ficheros de código de Erlang normalmente tiene la extensión
erl, sin embargo, cuando se trata de códigos de tipo cabecera,
estos ficheros mantienen una extensión a medio camino entre
los de cabecera de C (que tienen la extensión .h) y los de código
normales de Erlang. Su extensión es: hrl. En estos ficheros se
introducirán normalmente definiciones y registros.
Veamos con una pequeña prueba que si creamos una tupla A Erlang
la reconoce como tupla de cuatro elementos. Si cargamos después el
archivo registros.hrl cuyo contenido es la definición del registro
agenda el tratamiento de la tupla se modifica automáticamente y
ya podemos emplear la notación para registros de los ejemplos
subsiguientes:
> A = {agenda, "Manuel", "Rubio", 666666666}.
{agenda,"Manuel","Rubio",666666666}
> rr("registros.hrl").
[agenda]
> A.
#agenda{nombre = "Manuel",apellidos = "Rubio",
telefono = 666666666}
Erlang reconoce como primer dato de la tupla el nombre del registro
y como cuenta con el mismo número de elementos, si no tenemos en
cuenta el identificador, la considera automáticamente como un registro.
También se pueden seguir empleando las funciones y elementos típicos
de la tupla ya que a todos los efectos sigue siéndolo.
Nota
Para obtener la posición dentro de la tupla de un campo, basta
con escribirlo de la siguiente forma:
#agenda.nombre
Esto nos retornará la posición relativa definida como nombre con
respecto a la tupla que contiene el registro de tipo agenda.
Para tratar los datos de un registro, podemos realizar cualquiera de las
siguientes acciones:
> A#agenda.nombre.
26
El lenguaje
"Manuel"
> A#agenda.telefono.
666666666
> A#agenda{telefono=911232323}.
#agenda{nombre = "Manuel",apellidos = "Rubio",
telefono = 911232323}
> #agenda{nombre="Juan Antonio",apellidos="Rubio"}.
#agenda{nombre = "Juan Antonio",apellidos = "Rubio",
telefono = undefined}
Recordemos siempre que la asignación sigue siendo única.
Para acceder al contenido de un dato de un campo del registro,
accederemos indicando que es un registro (dato#registro, A#agenda en
el ejemplo) y después agregaremos un punto y el nombre del campo al
que queremos acceder.
Para modificar los datos de un registro existente en lugar del punto
emplearemos las llaves. Dentro de las llaves estableceremos tantas
igualdades clave=valor como necesitemos (separadas por comas), tal y
como se ve en el ejemplo anterior.
Para obtener en un momento dado información sobre los registros,
podemos emplear la función record_info. Esta función tiene dos
parámetros, el primero es un átomo que puede contener fields si
queremos que retorne una lista de átomos con el nombre de cada campo;
o size, para retornar el número de campos que tiene la tupla donde
se almacena el registro (incluído el identificativo, en nuestros ejemplos
agenda).
Importante
Como se ha dicho anteriormente, los registros son entidades
que trabajan a nivel de lenguaje pero Erlang no los contempla
en tiempo de ejecución. Esto quiere decir que el preprocesador
trabaja para convertir cada instrucción concerniente a registros
para que sean relativas a tuplas y por tanto la función record_info
no se puede emplear con variables. Algo como lo siguiente:
> A = agenda, record_info(fields, A).
Nos retornará illegal record info.
Como los registros son internamente tuplas cada campo puede contener
a su vez cualquier otro tipo de dato, no sólo átomos, cadenas de texto
o números, sino también otros registros, tuplas o listas. Con ello, esta
estructura nos propone un sistema organizativo interesante para poder
acceder directamente al dato que necesitemos en un momento dado
facilitando la labor del programador enormemente.
27
El lenguaje
2. Imprimiendo por pantalla
Muchas veces se nos presentará la necesidad de mostrar datos por
pantalla. De momento, toda la información que vemos es porque la
consola nos la muestra, como resultado de salida del código que vamos
escribiendo. No obstante, hay momentos, en los que será necesario
realizar una salida concreta de un dato con información más completa.
Para ello tenemos el módulo io, del que emplearemos de momento sólo
la función format. Esta función nos permite imprimir por pantalla la
información que queramos mostrar basado en un formato específico que
se pasa como primer parámetro.
Nota
Para los que hayan programado con lenguajes tipo C, Java, PHP, ...
esta función es equivalente y muy parecida a printf, es decir, la
función se basa en una cadena de texto con un formato específico
(agregando parámetros) que serán sustituidos por los valores que
se indiquen en los parámetros siguientes.
Por ejemplo, si quieres mostrar una cadena de texto por pantalla,
podemos escribir lo siguiente:
> io:format("Hola mundo!").
Hola mundo!ok
Esto sale así porque el retorno de la función es ok, por lo que se imprime
la cadena de texto y seguidamente el retorno de la función (el retorno de
función se imprime siempre en consola). Para hacer un retorno de carro,
debemos de insertar un caracter especial. A diferencia de otros lenguajes
donde se usan los caracteres especiales, Erlang no usa la barra invertida,
sino que emplea la virgulilla (~), y tras este símbolo, los caracteres se
interpretan de forma especial. Tenemos:
~
Imprime el símbolo de la virgulilla.
c
Representa un carácter que será reemplazado por el valor
correspondiente pasado en la lista como segundo parámetro. Antes
de la letra c se pueden agregar un par de números separados
por un punto. El primer número indica el tamaño del campo y
la justificación a izquierda o derecha según el signo positivo o
negativo del número. El segundo número indica las veces que se
repetirá el caracter. Por ejemplo:
28
El lenguaje
> io:format("[~c,~5c,~5.3c,~-5.3c]~n", [$a,$b,$c,$d]).
[a,bbbbb, ccc,ddd ]
ok
e/f/g
Se encargan de presentar números en coma flotante. El formato de
e es científico (X.Ye+Z) mientras que f lo presenta en formato con
coma fija. El formato g es una mezcla ya que presenta el formato
científico si el número se sale del rango [0.1,10000.0], y en caso
contrario presenta el formato como si fuese e. Los números que se
pueden anteponer a cada letra indican, el tamaño que se quiere
representar y justificación (como se vió antes). Tras el punto la
precisión. Unos ejemplos:
> io:format("[~7.2e,~7.2f,~7.4g]", [10.1,10.1,10.1]).
[ 1.0e+1, 10.10, 10.10]ok
> Args = [10000.67, 10123.23, 1220.32],
> io:format("~11.7e | ~11.3f | ~11.7g ", Args).
1.000067e+4 |
10123.230 |
1220.320 ok
s
Imprime una cadena de caracteres. Similar a c, pero el significado
del segundo número en este caso es la cantidad de caracteres de la
lista que se mostrará. Veamos algunos ejemplos:
> Hola = "Hola mundo!",
> io:format("[~s,~-7s,~-7.5s]", [Hola, Hola, Hola]).
[Hola mundo!,Hola mu,Hola
]ok
w/W
Imprime cualquier dato con su sintaxis estandar. Se usa sobretodo
para poder imprimir tuplas, pero imprime igualmente listas,
números, átomos, etc. La única salvedad, es que una cadena
de caracteres será considerada como una lista. Los números de
anteposición se emplean de la misma forma que en s. Un ejemplo:
> Data = [{hola,mundo},10,"hola",mundo],
> io:format("[~w,~w,~w,~w]~n", Data).
[{hola,mundo},10,[104,111,108,97],mundo]
ok
La versión de W es similar a la anterior aunque toma dos parámetros
de la lista de parámetros. El primero es el dato que se va a imprimir,
el segundo es la profundidad. Si imprimimos una lista con muchos
elementos, podemos mostrar únicamente un número determinado
de ellos. A partir de ese número agrega puntos suspensivos. Un
ejemplo:
29
El lenguaje
> io:format("[~W]", [[1,2,3,4,5],3]).
[[1,2|...]]ok
p/P
Es igual que w, pero intenta detectar si una lista es una cadena de
caracteres para imprimirla como tal. Si la impresión es demasiado
grande, la parte en varias líneas. La versión en mayúscula, también
es igual a su homónimo W, aceptando un parámetro extra para
profundidad.
b/B/x/X/+/#
Imprimen números según la base indicada. Los números anteriores
a cada letra (o símbolo) indican, el primero la magnitud y
justificación de la representación y el segundo la base en la que se
expresará el número. La diferencia entre ellos es que B imprime sólo
la representación numérica.
Con X se puede emplear un prefijo que se toma del siguiente
parámetro que haya en la lista de parámetros, consecutivo al valor
a representar.
El símobolo de almohadilla (#) siempre antepone la base
en formato Erlang: 10#20 (decimal), 8#65 (octal), 16#1A
(hexadecimal). La diferencia entre las mayúsculas y minúsculas
es precisamente esa, la representación de las letras de las bases
mayores a 10 en mayúsculas o minúsculas. Un ejemplo:
> io:format("[~.2b,~.16x,~.16#]", [21,21,"0x",21]).
[10101,0x15,16#15]ok
i
Ignora el parámetro que toque emplear. Es útil si el formato de
los parámetros que se pasa es siempre el mismo y en un formato
específico se desea ignorar uno concreto.
n
Retorno de carro, hace un salto de línea, de modo que se pueda
separar por líneas diferentes lo que se desee imprimir por pantalla.
Nota
Existe también el módulo io_lib que dispone también de la
función format. La única diferencia que presenta, es que en lugar
de presentar por pantalla la cadena resultante, la retorna como
cadena de caracteres.
30
El lenguaje
3. Fechas y Horas
El manejo de fechas y horas en Erlang no se realiza con un tipo estándar,
sino que se establece como un término encerrado en una tupla. Una
fecha tiene la siguiente forma de tupla:
{2012,5,22}
Es una tupla compuesta por tres campos enteros destinados al año, mes
y día, en ese orden. La función interna date/0 retorna este formato, pero
hay más funciones de tratamiento de fecha que emplean este formato.
El tiempo también se maneja en una tupla de tres elementos en la que se
pueden diferenciar en este orden: hora, minutos y segundos. Un ejemplo
sería el siguiente:
{22,10,5}
Una fecha y hora completa se representa a través de otra tupla que
contiene en su interior las tuplas mencionadas antes, separadas en dos
elementos diferenciados, es decir, un formato como el siguiente:
> erlang:localtime().
{{2012,5,22},{22,10,5}}
Para obtener la fecha y hora en la zona horaria local podemos emplear
también estas otras funciones dentro de una tupla de dos elementos:
{date(), time()}
Hay otras funciones como now/0, que retornan la fecha y hora actuales
2
en formato POSIX , en una tupla {MegaSeconds, Seconds, MicroSeconds},
lo que quiere decir que el cálculo de la hora en un sólo entero sería así:
> {M,S,_} = now(), M*1000000+S.
1337717405
Por último, indicar que las fechas también pueden ser convertidas
o empleadas en formato UTC (o GMT). Podemos convertir una
fecha a formato UTC (erlang:localtime_to_universaltime/1) o
viceversa (erlang:universaltime_to_localtime/1).
2
El formato de POSIX para fecha y hora consiste en un número entero que corresponde al número de
segundos transcurrido desde el 1 de enero de 1970 hasta la fecha que se indique.
31
El lenguaje
Nota
El módulo calendar provee una serie de funciones que
permiten averiguar si el año introducido es bisiesto
(is_leap_year/1), el día de la semana de una fecha concreta
(iso_week_number/0 e iso_week_number/1), el último día
del mes (last_day_of_the_month/2) y más aún.
Este módulo, además, tiene la capacidad de trabajar con segundos
gregorianos en lugar de POSIX. El número obtenido en segundos
3
(para representación interna) es contado desde el año cero , en
lugar de 1970. Esto da la posibilidad de dar fechas anteriores a
1970.
3
La toma de segundos siempre es en formato UTC (o GMT), por lo que las fechas que se proporcionen para
la conversión a segundos, serán tomadas como en hora local y convertidas a UTC antes de su conversión
a segundos.
32
Capítulo 3. Expresiones,
Estructuras y Excepciones
La mejor forma de predecir el futuro es
implementarlo.
—David Heinemeier Hansson
En este capítulo ampliamos lo visto en el capítulo anterior con el
conocimiento de las expresiones lógicas, las expresiones aritméticas, las
estructuras de control y el manejo de las excepciones.
1. Expresiones
Las expresiones son la conjunción de símbolos con datos para conformar
una sentencia válida para el lenguaje con significado para el compilador,
de modo que pueda ofrecer, en tiempo de ejecución, una representación
a nivel de código máquina del resultado que se pretende obtener.
Las expresiones pueden ser de tipo aritmético o lógico. Las aritméticas
buscan un valor a través de operaciones matemáticas simples o
complejas. De un conjunto de datos dados con las operaciones indicadas
y el orden representado por la expresión se obtiene un resultado. En las
lógicas se busca una conclusión lógica (o binaria) a la conjunción de los
predicados expuestos.
1.1. Expresiones Aritméticas
Con los números, de forma nativa, se pueden llevar a cabo expresiones
aritméticas. Las más básicas, como la suma, resta, multiplicación y
división son de sobra conocidas. Otras operaciones como la división
entera o el remanente (o módulo) se implementan en cada lenguaje de
una forma distinta, por lo que haremos un repaso rápido con un breve
ejemplo:
> 2 + 2.
4
> 2 - 2.
0
> 2 * 3.
6
> 10 / 3.
3.3333333333333335
> 10 div 3.
3
> 10 rem 3.
1
33
Expresiones, Estructuras
y Excepciones
Se puede hacer uso de los paréntesis para establecer una relación de
precedencia de operadores para, por ejemplo, anteponer una suma a una
multiplicación. También se pueden realizar operaciones encadenadas,
por ejemplo multiplicando más de dos operandos. Ejemplos de todo
esto:
> 2 * 3 + 1.
7
> 2 * (3 + 1).
8
> 3 * 3 * 3.
27
1.2. Expresiones Lógicas
Vamos a ver los operadores que se emplean en el álgebra de Boole band
(binary and), bor (binary or) y bxor (binary exclusive or). Estos operadores
tratan los números como binarios y operan con el valor de cada una de
sus posiciones (ceros o unos). Un ejemplo:
>
3
>
2
>
2
>
7
>
2
1 bxor 2.
1 bxor 3.
3 band 6.
2#011 bor 2#100.
(bnot 2#101) band 2#11.
Estas herramientas nos facilitan operar de forma binaria con los números.
También podemos encontrarnos con que queremos almacenar el
resultado, o emplear el valor lógico de una serie de comparaciones. Para
ello ya no operamos de forma binaria, sino que obtenemos resultados
binarios únicos como true o false. Podríamos hacer:
> C1 = 2 > 1.
true
> C2 = 1 > 2.
false
> C1 and C2.
false
> C1 or C2.
true
> C3 = 3 =:= (1 + 2).
true
> C1 and (C2 or C3).
true
Podemos construir todas las expresiones lógicas que queramos de
modo que a nivel de comparación podamos obtener un resultado
34
Expresiones, Estructuras
y Excepciones
lógico (verdadero o falso). En la siguiente sección se mencionan todos
los operadores de comparación que se pueden emplear para realizar
comparaciones entre cadenas, números, tuplas, listas y/o registros.
Nota
Además de los operadores and y or, en Erlang existen otros como
andalso y orelse. El resultado a nivel de cálculo es el mismo. Lo
único que varía es que los primeros realizan una comprobación
absoluta de los valores pasados, evaluando y comparando todos
los valores, mientras que los presentados recientemente, realizan
una comprobación vaga.
Esto quiere decir que se evalúa la primera parte de la expresión
y, en caso de andalso (por ejemplo), si es falsa, ya se sabe que
el resultado general será falso, por lo que no se comprueba la
segunda parte, retornando inmediatamente el valor false. Son
útiles si la comprobación se debe hacer consultado una función
que tiene un coste de comprobación asociado, ya que muchas
veces es mejor ahorrarse esas ejecuciones. Lo mismo se aplica a
una comprobación que pueda fallar por lo que necesitamos otra
anterior que descarta la segunda. Por ejemplo:
is_list(List) andalso length(List)
Si List no fuese una lista, la ejecución de length/1 fallaría. Al
emplear andalso esto no sucede, ya que sólo se comprueba la
primera parte, y al obtener false finaliza las comprobaciones.
1.3. Precedencia de Operadores
El orden de los operadores para Erlang de más prioritario a menos
prioritario es el siguiente:
Operador
Descripción
:
Ejecución de funciones
#
Resolución de registros
+ - bnot not
Unitarios
/ * div rem band and
División, Multiplicación e Y lógico.
+ - bor bxor bsl bsr or xor
Suma, resta y O inclusivo y
exclusivo.
++ --
Agrega/Sustrae de conjuntos/
listas.
== /= =< < >= > =:= =/=
Comparaciones
35
Expresiones, Estructuras
y Excepciones
Operador
Descripción
andalso
Y lógico con comprobación vaga
orelse
O lógico con comprobación vaga
=!
Asignación y Paso de mensaje
catch
Captura de errores
2. Estructuras de Control
A diferencia de los lenguajes imperativos en Erlang sólo hay dos
estructuras de control: if y case; aunque se puedan parecer a las
estructuras que existen en otros lenguajes, difieren.
Estas estructuras se basan en la concordancia de sus expresiones. Ambas
tienen que realizar una concordancia positiva con una expresión y
ejecutar un código que retorne un valor.
Como el que encajen los valores es tan importante para estas estructuras,
y para la mayoría de estructuras en ejecución dentro de la programación
de Erlang, en general, dedicaremos una parte a estudiar lo que
llamaremos a partir de ahora como concordancia y seguidamente
veremos las estructuras donde se aplica.
2.1. Concordancia
En este apartado revisaremos un aspecto bastante importante en lo que
respecta a la programación en Erlang y que conviene tener interiorizado,
lo que facilitará mucho la programación en este lenguaje. Me refiero a la
concordancia (en inglés match). Podríamos definir esta expresión como la
cualidad de una estructura de datos de asemejarse a otra, incluso aunque
haya que aplicar asignación para ello.
Si tenemos un conjunto de datos, por ejemplo una lista, podemos hacer
un simple concordancia haciendo:
[1,2,3] = [1,2,3]
Si realizamos esta asignación, veremos que nos da como resultado
[1,2,3], es decir, se acepta que el valor de la izquierda es igual al de la
derecha (como en matemáticas: es un aserto válido).
Ahora bien, si tenemos el dato de la derecha que lo desconocemos, como
habíamos visto en la listas, podemos hacer:
[A,B,C] = [1,2,3]
36
Expresiones, Estructuras
y Excepciones
Esto nos dará como resultado la asociación a A, B y C de los valores 1, 2 y
3, respectivamente, por lo que retornará como en el caso anterior, [1,2,3].
En la sección de listas comentamos más formas de hacer concordancia a
través de la agregación de conjunto (++) o con la lista en formato cabezacola ([H|T]). Con respecto a las tuplas, esto no es aplicable, ya que la tupla
tiene valores fijos, pero podemos ignorar los que no nos interesen de la
siguiente forma:
{A,_,C} = {1,2,3}
Con el símbolo de subrayado (o guión bajo "_"), le decimos al sistema
que en ese espacio debe de haber un dato (del tipo que sea: lista, tupla,
átomo, número o registro), pero que no nos interesa.
2.2. Estructura case
La primera estructura de control que vamos a tratar, probablemente la
más usada, es case. Esta estructura toma un valor inicial como referencia
y busca entre las opciones que se especifican la primera que concuerde
para ejecutar su bloque funcional y retornar el valor que establezca la
elección.
Como dijimos en un principio, la denominación de funcional, implica que
cada acción, estructura y función debe retornar un valor. Las estructuras
de control como case no son una excepción.
Veamos un ejemplo:
> Impuesto = case irpf of
irpf -> 0.25;
iva -> 0.18;
_ -> 0
end.
0.25
En este ejemplo podemos ver cómo, si la estructura que se indica en
case casa con cualquiera que se suceda en las subsiguientes líneas, se
ejecuta un bloque concreto, retornando el resultado de la ejecución de
dicho bloque (en este ejemplo sólo un valor). Si no se encontrase ningún
valor que casara, la estructura no podría retornar nada y daría un error.
Es aconsejable acabar con un subrayado (_) que casa con todo y tomarlo
como valor por defecto, a menos que se quiera expresamente que falle
en caso de que no se contenga un valor apropiado.
Podemos ver otro ejemplo más complejo como el siguiente:
> Resultado = case Fecha of
37
Expresiones, Estructuras
y Excepciones
{D,M,A} ->
integer_to_list(A) ++ "-" ++
integer_to_list(M) ++ "-" ++
integer_to_list(D);
<<Dia:2/binary,"/",Mes:2/binary,"/",Agno:4/binary>> ->
binary_to_list(Agno) ++ "-" ++
binary_to_list(Mes) ++ "-" ++
binary_to_list(Dia);
_ ->
""
end.
Si la variable Fecha la igualamos al retorno de la función date() el sistema
entenderá que casa con el primer bloque, ya que es una tupla de 3
elementos, convertirá cada dato y lo concatenará con los guiones para
retornarlo en modo texto con formato A-M-D. Si lo que enviamos es un
texto en una lista binaria separado por barras inclinadas (/), tomará cada
parte y lo representará análogamente. En caso de no casar con ninguno
de los anteriores, retorna una cadena vacía.
La estructura case puede agregar condicionales a cada opción para la
1
concordancia. Esto es lo que se conoce como guardas . Estas expresiones
se pueden agregar empleando conexiones como: andalso o "," y orelse o
";". Estas guardas se agregan tras cada opción con la palabra clave when,
tal y como se ve en el siguiente ejemplo:
> Resultado = case Fecha of
{D,M,A} when is_integer(D),
is_integer(M), is_integer(A) ->
integer_to_list(A) ++ "-" ++
integer_to_list(M) ++ "-" ++
integer_to_list(D);
<<Dia:2/binary,"/",Mes:2/binary,"/",Agno:4/binary>>
when is_binary(Fecha) ->
binary_to_list(Agno) ++ "-" ++
binary_to_list(Mes) ++ "-" ++
binary_to_list(Dia);
_ ->
""
end.
Con esto nos aseguramos de que los valores que se parsearán dentro de
cada bloque son del tipo que se esperan, y que algo como una tupla que
contenga listas de caracteres no haga fallar el primer bloque de opción.
Para las guardas se pueden emplear tanto "," como and, o andalso, en
caso de que se quiera el comportamiento del y lógico; o ";", or o orelse,
para conseguir el comportamiento del o inclusivo lógico.
La diferencia existente entre las tres formas es que el agregado also o
else hace que sea una comprobación vaga pudiendo finalizar antes de
1
Esta expresión inglesa se ha traducido en sitios como aprendiendo erlang como guardas.
38
Expresiones, Estructuras
y Excepciones
evaluar todos los predicados. Los signos de puntuación se comportan de
la misma forma en este caso.
La diferencia entre los signos "," y ";" con andalso y orelse es que
los signos capturan excepciones. Es decir mediante el uso de los
signos de puntuación se ignorarán los fallos que puedan suceder en la
evaluación, continuando con la evaluación de lo siguiente. Para aclarar
mejor las diferencias veamos tres ejemplos de código similares pero que
funcionan de forma bastante diferente:
> case a of
>
_ when (a+1)=:=a or b=:=b -> ok;
>
_ -> fail
> end.
* 1: syntax error before: '=:='
> case a of
>
_ when (a+1)=:=a orelse b=:=b -> ok;
>
_ -> fail
> end.
fail
> case a of
>
_ when (a+1)=:=a ; b=:=b -> ok;
>
_ -> fail
> end.
ok
El uso de or nos da un error de código directamente, ya que estamos
sumando 1 a un átomo llamado a y eso da bad argument in arithmetic
expression. Mediante el uso de orelse no nos da error, pero ignora toda
esa comprobación por ser errónea, pasando a comprobar el siguiente
bloque y devolviendo fail. Por último, con el signo ";", en lugar de tomar
ese resultado como no válido e invalidar toda la comprobación como el
caso anterior, sólo da como inválida la primera parte y pasa a comprobar
el siguiente predicado, considerando que la primera parte retorna false.
2.3. Estructura if
Otra de las estructuras que se puede emplear con Erlang es if. Esta
estructura guarda cierta similitud con las que se emplean en los
lenguajes imperativos, salvo porque debe existir una opción de código
que sea ejecutable en caso de que la cláusula previa se cumpla; además
y en todo caso que se debe retornar siempre un valor.
Si nos fijamos bien esta estructura podría tomarse como una
simplificación de la estructura case anterior. La única diferencia radica
en la eliminación de los bloques de concordancia. Es decir, sólo emplea
las guardas.
Por ejemplo, la siguiente estructura if devuelve el caso1 si el día de hoy
está entre los valores 1 y 10, y si es sobre 11 y 20, caso2. En caso de
39
Expresiones, Estructuras
y Excepciones
ejecutarse la función mostrada con los valores mayores o iguales a 21
daría un error:
> {A,M,D} = date().
{2012,4,25}
> Caso = if
>
(D >= 1) and (D =< 10) -> caso1;
>
(D >= 11) and (D =< 20) -> caso2
> end.
** exception error: no true branch found when evaluating an if
expression
Este error es debido a que esta estructura, al igual que el resto de
estructuras existentes en Erlang, debe de retornar un valor y en caso de
no poder ejecutar ningún bloque de código para resolver la función o
valor que debe devolver, origina el fallo.
Importante
En otros lenguajes, el operador de mayor que (>) y menor que
(<) se sitúa siempre antes del signo igual, mientras que, como se
vio en la tabla de precedencia de operadores, según si es uno u
otro, se coloca de modo que apunte siempre hacia el símbolo de
igualdad.
En la misma tabla de precedencia de operadores se puede ver que
and y or tienen más prioridad que las comparaciones, por lo que,
en caso de que se usen éstos y no el punto y coma (;) o la coma
(,) u orelse o andalso, es necesario encerrar la comparación entre
paréntesis.
Para que el sistema no nos falle cuando introduzcamos fechas a partir
del día 21, vamos a definir una acción por defecto:
> Caso = if
>
D >= 1 andalso D =< 10 -> caso1;
>
D >= 11 andalso D =< 20 -> caso2;
>
true -> unknown
> end.
A diferencia de la estructura case, el valor de comodín no se hace sobre
una variable que pueda contener cualquier valor (como en el caso de
subrayado, por ejemplo), sino se emplea la palabra reservada true por
tratarse de predicados lógicos.
2.4. Listas de Comprensión
Una de las ventajas de la programación funcional es sin duda su caracter
declarativo. El hecho de poder tener una estructura como las listas de
comprensión, nos puede ayudar a extraer información sin problemas,
40
Expresiones, Estructuras
y Excepciones
indicando: de donde procede esta información, cuál queremos que sea su
formato de salida y las condiciones que debe de cumplir nos proporciona
dicha información al instante.
Por ejemplo, si queremos sacar de una lista sólo los números pares, sería
tan sencillo como:
> [ X || X <- [1,2,3,4,5], X rem 2 =:= 0 ].
[2,4]
Si expresamos esto mismo en lenguaje natural sería algo así como:
[ Dame X || Donde X es un elemento de la lista <- [1,2,3,4,5], tal que la
condición X rem 2 =:= 0 se cumpla.
Las listas de comprensión tienen tres partes que se enmarcan dentro de
los corchetes. La primera es la proyección de los elementos, es decir,
indica la forma en la que se presentarán los datos o en la que queremos
que se configure la salida de la ejecución de la lista de comprensión.
La segunda es la selección de los datos. Esta parte está separada de la
primera por dos pipes (||) y tiene una flecha de derecha a izquierda que
indica a la derecha el origen de los datos y a la izquierda el patrón o forma
de los datos.
La tercera parte, separada por una coma de la anterior, son las
condiciones de la selección. Las condiciones que debe de cumplir cada
elemento de la lista, para ser seleccionado. En el caso del ejemplo se
indicó que el valor de X debía de ser par (que su remanente fuese cero
en una división por dos).
Nota
Las listas de comprensión son uno de los elementos más
importantes del lenguaje, por lo que conviene que se tenga
muy presente su forma, la utilidad que tienen con respecto a la
selección y proyección de información y realizar pruebas hasta
comprender su funcionamiento completa y correctamente.
Un truco bastante útil que yo empleo es compararlo con una
sentencia SELECT de SQL, ya que tiene la parte de la proyección
(inmediatamente después de SELECT), la parte de la selección (la
parte del FROM) y las condiciones de la selección para cada tupla
(la parte del WHERE).
Un ejemplo más completo, teniendo listas de listas, pero siendo una
matriz fija de 2xN, por ejemplo, podemos realizar la siguiente selección:
> A = [[1,1],[2,2],[3,3],[4,4],[5,5],[6,6]].
[[1,1],[2,2],[3,3],[4,4],[5,5],[6,6]]
41
Expresiones, Estructuras
y Excepciones
> [ X || [Y, X] <- A, Y rem 2 =:= 0, X >= 4 ].
[4,6]
La lista resultado nos muestra, dentro de una sublista de dos elementos
a los que asociamos como (Y,X), el hecho de que el elemento Y deba de
ser par y el elemento X mayor o igual a 4. Por lo que, en esta definición,
concuerdan los números 4 y 6.
3. Excepciones
Erlang es tolerante a fallos. Esto le viene dado por el empleo de procesos
en lugar de hilos. Si un proceso muere y deja su estado de memoria
corrupto no afectará a otros procesos, ya que ni siquiera comparten
memoria (cada proceso tiene la suya propia y es otra de las propiedades
de Erlang el nada compartido o share nothing en inglés), ni la ejecución
de uno está condicionada o afecta a otros procesos.
El tema de los procesos lo veremos en el siguiente capítulo de forma
más extensa. Ahora vamos a centrarnos en las excepciones, porque,
¿qué suecede cuando un proceso encuentra un fallo o una situación
inesperada por el programador? Normalmente se dispara una excepción
que hace que el proceso muera.
En el siguiente capítulo veremos que eso en muchos casos es asumible e
incluso deseable. Pero también hay casos en los que, si el código maneja
recursos que hay que tratar de llevar a una situación segura antes de que
suceda lo inevitable, es preferible intentar de realizar algún tratamiento
para esa excepción.
3.1. Recoger excepciones: catch
El primer tipo de instrucción que se introdujo en Erlang para la captura
de errores y excepciones es catch. Este comando se puede anteponer
a la ejecución de una función o de cualquier instrucción. Si se genera un
error, catch permite transformarlo en un dato recibido por la función
o instrucción que se hubiese ejecutado. Veamos un pequeño ejemplo
desde la consola de Erlang:
> 1 = a.
** exception error: no match of right hand side value a
> catch 1 = a.
{'EXIT',{{badmatch,a},[{erl_eval,expr,3}]}}
La ejecución de la primera expresión nos lleva a una excepción
que propocaría la finalización de ejecución del proceso, mientras
que anteponiendo catch a la misma expresión, Erlang convierte esa
excepción en un tipo de dato que se podría procesar a través de una
estructura de control.
42
Expresiones, Estructuras
y Excepciones
Un ejemplo del uso de catch con case:
> case catch 1 = a of
>
true -> caso1;
>
false -> caso2;
>
{'EXIT',Error} -> casoError
> end.
En este caso, el sistema no produce un error, sino que retorna el
casoError, que debe de ser manejado por el código que toma el retorno
de esta instrucción.
Importante
En este caso es una mala idea haber capturado la excepción ya
que tapa un error de código que hemos provocado y que, gracias
a catch, hace que consideremos el código como correcto, cuando
no es así.
3.2. Lanzar una excepción
Hay veces que, en lugar de capturar una excepción conviene provocarla.
Esto se puede hacer de muchas maneras. Podemos emplear asertos
(afirmaciones que se toman como axioma) para que generen una
excepción en ese punto. Por ejemplo:
> 2+3=5.
5
Si empleamos variables para almacenar los valores, y cometemos un
error:
> A=2, B=3, 5=A+A.
** exception error: no match of right hand side value 4
Como el código es erróneo y 5 no es igual a 4, el sistema se detiene
en ese punto. Esto nos garantiza que, si el código es crítico y no debe
de contener errores, en unas pruebas podría aparecer el error y ser
solucionado.
Además de esta técnica, podemos lanzar excepciones con mensajes
de error concretos, por si quisiéramos a otro nivel capturarlos para
procesarlos. Estos se lanzarían a través de throw. Podemos verlo más
claro a través de un ejemplo:
> throw({fallo, "Esto ha fallado"}).
** exception throw: {fallo,"Esto ha fallado"}
43
Expresiones, Estructuras
y Excepciones
En caso de que quisiéramos capturarlo con catch, el sistema trata este
lanzamiento de excepción como un error real provocado por el usuario,
por lo que se podría capturar como cualquier otro error provocado por
el sistema.
3.3. La estructura try...catch
try...catch es una nueva forma de tratar los errores, más clara y potente
que catch. Este bloque se presenta como los que existen en los lenguajes
imperativos. La parte try da cabida a ejecución de código que será
observado por la estructura y en caso del lanzamiento de cualquier
excepción, ya sea por fallo, throw o porque se haya ordenado al proceso
acabar su ejecución, todo esto se puede atrapar en el catch.
Un ejemplo de esta estructura:
> try
>
a = 1
> catch
>
throw:Term -> Term;
>
exit:Razon -> Razon;
>
error:Razon -> Razon
> end.
{badmatch,1}
En la parte de catch se declaran tres partes diferenciadas. Estas se
detallan con su clase, que puede ser cualquiera de las tres: throw, exit
o error. A continuación y después de los dos puntos (:) está la variable
que contendrá el mensaje en sí del error para poder emplearlo dentro
del bloque de código de recuperación.
Esta sentencia presenta también una zona en la que poder ejecutar
acciones que se lleven a cabo tanto si el código falla como si no. Esta
sección recibe el nombre de after, y es un bloque de código que se agrega
tras catch. Por ejemplo, si queremos imprimir por pantalla un saludo falle
o no el código:
> try
>
a=1
> catch
>
error:Error -> Error
> after
>
io:format("Adios~n")
> end.
Adios
{badmatch,1}
El código se ejecuta de modo que, como after está dentro de la estructura,
hasta que esa sección no termina (en este caso imprimir Adios por
pantalla) la estructura no retorna el valor correspondiente a su ejecución
(la excepción a través de la rama error:Error).
44
Expresiones, Estructuras
y Excepciones
Nota
Podríamos profundizar más en estas estructuras, pero lo dejo
en este punto porque me gusta más la filosofía de Erlang: let
it crash (deja que falle); que indica que el sistema debe de
poder fallar para volver a iniciar su ejecución de forma normal,
ya que mantenerse en ejecución tras un fallo podría provocar
una situación imprevista que, además, se prolongase, con lo que
dificultaría aún más la detección del fallo.
3.4. Errores de ejecución más comunes
En esta sección daremos un repaso a los errores de ejecución más
comunes que suelen surgir en Erlang cuando programamos de forma que
el lector pueda corregirlos rápidamente.
function_clause
Cuando se llama a una función con parámetros incorrectos, ya sea
en número, concordancia o por guardas, se dispara esta excepción:
> io:format("hola", [], []).
** exception error: no function clause matching...
case_clause
Prácticamente igual la anterior. Este se dispara cuando no hay
concordancia con ningún bloque (y sus guardas, en caso de que
tuviese), dentro de la cláusula case.
> case hola of adios -> "" end.
** exception error: no case clause matching hola
if_clause
Al igual que el resto de *_clause, este error se dispara cuando no
hay ninguna guarda del if aplicable. El sistema indicará que no hay
rama true disponible, ya que es una práctica habitual el disponer
de la misma.
> if false -> "" end.
** exception error: no true branch found when eval...
badmatch
Suelen suceder cuando falla la concordancia (matching), ya sea al
intentar asignar una estructura de datos sobre otra que no tiene la
misma forma o cuando se intenta hacer una asignación sobre una
variable que ya tiene un valor.
45
Expresiones, Estructuras
y Excepciones
> A=1, A=2.
** exception error: no match of right hand side value 2
badarg
Se suele disparar cuando llamamos a una función con argumentos
erróneos. A diferencia de las ya vistas esta excepción es introducida
como una validación de argumentos por el programador fuera de las
guardas, por lo que para emplearla, debemos de crear un bloque en
nuestras funciones de validación de argumentos que, en caso de no
ser correctos, la lancen. Un ejemplo de función que dispone de esto:
> io:format({hola}).
** exception error: bad argument
undef
Lanzada cuando se llama a una función que no está definida (no
existe), ya sea por su número de parámetros o por su nombre dentro
del módulo:
> lists:no_existe().
** exception error: undefined function lists:no_existe/0
badarith
Esta excepción es para errores matemáticos (aritméticos). Sucede
cuando se intenta realizar una operación con valores incorrectos
(como una suma de un número con una lista) o divisiones por cero.
Un ejemplo:
> 27 / 0.
** exception error: bad argument in an arithmetic expr...
badfun
Sucede cuando se intenta emplear una variable que no contiene
una función. Un ejemplo:
> A = hola, A(12).
** exception error: bad function hola
badarity
Es un caso específico de badfun, en este caso el error es debido a
que a la función que contiene la variable, se le pasa un número de
argumentos que no puede manejar, porque son más o menos de los
que soporta. Un ejemplo:
> A = fun(_,_) -> ok end, A(uno).
46
Expresiones, Estructuras
y Excepciones
** exception error: interpreted function with arity 2 ...
system_limit
Se alcanzó el límite del sistema. Esto puede pasar cuando:
tenemos demasiados procesos limitados por el parámetro de
procesos máximos (se puede ampliar), o demasiados argumentos
en una función, átomos demasiado grandes o demasiados átomos,
demasiados nodos conectados, etc. Para una mejor optimización
del sistema y entendimiento del mismo podemos leer la Guía de
2
Eficiencia de Erlang (en inglés) .
Importante
Hay que tener especial cuidado con los errores de system_limit.
Son lo suficientemente graves como para parar todo el sistema (la
máquina virtual de Erlang al completo).
Si capturamos estos errores, se presentarán de la forma:
{Error, Reason}
Donde Error puede tomar cualquiera de los valores indicados
anteriormente (bararg, function_clause, cause_clause, ...) y Reason tendrá
una descripción de las funciones que fueron llamadas, para llegar a ese
punto.
Nota
A partir de la versión de Erlang R15, en Reason se puede ver
además el nombre del fichero y número de línea en el se realizó
la llamada, lo cual facilita la detección de errores.
2
http://www.erlang.org/doc/efficiency_guide/advanced.html
47
Capítulo 4. Las funciones y
módulos
Divide y vencerás.
—Refrán popular
Hasta el momento hemos estado ejecutando el código desde la consola.
Todas las pruebas y códigos de ejemplo vistos se han escrito pensando
en que serán ejecutados desde la consola de la máquina virtual de Erlang.
Normalmente la programación en Erlang no se produce en la consola,
sino que se realiza a través de la escritura de módulos en los que hay
funciones.
Las funciones se podrían tratar como otras estructuras de control (como
case o if), ya que disponen de elementos similares aunque son elementos
de definición. No se ejecutan en el momento como las estructuras de
control, sino que la ejecución se realiza mediante una llamada a la
función.
En esta sección revisaremos los conceptos de módulo, función, el
polimorfismo y otros aspectos más avanzados de funciones y módulos
que permite Erlang.
1. Organización del código
El código en Erlang se organiza en módulos y dentro de cada módulo
puedes encontrar funciones. Anteriormente ya hemos visto algunos
de estos módulos, como el caso de proplists, por ejemplo, en el que
empleábamos el uso de funciones como get_value.
Un módulo se define en un fichero a través de unas instrucciones
de preprocesador iniciales que nos permiten definir el nombre y
las funciones que queremos exportar (para emplear desde fuera del
módulo).
El código podría ser como sigue:
-module(mi_modulo).
-export([mi_funcion/0]).
mi_funcion() ->
"Hola mundo!".
El módulo del código anterior llamado mi_modulo debe guardarse en un
fichero con el nombre mi_modulo.erl. El módulo exporta, o pone a
48
Las funciones y módulos
disposición de otros módulos y de la consola la posibilidad de usar la
función mi_funcion, cuya aridad (o número de parámetros) es cero.
Para simplificar el tema de la exportación en la codificación de nuestros
primeros módulos hasta que nos acostumbremos a ella, podemos obviar
el hecho de que habrá funciones privadas para el módulo y dejarlas todas
abiertas. Esto se haría escribiendo esta cabecera, en lugar de la anterior:
-module(mi_modulo).
-compile([export_all]).
Esta directiva le dice al compilador que exporte todas las funciones de
modo que no haya que nombrarlas una a una en la sentencia export.
Nota
Una vez tengamos el fichero creado, compilarlo es tan sencillo
como ir a una consola del sistema operativo y ejecutar:
erlc mi_modulo.erl
Esto genera un fichero mi_modulo.beam que será el que
empleará la máquina virtual para acceder a las funciones creadas.
También es posible compilar un módulo en la consola de Erlang,
en nuestro ejemplo, escribiendo:
> c(mi_modulo)
Lo cual compilará el código creando el fichero mencionado
anteriormente, dejándolo disponible para su uso.
Desde la consola de la máquina virtual podemos ejecutar:
> mi_modulo:mi_funcion().
"Hola mundo!"
La máquina virtual de Erlang busca el fichero beam en su ruta de módulos
por defecto y luego en el directorio actual. Si lo encuentra, lo carga y
busca la función dentro del mismo. En caso de que no encontrase la
función retornaría un fallo.
49
Las funciones y módulos
Importante
A diferencia de otros lenguajes donde los paquetes, módulos
o librerías se pueden encontrar de modo jerárquico, Erlang
establece el nombre de sus módulos de forma plana. Esto quiere
decir que si existe un módulo llamado mi_modulo e intentamos
cargar otro módulo con el mismo nombre, se emplearía el que
tuviese la fecha de compilación más reciente.
Hay que tener cuidado con el nombre de los módulos. Por
ejemplo, si se creara un módulo vacío de nombre erlang y se
intentara cargar el sistema completo se detendría, ya que se
intentarían emplear las funciones del propio sistema Erlang,
esenciales para su funcionamiento, y no estarían presentes en
este nuevo módulo de fecha más reciente.
2. Ámbito de las funciones
Cuando creamos un módulo podemos importar y exportar funciones
dentro o hacia fuera de él. El módulo encapsula un conjunto de funciones
que pueden ser accesibles por otros módulos si se especifica su
exportación.
La declaración export es una lista que puede contener tantas referencias
de funciones como se deseen publicar, e incluso pueden existir varias
declaraciones diferentes de export dentro de un mismo módulo.
La declaración import, al igual que la anterior, contiene una lista de
funciones como segundo parámetro, que se importan desde el módulo
que se detalla como primer parámetro. Puede haber tantas declaraciones
como se necesiten dentro de un módulo y cada declaración es sólo para
la importación desde un módulo.
Por ejemplo, tenemos el código de este módulo:
-module(traductor).
-export([get/1]).
-import(proplists, [get_value/2]).
data() ->
[{"hi", "hola"}, {"bye", "adios"}].
get(Key) ->
get_value(Key, data()).
En este caso y desde el punto de vista de la exportación, estamos dando
exclusivamente acceso a la función get con un parámetro, tanto a otros
módulos que importasen traductor como a la consola. Desde el punto de
vista de la importación, tenemos disponible la función get_value del
50
Las funciones y módulos
módulo proplists de modo que no tengamos que llamarla de forma
1
fully qualified .
Nota
La importación es una técnica que puede hacer confuso el
código escrito. Se recomienda no emplearla a menos que el uso
masificado de la función en cuestión sea más beneficioso para la
lectura del código que invocarla de manera fully qualified.
3. Polimorfismo y Concordancia
Una de las particularidades de las funciones de Erlang, es que disponen
de polimorfismo. Si tuviésemos que programar una función que tuviese
algunos de sus parámetros con valores por defecto, podríamos emplear
el polimorfismo tal y como se da en muchos otros lenguajes imperativos,
definiendo dos funciones con distinto número de parámetros, de la
siguiente forma:
multiplica(X, Y) ->
X * Y.
multiplica(X, Y, Z) ->
X * Y * Z.
En este caso, vemos que si la función es llamada con dos parámetros, se
ejecutaría la primera forma, ya que casa con el número de parámetros, y
en cambio, si pasamos tres parámetros, se ejecutaría la segunda forma.
En Erlang sin embargo este concepto se puede completar agregando la
característica de la simple asignación y la concordancia, de modo que
nos permite hacer algo como lo siguiente:
area(cuadrado, Base) ->
Base * Base;
area(circulo, Radio) ->
math:pi() * Radio * Radio.
area(rectangulo, Base, Altura) ->
Base * Altura;
area(triangulo, Base, Altura) ->
Base * Altura / 2.
Cada función anterior nos retorna un área, dependiendo del número
de argumentos pero además del contenido del primer parámetro.
Gracias a ello, podemos tener funciones con el mismo número de
parámetros y diferente comportamiento. Como se puede observar, el
1
Fully Qualified, deriviado de su uso en los nombres DNS como FQDN, reseña la llamada a una función
empleando toda la ruta completa para poder localizarlo, es decir, empleando también el módulo.
51
Las funciones y módulos
primer parámetro puede contener los valores: cuadrado, rectangulo,
triangulo o circulo (sin acentuar, ya que son átomos). En caso de
recibir, por ejemplo cubo, el sistema lanzaría una excepción al no poder
satisfacer la ejecución solicitada.
Importante
Cuando se emplea el polimorfismo, es decir la declaración de
un mismo nombre de función para igual número de parámetros
pero diferente contenido, se debe de separar la definición de una
función de la siguiente a través del punto y coma (;), mientras
que la última definición debe de llevar el punto final. Esto es
así para que los bloques de funciones polimórficas de este tipo
estén siempre agrupados, conformando una única estructura más
legible.
4. Guardas
Anteriormente ya vimos las guardas en las estructuras de control case
e if. Como la estructura de función es tan similar a las estructuras de
control, también contempla el uso de guardas, lo que le permite realizar
un polimorfismo todavía más completo.
Por ejemplo, si queremos, del ejemplo anterior del cálculo de áreas,
asegurarnos de que los datos de entrada son numéricos, podríamos
reescribir el código anterior de la siguiente forma:
area(cuadrado, Base) when is_number(Base) ->
Base * Base;
area(circulo, Radio) when is_number(Radio) ->
math:pi() * Radio * Radio.
area(rectangulo, Base, Altura)
when is_number(Base), is_number(Altura) ->
Base * Altura;
area(triangulo, Base, Altura)
when is_number(Base), is_number(Altura) ->
Base * Altura / 2.
Con esto agregamos un nivel más de validación, asegurándonos de que
las entradas de las variables sean numéricas o en caso contrario que no
se ejecutaría esa función. Podríamos agregar en las condiciones que la
Base sea mayor de 0, al igual que la Altura y Radio, y cualesquiera otras
comprobaciones más que se nos puedieran ocurrir.
5. Clausuras
Si revisamos un momento la teoría lo que ahora vamos a ver podría
encajar perfectamente como clausura, lambda o función anónima. En
principio, las definiciones:
52
Las funciones y módulos
Se llama clausura (en inglés clousure) a una función junto a un entorno
referenciado de variables no locales. Esto quiere decir que la función
tiene acceso a las variables del entorno en el que es definida como
si fuesen globales. Por ejemplo, si definimos una función calculadora
dentro de otra función llamada factoria, si en esta última función hay
definida una variable llamada contador, esta variable será accesible
también por calculadora.
Por otro lado, tenemos el cálculo lambda, inventado por Alonzo Church
y Stephen Kleen en 1930, que en un entorno matemático define lo
que es una función para abstraer las ecuaciones en un lenguaje más
simplificado (Peter Landin se encargó de llevar esta teoría a Algol 60). El
caso es que la teoría de funciones, subprogramas y subrutinas se basa
en esta teoría, pero el nombre lambda, en lenguajes imperativos ha sido
otorgado a funciones anónimas.
Por último, las funciones anónimas no son más que funciones que no
se declaran con un nombre sino que son declaradas y almacenadas en
una variable, de modo que la variable es empleada para hacer llamadas a
otras funciones, pudiendo ser pasada como parámetro o retornada como
resultado, ya que en sí, es tratada como un dato.
Las clausuras de Erlang se basan en todas estas premisas. Son funciones
que, al definirse, pueden tomar el valor de las variables del entorno en el
que son definidas (ya que las variables son de simple asignación y toman
su valor en ese momento), que cumplen con la adaptación del cálculo
lambda de Church y Kleen y son anónimas puesto que su definición
es como una instanciación que se almacena en una variable y puede
ser enviada como parámetro, retornada como valor y además de esto,
empleada como una función.
Se pueden escribir estas clausuras de la siguiente forma:
> A = 2. % dato de entorno
> F = fun(X) -> X * A end.
#Fun<erl_eval.6.111823515>
> F(5).
10
En este ejemplo a la variable F se le asigna la definición de la clausura,
introduciendo dentro de su contexto el uso de una variable del entorno
en el que está siendo definida, en este caso la variable A. De este
modo al ejecutar la función F, multiplica la variable que se le pasa como
parámetro por la que tiene contenida.
Podemos hacer también que una función normal, o incluso una anónima,
nos retorne una función específica que haga una acción concreta según
los datos con los que haya sido llamada la primera:
53
Las funciones y módulos
-module(clausura).
-compile([export_all]).
multiplicador(X) when is_integer(X) ->
fun(Y) -> X * Y end.
Emplearíamos este código desde la consola de la siguiente forma:
> Dos = clausura:multiplicador(2), Dos(3).
6
> F = fun(X) when is_integer(X) ->
>
fun(Y) -> X * Y end
> end.
#Fun<erl_eval.6.111823515>
> MDos = F(2).
#Fun<erl_eval.6.111823515>
> MDos(3).
6
Como se puede apreciar, no sólo se permite generar una clausura dentro
de otra, sino que la generación de las clausuras puede tener también
guardas. Si quisiéramos agregar una clausura más al código, para truncar
el valor de un número en coma flotante en caso de que llegase como X,
podríamos hacer lo siguiente:
> F = fun(X) when is_integer(X) ->
fun(Y) -> X * Y end;
(X) when is_float(X) ->
fun(Y) -> trunc(X) * Y end
end.
Así conseguiremos que el tratamiento de las clausuras se tome de la
misma forma, tanto si se envía un dato de tipo entero como si el dato es
de tipo real (o en coma flotante).
Nota
Referenciar una función definida de forma normal como una
función anónima o clausura se consigue de la siguiente forma:
F = fun io:format/1.
Esta declaración nos permitiría uilizar format/1 como una
clausura más empleando directamente F. Esto viene muy bien
para cuando se tienen varias funciones para trabajar de una cierta
forma y se desea pasar la función elegida como parámetro a un
código donde se empleará.
Por último, voy a comentar el uso de las clausuras en la evaluación
perezosa. Pongamos un ejemplo. Si en un momento dado queremos
54
Las funciones y módulos
trabajar con una lista de infinitos términos, o incluso con un contenido
que no queremos que esté siempre presente, sino que se vaya
generando a medida que se necesita, podemos realizar una clausura que
haga algo como lo siguiente:
-module(infinitos).
-compile([export_all]).
enteros(Desde) ->
fun() ->
[Desde|enteros(Desde+1)]
end.
Desde consola, podríamos emplear algo como lo siguiente:
> E = infinitos:enteros(5).
#Fun<infinitos.0.16233373>
> [N|F] = E().
[5|#Fun<infinitos.0.16233373>]
> [M|G] = F().
[6|#Fun<infinitos.0.16233373>]
Aunque hemos creado una recursividad infinita (algo parecido a un bucle
infinito), gracias a la evaluación perezosa de Erlang cada número se
va generando a medida que vamos avanzando. Retomaremos este uso
cuando tratemos el tema de la recursividad.
6. Programación Funcional
Cuando se piensa en programación funcional, normalmente, se piensa
en las listas de comprensión y en funciones sobre listas como son map,
filter o fold.
Estas funciones realizan un tratamiento de datos como podría hacerlo
un bucle en los lenguajes imperativos. En realidad termina siendo más
potente ya que, debido a su naturaleza, se puede paralelizar.
A través del uso de clausuras, podemos hacer que se aplique un código
específico a cada elemento de una lista de elementos. Veamos la lista de
funciones más importantes de este tipo que provee Erlang:
map/2
Se ejecuta la clausura pasada como parámetro, recibiendo cada
elemento de la lista como parámetro y retornando un valor por cada
llamada que será almacenado y retornado por map/2 al final de la
ejecución de todos los elementos. Por ejemplo:
> L = [1,2,3,4].
55
Las funciones y módulos
> lists:map(fun(X) -> X * 2 end, L).
[2,4,6,8]
any/2
Se evalúa cada elemento con la clausura pasada como parámetro,
debiendo retornar ésta true o false. Si alguno de los elementos
retorna true, la función any/2 retorna también true. Un ejemplo:
> L = [1,2,3,4].
> lists:any(fun(X) ->
>
if
>
X > 2 -> true;
>
true -> false
>
end
> end, L).
true
all/2
Igual que la anterior, con la salvedad de que todos los elementos
evaluados deben retornar true. En el momento en el que uno
retorne false, la función all/2 retornaría false. Un ejemplo:
> L = [1,2,3,4].
> lists:all(fun(X) ->
>
if
>
X > 2 -> true;
>
true -> false
>
end
> end, L).
false
foreach/2
Aplica la ejecución de la clausura a cada elemento de la lista. En
principio es igual que map/2, salvo que foreach/2 no guarda el
retorno de las clausuras que ejecuta ni lo retorna. Por ejemplo:
> L = [1,2,3,4].
> lists:foreach(fun(X) -> io:format("~p~n", [X]) end, L).
1
2
3
4
ok
foldl/3 - foldr/3
Esta función se encarga de ejecutar la clausura pasando como
parámetro el elemento de la lista y el retorno de la ejecución
anterior. Es como si encadenase la ejecución de las clausuras, que
forzosamente deben aceptar los dos parámetros. La última letra
56
Las funciones y módulos
(l o r) indica desde donde se inicia la toma de elementos de la
lista. Left o izquierda sería desde la cabeza hasta la cola, y right o
derecha empezaría a tomar elementos por el final de la lista hasta
el principio. A la función se le pasan tres parámetros, el primero es
la clausura, el segundo el valor inicial y el tercero la lista a procesar:
> L = [1,2,3,4],
> F = fun(X, Factorial) -> Factorial * X end,
> lists:foldl(F, 1, L).
24
mapfoldl/3 - mapfoldr/3
Estas funciones son una combinación de map/2 y fold/3.
Encadenan los resultados de cada una de las clausuras de la
anterior a la siguiente comenzando por un valor inicial, guardando
el resultado de ejecución de cada clausura. El retorno de la función
clausura debe ser una tupla en la que el primer valor es el resultado
de la parte map/2 y el segundo valor es el retorno para seguir
encadenando. El retorno de ambas funciones es también una tupla
en la que el primer elemento es una lista con todos los elementos
(tal y como lo haría map/2) y el segundo valor es el resultado de la
parte de fold/3. Un ejemplo:
> L = [1,2,3,4],
> F = fun(X, Factorial) -> {X*2, Factorial*X} end,
> lists:mapfoldl(F, 1, L).
{[2,4,6,8],24}
filter/2
El filtrado toma la lista inicial y ejecuta la clausura para cada
elemento. La clausura debe retornar verdadero o falso (true o false).
Cada elemento que cumpla con la clausura será agregado a la lista
del resultado de filter/2. Un ejemplo:
> L = [1,2,3,4],
> F = fun(X) -> if X > 2 -> true; true -> false end end,
> lists:filter(F, L).
[3,4]
takewhile/2
En este caso, la clausura se emplea como filtro al igual que con
filter/2, pero en el momento en el que un valor retorna falso
termina la ejecución. Por ejemplo:
> L = [1,2,3,4],
> F = fun(X) -> if X =< 2 -> true; true -> false end end,
> lists:takewhile(F, L).
57
Las funciones y módulos
[1,2]
dropwhile/2
Este es el complementario de takewhile. No toma ningún
elemento mientras se cumpla la condición. En el momento que se
incumple la condición, toma todos los elementos desde ese punto
hasta el final. Es decir, que toma todos los elementos que no tomaría
takewhile/2. Un ejemplo:
> L = [1,2,3,4],
> F = fun(X) -> if X =< 2 -> true; true -> false end end,
> lists:dropwhile(F, L).
[3,4]
splitwidth/2
Divide la lista en dos sublistas de manera equivalente a introducir
en una tupla como primer valor el resultado de takewhile/2 y
como segundo valor el resultado de dropwhile/2. Un ejemplo:
> L = [1,2,3,4],
> F = fun(X) -> if X =< 2 -> true; true -> false end end,
> lists:splitwith(F, L).
{[1,2],[3,4]}
Estas son las principales funciones que pertenecen al módulo lists.
La mayoría de estas funciones ya han sido agregadas a lenguajes
imperativos, al igual que las listas de comprensión, por lo que es posible
que muchas de ellas sean ya conocidas para el lector.
Es bueno conocer estas funciones para que cuando surja la necesidad
de resolución de un problema se pueda recurrir a ellas si es posible. Si
estás interesado en saber más acerca de estas funciones, puedes echar
un vistazo al módulo lists y así ampliar tu vocabulario en Erlang.
7. Recursividad
La recursividad define el hecho de que una función se pueda llamar a
sí misma para completar el procesamiento sobre una muestra de datos
a la que se puede aplicar el mismo algoritmo de forma recurrente hasta
conseguir una solución final.
La diferencia entre la recursividad y realizar un código iterativo, es que
las variables locales que se emplean, en el caso de la recursividad, son
propias para cada ejecución aislada del problema. El lazo común entre
cada solución o ejecución de la función, son los parámetros de entrada y
los parámetros de salida, el resto se almacena en variables locales, que
en la mayoría de lenguajes se almacena en una pila de ejecución.
58
Las funciones y módulos
Nota
Erlang implementa un sistema denominado tail recursion (o
recursividad de cola), que hace que la pila de una llamada a
la siguiente se libere dado que el código para ejecutar en esa
función ya no es necesario. Esto evita que se produzcan errores
por desbordamiento de pila, convirtiendo el código recursivo en
iterativo, al menos a efectos de consumo de memoria.
El ejemplo más simple de recursividad es la operación de factorial:
-module(fact).
-compile(export_all).
fact(0) -> 1;
fact(X) -> X * fact(X-1).
En esta functión, tenemos dos casos diferenciados. El caso particular
representado por la primera declaración de función, porque sabemos
que el factorial de cero es uno. También disponemos del caso general,
que serían el resto de casos para una variable X lo que se resuelven
multiplicando cada valor por su anterior hasta llegar a cero.
Un tipo de algoritmos que se puede implementar muy fácilmente con
recursión son los de divide y vencerás. Estos algoritmos se basan en la
división del problema en subproblemas más pequeños pero similares
llegando a los casos particulares. Se resuelve cada pequeño problema
de forma aislada y después se combinan las soluciones (si es necesario),
para conseguir la solución global del problema.
Las tres partes que se pueden diferenciar en este algoritmo son:
separación, recursión y combinación. Podemos ver algunos algoritmos
clásicos como los de ordenación de listas que nos pueden ayudar a
comprender mejor cómo funciona la recursividad.
7.1. Ordenación por mezcla (mergesort)
Comenzaremos viendo el algoritmo de ordenación por mezcla (o
mergesort), que se basa en hacer una partición de los elementos simple,
una recursividad sobre cada parte para descomponer el problema lo más
que se pueda y una mezcla en la que se va realizando combinación de
las partes ordenadas. Este algoritmo es simple en las dos primeras partes
y deja la complejidad para la tercera. Primero partimos la lista en trozos
de tamaño similar, idealmente igual:
> L = [5,2,8,4,3,2,1].
> {L1,L2} = lists:split(length(L) div 2, L).
{[5,2,8],[4,3,2,1]}
59
Las funciones y módulos
Esto lo podemos dejar dentro de una función que se llame separa/1
para semantizar el código y diferenciarla dentro del algoritmo. La mezcla
podemos hacerla a través de recursión también, de modo que, dadas dos
listas ordenadas podríamos definirla así:
mezcla([], L) ->
L;
mezcla(L, []) ->
L;
mezcla([H1|T1]=L1, [H2|T2]=L2) ->
if
H1 =< H2 -> [H1|mezcla(T1,L2)];
true -> [H2|mezcla(L1,T2)]
end.
La mezcla la realizamos tomando en cada paso de los datos de cabecera
de las listas, el que cumpla con la condición indicada (el que sea menor),
concatenando el elemento y llamando a la función con los elementos
restantes. Para que este algoritmo funcione ambas listas deben de estar
ordenadas, por lo que hay que ir separando elementos hasta llegar
al caso particular, que será la comparación de un elemento con otro
elemento (uno con uno). Para conseguir esto, realizamos la siguiente
recursión:
ordena([]) ->
[];
ordena([H]) ->
[H];
ordena(L) ->
{L1,L2} = separa(L),
mezcla(ordena(L1), ordena(L2)).
Como puedes observar, antes de llamar a la mezcla, para cada sublista,
se vuelve a llamar a la función ordena/1, con lo que llega hasta la
comparación de un sólo elemento con otro. Después un nivel más alto de
dos con dos, tres con tres, y así hasta poder comparar la mitad de la lista
con la otra mitad para acabar con la ordenación de la lista de números.
Como dijimos al principio, la complejidad se presenta en la combinación,
o función mezcla/2, que de forma recursiva se encarga de comparar
los elementos de una lista con la otra para conformar una sola en la que
estén todos ordenados.
El código completo del algoritmo es el siguiente:
-module(mergesort).
-export([ordena/1]).
separa(L) ->
lists:split(length(L) div 2, L).
60
Las funciones y módulos
mezcla([], L) ->
L;
mezcla(L, []) ->
L;
mezcla([H1|T1]=L1, [H2|T2]=L2) ->
if
H1 =< H2 -> [H1|mezcla(T1,L2)];
true -> [H2|mezcla(L1,T2)]
end.
ordena([]) ->
[];
ordena([H]) ->
[H];
ordena(L) ->
{L1,L2} = separa(L),
mezcla(ordena(L1), ordena(L2)).
Hemos dejado exportada solamente la función ordena/1, de modo que
para poder emplear el algoritmo habría que hacerlo así:
> mergesort:ordena([1,7,5,3,6,2]).
[1,2,3,5,6,7]
7.2. Ordenación rápida (quicksort)
En este ejemplo, vamos a llevarnos la complejidad de la parte de
combinación a la parte de separación. Esta función, que se llama
quicksort por lo rápida que es ordenando elementos, se basa en la
ordenación primaria de las listas para que la mezcla sea trivial.
Este algoritmo se basa en coger un elemento de la lista como pivote y
separar la lista en dos sublistas, una con los elementos menores al pivote
(la primera) y la otra con los elementos mayores (la segunda), para volver
a llamar al algoritmo para cada sublista.
Esta parte de código la simplificaremos empleando listas de
comprensión, de modo que podemos hacer lo siguiente:
> [Pivote|T] = [5,2,6,4,3,2,1],
> Menor = [ X || X <- T, X =< Pivote ],
> Mayor = [ X || X <- T, X > Pivote ],
> {Menor, [Pivote|Mayor]}.
{[2,4,3,2,1],[5,6]}
La parte de la mezcla es trivial puesto que se recibirán listas ya ordenadas
como parámetros. La mezcla consiste sólo en concatenar las sublistas y
retornar el resultado. La parte de la recursividad, es muy parecida a la de
mergesort. Viendo el código al completo:
-module(quicksort).
61
Las funciones y módulos
-export([ordena/1]).
separa([]) ->
{[], [], []};
separa([H]) ->
{[H], [], []};
separa([Pivote|T]) ->
Menor = [ X || X <- T, X =< Pivote ],
Mayor = [ X || X <- T, X > Pivote ],
{Menor, [Pivote], Mayor}.
mezcla(L1, L2) ->
L1 ++ L2.
ordena([]) ->
[];
ordena([H]) ->
[H];
ordena(L) ->
{L1, [Pivote], L2} = separa(L),
mezcla(ordena(L1) ++ [Pivote], ordena(L2)).
Se puede ver que la estrategia de divide y vencerás se mantiene.
Por un lado separamos la lista en dos sublistas seleccionando un
pivote, retornando ambas sublistas y el pivote. Las sublistas se ordenan
mediante recursión sobre cada sublista por separado.
La ejecución de este código sería así:
> quicksort:ordena([1,7,5,3,6,2]).
[1,2,3,5,6,7]
8. Funciones Integradas
En Erlang existen funciones que no están escritas en Erlang, sino que
el sistema las procesa a bajo nivel y forman parte de la máquina
virtual como instrucciones base que se ejecutan mucho más rápido.
Estas funciones construidas en el sistema se albergan bajo el módulo
erlang. Normalmente no hace falta referirse al módulo para emplearlas
(a menos que exista ambigüedad). Algunas de ellas ya las hemos
visto: is_integer/1, integer_to_list/1, length/1, e incluso las
operaciones matemáticas, lógicas y otras. Un ejemplo:
> erlang:'+'(2, 3).
5
Estas funciones reciben el nombre de BIF (en inglés Built-In Functions).
Otros ejemplos de BIFs son el cálculo de MD5 (md5/1), el redondeo de
números (round/1) y el cálculo de la fecha (date/0) o la hora (time/0).
62
Las funciones y módulos
Nota
Robert Virding, uno de los creadores/fundadores/inventores de
2
Erlang, comentó en un artículo de su blog , lo confuso que resulta
determinar qué es un BIF y qué no. Un intento de definirlo
por parte de Jonas Barklund y Robert Virding disponible en la
especificación (no indica URL específica el autor en su blog), es
que un BIF fue una parte del lenguaje Erlang que no disponía de
una sintaxis concreta o especial, por lo que se mostraba como una
llamada a función normal.
2
http://rvirding.blogspot.com.es/2009/10/what-are-bifs.html
63
Capítulo 5. Procesos
Cuando estás en un atasco de tráfico con un
Porsche, todo lo que puedes hacer es consumir
más combustible que el resto estando parado. La
escalabilidad va de construir carreteras más anchas,
no coches más rápidos.
—Steve Swartz
Una de las grandes fortalezas de la plataforma de Erlang es la gestión de
procesos. Los procesos en Erlang son propios de la máquina virtual y en
cada plataforma tienen las mismas características y se comportan de la
misma forma. En definitiva, no se emplean los mecanismos propios del
sistema operativo para ello sino que es la propia máquina virtual quien
provee los mecanismos para su gestión.
Para comenzar analizaremos la anatomía de un proceso en Erlang
para comprender para lo que es, los mecanismos de comunicación de
que dispone y sus características de monitorización y enlazado con
otros procesos. Muchas de estas características están presentes en los
procesos nativos de sistemas operativos como Unix o derivados (BSD,
Linux, Solaris, ...) y otras se pueden desarrollar sin estar a priori integradas
dentro del proceso.
Repasaremos también las ventajas e inconvenientes que tienen los
procesos de Erlang. Su estructura aporta ventajas como la posibilidad
de lanzar millones de procesos por nodo, teniendo en cuenta que
cada máquina puede ejecutar más de un nodo. También presenta
inconvenientes como la velocidad de procesamiento frente a los
procesos nativos del sistema operativo.
Por último, el sistema de compartición de información entre procesos
programados para la concurrencia emplea el paso de mensajes en lugar
de emplear mecanismos como la memoria compartida y semáforos, o
monitores. Para ello proporciona a cada proceso un buzón y la capacidad
de enviar mensajes a otros procesos a través de la sintaxis del propio
lenguaje, de una forma simple.
1. Anatomía de un Proceso
Un proceso cualquiera, no sólo los que son propios de Erlang, tiene
unas características específicas que lo distingue, por ejemplo, de un hilo.
Los procesos son unidades de un programa en ejecución que tienen un
código propio y un espacio de datos propio (normalmente llamado heap).
Se podría decir que un proceso cumple los principios del ser vivo, ya
que puede nacer (crearse), crecer (ampliando sus recursos asignados),
64
Procesos
reproducirse (generar otros procesos) y morir (terminar su ejecución).
El planificador de procesos de la máquina virtual de Erlang se encarga
de dar paso a cada proceso a su debido tiempo y de aprovechar los
recursos propios de la máquina, como son los procesadores disponibles,
para intentar paralelizar y optimizar al máximo posible la ejecución de
los procesos. Esta sería la vida útil de un proceso.
En Erlang el proceso es además un animal social. Tiene mecanismos que
le permiten comunicarse con el resto de procesos y enlazarse a otros
procesos de forma vital o informativa. En caso de que un proceso muera
(ya sea debido a un fallo o porque ya no haya más código que ejecutar),
el proceso que está enlazado con él de forma vital muere también,
mientras que el que está enlazado de forma informativa es notificado de
su muerte.
Para esta comunicación, el proceso dispone de un buzón. En este buzón
otros procesos pueden dejar mensajes encolados, de modo que el
proceso puede procesar estos mensajes en cualquier momento. El envío
de estos mensajes no sólo se puede realizar de forma local, dentro del
mismo nodo, sino que también es posible entre distintos nodos que
estén interconectados entre sí, ya sea dentro de la misma máquina o en
la misma red.
Nota
Cuando se lanza un proceso, en consola podemos ver su
representación, en forma de cadena, como <X.Y.Z>. Los valores
que se representan en esta forma equivalen a:
• X es el número del nodo, siendo cero el nodo local.
• Y son los primeros 15 bits del número del proceso, un índice a
la tabla de procesos.
• Z son los bits 16 a 18 del número del proceso.
El hecho de que los valores Y y Z estén representados como dos
valores aparte, viene de las versiones R9B y anteriores, donde Y
era de 15 bits y Z era un contador de reutilización. Actualmente
Y y Z se siguen representando de forma separada para no romper
esa compatibilidad.
2. Ventajas e inconvenientes
Hemos realizado una introducción rápida y esquemática de lo que es
un proceso en general y un proceso Erlang, para dar una visión a alto
nivel del concepto. Como dijimos al principio, los procesos en Erlang
no son los del sistema operativo y, por tanto, tienen sus diferencias,
sus características especiales y sus ventajas e inconvenientes. En este
65
Procesos
apartado concretaremos esas ventajas e inconvenientes para saber
manejarlos y conocer las limitaciones y las potencias que proporcionan.
Desde el principio hemos remarcado siempre que una de las potencias
de Erlang son sus procesos, y es porque me atrevería a decir que es
el único lenguaje que dispone de una máquina virtual sobre la que
se emplean procesos propios de la máquina virtual y no del sistema
operativo. Esto confiere las siguientes ventajas:
La limitación de procesos lanzados se amplia.
La mayoría de sistemas operativos que se basan en procesos o
hilos limitan su lanzamiento a unos 64 mil aproximadamente. La
máquina virtual de Erlang gestiona la planificación de los procesos
1
en ejecución y eleva ese límite a 2 millones .
La comunicación entre procesos es más simple y más nutrida.
La programación concurrente se basa la compartición de datos, bien
mediante mecanismos como la memoria compartida y el bloqueo
de la misma a través de semáforos, o bien mediante la existencia de
secciones críticas de código que manipulan los datos compartidos
a través de monitores. Erlang sin embargo emplea el paso de
mensajes. Existe un buzón en cada proceso al que se le puede
enviar información (cualquier dato) y el código del proceso puede
trabajar con ese dato de cualquier forma que necesite.
Son procesos y no hilos.
Cada proceso tiene su propia memoria y por tanto no comparte nada
con el resto de procesos. La ventaja principal de tener espacios
de memoria exclusiva es que cuando un proceso falla y deja su
memoria inconsistente, este hecho no afecta al resto de procesos
que pueden seguir trabajando con normalidad. Si el proceso vuelve
a levantarse y queda operativo el sistema se autorecupera del error.
En el caso de hilos, es posible que un fallo en la memoria (que sí es
compartida) afecte a más de un hilo, e incluso al programa entero.
No obstante, no todo es perfecto y siempre hay inconvenientes en las
ventajas que se pintan. Por un lado, el hecho de que la máquina virtual de
Erlang se encargue de los procesos y del planificador de procesos, tiene
su coste. Aunque BEAM está bastante optimizada y el rendimiento de la
máquina se ha ido incrementando en cada versión liberada de Erlang,
cualquier lenguaje que emplee directamente los procesos nativos del
sistema operativo será más rápido.
1
No obstante, por máquina virtual lanzada el límite es algo más bajo por defecto, con el parámetro +P
se puede configurar un número mayor, siendo el valor de procesos máximo por defecto de 32.768, y
pudiéndose ajustar este valor de 16 a 134.217.727.
66
Procesos
3. Lanzando Procesos
El lanzamiento de los procesos en Erlang se realiza con una construcción
del lenguaje, en concreto una función para facilitar su compresión y
uso (ya que es un BIF o función interna) llamado spawn/1. Esta función
interna se encarga de lanzar un proceso que ejecute el código pasado
como parámetro, junto con la configuración para lanzar el proceso. El
retorno a esta llamada es el identificador del proceso lanzado.
La identificación de la función, pasada como parámetro a spawn/1
puede realizarse de varias formas distintas. Se puede emplear una
clausura o indicar, a través de una tripleta de datos (módulo, función y
argumentos), la función que se ejecutará.
Las opciones que acepta spawn/1 se refieren sobretodo al nodo Erlang
en el que se lanza el proceso y al código para ser ejecutado. La primera
parte la veremos un poco más adelante. Ahora nos centraremos en el
lanzamiento del código en el nodo actual.
Por ejemplo, si quisiéramos ejecutar en un proceso separado la
impresión de un dato por pantalla, podríamos ejecutar lo siguiente:
> spawn(io, format, ["hola mundo!"]).
Podríamos hacer lo mismo en forma de clausura, obteniendo el mismo
resultado:
> spawn(fun() -> io:format("hola mundo!") end).
2
Si almacenásemos el identificador de proceso llamado comúnmente PID
en una variable veríamos que el proceso ya no está activo mediante la
función interna is_process_alive/1:
> Pid = spawn(fun() -> io:format("hola mundo!") end).
hola mundo!<0.38.0>
> is_process_alive(Pid).
false
Como dijimos en su definición un proceso se mantiene vivo mientras
tiene código que ejecutar. Obviamente, la llamada a la función format/1
termina en el momento en el que imprime por pantalla el texto que se
le pasa como parámetro, por lo tanto, el proceso nuevo finaliza en ese
momento.
Si el código se demorase más tiempo en ejecutarse, la función
is_process_alive/1 devolvería un resultado diferente.
2
PID, siglas de Process ID o Identificador de Proceso.
67
Procesos
4. Bautizando Procesos
Otra de las ventajas disponibles en Erlang sobre los procesos, es poder
darles un nombre. Esto facilita mucho la programación ya que sólo
necesitamos conocer el nombre de un proceso para poder acceder a él.
No es necesario que tengamos el identficador que se ha generado en un
momento dado para ese proceso.
El registro de los nombres de procesos se realiza a través de otra función
interna llamada register/2. Esta función se encarga de realizar la
asignación entre el nombre del proceso y el PID para que a partir de ese
momento el sistema pueda emplear el nombre como identificador del
proceso.
El nombre debe de suministrarse como átomo, y cuando se emplee, debe
de ser también como átomo. Un ejemplo de esto sería el siguiente:
> Pid = spawn(fun() -> timer:sleep(100000) end).
<0.53.0>
> register(timer, Pid).
true
5. Comunicación entre Procesos
Una vez que sabemos como lanzar procesos y bautizarlos para poder
localizarlos sin necesidad de conocer su identificador de proceso,
veamos cómo establecer una comunicación entre procesos. Esta sería la
faceta social de nuestros procesos.
Para que un proceso pueda recibir un mensaje debe permanecer en
escucha. Esto quiere decir que debe de mantenerse en un estado
especial, en el que se toman los mensajes recibidos en el buzón del
proceso o en caso de que esté vacío espera hasta la llegada de un nuevo
mensaje. El comando que realiza esta labor es receive. Tiene una sintaxis
análoga a case con alguna salvedad. En este ejemplo se puede observar
la sintaxis que presenta receive:
> receive
>
Dato -> io:format("recibido: ~p~n", [Dato]
> end.
Si ejecutamos esto en la consola, veremos que se queda bloqueada. Esto
ocurre porque el proceso está a la espera de recibir un mensaje de otro
proceso. La consola de Erlang es también un proceso Erlang en sí, si
escribiésemos self/0 obtendríamos su PID.
68
Procesos
El envío de un mensaje desde otro proceso se realiza a través de una
construcción simple del lenguaje. Vamos a probar con un el siguiente
código:
> Pid = spawn(fun() ->
>
receive Any ->
>
io:format("recibido: ~p~n", [Any])
>
end
> end).
<0.49.0>
> Pid ! "hola".
recibido: "hola"
"hola"
El símbolo de exclamación se emplea para decirle a Erlang que envíe
al PID que se especifica a la izquierda del signo la información de la
derecha. La información enviada puede ser de cualquier tipo, ya sea un
átomo, una lista, un registro o una tupla con la complejidad interna que
se desee.
Nota
Cada proceso en Erlang tiene una cola de mensajes que almacena
los mensajes recibidos durante la vida del proceso, para que
cuando se ejecute receive, el mensaje pueda ser desencolado y
procesado.
Para poder realizar una comunicación bidireccional, el envío debe de
agregar el PID de quién envía el mensaje. Si queremos como prueba
enviar información y recibir una respuesta podemos realizar lo siguiente:
> Pid = spawn(fun() ->
>
receive
>
{P,M} ->
>
io:format("recibido: ~p~n", [M]),
>
P ! "adios"
>
end
> end).
<0.40.0>
> Pid ! {self(), "hola"},
> receive
>
Msg ->
>
io:format("retorno: ~p~n", [Msg])
> end.
recibido: "hola"
retorno: "adios"
Con este código, el proceso hijo creado con spawn/1 se mantiene a la
escucha desde el momento de su nacimiento. Cuando recibe una tupla
con la forma {P,M}, imprime el mensaje M por pantalla y envía el mensaje
adios al proceso P.
69
Procesos
El proceso de la consola es quien se encarga de realizar el envío del
primer mensaje hacia el proceso con identificador Pid agregando su
propio identificador (obtenido mediante la función self/0) a la llamada.
A continuación se mantiene a la escucha de la respuesta que le envía el
proceso hijo, en este caso adios.
Importante
Las secciones de opción dentro de receive pueden tener también
guards. En caso de que el mensaje recibido no concuerde con
ninguna de las opciones dadas será ignorado y se seguirá
manteniendo el proceso en modo de escucha.
Como opción de salida para evitar posibles bloqueos en caso de que
un evento nunca llegue, o nunca concuerde, o si simplemente se quiere
escuchar durante un cierto período de tiempo, podemos emplear la
sección especial after. En esta sección podemos indicarle al sistema un
número de milisegundos a esperar antes de cesar la escucha, pudiendo
indicar un código específico en este caso.
Si por ejemplo, en el código anterior, queremos que el proceso que
lanzamos se mantenga sólo un segundo en escucha y si no le llega
ningún mensaje finalice indicando este hecho, podemos reescribirlo de
la siguiente forma:
> Pid = spawn(fun() ->
>
receive
>
{P,M} ->
>
io:format("recibido: ~p~n", [M]),
>
P ! "adios"
>
after 1000 ->
>
io:format("tiempo de espera agotado~n")
>
end
> end).
<0.47.0>
tiempo de espera agotado
Si ponemos más segundos y realizamos el envío del mensaje antes de
que finalice este período, el comportamiento es exactamente igual al
anterior. Si dejamos el tiempo pasar, el proceso finalizará su ejecución
informando por pantalla que el tiempo se ha agotado.
Desarrollado en forma de módulo, para aprovechar la recursividad y que
el proceso se mantenga siempre activo, podríamos hacerlo así:
-module(escucha).
-compile([export_all]).
escucha() ->
receive
{Desde, Mensaje} ->
70
Procesos
io:format("recibido: ~p~n", [Mensaje]),
Desde ! ok,
escucha();
stop ->
io:format("proceso terminado~n")
after 5000 ->
io:format("dime algo!~n"),
escucha()
end.
para(Pid) ->
Pid ! stop,
ok.
dime(Pid, Algo) ->
Pid ! {self(), Algo},
ok.
init() ->
spawn(escucha, escucha, []).
La función escucha/0 (del módulo homónimo) se mantiene a la espera
de mensajes. Acepta dos tipos de mensajes. Por un lado el que ya
habíamos visto antes, una tupla {proceso, mensaje} que recibirá desde
otro proceso que se comunica con éste (se presentará por pantalla). El
otro tipo es un simple mensaje de stop. Cuando se recibe, como ya no
volvemos a ejecutar la función de escucha/0, el proceso finaliza su
ejecución.
Además, cada 5 segundos desde el último mensaje enviado, o desde el
último tiempo agotado, o desde el inicio de la ejecución, se imprime el
mensaje dime algo!, ejecutando recursivamente la función escucha/0
para seguir con el proceso activo.
El código para utilizar este módulo podría ser algo como:
> Pid = escucha:init().
<0.34.0>
dime algo!
> escucha:dime(Pid, "hola").
recibido: "hola"
ok
dime algo!
> escucha:dime(Pid, "hola a todos").
recibido: "hola a todos"
ok
dime algo!
> escucha:para(Pid).
proceso terminado
Con este ejemplo queda claro que lanzar un proceso es una actividad
trivial, al igual que el intercambio de mensajes entre procesos. Esta
es la base sobre la que se fundamenta una de las aplicaciones
más importantes de Erlang, la solución de problemas en entornos
concurrentes. También es la base de la mayoría de código que se escribe
71
Procesos
en este lenguaje. A continuación iremos ampliando y matizando aún más
lo visto en este apartado.
6. Procesos Enlazados
Otra de las funcionalidades que proporciona Erlang respecto a los
procesos es la capacidad para enlazarlos funcionalmente. Es posible
establecer una vinculación o enlace vital entre procesos de modo que si
a cualquiera de ellos le sucede algo, el otro es inmediatamente finalizado
por el sistema.
Completando el ejemplo anterior, si el código contuviera un fallo (no
de compilación, sino de ejecución), el proceso lanzado moriría pero al
proceso lanzador no le sucedería absolutamente nada.
El siguiente fragmento de código contiene un error:
> Pid = spawn(fun() -> A = 5, case A of 6 -> no end end).
<0.39.0>
=ERROR REPORT==== 27-Apr-2012::19:10:51 ===
Error in process <0.39.0> with exit value: ...
El error aparece en la consola provocando que el proceso termine
inmediatamente. Al proceso principal, el de la consola, no le sucede
absolutamente nada. Ni tan siquiera se entera, ya que el proceso fue
lanzado sin vinculación.
Nota
La consola está diseñada para procesar las excepciones, por lo
que una vinculación de error con la misma no provoca su cierre
por el error recibido, sino que simplemente indica que ha recibido
una excepción de salida.
Cambiando spawn/1 por spawn_link/1 el lanzamiento del proceso se
realiza con vinculación, produciendo:
> Pid = spawn_link(fun() -> A = 5, case A of 6 -> no end end).
<0.42.0>
=ERROR REPORT==== 27-Apr-2012::19:10:51 ===
Error in process <0.39.0> with exit value: ...
** exception exit: {case_clause,5}
Vamos a hacer un ejemplo más completo en un módulo. Tenemos dos
procesos que se mantienen a la escucha por un tiempo limitado y uno de
ellos en su código tiene un error. En este caso ambos procesos, aunque
independientes, finalizarán, ya que uno depende del otro (así se indica
al lanzarlos enlazados).
72
Procesos
El código sería así:
-module(gemelos).
-compile([export_all]).
lanza() ->
spawn(gemelos, crea, []),
ok.
crea() ->
spawn_link(gemelos, zipi, [0]),
timer:sleep(500),
zape(0).
zipi(A) ->
io:format("zipi - ~w~n", [A]),
timer:sleep(1000),
zipi(A+1).
zape(A) ->
io:format("zape - ~w~n", [A]),
timer:sleep(1000),
case A of
A when A < 5 -> ok
end,
zape(A+1).
Al ejecutar la función lanza/0, se genera un nuevo proceso
independiente (sin enlazar). Este proceso a su vez genera otro enlazado
que ejecuta la función zipi/1. Después se mantiene ejecutando la
función zape/1. Tendríamos pues tres procesos: el de la consola
generado por la llamada a lanza/0, el proceso que ejecuta zipi/1 y el
proceso que ejecuta zape/1; todos ellos enlazados.
Revisando zape/1, podemos ver que cuando el contador llegue a 5,
no habrá concordancia posible en la sentencia case lo que generará un
error que terminará con el proceso. Como está enlazado a zipi/1, este
proceso también finalizará su ejecución.
Visto desde la consola:
> gemelos:lanza().
zipi - 0
ok
zape - 0
zipi - 1
zape - 1
zipi - 2
zape - 2
zipi - 3
zape - 3
zipi - 4
zape - 4
zipi - 5
zape - 5
zipi - 6
>
73
Procesos
=ERROR REPORT==== 30-Oct-2012::22:21:58 ===
Error in process <0.34.0> with exit value: ...
Analizando la salida, vemos que se imprime zape por pantalla hasta que
al evaluar el código se produce un error que termina ese proceso y su
enlace, es decir, el proceso zipi.
Los enlaces se puede establecer o eliminar a través de las funciones
link/1 y unlink/1. El parámetro que esperan ambas funciones es el
PID del proceso a enlazar con el actual en el que se ejecutan.
Volviendo sobre nuestro ejemplo anterior, podemos crear un proceso
que se encargue de lanzar a los otros manteniendo un enlace con cada
uno de ellos. De este modo si uno de ellos finaliza su ejecución el enlace
con el proceso lanzador hará que éste finalice por lo que el resto de
procesos serán también finalizados en cascada.
El código del lanzador podría crearse en un módulo que usara la función
link/1 de esta forma:
-module(lanzador).
-compile([export_all]).
init() ->
spawn(lanzador, loop, []).
loop() ->
receive
{link, Pid} ->
link(Pid);
error ->
throw(error)
end,
loop().
agrega(Lanzador, Pid) ->
Lanzador ! {link, Pid},
ok.
Ahora el módulo gemelos se simplifica de la siguiente forma:
-module(gemelos_lanzador).
-compile([export_all]).
lanza() ->
LanzadorPid = lanzador:init(),
Zipi = spawn(gemelos, zipi, [0]),
lanzador:agrega(LanzadorPid, Zipi),
timer:sleep(500),
Zape = spawn(gemelos, zape, [0]),
lanzador:agrega(LanzadorPid, Zape),
LanzadorPid.
zipi(A) ->
io:format("zipi - ~w~n", [A]),
timer:sleep(1000),
74
Procesos
zipi(A+1).
zape(A) ->
io:format("zape - ~w~n", [A]),
timer:sleep(1000),
zape(A+1).
En este caso, no hemos introducido un error en el código del módulo
gemelos_lanzador sino que el error se produce durante el procesamiento
de uno de los mensajes del lanzador. En concreto, al enviarle el
mensaje error al lanzador éste lanza una excepción produciendo la caída
automática de los tres procesos.
Importante
Para que la finalización de un proceso provoque que todos
sus enlaces también finalicen, debe producirse una finalización
por error. Si un proceso finaliza su ejecución de forma normal
y satisfactoria, queda finalizado y desenlazado del resto de
procesos pero los demás no finalizan. En otras palabras, para
que un proceso enlazado sea finalizado por otro, el proceso que
provoca la caída de los procesos en cascada debe de haber
acabado con un error de ejecución.
7. Monitorización de Procesos
En contraposición al enlace vital, el enlace informativo o monitorización
tal y como se conoce en Erlang, permite recibir el estado de cada proceso
como mensaje. Este mecanismo permite que podamos conocer si un
proceso sigue activo o si ha finalizado su ejecución, ya sea por un error
o de forma normal. Este tipo de enlace es diferente al anterior que
simplemente propaga los errores haciendo que se produzcan en todos
los procesos enlazados.
Un ejemplo simple del paso de mensajes cuando un proceso finaliza se
puede ver a través de este sencillo código:
> {Pid,MonRef} = spawn_monitor(fun() -> receive
>
Any ->
>
io:format("recibido: ~p~n", [Any])
>
end
> end).
{<0.58.0>,#Ref<0.0.0.46>}
> Pid ! "hola".
recibido: "hola"
> flush().
Shell got {'DOWN',#Ref<0.0.0.96>,process,<0.58.0>,normal}
ok
El primer proceso tiene un receive que lo mantiene en espera
hasta que le llegue un mensaje. Al enviarle hola, el proceso finaliza
75
Procesos
satisfactoriamente. La función spawn_monitor/1 se encarga de lanzar
el nuevo proceso y enlazarle el monitor al proceso de la consola. Cuando
ejecutamos la función flush/0 podemos ver los mensajes que ha
recibido la consola, entre ellos el de finalización del proceso lanzado
anteriormente.
Si queremos lanzar un monitor sobre un proceso ya creado tendríamos
que recurrir a la función monitor/2. El primer parámetro de esta función
es siempre process y el segundo parámetro será el PID del proceso a
monitorizar. Empleando el ejemplo anterior:
> Pid = spawn(fun() -> receive
>
Any ->
>
io:format("recibido: ~p~n", [Any])
>
end
> end).
<0.58.0>
> monitor(process, Pid).
#Ref<0.0.0.96>
> Pid ! "hola".
recibido: "hola"
> flush().
Shell got {'DOWN',#Ref<0.0.0.96>,process,<0.58.0>,normal}
ok
El mensaje de finalización enviado por el proceso es una tupla que consta
de las siguientes partes:
{'DOWN', MonitorRef, process, Pid, Reason}
La referencia, MonitorRef, es la misma que retorna la función monitor/2,
el Pid se refiere al identificador del proceso que se está monitorizando
y Reason es la razón de terminación. Si la razón es normal es que el
proceso ha finalizado de forma correcta, en caso contrario, será debido
a que encontró algún fallo.
El uso de monitores nos puede servir para crear un lanzador como el del
apartado anterior pero que, al morir un proceso, sea capaz de relanzarlo
cuando se recibe la notificación de terminación. Se trata de un monitor
que se puede implementar de la siguiente forma:
-module(monitor).
-export([init/0, agrega/2]).
init() ->
Pid = spawn(fun() -> loop([]) end),
register(monitor, Pid),
ok.
loop(State) ->
receive
{monitor, From, Name, Fun} ->
76
Procesos
Pid = lanza(Name, Fun),
From ! {ok, Name},
loop([{Pid,[Name, Fun]}|State]);
{'DOWN',_Ref,process,Pid,_Reason} ->
[Name, Fun] = proplists:get_value(Pid, State),
NewPid = lanza(Name, Fun),
io:format("reavivando hijo en ~p~n", [NewPid]),
AntiguoHijo = {Pid,[Name,Fun]},
NuevoHijo = {NewPid,[Name,Fun]},
loop([NuevoHijo|State] -- [AntiguoHijo])
end.
lanza(Name, Fun) ->
Pid = spawn(Fun),
register(Name, Pid),
monitor(process, Pid),
Pid.
agrega(Name, Fun) ->
monitor ! {monitor, self(), Name, Fun},
receive {ok, Pid} -> Pid end.
Como ejemplo, podemos utilizar este código en consola de la siguiente
forma:
> monitor:init().
ok
> monitor:agrega(hola_mundo, fun() ->
>
receive
>
Any ->
>
io:format("Hola ~s!~n", [Any])
>
end
>
end).
hola_mundo
> hola_mundo ! "Manuel".
Hola Manuel!
"Manuel"
reavivando hijo en <0.38.0>
> hola_mundo ! "Miguel".
Hola Miguel!
"Miguel"
reavivando hijo en <0.40.0>
El código presente en la clausura no mantiene ningún bucle. Cuando
recibe un mensaje se ejecuta presentando por pantalla el texto Hola ...!
y finaliza. El proceso monitor recibe la salida del proceso y vuelve a
lanzarlo de nuevo, tal y como se observa en los mensajes reavivando hijo
en ....
8. Recarga de código
Uno de los requisitos con los que se desarrolló la máquina virtual de
Erlang fue que el código pudiese cambiar en caliente sin afectar su
funcionamiento. El mecanismo para cambiar el código es parecido al que
se realiza con los lenguajes de scripting con algunos matices.
77
Procesos
Quizás sea un poco extraño encontrar este tema en un capítulo dedicado
a procesos, pero nos parece apropiado ya que la recarga de código
afecta directamente a los procesos. La recarga de código afecta más a un
proceso que lo emplea de forma continua (como es el código base del
proceso), que a otro que lo emplea de forma eventual (funciones aisladas
que se emplean en muchos sitios).
Pondremos un ejemplo. Teniendo este código:
-module(prueba).
-export([code_change/0, init/0]).
init() ->
loop().
code_change() ->
loop().
loop() ->
receive Any -> io:format("original: ~p~n", [Any]) end,
prueba:code_change().
Desde una consola podemos compilar y ejecutar el código como de
costumbre:
> c(prueba).
{ok,prueba}
> Pid = spawn(prueba, code_change, []).
<0.39.0>
> Pid ! "hola", ok.
original: "hola"
ok
Se genera un proceso que mantiene el código de loop/0 en ejecución y
atiende a cada petición que se le envía al proceso. La función loop/0 a
su vez llama, de forma fully qualified, a la función code_change/0. Esta
forma de llamar a la función le permite a la máquina virtual de Erlang
revisar si hay una nueva versión del código en el fichero BEAM y, en caso
de ser así, recargarla.
Importante
Erlang puede mantener hasta dos instancias de código en
ejecución. Si tenemos un código ejecutándose que no se llama
de forma full qualified, aunque cambiemos el código BEAM no
se recargará. Pero si se lanza otro proceso nuevo, se hará con la
nueva versión del código. En ese momento habrá dos instancias
diferentes de un mismo código. Si se volviese a modificar el
código, el sistema debe de extinguir la versión más antigua del
código para quedarse sólo con las dos últimas, por lo que los
procesos antiguos con el código más antiguo serían eliminados.
78
Procesos
Si cambiamos el código del listado anterior por lo siguiente:
loop() ->
receive Any -> io:format("cambio: ~p~n", [Any]) end,
prueba:code_change().
Vamos a la consola de nuevo y recompilamos:
> c(prueba).
{ok,prueba}
> Pid ! "hola", ok.
original: "hola"
ok
> Pid ! "hola", ok.
cambio: "hola"
ok
Dado que el proceso está ya en ejecución, hasta que no provocamos
una segunda ejecución no se ha producido la recarga del código ni
comenzado a ejecutar el nuevo código.
Es bueno saber que podemos hacer que la recarga de código se haga
bajo demanda, utilizando las funciones adecuadas:
-module(prueba).
-export([code_change/0]).
code_change() ->
loop().
loop() ->
receive
update ->
code:purge(?MODULE),
code:load_file(?MODULE),
?MODULE:code_change();
Any ->
io:format("original: ~p~n", [Any]),
loop()
end.
Para probar este ejemplo lo lanzamos como la primera vez, haciendo
una llamada. Después cambiamos el código modificando el texto que
imprime por pantalla el mensaje y lo compilamos con la orden erlc.
Una vez hecho esto podemos provocar la recarga del códig enviando el
mensaje update desde consola fácilmente:
> Pid ! update.
update
> Pid ! "hola", ok.
cambio: "hola"
ok
79
Procesos
Esta vez la llamada update nos ahorra el tener que hacer otra llamada
adicional para que se ejecute el código nuevo.
9. Gestión de Procesos
Como hemos dicho desde el principio, Erlang ejecuta su código dentro
de una máquina virtual, por lo que posee su propia gestión de procesos,
de la que ya comentamos sus ventajas e inconvenientes.
En este apartado revisaremos las características de que disponemos para
la administración de procesos dentro de un programa. Aunque ya hemos
visto muchas de estas características como la creación, vinculación
y monitorización, nos quedan otras como el listado, comprobación y
eliminación.
Comenzaremos por lo más básico, la eliminación. Erlang nos provee
de una función llamada exit/2 que nos permite enviar mensajes
de terminación a los procesos. Los procesos aceptan estas señales y
finalizan su ejecución. El primer parámetro es el PID que es el dato
que requiere exit/2 para finalizar el proceso. El segundo parámetro
es opcional y representa el motivo de la salida. Por defecto se envía el
átomo normal. Su sintaxis por tanto es:
exit(Pid, Reason).
Por otro lado processes/0 nos proporciona una lista de procesos
activos. Con process_info/1 obtenemos la información sobre un
proceso dado el PID e incluso mediante process_info/2 con
un parámetro que indica la información específica de la lista de
3
propiedades : enlaces con otros procesos (links), información de la
memoria usada por el proceso (memory), la cola de mensajes (messages),
por quién está siendo monitorizado (monitored_by) o a quién monitoriza
(monitors), el nombre del proceso (registered_name), etc.
10. Nodos Erlang
La máquina virtual de Erlang no sólo tiene la capacidad de gestionar
millones de procesos en un único nodo, o de facilitar la comunicación
entre procesos a través de paso de mensajes implementado a nivel
de proceso, sino que también facilita la comunicación entre lo que se
conoce como nodos, dando al programador la transparencia suficiente
para que dos procesos comunicándose entre nodos diferentes se
comporten como si estuviesen dentro del mismo.
3
Toda esta información puede ser consultada, con mayor detalle de la siguiente dirección: http://
www.erlang.org/doc/man/erlang.html#process_info-2
80
Procesos
Cada nodo es una instancia en ejecución de la máquina virtual de Erlang.
Esta máquina virtual posee la capacidad de poder comunicarse con otros
nodos siempre y cuando se cumplan unas características concretas:
• El nodo se debe haber lanzado con un nombre de nodo válido.
• La cookie debe de ser la misma en ambos nodos.
• Deben de poder conectarse, estando en la misma red.
Erlang dispone de un mecanismo de seguridad de conexión por clave,
a la que se conoce como cookie. La cookie es una palabra de paso que
permite a un nodo conectarse con otros nodos siempre que compartan
la misma cookie.
Un ejemplo de lanzamiento de un nodo Erlang, desde una terminal sería
el siguiente:
erl -sname test1 -setcookie mitest
Si lanzamos esta línea para test1 y test2, veremos que el símobolo
de sistema de la consola de Erlang se modifica adoptando el nombre
del nodo de cada uno. En caso de que el nombre de la máquina en
la que ejecutamos esto fuese por ejemplo bosqueviejo, tendríamos
dos nodos en estos momentos levantados: test1@bosqueviejo y
test2@bosqueviejo.
El nombre propio del nodo se obtiene a través de la función interna
node/0. Los nodos a los que está conectado ese nodo se obtienen con
la función interna nodes/0. Los nodos de un cluster se obtienen con la
forma:
[node()|nodes()]
Desde la consola podemos usar el siguiente comando para conectar los
dos nodos:
(test1@bosqueviejo)>
[]
(test1@bosqueviejo)>
(test1@bosqueviejo)>
true
(test1@bosqueviejo)>
[test2@bosqueviejo]
nodes().
Remoto = test2@bosqueviejo,
net_kernel:connect_node(Remoto).
nodes().
11. Procesos Remotos
Hasta ahora, cuando empleábamos la función interna spawn/1
generábamos un proceso local, que se ejecutaba en el nodo que
81
Procesos
corre la función. Si tenemos otros nodos conectados, podemos realizar
programación paralela o distribuida, lanzando la ejecución de los
procesos en otros nodos. Es lo que se conoce como un proceso remoto.
Se puede lanzar un proceso remoto con la misma función spawn/1
agregando como primer parámetro el nombre del nodo donde queremos
lanzar el proceso. Por ejemplo, si queremos lanzar un proceso que se
mantenga a la escucha para dar información en el cluster montado por
los dos nodos que lanzamos en el apartado anterior, podríamos hacerlo
con el siguiente código:
-module(hash).
-export([init/1, get/2, set/3]).
get(Pid, Key) ->
Pid ! {get, self(), Key},
receive
Any -> Any
end.
set(Pid, Key, Value) ->
Pid ! {set, Key, Value},
ok.
init(Node) ->
io:format("iniciado~n"),
spawn(Node, fun() ->
loop([{"hi", "hola"}, {"bye", "adios"}])
end).
loop(Data) ->
receive
{get, From, Key} ->
Val = proplists:get_value(Key, Data),
From ! Val,
loop(Data);
{set, Key, Value} ->
loop([{Key, Value}|Data]);
stop ->
ok
end.
En la función init/1, se agrega el nombre del nodo que se pasa
como parámetro a spawn/2. La comunicación la podemos realizar
normalmente como en todos los casos anteriores que hemos visto sin
problemas. No obstante, el PID devuelto, a diferencia de los vistos
anteriormente, tiene su primera parte distinta de cero lo que indica que
está corriendo en otro nodo. Los procesos en nodos remotos no se puede
registrar con la función interna register/2, es decir, no se les puede
asociar un nombre y por ello, son sólo accesibles desde el nodo que los
lanzó.
82
Procesos
12. Procesos Locales o Globales
Todos los procesos que hemos registrado hasta ahora eran locales. Si
queremos que un proceso sea accesible desde diferentes nodos debe
registrarse como proceso global.
El lanzamiento del proceso se realiza como hasta ahora, lo único que varía
es la forma en la que se registra su nombre. Debe usarse el módulo global
con global:register_name/2. El acceso a un proceso así registrado
se realiza como hasta ahora, a través del nombre. La accesibilidad existe
desde cualquier nodo que esté conectado con el que posee el proceso.
Vamos a lanzar un proceso global en un nodo:
(test1@bosqueviejo)> global:register_name(consola, self()).
yes
(test1@bosqueviejo)> receive
(test1@bosqueviejo)>
Any -> io:format("~p~n", [Any])
(test1@bosqueviejo)> end.
"hola"
ok
Registramos el proceso de la consola con el nombre consola. Desde el
otro nodo de Erlang podemos enviar un mensaje de la siguiente forma:
(test2@bosqueviejo)> Remoto = test1@bosqueviejo,
(test2@bosqueviejo)> net_kernel:connect_node(Remoto).
true
(test2@bosqueviejo)> global:whereis_name(consola) ! "hola".
"hola"
El envío del mensaje lo podemos realizar a través del PID o a través
de la función send/2 del módulo global. En todo caso, obtenemos la
capacidad de tener accesibilidad a los procesos remotos desde cualquier
nodo del cluster.
Nota
Hay muchos casos en los que el módulo global puede tener un
rendimiento bastante bajo, o incluso hasta defectuoso. Por esta
4
razón han aparecido sustitutos como gproc (que requiere del
parcheo de parte del código OTP de Erlang), o módulos que no
5
requieren de ninguna modificación en la base como nprocreg .
4
5
https://github.com/uwiger/gproc
https://github.com/nitrogen/nprocreg
83
Procesos
13. RPC: Llamada Remota a Proceso
Otra de las propiedades que tiene la máquina virtual de Erlang, es
que permite conectarse a un nodo específico, ejecutar un comando y
obtener una respuesta. La principal diferencia con ejecutar un proceso
remotamente es que el comando RPC se lanza y se mantiene a la espera
de un retorno para esa ejecución.
Por ejemplo, si queremos obtener el identificador de los procesos de
cada nodo conectado al cluster, podemos conectarnos a cada nodo
remoto y obtener esta información vía RPC de la siguiente manera:
(test1@bosqueviejo)> lists:map(fun(Nodo) ->
(test1@bosqueviejo)>
rpc:call(Nodo, erlang, processes, [])
(test1@bosqueviejo)> end, nodes()).
[[<6759.0.0>,<6759.3.0>,<6759.5.0>,<6759.6.0>,<6759.8.0>,
<6759.9.0>,<6759.10.0>,<6759.11.0>,<6759.12.0>,<6759.13.0>,
<6759.14.0>,<6759.15.0>,<6759.17.0>,<6759.18.0>,<6759.19.0>,
<6759.20.0>,<6759.21.0>,<6759.22.0>,<6759.23.0>,<6759.24.0>,
<6759.25.0>,<6759.26.0>,<6759.27.0>,<6759.28.0>,<6759.29.0>,
<6759.30.0>,<6759.31.0>,<6759.32.0>|...]]
Aunque el código se ejecuta en el otro nodo, datos como los PID se
adaptan a la comunicación entre nodos, por lo que podríamos emplear
cualquiera de esos identificadores para obtener información del proceso
remoto.
6
Cualquier código que se ejecute a través de este sistema de RPC será
ejecutado en el nodo que se indique como primer parámetro, por lo que
el código debe de existir en ese nodo.
En caso de que el código resida únicamente en el nodo que solicita la
ejecución remota, existe la posibilidad de exportar el código al nodo
donde queremos que se ejecute. Esto puede conseguirse con el siguiente
código:
(test1@bosqueviejo)> {hash,B,F} = code:get_object_code(hash).
{hash,<<70,79,82,49,0,0,4,0,66,69,65,77,65,116,111,109,0,
0,0,126,0,0,0,17,4,104,97,...>>,
"/home/bombadil/hash.beam"}
(test1@bosqueviejo)> A = [hash, F, B].
(test1@bosqueviejo)> Host = test2@bosqueviejo,
(test1@bosqueviejo)> rpc:call(Host, code, load_binary, A).
{module,hash}
De esta forma, podríamos levantar cada nuevo nodo en cualquier
máquina sin tener el código. Todo quedaría en llamadas RPC desde el
nodo maestro hacia los demás nodos para ir levantando instancias del
código y lanzar los procesos que se requieran.
6
Son las siglas de Remote Procedure Call, o Llamada a Proceso Remoto.
84
Procesos
Nota
A través de multicall/3 en lugar de call/4, del módulo rpc
podemos envíar el código a cada uno de los nodos conectados
en el cluster.
14. Diccionario del Proceso
Para finalizar y dar por terminado este capítulo, indicar que Erlang
dispone de un diccionario de datos que puede ser empleado para
mantener datos propios del proceso. Podríamos considerarlo como
atributos propios del proceso.
Estos datos pueden ser manejados a través del uso de las siguientes
funciones internas:
get/0, get/1
Cuando se indica sin parámetros se obtienen todos los datos
contenidos dentro de ese proceso. El formato de esta devolución es
una lista de propiedades que puede ser manejada con las funciones
del módulo proplists.
Cuando se indica un parámetro, se toma como clave y se retorna
únicamente el valor solicitado.
get_keys/1
Se emplea para obtener todas las claves cuyos valores son los
indicados como único parámetro de la llamada a la función.
put/2
Almacena el par clave-valor pasados como parámetros, siendo el
primero la clave y el segundo el valor.
erase/0, erase/1
Sin parámetros se encarga de eliminar todas las ocurrencias del
diccionario. Es muy útil para limpiar completamente el diccionario.
Con el parámetro clave, se encarga únicamente de eliminar el valor
correspondiente a esa clave.
Este diccionario es útil para poder desarrollar procesos en los que
queramos manejar atributos, para modificar o eliminar elementos del
mismo. Proporciona un depósito de datos por proceso que nos puede
ayudar a mantener información de estado entre llamadas a un mismo
proceso.
85
Capítulo 6. ETS, DETS y Ficheros
Escribir es recordar, pero leer también es recordar.
—François Mauriac
Uno de los puntos importantes en un lenguaje de programación es
la gestión de ficheros. Los ficheros tienen innumerables usos, desde
escritura de logs, hasta el almacenamiento o lectura de datos o
configuraciones en formatos como CSV, XML o YAML, pasando por los
contenidos multimedia: imágenes en formato PNG, o vídeos de tipo AVI.
Necesitamos pues los mecanismos que nos permitan realizar todas las
operaciones con ficheros (renombrar, copiar, mover, etc.).
Erlang provee funciones básicas y muy simplificadas de acceso a ficheros
y directorios. Nos permite realizar la lectura de un fichero en texto
plano formateado como datos de Erlang (listas, tuplas, átomos, números,
etc.). Puede además emplear tablas ETS (Erlang Term Storage) para el
almacenaje en disco o, empleando directamente DETS (Disk Erlang Term
Storage), acceder a un directorio para su procesado.
En este capítulo nos adentraremos en cada aspecto referente a los
ficheros y las tablas ETS y DETS, a la lectura y escritura de ficheros de
texto y binarios, y a los mecanismos que nos da Erlang para navegar por
directorios.
1. ETS
Las siglas ETS se refieren a Erlang Term Storage, o almacenaje de términos
de Erlang. Los términos ya los habíamos revisado anteriormente, por lo
que sabemos que se trata de tuplas, en las que el primer elemento de la
tupla actúa como clave.
La razón para crear las tablas ETS fue la de poder almacenar gran cantidad
de datos con un tiempo de acceso siempre constante, ya que en los
lenguajes funcionales el tiempo de acceso a la información suele ser
función logarítmica. Otro motivo fue el proveer al desarrollador de un
modo de extraer, almacenar y tratar la información con los mecanismos
1
propios del lenguaje . Además, para que el uso de este sistema fuese
más rápido, las funciones para manejar las funcionalidades de ets se
encuentran en formato de BIF.
Por todo ello, el almacenaje de términos Erlang constituye una
herramienta fundamental de gestión de la información con Erlang,
especialmente cuando el tamaño de la información es elevado y se
necesita optimizar los tiempos de acceso.
1
A diferencia de llamadas a sistemas, como el SQL, las tablas ETS se quería que fuesen tratadas con
directivas, sentencias y funciones del lenguaje y no enviadas a un subsistema.
86
ETS, DETS y Ficheros
1.1. Tipos de Tablas
Podemos encontrar cuatro tipos de tablas ETS dependiendo de los
algoritmos empleados para la constitución de la tabla, su almacenaje y
la extracción de datos:
Conjunto (set)
Es el tipo por defecto. A semejanza de los conjuntos como concepto
matemático, cada elemento (cada clave de cada tupla) debe de ser
único a la hora de realizar la inserción dentro del conjunto. El orden
interno de los elementos no está definido.
Conjunto ordenado (ordered_set)
Es igual que el tipo anterior, pero en este caso los datos entrantes en
el conjunto son ordenados mediante la comparación de su clave con
las claves de los datos almacenados a través de los comparadores
< y >, siendo el primer elemento el más pequeño.
Bolsa (bag)
La bolsa elimina la restricción de que el primer elemento ya exista,
pero mantiene la propiedad de que las tuplas, comparadas en su
conjunto con otras, deben de ser distintas.
Bolsa duplicada (duplicate_bag)
Igual que la anterior, pero eliminando la restricción de que las tuplas
en su conjunto y comparadas con el resto deban de ser diferentes,
es decir, se permiten tuplas repetidas (o duplicadas).
Dependiendo del tipo de datos que necesitemos almacenar en las tablas
podremos elegir uno u otro tipo de tabla. Por ejemplo, si tenemos que
almacenar términos de forma ordenada para su extracción podemos
emplear un ordered_set, mientras que si la información que queremos
almacenar puede llegar a repetirse podríamos optar por alguna de las
bolsas, según el grado de repetición que queramos o tengamos que
permitir para los datos.
1.2. Acceso a las ETS
Las tablas ETS son creadas por un proceso que puede hacerlo con
opciones de accesibilidad que permitan su acceso por otros procesos o
no. Los parámetros de seguridad que podemos emplear para garantizar
el acceso o denegarlo, según el caso, son los siguientes:
private
Crea la ETS de ámbito privado. Esto quiere decir que no permite a
ningún otro proceso el acceso a la misma.
87
ETS, DETS y Ficheros
protected
El ámbito protegido para la ETS garantiza el acceso de lectura a
todos los procesos que conozcan el identificador de la ETS, pero
sólo permite la escritura para el proceso que la creó.
public
Garantiza el acceso a todos los procesos, tanto para lectura como
escritura, a la ETS a través del identificador de la misma.
Nota
Una ETS mantiene, a nivel de concurrencia, siempre los
parámetros de aislamiento y atomicidad íntegros, por lo que
asegura que una operación de escritura sobre un objeto de
una ETS, en caso de que sea correcta o falle lo hará de forma
completa (atomicidad). Cualquier operación de lectura sólo podrá
ver el conjunto final de las modificaciones en caso de éxito
(aislamiento).
1.3. Creación de una ETS
Para crear una tabla ETS emplearemos la función new/2 del módulo ets
cuyos parámetros son el nombre de la tabla (un átomo) y las opciones
para la creación de la misma.
Las opciones están en formato de lista. Cada elemento de la lista
corresponderá a cada una de las siguientes secciones:
Tipo de tabla
Se debe de especificar alguno de los tipos de ETS vistos: set,
ordered_set, bag o duplicate_bag.
Acceso a la tabla
Se debe de especificar alguno de los tipos de accesos para la ETS
vistos: public, protected o private.
named_table
Si se especifica esta opción, el primer parámetro de la función es
empleado como identificador para poder acceder a la tabla.
keypos
En caso de que queramos que la clave de la ETS no sea el primer
elemento de la tupla podemos agregar esta opción de la forma:
{keypos, Pos}
88
ETS, DETS y Ficheros
Siendo Pos un número entero dentro del rango de elementos de la
tupla.
heir
El sistema puede establecer un proceso hijo al que pasarle el control
de la ETS, de modo que si algo le sucediese al proceso que creó la
ETS, el proceso hijo recibiría el mensaje:
{'ETS-TRANSFER', id, FromPid, HeirData}
Y tomaría en propiedad la tabla. La configuración sería:
{heir, Pid, HeirData}
En caso de no especificar un heredero, si el proceso propietario de
la tabla termina su ejecución la tabla desaparece con él.
Concurrencia
Por defecto, las ETS mantienen un nivel de concurrencia por
bloqueo completo, es decir, mientras se está trabajando con la tabla
ningún otro proceso puede acceder a ella, ya sea para leer o escribir.
No obstante, a través de la opción:
{read_concurrency, true}
Activamos la concurrencia de lectura. Esta opción es buena si
el número de lecturas es mayor que el de escrituras, ya que el
sistema adapta internamente los datos para que las lecturas puedan
emplear incluso los diferentes procesadores que pueda tener la
máquina.
Para activar la concurrencia en la escritura, se debe emplear la
opción siguiente:
{write_concurrency, true}
La activación de la escritura sigue garantizando tanto la atomicidad
como el aislamiento. Esta opción está recomendada si el nivel de
concurrencia de lectura/escritura de los datos almacenados en la
ETS provocan excesivo tiempo de espera y fallos a consecuencia de
este cuello de botella. La nota negativa, vuelve a ser la penalización
existente al realizar las escrituras concurrentes.
compressed
Los datos de la ETS se comprimen para almacenarse en memoria. Al
trabajar sobre datos comprimidos los procesos de búsqueda y toma
89
ETS, DETS y Ficheros
de datos son más costosos. Esta opción es aconsejable cuando sea
más importante el consumo de memoria que la velocidad de acceso.
Como ejemplos de creación de ETS:
> T = ets:new(prueba, []).
16400
> ets:new(tabla, [named_table]).
tabla
> ets:new(conjunto, [set, named_table]).
conjunto
> ets:new(bolsa, [bag, named_table]).
bolsa
Todas las opciones vistas anteriormente se pueden emplear en cualquier
orden dentro de la lista de opciones, y se pueden poner tantas como se
necesite.
Importante
Ante el uso de dos opciones que colisionen, como el hecho de
emplear conjuntamente la opción bag y la opción set, el sistema
empleará la última leída de la lista de opciones. Por ejemplo, en
este caso:
ets:new(tabla, [set, bag, private, public])
Las opciones que predominan finalmente y las que se quedarán
configuradas son bag y public.
1.4. Lectura y Escritura en ETS
Una vez que tenemos creada una ETS podemos comenzar a trabajar con
ella. Para agregar elementos podemos emplear la función insert/2
cuyo primer parámetro es el identificador de la tabla (o su nombre en
caso de named_table), siendo el segundo parámetro un término o una
lista de términos para su inserción.
Un ejemplo para nuestra bolsa creada anteriormente sería:
> ets:insert(bolsa, {rojo, 255, 0, 0}).
true
Con esta llamada hemos introducido un término en el que la clave es rojo
dentro de la bolsa. Si queremos ver el contenido de la tabla, podemos
emplear la función match. Esta función emplea dos parámetros: el
primero es el nombre de la ETS, y el segundo es el patrón que debe
de cumplir el dato para ser mostrado. De momento, daremos '$1' es el
comodín que nos permite sacar todos los datos:
90
ETS, DETS y Ficheros
> ets:match(bolsa, '$1').
[[{rojo,255,0,0}]]
Si insertamos algunos elementos más podemos ver cómo se van
almacenando del mismo modo:
> ets:insert(bolsa, [{verde,0,255,0},{azul,0,0,255}]).
true
> ets:match(bolsa, '$1').
[[{rojo,255,0,0}],[{azul,0,0,255}],[{verde,0,255,0}]]
Los elementos se insertan donde mejor conviene al sistema interno, tal y
como se puede ver en el listado. Para extraer un elemento concreto dado
el identificador de la tupla podemos emplear la función lookup/2:
> ets:lookup(bolsa, azul).
[{azul,0,0,255}]
Con el uso de las funciones first/1 y next/2, o last/1 y prev/2,
podemos recorrer la lista utilizando la recursión. Si llegamos al final nos
devolverá el átomo '$end_of_table'.
Un ejemplo de esto se puede ver en el siguiente módulo:
-module(ets_show).
-compile([export_all]).
show_all(Ets) ->
show_all(Ets, ets:first(Ets), []).
show_all(_Ets, '$end_of_table', List) ->
List;
show_all(Ets, Id, List) ->
show_all(Ets,ets:next(Ets,Id),ets:lookup(Ets,Id) ++ List).
main() ->
ets:new(bolsa, [named_table, bag]),
Colores = [{rojo,255,0,0},{verde,0,255,0},{azul,0,0,255}],
ets:insert(bolsa, Colores),
show_all(bolsa).
Si ejecutamos la función main/0, veremos como nos retorna todo
lo insertado dentro de bolsa. Igualmente, si creamos una nueva ETS,
podemos emplear la función show_all/1 para listar todo su contenido.
1.5. Match: búsquedas avanzadas
En la sección anterior vimos que el listado general se podía conseguir con
una forma específica de la función match. Esta función, a través del uso
de los patrones, nos permite mucha mayor potencia a la hora de rescatar
datos de la ETS.
91
ETS, DETS y Ficheros
La teoría de la concordancia para las ETS se puede emplear tanto
para funciones select/2, como para las funciones match/2. Esta
concordancia se basa en pasarle la información al núcleo de ETS para
realizar la extracción. Requiere que se puedan identificar variables como
tal o bien con el uso de comodines.
Para esto se definen dos átomos que tienen una semántica especial para
2
el gestor de las ETS. Son las llamadas variables sin importancia y el
comodín. Las variables sin importancia se pueden especificar como '$0',
'$1', '$2', ...; La numeración sólo es relevante en caso de especificar la
forma en la que se obtendrán los resultados (para las funciones como
select/2).
El otro tipo de átomo con significado específico para las ETS es el
comodín '_'. Ya vimos en su momento que el signo de subrayado se utiliza
para indicar que el dato en esa posición no interesa.
Rescatando el ejemplo anterior, vemos que habíamos escrito como
parámetro de la función match/2 la siguiente expresión:
'$1'
Al no tener $1 forma de tupla, esta expresión de la variable sin
importancia concuerda con toda la tupla al completo. Si pusiéramos en
el match lo siguiente:
{'$1',255,'_','_'}
Veremos que extraemos el valor rojo, ya que es el único que cumple la
condición de concordancia de tener en su segunda posición el valor 255.
Como la variable sin importancia sólo la hemos situado en la primera
posición, sólo recibiremos esta.
Nota
Si empleamos la función match_object/2 en lugar de match/2
se retornará siempre el objeto completo. La concordancia se
tendrá en cuenta sólo a nivel de elección y no a la hora de
organizar los datos para su devolución.
Por último, vamos a ver el uso de la función select/2, como una función
más avanzada que nos da la posibilidad, no sólo de enviar una tupla
de concordancia, sino también una parte de guardas y la proyección (el
cómo se visualizarán en el resultado). Esta función nos da para las ETS la
2
El nombre de variable sin importancia es una traducción prestada del inglés don't care al que hace
referencia el sitio Learn You Some Erlang [http://learnyousomeerlang.com/].
92
ETS, DETS y Ficheros
misma potencia que nos brindan las listas de compresión sobre las listas
que ya vimos en Sección 1.4, “Listas” del Capítulo 2, El lenguaje.
El formato que se emplea se denomina especificaciones de concordancia
y consta de una tupla de tres elementos: el primero el de la concordancia,
ya visto anteriormente, el segundo es el que almacena las guardas y el
tercero el que se encarga de especificar la proyección de elementos.
Comenzaremos con este ejemplo:
{
{'$0','$1','$2','$3'},
[{'<','$1',0}],
['$0']
}
He separado en cada línea cada uno de los tres parámetros que se deben
enviar para cumplir con la especificación de concordancia. En la primera
línea se puede ver que no se ha realizado ninguna primera criba, sino
que se aceptan todas las ETS que tengan ese número de tuplas.
El segundo parámetro contiene una operación. Como se puede observar
el formato es igual al conocido como calculadora polaca, la operación
es el primer elemento que se encuentra y los operandos los que vienen
a continuación. Volviendo a nuestro ejemplo concreto la condición que
debe de cumplir la tupla es que su elemento '$1' sea mayor que cero.
Por último, en el tercer elemento, realizamos una proyección para
devolver como resultado único el primer elemento de la tupla
(precisamente '$0' el identificador o clave).
Otro ejemplo más complejo o completo se deja a la investigación del
lector:
{
{'$1','$2','$4','$8'},
[{'andalso',
{'==','$2',0},
{'==','$8',0}
}],
[ '$$' ]
}
1.6. Eliminando tuplas
Para eliminar una o varias tuplas, se utiliza la función delete/2.
Esta función permite eliminar una clave concreta de una ETS dada, un
funcionamiento muy parecido al de la función lookup/2.
ets:delete(bolsa, verde)
93
ETS, DETS y Ficheros
También cabe la posibilidad de realizar una eliminación completa de la
tabla con la función delete/1, o bien el vaciado de sus elementos con
la función delete_all_objects/1.
Si lo que queremos es eliminar una serie de objetos específicos lo
podemos realizar a través de la función match_delete/2. Esta función
acepta como parámetros la ETS, y el patrón de elementos a eliminar, a
igual que en la función match/2 ya vista.
1.7. ETS a fichero
Una forma de leer la información de una ETS desde un fichero y volver
a almacenarla en un fichero es a través de las funciones tab2file/2 y
file2tab/1.
La función tab2file/2 tiene la siguiente forma:
tab2file(Tab, Filename) -> ok | {error, Reason}
En el fichero que se especifica se almacena toda la ETS, con una cabecera
en la que se almacenan las opciones con las que fue creada, para que
cuando se vuelva a leer el fichero, la ETS se instancie de la misma forma
en la que se guardó.
La función file2tab/1 tiene la siguiente forma:
file2tab(Filename) -> {ok, Tab} | {error, Reason}
Se encarga de leer el fichero, tomar la cabecera y crear la ETS con su
contenido, tal y como se guardó.
El almacenamiento de archivos es un buen sistema para poder gestionar
los datos de una ETS de manera persistente. No obstante hay que tener
en mente que siempre pueden surgir problemas. Por ejemplo el tamaño
de la ETS puede superar el de la memoria que se puede emplear, o bien el
sistema puede fallar antes de que se haya podido guardar la información
en el fichero. Incluso podría ser que el fichero se corrompiera durante
su escritura. Para evitar estos problemas necesitamos un sistema que
trabaje directamente con el fichero y que la robustez para haber previsto
este tipo de problemas. En Erlang este sistema son las DETS.
2. DETS
Las DETS son ETS que se almacenan en disco (Disk Erlang Term Storage).
Al tratarse también de almacenaje de términos, poseen un interfaz
al programador muy parecido al de las ETS. El medio de tratamiento
y almacenamiento de la información es distinto. Las DETS pueden
mantener persistencia de información mientras que las ETS no la tienen.
94
ETS, DETS y Ficheros
El sistema DETS se suele emplear cuando se requieren almacenar con
cierta persistencia información en forma de términos Erlang y cuyo
fichero de almacenaje no exceda los 2 GiB de espacio.
3
Este sistema de almacenaje es el mismo que emplea Mnesia , el motor
de base de datos que integra OTP y que viene por defecto con Erlang.
Mnesia, como motor de base de datos, no sólo posee la capacidad de
trabajar con términos para su almacenaje, sino que proporciona otros
elementos como transacciones, consultas, distribución y fragmentación
de tablas, por lo que, puede ser un entorno más complejo y potente que
si sólo se requieren almacenar términos.
2.1. Tipos de Tablas
Al igual que las ETS, las DETS se pueden crear de varios tipos y comparten
estos tipos con las ETS, a excepción de los conjuntos ordenados que no
se incluyen de momento por no haber encontrado una forma óptima de
realizar su tratamiento.
Repasamos los tipos que se pueden crear para las DETS:
Conjunto (set)
Este es el tipo por defecto. A semejanza de los conjuntos como
concepto matemático, cada elemento (cada clave de cada tupla)
debe de ser única a la hora de realizar la inserción del elemento
dentro del conjunto. El orden interno no está definido.
Bolsa (bag)
La bolsa elimina la restricción de que el identificador de la tupla sea
igual, pero mantiene la propiedad de que las tuplas, comparadas en
su conjunto con otras, deben de ser distintas.
Bolsa duplicada (duplicate_bag)
Igual que la anterior, pero eliminando la restricción de que las tuplas
en su conjunto y comparadas con el resto deban de ser diferentes,
es decir, se permiten tuplas repetidas (o duplicadas).
Nota
A día de hoy (en la revisión R15 de Erlang/OTP), no existe librería
que permita escribir de forma ordenada los términos hacia disco.
Está pendiente y posiblemente en futuras liberaciones veamos
que finalmente se agrega a esta lista el conjunto ordenado.
3
En este libro no se tratará Mnesia, porque sino el texto se nos extendería unas decenas de páginas más
y no conseguiríamos abarcarlo como se merece.
95
ETS, DETS y Ficheros
2.2. Crear o abrir una DETS
Esta operación se realiza mediante la función open_file/2. Este
comando no sólo sirve para crear la DETS, sino que además una vez que la
DETS exista, nos permite abrirla y seguir usándola en otras ejecuciones.
Veamos los parámetros de la función para abrir o crear una DETS:
open_file(Name, Args) -> {ok, Name} | {error, Reason}
El parámetro Name será el que le dé nombre a la DETS. El nombre debe
de ser un átomo como ocurre con las ETS. Este dato será el que se solicite
en el resto de funciones para poder acceder a la DETS.
Las opciones que se pueden agregar como segundo parámetro son las
siguientes:
{access, Access}
Como acceso son válidos los valores read o read_write, siendo este
último el que se toma por defecto.
{auto_save, AutoSave}
Se indica un valor entero que indica el intervalo de autoguardado de
la DETS. Es decir, el tiempo en el que se realiza una sincronización
entre lo que se mantiene en memoria y el disco. Si se especifica
infinity, el autoguardado es deshabilitado. La opción por defecto es
180000 (3 minutos).
{min_no_slots, Slots}
Es un ajuste de rendimiento que permite especificar en la creación
de la tabla el número de claves estimado que serán almacenadas.
El valor por defecto es 256.
{max_no_slots, Slots}
El número máximo de slots que será usado. El valor por defecto y
máximo permitido es de 32000000.
{keypos, Pos}
La posición dentro del término en el que se encontrará la clave de
la tupla.
{file, File}
El nombre del fichero que se usará. Por defecto se toma el nombre
de la DETS para la escritura del fichero.
96
ETS, DETS y Ficheros
{ram_file, boolean()}
Si la DETS se mantendrá en memoria. Esto quiere decir que la
DETS se copia íntegramente a la memoria al momento de abrirla
realizando el volcado a disco cuando se cierra. Por defecto esta
característica no está activa (false).
{repair, true | false | force}
Le dice al sistema que debe ejecutar la reparación de la DETS al
abrirla, en caso de que no se hubiese cerrado correctamente. Por
defecto está activa (true). En caso de que se indique false y se
requiera reparación, se retornará el error:
{error, {needs_repair, File}}
El valor force quiere decir que la reparación se llevará a cabo aunque
la tabla haya sido cerrada correctamente.
{type, Tipo}
El tipo de la tabla, tal y como vimos en la sección anterior.
Importante
Para no perder información, es importante que siempre cerremos
la DETS de forma apropiada, a través de la función close/1. Si
no lo hacemos, al volver a ejecutar el programa, es seguro que
se requerirá una reparación del fichero e incluso podrían llegar a
perderse datos.
Veamos un ejemplo de apertura de un par de tablas:
> dets:open_file(bolsa, [{type, bag}, {file, "bolsa.dat"}]).
{ok,bolsa}
> dets:info(bolsa).
[{type,bag},
{keypos,1},
{size,0},
{file_size,5464},
{filename,"bolsa.dat"}]
> dets:info(bolsa, file_size).
5464
> dets:open_file(conjunto, [{type, set}, {access, read}]).
{error,{file_error,"conjunto",enoent}}
Al igual que con las ETS, las funciones info/1 e info/2, nos
proporcionan información sobre la DETS abierta dado su nombre.
La última apertura, como se puede ver, nos origina un error. Esto es
debido a que se ha intentado abrir un fichero llamado conjunto que no
97
ETS, DETS y Ficheros
existe. Como el acceso que se da es de sólo lectura (read), no se le está
capacitando para crear el fichero y por tanto se origina el error.
Importante
Si se intenta abrir el mismo fichero desde dos partes diferentes
del código con los mismos parámetros, el fichero es abierto sin
problemas pero sólo la primera vez, el segundo usa esta primera
instancia. Si alguna de las partes cerrase el fichero, como la
instancia tiene reflejado dos usos, se mantiene abierta hasta que
la otra parte también cierre el fichero.
2.3. Manipulación de las DETS
Si nos fijamos en las funciones disponibles para las DETS, veremos que
son prácticamente iguales que las que hay disponbiles para las ETS. Por
tanto, damos por hecho que el comportamiento de cara al programador
debe de ser el mismo.
Como todas las funciones son iguales y se pueden emplear funciones
como lookup/2, delete/2, delete_all_objects/1, first/1,
last/1, next/2, prev/2, ...; se puede repasar el apartado de Lectura y
Escritura de ETS, así como el de búsquedas avanzadas y la eliminación
de tuplas.
2.4. De ETS a DETS y viceversa
Aunque ambas estructuras están optimizadas para sus respectivos
trabajos, la manipulación de las entidades de memoria siempre resulta
más rápido que las de disco. Por esta razón puede haber momentos en los
que, aunque se tenga almacenado todo en una DETS por la persistencia,
se quiera por motivos de rendimiento utilizar una ETS para trabajar y a la
hora de almacenar los datos a disco, volver a trabajar con una DETS.
Para esto se utilizan las funciones to_ets/2 y from_ets/2. Su
definición:
to_ets(Name, EtsTab) -> EtsTab | {error, Reason}
Con el uso de esta función los datos de la DETS son volcados en la ETS.
Cabe destacar que es necesario haber abierto la DETS con anterioridad.
Los datos que contenga la ETS previamente no se eliminan, a menos que
colisionen con los que vienen de la DETS en cuyo caso se sobreescribirán.
La especificación de la función from_ets/2 es:
from_ets(Name, EtsTab) -> ok | {error, Reason}
98
ETS, DETS y Ficheros
En este caso, la DETS sí es vaciada (se eliminan todos sus elementos) y a
continuación se inserta la ETS tal cual dentro de la DETS.
Nota
En ambos casos, el orden en el que se guardan los elementos es
indeterminado, tanto de DETS a ETS, como en el caso opuesto.
3. Ficheros
En este apartado revisaremos lo que se puede hacer desde Erlang con
los ficheros. Para ello, vamos a diferenciar el tratamiento de los ficheros
entre los dos tipos existentes: binarios y de texto.
Los ficheros de texto tienen un tratamiento especial, ya que se
interpretan algunos caracteres especiales como fin de fichero, salto de
línea, e incluso se pueden tomar ficheros formateados de cierta forma
para que se carguen como formas de datos de Erlang.
En cambio, los ficheros binarios, tienen un tamaño definido y todos sus
bytes son iguales, es decir, no tienen ningún significado especial, aunque
se pueden agrupar para definir formas de datos estructuradas.
3.1. Abriendo y Cerrando Ficheros
Cualquier fichero que haya en un sistema de ficheros al que tenga acceso
Erlang es susceptible de poder ser abierto. También podemos generar
nuevos ficheros dentro de ese mismo sistema de ficheros.
Los ficheros que podemos abrir o crear son ficheros que pueden
contener texto, imágenes, audio, vídeo, documentos formateados como
los RTF, o ficheros binarios de cualquier otro tipo.
La función de que disponemos para poder abrir o crear ficheros es la
siguiente:
open(Filename, Modes) -> {ok, IoDevice} | {error, Reason}
Como primer parámetro tenemos el nombre del fichero (Filename), y el
segundo parámtro corresponde a una lista en la que se pueden agregar
tantas opciones de las siguientes como se necesite:
read | write | append | exclusive
El fichero se puede abrir en modo de lectura (read), escritura (write)
o para agregación al final (append). El caso de exclusive, es usado
para crear el fichero, devolviendo un error en caso de que ya exista.
99
ETS, DETS y Ficheros
raw
Abre el fichero mucho más rápido, ya que ningún proceso de Erlang
se encarga de manejar el fichero. Sin embargo, este modo de trabajo
tiene limitaciones, como que las funciones del módulo io no pueden
ser empleadas o que sólo el proceso que haya abierto el fichero
puede utilizarlo.
binary
Las operaciones de lectura retornarán listas binarias en lugar de
listas.
{delayed_write, Size, Delay}
Los datos se mantienen en un buffer hasta que se alcanza el tamaño
indicado por Size o hasta que el dato más antiguo en el buffer es de
más allá del tiempo especificado en Delay, entonces se escriben a
disco. Esta opción se emplea para decrementar los accesos a disco
y, por lo tanto, intentar incrementar el rendimiento del sistema.
{read_ahead, Size}
Activa el buffer de lectura para las operaciones de lectura que son
inferiores al tamaño definido en Size. Igual que en el caso anterior,
decrementa el número de llamadas al sistema para acceso a disco,
por lo que aumenta el rendimiento.
compressed
Crea o abre ficheros comprimidos con gzip. Esta opción puede
combinarse con read o write, pero no ambas.
{encoding, Encoding}
Realiza la conversión automática de caracteres para y desde un
tipo específico. La codificación por defecto es latin1. Las tablas
de codificación permitidas se pueden revisar en la documentación
4
oficial de la función open/2 .
5
Un ejemplo de apertura de un fichero tan famoso como /etc/
debian_version, para lectura o escritura y el resultado obtenido:
> file:open("/etc/debian_version", [read]).
{ok,<0.52.0>}
> file:open("/etc/debian_version", [write]).
{error,eacces}
4
http://www.erlang.org/doc/man/file.html#open-2
Para los que usan Debian o Ubuntu, o alguna distribución derivada de estas, es frecuente encontrar el
fichero /etc/debian_version en el sistema de ficheros.
5
100
ETS, DETS y Ficheros
Nota
Vemos que el retorno de la primera operación que se realiza
correctamente, nos devuelve un PID. Al no haber empleado la
opción raw se crea un proceso Erlang intermedio que se encarga
de la información del fichero y de realizar los accesos de lectura
y escritura.
Al intentar abrir un fichero para escritura hemos obtenido un error de
acceso (eaccess) debido a la falta de permisos, ya que un usuario normal
no tiene permisos para escribir en ese fichero.
Para cerrar el fichero, y con ello liberar el proceso que se mantiene a la
espera de indicaciones para tratar dicho fichero, debemos de emplear la
función close/1. En los ejemplos anteriores, sería hacer lo siguiente:
> {ok, Pid} = file:open("/etc/debian_version", [read]).
{ok,<0.34.0>}
> file:close(Pid).
ok
Importante
Es importante que cerremos todos los ficheros que abramos ya
que esto repercute, no sólo en un uso innecesario de los recursos
6
de los descriptores de ficheros , sino también de procesos, ya que
cada fichero abierto de un modo no raw lleva asociado un proceso
Erlang.
3.2. Lectura de Ficheros de Texto
Los ficheros de texto son los que el sistema interpreta dando un
significado concreto a ciertos caracteres especiales. El caracter de avance
de línea (\n), es tomado como un salto de línea y el caracter de retorno de
carro (\r), en caso de ir seguido al avance de línea, es ignorado y tomado
como parte del salto de línea.
Con esta consideración, funciones como read_line/1, se encargan de
leer una línea del fichero. Leen tantos bytes como sean necesarios hasta
llegar a un salto de línea o el final del fichero.
Como el fichero que vimos en el ejemplo de apertura de ficheros es
de tipo texto, podemos ir leyendo línea a línea mediante la función
read_line/1 hasta el fin de fichero y luego cerrarlo, como se ve en el
siguiente ejemplo:
6
Son las estructuras del sistema operativo que se emplean para designar que un programa tiene un
fichero abierto.
101
ETS, DETS y Ficheros
> {ok, Pid} = file:open("/etc/motd", [read]).
{ok,<0.34.0>}
> file:read_line(Pid).
{ok,"Linux barbol 3.1.0-1-amd64 Tue Jan 10 05:01:58 UTC..."}
> file:read_line(Pid).
{ok,"\n"}
> file:read_line(Pid).
{ok,"The programs included with the Debian GNU/Linux..."}
> file:read_line(Pid).
{ok,"the exact distribution terms for each program are..."}
> file:read_line(Pid).
{ok,"individual files in /usr/share/doc/*/copyright.\n"}
> file:read_line(Pid).
{ok,"\n"}
> file:read_line(Pid).
{ok,"Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY..."}
> file:read_line(Pid).
{ok,"permitted by applicable law.\n"}
> file:read_line(Pid).
eof
> file:close(Pid).
ok
Si lo que queremos es leer todo el contenido del fichero para
almacenarlo en una variable de texto, podemos emplear la función
read_file/1:
> file:read_file("/etc/debian_version").
{ok,<<"wheezy/sid\n">>}
Nota
Al igual que hemos empleado read_line/1, podemos emplear
io:get_line/2 para realizar la lectura, pasando como primer
parámetro el identificador del fichero abierto.
Por último, si el contenido del fichero que queremos leer contiene
elementos de Erlang, como términos o listas, separados por un punto
cada uno de los elementos base que conforman el documento, este
puede ser leído y evaluado como datos Erlang directamente.
Esto se realiza con al función consult/1. Esta función puede leer un
fichero como el que se muestra a continuación:
{nombre, "Manuel"}.
{apellido1, "Rubio"}.
{apellido2, "Jimenez"}.
Leyendo este fichero (datos_personales.cfg) desde consola:
> file:consult("datos_personales.cfg").
102
ETS, DETS y Ficheros
{ok,[{nombre,"Manuel"},
{apellido1,"Rubio"},
{apellido2,"Jimenez"}]}
Con esta sintaxis crearemos archivos que podamos emplear como
configuración, parametrización o salvaguarda de información, cuyos
datos podemos rescatar en cualquier momento.
3.3. Escritura de Ficheros de Texto
La escritura de ficheros de texto la podemos realizar simplemente a
través de las funciones format/3 o write/2. La primera función nos
permite formatear el texto que será escrito en el fichero tal y como se
haría en la pantalla, teniendo en cuenta los saltos de línea y los espacios;
la segunda nos permite escribir sólo lo que contenga la lista de caracteres
que enviamos como segundo parámetro.
Si nuestro fichero debe disponer de un formato específico (espacios,
saltos de línea, formato de números), puede ser más fácil formatearlo a
través de la función format/3, tal y como hacíamos con la pantalla, por
ejemplo así:
> {ok, Pid} = file:open("mifile.txt", [write]).
{ok,<0.42.0>}
> io:format(Pid,"~nSaldo: ~6.2f~nTotal: ~6.2f~n",[12.3,20.1]).
ok
> file:close(Pid).
ok
Si por contra lo que queremos es simplemente escribir un texto ya
almacenado dentro de una cadena, podemos simplificar empleando la
función write/2 tal y como se ve en este ejemplo:
> {ok, Pid} = file:open("mifile.txt", [write]).
{ok,<0.42.0>}
> file:write(Pid, "fichero de texto").
ok
> file:close(Pid).
ok
También es posible utilizar ambos métodos realizando escrituras
combinadas, es decir, primero una y después la otra, tantas veces como
queramos o necesitemos.
3.4. Lectura de Ficheros Binarios
La lectura de estos archivos la realizaremos expresamente con la función
read/2. Como primer parámetro emplearemos el identificador para el
103
ETS, DETS y Ficheros
fichero abierto y, como segundo parámetro, el tamaño del fichero que
será leído.
Aunque podemos emplear otras funciones, esta es la más genérica y nos
permitirá realizar las lecturas de los ficheros sin problemas. Si queremos
leer la totalidad del fichero es más aconsejable emplear la función
read_file/1, ya que nos auna la apertura, lectura y cierre del fichero
en una sola función.
Un ejemplo de lectura de una imagen sería la siguiente:
> Filename = "/usr/share/pixmaps/debian-logo.png".
"/usr/share/pixmaps/debian-logo.png"
> {ok, Pid} = file:open(Filename, [read,binary]).
{ok,<0.34.0>}
> file:read(Pid, 16).
{ok,<<137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82>>}
> file:close(Pid).
ok
Los bytes leídos de nuestro fichero binario imagen se muestran como una
lista binaria de enteros. En concreto hemos leído 16 bytes del principio
del archivo PNG. Podemos leer 8 bytes que conforman la firma del PNG
y los 8 que contienen la cabecera del PNG en la siguiente forma (como
7
podemos ver en la wikipedia ):
> Filename = "/usr/share/pixmaps/debian-logo.png".
"/usr/share/pixmaps/debian-logo.png"
> {ok, Pid} = file:open(Filename, [read,binary]).
{ok,<0.34.0>}
> {ok, <<137,"PNG",13,10,26,10>>} = file:read(Pid, 8).
{ok,<<137,80,78,71,13,10,26,10>>}
> {ok, <<Length:32, "IHDR">>} = file:read(Pid, 8).
{ok,<<0,0,0,13,73,72,68,82>>}
> {ok, <<Width:32, Height:32, Depth:8, Color:8,
>
Compression:8, Filter:8, Interlace:8>>}
> = file:read(Pid, Length).
{ok,<<0,0,0,48,0,0,0,48,8,6,0,0,0>>}
> io:format("Image ~bx~b pixels~n", [Width,Height]).
Image 48x48 pixels
ok
> file:close(Pid).
ok
Siguiendo las directrices de la especificación de los ficheros PNG, hemos
podido extraer, gracias a las listas binarias y al tratamiento que se puede
realizar con los bits, el tamaño de la imagen y muchos otros datos que
podríamos también pasar por pantalla.
Es aconsejable realizar el tratamiento de ficheros binarios siempre a
través de listas binarias. En el ejemplo de la lectura del PNG hemos
7
http://en.wikipedia.org/wiki/Portable_Network_Graphics
104
ETS, DETS y Ficheros
visto cómo se desempaqueta el entramado de bytes para obtener la
información codificada en el fichero. Teniendo la definición de otros
tipos de documentos binarios se podría hacer lo mismo para ficheros de
audio como los WAV o MP3, o para ficheros de vídeo como los AVI o MOV.
3.5. Escritura de Ficheros Binarios
La escritura de ficheros binarios se realiza con la función write/2 igual
que hemos hecho con los archivos de texto. La diferencia radica en que
el parámetro que se envía para ser escrito no es una lista de caracteres,
sino una lista binaria.
El ejemplo más simple, sería la copia de un fichero. Abrimos el fichero
que queremos leer y el que queremos escribir para lectura y escritura
respectivamente. A continuación hacemos una lectura completa del
fichero con la función read_file/1:
> {ok, Contenido} = file:read_file("logo.png",[read,binary]).
{ok,<<137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,
0,48,0,0,0,48,8,6,0,...>>}
2> {ok, Destino} = file:open("logo2.png", [write,binary]).
{ok,<0.35.0>}
3> file:write(Destino, Contenido).
ok
4> file:close(Destino).
ok
3.6. Acceso aleatorio de Ficheros
Hasta ahora hemos eliminado el contenido de los archivos que hemos
escrito. Si lo que quisiéramos es modificar un archivo binario (en nuestro
ejemplo es modificar la cabecera PNG), la apertura del fichero debería
agregar el parámetro read además de write, tal y como se ve en este
ejemplo:
file:open("file.bin", [read,write,binary])
Para modificar una parte específica del fichero, habrá que desplazar el
puntero al punto exacto donde queremos escribir. Esto es lo que se
conoce como escrituras y/o lecturas aleatorias (o no secuenciales).
La función que permite realizar este tipo de movimientos por el fichero
es position/2 (del módulo file). Esta función nos permite desplazarnos
por el fichero a posiciones absolutas (o relativas al principio del fichero,
también llamado bof, o begin of file), relativas a la posición actual: cur; o
relativas al final del fichero: eof.
Los parámetros que podemos emplear son:
105
ETS, DETS y Ficheros
> {ok, Pid} = file:open("logo.png", [binary,write,read]).
{ok,<0.99.0>}
> file:position(Pid, 1024).
{ok,1024}
> file:position(Pid, {cur, -24}).
{ok,1000}
> file:position(Pid, eof).
{ok,1718}
> file:position(Pid, {eof, 24}).
{ok,1742}
> file:position(Pid, {eof, -24}).
{ok,1694}
> file:position(Pid, bof).
{ok,0}
> file:close(Pid).
ok
Vemos que incluso podemos desplazarnos más allá del tamaño del
fichero (que en ese caso es de 1718 bytes), debido a que hemos abierto
el fichero para escritura y se nos permite agregar más información para
ampliar el tamaño del fichero.
3.7. Lecturas y Escrituras por Lotes
Erlang tiene muchos mecanismos para optimizar al máximo posible las
operaciones que se realicen con él. Como las lecturas aleatorias son
lentas, sobretodo si hay que llamar a la misma función una y otra vez con
desplazamientos del puntero del fichero, el sistema interno de Erlang
nos facilita esta labor proporcionando un par de funciones para realizar
estas lecturas o escrituras por lotes. Estas funciones son pwrite/2 y
pread/2.
Estas funciones están pensadas para cuando tenemos la información
en un fichero pero al mismo tiempo en memoria. Lo que ocurre es que
trabajamos en memoria almacenando un log de cambios y los pasamos
al fichero de una vez cada cierto tiempo.
Estas anotaciones de cambios pueden ser del tipo:
[
{{bof, 0}, <<137,"PNG">>},
{{eof, -24}, <<0,0,0,0>>},
{{bof, 12}, <<"IHDR">>}
]
Con esta secuencia indicamos la posición y lo que deseamos escribir
en cada punto. Esto se pasaría como segundo parámetro en la función
pwrite/2.
De forma análoga, también se puede dar la lista de direcciones para
realizar las lecturas oportunas, es decir, para pread/2, de la siguiente
forma:
106
ETS, DETS y Ficheros
[
{{bof,0}, 4},
{{eof, -24}, 4},
{{bof, 12}, 4}
]
En este caso, en lugar de especificar contenido para escribir,
especificamos el tamaño para leer a partir de la posición dada.
Importante
En caso de emplear listas de caracteres, hay que tener especial
cuidado con los caracteres de UTF-8, ya que algunos emplean
dos bytes para su almacenaje en lugar de sólo uno y esto
puede provocar que el cómputo de la posición sea erróneo (o
susceptible de errores).
Para evitar estos errores, aconsejo emplear el acceso aleatorio
tan sólo en ficheros binarios, ya que las unidades están mejor
definidas en este caso.
4. Gestión de Ficheros
Además de todo lo visto anteriormente para la creación, modificación
y lectura de un fichero, podemos realizar más acciones aún con
estos ficheros, como puede ser: renombrarlos, cambiar sus permisos,
8
propietario , copiar el fichero, truncarlo o eliminarlo.
4.1. Nombre del fichero
Doy por supuesto que todos conocemos que, los nombres de los ficheros
se componen por la ruta en la que se ubica el fichero, su nombre
y extensión. En Erlang, el módulo filename nos permite obtener la
información correspondiente a un nombre de fichero: su ruta, su nombre,
su nombre raíz (sin extensión) y/o su extensión.
Para esto, disponemos de varias funciones:
> Filename = "/home/bombadil/logo.png".
"/home/bombadil/logo.png"
> filename:basename(Filename).
"logo.png"
> filename:rootname(Filename).
"/home/bombadil/logo"
> filename:dirname(Filename).
"/home/bombadil"
> filename:extension(Filename).
8
El cambio de permisos y propietario depende de cada sistema operativo y los permisos en sí que tenga
el usuario que lanzó la ejecución del programa.
107
ETS, DETS y Ficheros
".png"
Como en otros lenguajes basename/1 nos retorna el nombre del fichero
sin ruta. dirname/1 nos devuelve la ruta sin el nombre del fichero.
También tenemos rootname/1 que nos retorna el nombre del fichero
(con ruta si dispone de ella) y extension/1 que nos da únicamente la
extensión (con el punto incluído).
También disponemos de la función absname/1 que retorna siempre
el nombre del fichero de forma absoluta. Si le pasamos una ruta
relativa o un fichero sin ruta, obtenemos un nombre de fichero absoluto,
completado con la ruta de trabajo actual:
> filename:absname("logo.png").
"/home/bombadil/logo.png"
4.2. Copiar, Mover y Eliminar Ficheros
Una de las acciones básicas cuando se trata con ficheros son estas tres
que encabezan la sección actual. Para estas acciones Erlang provee de
tres funciones: copy/2, rename/2 y delete/1.
Un ejemplo de cómo podemos emplear estas funciones:
> file:copy("logo.png", "logo2.png").
{ok,1718}
> file:rename("logo2.png", "milogo.png").
ok
> file:delete("milogo.png").
ok
La función de rename/2, además de para cambiar el nombre del fichero,
nos puede servir para cambiar la ubicación del fichero si indicamos una
ruta distinta, por ejemplo:
file:rename("logo.png", "/tmp/logo.png")
Nota
Las operaciones se realizan sobre ficheros específicos, no sobre
grupos de ficheros como los comandos de consola de los sistemas
operativos, por lo que el uso de comodines como asterisco (*) o
interrogante (?) no se tienen en cuenta como tal, sino que son
interpretados como parte del nombre del fichero.
En caso de que no queramos eliminar un fichero (porque un programa lo
tenga abierto o por otro motivo) y sólo queramos eliminar su contenido
108
ETS, DETS y Ficheros
y reiniciar sus punteros a la posición cero, esto lo podemos realizar
mediante el uso de la función truncate/1.
4.3. Permisos, Propietarios y Grupos
Otro de los aspectos relevantes cuando gestionamos ficheros, son sus
permisos y su pertenencia a un usuario o grupo. El cambio de los
permisos se puede realizar mediante la función change_mode/2. El
cambio de propietario se hace mediante change_owner/2 y el cambio
de grupo a través de change_group/2.
La función change_mode/2 permite cambiar los permisos del fichero.
Como primer parámetro se pasa el nombre del fichero y como segundo
parámetro el modo que se desea establecer, en modo numérico. En modo
octal, tenemos esta tabla de permisos:
Valor numérico
Permiso
Usuario
8#00400
Lectura
Propietario
8#00200
Escritura
Propietario
8#00100
Ejecución
Propietario
8#00040
Lectura
Grupo
8#00020
Escritura
Grupo
8#00010
Ejecución
Grupo
8#00004
Lectura
Otros
8#00002
Escritura
Otros
8#00001
Ejecución
Otros
Por lo que si queremos que el fichero logo.png tenga permisos de
lectura y escritura para su propietario y lectura para el grupo y otros,
tendremos que ejecutar:
file:change_mode("logo.png", 8#00644)
El cambio de propietario se puede realizar a través de la función
change_owner/2 o change_owner/3 si además queremos cambiar el
9
grupo. Los parámetros de UID y GID se dan en formato entero .
Hay una función que engloba todas las funciones del módulo file
para la gestión de usuarios, grupos y permisos y permite realizar
9
En los sistemas de tipo Unix este dato se puede ver en /etc/passwd donde hay una correspondencia
entre el nombre del usuario y su UID.
109
ETS, DETS y Ficheros
todas las modificaciones en una sola acción, tanto para la lectura:
read_file_info/1; como para la escritura: write_file_info/2.
Vamos a verlas un poco más en detalle:
read_file_info/1
Permite leer las propiedades de un fichero retornando un registro
en el que aparecen datos como la fecha y hora de creación, fecha y
hora de modificación y fecha y hora del último acceso, además de
los permisos, tipo de fichero y tamaño del mismo. Por ejemplo:
> file:read_file_info("logo.png",size).
{ok,#file_info{size=1718,type=regular,
access=read_write,
atime={{2012,7,18},{14,23,1}},
mtime={{2012,7,18},{14,23,1}},
ctime={{2012,7,18},{14,23,1}},
mode=33188,links=1,major_device=2049,
minor_device=0,
inode=11150880,uid=1000,gid=1000}}
write_file_info/2
Permite modificar cualquiera de los datos del fichero, para ello,
se debe de especificar, como segundo parámetro, un registro de
tipo file_info y rellenarlo con los datos del fichero que deseemos
modificar.
5. Gestión de Directorios
Hasta el momento hemos visto como trabajar con ficheros, su contenido
ya sea de tipo texto o de tipo binario, así como la gestión propia de
los ficheros (copia, renombrado, eliminación, ...), ahora vamos a tratar la
gestión de los directorios.
Los directorios nos permiten organizar nuestros ficheros de una forma
más categorizada. Para sistemas que trabajan con miles de ficheros esto
no es una opción sino una necesidad, ya que, en el terreno informático
no hay recursos infinitos e incluso el número de ficheros que pueden
10
albergarse en un directorio está limitado .
Como la gestión de los directorios, e incluso la de los ficheros se puede
realizar desde un programa, un sistema que cree muchos ficheros puede
particionar estos en directorios y subdirectorios, de modo que el acceso
a cada directorio sea más rápido que en el caso de tener un directorio
con miles de ficheros.
10
Hay sistemas de ficheros que establecen este límite a 1024 y otros que permiten miles o millones
de ficheros por directorio, pero esto no es nada aconsejable, ya que el tratamiento y gestión del propio
directorio o de los propios ficheros puede ser extremadamente lento.
110
ETS, DETS y Ficheros
Veremos a continuación las funciones relativas a la gestión de directorios
bajo los conceptos en los que se emplean.
5.1. Directorio de Trabajo
Las rutas que indicamos para los nombres de ficheros las podemos
indicar de forma absoluta o relativa tanto para su apertura como para
su gestión. La ruta absoluta nos indica dónde se encuentra un fichero
mientras que la ruta relativa se basa en la ruta activa en la que se esté
trabajando.
Erlang establece una ruta de trabajo que puede ir cambiando a través de
llamadas específicas al sistema. La ruta de trabajo podemos extraerla con
get_cwd/0 del módulo file. Cualquier referencia a fichero que hagamos
de forma relativa será siempre relativa a esta ruta.
Podemos cambiar la ruta de trabajo mediante la función set_cwd/1,
donde especificamos cuál será la nueva ruta de trabajo. Esto afectará a
todas las rutas relativas que se empleen a partir del cambio.
Es un método bastante frecuente el cambiar la ruta para la ejecución
de un código específico y volver inmediatamente a la ruta anterior, algo
como esto:
Dir = file:get_cwd(),
file:set_cwd("/miruta"),
%% ejecuta_codigo...
file:set_cwd(Dir).
La ruta inicial se sitúa en el directorio en el que nos encontrásemos al
ejecutar la consola de Erlang.
5.2. Creación y Eliminación de Directorios
Una de las acciones básicas con los directorios es la de su creación y
eliminación. Comenzaremos con el primer caso, la creación. La creación
se puede indicar de forma absoluta o relativa. Un ejemplo:
file:make_dir("/home/bombadil/logos")
En sistemas como la shell de los sistemas tipo Unix, se permite realizar
el comando:
mkdir -p /miruta/nuevo1/nuevo2/midir
Con lo que no sólo se crea un directorio, sino todo los necesarios hasta
llegar al último indicado en la ruta pasada como parámetro. Esto no lo
realiza make_dir/1. Esta función debe de recibir una ruta existente y
111
ETS, DETS y Ficheros
creará el último directorio que se indique en la ruta, siempre que no
exista ya.
En caso de que quisiéramos crear un directorio con todos sus
directorios padres, en caso de que no existan, podemos emplear la
función ensure_dir/1 del módulo filelib. Esta función crea todos los
directorios necesarios para que la ruta exista.
Podemos ver un ejemplo:
> file:make_dir("/tmp/prueba/dir1").
{error,enoent}
> filelib:ensure_dir("/tmp/prueba/dir1/").
ok
Partimos de que /tmp/prueba no existe. Por este motivo la función
make_dir/1 no puede crear el directorio final, dir1. La función
ensure_dir/1, crea ambos directorios, primero prueba y después dir1
dentro de prueba.
Importante
El parámetro de ensure_dir/1 debe de terminar en barra para
que cree hasta el último directorio, ya que la función está creada
con la idea de que se pueda pasar como parámetro la ruta de un
fichero (con el nombre del fichero incluído) y cree el directorio
para albergar al fichero:
filelib:ensure_dir("/tmp/midir/logo.png")
5.3. ¿Es un fichero?
Para emplear en las guardas (o guards), al igual que disponemos de las
funciones is_list/1, podemos hacer uso de las funciones del módulo
filelib: is_dir/1 o is_file/1.
Esto nos permite realizar funciones que nos permitan realizar un proceso
previo de validación, por ejemplo, en caso de las configuraciones en las
que se nos proporciona una ruta:
temp_dir_config(Dir) when not is_dir(Dir) ->
ok = filelib:ensure_dir(Dir ++ "/"),
temp_dir_config(Dir);
temp_dir_config(Dir) ->
to_do.
Igualmente podemos emplear las funciones file_size/1 (tamaño del
fichero) o last_modified/1 (la última fecha de modificación) para
agregar más semántica o funcionalidad al código.
112
ETS, DETS y Ficheros
5.4. Contenido de los Directorios
Hay momentos en los que queremos tener un listado de todos los
ficheros que se encuentran dentro de un directorio, para realizar un
listado dentro del programa ya sea para tener un control del número de
ficheros que se van generando, para realizar búsquedas o por cualquier
otro motivo.
Una forma rápida de obtener los ficheros que queremos es emplear la
función list_dir/1, lo cual nos retorna una lista de todos los nombres
de ficheros que se encuentran en la ruta pasada como parámetro:
> file:list_dir("/home").
{ok,["bombadil","bayadeoro"]}
Otra forma de obtener los ficheros que nos interesan que podemos
encontrar dentro del módulo filelib es a través de la función
wildcard/1, la cual nos permite, no sólo poner una ruta, sino además
emplear los comodines para obtener los ficheros que concuerden:
> filelib:wildcard("/home/bombadil/*.png").
["/home/bombadil/logo.png"]
Con estos listados, a través de funciones sobre listas como map/2,
podemos realizar un procesado individualizado de los ficheros que nos
retornen las funciones. Por ejemplo, si queremos extraer, además del
nombre del fichero el tamaño y mostrarlo:
> lists:map(fun(X) ->
>
io:format("~-30s ~w~n", [X,filelib:file_size(X)])
> end, filelib:wildcard("/home/bombadil/*.png")).
/home/bombadil/logo.png
1718
Esto nos abre una cantidad de posibilidades para obtener información
de los ficheros albergados en un directorio, o incluso para poder recorrer
directorios a través de funciones recursivas, en las que poder emplear
las guardas vistas referentes a los ficheros.
113
Capítulo 7. Comunicaciones
No hay lugares remotos. En virtud de los medios de
comunicación actuales, todo es ahora.
—Herbert Marshall Mcluhan
Uno de los principales cometidos de un servidor, es establecer puertos
de comunicación para recibir conexiones entrantes. La comunicación
se establece a varios niveles, empleando en cada uno de los niveles
un protocolo específico para la comunicación. En este capítulo nos
centraremos en el protocolo IP, TCP y UDP para la pila de conexiones más
popular: TCP/IP.
1. Conceptos básicos de Redes
Cuando se establece una conexión, la información que se percibe en el
más alto nivel (o nivel de aplicación), es una representación que se ha
ido resolviendo de un sistema de empaquetado anterior, encargado de
agregar información sobre el paquete y enviarlo a través de la red.
Las capas que se distinguen en el envío de información de un punto a
otro, en el modelo de Internet denominado TCP/IP, son:
Nivel físico
En este nivel se encuentran las conexiones físicas y sus protocolos
específicos, según la tecnología en uso: Ethernet, 802.11, Fibre
Channel, etc. A través de los drivers (o módulos del kernel) estos
protocolos son transparentes para las aplicaciones. El nivel físico
siempre se emplea punto a punto, cada máquina se conecta a través
de un cable o de forma inalámbrica con otra y establecen una
comunicación uno a uno.
Nivel de red
Es el nivel en el que se establece la base de la red, la identificación
de los sistemas y el transporte hacia los mismos. En este nivel y en el
alcance que nos hemos propuesto para este libro, sólo nos importa
el protocolo IP. Este protocolo proporciona una dirección dentro de
una red y permite establecer una comunicación a través de diversos
dispositivos hasta encontrar la dirección de la máquina que debe
recibir el mensaje.
Nivel de transporte
Este nivel es el que establece la forma de conexión entre las
máquinas, la forma en la que se envían y trocean los paquetes para
114
Comunicaciones
que lleguen a su destino y los acuses de recibo para asegurar de
que el paquete es recibido correctamente. En este nivel veremos
dos protocolos: TCP y UDP.
Nivel de aplicación
Este es el nivel más alto que podemos encontrar en comunicación.
Aquí se definen y usan protocolos como: HTTP, FTP, SMTP, POP3,
IMAP, etc.
1.1. Direcciones IP
Una dirección IP se representa mediante un número binario de 32 bits
(según IPv4). Las direcciones IP se pueden expresar como números de
notación decimal: se dividen los 32 bits de la dirección en cuatro octetos.
1
El valor decimal de cada octeto puede estar entre 0 y 255 .
En la expresión de direcciones IPv4 en decimal se separa cada octeto
por un punto. Cada uno de estos octetos puede estar comprendido
entre 0 y 255, salvo algunas excepciones. Los ceros iniciales, si los
hubiera, se pueden obviar. Ejemplo de representación de dirección IP:
164.12.123.65
Importante
Las direcciones IP en Erlang se emplean a través de un formato de
tupla formada por cuatro elementos enteros. Esta forma es en la
que generalmente trabaja el módulo inet que es el que se encarga
de las comunicaciones, tanto para conexiones cliente, como para
servidor:
{127,0,0,1}
Este sería el formato de IP para 127.0.0.1. La función inet:ip/1
nos permite realizar la conversión del formato de texto al formato
de tupla.
Hay tres clases de direcciones IP que una organización puede recibir
de parte de la Internet Corporation for Assigned Names and Numbers
(ICANN): clase A, clase B y clase C. En la actualidad, ICANN reserva
2
las direcciones de clase A para los gobiernos de todo el mundo y
las direcciones de clase B para las medianas empresas. Se otorgan
direcciones de clase C para todos los demás solicitantes. Cada clase de
red permite una cantidad fija de equipos (hosts).
1
El número binario de 8 bits más alto es 11111111 y esos bits, de derecha a izquierda, tienen valores
decimales de 1, 2, 4, 8, 16, 32, 64 y 128, lo que suma 255 en total.
2
Aunque en el pasado se le hayan otorgado a empresas de gran envergadura como, por ejemplo, Hewlett
Packard.
115
Comunicaciones
• En una red de clase A, se asigna el primer octeto para identificar la red,
reservando los tres últimos octetos (24 bits) para que sean asignados a
los hosts, de modo que la cantidad máxima de hosts es 224 menos dos:
las direcciones reservadas de broadcast (tres últimos octetos a 255) y
de red (tres últimos octetos a 0), es decir, 16.777.214 equipos.
• En una red de clase B, se asignan los dos primeros octetos para
identificar la red, reservando los dos octetos finales (16 bits) para
que sean asignados a los equipos, de modo que la cantidad máxima
de equipos es 216 (de nuevo menos dos), lo que equivale a 65.534
equipos.
• En una red de clase C, se asignan los tres primeros octetos para
identificar la red, reservando el octeto final (8 bits) para que sea
asignado a los equipos, de modo que la cantidad máxima de equipos
es 28 (menos dos), o 254 equipos.
• La dirección 0.0.0.0 es utilizada por las máquinas cuando están
arrancando o no se les ha asignado dirección.
• La dirección que tiene a cero su parte destinada a equipos sirve para
definir la red en la que se ubica. Se denomina dirección de red.
• La dirección que tiene a uno todos los bits de su parte de equipo sirve
para comunicar con todos los equipos de la red en la que se ubica. Se
denomina dirección de broadcast.
• Las direcciones 127.x.x.x se reservan para pruebas de
retroalimentación. Se denomina dirección de bucle local o loopback.
Hay ciertas direcciones en cada clase de dirección IP que no están
asignadas y que se denominan direcciones privadas. Las direcciones
privadas pueden ser utilizadas por los equipos que usan traducción de
dirección de red (NAT) para conectarse a una red pública o por los hosts
que no se conectan a Internet. En una misma red no pueden existir dos
direcciones iguales, pero sí se pueden repetir en dos redes privadas que
no tengan conexión entre sí directamente. Las direcciones privadas son:
• Clase A: 10.0.0.0 a 10.255.255.255 (8 bits red, 24 bits equipos)
• Clase B: 172.16.0.0 a 172.31.255.255 (16 bits red, 16 bits equipos)
• Clase C: 192.168.0.0 a 192.168.255.255 (24 bits red, 8 bits equipos)
Muchas aplicaciones requieren conectividad dentro de una sola red,
y no necesitan conectividad externa. En las redes de gran tamaño a
menudo se usa TCP/IP. Por ejemplo, los bancos pueden utilizar TCP/
IP para conectar los cajeros automáticos que no se conectan a la red
116
Comunicaciones
pública, de manera que las direcciones privadas son ideales para ellos.
Las direcciones privadas también se pueden utilizar en una red en la que
no hay suficientes direcciones públicas disponibles.
Las direcciones privadas se pueden utilizar junto con un servidor de
traducción de direcciones de red (NAT) para suministrar conectividad a
todos los equipos de una red que tiene relativamente pocas direcciones
públicas disponibles. Según lo acordado, cualquier tráfico que posea una
dirección destino dentro de uno de los intervalos de direcciones privadas
no se enrutará a través de Internet.
1.2. Puertos
Los puertos de comunicaciones son la base sobre la que se sustentan
los protocolos de transporte TCP y UDP. Estos protocolos establecen
conexiones salientes y entrantes en puertos denominados activos o
pasivos respectivamente.
Los puertos se representan como números en rango de 16 bits, que
pueden ir desde el 0 hasta el 65535. Los puertos por debajo del 1024
se denominan puertos privilegiados y en sistemas como los UNIX se
requieren permisos de super-usuario (o root) para poder emplear estos
3
puertos .
Nota
En la mayoría de sistemas operativos existe un fichero de texto
plano denominado services que contiene, formateado en dos
columnas: el nombre de servicio y el puerto que emplea dicho
servicio. La columna del puerto, además, viene formateada de
forma que se indica el número, una barra inclinada y el tipo de
transporte que se emplea:
http
http
ftp-data
ftp
domain
domain
80/tcp
80/udp
20/tcp
21/tcp
53/tcp
53/udp
Cada protocolo de transporte puede hacer uso del rango de
numeración sin colisionar con ningún otro. TCP puede hacer uso
del puerto 22 e igualmente UDP podría usar el mismo sin provocar
colisión. La asignación de puertos es realizada por los protocolos
de transporte, cada uno mantiene su propia numeración.
3
Esto es debido también a que la mayoría de servicios que se prestan están en este rango, de modo que
en un servidor un usuario sin privilegios no pueda establecer un puerto pasivo en el puerto 80 (dedicado
a HTTP), 25 (de SMTP), 22 (de SSH) o 21 (de FTP) entre otros.
117
Comunicaciones
El puerto activo toma de la numeración de puertos un número y lo reserva
para establecer la comunicación con el puerto pasivo. El número del
puerto activo puede ser elegido o no dependiendo del protocolo de
transporte.
La comunicación se identifica para el sistema operativo con el par de
puertos activo/pasivo además de la dirección IP tanto de origen como de
destino. Esto hace posible que a un puerto pasivo se pueda conectar más
de un puerto activo.
Nota
En la jerga de los sistemas de comunicación existen algunas
palabras clave que se emplean para determinar ciertos aspectos
de la comunicación o los elementos que la componen. La palabra
anglosajona socket (traducida como zócalo o conector) es la
palabra que se suele emplear para indicar una conexión. Cuando
se establece un puerto pasivo mediante TCP se suele decir que el
servidor escucha mientras que si es mediante UDP se dice que el
servidor está enlazado a ese puerto.
En el esquema cliente-servidor existe un servidor que se mantiene a la
espera de una petición entrante y un cliente de forma activa realizas
las peticiones al servidor. El servidor emplearía un puerto pasivo para
establecer la comunicación mientras que el cliente usaría uno activo.
Como hemos ido comentando a lo largo de la sección hay dos protocolos
para transporte que emplean los puertos de comunicación:
TCP
Es orientado a conexión. Requiere que el cliente formalice la
conexión con el servidor y esta se mantiene hasta que una de las
dos partes solicita la desconexión o durante un envío se produzca
un tiempo de espera agotado.
UDP
UDP es un protocolo de datagramas. Un datagrama puede ser
enviado hacia un servidor pero no se comprueba el estado de
recepción. No se espera respuesta del mismo. El tratamiento de este
tipo de paquetes carga menos la red y los sistemas operativos pero
si la red no es fiable puede existir pérdida de información.
2. Servidor y Cliente UDP
Erlang provee un módulo llamado gen_udp que nos permite establecer
un puerto pasivo para mantenernos enlazados y recibir paquetes
118
Comunicaciones
entrantes. También permite emplear un puerto activo para el envío de
un paquete hacia un servidor que se mantenga enlazado. Resumiendo,
permite tanto programar servidores como clientes UDP.
En UDP tanto cliente como servidor deben enlazar un puerto. El servidor
lo hará de forma pasiva para recibir mensajes. El cliente lo hará de
forma activa para enviarlos y tener la posibilidad de recibir respuesta del
servidor.
El módulo gen_udp aprovecha las capacidades propias de los procesos
enviando cada paquete recibido al proceso que solicitó el enlace con el
puerto.
Hay tres funciones que emplearemos con mucha frecuencia en la
construcción de servidores y clientes UDP: open/1 o open/2, send/4
y close/1.
La función open/2 se presenta:
open(Port, Opts) -> {ok, Socket} | {error, Reason}
Puede recibir como segundo parámetro opciones que permiten variar la
forma en la que establecer el enlace con el puerto. Las opciones son:
list | binary
Indica si se quiere recibir el paquete como una lista o un binario. El
valor por defecto es list.
{ip | ifaddr, ip_address()}
La dirección IP en la que enlazar el puerto.
inet | inet6
Emplea IPv4 o IPv6 para las conexiones. El valor por defecto es inet.
{active, true | false | once}
Indica si todos los mensajes recibidos por red serán pasados al
proceso (true) o no (false) o si se hará sólo la primera vez (once). El
valor por defecto es true.
{reuseaddr, true | false}
Permite reutilizar el puerto. No lo bloquea. Por defecto esta opción
está deshabilitada.
119
Comunicaciones
Nota
Hay disponibles muchas opciones más a las que no entraremos ya
que son conceptos más avanzados o muy específicos, con lo que
salen del ámbito de explicación de este capítulo. Si desea más
información sobre la función gen_udp:open/2 puede revisar la
siguiente dirección:
http://www.erlang.org/doc/man/gen_udp.html#open-2
Pondremos en práctica lo aprendido. Vamos a escribir un módulo que
enlace el puerto 2020 y cada paquete que reciba lo pase por pantalla:
-module(udpsrv).
-export([start/1, init/1, loop/1]).
-record(udp, {socket, ip, port, msg}).
start(Port) ->
spawn(?MODULE, init, [Port]),
ok.
init(Port) ->
{ok, Socket} = gen_udp:open(Port),
loop(Socket).
loop(Socket) ->
receive
stop ->
gen_udp:close(Socket);
Packet when is_record(Packet, udp) ->
io:format("recibido(~p): ~p~n", [
Packet#udp.ip, Packet#udp.msg
]),
#udp{ip=IP,port=Port} = Packet,
gen_udp:send(Socket, IP, Port, "recibido"),
loop(Socket)
end.
El código se inicia mediante la función start/1 indicando un número
entero correspondiente al puerto enlazado. Lanza un nuevo proceso que
ejecuta la función init/1 encargada de abrir el puerto y pasar el control
a la función loop/1 que se encarga de atender los paquetes que vayan
llegando.
En el ejemplo hemos usado un registro formado por los cuatro datos que
nos envía cada paquete UDP además del identificador:
{udp, Socket, IP, InPortNo, Packet}
La definición de estos datos es la siguiente:
120
Comunicaciones
Socket
El manejador retornado por la función open/1 u open/2.
IP
La dirección IP en formato de tupla.
InPortNo
El puerto origen del paquete recibido.
Packet
El paquete recibido.
Podemos abrir una consola de Erlang y lanzar el servidor. Para saber que
el puerto se encuentra enlazado emplearemos la función inet:i/0 que
nos proporciona información sobre las comunicaciones:
> udpsrv:start(2020).
ok
> inet:i().
Port [...] Recv Sent Owner
Local Address [...] State Type
593 [...] 0
0
<0.34.0> *:2020
[...] BOUND DGRAM
ok
La salida nos muestra el proceso (Owner) que tiene enlazado (State =:=
BOUND) el puerto 2020 (Local) de tipo UDP (Type =:= DGRAM). Nos
proporciona también otros datos estadísticos como los bytes recibidos
(Recv) y enviados (Sent).
Podemos escribir estas líneas en la consola de Erlang para probar el
código del servidor:
> {ok, Socket} = gen_udp:open(0).
{ok,#Port<0.618>}
21> gen_udp:send(Socket, {127,0,0,1}, 2020, "hola mundo!").
ok
recibido({127,0,0,1}): "hola mundo!"
La operación de conexión se ha realizado abriendo una comunicación
con la función gen_udp:open/1 pasando como parámetro el puerto 0.
El puerto 0 se usa para indicar que queremos que el sistema operativo
seleccione un puerto automáticamente por nosotros. Podemos recurrir
a ejecutar de nuevo inet:i/0 para ver las conexiones abiertas y las
estadísticas:
> inet:i().
Port [...] Recv Sent Owner
Local Address [...] State Type
593 [...] 11
0
<0.34.0> *:2020
[...] BOUND DGRAM
121
Comunicaciones
618
ok
[...] 0
11
<0.38.0> *:33361
[...] BOUND DGRAM
Ahora vemos dos líneas. La primera sigue siendo la del servidor y la
segunda pertenece al cliente. Por los datos estadísticos vemos que la
información que ha enviado el cliente (columna Sent) es la que ha
recibido el servidor (columna Recv).
Vamos a modificar el código del servidor para que retorne al cliente una
cadena de texto:
-module(udpsrv).
-export([start/1, init/1, loop/1]).
-record(udp, {socket, ip, port, msg}).
start(Port) ->
spawn(?MODULE, init, [Port]),
ok.
init(Port) ->
{ok, Socket} = gen_udp:open(Port),
loop(Socket).
loop(Socket) ->
receive
stop ->
gen_udp:close(Socket);
Packet when is_record(Packet, udp) ->
io:format("recibido(~p): ~p~n", [
Packet#udp.ip, Packet#udp.msg
]),
#udp{ip=IP,port=Port} = Packet,
gen_udp:send(Socket, IP, Port, "recibido"),
loop(Socket)
end.
Hemos empleado la función gen_udp:send/4 para enviar al remitente
una respuesta enviando un texto fijo al cliente. Podemos probarlo en
consola de la siguiente forma:
> udpsrv:start(2020).
ok
> {ok, Socket} = gen_udp:open(0, [{active, false}]).
{ok,#Port<0.599>}
> gen_udp:send(Socket, {127,0,0,1}, 2020, "hola mundo!").
ok
recibido({127,0,0,1}): "hola mundo!"
> gen_udp:recv(Socket, 1024).
{ok,{{127,0,0,1},2020,"recibido"}}
El cliente recibe un paquete del servidor en el que le dice recibido. En la
función gen_udp:open/2 hemos empleado el parámetro de opciones
para indicar a gen_udp que no envíe el paquete recibido al proceso. Al
122
Comunicaciones
ejecutar gen_udp:recv/2 es cuando se obtiene la información que
llega del servidor.
3. Servidor y Cliente TCP
Para establecer comunicaciones TCP en Erlang disponemos del módulo
gen_tcp. En TCP el servidor escucha de un puerto y se mantiene
aceptando peticiones entrantes que quedan conectadas tras su
aceptación. La dinámica está protocolizada de forma que un servidor
establece una escucha en un puerto específico con posibilidad de envío
de opciones a través de la función listen/2 cuya definición es:
listen(Port, Options) -> {ok, ListenSocket} | {error, Reason}
Las opciones disponibles son iguales a las que se mostraron en la función
gen_udp:open. Son las siguientes:
list | binary
Indica si se quiere recibir el paquete como una lista o un binario. El
valor por defecto es list.
{ip | ifaddr, ip_address()}
La dirección IP en la que enlazar el puerto.
inet | inet6
Emplea IPv4 o IPv6 para las conexiones. El valor por defecto es inet.
{active, true | false | once}
Indica si todos los mensajes recibidos por red serán pasados al
proceso (true) o no (false) o sólo la primera vez (once). El valor por
defecto es true.
{reuseaddr, true | false}
Permite reutilizar el puerto. No lo bloquea. Por defecto esta opción
está deshabilitada.
Nota
Hay disponibles muchas opciones más a las que no entraremos ya
que son conceptos más avanzados o muy específicos, con lo que
salen del ámbito de explicación de este capítulo. Si desea más
información sobre la función gen_tcp:listen/2 puede revisar
4
este enlace
4
http://www.erlang.org/doc/man/gen_tcp.html#listen-2
123
Comunicaciones
Si ponemos en escucha el puerto 2020 y volvemos a listar el estado de
la red podemos ver que el proceso (en Owner) pasa a escuhar (State =:=
LISTEN) en el puerto 2020 (en Local) y para el tipo TCP (Type =:= STREAM):
> {ok, Socket} = gen_tcp:listen(2020, [{reuseaddr, true}]).
{ok,#Port<0.609>}
3> inet:i().
Port [...] Recv Sent Owner
Local Address [...] State Type
609 [...] 0
0
<0.32.0> *:2020
[...] LISTEN STREAM
ok
El siguiente paso será mantener el proceso a la espera de una conexión
entrante. Es el proceso que se llama aceptación y se realiza con la
función accept/1. Si lo ejecutamos en la consola de Erlang el sistema
se quedará bloqueado a la espera de una conexión entrante:
> {ok, SockAceptado} = gen_tcp:accept(Socket).
La función emplea la variable Socket creada en listen/2 para esperar
nuevas conexiones entrantes. Cuando una conexión entrante llega la
función accept/1 acepta la conexión y finaliza su ejecución retornando
otra variable SockAceptado. Este socket se empleará para comunicarse
con el cliente y obtener información del mismo.
En otra consola podemos realizar la conexión entrante. Emplearemos la
función connect/3 que tiene la siguiente sintaxis:
connect(Address, Port, Options) -> {ok, Socket} | {error, Reason}
Como parámetros se indica la dirección IP a la que conectarse (Address
en formato tupla), el puerto al que conectarse (Port) y las opciones para
establecer la comunicación (Options). Las opciones son las mismas que
se describieron para la función listen/2.
Abrimos otra consola y establecemos una comunicación local entre
ambos puntos escribiendo lo siguiente:
> {ok, Socket} = gen_tcp:connect({127,0,0,1}, 2020, []).
{ok,#Port<0.599>}
2> inet:i().
Port [...] Recv Sent [...] Local
Foreign State
Type
599 [...] 0
0
[...] *:38139 *:2020 CONNECTED STREAM
ok
La nueva consola de Erlang sólo tiene constancia de una conexión
existente que está en estado conectada (State) y va del puerto local
38139 al puerto 2020.
Desde la nueva consola podemos enviar información al servidor a través
de la función send/2 la cual tiene la forma:
124
Comunicaciones
send(Socket, Packet) -> ok | {error, Reason}
Como Socket emplearemos el que nos retornó la función connect/2.
El Packet es la información que queremos enviar. El paquete permite
formatos de lista de caracteres y lista binaria. Podemos ejecutarlo de la
siguiente manera:
> gen_tcp:send(Socket, "hola mundo!").
En el servidor no vemos de momento nada por consola. Si ejecutamos
5
flush/0 aparecerán los mensajes recibidos al proceso de la consola .
Si agregamos a las opciones de la función listen/2 {active, false} el
mensaje no es enviado al proceso sino que espera a que lo recibamos
a través del uso de la función recv/2. Esta función tiene la siguiente
sintaxis:
recv(Socket, Length) -> {ok, Packet} | {error, Reason}
El Socket es el valor de retorno obtenido tras la ejecución de la función
accept/1. Length es el tamaño máximo que se espera recibir del
paquete. En caso de que el paquete sea mayor que el tamaño una nueva
ejecución de recv/2 recogerá el siguiente trozo de información. En caso
de indicar un tamaño cero se recibe todo el paquete sin limitación de
tamaño.
Para establecer el diálogo entre servidor y cliente sólo necesitamos
emplear las funciones send/2 y recv/2 para realizar la comunicación
bidireccional. En el momento en el que se desee finalizar la
comunicación por cualquiera de las dos partes emplearíamos la función
close/1.
Importante
El socket que se estableció para escucha se puede igualmente
cerrar con close/1. Conviene cerrar los puertos antes de finalizar
la ejecución de los servidores para liberar los puertos empleados.
4. Servidor TCP Concurrente
La comunicación TCP se basa en la interconexión de dos puntos. En
Erlang se conecta cada socket a un proceso para recibir información por
5
Recordemos que la consola es un proceso que se mantiene a la espera de recibir eventos. Todos los
eventos que reciba la consola son interceptados por esta. Véase apéndice B para más información.
125
Comunicaciones
lo que hasta que la conexión entre cliente y servidor no se cierra ningún
otro cliente puede ser atendido por el servidor. Este problema no se
presenta en comunicaciones UDP. En TCP cada conexión servidora genera
un socket de conexión con el cliente específico.
Para que el proceso de servidor no permanezca bloqueado atendiendo
la conexión del primer cliente que conecte generamos un nuevo proceso
para atender esa petición entrante. De esta forma el proceso principal
queda liberado para aceptar más peticiones y generar nuevos procesos
a medida que vayan llegando nuevas peticiones de clientes.
Para que el nuevo socket generado sepa que tiene que enviar
sus paquetes al nuevo proceso hay que emplear la función
controlling_process/2, que tiene la forma:
controlling_process(Socket, Pid) -> ok | {error, Reason}
Escribiremos un pequeño módulo para comprobar cómo funciona.
Llamaremos al módulo tcpsrv y agregaremos las funciones del servidor:
start(Port) ->
spawn(fun() -> srv_init(Port) end).
srv_init(Port) ->
Opts = [{reuseaddr, true}, {active, false}],
{ok, Socket} = gen_tcp:listen(Port, Opts),
srv_loop(Socket).
srv_loop(Socket) ->
{ok, SockCli} = gen_tcp:accept(Socket),
Pid = spawn(fun() -> worker_loop(SockCli) end),
gen_tcp:controlling_process(SockCli, Pid),
inet:setopts(SockCli, [{active, true}]),
srv_loop(Socket).
worker_loop(Socket) ->
receive
{tcp, Socket, Msg} ->
io:format("Recibido ~p: ~p~n", [self(), Msg]),
timer:sleep(5000), %% 5 segundos de espera
Salida = io_lib:format("Eco: ~s", [Msg]),
gen_tcp:send(Socket, Salida),
worker_loop(Socket);
{tcp_closed, Socket} ->
io:format("Finalizado.~n");
Any ->
io:format("Mensaje no reconocido: ~p~n", [Any])
end.
La función start/1 se encarga de lanzar en un proceso aparte la
ejecución de la función srv_init/1. El cometido de esta función
inicializadora es establecer la escucha en un puerto TCP. El bucle de
ejecución para el servidor se basa en aceptar una conexión, generar un
126
Comunicaciones
nuevo proceso, pasarle el control de la conexión con el cliente al nuevo
proceso y vuelta a empezar.
La generación del nuevo proceso tiene como función de bucle principal a
worker_loop. Esta función integraría el protocolo a nivel de aplicación
para interactuar con el cliente. En nuestro ejemplo esperamos a recibir
un mensaje y lo retornamos precedido de la palabra Eco.
El siguiente código es un ejemplo para probar el servidor:
cli_send(Port, Msg) ->
Opts = [{active, true}],
{ok, Socket} = gen_tcp:connect({127,0,0,1}, Port, Opts),
gen_tcp:send(Socket, Msg),
receive
{tcp, Socket, MsgSrv} ->
io:format("Retornado ~p: ~p~n", [self(), MsgSrv]);
Any ->
io:format("Mensaje no reconocido: ~p~n", [Any])
end,
gen_tcp:close(Socket).
6
La función cli_send/2 permite conectarse a un puerto local , enviar
un mensaje y esperar por el retorno antes de finalizar la comunicación.
Hasta el momento todo funciona como la versión anterior. No obstante,
hemos agregado en el servidor un retraso de 5 segundos que nos ayudará
a ver la concurrencia en ejecuciones múltiples de cli_send/2. Lo
podemos realizar con varias consolas o a través de un código como el
siguiente:
cli_concurrent_send(Port) ->
Send = fun(I) ->
Text = io_lib:format("i=~p", [I]),
spawn(tcpcli, cli_send, [Port, Text])
end,
lists:foreach(Send, lists:seq(1,10)).
Este código genera 10 procesos que ejecutan la función cli_send/2
enviando el mensaje i=I, siendo I el valor pasado por foreach/2 a cada
uno de los procesos.
La ejecución muestra como todos los procesos llegan a recibir el mensaje
en el servidor y quedan esperando por el resultado 5 segundos después.
5. Ventajas de inet
Erlang no sólo dispone de funciones para manejar las comunicaciones a
nivel transporte. El módulo inet a través de la función setopts/2 provee
6
En este ejemplo no hemos empleado direcciones IP, por lo que se emplea por defecto la IP local o
127.0.0.1.
127
Comunicaciones
la capacidad de interpretar los paquetes recibidos a través de TCP o UDP
y enviarlos como mensaje al proceso ya procesados.
7
Según la documentación de inet los formatos que procesa son: CORBA,
ASN-1, SunRPC, FastCGI, Line, TPKT y HTTP.
Nota
La decodificación la realiza únicamente a nivel de recepción, el
envío deberemos de componerlo nosotros mismos y enviarlo con
la función de send/2 de gen_tcp.
Para construir nuestro propio servidor HTTP y aprovechar la característica
que nos provee inet sólo tendríamos que agregar la opción a la función
listen/2. Vamos a verlo con un ejemplo:
> Opts = [{reuseaddr, true}, {active, true}, {packet, http}],
> {ok, Socket} = gen_tcp:listen(8080, Opts).
{ok,#Port<0.604>}
> {ok, SC} = gen_tcp:accept(Socket).
En este momento el sistema queda en espera de que llegue una petición.
Como hemos levantado un puerto TCP y le hemos configurado las
características de HTTP, vamos a abrir un navegador con la siguiente URL:
http://localhost:8080/
En la consola veremos que ya prosigue la ejecución:
{ok,#Port<0.605>}
> flush().
Shell got {http,#Port<0.605>,
{http_request,'GET',{abs_path,"/"},{1,1}}}
Shell got {http,#Port<0.605>,
{http_header,14,'Host',undefined,"localhost:8080"}}
[...]
Shell got {http,#Port<0.605>,http_eoh}
ok
> Msg = "HTTP/1.0 200 OK
> Content-length: 1
> Content-type: text/plain
>
> H",
> gen_tcp:send(SC, Msg).
ok
Los mensajes recibidos por el sistema son tuplas que tienen como primer
elemento http. Como en los casos de tcp el segundo parámetro es Socket.
Como tercer parámetro puede aparecer otra tupla cuyo primer parámetro
es:
7
http://www.erlang.org/doc/man/inet.html#setopts-2
128
Comunicaciones
http_request
Si se trata de la primera línea de petición. Esta tupla tendrá 4
campos: http_request, método HTTP (GET, POST, PUT o DELETE entre
otros), URI y versión HTTP en forma de tupla de dos elementos. Un
ejemplo:
{http_request, 'GET', {abs_path,"/"},{1,1}}
http_header
Las siguientes líneas a la petición son las líneas de cabecera. Que se
estructuran en una tupla de 5 campos: http_header, bit de cabecera,
nombre de la cabecera, valor reservado (undefined) y valor de la
cabecera.
http_eoh
Este dato se transmite en forma de átomo. Indica que la recepción
de cabeceras ha finalizado.
A continuación vemos un ejemplo completo. Presenta las peticiones
recibidas por pantalla junto con su contenido:
-module(httpsrv).
-export([start/1]).
-define(RESP, "HTTP/1.1 200 OK
Content-Length: 2
Content-Type: text/plain
OK").
start(Port) ->
spawn(fun() -> srv_init(Port) end).
srv_init(Port) ->
Opts = [{reuseaddr, true}, {active, false}, {packet, http}],
{ok, Socket} = gen_tcp:listen(Port, Opts),
srv_loop(Socket).
srv_loop(Socket) ->
{ok, SockCli} = gen_tcp:accept(Socket),
Pid = spawn(fun() -> worker_loop(SockCli) end),
gen_tcp:controlling_process(SockCli, Pid),
inet:setopts(SockCli, [{active, true}]),
srv_loop(Socket).
worker_loop(Socket) ->
receive
{http, Socket, http_eoh} ->
inet:setopts(Socket, [{packet, raw}]),
worker_loop(Socket);
{http, Socket, Header} ->
io:format("Recibido ~p: ~p~n", [self(), Header]),
worker_loop(Socket);
{tcp, Socket, Msg} ->
129
Comunicaciones
io:format("Recibido ~p: ~p~n", [self(), Msg]),
gen_tcp:send(Socket, ?RESP),
gen_tcp:close(Socket);
{tcp_closed, Socket} ->
io:format("Finalizado.~n"),
gen_tcp:close(Socket);
Any ->
io:format("Mensaje no reconocido: ~p~n", [Any]),
gen_tcp:close(Socket)
end.
El servidor es bastante simple ya que siempre retorna el mismo resultado.
Si accedemos desde un navegador veremos en modo texto el mensaje
OK.
En la función worker_loop/1 cuando se recibe http_eoh se puede
ver que se modifica el tipo de paquete para poder recibir el contenido.
Además vemos que se diferencian bien los mensajes que se reciben de
tipo http de los que son de tipo tcp.
Nota
Si empleamos el parámetro {active, false} para emplear la función
recv/2 en lugar de receive hay que tener presente que el retorno
de la función recv/2 será: {ok, HttpPacket}, mientras que el
retorno de receive será: {http, Socket, HttpPacket}.
130
Capítulo 8. Ecosistema Erlang
La construcción exitosa de toda máquina depende
de la perfección de las herramientas empleadas.
Quien sea un maestro en el arte de la fabricación de
herramientas poseerá la clave para la construcción
de todas las máquinas.
—Charles Babbage
Un ecosistema es un ambiente en el que conviven elementos en
un espacio relacionandose entre sí. En software se ha tomado esta
definición para definir al conjunto de herramientas y sistemas que
permiten realizar software.
En Erlang lo usaremos para identificar el uso de unas herramientas junto
con sus buenas prácticas a la hora de desarrollar proyectos de software.
Para este fin daremos un repaso a la herramienta de construcción rebar
que ha llegado a convertirse en un estándar dentro de la comunidad de
Erlang.
1. Iniciar un Proyecto
A lo largo de los capítulos hemos realizado la mayor parte del código
en la consola de Erlang y vimos la organización del código interno y la
realización de módulos. Aún no hemos comentado la forma que debe
tener nuestro espacio de trabajo, los directorios que es conveniente crear
y la disposición de los ficheros dentro de estos directorios.
La herramienta rebar es la más empleada entre las utilidades de terceros
1
del mundo Erlang. La empresa Basho es la desarrolladora principal de
esta herramienta aunque cada día hay más contribuidores al proyecto.
Un proyecto en Erlang/OTP debe disponer de una estructura base como
la siguiente:
src
Este directorio contendrá el código fuente. Todos los ficheros cuya
extensión sea .erl.
ebin
Aquí se almacenarán los ficheros de tipo .beam, es decir la
compilación de nuestra aplicación.
1
Basho Technologies es una empresa estadounidense que desarrolla la base de datos Riak.
131
Ecosistema Erlang
include
Los ficheros que se almacenan en este directorio son los de tipo
cabecera .hrl.
priv
Cuando el proyecto requiere de ficheros específicos para funcionar
se introducen en este directorio ficheros como certificados, páginas
HTML, hojas de estilo CSS o códigos JavaScript entre otros.
Nota
Hay más directorios por defecto para proyectos Erlang/OTP como
c_src donde se alojan los ficheros de extensión escritos en C,
test para los códigos de pruebas de EUnit o CommonTest o deps
es donde se bajan otros proyectos de terceros para incluir su
código dentro de nuestro proyecto.
Crearemos un proyecto que ilustre cómo organizar los ficheros del
código y cómo ejecutar ese mismo código de forma autónoma.
Para esta tarea nos ayudaremos de rebar. Creamos los tres directorios
base y pasamos a instalar rebar.
1.1. Instalar rebar
La instalación de rebar se basa en descargar el código de su repositorio y
ejecutar el script bootstrap. El sistema generará el script rebar en ese
mismo directorio. Este script se puede copiar a un directorio del PATH o
localmente dentro del proyecto que estemos desarrollando.
$ git clone git://github.com/basho/rebar.git
$ cd rebar
$ ./bootstrap
Recompile: src/getopt
...
Recompile: src/rebar_utils
==> rebar (compile)
Congratulations! You now have a self-contained script called
"rebar" in your current working directory. Place this script
anywhere in your path and you can use rebar to build OTPcompliant apps.
Ahora solo nos falta copiar el script generado a una ruta visible por
nuestro PATH. Normalmente como super usuario en sistemas de tipo
2
Unix en una ruta como /usr/bin, /usr/local/bin o /opt/local/
bin.
2
Sistemas Unix o tipo Unix como BSD, Linux, MacOS X u OpenSolaris entre otros.
132
Ecosistema Erlang
Para asegurarnos de que es accesible podemos ejecutarlo en la consola.
Nota
La utilidad rebar se encuentra también disponible para Windows
3
a través del repositorio bifurcado (fork) de IRONkyle .
Recomiendo que los proyectos iniciales y el aprendizaje se lleven
a cabo en sistemas tipo Unix como MacOS o GNU/Linux por el
motivo de que la mayoría de soporte y desarrollos se realizan en
estos sistemas.
1.2. Escribiendo el Código
Vamos a crear un proyecto que consistirá en un servidor web al que se le
solicitarán ficheros y en caso de existir en el directorio priv se retornará
su contenido.
El desarrollo lo realizaremos en dos ficheros. El primer módulo se
encargará de establecer la escucha para el servidor web y atender las
peticiones:
-module(webserver).
-export([start/1]).
start(Port) ->
spawn(fun() -> srv_init(Port) end).
srv_init(Port) ->
Opts = [{reuseaddr, true}, {active, false}, {packet, http}],
{ok, Socket} = gen_tcp:listen(Port, Opts),
srv_loop(Socket).
srv_loop(Socket) ->
{ok, SockCli} = gen_tcp:accept(Socket),
Pid = spawn(fun() -> worker_loop(SockCli, []) end),
gen_tcp:controlling_process(SockCli, Pid),
inet:setopts(SockCli, [{active, true}]),
srv_loop(Socket).
worker_loop(Socket, State) ->
receive
{http, Socket, {http_request, Method, TPath, _}} ->
{abs_path, Path} = TPath,
error_logger:info_msg("Peticion: ~p~n", [Path]),
worker_loop(Socket, State ++ [
{method, Method}, {path, Path}
]);
{http, Socket, {http_header, _, Key, _, Value}} ->
worker_loop(Socket, State ++ [{Key, Value}]);
{http, Socket, http_eoh} ->
Response = fileserver:send(State),
gen_tcp:send(Socket, Response),
gen_tcp:close(Socket);
3
https://github.com/IRONkyle/rebar
133
Ecosistema Erlang
{tcp_closed, Socket} ->
error_logger:info_msg("Finalizado.~n"),
gen_tcp:close(Socket);
Any ->
error_logger:info_msg("No reconocido: ~p~n", [Any]),
gen_tcp:close(Socket)
end.
El código listado fue visto en la Sección 4, “Servidor TCP Concurrente”
del Capítulo 7, Comunicaciones. Solo agregaremos la llamada al módulo
fileserver.
El segundo módulo se encargará de buscar el fichero solicitado y
retornarlo como texto identificando su tipo. El código es el siguiente:
-module(fileserver).
-export([send/1]).
-define(RESP_404, <<"HTTP/1.1 404 Not Found
Server: Erlang Web Server
Connection: Close
">>).
-define(RESP_200, <<"HTTP/1.1 200 OK
Server: Erlang Web Server
Connection: Close
Content-type: ">>).
send(Request) ->
"/" ++ Path = proplists:get_value(path, Request, "/"),
{ok, CWD} = file:get_cwd(),
RealPath = filename:join(CWD, Path),
case file:read_file(RealPath) of
{ok, Content} ->
Size = list_to_binary(
io_lib:format("~p", [byte_size(Content)])
),
Type = mimetype(Path),
<<
?RESP_200/binary, Type/binary,
"\nContent-lenght: ", Size/binary,
"\r\n\r\n", Content/binary
>>;
{error, _} ->
?RESP_404
end.
mimetype(File) ->
case filename:extension(string:to_lower(File)) of
".png" -> <<"image/png">>;
".jpg" -> <<"image/jpeg">>;
".jpeg" -> <<"image/jpeg">>;
".zip" -> <<"application/zip">>;
".xml" -> <<"application/xml">>;
".css" -> <<"text/css">>;
".html" -> <<"text/html">>;
".htm" -> <<"text/html">>;
".js" -> <<"application/javascript">>;
".ico" -> <<"image/vnd.microsoft.icon">>;
134
Ecosistema Erlang
_ -> <<"text/plain">>
end.
Con esto ya tenemos el código preparado. Solos nos falta escribir la
definición necesaria para que rebar pueda identificar la aplicación y
construir el producto final. Crearemos el fichero en el directorio src con
el nombre webserver.app.src:
{application, webserver
{description
{vsn
, [
, "Erlang Web Server"},
, "1.0"},
{applications ,[
kernel, stdlib, inets
]}
]}.
El nombre que tendrá la aplicación.
Como descripción se especifica un texto. Es deseable que no sea
muy extenso. Se puede poner el nombre completo de la aplicación.
La versión de la aplicación. Se puede especificar de la forma que
se desee.
Aplicaciones que deben de iniciarse antes de iniciar la nuestra.
Dependencias.
Nota
Como versión en la línea de vsn podemos emplear las palabras
clave: git, hg, bzr, svn o {cmd, Cmd}. Las primeras indican al sistema
que tome el tag o el número de revisión del sistema de control de
versiones. La última indica que ejecute el comando contenido en
Cmd para obtener la versión.
4
En la página de referencia de app podemos ver una lista más completa
y detallada de las opciones que permite el fichero para iniciar una
aplicación.
2. Compilar y Limpiar
Una vez que tenemos el directorio src creado podemos compilarlo todo
ejecutando el comando: rebar compile. El comando rebar se encarga de
crear el directorio ebin y depositar los ficheros beam dentro de él:
$ rebar compile
==> webserver_simple (compile)
Compiled src/webserver.erl
Compiled src/fileserver.erl
4
http://www.erlang.org/doc/man/app.html
135
Ecosistema Erlang
El espacio de trabajo es como se puede observar a continuación:
El directorio ebin contiene la compilación de los códigos listados en
la sección anterior. El fichero webserver.app.src se analiza y se
completa para generar el fichero webserver.app dentro del directorio
ebin.
Si queremos ejecutar el código, podemos iniciar la consola de la
siguiente forma:
$ erl -sname webserver -pa ebin
(webserver@bosqueviejo)1> webserver:start(8888).
<0.39.0>
De esta forma tenemos el código en ejecución. Para eliminar estos
ficheros generados ejecutamos el comando rebar clean.
3. Creando y lanzando una aplicación
En la sección anterior vimos que el lanzamiento del código escrito se
hacía de forma manual. Si desarrollamos una aplicación de servidor hay
que poder lanzar esta aplicación de forma automática.
Erlang proporciona al programador una forma de realizar esto a través de
5
un comportamiento denominado application.
Para ello necesitamos crear otro fichero de código dentro del directorio
src. Llamaremos a este fichero webserver_app.erl y pondremos el
siguiente contenido:
-module(webserver_app).
5
Los comportamientos (behaviours) son un mecanismo de inversión de control (IoC) que posibilita la
creación de código abstracto más concreto para el usuario. Estos serán vistos en mayor profundidad en
el Volumen II.
136
Ecosistema Erlang
-behaviour(application).
-export([start/0, start/2, stop/1]).
start() ->
application:start(webserver).
start(_StartType, _StartArgs) ->
{ok, webserver:start(8888)}.
stop(_State) ->
ok.
Este módulo dispone de tres funciones. Las funciones start/2 y
stop/1 son requeridas por el comportamiento application, mientras que
start/0 la emplearemos para la línea de comandos.
En el fichero webserver.app.src solo debemos de agregar una nueva
línea que indique qué módulo se hará cargo de las llamadas propias de
la aplicación para su inicio y fin:
{application, webserver, [
{description, "Erlang Web Server"},
{vsn, "1.0"},
{applications,[
kernel, stdlib, inets
]},
{mod, {webserver_app, []}}
]}.
Línea que indica el módulo de comportamiento application que se
hará cargo del inicio y parada de la aplicación.
6
En la consola agregaremos un par de argumentos más :
$ erl -sname test -pa ebin -s inets -s webserver_app \
-noshell -detached
El comando se encarga de dar un nombre al nodo (-sname), decir donde
se encuentra el código que queremos lanzar (-pa), arrancar la aplicación
inets (-s) y la aplicación webserver. Indicamos además que no queremos
que se ejecute una consola o shell (-noshell) y que se ejecute en segundo
plano (-detached).
4. Dependencias
En Internet existen repositorios con miles de librerías para Erlang.
7
8
Los más representativos son github.com y bitbucket . En estos sitios
6
Los argumentos usados para la línea de comandos se pueden revisar en el Apéndice B, La línea de
comandos.
7
https://github.com
8
https://bitbucket.org
137
Ecosistema Erlang
podemos encontrar librerías para conectar con MySQL, PostgreSQL,
Memcached, o frameworks web como ChicagoBoss o Nitrogen, o
frameworks para crear servidores web como cowboy o mochiweb entre
otras muchas.
La herramienta rebar posibilita a través de su fichero de configuración
que podamos instalar en nuestro proyecto una librería externa con muy
poco esfuerzo.
El código del fichero fileserver.erl muestra una función llamada
mimetype/1. Esa función es insuficiente para cubrir todos los tipos
posibles de ficheros que pudiésemos utilizar en nuestra aplicación.
9
Podemos emplear en su lugar la librería mimetypes .
Para ello generaríamos el fichero de configuración rebar.config en la
ruta raíz del proyecto y con el siguiente contenido:
{deps, [
{mimetypes, ".*",
{git, "https://github.com/spawngrid/mimetypes.git",
"master"}
}
]}.
La especificación de la aplicación la cambiamos también para agregar la
nueva dependencia de la siguiente manera:
{application, webserver, [
{description, "Erlang Web Server"},
{vsn, "1.0"},
{applications,[
kernel, stdlib, inets, mimetypes
]},
{mod, {webserver_app, []}}
]}.
Agregamos en el fichero webserver_app.erl el lanzamiento de la
aplicación mimetypes para cumplir con la dependencia. La función
start/0 quedaría así:
-module(webserver_app).
-behaviour(application).
-export([start/0, start/2, stop/1]).
start() ->
application:start(mimetypes),
application:start(webserver).
start(_StartType, _StartArgs) ->
{ok, webserver:start(8888)}.
9
https://github.com/spawngrid/mimetypes.git
138
Ecosistema Erlang
stop(_State) ->
ok.
Por último, cambiamos el código escrito para que en lugar de tener el uso
de nuestra función mimetype/1 emplee las que provee la librería:
-module(fileserver).
-export([send/1]).
-define(RESP_404, <<"HTTP/1.1 404 Not Found
Server: Erlang Web Server
Connection: Close
">>).
-define(RESP_200, <<"HTTP/1.1 200 OK
Server: Erlang Web Server
Connection: Close
Content-type: ">>).
send(Request) ->
"/" ++ Path = proplists:get_value(path, Request, "/"),
{ok, CWD} = file:get_cwd(),
RealPath = filename:join(CWD, Path),
case file:read_file(RealPath) of
{ok, Content} ->
Size = list_to_binary(
io_lib:format("~p", [byte_size(Content)])
),
[Type] = mimetypes:filename(Path),
<<
?RESP_200/binary, Type/binary,
"\nContent-lenght: ", Size/binary,
"\r\n\r\n", Content/binary
>>;
{error, _} ->
?RESP_404
end.
Antes de compilar debemos de lanzar el siguiente comando para
descargar las dependencias que hemos indicado que necesitamos en
nuestro proyecto:
$ rebar get-deps
==> webserver_deps (get-deps)
Pulling mimetypes from {git,
"https://github.com/spawngrid/mimetypes.git",
"master"}
Cloning into 'mimetypes'...
==> mimetypes (get-deps)
$ rebar compile
==> mimetypes (compile)
Compiled src/mimetypes_scan.xrl
Compiled src/mimetypes_parse.yrl
Compiled src/mimetypes_loader.erl
Compiled src/mimetypes_scan.erl
Compiled src/mimetypes_sup.erl
Compiled src/mimetypes_app.erl
139
Ecosistema Erlang
Compiled src/mimetypes.erl
Compiled src/mimetypes_parse.erl
==> webserver_deps (compile)
Compiled src/webserver_app.erl
Compiled src/webserver.erl
Compiled src/fileserver.erl
Nota
El comando rebar get-deps se emplea para descargar las
dependencias mientras que rebar del-deps se encarga de
eliminarlas. Este último comando es útil para realizar una limpieza
del proyecto junto con rebar clean:
$ rebar del-deps
==> mimetypes (delete-deps)
==> webserver (delete-deps)
$ rebar clean
==> webserver (clean)
Para lanzar de nuevo la aplicación agregaremos la ruta de las
dependencias de esta forma:
$ erl -pa deps/*/ebin -pa ebin -sname test -s inets \
-s webserver_app -noshell -detached
Vemos al ejecutarlo que volvemos a tener el puerto 8888 disponible y
los ficheros solicitados presentan ya unos tipos MIME más precisos:
$ netstat -tln | grep 8888
tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN
$ curl -i http://localhost:8888/rebar.config
HTTP/1.1 200 OK
Server: Erlang Web Server
Connection: Close
Content-type: application/octet-stream
Content-lenght: 119
{deps, [
{mimetypes, ".*",
{git, "https://github.com/spawngrid/mimetypes.git",
"master"}
}
]}.
5. Liberar y Desplegar
El desarrollo de software tiene su culminación cuando el software puede
ser instalado en sistemas en producción. Liberar el código consiste en
dejar preparado el producto para su instalación. Este debe de poderse
empaquetar, construir y lanzar de forma fácil y simple. El despliegue
consiste en el procedimiento de instalación de este código liberado.
140
Ecosistema Erlang
La herramienta rebar nos facilita la tarea de la liberación generando una
serie de scripts y ficheros de configuración. Crearemos el directorio rel y
dentro de él ejecutaremos el comando:
$ rebar create-node nodeid=webserver
Importante
El nombre que se dé al nodo es preferible que no contenga
guiones bajos. En el proceso de generación de actualizaciones
(appups y upgrades) podría generar errores.
Lo siguiente será crear el directorio apps y un subdirectorio webserver.
Dentro de webserver moveremos los directorios src y ebin.
Ajustaremos los ficheros rebar.config y el nuevo fichero dentro del
directorio rel llamado reltool.config para adaptarlos a la nueva
ubicación del código. En el fichero rebar.config basta con agregar
esta línea:
141
Ecosistema Erlang
{sub_dirs, ["apps/*"]}.
Nota
El directorio apps se emplea cuando se requieren escribir
programas con varias aplicaciones. El comando rebar generate
requiere que esta estructura exista para realizar la liberación.
El fichero reltool.config es la configuración que necesita el sistema
denominado reltool para generar la liberación. Las posibles entradas de
configuración que se pueden agregar al fichero son muy numerosas. Para
profundizar más el tema puedes visitar su página de documentación
10
oficial . En este apartado recogeremos las más importantes que se
agregarán bajo la clave sys:
{lib_dirs, [Dir1,Dir2..DirN]}
El directorio (o directorios) que contiene las aplicaciones.
Deberemos de agregarlo de la siguiente forma:
{lib_dirs, ["../apps", "../deps"]},
{rel, App, Vsn, [App1,App2..AppN]}
Se especifica el nombre de la aplicación (primer parámetro), la
versión de la aplicación (segundo parámetro) y las aplicaciones a
ejecutar. El listado de aplicaciones debe contener las applicaciones
en el orden en el que se deben de ir iniciando cuando se arranque
el programa. Por lo tanto deberemos de agregar:
{rel, "webserver", "1.0", [
kernel, stdlib, sasl, inets, mimetypes, webserver
]},
{boot_rel, App}
Se pueden crear tantos apartados rel como se necesiten. Uno de
ellos debe marcarse por defecto con esta opción.
{profile, development | standalone | embedded}
El perfil indica el nivel de restricción a aplicar para la copia de
dependencias, librerías o binarios entre otros. Hay tres perfiles de
menos a más restrictivo: development, standalone y embedded.
10
http://www.erlang.org/doc/man/reltool.html
142
Ecosistema Erlang
{incl_cond, include | exclude | derived }
Indica el modo en que serán elegidas las aplicaciones que entrarán
en la liberación. Las opciones disponibles son:
include
Entran todas las aplicaciones menos la que explícitamente se
indique que no entre.
exclude
Entran solo las aplicaciones que se indiquen de forma explícita
que deban de entrar.
derived
Se incluyen las aplicaciones indicadas explícitamente y todas
sus dependencias.
{mod_cond, all | app | ebin | derived | none}
Es como incl_cond pero a nivel de aplicación. Las opciones que
permite son:
all
Se incluyen todos los módulos de cada aplicación incluída en
la liberación.
app
Se incluyen todos los módulos listados en el fichero .app y
11
derivados .
ebin
Se incluyen todos los módulos que estén en el directorio ebin
11
de la aplicación y los derivados .
derived
Se incluyen los módulos que estén siendo usados por los
incluídos explícitamente.
none
No se incluye ninguno.
{app, App, [ConfList]}
Esta es la especificación individual de cada aplicación. Debe de
existir al menos la principal. Como configuración se puede indicar
11
Se refiere a todos los módulos que no estén incluídos pero que reltool detecte que puedan ser
utilizados.
143
Ecosistema Erlang
una entrada incl_cond que aplique solo sobre la aplicación y
opcionalmente otra mod_cond que actúe solo sobre la aplicación.
En nuestro fichero pondremos únicamente:
{app, webserver, [{mod_cond, app}, {incl_cond, include}]}
Nota
Se pueden emplear filtros para agregar ciertos ficheros
sólo y según qué nivel (archivo, sistema o aplicación). No
profundizaremos en este tema para no extendernos más y porque
este uso es más una referencia que el lector puede encontrar
fácilmente en la web oficial.
Otras entradas al mismo nivel de sys que se pueden encontrar son
target_dir que indica el nombre del directorio donde se situará el
resultado de la liberación y overlay que contiene comandos adicionales
a ejecutar durante la liberación. Estos comandos adicionales son del
tipo crear directorio (mkdir), copiar fichero (copy) o emplear una plantilla
(template) a fusionar con un fichero de variables y generar el fichero que
estará disponible en el despliegue.
Aquí el fichero completo reltool.config:
{sys, [
{lib_dirs, ["../apps", "../deps"]},
{erts, [{mod_cond, derived}, {app_file, strip}]},
{app_file, strip},
{rel, "webserver", "1.0", [
kernel,
stdlib,
sasl,
inets,
mimetypes,
webserver
]},
{rel, "start_clean", "", [
kernel,
stdlib
]},
{boot_rel, "webserver"},
{profile, embedded},
{incl_cond, derived},
{mod_cond, derived},
{excl_archive_filters, [".*"]}, %% Do not archive built libs
{excl_sys_filters, [
"^bin/.*",
"^erts.*/bin/(dialyzer|typer)",
"^erts.*/(doc|info|include|lib|man|src)"]
},
{excl_app_filters, ["\.gitignore"]},
{app, webserver, [{mod_cond, app}, {incl_cond, include}]}
]}.
144
Ecosistema Erlang
{target_dir, "webserver"}.
{overlay, [
{mkdir, "log/sasl"},
{copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"},
{copy, "files/nodetool", "\{\{erts_vsn\}\}/bin/nodetool"},
{copy, "files/webserver", "bin/webserver"},
{copy, "files/webserver.cmd", "bin/webserver.cmd"},
{copy, "files/start_erl.cmd", "bin/start_erl.cmd"},
{copy, "files/install_upgrade.escript",
"bin/install_upgrade.escript"},
{copy, "files/sys.config",
"releases/\{\{rel_vsn\}\}/sys.config"},
{copy, "files/vm.args", "releases/\{\{rel_vsn\}\}/vm.args"}
]}.
Dentro del directorio rel ejecutamos el comando:
$ rebar generate
==> rel (generate)
Obtenemos como resultado un directorio webserver en el que se
encuentra la liberación. El comando rebar nos proporciona en el
directorio bin un script que nos permite lanzar el programa de varias
formas:
console
En primer plano. Abre una consola y ejecuta todas las aplicaciones
mientras vemos en pantalla los mensajes que imprime cada una de
las aplicaciones al lanzarse.
start / stop
Estos comandos permiten iniciar y detener la aplicación que se
lanza en segundo plano.
ping
Hace un ping al nodo de la aplicación. En caso de que esté activo el
nodo responderá con un pong.
attach
Permite conectarse a una aplicación ejecutándose en segundo
plano.
Si ejecutamos el comando start y después ping podremos ver que el
sistema responde sin problemas. Entramos a consola a través de attach
y podemos ver los mensajes de log que hayamos escrito en el fichero:
$ webserver/bin/webserver start
145
Ecosistema Erlang
/tmp/rel$ webserver/bin/webserver ping
pong
/tmp/rel$ webserver/bin/webserver attach
Attaching to /tmp/rel/webserver/erlang.pipe.1 (^D to exit)
([email protected])1>
Importante
Cuando nos conectamos a una aplicación en ejecución con attach
debemos siempre salir con la pulsación de las teclas Control+D.
Si salimos interrumpiendo la consola la aplicación se detendrá.
El despliegue en un servidor u otro equipo informático se realizará
comprimiendo el resultado que se ha obtenido en webserver y
descomprimirlo en el destino. El lanzamiento lo podemos agregar como
script en los sistemas tipo Unix gracias a que respeta la forma de start
y stop.
Nota
Una buena práctica es que cada vez que demos una versión como
terminada, hagamos una compilación en un fichero comprimido
de la misma. Esto nos servirá para poder transportar nuestro
proyecto a producción y crear actualizaciones.
6. Actualizando en Caliente
Una de las ventajas que reseñamos de Erlang al principio es su capacidad
para cambiar el código en caliente sin necesidad de detener la ejecución
del programa. En la Sección 8, “Recarga de código” del Capítulo 5,
Procesos vimos cómo cargar código en caliente. En esta sección veremos
cómo realiza esta acción rebar para cambiar el código en caliente de todo
un proyecto completo.
Como ejemplo pensemos que necesitamos modificar el fichero
fileserver.erl para que responda a una solicitud con una URI /help
que retorne un texto personalizado de ayuda.
Lo primero que haremos será generar la versión 1.0 y modificar el nombre
dentro del directorio rel de webserver a webserver_old.
Hacemos los cambios oportunos en el fichero:
-module(fileserver).
-export([send/1]).
-define(RESP_404, <<"HTTP/1.1 404 Not Found
146
Ecosistema Erlang
Server: Erlang Web Server
Connection: Close
">>).
-define(RESP_200, <<"HTTP/1.1 200 OK
Server: Erlang Web Server
Connection: Close
Content-type: ">>).
-define(HELP_TEXT, <<"Texto de ayuda!">>).
send(Request) ->
case proplists:get_value(path, Request, "/") of
"/help" ->
Content = ?HELP_TEXT,
Size = list_to_binary(
integer_to_list(byte_size(Content))
),
<<
?RESP_200/binary, "text/html",
"\nContent-lenght: ", Size/binary,
"\n\n", Content/binary
>>;
"/" ++ Path ->
{ok, CWD} = file:get_cwd(),
RealPath = filename:join(CWD, Path),
case file:read_file(RealPath) of
{ok, Content} ->
Size = list_to_binary(
integer_to_list(byte_size(Content))
),
Type = mimetype(Path),
<<
?RESP_200/binary, Type/binary,
"\nContent-lenght: ", Size/binary,
"\n\n", Content/binary
>>;
{error, _} ->
?RESP_404
end
end.
mimetype(File) ->
case filename:extension(string:to_lower(File)) of
".png" -> <<"image/png">>;
".jpg" -> <<"image/jpeg">>;
".jpeg" -> <<"image/jpeg">>;
".zip" -> <<"application/zip">>;
".xml" -> <<"application/xml">>;
".css" -> <<"text/css">>;
".html" -> <<"text/html">>;
".htm" -> <<"text/html">>;
".js" -> <<"application/javascript">>;
".ico" -> <<"image/vnd.microsoft.icon">>;
_ -> <<"text/plain">>
end.
También
modificamos
la
versión
dentro
del
fichero
webserver.app.src para que refleje el cambio de versión y la versión
en el fichero reltool.cfg para que sea 2.0 en lugar de 1.0.
147
Ecosistema Erlang
Volvemos a generar el proyecto compilando y generando el producto
final:
$ rebar clean compile
$ cd rel
$ rebar generate
Tenemos dos directorios de nuestro proyecto. Uno con la versión 1.0 y
otro con la versión 2.0. Es ahora cuando generamos los ficheros appup.
Estos ficheros se generan por aplicación y contienen información sobre
los cambios que hay que realizar en caliente.
Dejamos que rebar generate-appups nos genere todos los ficheros
necesarios:
$ rebar generate-appups previous_release=webserver_old
==> rel (generate-appups)
Generated appup for webserver
Appup generation complete
El fichero generado para nuestra aplicación es webserver.appup. Este
fichero se crea en la ruta webserver/lib/webserver-2.0/ebin. Su forma es:
{"2.0", [
{"1.0", [
{load_module,fileserver}
]}
], [
{"1.0", [
{load_module,fileserver}
]}
]}.
La versión que va a ser instalada.
Bloque que indica las acciones a llevar para pasar de la versión 1.0
a la 2.0.
Cada opción a llevar a cabo tendrá forma de tupla. La acción
load_module se refiere a la recarga del módulo que se indica (en
este caso fileserver).
Bloque de las acciones a llevar a cabo en caso de querer realizar una
marcha atrás de la versión 2.0 a la versión 1.0.
148
Ecosistema Erlang
Nota
El fichero appup permite muchos más comandos. Si los
cambios han sido más significativos como la agregación o
eliminación de módulos se pueden emplear otros comandos
como add_module y/o delete_module. El sistema también permite
trazar la dependencia de módulos y el orden en el que se deben
de ir cargando a través de las opciones PrePurge, PostPurge y
DepMods de las formas completas de las tuplas de comandos que
12
pueden verse en la web oficial de appup . Por ejemplo:
{add_module, filesystem},
{add_module, ftp},
{load_module, webserver},
{code_change, [{webserver, undefined}]},
{delete_module, fileserver}
Esta forma se aplicaría cuando cambiamos fileserver.erl
por otros módulos diferentes. Primero cargamos los nuevos,
recargamos el manejador, enviamos el código de cambio para
13
el manejador y eliminamos el módulo antiguo. El soporte
para system_code_change/4 debe de existir en el módulo
webserver.
Ahora generamos el paquete de la nueva versión. Para ello utilizamos el
comando rebar generate-upgrade de la siguiente forma:
$ rebar generate-upgrade previous_release=webserver_old
==> rel (generate-upgrade)
webserver_2.0 upgrade package created
El resultado será un fichero webserver_2.0.tar.gz. Este fichero
contiene los binarios del programa para ejecutarse en producción, así
como la información de cada aplicación y los ficheros para recargar cada
una de manera adecuada.
El fichero debe de copiarse en la ruta webserver/releases. Si hemos
lanzado el código antiguo que está en webserver_old, copiamos dentro
de su directorio releases este fichero. Ejecutamos:
$ cp webserver_2.0.tar.gz webserver_old/releases
$ webserver_old/bin/webserver upgrade webserver_2.0
Unpacked Release "2.0"
Installed Release "2.0"
Made Release "2.0" Permanent
12
13
http://www.erlang.org/doc/man/appup.html
http://www.erlang.org/doc/man/sys.html#Mod:system_code_change-4
149
Ecosistema Erlang
Importante
Para que el sistema no falle habrá que revisar la configuración
del nombre de nodo en el fichero vm.args dentro del directorio
files antes de realizar la generación del producto final. Es
recomendable emplear sname y solo indicar el nombre del nodo
eliminando el nombre de la máquina.
Si accedemos mediante navegador web a la URI /help veremos que el
código se ha actualizado y muestra ahora el nuevo texto de ayuda que
hemos agregado.
7. Guiones en Erlang
Los guiones son un tipo de programación en la que se desarrolla un
código de forma rápida para servir de guión a una tarea automatizada.
Normalmente por un administrador de sistemas. Dentro de las tareas más
usuales de los guiones se encuentran el lanzamiento, monitorización y
parada de aplicaciones.
Erlang permite la realización de este tipo de guiones a través de escript.
El comando escript interpreta de forma rápida códigos Erlang que deben
constar de una función main/1. Por ejemplo:
#!/usr/bin/env escript
main([]) ->
io:format("Usage: fact <number>~n~n"),
1;
main([NumberTxt]) ->
try
Number = list_to_integer(NumberTxt),
io:format("~p! = ~p~n", [Number, fact(Number)]),
0
catch
error:badarg -> main([])
end.
fact(1) -> 1;
fact(N) -> N * fact(N-1).
La función main/1 siempre recibe una lista de cadenas de texto. Si no
hay argumentos la lista está vacía.
Nota
La primera línea es conocida como shebang o hashbang. Indica el
comando con el que hay que ejecutar ese guión.
150
Ecosistema Erlang
En el código no existe declaración de módulo ni exportación de
funciones. Aún sin estar compilado puede hacer uso de cualquier librería
de Erlang además de todos los ejemplos de código vistos anteriormente.
Podemos probarlo así:
$ chmod +x fact
$ ./fact
Usage: fact <number>
$ ./fact a
Usage: fact <number>
$ ./fact 12
12! = 479001600
14
El comando rebar escriptize se encarga tomar todos los módulos
compilados de un proyecto e introducirlos en un fichero binario y
ejecutable. Esto permite la distribución fácil de ese script y su instalación
en el sistema de ficheros.
Si creamos una estructura de aplicación normal con su directorio src con
el fichero fact.erl que empleamos cambiándolo de esta forma:
-module(fact).
-export([main/1]).
main([]) ->
io:format("Usage: fact <number>~n~n"),
1;
main([NumberTxt]) ->
try
Number = list_to_integer(NumberTxt),
io:format("~p! = ~p~n", [Number, fact(Number)]),
0
catch
error:badarg -> main([])
end.
fact(1) -> 1;
fact(N) -> N * fact(N-1).
Agregamos también el fichero de aplicación siguiente:
{application, fact, [
{description, "Factorial"},
{vsn, "1.0"},
{applications,[
kernel, stdlib, inets
]}
]}.
14
Aunque hay versiones de rebar en las que la ayuda no lo muestra existe y funciona en esas mismas
funciones.
151
Ecosistema Erlang
Podemos crear nuestro guión compilado en forma de binario:
$ rebar compile escriptize
==> fact_scriptize (compile)
Compiled src/fact.erl
==> fact_scriptize (escriptize)
Si realizamos la prueba que hicimos con el guión anterior veremos
que se comporta exactamente igual. Esta forma tiene la ventaja de que
se pueden incrustar más módulos, dependencias y que el código se
encuentra ya compilado por lo que es más rápido que en el ejemplo
anterior.
8. El camino a OTP
Como ya avancé en la introducción este libro consta de dos partes.
En esta primera parte hemos visto todo lo necesario para conocer el
lenguaje y el funcionamiento de la máquina virtual de Erlang. Hemos
repasado cómo trabajar con los proyectos. Hemos formado nuestra
mente a un nuevo conocimiento y a una nueva forma de hacer las cosas.
Sin embargo en los proyectos profesionales de Erlang se emplea y con
mucha frecuencia OTP.
Lo aprendido a lo largo de estas páginas constituye una base de
conocimiento y una forma de trabajo con el lenguaje, pero ese
conocimiento debe ser ampliado a través del estudio de OTP para brindar
mejores soluciones al código escrito.
Espero que el libro haya resultado útil, ameno y que la curiosidad
despertada por Erlang haya sido satisfecha e incluso las ganas de seguir
aprendiendo con el siguiente volumen de este libro.
Hasta entonces, un saludo y suerte con el código.
152
Apéndices
Apéndice A. Instalación de Erlang
Tener la máquina virtual de Erlang operativa con todas sus características
es bastante fácil gracias a la gran cantidad de instaladores y
distribuciones preparadas que existen en la web. Las versiones oficiales
1
se ofrecen desde la página web oficial de Erlang .
En este apéndice veremos como bajar e instalar Erlang, tanto si lo
queremos hacer desde paquetes listos para funcionar directamente o
desde código fuente.
1. Instalación en Windows
Aunque siempre recomiendo GNU/Linux o incluso algún sistema BSD
para programar y desarrollar software, las preferencias de cada uno son
distintas y hay muchos usuarios y programadores que prefieren Windows
a cualquier otro sistema operativo.
2
La descarga para Windows se puede realizar desde la web de descargas
de la página oficial de Erlang. Entre los paquetes que hay para descargar
3
se puede encontrar Windows Binary File . Se trata de un instalador que
nos guiará paso a paso en la instalación.
La instalación en estos sistemas se divide en varios pasos. Se seleccionan
los paquetes a instalar y la ruta:
1
2
3
http://erlang.org/
http://www.erlang.org/download.html
También se encuentra la versión de 64 bits para los que tengan sistemas Windows de 64 bits.
154
Instalación de Erlang
Nota
La instalación de la versión R12B02 requiere de la instalación de
unas DLLs que son propiedad de Microsoft. El instalador inicia un
proceso de instalación para estas librerías en las que habrá que
aceptar las licencias y acuerdos de uso de las propias librerías.
En un futuro se plantea la eliminación de estas librerías en favor
de otras de mingw (una versión de GCC para Windows) que nos
permitirán saltar esta parte.
Seguimos con la instalación hasta que el instalador nos informe de que
ha finalizado con éxito. En el menú de inicio veremos que se ha creado
un nuevo grupo de programas. Podemos lanzar el que tiene como título
Erlang con el logotipo del lenguaje a su lado para que se muestre la
siguiente pantalla:
Ahora ya tenemos lista la consola de Erlang. Podemos tomar cualquier
ejemplo del libro para probar su funcionamiento.
2. Instalación en sistemas GNU/Linux
La mayoría de distribuciones GNU/Linux disponen de sistemas de
instalación de paquetes de forma automatizada. Erlang está disponible
por defecto en la mayoría de estas distribuciones pero, dado que estos
paquetes en muchas de estas distribuciones se marcaron como estables
155
Instalación de Erlang
hace mucho tiempo las versiones de Erlang disponibles pueden ser algo
antiguas.
2.1. Desde Paquetes Binarios
Tenemos la opción de descargar un paquete actualizado e instalarlo en
lugar del que provee por defecto la distribución que estemos usando.
Los paquetes actuales se pueden descargar desde la web de Erlang
Solutions:
https://www.erlang-solutions.com/downloads/download-erlang-otp
Las versiones para CentOS y Fedora se descargan en formato RPM y
pueden instalarse a través de la herramienta rpm.
Las versiones para Debian, Ubuntu y Raspbian se descargan en formato
DEB y pueden instalarse a través de la herramienta dpkg.
Una vez instalado podemos ejecutar desde consola el comando erl o erlc
entre otros.
2.2. Compilando el Código Fuente
Otra opción es compilar el código fuente para los sistemas en los que
no se encuentre Erlang en la última versión o se quiera disponer de
varias versiones instaladas en rutas diferentes a las que se establecen
por defecto.
En este caso habrá que descargar el último archivo comprimido de
código:
#
#
#
#
#
wget http://www.erlang.org/download/otp_src_R15B02.tar.gz
tar xzf otp_src_R15B02.tar.gz
cd otp_src_R15B02
./configure
make && make install
La compilación requiere que se disponga en el sistema de un compilador
y las librerías en las que se basa Erlang. El comando configure nos dará
pistas sobre las librerías que haya que instalar.
Importante
Sistemas como Ubuntu no disponen acceso directo como usuario
root. En su lugar se debe de acceder a root a través del comando
sudo. Para realizar la acción anterior sin que surjan problemas,
deberemos de ejecutar antes: sudo su.
156
Instalación de Erlang
3. Otros sistemas
La empresa Erlang Solutions provee paquetes de instalación para otros
sistemas como MacOS X. En este sistema podemos optar por instalar este
4
paquete o por la instalación desde otros sistemas como MacPorts .
En sistemas como OpenSolaris o BSD (FreeBSD, OpenBSD o NetBSD)
la solución más común es instalar desde código fuente tal y como se
comentó en la sección Sección 2.2, “Compilando el Código Fuente” .
4
http://www.macports.org/
157
Apéndice B. La línea de
comandos
El código de la mayoría de ejemplos del libro han sido desarrollados
en la consola o línea de comandos. Erlang como máquina virtual
dispone de esta línea de comandos para facilitar su gestión y demostrar
su versatilidad permitiendo conectar una consola a un nodo que se
encuentre en ejecución y permitir al administrador obtener información
del servidor en ejecución.
La línea de comandos es por tanto uno de los principales elementos
de Erlang. En este apéndice veremos las opciones que nos ofrece este
intérprete de comandos para facilitar la tarea de gestión. Muchas de estas
funciones ya se han ido mostrando a través de los capítulos del libro por
lo que este compendio será una referencia útil para nuestro trabajo del
día a día.
Importante
Las funciones que se listan a continuación están disponibles solo
en la línea de comandos, no es posible emplearlos en el código
de un programa convencional.
1. Registros
Los registros se comentaron en la Sección 1.6, “Registros” del Capítulo 2,
El lenguaje. En la consola se pueden gestionar los registros a través de
las siguientes funciones:
rd(R,D)
Define un registro en la línea de comandos:
> rd(state, {hits, miss, error}).
rl() / rl(R)
Muestra todos los registros definidos en la línea de comandos en el
primer caso y solo el registro pasado como parámetro en el segundo
caso. La definición se muestra como se escribiría dentro de un
fichero de código.
rf() / rf(R)
Elimina la definición de los registros cargados. La primera forma
elimina todos los registros mientras que la segunda solo elimina el
registro pasado como parámetro R.
158
La línea de comandos
rr(Modulo) / rr(Wildcard) / rr(MoW,R) / rr(MoW,R,O)
Carga los módulos de uno o varios ficheros. Los ficheros se pueden
indicar mediante el nombre de un módulo (véase m()) o el nombre
de un fichero o varios con el uso de comodines (wildcard). Se
puede agregar un segundo parámetro que indique el registro que
se desea cargar y un tercer parámetro que se usará como conjunto
1
de opciones .
2. Módulos
Indicaremos todos los comandos referentes a la compilación, carga e
información para los módulos:
c(FoM)
Compila un fichero pasando su nombre como parámetro. El nombre
proporcionado será un átomo con el nombre del módulo o una
cadena que indique el nombre del fichero, opcionalmente con su
ruta.
l(M)
Permite cargar un módulo. Conviene recordar lo ya mencionado
sobre la carga de módulos en la Sección 8, “Recarga de código” del
Capítulo 5, Procesos.
m() / m(M)
Muestra todos los módulos cargados en memoria en el primer caso
e información detallada del módulo cargado en el segundo caso.
Se muestra información como la fecha y hora de compilación, la
ruta de dónde se encuentra el módulo en el sistema de ficheros, las
funciones que exporta y las opciones de compilación.
lc([F])
Lista de ficheros a compilar.
nl(M)
Carga el módulo indicado en todos los nodos conectados.
nc(FoM)
Compila y carga el módulo o fichero en todos los nodos conectados.
y(F)
Genera un analizador Yecc, el fichero pasado como parámetro debe
de ser un fichero con sintaxis válida para Yecc.
1
Las opciones que se pueden usar con rr/3 son las mismas que se pueden emplear para la compilación.
159
La línea de comandos
3. Variables
En la línea de comandos se pueden emplear variables. Estas variables
tienen el comportamiento de única asignación igual que el código que
podemos escribir en cualquier módulo. Las siguientes funciones nos
permiten gestionar estas variables:
b()
Muestra todas las variables empleadas o enlazadas (binding) a un
valor en la línea de comandos.
f() / f(X)
Indica a la línea de comandos que olvide (forget) todas las variables
o solo la indicada como parámetro.
4. Histórico
La consola dispone de un histórico que nos permite repetir comandos
ya utilizados en la consola. El histórico es configurable y contendrá
los últimos comandos tecleados. El símbolo del sistema (o prompt) nos
indicará el número de orden que estamos ejecutando.
Además de los comandos, la consola de Erlang también almacena los
últimos resultados. El número de resultados almacenados también es
configurable.
Estas son las funciones que pueden emplearse:
e(N)
Repite el comando con orden N según el símbolo de sistema de la
consola.
h()
Muestra el histórico de comandos ejecutados.
history(N)
Configura el número de entradas que serán almacenadas como
histórico.
results(N)
Configura el número de resultados que serán almacenados como
histórico.
160
La línea de comandos
v(N)
Obtiene el resultado de la línea correspondiente pasada como
parámetro. A diferencia de e(N) el comando no se vuelve a
ejecutar, solo se muestra el resultado del comando N ejecutado
anteriormente.
5. Procesos
Estas son funciones rápidas y de gestión sobre los temas que ya se
revisaron en el Capítulo 5, Procesos:
bt(Pid)
Obtiene el trazado de pila del proceso en ejecución.
flush()
Muestra todos los mensajes enviados al proceso de la consola.
i(X,Y,Z)
Muestra información de un proceso dando sus números como
argumentos separados de la función. La información obtenida es el
estado de ejecución del proceso, procesos a los que está enlazado,
la cola de mensajes, el diccionario del proceso y memoria utilizada
entre otras opciones más.
pid(X,Y,Z)
Obtiene el tipo de dato PID de los números dados.
regs() / nregs()
Lista todos los procesos registrados (con nombre) en el nodo actual
o en todos los nodos conectados respectivamente.
catch_exception(B)
Cada ejecución se realiza mediante un evaluador. Cuando se lanza
una excepción el evaluador es regenerado por el proceso de la
consola. Esto provoca que se pierdan tablas ETS entre otras cosas. Si
ejecutamos esta función con true el evaluador captura la excepción
y no muere.
i() / ni()
Muestra todos los procesos del nodo o de todos los nodos
conectados respectivamente.
161
La línea de comandos
6. Directorio de trabajo
En cualquier momento podemos modificar el directorio de trabajo dentro
de la consola. Las siguientes funciones nos ayudan en esta y otras tareas
relacionadas:
cd(Dir)
Cambia el directorio de trabajo. Se indica una lista de caracteres con
la ruta relativa o absoluta para el cambio.
ls() / ls(Dir)
Lista el directorio actual u otro indicado como parámetro de forma
relativa o absoluta a través de una lista de caracteres.
pwd()
Imprime el directorio de trabajo actual.
7. Modo JCL
Cuando se presiona la combinación de teclas Control+G se accede a
una nueva consola. Esta consola es denominada JCL (Job Control Mode o
modo de control de trabajos). Este modo nos permite lanzar una nueva
consola, conectarnos a una consola remota, detener una consola en
ejecución o cambiar de una a otra consola.
Importante
Cada trabajo que se lanza es una consola (shell). Este modo nos
permite gestionar estas consolas. Cada nodo puede tener tantas
consolas como se quiera.
Estos son los comandos que podemos emplear en este modo:
c [nn]
Conectar a una consola. Si no se especifica un número vuelve al
actual.
i [nn]
Detiene la consola actual o la que corresponda al número que se
indique como argumento. Es útil cuando se quiere interrumpir un
bucle infinito sin perder las variables empleadas.
k [nn]
Mata la consola actual o la que corresponda al número que se
indique como argumento.
162
La línea de comandos
j
Lista las consolas en ejecución. La consola actual se indicará con un
asterisco (*).
s [shell]
Inicia una consola nueva. Si se indica el nombre de un módulo como
argumento se intentará lanzar un proceso con ese módulo como
consola alternativa.
r [node [shell]]
Indica que deseamos crear una consola en un nodo al que se
tiene conexión. Se lanza una consola en ese nodo y queda visible
en el listado de consolas. Se puede indicar también una consola
alternativa en caso de disponer de ella.
q
Finaliza la ejecución del nodo Erlang en el que estemos ejecutando
el modo JCL.
8. Salir de la consola
Para salir de la consola hay varias formas. Se puede salir presionando
dos veces la combinación de teclas Control+C, ejecutando la función de
consola q() o a través del modo JCL y su comando q.
163
Apéndice C. Herramientas
gráficas
Erlang es una máquina virtual además de un lenguaje por lo que requiere
de herramientas que le permitan gestionar sus procesos de una forma
fácil para el usuario. En el capítulo Capítulo 5, Procesos tratamos la forma
en la que listar los procesos de consola. Ahora veremos la forma de ver
estos procesos de forma gráfica, así como las tablas ETS y Mnesia y la
depuración de los procesos que ejecutemos.
1. Barra de herramientas
Para facilitar la tarea de acceder al conjunto de herramientas gráficas
disponemos de toolbar. Esta aplicación nos proporciona una ventana con
botones de acceso directo a las herramientas de la interfaz gráfica de
Erlang.
La podemos lanzar de la siguiente manera:
> toolbar:start().
Se abrirá una ventana como la siguiente:
Los cuatro botones que se pueden observar en la imagen son (de
izquierda a derecha):
tv
Table Visualizer o visor de tablas. Se emplea para poder visualizar
el contenido de las tablas ETS y las que maneja la base de datos
Mnesia.
pman
Process Manager o administrador de procesos. Es la versión gráfica
de lo que conseguimos con las funciones de consola i/0, i/1 y
otras funciones como exit/1 integradas en una única interfaz.
debugger
El depurador nos permite seguir la ejecución de un código en la
ventana de proceso y revisar los datos de sus variables en ese
momento.
164
Herramientas gráficas
appmon
Application Monitor o monitor de aplicaciones. Permite ver la carga
que supone en el nodo la aplicación y el árbol de dependencia de
procesos entre otras opciones.
Nota
En los menús podemos ver opciones como Add GS contributions
que agrega otros cuatro botones extra: el juego bonk, mandelbrot
que es un generador de fractales, el juego othello y el juego Cols.
El código fuente de estas aplicaciones está disponible junto con
el código fuente de Erlang, en el directorio:
otp_src_R15B02/lib/gs/contribs
2. Monitor de aplicaciones
El monitor de aplicaciones nos proporciona información sobre las
aplicaciones ejecutadas en la máquina virtual de Erlang. El concepto de
aplicación proviene de OTP y se comentó en el Capítulo 8, Ecosistema
Erlang.
La aplicación al lanzarse genera un proceso principal que puede estar
enlazado con otros. Esta relación de procesos es la que se monitoriza
bajo el nombre propio de cada aplicación que se ejecuta.
En el gráfico adjunto se puede ver el nombre del nodo (con fondo negro)
del que cuelgan todas las aplicaciones que se han sido lanzadas.
Estas aplicaciones son botones que si se presionan nos muestran
una jerarquía de procesos. Cada proceso se identifica con su nombre
registrado o con su PID en caso de no disponer de nombre. Sobre cada
proceso podemos ejecutar una serie de acciones que se representan con
los botones superiores que se pueden ver en la ventana:
165
Herramientas gráficas
La barra superior de botones es como una barra de herramientas.
Siempre hay un botón que aparece como marcado indicando así la
opción que se hará sobre cualquier proceso cuando se haga clic sobre
él. Las acciones son:
Info
Se abrirá una nueva ventana que mostrará la información del
proceso.
Send
Permite enviar un mensaje al proceso. Es equivalente a:
Pid ! Mensaje
Trace
Cada mensaje recibido al proceso marcado es impreso por la
consola. Es equivalente a la función erlang:trace/3.
Kill
Envía la señal de finalización al proceso. Equivalente a exit/1.
El menú de la ventana principal dispone además de otras opciones en
su menú Actions como: Reboot, Restart, Stop y Ping. Estas opciones son
útiles cuando se emplea la herramienta con otros nodos, ya que permite
reiniciar estos nodos y cuando se vean como desconectados, hacerles
ping para volver a conectarlos.
166
Herramientas gráficas
Nota
El monitor de aplicaciones no solo puede visualizar el estado de
las aplicaciones del nodo en el que fue lanzado, sino que también
puede ver el estado de las aplicaciones de otros nodos a los que
se encuentre conectado.
Esto es posible a través del menú Nodes, donde se listarán todos
los nodos a los que esté conectada la máquina virtual de Erlang.
3. Gestor de procesos
Una forma simple de gestionar los procesos que se ejecutan en la
máquina virtual de Erlang es a través de su gestor de procesos gráfico.
El gestor de procesos nos permite visualizar de forma rápida y en
tiempo real los procesos que se están ejecutando en la máquina virtual
de Erlang su PID, nombre registrado (si tienen), la función actual que
están ejecutando, los mensajes que tienen pendientes en el buźon, las
reducciones aplicadas y el tamaño que ocupa el proceso.
Esta interfaz permite activar el trazado de procesos en una ventana
aparte, visualizando en ella los mensajes entrantes al proceso que se esté
trazando. Para trazar un proceso solo hay que seleccionarlo y a través del
menú Trace seleccionar la opción Trace selected process.
Nota
El gestor de procesos no solo puede visualizar y gestionar los
procesos del nodo en el que fue lanzado, sino que también puede
ver y gestionar los procesos de otros nodos a los que se encuentre
conectado.
Esto es posible a través del menú Nodes, donde se listarán todos
los nodos a los que esté conectada la máquina virtual de Erlang.
167
Herramientas gráficas
4. Visor de tablas
Las tablas ETS y las que se crean con la base de datos de Mnesia están
disponibles para su visualización a través de esta interfaz. Por defecto
nos muestra un listado de las tablas ETS que hay creadas en el nodo.
La información que se muestra en la tabla principal es: nombre de la
tabla, identificador de la tabla, PID del propietario de la tabla, nombre
registrado del proceso propietario (si tiene) y tamaño de la tabla (en
número de entradas).
A través del menú View se puede conmutar entre la visualización de
las tablas ETS y las tablas Mnesia. En el menú Options hay opciones
referentes a la visualización de las tablas en el listado: refrescar, ver
tablas del sistema o no legibles son algunas de las opciones.
Si hacemos doble clic sobre cualquier entrada de la tabla principal, se
abrirá una segunda ventana en la que se mostrará el contenido de la tabla
ETS seleccionada. Se visualiza como si fuese una hoja de cálculo y se
marca sobre las columnas con el símbolo de una llave cuál es la clave
primaria de la tabla.
Esta es una ventana de visualización y edición por lo que podemos
seleccionar las entradas de la tabla y editarlas o eliminarlas a través de
las opciones disponibles en el menú Edit.
Nota
El visualizador de tablas no solo puede visualizar y gestionar las
tablas ETS y Mnesia del nodo en el que fue lanzado, sino que
también puede hacer lo mismo con otros nodos a los que se
encuentre conectado.
Esto es posible a través del menú File, opción Nodes..., donde
se listarán todos los nodos a los que esté conectada la máquina
virtual de Erlang.
168
Herramientas gráficas
5. Observer
El programa observer es una unificación de pman, tv y appmon además
de otras características más en un entorno wxWindow mejorado con
respecto a los anteriores.
Podemos lanzar este programa de la siguiente manera:
> observer:start().
La ventana abierta dispone de un conjunto de pestañas o lengüetas que
disponen de todas las funcionalidades del conjunto de programas vistos
en las secciones anteriores:
System
Ofrece información del sistema: versión y arquitectura de la
máquina virtual de Erlang, CPUs e hilos en ejecución, uso de la
memoria y estadísticas de uso.
Load Charts
Se presentan tres gráficos en tiempo real: utilización del
programador de tareas (equivalente a la carga del procesador), uso
de la memoria y uso de la Entrada/Salida.
Applications
Ofrece la misma información que appmon. Visualiza el listado de
aplicaciones a la izquierda y el árbol de procesos a la derecha, previa
selección de una de las aplicaciones.
Processes
Listado de procesos tal y como se presentaban también en pman.
Se muestra una tabla con los procesos activos y su información:
PID, nombre o función inicial, reducciones, memoria, mensajes
encolados y función en ejecución actualmente.
Table Viewer
Lista las tablas ETS y Mnesia como lo hace la aplicación tv. A
diferencia de tv, observer no permite la creación de nuevas tablas
ETS aunque sí permite modificar y/o eliminar entradas de las tablas
existentes.
Trace Overview
Tanto la aplicación appmon como pman permiten trazar procesos.
La aplicación observer unifica esto en una sola pestaña cambiando
la forma en la que realizar las trazas de los procesos.
169
Herramientas gráficas
La ventana tiene este aspecto:
Nota
Observer no solo puede actuar en el nodo en el que es lanzado,
sino que también puede interactuar con otros nodos a los que se
encuentre conectado.
Esto es posible a través del menú Nodes, donde se listarán todos
los nodos a los que esté conectada la máquina virtual de Erlang
además de dar la posibilidad de conectar con otros nodos que no
se encuentren en el listado.
6. Depurador
Esta es quizás la herramienta más importante y que mejor se debería
de aprender a utilizar para poder analizar el código realizado de una
forma más cercana al dato. Aunque las trazas serán suficientes en algunos
casos, siempre es mejor depurar un código que nos origina un error que
no conseguimos entender bien que abarcar el problema al modo pruebaensayo-error.
El depurador se lanza desde consola así:
> debugger:start(local).
La ventana que se abre tendrá esta forma:
170
Herramientas gráficas
La forma más sencilla de emplear el depurador es a través del menú
Module, opción Interpret... cargar un fichero de código fuente. En el
propio menú Module debe de agregarse después una opción con el
nombre del módulo cargado.
Tras esto, presionamos los tres cuadros de verificación visibles bajo el
cuadro de la ventana donde aparece el nombre del módulo: First Call, On
Break y On Exit. Con esto conseguimos que el depurador se active cuando
se suceda cualquiera de estos tres eventos sobre el módulo.
En la consola de Erlang ejecutamos una función del módulo. Vemos que
se abrirá otra ventana de depuración con el código en la parte principal,
una botonera en la parte media con los botones: Next, Step, Finish, Where,
Up y Down.
171
Herramientas gráficas
La parte inferior de la ventana muestra un listado de las variables del
contexto de la función que se está depurando a la derecha. En la parte
izquierda hay un evaluador que permite escribir expresiones simples
para comprobación de datos.
Durante la ejecución la ventana principal mostrará la información de los
procesos que hay en ejecución, la llamada que los inició, el nombre del
proceso, el estado del proceso (ejecución, ocioso u otro) e información
sobre la ejecución del proceso.
Nota
El depurador se puede lanzar en dos modos: local o global. Por
defecto se lanza de modo global, por lo que cualquier módulo que
se interprete será mostrado en la ventana del monitor.
No es aconsejable lanzar el depurador en un cluster de más de dos
nodos, ya que la ejecución concurrente de un mismo módulo en
varios nodos al mismo tiempo podría llevar a un funcionamiento
inconsistente.
Tomando ejemplos de los últimos capítulos podemos hacer pruebas de
ejecución del código, probar opciones de compilación, ver los procesos,
la evaluación de expresiones, el contenido de las variables y otros
aspectos que nos ayuden a aprender a utilizar bien esta herramienta.
Para más información sobre la misma:
http://www.erlang.org/doc/apps/debugger/debugger_chapter.html
172
173

Documentos relacionados