Biblioteca

Erlang/OTP

Volumen I: Un Mundo Concurrente

Manuel Angel Rubio Jiménez

Erlang/OTP, Volumen I: Un Mundo Concurrente por Manuel Ángel Rubio Jiménez se encuentra bajo una Licencia Creative Commons Reconocimiento-NoComercial-CompartirIgual 3.0 Unported.

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.


Tabla de contenidos

Prólogo
Introducción
Acerca del autor
Acerca de los Revisores
Acerca del libro
Objetivo del libro
¿A quién va dirigido este libro?
Estructura de la colección
Nomenclatura usada
Agradecimientos
Más información en la web
1. Lo que debes saber sobre Erlang
¿Qué es Erlang?
Características de Erlang
Historia de Erlang
Desarrollos con Erlang
Sector empresarial
Software libre
Erlang y la Concurrencia
El caso de Demonware
Yaws contra Apache
2. El lenguaje
Tipos de Datos
Átomos
Números Enteros y Reales
Variables
Listas
Tuplas
Registros
Imprimiendo por pantalla
Fechas y Horas
3. Expresiones, Estructuras y Excepciones
Expresiones
Expresiones Aritméticas
Expresiones Lógicas
Precedencia de Operadores
Estructuras de Control
Concordancia
Estructura case
Estructura if
Listas de Comprensión
Excepciones
Recoger excepciones: catch
Lanzar una excepción
La estructura try...catch
Errores de ejecución más comunes
4. Las funciones y módulos
Organización del código
Ámbito de las funciones
Polimorfismo y Concordancia
Guardas
Clausuras
Programación Funcional
Recursividad
Ordenación por mezcla (mergesort)
Ordenación rápida (quicksort)
Funciones Integradas
5. Procesos
Anatomía de un Proceso
Ventajas e inconvenientes
Lanzando Procesos
Bautizando Procesos
Comunicación entre Procesos
Procesos Enlazados
Monitorización de Procesos
Recarga de código
Gestión de Procesos
Nodos Erlang
Procesos Remotos
Procesos Locales o Globales
RPC: Llamada Remota a Proceso
Diccionario del Proceso
6. ETS, DETS y Ficheros
ETS
Tipos de Tablas
Acceso a las ETS
Creación de una ETS
Lectura y Escritura en ETS
Match: búsquedas avanzadas
Eliminando tuplas
ETS a fichero
DETS
Tipos de Tablas
Crear o abrir una DETS
Manipulación de las DETS
De ETS a DETS y viceversa
Ficheros
Abriendo y Cerrando Ficheros
Lectura de Ficheros de Texto
Escritura de Ficheros de Texto
Lectura de Ficheros Binarios
Escritura de Ficheros Binarios
Acceso aleatorio de Ficheros
Lecturas y Escrituras por Lotes
Gestión de Ficheros
Nombre del fichero
Copiar, Mover y Eliminar Ficheros
Permisos, Propietarios y Grupos
Gestión de Directorios
Directorio de Trabajo
Creación y Eliminación de Directorios
¿Es un fichero?
Contenido de los Directorios
7. Comunicaciones
Conceptos básicos de Redes
Direcciones IP
Puertos
Servidor y Cliente UDP
Servidor y Cliente TCP
Servidor TCP Concurrente
Ventajas de inet
8. Ecosistema Erlang
Iniciar un Proyecto
Instalar rebar
Escribiendo el Código
Compilar y Limpiar
Creando y lanzando una aplicación
Dependencias
Liberar y Desplegar
Actualizando en Caliente
Guiones en Erlang
El camino a OTP
Apéndices
A. Instalación de Erlang
Instalación en Windows
Instalación en sistemas GNU/Linux
Desde Paquetes Binarios
Compilando el Código Fuente
Otros sistemas
B. La línea de comandos
Registros
Módulos
Variables
Histórico
Procesos
Directorio de trabajo
Modo JCL
Salir de la consola
C. Herramientas gráficas
Barra de herramientas
Monitor de aplicaciones
Gestor de procesos
Visor de tablas
Observer
Depurador

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;

  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

Introducción

 

Sorprendernos por algo es el primer paso de la mente hacia el descubrimiento.

 
 --Louis Pasteur

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 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 que he conocido recientemente del equipo que mantiene Erlware, muy orientado al framework OTP y la metodología que propone.

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 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.

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 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.

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 nuevas herramientas que va ganando cada vez más relevancia en los entornos mencionados.

