Scala Overview

  • 2,567 views
Uploaded on

Un vistazo general e introductorio al lenguaje de programación Scala

Un vistazo general e introductorio al lenguaje de programación Scala

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
    Be the first to like this
No Downloads

Views

Total Views
2,567
On Slideshare
0
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
94
Comments
0
Likes
0

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Scala: Un lenguaje escalable para la JVM Miguel Pastor
  • 2. Scala: Un lenguaje escalable para la JVMMiguel Pastor 2
  • 3. Tabla de contenidos1.-¿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 52.-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 233.-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 294.-Conclusiones y trabajo futuro 31 4.1.-Conclusiones 31 4.2.-Líneas de trabajo 32A.-Modelo de objetos de Scala 33B.-Producers 34 B.1.-Código fuente completo 34Bibliografí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 que2http://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 de4Un 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?5Concretamente la fecha data del 19 de Abril de 19656http://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. ConstructoresPuesto 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étodosSi 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 + "/" + d2.4.4. PrecondicionesUna 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 10
  • 15. Fundamentos de Scalaestablecerse 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étodosDefinamos 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)} 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 22.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.2También podríamos escribir a.+(b) aunque en este caso el código resultante sería mucho menos legible 12
  • 17. Fundamentos de ScalaCuando 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-classScala 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 + 1Las 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. ClosuresLas 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 * otherLa 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. 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 ScalaEn 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 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 rectangularesLas 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 { 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. 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 modificationsHasta 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. 17
  • 22. Fundamentos de ScalaDada 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ñadido a 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.ArrayBufferabstract 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 MyQueuescala> val queue = new MyQueuequeue: MyQueue = MyQueue@91f017 18
  • 23. Fundamentos de Scalascala> queue.put(10)scala> queue.get()res12: Int = 20Para 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 = 1scala> queue.get()res16: Int = 2El 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 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ámetrosdel constructor 20
  • 25. Fundamentos de Scala2.8.2. Patrones: estructura y tiposLa 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 wildcardEl 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 constantesUn 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 variablesUn 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 { 21
  • 26. Fundamentos de Scala case 0 => "zero value" case somethingElse => "not zero: "+ somethingElse + " value"}2.8.2.4. Patrones constructoresSon 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 secuenciaPodemos 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 tipadosPodemos 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} 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.1Realmente Scala ofrece el modelo de actores mediante una librería del lenguaje en lugar de manera nativa 24
  • 29. Actors y concurrencia3.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 concurrenciascala> 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 Messagescala> echoActor ! 11Received message 113.3.1. Buenas prácticasLlegados 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 bloqueosUn 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 26
  • 31. Actors y concurrenciael 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 mensajesLa 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 inmutablesPuesto 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 autocontenidosCuando 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: 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 dispongamos3.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 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 iteratorNuestro 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 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 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. 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 futuro4.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 ScalaA continuación, a modo de resumen, se incluye un diagrama en el que se refleja la jerarquíade 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 actoresB.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íaScala 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