Download - Scala Overview

Transcript
Page 1: Scala Overview

Scala: Un lenguaje escalable para la JVMMiguel Pastor

Page 2: Scala Overview

2

Scala: Un lenguaje escalable para la JVMMiguel Pastor

Page 3: Scala Overview

iii

Tabla de contenidos

1.-¿Por qué Scala? 11.1.-¿Qué es Scala? 1

1.1.1.-Orientación a objetos 21.1.2.-Lenguaje funcional 21.1.3.-Lenguaje multiparadigma 31.1.4.-Lenguaje extensible y escalable 31.1.5.-Ejecución sobre la JVM 4

1.2.-En crisis 41.2.1.-Ley de Moore 41.2.2.-Programando para multinúcleo 4

1.3.-Objetivos 52.-Fundamentos de Scala 7

2.1.-Clases y objetos 72.2.-Reglas de inferencia de puntos y coma 82.3.-Singleton objects 92.4.-Objetos funcionales 9

2.4.1.-Números racionales 92.4.2.-Constructores 102.4.3.-Sobreescritura de métodos 102.4.4.-Precondiciones 102.4.5.-Atributos y métodos 112.4.6.-Operadores 12

2.5.-Funciones y closures 122.5.1.-Funciones first-class 132.5.2.-Closures 132.5.3.-Tail recursion 14

2.6.-Currying 142.7.-Traits 15

2.7.1.-¿Cómo funcionan los traits? 152.7.2.-Ejemplo: objetos rectangulares 162.7.3.-Uso de traits como stackable modifications 172.7.4.-Traits: ¿si o no? 19

2.8.-Patrones y clases case 202.8.1.-Clases case 202.8.2.-Patrones: estructura y tipos 21

2.8.2.1.-Patrones wildcard 212.8.2.2.-Patrones constantes 212.8.2.3.-Patrones variables 21

Page 4: Scala Overview

iv

2.8.2.4.-Patrones constructores 222.8.2.5.-Patrones de secuencia 222.8.2.6.-Patrones tipados 22

2.9.-Conclusiones 233.-Actors y concurrencia 24

3.1.-Problemática 243.2.-Modelo de actores 253.3.-Actores en Scala 25

3.3.1.-Buenas prácticas 263.3.1.1.-Ausencia de bloqueos 263.3.1.2.-Comunicación exclusiva mediante mensajes 273.3.1.3.-Mensajes inmutables 273.3.1.4.-Mensajes autocontenidos 27

3.4.-Un ejemplo completo 283.4.1.-Especificación del problema 283.4.2.-Implementación del producer y coordinator 283.4.3.-Interfaz iterator 29

4.-Conclusiones y trabajo futuro 314.1.-Conclusiones 314.2.-Líneas de trabajo 32

A.-Modelo de objetos de Scala 33B.-Producers 34

B.1.-Código fuente completo 34Bibliografía 37

Page 5: Scala Overview

¿Por qué Scala?

1

Capítulo 1. ¿Por qué Scala?

Durante este capítulo se cubrirán aspectos relativos como qué es Scala, características dealto nivel del lenguaje o por qué deberíamos escoger Scala como nuestro siguiente lenguajede programación. En la parte final del mismo analizaremos los objetivos perseguidos con eldesarrollo de este trabajo así como la estructura del mismo.

Debido a la creciente proliferación de una gran cantidad de diferentes lenguajes enlas plataformas JVM, .NET, OTP entre otras muchas ha surgido el dilema entre losdesarrolladores acerca de cuál debería ser el siguiente lenguaje de progragramación que sedeberí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 lenguajeestableciendo una comparación con aquellos lenguajes con los que estemos másfamiliarizados. Los programadores provenientes de la orientación a objetos así comoaquellos cuyo origen es la programación funcional rápidamente se sentirán cómodos conScala dado que este lenguaje soporta ambos paradigmas. Scala es uno de esos extrañoslenguajes en los que se integran de manera satisfactoria las características de los lenguajesorientados a objetos y los funcionales.

1.1. ¿Qué es Scala?

Scala es un lenguaje de propósito general diseñado para expresar los patrones deprogramación más comunes de una manera sencilla, elegante y segura. Integra de manerasencilla características de orientación a objetos y lenguajes funcionales, permitiendo de estemodo que los desarrolladores puedan ser más productivos. Su creador, Martin Odersky, y suequipo comenzaron el desarrollo de este nuevo lenguaje en el año 2001, en el laboratoriode métodos de programación en EPFL1

Scala hizo su aparación pública sobre la plataforma JVM (Java Virtual Machine) en enero de2004 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 lenguajesde programación, ha adquirido una notable popularidad la cual se acrecenta día tras día.

1École Polytechnique Fédérale de Lausanne

Page 6: Scala Overview

¿Por qué Scala?

2

1.1.1. Orientación a objetos

La popularidad de lenguajes como Java, C# o Ruby han hecho que la programaciónorientada a objetos sea un paradigma ampliamente aceptado entre la mayoría dedesarrolladores. Aunque existen numerosos lenguajes orientados a objetos en el ecosistemaactual únicamente podríamos encajar unos pocos si nos ceñimos a una definición estrictade orientación a objetos. Un lenguaje orientado a objetos "puro" debería presentar lassiguientes 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 modelopuro de orientación a objetos muy similar al presentado por Smalltalk (lenguaje creado porAlan Kay sobre el año 1980).2