¿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.

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:

  • 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]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.

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]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]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.

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 apoyado en el desarrollo de mis habilidades en otros aspectos de la vida.

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

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.

¿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.

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 pseudo-compilado de Erlang, que le proporciona todas las características de distribución y comunicación de procesos, es también una máquina que interpreta un pseudocódigo máquina[1] 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.

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.

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. 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 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 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.

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í, 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:

  • 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]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 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.

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.

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.

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 de empresas es Wooga.

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ó 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 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.

En la web de Aprendiendo Erlang mantienen un listado mixto de software libre y empresas que emplean Erlang.

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]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

    • 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.

    • 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.

    • 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.

    • 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

    • 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

    • 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.

    • BeepBeep, es un framework inspirado en Rails y Merb aunque sin integración con base de datos.

    • 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.

    • 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.

    • 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 través de un sistema ORM[2].

  • CMS (Content Management System)

    • Zotonic, sistema CMS[3] 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

    • 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

    • 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.

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 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.

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 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.

Yaws contra Apache

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 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.



[1] O trozos de código nativo si se emplea HiPE.

[2] 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.

[3] Content Management System, Sistema de Administración de Contenido

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:

[ 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.

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.

Á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 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]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.

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).
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]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.

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
> 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]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 el comportamiento de dicha función es muy fácilmente verificable[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.

¿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.
[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.

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]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:

> "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.

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>>.
<<"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.

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-Signo-Tipo-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>>.
    <<"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]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

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. 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]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.

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).
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]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.

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}).

Para declarar un registro desde un archivo el formato es el siguiente:

-record(agenda, {nombre, apellidos, telefono}).
[Nota]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]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.
"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]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.

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]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:

> 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:

> 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]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.

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 en formato POSIX[5], 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).

[Nota]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 (para representación interna) es contado desde el año cero[6], en lugar de 1970. Esto da la posibilidad de dar fechas anteriores a 1970.



[4] Muestra de ello es dialyzer, una buena herramienta para comprobar el código escrito en Erlang.

[5] 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.

[6] 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.

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.

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.

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

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

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:

> 1 bxor 2.
3
> 1 bxor 3.
2
> 3 band 6.
2
> 2#011 bor 2#100.
7
> (bnot 2#101) band 2#11.
2

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 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]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.

Precedencia de Operadores

El orden de los operadores para Erlang de más prioritario a menos prioritario es el siguiente:

OperadorDescripción
:Ejecución de funciones
#Resolución de registros
+ - bnot notUnitarios
/ * div rem band andDivisión, Multiplicación e Y lógico.
+ - bor bxor bsl bsr or xorSuma, resta y O inclusivo y exclusivo.
++ --Agrega/Sustrae de conjuntos/listas.
== /= =< < >= > =:= =/=Comparaciones
andalsoY lógico con comprobación vaga
orelseO lógico con comprobación vaga
= !Asignación y Paso de mensaje
catchCaptura de errores

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.

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]

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 cabeza-cola ([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.

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
    {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 concordancia. Esto es lo que se conoce como guardas[7]. 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 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.

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 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]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.

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, 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]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]]
> [ 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.

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.

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.

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]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í.

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"}

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.

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).

[Nota]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.

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.

> 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).
** 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 Eficiencia de Erlang (en inglés).

[Importante]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]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.



[7] Esta expresión inglesa se ha traducido en sitios como aprendiendo erlang como guardas.

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.

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 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]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.

[Importante]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.

Á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 módulo proplists de modo que no tengamos que llamarla de forma fully qualified[8].

[Nota]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.

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 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]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.

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.

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:

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:

-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]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 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.

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].
> 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 (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).
[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.

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.

[Nota]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.

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]}

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).

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]

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).
-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]

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).

[Nota]Nota

Robert Virding, uno de los creadores/fundadores/inventores de Erlang, comentó en un artículo de su blog[9], 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.



[8] 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.

[9] http://rvirding.blogspot.com.es/2009/10/what-are-bifs.html

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.

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), 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]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.

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 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 en ejecución y eleva ese límite a 2 millones[10].

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.

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).

Si almacenásemos el identificador de proceso llamado comúnmente PID[11] 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.

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

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.

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]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.

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]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} ->
            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 en este lenguaje. A continuación iremos ampliando y matizando aún más lo visto en este apartado.

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]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).

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
> 
=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),
    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]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.

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 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} ->
            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 ....

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.

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]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.

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

