3. Tabla de contenidos
1.-¿Por qué Scala? 1
1.1.-¿Qué es Scala? 1
1.1.1.-Orientación a objetos 2
1.1.2.-Lenguaje funcional 2
1.1.3.-Lenguaje multiparadigma 3
1.1.4.-Lenguaje extensible y escalable 3
1.1.5.-Ejecución sobre la JVM 4
1.2.-En crisis 4
1.2.1.-Ley de Moore 4
1.2.2.-Programando para multinúcleo 4
1.3.-Objetivos 5
2.-Fundamentos de Scala 7
2.1.-Clases y objetos 7
2.2.-Reglas de inferencia de puntos y coma 8
2.3.-Singleton objects 9
2.4.-Objetos funcionales 9
2.4.1.-Números racionales 9
2.4.2.-Constructores 10
2.4.3.-Sobreescritura de métodos 10
2.4.4.-Precondiciones 10
2.4.5.-Atributos y métodos 11
2.4.6.-Operadores 12
2.5.-Funciones y closures 12
2.5.1.-Funciones first-class 13
2.5.2.-Closures 13
2.5.3.-Tail recursion 14
2.6.-Currying 14
2.7.-Traits 15
2.7.1.-¿Cómo funcionan los traits? 15
2.7.2.-Ejemplo: objetos rectangulares 16
2.7.3.-Uso de traits como stackable modifications 17
2.7.4.-Traits: ¿si o no? 19
2.8.-Patrones y clases case 20
2.8.1.-Clases case 20
2.8.2.-Patrones: estructura y tipos 21
2.8.2.1.-Patrones wildcard 21
2.8.2.2.-Patrones constantes 21
2.8.2.3.-Patrones variables 21
iii
4. 2.8.2.4.-Patrones constructores 22
2.8.2.5.-Patrones de secuencia 22
2.8.2.6.-Patrones tipados 22
2.9.-Conclusiones 23
3.-Actors y concurrencia 24
3.1.-Problemática 24
3.2.-Modelo de actores 25
3.3.-Actores en Scala 25
3.3.1.-Buenas prácticas 26
3.3.1.1.-Ausencia de bloqueos 26
3.3.1.2.-Comunicación exclusiva mediante mensajes 27
3.3.1.3.-Mensajes inmutables 27
3.3.1.4.-Mensajes autocontenidos 27
3.4.-Un ejemplo completo 28
3.4.1.-Especificación del problema 28
3.4.2.-Implementación del producer y coordinator 28
3.4.3.-Interfaz iterator 29
4.-Conclusiones y trabajo futuro 31
4.1.-Conclusiones 31
4.2.-Líneas de trabajo 32
A.-Modelo de objetos de Scala 33
B.-Producers 34
B.1.-Código fuente completo 34
Bibliografía 37
iv
5. ¿Por qué Scala?
Capítulo 1. ¿Por qué Scala?
Durante este capítulo se cubrirán aspectos relativos como qué es Scala, características de
alto nivel del lenguaje o por qué deberíamos escoger Scala como nuestro siguiente lenguaje
de programación. En la parte final del mismo analizaremos los objetivos perseguidos con el
desarrollo de este trabajo así como la estructura del mismo.
Debido a la creciente proliferación de una gran cantidad de diferentes lenguajes en
las plataformas JVM, .NET, OTP entre otras muchas ha surgido el dilema entre los
desarrolladores acerca de cuál debería ser el siguiente lenguaje de progragramación que se
debería aprender. Entre la amplia variedad de lenguajes disponibles como Groovy, Erlang,
Ruby o F# ¿por qué deberíamos aprender Scala ?.
Durante este capítulo analizaremos las características de alto nivel del lenguaje
estableciendo una comparación con aquellos lenguajes con los que estemos más
familiarizados. Los programadores provenientes de la orientación a objetos así como
aquellos cuyo origen es la programación funcional rápidamente se sentirán cómodos con
Scala dado que este lenguaje soporta ambos paradigmas. Scala es uno de esos extraños
lenguajes en los que se integran de manera satisfactoria las características de los lenguajes
orientados a objetos y los funcionales.
1.1. ¿Qué es Scala?
Scala es un lenguaje de propósito general diseñado para expresar los patrones de
programación más comunes de una manera sencilla, elegante y segura. Integra de manera
sencilla características de orientación a objetos y lenguajes funcionales, permitiendo de este
modo que los desarrolladores puedan ser más productivos. Su creador, Martin Odersky, y su
equipo comenzaron el desarrollo de este nuevo lenguaje en el año 2001, en el laboratorio
de métodos de programación en EPFL1
Scala hizo su aparación pública sobre la plataforma JVM (Java Virtual Machine) en enero de
2004 y unos meses después haria lo propio sobre la plataforma .NET.
Aunque se trata de un elemento relativamente novedoso dentro del espacio de los lenguajes
de programación, ha adquirido una notable popularidad la cual se acrecenta día tras día.
1
École Polytechnique Fédérale de Lausanne
1
6. ¿Por qué Scala?
1.1.1. Orientación a objetos
La popularidad de lenguajes como Java, C# o Ruby han hecho que la programación
orientada a objetos sea un paradigma ampliamente aceptado entre la mayoría de
desarrolladores. Aunque existen numerosos lenguajes orientados a objetos en el ecosistema
actual únicamente podríamos encajar unos pocos si nos ceñimos a una definición estricta
de orientación a objetos. Un lenguaje orientado a objetos "puro" debería presentar las
siguientes características:
• Encapsulamiento/ocultación de información.
• Herencia.
• Polimorfismo/Enlace dinámico.
• Todos los tipos predefinidos son objetos.
• Todas las operaciones son llevadas a cabo mediante en envío de mensajes a objetos.
• Todos los tipos definidos por el usuario son objetos.
Scala da soporte a todas las características anteriores mediante la utilización de un modelo
puro de orientación a objetos muy similar al presentado por Smalltalk (lenguaje creado por
Alan Kay sobre el año 1980).2
De manera adicional a todas las caracteríscticas puras de un lenguaje orientado a objetos
presentadas anteriormente, Scala añade algunas innovaciones en el espacio de los lenguajes
orientados a objetos:
• Composición modular de mixin. Mecanismo que permite la composición de clases para el
diseño de componentes reutilizables evitando los problemas presentados por la herencia
múltiple. Similar a los interfaces Java y las clases abstractas. Por una parte se pueden
definir múltiples "contratos" (del mismo modo que los interfaces). Por otro lado, se
podrían tener implementaciones concretas de los métodos.
• Self-type. Los mixin no dependen de ningún método y/o atributo de aquellas clases con las
que se está entremezclando aunque en determinadas ocasiones será necesario hacer uso
de las mismas. Esta capacidad es conocida en Scala como self-type3
• Abstracción de tipos. Existen dos mecanismos principales de abstracción en los lenguajes
de programación: la parametrización y los miembros abstractos. Scala soporta ambos
estilos de abstracción de manera uniforme para tipos y valores.
1.1.2. Lenguaje funcional
La programación funcional es un paradigma en el que se trata la computación como la
evaluación de funciones matemáticas y se evitan los programas con estado y datos que
puedan ser modificados. Se ofrece adopta una visión más matemática del mundo en el que
2
http://en.wikipedia.org/wiki/Smalltalk
2
7. ¿Por qué Scala?
los programas están compuestos por numerosas funciones que esperan una determinada
entrada y producen una determinada salida y, en muchas ocasiones, otras funciones.
Otro de los aspectos de la programación funcional es la ausencia de efectos colaterales
gracias a los cuales los programas desarrollados son mucho más sencillos de comprender y
probar. Adicionalmente, se facilita la programación concurrente, evitando que se convierta
en un problema gracias a la ausencia de cambio.
Los lenguajes de programación que soportan este estilo de programación deberían ofrecer
algunas de las siguientes características:
• Funciones de primer nivel.
• Closures
• Asignación simple.
• Evaluación tardía
• Inferencia de tipos
• Optimización del tail call
• Efectos monadic
Es importante tener claro que Scala no es un lenguaje funcional puro dado que en este
tipo de lenguajes no se permiten las modificaciones y las variables se utilizan de manera
matemática.4. Scala da soporte tanto a variables inmutables (tambien conocidas como
values) como a variables que apuntan estados no permanentes
1.1.3. Lenguaje multiparadigma
Scala ha sido el primero en incorporar y unificar la programación funcional y la orientación
a objetos en un lenguaje estáticamente tipado. La pregunta es por qué necesitamas más de
un estilo de programación.
El objetivo principal de la computación multiparadigma es ofrecer un determinado conjunto
de mecanismos de resolución de problemas de modo que los desarrolladores puedan
seleccionar la técnica que mejor se adapte a las características del problema que se está
tratando de resolver.
1.1.4. Lenguaje extensible y escalable
Uno de los principales objetivos del diseño de Scala es la construcción de un lenguaje que
permita el crecimiento y la escalabilidad en función de la exigencia del desarrollador. Scala
puede ser utilizado como lenguaje de scripting así como también se puede adoptar en el
proceso de construcción de aplicaciones empresariales. La conjunción de su abastracción de
4
Un ejemplo de lenguaje funcional puro sería Haskell
3
8. ¿Por qué Scala?
componentes, su sintaxis reducida, el soporte para la orientación a objetos y funcional han
contribuido a que el lenguaje sea más escalable.
1.1.5. Ejecución sobre la JVM
La características más relevante de Java no es el lenguaje sino su máquina virtual (JVM), una
pulida maquinaria que el equipo de HotSpot ha ido mejorando a lo largo de los años. Puesto
que Scala es un lenguaje basado en la JVM se integra a la perfección dentro con Java y su
ecosistema (herramientas, IDEs, librerías, . . .) por lo que no será necesario desprenderse de
todas las inversiones hechas en el pasado.
El compilador de Scala genera bytecode siendo indistinguible, a este nivel, el código escrito
en Java y el escrito en Scala. Adicionalmente, puesto que se ejecuta sobre la JVM, se beneficia
del rendimiento y estabilidad de dicha plataforma. Y siendo un lenguaje de tipado estático
los programas construidos con Scala se ejecutan tan rápido como los programas Java.
1.2. En crisis
A pesar de las altas prestaciones que los procesadores están adquiriendo, los desarrolladores
software encuentran los mecanismos para agotarla. El motivo es que, gracias al software,
se están resolviendo problemas muy complejos, y esta tendencia continuará creciendo, al
menos en el futuro cercano.
La pregunta clave es si los fabricantes de procesadores serán capaces de sostener la demanda
de potencia y velocidad exigida por los desarrolladores.
1.2.1. Ley de Moore
Si nos hacemos eco de la ley postulada por Moore por el año 19655, el número de transistores
por circuito integrado se duplica cada dos años aproximadamente. Sin embargo, muchos
fabricantes están tocando techo con esta ley6 y están apostando por los procesadores
multinúcleo. Las buenas noticias es que la potencia de los procesadores seguirá creciendo
de manera notable aunque las malas noticias es que los programas actuales y entornos de
desarrollo necesitarán cambiar para hacer uso de las ventajas ofrecidas por una CPU con
varios núcleos.
1.2.2. Programando para multinúcleo
¿Cómo se puede beneficiar el software de la nueva relución iniciada por los procesadores
multimedia?
5
Concretamente la fecha data del 19 de Abril de 1965
6
http://www.gotw.ca/publications/concurrency-ddj.htm
4
9. ¿Por qué Scala?
Concurrencia. La concuncurrencia será, si no lo es ya, el modo en el que podremos
escribir soluciones software que nos permitan resolver problemas complejos, distrubidos y
empresariales, beneficiándonos de la productividad ofrecida por múltiples núcleos. Al fin y
al cabo, ¿quién no desea software eficiente?.
En el modelo tradicional de concurrencia basado en hilos los programas son "troceados" en
múltiples unidades de ejecución concurrentes (threads) en el que cada de ellos opera en
un segmento de memoria compartida. En numerosas ocasiones el modelo anterior ocasiona
"condiciones de carrera" complicadas de detectar así como siuaciones de "deadlocks" que
ocasionan inversiones de semanas completas intentando reproducir, aislar y subsanar el
error. El origen de todas estos problemas no radica en el modelo de hilos sino que reside
en segmentos de memoria compartida. La programación concurrente se ha convertido en
un modelo demasiado complicado para los desarrolladores por lo que necesitaríamos un
mejor modelo de programación concurrente que nos permita crear y mantener programas
concurrentes de manera sencilla.
Scala adopta un enfoque completamente diferente a la problemática de la concurrencia: el
modelo basado en actores. Un actor es un modelo matemático de computación concurrente
en el que se encapsulan datos, código y su propio hilo de control, comunicándose de manera
asíncrona mediante técnicas de paso de mensajes inmutables. La arquitectura base de este
modelo está basada en políticas de compartición cero y componentes ligeros.
Haciendo un poco de historia, el modelo de actores fue propuesto por primera vez por Carl
Hewitt en el año 1973 en el famoso artículo "A Universal Modular ACTOR Formalism for
Artificial Intelligence " , para posteriormente ser mejorado por Gul Agha con su “ACTORS: A
Model of Concurrent Computation in Distributed Systems”).
El primer lenguaje que llevó a cabo la implementación de este modelo fue Erlang. Tras el éxito
obtenido por el lenguaje en lugares tan populares como Ericsson (su lugar de nacimiento),
Yahoo o Facebook, el modelo de actores se ha convertido en una alternativa viable para
solucionar la problemática derivada de la concurrencia, por lo que Scala ha decidido adoptar
este mismo enfoque.
1.3. Objetivos
A lo largo de las secciones anteriores hemos descrito de manera superficial algunas de las
características más relevantes del lenguaje Scala así como las motivaciones principales del
mismo. El resto del trabajo estará dividido en las siguientes secciones:
• Durante la primera sección, esta que nos ocupa, analizamos las características generales
del lenguaje y los objetivos del presente documento.
• La segunda sección abarcará los principales fundamentos del lenguaje, describiendo tanto
los mecanismos funcionales como la orientación a objetos, ambos disponibles de manera
nativa en el lenguaje.
• Analizaremos, aunque no de manera excesivamente exhaustiva, el modelo de
programación concurrente propuesto por Scala (basado en una librería de actores).
Construiremos una pequeña serie de ejemplos que nos permita poner en marcha los
5
10. ¿Por qué Scala?
conocimientos adquiridos, tanto aquellos relativos a la programación concurrente como
los analizados en la primera parte del trabajo.
• Debido a limitaciones de tiempo y espacio no podremos abordar muchos temas
interesantes realacionados con el lenguaje Scala. por lo que durante la última sección
de este trabajo se propondrán numerosos temas de ampliación: web funcional, otras
aproximaciones de actores en Scala, arquitectura del compilador Scala, etc
6
11. Fundamentos de Scala
Capítulo 2. Fundamentos de Scala
A lo largo de este capítulo ahondaremos en los aspectos fundalmentales del lenguaje,
describiendo las características más relevantes tanto de la orientación a objectos como la
funcional. No olvidemos que Scala, tal y como hemos descrito durante el capítulo anterior,
permite la confluencia del paradigma funcional y la orientación a objetos.
Aquellos lectores familiarizados con el lenguaje de programación Java encontrará muchos de
los conceptos aquí descritos (sobre todo aquellos conceptos relativos al paradigma funcional)
similares aunque no son exactamente idénticos.
2.1. Clases y objetos
Del mismo modo que en todos los lenguajes orientados a objetos Scala permite la definición
de clases en las que podremos añadir métodos y atributos:
class MyFirstClass{
val a = 1
}
Si deseamos instanciar un objeto de la clase anterior tendremos que hacer uso de la palabra
reservada new
val v = new MyFirstClass
En Scala existen dos tipos de variables, vals y vars, que deberemos especificar a la hora de
definir las mismas:
• Se utilizará la palabra reservada val para indicar que es inmutable. Una variable de este
tipo es similar al uso de final en Java. Una vez inicializada no se podrá reasignar jamás.
• De manera contraria, podremos indicar que una variable es de clase var, consiguiendo con
esto que su valor pueda ser modificado durante todo su ciclo de vida.
Uno de los principales mecanismos utilizados que garantizan la robustez de un objeto es la
afirmación que su conjunto de atributos (variables de instancia) permanece constante a lo
largo de todo el ciclo de vida del mismo. El primer paso para evitar que agentes externos
tengan acceso a los campos de una clase es declarar los mismos como private. Puesto que
los campos privados sólo podrán ser accedidos desde métodos que se encuentran definidos
7
12. Fundamentos de Scala
en la misma clase, todo el código podría modificar el estado del mismo estará localizado en
dicha clase.1
El siguiente paso será incorporar funcionalidad a nuestras clases; para ello podremos definir
métodos mediante el uso de la palabra reservada def:
class MyFirstClass{
var a = 1
def add(b:Byte):Unit={
a += b
}
}
Una característica importante de los métodos en Scala es que todos los parámetros son
inmutables, es decir, vals. Por tanto, si intentamos modificar el valor de un parámetro en el
cuerpo de un método obtendremos un error del compilación:
def addNotCompile(b:Byte) : Unit = {
b = 1 // Esto no compilará puesto que el
// parámetro b es de tipo val
a += b
}
Otro aspecto relevante que podemos apreciar en el código anterior es que no es necesario
el uso explícito de la palabra return, Scala retornará el valor de la última expresión que
aparece en el cuerpo del método. Adicionalmente, si el cuerpo de la función retorna una
única expresión podemos obviar la utilización de las llaves.
Habitualmente los métodos que presentan un tipo de retorno Unit tienen efectos colaterales,
es decir, modifican el estado del objeto sobre el que actúan. Otra forma diferente de llevar a
cabo la definición de este tipo de métodos consiste en eliminar el tipo de retorno y el símbolo
igual y englobar el cuerpo de la función entre llaves, tal y como se indica a continuación:
class MyFirstClass {
private var sum = 0
def add(b:Byte) { sum += b }
}
2.2. Reglas de inferencia de puntos y coma
La utilización de los puntos y coma como indicadores de terminación de sentencia es,
habitualmente, opcional aunque en determinadas ocasiones la ausencia de los mismos
puede llevarnos a resultados no esperados. Por noram general los saltos de línea son tratados
como puntos y coma salvo que algunas de las siguientes condiciones sea cierta:
1
Por defecto, si no se especifica en el momento de la definición, los atributos y/o métodos, de una clase tienen acceso público.
Es decir, public es el cualificador por defecto en Scala
8
13. Fundamentos de Scala
• La línea en cuestión finaliza con una palabra que no puede actuar como final de sentencia,
como por ejemplo un espacio (" ") o los operadores infijos.
• La siguiente línea comienza con una palabra que no puede actuar como inicio de sentencia.
• La línea termina dentro de paréntesis ( . . . ) o corchetes [ . . .] puesto que éstos últimos
no pueden contener múltiples sentencias.
2.3. Singleton objects
Scala no soporta la definición de atributos estáticos en las clases, incorporando en su lugar
el concepto de singleton objects. La definición de objetos de este tipo es muy similar a la de
las clases salvo que se utiliza la palabra reservada object en lugar de class.
Cuando un objeto singleton comparte el mismo nombre de una clase el primero de ellos es
conocido como companion object mientras que la clase se denomina companion class del
objeto singleton. Inicialmente, sobre todo aquellos desarrolladores provenientes del mundo
Java, podrían ver este tipo de objetos como un contenedor en el que se podrían definir tantos
métodos estáticos como quisiéramos.
Una de las principales diferencias entre los singleton objects y las clases es que los primeros
no aceptan parámetros (no podemos instanciar un objeto singleton mediante la palabra
reservada new) mientras que las segundos si lo permiten. Cada uno de los singleton objects
es implementado mediante una instancia de una synthetic class referenciada desde una
variable estática, por lo que presentan la misma semántica de inicialización que los estáticos
de Java. Un objeto singleton es inicializado la primera vez que es accedido por algún código.
2.4. Objetos funcionales
A lo largo de las secciones anteriores hemos adquirido una serie de conocimientos básicos
relativos a la orientación a objetos ofrecida por Scala. Durante las siguientes páginas
analizaremos cómo se pueden construir objetos funcionales, es decir, inmutables, mediante
la definición de clases. El desarrollo de esta sección nos permitirá ahondar en cómo los
aspectos funcionales y los de orientación a objetos confluyen en el lenguaje. Adicionalmente
las siguientes secciones no servirán como base para la introducción de nuveos conceptos de
orientación a objetos cómo parámetros de clase, sobreescritura, self references o métodos
entre otros muchos.
2.4.1. Números racionales
Los números racionales son aquellos que pueden ser expresados como un cociente n/
d. Durante las siguientes secciones construiremos una clase que nos permita modelar el
comportamiento de este tipo de números. A continuación se presentan algunas de sus
características principales:
9
14. Fundamentos de Scala
• Suma/resta de números racionales. Se debe obtener un común denominador de ambos
denominadores y posteriormente sumar/restar los numeradores.
• Multiplicación de números racionales. Se multiplican los numeradores y denominadores
de los integrantes de la operación.
• División de números racionales. Se intercambian el numerador y denominador del
operando que aparece a la derecha y posteriormente se realiza una operación de
multiplicación.
2.4.2. Constructores
Puesto que hemos decidido que nuestros números racionales sean inmutables
necesitaremos que los clientes de esta clase proporcionen toda la información en el
momento de creación de un objeto. Podríamos comenzar nuestro diseño del siguiente modo:
class Rational (n:Int,d:Int)
Los parámetros definidos tras el nombre de la clase son conocidos como parámetros de
clase. El compilador generará un constructor primario en cuya signatura aparecerán los dos
parámetros escritos en la definición de la clase. Cualquier código que escribamos dentro del
cuerpo de la clase que no forme parte de un atributo o de un método será incluido en el
constructor primario indicado anteriormente.
2.4.3. Sobreescritura de métodos
Si deseamos sobreescribir un método heredado de una clase padre en la jerarquía tendremos
que hacer uso de la palabra reservada override. Por ejemplo, si en la clase Rational deseamos
sobreescribir la implementación por defecto de toString podríamos actuar del siguiente
modo:
override def toString = n + "/" + d
2.4.4. Precondiciones
Una de las características de los números racionales no admiten el valor cero como
denominador aunque sin embargo, con la definición actual de nuestra clase Rational
podríamos escribir código como:
new Rational(11,0)
algo que violaría nuestra definición actual de números racionales. Dado que estamos
construyendo una clase inmutable y toda la información debe estar disponible en el
momento que se invoca al constructor este último deberá asegurarse de que el denominador
indicado no toma el valor cero (0).
La mejor aproximación para resolver este problema pasa por hacer uso de las precondiciones.
Este concepto, incluido en el lenguaje, representa un conjunto de restricciones que pueden
10
15. Fundamentos de Scala
establecerse sobre los valores pasados a métodos o constructores y que deben ser
satisfechas por el cliente que realiza la llamada del método/constructor:
class Rational(n: Int, d: Int) {
require(d != 0)
override def toString = n +"/"+ d
}
La restricciones se establecen mediante el uso del método require el cual espera un
argumento booleano. En caso de que la condición exigida no se cumpla el método require
disparará una excepción de tipo IllegalArgumentException.
2.4.5. Atributos y métodos
Definamos en nuestra clase un método público que reciba un número racional como
parámetro y retorne como resultado la suma de ambos operandos. Puesto que estamos
construyendo una clase inmutable el nuevo método deberá retornar la suma en un nuevo
número racional:
class Rational(n: Int, d: Int) {
require(d != 0)
override def toString = n +"/"+ d
// no compila: no podemos hacer that.d o that.n
// deben definirse como atributos
def add(that: Rational): Rational =
new Rational(n * that.d + that.n * d, d * that.d)
}
El código anterior muestra una primera aproximación de solución aunque incorrecta dado
que se producirá un error de compilación. Aunque los parámetros de clase n y d están el
ámbito del método add solo se puede acceder a su valor en el objeto sobre el que se realiza
la llamada.
Para resolver el problema planteado en el fragmento de código anterior tendremos que
declarar d y n como atributos de la clase Rational:
class Rational(n: Int, d: Int) {
require(d != 0)
val numer: Int = n // declaración de atributos
val denom: Int = d
override def toString = numer +"/"+ denom
def add(that: Rational): Rational =
new Rational(numer * that.denom +
that.numer * denom,
denom * that.denom)
}
11
16. Fundamentos de Scala
Nótese que en los fragmentos de código anteriores estamos manteniendo la inmutabilidad
de nuestro diseño. En este caso, el operador de adición add retorna un nuevo objeto racional
que representa la suma de ambos números, en lugar de realizar la suma sobre el objeto que
realiza la llamada.
A continuación incorporemos a nuestra clase un método privado que nos ayude a determinar
el máximo común divisor:
private def gcd(a:Int,b:Int):Int =
if(b == 0)
a
else gcd(b,a%b)
El listado de código anterior nos muestra como podemos incorporar un método privado a
nuestra clase que, en este caso, nos sirve como método auxiliar para calcular el máximo
común divisor de dos números enteros.
2.4.6. Operadores
La implementación actual de nuestra clase Rational es correcta aunque podríamos definirla
de modo que su uso resultara mucho más intuitivo. Una de las posibles mejoras que
podríamos introducir sería la inclusión de operadores:
def + (that: Rational): Rational =
new Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
def * (that: Rational): Rational =
new Rational(numer * that.numer,
denom * that.denom)
De este modo podríamos escribir código como el que a continuación se indica:
var a = new Rational(2,5)
var b = new Rational(1,5)
var sum = a + b
2
2.5. Funciones y closures
Hasta el momento hemos analizado algunas de las características más relevantes del lenguaje
Scala, poniendo de manifiesto la incorporación de fundamentos de lenguajes funcionales así
como de lenguajes orientados a objetos.
2
También podríamos escribir a.+(b) aunque en este caso el código resultante sería mucho menos legible
12
17. Fundamentos de Scala
Cuando nuestros programas crecen necesitamos hacer uso de un conjunto de abstracciones
que nos permitan dividir dicho programa en piezas más pequeñas y manejables que permitan
una mejor comprensión del mismo. Scala ofrece varios mecanismos para definir funciones
que no están presentes en Java. Además de los métodos, que no son más que funciones
miembro de un objeto, podemos hacer uso de funciones anidadas en funciones, function
literals y function values. Durante las siguientes secciones de este apartado profundizaremos
en alguno de los mecanismos anteriores no analizados en produndidad anteriormente.
2.5.1. Funciones first-class
Scala incluye una de las características principales del paradigma funcional: first class
functions. No sólamente podemos definir funciones e invocarlas sino que también podemos
definirlas como literales para, posteriormente, pasarlas como valores.
Las funciones literales son compiladas en una clase que, cuando es instanciada, se convierte
en una function value. Por lo tanto, la principal diferencia entre las funciones literales y las
funciones valor es que las primeras existen en el código fuente mientras que las segundas
existen como objetos en tiempo de ejecución.
A continuación se define un pequeño ejemplo de una función literal que suma el valor 1 al
número indicado:
(x:Int) => x + 1
Las funciones valor son objetos propiamente dichos por lo que podemos almacenarlas en
variables o invocarlas mediante la notación de paréntesis habitual.
2.5.2. Closures
Las funciones literales que hemos visto hasta este momento han hecho uso, única y
exclusivamente, de los parámetros pasados a la función. Sin embargo, podríamos definir
funciones literales en las que se hace uso de variables definidas en otro punto de nuestro
programa:
(x:Int) = x * other
La variable other es conocida como una free variable puesto que la función no le da un
significado a la misma. Al contrario, la variable x es conocida como bound variable puesto
que tiene un significado en el contexto de la función. Si intentamos utilizar esta función en un
contexto en el que no está accesible una variable other obtendremos un error de compilación
indicándonos que dicha variable no está disponible.
La función valor creada en tiempo de ejecución a partir de la función literal es conocida como
closure. El nombre se deriva del acto de "cerrar" la función literal mediante la captura en el
ámbito de la función de los valores de sus free variables. Una función valor que no presenta
free variables, creada en tiempo de ejecución a partir de su función literal no es una closure
en el sentido más estricto de la definición dado que dicha función ya se encuetra "cerrada"
en el momento de su escritura.
13
18. Fundamentos de Scala
El fragmento de código anterior hace que nos planteemos la siguiente pregunta: ¿que ocurre
si la variable other es modificada después de que la closure haya sido creada? La respuesta es
sencilla: en Scala la closure tiene visión sobre el cambio ocurrido. La regla anterior también
se cumple en sentido contrario: su una closure modifica alguno de sus valores capturados
estos últimos son visibles fuera del ámbito de la misma.
2.5.3. Tail recursion
A continuación presentamos una función recursiva que aproxima un valor mediante un
conjunto de repetidas mejoras hasta que es suficientemente bueno:
def aproximate (guess:Double):Double =
if(isGoodEnough(guess) guess
else aproximate(improve(guess))
Las funciones que presentan este tipo de tipología (se llaman a si mismas en la última
sentencia del cuerpo de la función) son llamadas funciones tail recursive. El compilador de
Scala detecta esta situación y reemplaza la última llamada con un salto al comienzo de la
función tras actualizar los parámetros de la función con los nuevos valores.
El uso de tail recursion es limitado debido a que el conjunto de instrucciones ofrecido por la
máquina virtual (JVM) dificulta de manera notable la implementación de otros tipos de tail
recursion. Scala únicamente optimiza llamadas recursivas a una función dentro de la misma.
Si la recursión es indirecta, como la que se muestra en el siguiente fragmento de código, no
se puede llevar a cabo ningún tipo de optimización:
def isEven(x:Int): Boolean =
if(x==0) true else isOdd(x-1)
def isOdd(x:Int): Boolean =
if(x==0) false else isEven(x-1)
2.6. Currying
Scala no incluye un excesivo número de instrucciones de control de manera nativa aunque
nos permite llevar a cabo la definición de nuestras propias construcciones de manera sencilla.
A lo largo de esta seccion analizaremos como definir nuestras propias abstracciones de
control con un parecido muy próximo a extensiones del lenguaje.
El primer paso consiste en comprender una de las técnicas más comunes de los lenguajes
funcionales: currying. Una curried function es aplicada múltiples listas de argumentos en
lugar de una sola. El siguiente fragmento de código nos muestras una función tradicional que
recibe dos argumentos de tipo entero y retorna la suma de ambos:
def plainSum(x:Int, y:Int) = x + y
A continuación se muestras una curried function similar a la descrita en el fragmento de
código anterior:
14
19. Fundamentos de Scala
def curriedSum(x:Int)(y:Int) = x + y
Cuando ejecutamos la sentencia curriedSum(9)(2) estamos obteniendo dos llamadas
tradicionales de manera consecutiva. La primera invocación recibe el parámetro x y retorna
una función valor para la segunda función. Esta segunda función recibe el parámetro y. El
siguiente código muestra una función first que lleva a cabo lo que haría la primera de las
invocaciones de la función curriedSum anterior:
def first(x: Int) = (y: Int) => x + y
Invocando a la función anterior con el valor 1 obtendríamos una nueva función:
def second = first(1)
La invocación de este segunda función con el parámetro 2 retornaría el resultado.
2.7. Traits
Los traits son la unidad básica de reutilización de código en Scala. Un trait encapsula
definiciones de métodos y atributos que pueden ser reutilizados mediante un proceso de
mixin llevado a cabo en conjunción con las clases. Al contrario que en el mecanismo de
herencia, en el que únicamente se puede tener un padre, una clase puede llevar a cabo un
proceso de mixin con un número indefinido de traits.
2.7.1. ¿Cómo funcionan los traits?
La definición de un trait es similar a la de una clase tradicional salvo que se utiliza la palabra
reservada trait en lugar de class.
trait MyFirstTrait {
def printMessage(){
println("This is my first trait")
}
}
Una vez definido puede ser "mezclado" junto a una clase mediante el uso de las palabras
reservadas extend o with en XXXX analizaremos las diferencias e implicaciones de cada una
de estas alternativas):
class MyFirstMixin extends MyFirstTrait{
override def toString = "This is my first mixin in Scala"
}
Cuando utilizamos la palabra reservada extends para realizar el proceso de mixin estaremos
heredando de manera implícita las superclases del trait. Los métodos heredados de un trait
se utilizan del mismo modo que se utilizan los métodos heredados de una clase. De manera
adicional, un trait también define un tipo.
15
20. Fundamentos de Scala
En el caso de que deseemos realizar un proceso de mixin en el que una clase ya indica
un padre de manera explicita mediante el uso extends tendremos que utilizar la palabra
reservada with. Si deseamos incluir en el proceso de mixin múltiples traits no trendremos
más que incluir más cláusulas with.
Llegados a este punto podríamos pensar que los traits son como interfaces Java con métodos
concretos pero realmente pueden hacer muchas más cosas. Por ejemplo, los traits pueden
definir atributos y mantener un estado. Realmente, en un trait podemos hacer lo mismo que
en una definición de clase con una sintaxis similar aunque existen dos excepciones:
• Un trait no puede tener parámetros de clase (los parámetros pasados al constructor
primario de la clase).
• Mientras que en las clases las llamadas a métodos de clases padre (super.xxx) son
enlazadas de manera estática en el caso de los traits dichas llamadas son enlazadas
dinámicamente. Si en una clase escribimos super.method() sabremos en todo momento
que implementación del método será invocada. Sin embargo, el mismo código escrito en
un trait provoca un desconocimiento de la implementación del método que será invocado
en tiempo de ejecución. Dicha implementación será determinada cada una de las veces
que un trait y una clase realizan un proceso de mixin. Este curioso comportamiento de
super es la clave que permite a los traits trabajar como stackable modifications3 que
veremos a continuación.
2.7.2. Ejemplo: objetos rectangulares
Las librerías gráficas habitualmente presentan numerosas clases que representan objetos
rectangulares: ventanas, selección de una región de la pantalla, imágenes, etc. Son muchos
los métodos que nos gustaría tener en el API por lo que necesitaríamos una gran cantidad de
desarrolladores que poblaran la librería con métodos para todos los objetos rectangulares.
En Scala, los desarrolladores de la librería podrían hacer uso de traits para incorporar los
métodos necesarios en aquellas clases que se deseen.
Una primera aproximación en la que no se hace uso de traits se muestra a continuación:
class Point(val x: Int, val y: Int)
class Rectangle(val topLeft: Point, val bottomRight: Point) {
def left = topLeft.x
def right = bottomRight.x
def width = right - left
// más métodos . . .
}
Incorporemos un nuevo componente gráfico:
abstract class Component {
16
21. Fundamentos de Scala
def topLeft: Point
def bottomRight: Point
def left = topLeft.x
def right = bottomRight.x
def width = right - left
// más métodos . . .
}
Modifiquemos ahora la aproximación anterior e incorporemos el uso de traits. Incorporemos
un trait que incorpore la funcionalidad común vista en los dos fragmentos de código anterior:
trait Rectangular {
def topLeft: Point
def bottomRight: Point
def left = topLeft.x
def right = bottomRight.x
def width = right - left
// más métodos
}
La clase Component podría realizar un proceso de mix con el trait anterior para incorporar
toda la funcionalidad proporcionada por este último:
abstract class Component extends Rectangular{
// nuevos métodos para este tipo de widgets
}
Del mismo modo que la clase Component , la clase Rectangle podría realizar el proceso de
mixin con el trait Rectangular:
class Rectangle(val topLeft: Point, val bottomRight: Point)
extends Rectangular {
// métodos propios de la clase rectangle
}
2.7.3. Uso de traits como stackable modifications
Hasta el momento hemos visto uno de los principales usos de los traits: el enriquecimiento
de interfaces. Durante la sección que nos ocupa analizaremos otro de los usos más populares
de los traits: facilitar stackable modifications en las clases. Los traits nos permitirán modificar
los métodos de una clase y, adicionalmente, nos permitirá apilarlas entre si.
Apilemos modificaciones sobre una cola de números enteros. Dicha cola tendrá dos
operaciones: put, que añadirá números a la cola, y get que los sacará de la misma.
Generalmente las colas siguen el comportamiento "primero en entrar, primero en salir" por
lo que el método get tendría que retornar los elementos en el mismo orden en el que fueron
introducidos.
17
22. Fundamentos de Scala
Dada una clase que implementa el comportamiento descrito en el párrafo anterior
podríamos definir un trait que llevara a cabo modificaciones como:
• Multiplicar por dos todos cualquier elemento que se añada en la cola.
• Incrementar en una unidad cada uno de los elementos que se añaden en la cola.
• Filtrado de elementos negativos. Evita que cualquier número menor que cero sea añadido
a la cola.
Los tres traits anteriores representan modificaciones dado que no definen una cola por
si mismos sino que llevan a cabo modificaciones sobre la cola subyacente con la que
realizan el proceso de mixin. Los traits también son apilables: podríamos escoger cualquier
subconjunto de los tres anteriores e incorporarlos a una clase de manera que conseguiríamos
una nueva clase con la funcionalidad deseada. El siguiente fragmento de código representa
una implementación reducida del comportamiento de una cola descrito en el inicio de esta
sección:
import scala.collection.mutable.ArrayBuffer
abstract class IntQueue {
def get(): Int
def put(x: Int)
}
class BasicIntQueue extends IntQueue {
private val buf = new ArrayBuffer[Int]
def get() = buf.remove(0)
def put(x: Int) { buf += x }
}
Realicemos ahora un conjunto de modificaciones sobre la clase anterior; para ello, vamos a
hacer uso de los traits. El siguiente fragmento de código muestra un trait que duplica el valor
de un elemento que se desea añadir a la cola:
trait Duplication extends IntQueue{
abstract override def put(x:Int) { super.put(2*x) }
}
Nótese el uso de las palabras reservadas abstract override. Esta combinación de
modificadores sólo puede ser utilizada en los traits y no en las clases, e indica que el trait
debe ser integrado (mixed) con una clase que presenta una implementación concreta del
método en cuestión.
A continuación se muestra un ejemplo de uso del trait anterior:
scala> class MyQueue extends BasicIntQueue with Doubling
defined class MyQueue
scala> val queue = new MyQueue
queue: MyQueue = MyQueue@91f017
18
23. Fundamentos de Scala
scala> queue.put(10)
scala> queue.get()
res12: Int = 20
Para analizar el mecanismo de apilado de modificaciones implementemos en primer lugar
los dos traits restantes que hemos descrito al inicio de esta sección:
trait Increment extends IntQueue{
abstract override def put(x:Int) { super.put(x + 1) }
}
trait Filter extends IntQueue{
abstract override def put(x:Int) { if ( x >= 0 ) super.put(x) }
}
Una vez tenemos disponibles las modificaciones podríamos generar una nueva cola del modo
que más nos interese:
scala> val queue = (new BasicIntQueue
with Increment with Filter)
queue: BasicIntQueue with Increment with Filter...
scala> queue.put(-1); queue.put(0); queue.put(1)
scala> queue.get()
res15: Int = 1
scala> queue.get()
res16: Int = 2
El orden de los mixins es importante . De manera resumida, cuando invocamos a un método
de una clase con mixins el método del trait definido más a la derecha es el primero en
ser invocado. Si dicho método invoca a super este invocará al trait que se encuentra más
a la izquierda y así sucesivamente. En el ejemplo anterior, el método put del trait Filter
será invocado en primer lugar, por lo que aquellos números menores que cero no serán
incorporados a la cola. El método put del trait Filter sumará el valor uno a cada uno de los
números (mayores o iguales que cero).
2.7.4. Traits: ¿si o no?
A continuación se presentan una serie de criterios más o menos objetivos que pretenden
ayudar al lector a determinar cuando debería usar estas construcciones proporcionadas por
el lenguaje Scala:
• Si el comportamiento no pretende ser reutilizado entonces encapsularlo en una clase.
• Si el comportamiento pretende ser reutilizado en múltiples clases no relacionadas
entonces construir un trait.
19
24. Fundamentos de Scala
• Si se desee que una clase Java herede de nuestra funcionalidad entonces deberemos
utilizar una clase abstracta.
• Si la eficiencia es importante deberíamos inclinarnos hacia el uso de las clases. La mayoría
de los entornos de ejecución Java hacen una llamada a un método virtual de una
clase mucho más rápido que la invocación de un método de un interfaz. Los traits son
compilados a interfaces y podríamos penalizar el rendimiento.
• Si tras todas las opciones anteriores no tenemos claro qué aproximación deseamos
utilizar deberíamos comenzar por el uso de traits. Lo podremos cambiar en el futuro y,
generalmente, mantedremos más opciones abiertas.
2.8. Patrones y clases case
Aquellos lectores que hayan progamado en algún lenguaje perteneciente al paradigma
funcional reconocerán el uso de la concordancia de patrones. Las clases case son un concepto
relativamente novedoso y nos permiten incorporar el mecanismo de matching de patrones
sobre objetos sin la necesidad de código repetitivo. De manera general, no tendremos más
que prefijar la definición de una clase con la palabra reservada case para indicar que la clase
definida pueda ser utilizada en la definición de patrones.
A lo largo de esta sección analizaremos los dos conceptos anteriores en conjunción con un
conjunto de ejemplos con el objetivo de ilustrar y amenizar la lectura de esta sección.
2.8.1. Clases case
El uso del modificador case provoca que el compilador de Scala incorpore una serie de
facilidades a la clase indicada.
En primer lugar incorpora un factory-method con el nombre de la clase. Gracias a esto
podríamos escribir código como Foo("x") para construir un objeto Foo en lugar de new
Foo("x"). Una de las principales ventajas de este tipo de métodos es la ausencia de
operadores new cuando los anidamos:
val op = BinaryOperation("+", Number(1), v)
Otra funcionalidad sintáctica incorporada por el compilador es que todos los argumentos en
la lista de parámetros incorporan de manera implicita el prefijo val por lo que éstos últimos
serán atributos de clase. Por último, pero no por ello no menos importante, el compilador
añade implementaciones "instintivas" de los métodos toString, hashCode e equals.
Todas estas facilidades incorporadas acarrean un pequeño coste: las clases y objetos
generados son un poco más grandes4 y tenemos que incorporar la palabra case en las
definiciones de nuestras clases. La principal ventaja de este tipo de clases es que soportan
la concordancia de patrones.
4
Son más grandes porque se generan métodos adicionales y se incorporan atributos implícitos para cada uno de los parámetros
del constructor
20
25. Fundamentos de Scala
2.8.2. Patrones: estructura y tipos
La estructura general de un patrón en Scala presenta la siguiente estructura:
selector match { alternatives }
Incorporan un conjunto de alternativas en las que cada una de ellas comienza por la palabra
reservada case. Cada una de estas alternativas incorpora un patrón y una o más expresiones
que serán evaluadas en caso de que se produzca la concordancia del patrón. Se utiliza el
símbolo de flecha (=>) para separar el patrón de las expresiones.
Como hemos visto al comienzo de esta sección la sintaxis de los patrones es sumamente
sencilla por lo que vamos a profundizar en los diferentes tipos de patrones que podemos
construir.
2.8.2.1. Patrones wildcard
El patrón (_) concuerda con cualquier objeto por lo que podríamos utilizarlo como una
alternativa catch-all tal y como se muestra en el siguiente ejemplo:
expression match {
case BinaryOperation(op,leftSide,rightSide) =>
println(expression + " is a BinaryOperation")
case _ =>
}
2.8.2.2. Patrones constantes
Un patrón constante concuerda única y exclusivamente consigo mismo. El siguiente
fragmento de código muestra algunos ejemplos de patrones constantes:
def describe(x: Any) = x match {
case 5 => "five"
case true => "truth"
case "hello" => "hi!"
case Nil => "this is an empty list"
case _ => "anything else"
}
2.8.2.3. Patrones variables
Un patrón variable concuerda con cualquier objeto, del mismo modo que los patrones
wildcard. A diferencia de los patrones wildcard, Scala enlaza la variable al objeto, por lo que
posteriormente podremos utilizar dicha variable para actuar sobre el objeto:
expr match {
21
26. Fundamentos de Scala
case 0 => "zero value"
case somethingElse => "not zero: "+ somethingElse + " value"
}
2.8.2.4. Patrones constructores
Son en este tipo de construcciones donde los patrones se convierten en una herramienta
muy poderosa. Básicamente están formados por un nombre y un número indefinido de
patrones. Asumiendo que el nombre designa una clase de tipo case este tipo de patrones
comprobarán primero si el objeto pertenece a dicha clase, para, posteriormente comprobar
si los parámetros del constructor concuerdan con el conjunto de patrones extra indicados.
La definición anterior puede no resultar demasiado explicativa por lo que a continuación se
incluye un pequeño ejemplo en el que se comprueba que el objeto de primer nivel es de tipo
BinaryOperation y que su tercer argumento es de tipo Number y su atributo de clase vale 0:
expr match {
case BinaryOperation("+", e, Number(0))
=> println("a deep match")
case _ =>
}
2.8.2.5. Patrones de secuencia
Podemos establecer patrones de concordancia sobre listas o arrays del mismo modo que lo
hacemos para las clases. Deberá utilizarse la misma sintáxis aunque ahora podremos indicar
cualquier número de elementos en el patrón.
El siguiente fragmento de código muestra un patrón que comprueba una lista de tres
elementos cuyo primer valor toma 0:
expr match {
case List(0, _, _) => println("found it")
case _ =>
}
2.8.2.6. Patrones tipados
Podemos utilizar este tipo de construcciones como reemplazo de las comprobaciones y
conversiones de tipos:
def genericSize(x: Any) = x match {
case s: String => s.length
case m: Map[_, _] => m.size
case _ => -1
}
22
27. Fundamentos de Scala
El método genericSize retorna la longitud de un objeto cualquiera. El patrón "s:String" es un
patrón tipado: cualquier instancia no nula de tipo String concordará con dicho patrón. La
variable de patrón s hará referencia a dicha cadena.
2.9. Conclusiones
A lo largo de este capítulo hemos analizado varias de las características principales del
lenguaje de programación Scala, haciendo especial hincapié en como el lenguaje incorpora
funcionalidades provenientes de los paradigmas funcional y orientado a objetos.
Las secciones anteriores nos permitirán comenzar a escribir nuestros primeros programas en
Scala aunque nos faltaría un largo camino para convertirnos en unos expertos en la materia.
Para el lector más interesado a continuación se indican algunos conceptos en los que se
podría profundizar:
• Interoperabilidad entre Scala y Java.
• Parametrización de tipos.
• Extractors.
• Trabajar con XML.
• Inferencia de tipos
• ...
En la siguiente parte del trabajo analizaremos el modelo de actores propuesto por Scala y
cómo esta aproximación nos permitirá construir aplicaciones concurrentes de manera más
sencilla.
23
28. Actors y concurrencia
Capítulo 3. Actors y concurrencia
En muchas ocasiones cuando estamos escribiendo nuestros programas necesitamos que
muchas de sus partes se ejecuten de manera independiente, es decir, de manera
concurrente. Aunque el soporte introducido por Java es suficiente, a medida que la
complejidad y tamaño de los programas se incrementan, conseguir que nuestro código
concurrente funcione de manera correcta se convierte en una tarea complicada. Los actores
incluyen un modelo de concurrencia más sencillo con el que podremos evitar la problemática
habitual ocasionada por el modelo de concurrencia nativo de Java.
Durante este capítulo presentaremos los fundamentos del modelo de actores y como
podremos utilizar la librería de actores facilitada Scala.
3.1. Problemática
La plataforma Java proporciona de manera nativa un modelo de hilos basado en bloqueos
y memoria compartida. Cada uno de los objetos lleva asociado un monitor que puede
ser utilizado para realizar el control de acceso de múltiples hilos sobre los datos. Para
utilizar este modelo debemos decidir que datos queremos compartir entre múltiples hilos
y establecer como synchronized aquellas secciones de código que acceden a segmentos
de datos compartidos. En tiempo de ejecución, Java utiliza un mecanismo de bloqueo con
el que se garantiza que en un instante de tiempo T un único hilo accede a una sección
sincronizada S. Los desarrolladores han encontrado numerosos problemas para construir
aplicaciones robustas con esta aproximación, especialmente cuando los problemas crecen
en tamaño y complejidad. En cada punto del programa el desarrollador debe razonar sobre
los datos modificados y/o accedidos que pueden ser accedidos por otros hilos y establecer
los bloqueos necesarios.
Añadiendo un nuevo inconveniente a los descritos en el párrafo anterior, los procesos de
prueba no son fiables cuando tratamos con códigos multihilo. Dado que los hilos son no
deterministas, podríamos probar nuestra aplicaciones miles de veces de manera satisfactoria
y obtener un error la primera vez que se ejecuta el mismo código en la máquina de nuestro
cliente.
La aproximación seguida por Scala1 para resolver el problema de la concurrencia ofrece
un modelo alternativo de no compartición y paso de mensajes. En la siguiente sección se
ofrecerá una definición resumida del modelo de actores.
1
Realmente Scala ofrece el modelo de actores mediante una librería del lenguaje en lugar de manera nativa
24
29. Actors y concurrencia
3.2. Modelo de actores
El modelo de actores ofrece una solución diferente al problema de la concurrencia. En lugar
de procesos interactuando mediante memoria compartida, el modelo de actores ofrece una
solución basada en buzones y paso de mensajes asíncronos. En estos buzones los mensajes
pueden ser almacenados y recuperados para su procesamiento por otros actores. En lugar
de compartir variables en memoria, el uso de estos buzones nos permite aislar cada uno de
los procesos.
Los actores son entidades independientes que no comparten ningún tipo de memoria para
llevar a cabo el proceso de comunicación. De hecho, los actores únicamente se pueden
comunicar a través de los buzones descritos en el párrafo anterior. En esta aproximación
de concurrencia no existen bloqueos ni secciones sincronizadas por lo que los problemas
derivados de las mismas (deadlocks, pérdida de actualizaciones de datos, . . .) no existen en
este modelo. Los actores están pensados para trabajar de manera concurrente, no de modo
secuencial.
El modelo de actores no es una novedad: Erlang basa su modelo de concurrencia en actores
en lugar de hilos. De hecho, la popularidad alcanzada por Erlang en determinados ámbitos
empresariales han hecho que la popularidad del modelo de actores haya crecido de manera
notable y lo ha convertido en una opción viable para otros lenguajes.
3.3. Actores en Scala
Para implementar un actor en Scala no tenemos más que extender scala.actors.Actor e
implementar el método act. El siguiente fragmento de código ilustra un actor sumamente
simple que no realiza nada con su buzón:
import scala.actors._
object FooActor extends Actor{
def act(){
for(i <- 1 to 11){
println("Executing actor!")
Thread.sleep(1000)
}
}
}
Si deseamos ejecutar un actor no tenemos más que invocar a su método start()
scala> FooActor.start()
res0: scala.actors.Actor = FooActor$@681070
Otro mecanismo diferente que nos permitiría instanciar un actor sería hacer uso del método
de utilidad actor disponible en scala.actors.Actor:
scala> import scala.actors.Actor._
25
30. Actors y concurrencia
scala> val otherActor = actor {
for (i <- 1 to 11)
println("This is other actor.")
Thread.sleep(1000)
}
Hasta el momento hemos visto como podemos crear un actor y ejecutarlo de manera
independiente pero, ¿cómo conseguimos que dos actores trabajen de manera conjunta? Tal
y como hemos descrito en la sección anterior, los actores se comunican mediante el paso de
mensajes. Para enviar un mensaje haremos uso del operador !.
Definamos ahora un actor que haga uso de su buzón, esperando por un mensaje e
imprimiendo aquello que ha recibido:
val echoActor = actor {
while (true) {
receive {
case msg =>
println ("Received message " + msg)
}
}
}
Cuando un actor envía un mensaje no se bloquea y cuando lo recibe no es interrumpido. El
mensaje enviado queda a la espera en el buzón del receptor hasta que este último ejecute
la instrucción receive. El siguiente fragmento de código ilustra el comportamiento descrito:
scala> echoActor ! "My First Message"
Received message My First Message
scala> echoActor ! 11
Received message 11
3.3.1. Buenas prácticas
Llegados a este punto conocemos los fundamentos básicos para escribir nuestros propios
actores. El punto fuerte de los métodos vistos hasta este momento es que ofrecen un
modelo de programación concurrente basado en actores por lo que, en la medida que
podamos escribir siguiendo este estilo nuestro código será más sencillo de depurar y tendrá
menos deadlocks y condiciones de carrera. Las siguientes secciones describen,de manera
breve, algunas directrices que nos permitirán adopotar un estilo de programación basado
en actores.
3.3.1.1. Ausencia de bloqueos
Un actor no debería bloquearse mientras se encuentra procesando un mensaje. El problema
radica en que mientras un actor se bloquea, otro actor podría realizar una petición sobre
26
31. Actors y concurrencia
el primero. Si el actor se bloquea en la primera petición no se dará cuenta de una segunda
solicitud. En el peor de los casos, se podría producir un deadlock en el que varios actores
están esperando por otros actores que a su vez están bloqueados.
En lugar de bloquearse, el actor debería esperar la llegada de un mensaje indicando que la
acción está lista para ser ejecutada. Esta nueva disposición, por norma general, implicará la
participación de otros actores.
3.3.1.2. Comunicación exclusiva mediante mensajes
La clave de los actores es el modelo de no compartición, ofreciendo un espacio seguro (el
método act de cada actor) en el que podríamos razonar de manera secuencial. Expresándolo
de manera diferente, los actores nos permiten escribir programas multihilo como un
conjunto independiente de programas monohilo. La simplificación anterior se cumple
siempre y cuando el único mecanismo de comunicación entre actores sea el paso de
mensajes.
3.3.1.3. Mensajes inmutables
Puesto que el modelo de actores provee un entorno monohilo dentro de cada método act no
debemos preocuparnos si los objetos que utilizamos dentro de la implementación de dicho
método son thread-safe. Este es el motivo por el que el modelo de actores es llamado shared-
nothing, los datos están confinados en un único hilo en lugar de ser compartidos por varios.
La excepción a esta regla reside en la información de los mensajes intercambiados entre
actores dado que es compartida por varios de ellos. Por tanto, tendremos que preocuparnos
de que los mensajes intercambiados entre actores sean thread-safe.
3.3.1.4. Mensajes autocontenidos
Cuando retornamos un valor al finalizar la ejecución de un método el fragmento de
código que realiza la llamada se encuentra en una posición idónea para recordar lo que
estaba haciendo anteriormente a la ejecución del método, recoger el resultado y actuar en
consecuencia.
Sin embargo, en el modelo de actores las cosas se vuelven un poco más complicadas. Cuando
un actor realiza una petición a otro actor el primero de ellos no es consciente del tiempo que
tardará la respuesta, instantes en los que dicho actor no debería bloquearse, sino que debería
continuar ejecutando otro trabajo hasta que la respuesta a su petición le sea enviada. ¿Puede
el actor recordar qué estaba haciendo en el momento en el que envió la petición inicial?
Podríamos adoptar dos soluciones para intetar resolver el problema planteado en el párrafo
anterior:
27
32. Actors y concurrencia
• Un mecanismo para simplificar la lógica de los actores sería incluir información redundante
en los mensajes. Si la petición es un objeto inmutable, el coste de incluir una referencia a
la solicitud en el valor de retorno no sería costoso.
• Otro mecanismo adicional que nos permitiría incrementar la redundancia en los mensajes
sería la utilización de una clase diferente para cada uno de las clases de mensajes que
dispongamos
3.4. Un ejemplo completo
Como ejemplo completo de aplicación del modelo de actores descrito a lo largo de las
secciones anteriores y con el objetivo de asentar y poner en marcha los conocimientos
adquiridos vamos a construir, paso a paso, una pequeña aplicación de ejemplo.
3.4.1. Especificación del problema
Durante la construcción de este ejemplo desarrollaremos una abstraccción de producers la
cual ofrecerá una interfaz de iterador estándar para la recuperación de una secuencia de
valores.
La definición de un producer específico se realiza mediante la implementación del método
abstracto produceValues. Los valores individuales son generados utilizados el método
produce. Por ejemplo, un producer que genera en preorden los valores contenidos en un
árbol podría ser definido como:
class TraversePreorder(n:Tree)
extends Producer[int]{
def produceValues = traverse(n)
def traverse(n:Tree){
if( n!= null){
produce(n.elem)
traverse(n.left)
traverse(n.right)
}
}
}
3.4.2. Implementación del producer y coordinator
La abstracción producer se implementa en base a dos actores: un actor productor y un
actor coordinador. El siguiente fragmento de código ilustra cómo podríamos llevar a cabo la
definición del actor productor:
abstract class Producer[T] {
28
33. Actors y concurrencia
protected def produceValues: unit
protected def produce(x: T) {
coordinator ! Some(x)
receive { case Next => }
}
private val producer: Actor = actor {
receive {
case Next =>
produceValues
coordinator ! None
}
}
...
}
¿Cuál es el mecanismo de funcionamiento del actor productor anterior? Cuando un actor
productor recibe el mensaje Next éste ejecuta el método (abstracto) produceValues, que
desencadena la ejecución del método produce. Ésta ultima ejecución provoca el envío de una
serie de valores (recubiertos por el message Some) al coordinador.
El coordinador es el responsable de sincronizar las peticiones de los clientes y los valores
provenientes del productor. Una posible implementación del actor coordinador se ilustra en
el siguiente fragmento de código:
private val coordinator:Actor = actor {
loop {
react {
case Next =>
producer ! Next
reply{
receive {case x: Option[_] => x}
}
case Stop => exit('stop)
}
}
}
3.4.3. Interfaz iterator
Nuestro objetivo es que los producers se puedan utilizar del mismo modo en que utilizamos
los iteradores tradicionales. Nótese como los métodos hasNext y next envían sendos
mensajes al actor coordinador para llevar a cabo su tarea:
def iterator = new Iterator[T] {
private var current: Any = Undefined
29
34. Actors y concurrencia
private def lookAhead = {
if(current == Undefined)
current = coordinator !? Next
current
}
def hasNext: boolean = lookAhead match {
case Some(x) => true
case None => { coordinator ! Stop; false}
}
def next:T = lookAhead match{
case Some(x) => current = Undefined; x.asInstanceOf[T]
}
}
Centremos nuestra atención en el método lookAhead. Cuando el atributo current tenga un
valor indefinido significa que tendremos que recuperar el siguiente valor. Para llevar a cabo
dicha tarea se hace uso del operador de envío de mensajes síncronos !? lo cual implica que
se queda a la espera de la respuesta del coordinador. Los mensajes enviados mediante el
operador !? debe ser respondidos mediante el uso de reply.
En el apéndice Código fuente producers se puede ver código completo del ejemplo descrito
durante esta sección.
30
35. Conclusiones y trabajo futuro
Capítulo 4. Conclusiones y trabajo
futuro
Como punto final de este trabajo haremos un breve resumen de los conceptos analizados
durante todo el trabajo y plantearemos futuras líneas de trabajo que podrían resultar
atractivas para aquellas personas interesadas en esta temática.
4.1. Conclusiones
Durante el desarrollo de este trabajo hemos vistos algunas de las principales características
del lenguaje Scala, haciendo espacial hincapié en cómo se integran las capacidades del
paradigma de orientación a objetos y el funcional. Asimismo hemos analizado el modelo de
concurrencia basado en actores y cómo Scala ofrece dicho modelo de computación mediante
una librería que complementa al lenguaje.
Como ya hemos indicado al inicio del trabajo Scala es el primer lenguaje de propósito general
que integra conceptos del paradigma funcional y el orientado a objetos. Muchos de los
lectores se estarán preguntando cuál es el ámbito de aplicación del lenguaje. La respuesta
corta y concisa podría ser: en todos aquellos lugares donde utilizamos Java. Entrando un poco
más en detalle en las posibles escenarios de aplicación a continuación se indican algunos de
los posibles usos:
• Lenguaje de parte servidora.
• Escritura de scripts
• Desarrollo de aplicaciones robustas, escalables y fiables.
• Desarrollo de aplicaciones web.
• Construcción de lenguajes de dominio específico (DSL)
• ...
31
36. Conclusiones y trabajo futuro
4.2. Líneas de trabajo
Por motivos de espacio y, principalmente, de tiempo se han quedado numerosos temas en
el tintero que podrían resultar interesantes. A continuación se listan y describen, de manera
sumamente breve, algunas sugerencias consideradas interesantes:
• Análisis de la plataforma Akka [http://akka.io] . Plataforma basada en Scala que ofrece
un modelo de actores junto con Sofware Transactional Memory con el objetivo de
proporcionar los fundamentos correctos para la construcción de aplicaciones escalables
y concurrentes.
Una comparación entre el modelo de actores ofrecido por Scala y el modelo de actores
ofrecido por Akka podría resultar atractiva.
• Web funcional. Análisis de cómo las características de los lenguajes funcionales se pueden
utilizar para construir aplicaciones web. Análisis de frameworks web funcionales como Lift
[http://www.liftweb.net/] o Play [http://www.playframework.org/]
• Análisis de interoperabilidad de Scala y Java. Mecanismos y modos de interacción entre
ambos lenguajes. Problemática habitual.
• Scala en .NET.
• Colecciones. Análisis del nuevo API de colecciones diseñado a partir de Scala 2.8. Análisis
de colecciones concurrentes.
• GUI en Scala. Análisis de cómo podemos utilizar Scala para la construcción de aplicaciones
de escritorio.
• Monads en Scala.
• Arquitectura del compilador de Scala.
• Lenguajes de dominio específicos (DSLs). Construcción de lenguajes de dominio específicos
basados en el lenguaje Scala. Combinator parsing.
32
37. Apéndice A. Modelo de objetos de
Scala
A continuación, a modo de resumen, se incluye un diagrama en el que se refleja la jerarquía
de clases presentes en Scala:
33
38. Apéndice B. Producers
El siguiente código fuente contiene la definición del interfaz producer analizado en Ejemplo
completo de actores
B.1. Código fuente completo
package com.blogspot.miguelinlas3.phd.paragprog.actors
import scala.actors.Actor
import scala.actors.Actor._
abstract class Producer[T] {
/** Mensaje indicando que el siguiente valor debe de computarse. */
private val Next = new Object
/** Estado indefinido del iterator. */
private val Undefined = new Object
/** Mensaje para detener al coordinador. */
private val Stop = new Object
protected def produce(x: T) {
coordinator ! Some(x)
receive { case Next => }
}
protected def produceValues: Unit
def iterator = new Iterator[T] {
private var current: Any = Undefined
private def lookAhead = {
if (current == Undefined) current = coordinator !? Next
current
}
def hasNext: Boolean = lookAhead match {
case Some(x) => true
34
39. case None => { coordinator ! Stop; false }
}
def next: T = lookAhead match {
case Some(x) => current = Undefined; x.asInstanceOf[T]
}
}
private val coordinator: Actor = actor {
loop {
react {
case Next =>
producer ! Next
reply {
receive { case x: Option[_] => x }
}
case Stop =>
exit('stop)
}
}
}
private val producer: Actor = actor {
receive {
case Next =>
produceValues
coordinator ! None
}
}
}
object producers extends Application {
class Tree(val left: Tree, val elem: Int, val right: Tree)
def node(left: Tree, elem: Int, right: Tree): Tree =
new Tree(left, elem, right)
def node(elem: Int): Tree = node(null, elem, null)
def tree =
node(node(node(3), 4, node(6)), 8, node(node(9), 10, node(11)))
class PreOrder(n: Tree) extends Producer[Int] {
def produceValues = traverse(n)
def traverse(n: Tree) {
if (n != null) {
produce(n.elem)
traverse(n.left)
traverse(n.right)
}
35
40. }
}
class PostOrder(n: Tree) extends Producer[Int] {
def produceValues = traverse(n)
def traverse(n: Tree) {
if (n != null) {
traverse(n.left)
traverse(n.right)
produce(n.elem)
}
}
}
class InOrder(n: Tree) extends Producer[Int] {
def produceValues = traverse(n)
def traverse(n: Tree) {
if (n != null) {
traverse(n.left)
produce(n.elem)
traverse(n.right)
}
}
}
actor {
print("PreOrder:")
for (x <- new PreOrder(tree).iterator) print(" "+x)
print("nPostOrder:")
for (x <- new PostOrder(tree).iterator) print(" "+x)
print("nInOrder:")
for (x <- new InOrder(tree).iterator) print(" "+x)
print("n")
}
}
36
41. Bibliografía
Scala in Action. Nilanjan Raychaudhuri. Early Access started on March 2010.
Scala in Depth. Joshua D. Suereth. Early Access started on September 2010.
Programming in Scala, Second Edition. Martin Odersky. Lex Spoon. Bill Venners. December 13, 2010.
Programming Scala: Tackle Multi-Core Complexity on the Java Virtual Machine. Venkat Subramaniam.
Jul 2009.
ACTORS: A Model of Concurrent Computation in Distributed Systems. Agha, Gul Abdulnabi. 1985-06-01.
Martin Odersky interview: the future of Scala. http://www.infoq.com/interviews/martin-odersky-
scala-future.
Akka framework. http://akka.io/.
Play framework. http://www.playframework.org/.
Lift framework. http://www.liftweb.net/.
Haskell language. http://www.haskell.org/haskellwiki/Haskell.
Scalaz: pure functional data structures. http://code.google.com/p/scalaz/.
37