De manera adicional a todas las caracteríscticas puras de un lenguaje orientado a objetospresentadas anteriormente, Scala añade algunas innovaciones en el espacio de los lenguajesorientados a objetos:

• Composición modular de mixin. Mecanismo que permite la composición de clases para eldiseño de componentes reutilizables evitando los problemas presentados por la herenciamúltiple. Similar a los interfaces Java y las clases abstractas. Por una parte se puedendefinir múltiples "contratos" (del mismo modo que los interfaces). Por otro lado, sepodrí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 lasque se está entremezclando aunque en determinadas ocasiones será necesario hacer usode las mismas. Esta capacidad es conocida en Scala como self-type3

• Abstracción de tipos. Existen dos mecanismos principales de abstracción en los lenguajesde programación: la parametrización y los miembros abstractos. Scala soporta ambosestilos 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 laevaluación de funciones matemáticas y se evitan los programas con estado y datos quepuedan ser modificados. Se ofrece adopta una visión más matemática del mundo en el que

2http://en.wikipedia.org/wiki/Smalltalk

Page 7: Scala Overview

¿Por qué Scala?

3

los programas están compuestos por numerosas funciones que esperan una determinadaentrada 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 colateralesgracias a los cuales los programas desarrollados son mucho más sencillos de comprender yprobar. Adicionalmente, se facilita la programación concurrente, evitando que se conviertaen un problema gracias a la ausencia de cambio.

Los lenguajes de programación que soportan este estilo de programación deberían ofreceralgunas 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 estetipo de lenguajes no se permiten las modificaciones y las variables se utilizan de maneramatemática.4. Scala da soporte tanto a variables inmutables (tambien conocidas comovalues) 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óna objetos en un lenguaje estáticamente tipado. La pregunta es por qué necesitamas más deun estilo de programación.

El objetivo principal de la computación multiparadigma es ofrecer un determinado conjuntode mecanismos de resolución de problemas de modo que los desarrolladores puedanseleccionar 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 quepermita el crecimiento y la escalabilidad en función de la exigencia del desarrollador. Scalapuede ser utilizado como lenguaje de scripting así como también se puede adoptar en elproceso de construcción de aplicaciones empresariales. La conjunción de su abastracción de

4Un ejemplo de lenguaje funcional puro sería Haskell

Page 8: Scala Overview

¿Por qué Scala?

4

componentes, su sintaxis reducida, el soporte para la orientación a objetos y funcional hancontribuido 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), unapulida maquinaria que el equipo de HotSpot ha ido mejorando a lo largo de los años. Puestoque Scala es un lenguaje basado en la JVM se integra a la perfección dentro con Java y suecosistema (herramientas, IDEs, librerías, . . .) por lo que no será necesario desprenderse detodas las inversiones hechas en el pasado.

El compilador de Scala genera bytecode siendo indistinguible, a este nivel, el código escritoen Java y el escrito en Scala. Adicionalmente, puesto que se ejecuta sobre la JVM, se beneficiadel rendimiento y estabilidad de dicha plataforma. Y siendo un lenguaje de tipado estáticolos 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 desarrolladoressoftware encuentran los mecanismos para agotarla. El motivo es que, gracias al software,se están resolviendo problemas muy complejos, y esta tendencia continuará creciendo, almenos en el futuro cercano.

La pregunta clave es si los fabricantes de procesadores serán capaces de sostener la demandade 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 transistorespor circuito integrado se duplica cada dos años aproximadamente. Sin embargo, muchosfabricantes están tocando techo con esta ley6 y están apostando por los procesadoresmultinúcleo. Las buenas noticias es que la potencia de los procesadores seguirá creciendode manera notable aunque las malas noticias es que los programas actuales y entornos dedesarrollo necesitarán cambiar para hacer uso de las ventajas ofrecidas por una CPU convarios núcleos.

1.2.2. Programando para multinúcleo

¿Cómo se puede beneficiar el software de la nueva relución iniciada por los procesadoresmultimedia?

5Concretamente la fecha data del 19 de Abril de 19656http://www.gotw.ca/publications/concurrency-ddj.htm

Page 9: Scala Overview

¿Por qué Scala?

5

Concurrencia. La concuncurrencia será, si no lo es ya, el modo en el que podremosescribir soluciones software que nos permitan resolver problemas complejos, distrubidos yempresariales, beneficiándonos de la productividad ofrecida por múltiples núcleos. Al fin yal cabo, ¿quién no desea software eficiente?.

En el modelo tradicional de concurrencia basado en hilos los programas son "troceados" enmúltiples unidades de ejecución concurrentes (threads) en el que cada de ellos opera enun segmento de memoria compartida. En numerosas ocasiones el modelo anterior ocasiona"condiciones de carrera" complicadas de detectar así como siuaciones de "deadlocks" queocasionan inversiones de semanas completas intentando reproducir, aislar y subsanar elerror. El origen de todas estos problemas no radica en el modelo de hilos sino que resideen segmentos de memoria compartida. La programación concurrente se ha convertido enun modelo demasiado complicado para los desarrolladores por lo que necesitaríamos unmejor modelo de programación concurrente que nos permita crear y mantener programasconcurrentes de manera sencilla.