Esta vez la llamada update nos ahorra el tener que hacer otra llamada adicional para que se ejecute el código nuevo.

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 propiedades[12]: 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.

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.

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)> nodes().
[]
(test1@bosqueviejo)> Remoto = test2@bosqueviejo,
(test1@bosqueviejo)> net_kernel:connect_node(Remoto).
true
(test1@bosqueviejo)> nodes().
[test2@bosqueviejo]

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 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ó.

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]Nota

Hay muchos casos en los que el módulo global puede tener un rendimiento bastante bajo, o incluso hasta defectuoso. Por esta razón han aparecido sustitutos como gproc (que requiere del parcheo de parte del código OTP de Erlang), o módulos que no requieren de ninguna modificación en la base como nprocreg.

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.

Cualquier código que se ejecute a través de este sistema de RPC[13] 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.

[Nota]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.

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.



[10] 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.

[11] PID, siglas de Process ID o Identificador de Proceso.

[12] 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

[13] Son las siglas de Remote Procedure Call, o Llamada a Proceso Remoto.

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.

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 propios del lenguaje[14]. 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.

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.

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.

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]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).

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}

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 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]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.

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:

> 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.

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.

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 el gestor de las ETS. Son las llamadas variables sin importancia[15] 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]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 misma potencia que nos brindan las listas de compresión sobre las listas que ya vimos en “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}
    }],
    [ '$$' ]
}

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)

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.

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.

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.

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.

Este sistema de almacenaje es el mismo que emplea Mnesia[16], 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.

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]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.

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.

{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]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 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]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.

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.

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}

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]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.

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.

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.

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 oficial de la función open/2.

Un ejemplo de apertura de un fichero tan famoso[17] 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}
[Nota]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]Importante

Es importante que cerremos todos los ficheros que abramos ya que esto repercute, no sólo en un uso innecesario de los recursos de los descriptores de ficheros[18], sino también de procesos, ya que cada fichero abierto de un modo no raw lleva asociado un proceso Erlang.

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:

> {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]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").
{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.

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.

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 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 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 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.

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

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:

> {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.

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:

[
    {{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]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.

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, propietario[19], copiar el fichero, truncarlo o eliminarlo.

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).                 
".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"

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]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 y reiniciar sus punteros a la posición cero, esto lo podemos realizar mediante el uso de la función truncate/1.

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éricoPermisoUsuario
8#00400LecturaPropietario
8#00200EscrituraPropietario
8#00100EjecuciónPropietario
8#00040LecturaGrupo
8#00020EscrituraGrupo
8#00010EjecuciónGrupo
8#00004LecturaOtros
8#00002EscrituraOtros
8#00001EjecuciónOtros

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 grupo. Los parámetros de UID y GID se dan en formato entero[20].

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 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.

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 albergarse en un directorio está limitado[21].

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.

Veremos a continuación las funciones relativas a la gestión de directorios bajo los conceptos en los que se emplean.

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.

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 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]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")

¿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.

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.



[14] 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.

[15] 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.

[16] 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.

[17] 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.

[18] Son las estructuras del sistema operativo que se emplean para designar que un programa tiene un fichero abierto.

[19] 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.

[20] 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.

[21] 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.

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.

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 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.

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. El valor decimal de cada octeto puede estar entre 0 y 255[22].

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]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 las direcciones de clase A para los gobiernos de todo el mundo[23] 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).

  • 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 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.

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 puertos[24].

[Nota]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            80/tcp
http            80/udp
ftp-data        20/tcp
ftp             21/tcp
domain          53/tcp
domain          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.

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]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.

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 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.

[Nota]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:

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 
618  [...] 0    11   <0.38.0> *:33361       [...] BOUND DGRAM 
ok

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 ejecutar gen_udp:recv/2 es cuando se obtiene la información que llega del servidor.

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]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 este enlace

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:

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 flush/0 aparecerán los mensajes recibidos al proceso de la consola[25].

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]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.

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 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 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).

La función cli_send/2 permite conectarse a un puerto local[26], 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.

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 la capacidad de interpretar los paquetes recibidos a través de TCP o UDP y enviarlos como mensaje al proceso ya procesados.

Según la documentación de inet los formatos que procesa son: CORBA, ASN-1, SunRPC, FastCGI, Line, TPKT y HTTP.

[Nota]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:

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} ->
            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]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}.



[22] 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.