Scala adopta un enfoque completamente diferente a la problemática de la concurrencia: elmodelo basado en actores. Un actor es un modelo matemático de computación concurrenteen el que se encapsulan datos, código y su propio hilo de control, comunicándose de maneraasíncrona mediante técnicas de paso de mensajes inmutables. La arquitectura base de estemodelo 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 CarlHewitt en el año 1973 en el famoso artículo "A Universal Modular ACTOR Formalism forArtificial Intelligence " , para posteriormente ser mejorado por Gul Agha con su “ACTORS: AModel of Concurrent Computation in Distributed Systems”).

El primer lenguaje que llevó a cabo la implementación de este modelo fue Erlang. Tras el éxitoobtenido 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 parasolucionar la problemática derivada de la concurrencia, por lo que Scala ha decidido adoptareste mismo enfoque.

1.3. Objetivos

A lo largo de las secciones anteriores hemos descrito de manera superficial algunas de lascaracterísticas más relevantes del lenguaje Scala así como las motivaciones principales delmismo. El resto del trabajo estará dividido en las siguientes secciones:

• Durante la primera sección, esta que nos ocupa, analizamos las características generalesdel lenguaje y los objetivos del presente documento.

• La segunda sección abarcará los principales fundamentos del lenguaje, describiendo tantolos mecanismos funcionales como la orientación a objetos, ambos disponibles de maneranativa en el lenguaje.

• Analizaremos, aunque no de manera excesivamente exhaustiva, el modelo deprogramació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

Page 10: Scala Overview

¿Por qué Scala?

6

conocimientos adquiridos, tanto aquellos relativos a la programación concurrente comolos analizados en la primera parte del trabajo.

• Debido a limitaciones de tiempo y espacio no podremos abordar muchos temasinteresantes realacionados con el lenguaje Scala. por lo que durante la última secciónde este trabajo se propondrán numerosos temas de ampliación: web funcional, otrasaproximaciones de actores en Scala, arquitectura del compilador Scala, etc

Page 11: Scala Overview

Fundamentos de Scala

7

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 lafuncional. 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 delos 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ónde 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 palabrareservada new

val v = new MyFirstClass

En Scala existen dos tipos de variables, vals y vars, que deberemos especificar a la hora dedefinir las mismas:

• Se utilizará la palabra reservada val para indicar que es inmutable. Una variable de estetipo 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 conesto 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 laafirmación que su conjunto de atributos (variables de instancia) permanece constante a lolargo de todo el ciclo de vida del mismo. El primer paso para evitar que agentes externostengan acceso a los campos de una clase es declarar los mismos como private. Puesto quelos campos privados sólo podrán ser accedidos desde métodos que se encuentran definidos

Page 12: Scala Overview

Fundamentos de Scala

8

en la misma clase, todo el código podría modificar el estado del mismo estará localizado endicha clase.1

El siguiente paso será incorporar funcionalidad a nuestras clases; para ello podremos definirmé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 soninmutables, es decir, vals. Por tanto, si intentamos modificar el valor de un parámetro en elcuerpo 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 necesarioel uso explícito de la palabra return, Scala retornará el valor de la última expresión queaparece 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 acabo la definición de este tipo de métodos consiste en eliminar el tipo de retorno y el símboloigual 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 mismospuede llevarnos a resultados no esperados. Por noram general los saltos de línea son tratadoscomo puntos y coma salvo que algunas de las siguientes condiciones sea cierta:

1Por 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

Page 13: Scala Overview

Fundamentos de Scala

9

• 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 últimosno 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 lugarel concepto de singleton objects. La definición de objetos de este tipo es muy similar a la delas 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 esconocido como companion object mientras que la clase se denomina companion class delobjeto singleton. Inicialmente, sobre todo aquellos desarrolladores provenientes del mundoJava, podrían ver este tipo de objetos como un contenedor en el que se podrían definir tantosmétodos estáticos como quisiéramos.

Una de las principales diferencias entre los singleton objects y las clases es que los primerosno aceptan parámetros (no podemos instanciar un objeto singleton mediante la palabrareservada new) mientras que las segundos si lo permiten. Cada uno de los singleton objectses implementado mediante una instancia de una synthetic class referenciada desde unavariable estática, por lo que presentan la misma semántica de inicialización que los estáticosde 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ásicosrelativos a la orientación a objetos ofrecida por Scala. Durante las siguientes páginasanalizaremos cómo se pueden construir objetos funcionales, es decir, inmutables, mediantela definición de clases. El desarrollo de esta sección nos permitirá ahondar en cómo losaspectos funcionales y los de orientación a objetos confluyen en el lenguaje. Adicionalmentelas siguientes secciones no servirán como base para la introducción de nuveos conceptos deorientación a objetos cómo parámetros de clase, sobreescritura, self references o métodosentre 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 elcomportamiento de este tipo de números. A continuación se presentan algunas de suscaracterísticas principales:

Page 14: Scala Overview

Fundamentos de Scala

10

• Suma/resta de números racionales. Se debe obtener un común denominador de ambosdenominadores y posteriormente sumar/restar los numeradores.

• Multiplicación de números racionales. Se multiplican los numeradores y denominadoresde los integrantes de la operación.

• División de números racionales. Se intercambian el numerador y denominador deloperando que aparece a la derecha y posteriormente se realiza una operación demultiplicación.

2.4.2. Constructores

Puesto que hemos decidido que nuestros números racionales sean inmutablesnecesitaremos que los clientes de esta clase proporcionen toda la información en elmomento 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 declase. El compilador generará un constructor primario en cuya signatura aparecerán los dosparámetros escritos en la definición de la clase. Cualquier código que escribamos dentro delcuerpo de la clase que no forme parte de un atributo o de un método será incluido en elconstructor 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 tendremosque hacer uso de la palabra reservada override. Por ejemplo, si en la clase Rational deseamossobreescribir la implementación por defecto de toString podríamos actuar del siguientemodo:

override def toString = n + "/" + d

2.4.4. Precondiciones

Una de las características de los números racionales no admiten el valor cero comodenominador aunque sin embargo, con la definición actual de nuestra clase Rationalpodríamos escribir código como:

new Rational(11,0)

algo que violaría nuestra definición actual de números racionales. Dado que estamosconstruyendo una clase inmutable y toda la información debe estar disponible en elmomento que se invoca al constructor este último deberá asegurarse de que el denominadorindicado 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

Page 15: Scala Overview

Fundamentos de Scala

11

establecerse sobre los valores pasados a métodos o constructores y que deben sersatisfechas 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 unargumento booleano. En caso de que la condición exigida no se cumpla el método requiredisparará 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 comoparámetro y retorne como resultado la suma de ambos operandos. Puesto que estamosconstruyendo una clase inmutable el nuevo método deberá retornar la suma en un nuevonú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 dadoque 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 realizala llamada.

Para resolver el problema planteado en el fragmento de código anterior tendremos quedeclarar 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)}

Page 16: Scala Overview

Fundamentos de Scala

12

Nótese que en los fragmentos de código anteriores estamos manteniendo la inmutabilidadde nuestro diseño. En este caso, el operador de adición add retorna un nuevo objeto racionalque representa la suma de ambos números, en lugar de realizar la suma sobre el objeto querealiza la llamada.

A continuación incorporemos a nuestra clase un método privado que nos ayude a determinarel 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 anuestra clase que, en este caso, nos sirve como método auxiliar para calcular el máximocomún divisor de dos números enteros.

2.4.6. Operadores

La implementación actual de nuestra clase Rational es correcta aunque podríamos definirlade modo que su uso resultara mucho más intuitivo. Una de las posibles mejoras quepodrí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 lenguajeScala, poniendo de manifiesto la incorporación de fundamentos de lenguajes funcionales asícomo de lenguajes orientados a objetos.

2También podríamos escribir a.+(b) aunque en este caso el código resultante sería mucho menos legible

Page 17: Scala Overview

Fundamentos de Scala

13

Cuando nuestros programas crecen necesitamos hacer uso de un conjunto de abstraccionesque nos permitan dividir dicho programa en piezas más pequeñas y manejables que permitanuna mejor comprensión del mismo. Scala ofrece varios mecanismos para definir funcionesque no están presentes en Java. Además de los métodos, que no son más que funcionesmiembro de un objeto, podemos hacer uso de funciones anidadas en funciones, functionliterals y function values. Durante las siguientes secciones de este apartado profundizaremosen 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 classfunctions. No sólamente podemos definir funciones e invocarlas sino que también podemosdefinirlas como literales para, posteriormente, pasarlas como valores.

Las funciones literales son compiladas en una clase que, cuando es instanciada, se convierteen una function value. Por lo tanto, la principal diferencia entre las funciones literales y lasfunciones valor es que las primeras existen en el código fuente mientras que las segundasexisten 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 alnúmero indicado:

(x:Int) => x + 1

Las funciones valor son objetos propiamente dichos por lo que podemos almacenarlas envariables 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 yexclusivamente, de los parámetros pasados a la función. Sin embargo, podríamos definirfunciones literales en las que se hace uso de variables definidas en otro punto de nuestroprograma:

(x:Int) = x * other

La variable other es conocida como una free variable puesto que la función no le da unsignificado a la misma. Al contrario, la variable x es conocida como bound variable puestoque tiene un significado en el contexto de la función. Si intentamos utilizar esta función en uncontexto en el que no está accesible una variable other obtendremos un error de compilaciónindicá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 comoclosure. 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 presentafree variables, creada en tiempo de ejecución a partir de su función literal no es una closureen el sentido más estricto de la definición dado que dicha función ya se encuetra "cerrada"en el momento de su escritura.

Page 18: Scala Overview

Fundamentos de Scala

14

El fragmento de código anterior hace que nos planteemos la siguiente pregunta: ¿que ocurresi la variable other es modificada después de que la closure haya sido creada? La respuesta essencilla: en Scala la closure tiene visión sobre el cambio ocurrido. La regla anterior tambiénse cumple en sentido contrario: su una closure modifica alguno de sus valores capturadosestos ú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 unconjunto 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 últimasentencia del cuerpo de la función) son llamadas funciones tail recursive. El compilador deScala detecta esta situación y reemplaza la última llamada con un salto al comienzo de lafunció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 lamáquina virtual (JVM) dificulta de manera notable la implementación de otros tipos de tailrecursion. 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, nose 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 aunquenos 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 decontrol 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 lenguajesfuncionales: currying. Una curried function es aplicada múltiples listas de argumentos enlugar de una sola. El siguiente fragmento de código nos muestras una función tradicional querecibe 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 decódigo anterior:

Page 19: Scala Overview

Fundamentos de Scala

15

def curriedSum(x:Int)(y:Int) = x + y

Cuando ejecutamos la sentencia curriedSum(9)(2) estamos obteniendo dos llamadastradicionales de manera consecutiva. La primera invocación recibe el parámetro x y retornauna función valor para la segunda función. Esta segunda función recibe el parámetro y. Elsiguiente código muestra una función first que lleva a cabo lo que haría la primera de lasinvocaciones 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 encapsuladefiniciones de métodos y atributos que pueden ser reutilizados mediante un proceso demixin llevado a cabo en conjunción con las clases. Al contrario que en el mecanismo deherencia, en el que únicamente se puede tener un padre, una clase puede llevar a cabo unproceso 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 palabrareservada 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 palabrasreservadas extend o with en XXXX analizaremos las diferencias e implicaciones de cada unade 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 estaremosheredando de manera implícita las superclases del trait. Los métodos heredados de un traitse utilizan del mismo modo que se utilizan los métodos heredados de una clase. De maneraadicional, un trait también define un tipo.

Page 20: Scala Overview

Fundamentos de Scala

16

En el caso de que deseemos realizar un proceso de mixin en el que una clase ya indicaun padre de manera explicita mediante el uso extends tendremos que utilizar la palabrareservada with. Si deseamos incluir en el proceso de mixin múltiples traits no trendremosmás que incluir más cláusulas with.

Llegados a este punto podríamos pensar que los traits son como interfaces Java con métodosconcretos pero realmente pueden hacer muchas más cosas. Por ejemplo, los traits puedendefinir atributos y mantener un estado. Realmente, en un trait podemos hacer lo mismo queen 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 constructorprimario de la clase).

• Mientras que en las clases las llamadas a métodos de clases padre (super.xxx) sonenlazadas de manera estática en el caso de los traits dichas llamadas son enlazadasdinámicamente. Si en una clase escribimos super.method() sabremos en todo momentoque implementación del método será invocada. Sin embargo, el mismo código escrito enun trait provoca un desconocimiento de la implementación del método que será invocadoen tiempo de ejecución. Dicha implementación será determinada cada una de las vecesque un trait y una clase realizan un proceso de mixin. Este curioso comportamiento desuper es la clave que permite a los traits trabajar como stackable modifications3 queveremos a continuación.

2.7.2. Ejemplo: objetos rectangulares