[23] Aunque en el pasado se le hayan otorgado a empresas de gran envergadura como, por ejemplo, Hewlett Packard.

[24] 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.

[25] 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.

[26] En este ejemplo no hemos empleado direcciones IP, por lo que se emplea por defecto la IP local o 127.0.0.1.

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.

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 del mundo Erlang. La empresa Basho[27] 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.

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]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.

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 OTP-
compliant apps.

Ahora solo nos falta copiar el script generado a una ruta visible por nuestro PATH. Normalmente como super usuario en sistemas de tipo Unix[28] en una ruta como /usr/bin, /usr/local/bin o /opt/local/bin.

Para asegurarnos de que es accesible podemos ejecutarlo en la consola.

[Nota]Nota

La utilidad rebar se encuentra también disponible para Windows 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.

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);
        {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 “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">>;
        _ -> <<"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, webserver1, [
    {description2, "Erlang Web Server"},
    {vsn3, "1.0"},
    {applications4,[
        kernel, stdlib, inets
    ]}
]}.

1

El nombre que tendrá la aplicación.

2

Como descripción se especifica un texto. Es deseable que no sea muy extenso. Se puede poner el nombre completo de la aplicación.

3

La versión de la aplicación. Se puede especificar de la forma que se desee.

4

Aplicaciones que deben de iniciarse antes de iniciar la nuestra. Dependencias.

[Nota]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.

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.

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

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.

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 un comportamiento[29] 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).
-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, []}}1
]}.

1

Línea que indica el módulo de comportamiento application que se hará cargo del inicio y parada de la aplicación.

En la consola agregaremos un par de argumentos más[30]:

$ 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).

Dependencias

En Internet existen repositorios con miles de librerías para Erlang. Los más representativos son github.com y bitbucket. En estos sitios 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. 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)}.

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
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]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"}
    }
]}.

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.

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]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:

{sub_dirs, ["apps/*"]}.
[Nota]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 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.

{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 derivados[31].

ebin

Se incluyen todos los módulos que estén en el directorio ebin de la aplicación y los derivados[31].

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 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]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}]}
]}.

{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
/tmp/rel$ webserver/bin/webserver ping
pong
/tmp/rel$ webserver/bin/webserver attach
Attaching to /tmp/rel/webserver/erlang.pipe.1 (^D to exit)

(webserver@127.0.0.1)1>
[Importante]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]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.

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 “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
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.

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
    {"1.0", [ 2
        {load_module,fileserver} 3
    ]}
], [
    {"1.0", [ 4
        {load_module,fileserver}
    ]}
]}.

1

La versión que va a ser instalada.

2

Bloque que indica las acciones a llevar para pasar de la versión 1.0 a la 2.0.

3

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).

4

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.

[Nota]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 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 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
[Importante]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.

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]Nota

La primera línea es conocida como shebang o hashbang. Indica el comando con el que hay que ejecutar ese guión.

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

El comando rebar escriptize[32] 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
    ]}
]}.

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.

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.



[27] Basho Technologies es una empresa estadounidense que desarrolla la base de datos Riak.

[28] Sistemas Unix o tipo Unix como BSD, Linux, MacOS X u OpenSolaris entre otros.

[29] 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.

[30] Los argumentos usados para la línea de comandos se pueden revisar en el Apéndice B, La línea de comandos.

[31] Se refiere a todos los módulos que no estén incluídos pero que reltool detecte que puedan ser utilizados.

[32] Aunque hay versiones de rebar en las que la ayuda no lo muestra existe y funciona en esas mismas funciones.

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 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.

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.

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 se puede encontrar Windows Binary File[33]. 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:

[Nota]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.

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 hace mucho tiempo las versiones de Erlang disponibles pueden ser algo antiguas.

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.

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]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.

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 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 “Compilando el Código Fuente”.



[33] También se encuentra la versión de 64 bits para los que tengan sistemas Windows de 64 bits.

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]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.

Registros

Los registros se comentaron en la “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.

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 de opciones[34].

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 “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.

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.

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.

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.

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.

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.

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]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.

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.

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.



[34] Las opciones que se pueden usar con rr/3 son las mismas que se pueden emplear para la compilación.

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.

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.

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]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

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:

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.

[Nota]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.

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]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.

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]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.

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.

La ventana tiene este aspecto:

[Nota]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.

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 prueba-ensayo-error.

El depurador se lanza desde consola así:

> debugger:start(local).

La ventana que se abre tendrá esta forma:

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.

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]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

Colofón