Las librerías gráficas habitualmente presentan numerosas clases que representan objetosrectangulares: ventanas, selección de una región de la pantalla, imágenes, etc. Son muchoslos métodos que nos gustaría tener en el API por lo que necesitaríamos una gran cantidad dedesarrolladores 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 losmé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 {

Page 21: Scala Overview

Fundamentos de Scala

17

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. Incorporemosun 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 incorporartoda 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 demixin 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 enriquecimientode interfaces. Durante la sección que nos ocupa analizaremos otro de los usos más popularesde los traits: facilitar stackable modifications en las clases. Los traits nos permitirán modificarlos métodos de una clase y, adicionalmente, nos permitirá apilarlas entre si.

Apilemos modificaciones sobre una cola de números enteros. Dicha cola tendrá dosoperaciones: 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" porlo que el método get tendría que retornar los elementos en el mismo orden en el que fueronintroducidos.

Page 22: Scala Overview

Fundamentos de Scala

18

Dada una clase que implementa el comportamiento descrito en el párrafo anteriorpodrí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ñadidoa la cola.

Los tres traits anteriores representan modificaciones dado que no definen una cola porsi mismos sino que llevan a cabo modificaciones sobre la cola subyacente con la querealizan el proceso de mixin. Los traits también son apilables: podríamos escoger cualquiersubconjunto de los tres anteriores e incorporarlos a una clase de manera que conseguiríamosuna nueva clase con la funcionalidad deseada. El siguiente fragmento de código representauna implementación reducida del comportamiento de una cola descrito en el inicio de estasecció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 ahacer uso de los traits. El siguiente fragmento de código muestra un trait que duplica el valorde 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 demodificadores sólo puede ser utilizada en los traits y no en las clases, e indica que el traitdebe ser integrado (mixed) con una clase que presenta una implementación concreta delmétodo en cuestión.

A continuación se muestra un ejemplo de uso del trait anterior:

scala> class MyQueue extends BasicIntQueue with Doublingdefined class MyQueue

scala> val queue = new MyQueuequeue: MyQueue = MyQueue@91f017

Page 23: Scala Overview

Fundamentos de Scala

19

scala> queue.put(10)

scala> queue.get()res12: Int = 20

Para analizar el mecanismo de apilado de modificaciones implementemos en primer lugarlos 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 modoque 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étodode una clase con mixins el método del trait definido más a la derecha es el primero enser invocado. Si dicho método invoca a super este invocará al trait que se encuentra mása la izquierda y así sucesivamente. En el ejemplo anterior, el método put del trait Filterserá invocado en primer lugar, por lo que aquellos números menores que cero no seránincorporados a la cola. El método put del trait Filter sumará el valor uno a cada uno de losnú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 pretendenayudar al lector a determinar cuando debería usar estas construcciones proporcionadas porel 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 relacionadasentonces construir un trait.

Page 24: Scala Overview

Fundamentos de Scala

20

• Si se desee que una clase Java herede de nuestra funcionalidad entonces deberemosutilizar una clase abstracta.

• Si la eficiencia es importante deberíamos inclinarnos hacia el uso de las clases. La mayoríade los entornos de ejecución Java hacen una llamada a un método virtual de unaclase mucho más rápido que la invocación de un método de un interfaz. Los traits soncompilados a interfaces y podríamos penalizar el rendimiento.

• Si tras todas las opciones anteriores no tenemos claro qué aproximación deseamosutilizar 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 paradigmafuncional reconocerán el uso de la concordancia de patrones. Las clases case son un conceptorelativamente novedoso y nos permiten incorporar el mecanismo de matching de patronessobre objetos sin la necesidad de código repetitivo. De manera general, no tendremos másque prefijar la definición de una clase con la palabra reservada case para indicar que la clasedefinida 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 unconjunto 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 defacilidades a la clase indicada.

En primer lugar incorpora un factory-method con el nombre de la clase. Gracias a estopodríamos escribir código como Foo("x") para construir un objeto Foo en lugar de newFoo("x"). Una de las principales ventajas de este tipo de métodos es la ausencia deoperadores new cuando los anidamos:

val op = BinaryOperation("+", Number(1), v)

Otra funcionalidad sintáctica incorporada por el compilador es que todos los argumentos enla lista de parámetros incorporan de manera implicita el prefijo val por lo que éstos últimosserán atributos de clase. Por último, pero no por ello no menos importante, el compiladorañade implementaciones "instintivas" de los métodos toString, hashCode e equals.

Todas estas facilidades incorporadas acarrean un pequeño coste: las clases y objetosgenerados son un poco más grandes4 y tenemos que incorporar la palabra case en lasdefiniciones de nuestras clases. La principal ventaja de este tipo de clases es que soportanla concordancia de patrones.

4Son más grandes porque se generan métodos adicionales y se incorporan atributos implícitos para cada uno de los parámetrosdel constructor

Page 25: Scala Overview

Fundamentos de Scala

21

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 palabrareservada case. Cada una de estas alternativas incorpora un patrón y una o más expresionesque serán evaluadas en caso de que se produzca la concordancia del patrón. Se utiliza elsí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 sumamentesencilla por lo que vamos a profundizar en los diferentes tipos de patrones que podemosconstruir.

2.8.2.1. Patrones wildcard

El patrón (_) concuerda con cualquier objeto por lo que podríamos utilizarlo como unaalternativa 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 siguientefragmento 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 patroneswildcard. A diferencia de los patrones wildcard, Scala enlaza la variable al objeto, por lo queposteriormente podremos utilizar dicha variable para actuar sobre el objeto:

expr match {

Page 26: Scala Overview

Fundamentos de Scala

22

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 herramientamuy poderosa. Básicamente están formados por un nombre y un número indefinido depatrones. Asumiendo que el nombre designa una clase de tipo case este tipo de patronescomprobarán primero si el objeto pertenece a dicha clase, para, posteriormente comprobarsi 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 seincluye un pequeño ejemplo en el que se comprueba que el objeto de primer nivel es de tipoBinaryOperation 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 lohacemos para las clases. Deberá utilizarse la misma sintáxis aunque ahora podremos indicarcualquier número de elementos en el patrón.

El siguiente fragmento de código muestra un patrón que comprueba una lista de treselementos 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 yconversiones de tipos:

def genericSize(x: Any) = x match { case s: String => s.length case m: Map[_, _] => m.size case _ => -1}

Page 27: Scala Overview

Fundamentos de Scala

23

El método genericSize retorna la longitud de un objeto cualquiera. El patrón "s:String" es unpatrón tipado: cualquier instancia no nula de tipo String concordará con dicho patrón. Lavariable 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 dellenguaje de programación Scala, haciendo especial hincapié en como el lenguaje incorporafuncionalidades provenientes de los paradigmas funcional y orientado a objetos.

Las secciones anteriores nos permitirán comenzar a escribir nuestros primeros programas enScala 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 sepodrí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 ycómo esta aproximación nos permitirá construir aplicaciones concurrentes de manera mássencilla.

Page 28: Scala Overview

Actors y concurrencia

24

Capítulo 3. Actors y concurrencia

En muchas ocasiones cuando estamos escribiendo nuestros programas necesitamos quemuchas de sus partes se ejecuten de manera independiente, es decir, de maneraconcurrente. Aunque el soporte introducido por Java es suficiente, a medida que lacomplejidad y tamaño de los programas se incrementan, conseguir que nuestro códigoconcurrente funcione de manera correcta se convierte en una tarea complicada. Los actoresincluyen un modelo de concurrencia más sencillo con el que podremos evitar la problemáticahabitual ocasionada por el modelo de concurrencia nativo de Java.

Durante este capítulo presentaremos los fundamentos del modelo de actores y comopodremos 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 bloqueosy memoria compartida. Cada uno de los objetos lleva asociado un monitor que puedeser utilizado para realizar el control de acceso de múltiples hilos sobre los datos. Parautilizar este modelo debemos decidir que datos queremos compartir entre múltiples hilosy establecer como synchronized aquellas secciones de código que acceden a segmentosde datos compartidos. En tiempo de ejecución, Java utiliza un mecanismo de bloqueo conel que se garantiza que en un instante de tiempo T un único hilo accede a una secciónsincronizada S. Los desarrolladores han encontrado numerosos problemas para construiraplicaciones robustas con esta aproximación, especialmente cuando los problemas crecenen tamaño y complejidad. En cada punto del programa el desarrollador debe razonar sobrelos datos modificados y/o accedidos que pueden ser accedidos por otros hilos y establecerlos bloqueos necesarios.

Añadiendo un nuevo inconveniente a los descritos en el párrafo anterior, los procesos deprueba no son fiables cuando tratamos con códigos multihilo. Dado que los hilos son nodeterministas, podríamos probar nuestra aplicaciones miles de veces de manera satisfactoriay obtener un error la primera vez que se ejecuta el mismo código en la máquina de nuestrocliente.

La aproximación seguida por Scala1 para resolver el problema de la concurrencia ofreceun modelo alternativo de no compartición y paso de mensajes. En la siguiente sección seofrecerá una definición resumida del modelo de actores.

1Realmente Scala ofrece el modelo de actores mediante una librería del lenguaje en lugar de manera nativa

Page 29: Scala Overview

Actors y concurrencia

25

3.2. Modelo de actores

El modelo de actores ofrece una solución diferente al problema de la concurrencia. En lugarde procesos interactuando mediante memoria compartida, el modelo de actores ofrece unasolución basada en buzones y paso de mensajes asíncronos. En estos buzones los mensajespueden ser almacenados y recuperados para su procesamiento por otros actores. En lugarde compartir variables en memoria, el uso de estos buzones nos permite aislar cada uno delos procesos.

Los actores son entidades independientes que no comparten ningún tipo de memoria parallevar a cabo el proceso de comunicación. De hecho, los actores únicamente se puedencomunicar a través de los buzones descritos en el párrafo anterior. En esta aproximaciónde concurrencia no existen bloqueos ni secciones sincronizadas por lo que los problemasderivados de las mismas (deadlocks, pérdida de actualizaciones de datos, . . .) no existen eneste modelo. Los actores están pensados para trabajar de manera concurrente, no de modosecuencial.

El modelo de actores no es una novedad: Erlang basa su modelo de concurrencia en actoresen lugar de hilos. De hecho, la popularidad alcanzada por Erlang en determinados ámbitosempresariales han hecho que la popularidad del modelo de actores haya crecido de maneranotable 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 eimplementar el método act. El siguiente fragmento de código ilustra un actor sumamentesimple 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étodode utilidad actor disponible en scala.actors.Actor:

scala> import scala.actors.Actor._

Page 30: Scala Overview

Actors y concurrencia

26

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 maneraindependiente pero, ¿cómo conseguimos que dos actores trabajen de manera conjunta? Taly como hemos descrito en la sección anterior, los actores se comunican mediante el paso demensajes. Para enviar un mensaje haremos uso del operador !.

Definamos ahora un actor que haga uso de su buzón, esperando por un mensaje eimprimiendo 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. Elmensaje enviado queda a la espera en el buzón del receptor hasta que este último ejecutela 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 ! 11Received message 11

3.3.1. Buenas prácticas

Llegados a este punto conocemos los fundamentos básicos para escribir nuestros propiosactores. El punto fuerte de los métodos vistos hasta este momento es que ofrecen unmodelo de programación concurrente basado en actores por lo que, en la medida quepodamos 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 manerabreve, algunas directrices que nos permitirán adopotar un estilo de programación basadoen actores.

3.3.1.1. Ausencia de bloqueos

Un actor no debería bloquearse mientras se encuentra procesando un mensaje. El problemaradica en que mientras un actor se bloquea, otro actor podría realizar una petición sobre

Page 31: Scala Overview

Actors y concurrencia

27

el primero. Si el actor se bloquea en la primera petición no se dará cuenta de una segundasolicitud. En el peor de los casos, se podría producir un deadlock en el que varios actoresestá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 laacción está lista para ser ejecutada. Esta nueva disposición, por norma general, implicará laparticipació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 (elmétodo act de cada actor) en el que podríamos razonar de manera secuencial. Expresándolode manera diferente, los actores nos permiten escribir programas multihilo como unconjunto independiente de programas monohilo. La simplificación anterior se cumplesiempre y cuando el único mecanismo de comunicación entre actores sea el paso demensajes.

3.3.1.3. Mensajes inmutables

Puesto que el modelo de actores provee un entorno monohilo dentro de cada método act nodebemos preocuparnos si los objetos que utilizamos dentro de la implementación de dichomé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 entreactores dado que es compartida por varios de ellos. Por tanto, tendremos que preocuparnosde 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 decódigo que realiza la llamada se encuentra en una posición idónea para recordar lo queestaba haciendo anteriormente a la ejecución del método, recoger el resultado y actuar enconsecuencia.

Sin embargo, en el modelo de actores las cosas se vuelven un poco más complicadas. Cuandoun actor realiza una petición a otro actor el primero de ellos no es consciente del tiempo quetardará la respuesta, instantes en los que dicho actor no debería bloquearse, sino que deberíacontinuar ejecutando otro trabajo hasta que la respuesta a su petición le sea enviada. ¿Puedeel 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árrafoanterior:

Page 32: Scala Overview

Actors y concurrencia

28

• Un mecanismo para simplificar la lógica de los actores sería incluir información redundanteen los mensajes. Si la petición es un objeto inmutable, el coste de incluir una referencia ala solicitud en el valor de retorno no sería costoso.

• Otro mecanismo adicional que nos permitiría incrementar la redundancia en los mensajessería la utilización de una clase diferente para cada uno de las clases de mensajes quedispongamos

3.4. Un ejemplo completo

Como ejemplo completo de aplicación del modelo de actores descrito a lo largo de lassecciones anteriores y con el objetivo de asentar y poner en marcha los conocimientosadquiridos 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 lacual ofrecerá una interfaz de iterador estándar para la recuperación de una secuencia devalores.

La definición de un producer específico se realiza mediante la implementación del métodoabstracto produceValues. Los valores individuales son generados utilizados el métodoproduce. 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 unactor coordinador. El siguiente fragmento de código ilustra cómo podríamos llevar a cabo ladefinición del actor productor:

abstract class Producer[T] {

Page 33: Scala Overview

Actors y concurrencia

29

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 actorproductor recibe el mensaje Next éste ejecuta el método (abstracto) produceValues, quedesencadena la ejecución del método produce. Ésta ultima ejecución provoca el envío de unaserie de valores (recubiertos por el message Some) al coordinador.

El coordinador es el responsable de sincronizar las peticiones de los clientes y los valoresprovenientes del productor. Una posible implementación del actor coordinador se ilustra enel 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 utilizamoslos iteradores tradicionales. Nótese como los métodos hasNext y next envían sendosmensajes al actor coordinador para llevar a cabo su tarea:

def iterator = new Iterator[T] {

private var current: Any = Undefined

Page 34: Scala Overview

Actors y concurrencia

30

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 unvalor indefinido significa que tendremos que recuperar el siguiente valor. Para llevar a cabodicha tarea se hace uso del operador de envío de mensajes síncronos !? lo cual implica quese queda a la espera de la respuesta del coordinador. Los mensajes enviados mediante eloperador !? debe ser respondidos mediante el uso de reply.

En el apéndice Código fuente producers se puede ver código completo del ejemplo descritodurante esta sección.

Page 35: Scala Overview

Conclusiones y trabajo futuro

31

Capítulo 4. Conclusiones y trabajofuturo

Como punto final de este trabajo haremos un breve resumen de los conceptos analizadosdurante todo el trabajo y plantearemos futuras líneas de trabajo que podrían resultaratractivas para aquellas personas interesadas en esta temática.

4.1. Conclusiones

Durante el desarrollo de este trabajo hemos vistos algunas de las principales característicasdel lenguaje Scala, haciendo espacial hincapié en cómo se integran las capacidades delparadigma de orientación a objetos y el funcional. Asimismo hemos analizado el modelo deconcurrencia basado en actores y cómo Scala ofrece dicho modelo de computación medianteuna librería que complementa al lenguaje.

Como ya hemos indicado al inicio del trabajo Scala es el primer lenguaje de propósito generalque integra conceptos del paradigma funcional y el orientado a objetos. Muchos de loslectores se estarán preguntando cuál es el ámbito de aplicación del lenguaje. La respuestacorta y concisa podría ser: en todos aquellos lugares donde utilizamos Java. Entrando un pocomás en detalle en las posibles escenarios de aplicación a continuación se indican algunos delos 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)

• . . .

Page 36: Scala Overview

Conclusiones y trabajo futuro

32

4.2. Líneas de trabajo

Por motivos de espacio y, principalmente, de tiempo se han quedado numerosos temas enel tintero que podrían resultar interesantes. A continuación se listan y describen, de manerasumamente breve, algunas sugerencias consideradas interesantes:

• Análisis de la plataforma Akka [http://akka.io] . Plataforma basada en Scala que ofreceun modelo de actores junto con Sofware Transactional Memory con el objetivo deproporcionar los fundamentos correctos para la construcción de aplicaciones escalablesy concurrentes.

Una comparación entre el modelo de actores ofrecido por Scala y el modelo de actoresofrecido por Akka podría resultar atractiva.

• Web funcional. Análisis de cómo las características de los lenguajes funcionales se puedenutilizar 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 entreambos lenguajes. Problemática habitual.

• Scala en .NET.

• Colecciones. Análisis del nuevo API de colecciones diseñado a partir de Scala 2.8. Análisisde colecciones concurrentes.

• GUI en Scala. Análisis de cómo podemos utilizar Scala para la construcción de aplicacionesde escritorio.

• Monads en Scala.

• Arquitectura del compilador de Scala.

• Lenguajes de dominio específicos (DSLs). Construcción de lenguajes de dominio específicosbasados en el lenguaje Scala. Combinator parsing.

Page 37: Scala Overview

33

Apéndice A. Modelo de objetos deScala

A continuación, a modo de resumen, se incluye un diagrama en el que se refleja la jerarquíade clases presentes en Scala:

Page 38: Scala Overview

34

Apéndice B. Producers

El siguiente código fuente contiene la definición del interfaz producer analizado en Ejemplocompleto de actores

B.1. Código fuente completo

package com.blogspot.miguelinlas3.phd.paragprog.actors

import scala.actors.Actorimport 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

Page 39: Scala Overview

35

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

Page 40: Scala Overview

36

} }

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

Page 41: Scala Overview

37

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


Top Related