SlideShare una empresa de Scribd logo
Programación Funcional en Scala
Programación Funcional en Scala
UNIVERSIDAD DE MÁLAGA
ESCUELA TÉCNICA SUPERIOR DE INGENIERÍA
INFORMÁTICA
INGENIERO EN INFORMÁTICA
PROGRAMACIÓN FUNCIONAL EN SCALA
(FUNCTIONAL PROGRAMMING IN SCALA)
Realizado por
RUBÉN PÉREZ LUJANO
Dirigido por
JOSÉ ENRIQUE GALLARDO RUIZ
Departamento
LENGUAJES Y CIENCIAS DE LA COMPUTACIÓN
MÁLAGA, SEPTIEMBRE 2016
Programación Funcional en Scala
Dedicado a
la memoria de mi padre
Programación Funcional en Scala
Agradecimientos
Después de recorrer un largo camino ha llegado el momento de parar, coger aire, mirar hacia
atrás un instante y dar las gracias a todas esas personas que en algún momento han formado
parte del mismo, que han compartido los mejores momentos, que me han ayudado a mirar hacia
delante a pesar de las adversidades y junto a las que me gustaría continuar recorriendo el camino
de la vida.
Quisiera comenzar dando las gracias a D. José Enrique Gallardo Ruiz, tutor del proyecto,
por su comprensión, dedicación y por el gran esfuerzo realizado, así como a D. Blas Carlos
Ruiz Jiménez, un gran profesor y la persona con la que comencé mi proyecto.
Gracias a Irene, mi mujer, por saber sacarme una sonrisa en esos días grises, por tenderme la
mano y ayudar a levantarme en los días más oscuros y por darme ánimos para continuar cuando
más duro se hacía el camino. Gracias por hacerme el hombre más feliz del mundo. Gracias por
apostar por mí. Tú siempre has sido y serás mi apuesta.
Gracias a Carmen, mi madre, por los valores que me ha transmitido, por todo lo que me ha
enseñado durante la vida y por creer en mí hasta el final. Gracias por esa canción inolvidable.
Gracias a Lidia, mi hermana, por su cariño, su ayuda, sus bromas y por confiar ciegamente
en mí. Gracias por todos y cada uno de los momentos que hemos vivido.
Gracias a mis titos, Juan y Paqui, por ofrecer siempre todo su apoyo y demostrarme que
siempre podré contar con ellos.
Gracias a mis amigos Nacho y Jesús, con los que he compartido algunos de los mejores
momentos de este camino. Gracias por vuestros consejos, por esas tardes de risas en el salón.
Gracias a Dña. Lidia Fuentes por ofrecerme esa beca en el momento que más lo necesitaba
y a Dña. Mariam Cobaleda por todos los buenos consejos que me dio.
Gracias a todos los miembros de los centros de día para personas mayores de Estepona y
Coín por acogerme en vuestra familia y por todos los buenos momentos que compartimos.
Para finalizar, aunque no los mencione de una forma explícita, quiero dar las gracias a mis
compañeros de universidad y a mis compañeros de piso.
A todos, eternamente agradecido.
Programación Funcional en Scala
Introducción
La elección de un lenguaje para introducir la programación a los alumnos de las actuales
ingenierías en informática es una decisión trascendental; esta elección está ligada a la pregunta:
¿qué características se deben exigir a un “primer” lenguaje para describir de forma limpia y
sencilla los conceptos de la programación?
Hoy en día es comúnmente aceptado entre los profesionales de la enseñanza (y entre los alum-
nos) que hay dos paradigmas esenciales que simplifican los conceptos de la programación, y
que debe conocer un futuro informático: el funcional y el orientado a objetos. Sin embargo es
difícil encontrar un lenguaje que integre ambos paradigmas de forma sencilla si se parte de la
base de que tal lenguaje será el primer contacto de un estudiante con la programación. Además
de esto, se quiere una buena elección desde el punto de vista del programador profesional; es
decir, tal lenguaje debe facilitar de forma “natural” el aprendizaje de los principales lenguajes
con los que se enfrentará el futuro informático profesional.
Entre la oferta actual de lenguajes hay uno que cada vez toma más adeptos en el mundo edu-
cativo: el lenguaje Scala. Scala es un lenguaje de programación multiparadigma diseñado para
expresar patrones comunes de programación en forma concisa, elegante y con tipos seguros.
Integra sutilmente características de lenguajes funcionales y orientados a objetos. La imple-
mentación actual corre en la máquina virtual de Java y es compatible con las aplicaciones Java
existentes; por ello el uso de Scala como un primer lenguaje será un puente importante con el
mundo de la programación profesional.
El trabajo en Scala surge a partir de un esfuerzo de investigación para desarrollar un mejor
soporte de los lenguajes de programación para la composición de software. Hay dos hipótesis
que se desea validar con el experimento Scala. Primera, se postula que un lenguaje de progra-
mación para la composición de software necesita ser escalable en el sentido de que los mismos
conceptos pueden describir tanto partes pequeñas como grandes. Por tanto, los autores se han
concentrado en los mecanismos para la abstracción, composición y descomposición en vez de
añadir un conjunto grande de primitivas que pueden ser útiles para los componentes a algún
nivel de escala, pero no a otro nivel. Segundo, se postula, que el soporte escalable para los
componentes puede ser previsto por un lenguaje de programación que unifica y generaliza la
programación orientada a objetos y la funcional. Para los lenguajes con tipos estáticos, de los
que Scala es un ejemplo, estos dos paradigmas estaban hasta ahora en gran medida separados.
El principal objetivo de este PFC es desarrollar un material didáctico a modo de una guía
donde el programador (tanto el alumno como el profesor) pueda ver de forma clara y conci-
sa, tras una primera toma de contacto con el lenguaje, las ventajas de utilizar un lenguaje de
programación multiparadigma como Scala en la resolución de los diferentes problemas que se
puedan plantear.
En el Capítulo 1: Scala « página 1 » se realiza una breve introducción a Scala, se presenta
el lenguaje y se analizan conceptos básicos de los lenguajes de programación como los tipos de
datos básicos, los operadores, las estructuras de control o la evaluación en Scala.
Página I
II
En el Capítulo 2: Programación Orientada a Objetos en Scala « página 31 » se realiza un
análisis de Scala como un lenguaje orientado a objetos puro, se presenta la jerarquía de clases
y conceptos como polimorfismo, genericidad, acotación de tipos y varianza en Scala.
En las primeras secciones del Capítulo 3: Programación Funcional en Scala « página 53 »
se describe el paradigma funcional utilizando Scala y se enseñan conceptos de la programación
a través del estilo funcional. Posteriormente, se detalla la implementación en Scala de las es-
tructuras básicas de la programación, aprovechando dichas estructuras para el aprendizaje de
la programación funcional. Para finalizar el capítulo se hace un repaso por las colecciones que
Scala ofrece al programador.
Es conocido que es posible unificar los estilos imperativos y orientado a objetos con el fun-
cional puro a través de mónadas, pero el uso de éstas no es apropiado para un curso introductorio
a la programación. Después de haber estudiado previamente los aspectos fundamentales de la
programación funcional, en el Capítulo 4: Programación Funcional Avanzada en Scala « página
129 » se analizan conceptos más complejos de este paradigma, como la programación monádica
a través de las mónadas más populares del lenguaje.
En el Capítulo 5: Tests en Scala « página 155 » se presentan brevemente algunas de las
soluciones más populares para realizar pruebas al código realizado en Scala.
Scala propone una solución basada en el paso asíncrono de mensajes inmutables para resol-
ver la problemática de la concurrencia. El modelo de actores de Scala, así como la biblioteca
Akka, se presentan en el Capítulo 6: Concurrencia en Scala. Modelo de actores « página 165 ».
En el Capítulo 7: Conclusiones « página 185 » se justifica razonadamente el uso de Scala
como lenguaje de programación adecuado para ser utilizado dentro del ámbito de la docencia.
Finalmente, en el Capítulo 8: Solución a los ejercicios propuestos « página 189 » se pueden
encontrar las soluciones de los ejercicios propuestos a lo largo de la guía.
Índice general
1. Scala 1
1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.1. Scala. Un lenguaje escalable . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.2. Paradigmas de la programación . . . . . . . . . . . . . . . . . . . . . 2
1.1.2.1. Scala. Un lenguaje multiparadigma . . . . . . . . . . . . . . 3
1.1.3. Preparación del sistema . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1.3.1. Descargar Scala . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1.3.2. Herramientas de Scala . . . . . . . . . . . . . . . . . . . . . 3
El compilador de Scala: scalac . . . . . . . . . . . . . . . . . . 3
El intérprete de código: scala . . . . . . . . . . . . . . . . . . . 4
Scala como lenguaje compilado . . . . . . . . . . . . . . . . . 4
Scala como lenguaje interpretado desde un script . . . . . . . . 5
Scala como lenguaje interpretado desde un intérprete . . . . . . 5
1.2. Conceptos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2.1. Elementos de un lenguaje de programación . . . . . . . . . . . . . . . 6
1.2.2. Elementos básicos en Scala . . . . . . . . . . . . . . . . . . . . . . . 6
1.2.2.1. Tipos de datos básicos en Scala . . . . . . . . . . . . . . . . 6
1.2.2.2. Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Operadores de igualdad. . . . . . . . . . . . . . . . . . . . . . 15
1.2.2.3. Nombrar expresiones . . . . . . . . . . . . . . . . . . . . . 15
1.2.2.4. Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.2.3. Uso del carácter punto y coma (;) en Scala . . . . . . . . . . . . . . . . 17
1.3. Bloques en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.3.1. Visibilidad y bloques en Scala . . . . . . . . . . . . . . . . . . . . . . 18
1.4. Evaluación en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.1. Evaluación de expresiones . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.2. Evaluación de funciones . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.3. Sistema de Evaluación de Scala . . . . . . . . . . . . . . . . . . . . . 19
1.4.3.1. Valores de las definiciones . . . . . . . . . . . . . . . . . . . 19
1.4.3.2. Evaluación de Booleanos . . . . . . . . . . . . . . . . . . . 19
1.4.4. Ámbito y visibilidad de las variables . . . . . . . . . . . . . . . . . . . 20
1.5. Estructuras de control en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.5.1. Estructuras condicionales . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.5.1.1. La sentencia if . . . . . . . . . . . . . . . . . . . . . . . . . 21
La sentencia if / else . . . . . . . . . . . . . . . . . . . . . . . 22
La sentencia if...else if ...else . . . . . . . . . . . . . . . . . 22
Estructuras condicionales anidadas . . . . . . . . . . . . . . . . 23
1.5.2. Estructuras iterativas . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Página III
IV ÍNDICE GENERAL
1.5.2.1. Bucles while . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.5.2.2. Bucles do...while . . . . . . . . . . . . . . . . . . . . . . . 24
1.5.2.3. Bucles for . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Bucles for con rangos . . . . . . . . . . . . . . . . . . . . . . . 25
Bucles for con colecciones . . . . . . . . . . . . . . . . . . . . 26
Bucles for con filtros . . . . . . . . . . . . . . . . . . . . . . . 27
Bucles for con yield. . . . . . . . . . . . . . . . . . . . . . . . 28
1.6. Interacción con Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.6.1. Ejecución sobre la JVM . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2. Programación Orientada a Objetos en Scala 31
2.1. Introducción a la programación orientada a objetos en Scala . . . . . . . . . . 31
2.1.1. Características principales de la programación orientada a objetos . . . 31
2.1.2. Scala como lenguaje orientado a objetos . . . . . . . . . . . . . . . . . 31
2.2. Paquetes, clases, objetos y namespaces . . . . . . . . . . . . . . . . . . . . . . 32
2.2.1. Objetos Singleton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.2.2. Módulos, objetos, paquetes y namespaces . . . . . . . . . . . . . . . . 32
2.2.3. Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.2.4. Objetos funcionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.2.4.1. Constructores . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.2.4.2. Sobrescritura de métodos . . . . . . . . . . . . . . . . . . . 35
2.2.4.3. Precondiciones . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.2.4.4. Atributos y Métodos . . . . . . . . . . . . . . . . . . . . . . 36
2.2.4.5. Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.3. Jerarquía de clases en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.3.1. Herencia en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.3.1.1. Rasgos y herencia múltiple en Scala . . . . . . . . . . . . . 39
2.3.1.2. Funcionamiento de los rasgos . . . . . . . . . . . . . . . . . 39
2.3.1.3. Rasgos como modificaciones apiladas . . . . . . . . . . . . 40
2.3.1.4. ¿Cuándo usar rasgos? . . . . . . . . . . . . . . . . . . . . . 42
2.4. Patrones y clases case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.4.1. Clases case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.4.2. Patrones: estructuras y tipos . . . . . . . . . . . . . . . . . . . . . . . 43
2.4.2.1. Patrones comodín . . . . . . . . . . . . . . . . . . . . . . . 43
2.4.2.2. Patrones constantes . . . . . . . . . . . . . . . . . . . . . . 43
2.4.2.3. Patrones variables . . . . . . . . . . . . . . . . . . . . . . . 44
2.4.2.4. Patrones constructores . . . . . . . . . . . . . . . . . . . . . 44
2.4.2.5. Patrones de secuencia . . . . . . . . . . . . . . . . . . . . . 44
2.4.2.6. Patrones tipados . . . . . . . . . . . . . . . . . . . . . . . . 45
2.5. Polimorfismo en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.5.1. Acotación de tipos y varianza . . . . . . . . . . . . . . . . . . . . . . 47
2.5.1.1. Acotación de tipos . . . . . . . . . . . . . . . . . . . . . . . 47
2.5.1.2. Varianza . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
ÍNDICE GENERAL V
3. Programación Funcional en Scala 53
3.1. Introducción a la programación funcional . . . . . . . . . . . . . . . . . . . . 53
3.1.1. Características de los Lenguajes de Programación Funcionales . . . . . 53
3.1.2. Scala como lenguaje funcional . . . . . . . . . . . . . . . . . . . . . . 54
3.1.3. ¿Por qué la programación funcional? . . . . . . . . . . . . . . . . . . 54
3.2. Sentido estricto y amplio de la programación funcional . . . . . . . . . . . . . 54
3.2.1. ¿Qué son las funciones puras? . . . . . . . . . . . . . . . . . . . . . . 55
3.3. Funciones y cierres en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.3.1. Definición de funciones . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.3.2. Funciones anidadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.3.3. Diferencias entre métodos y funciones . . . . . . . . . . . . . . . . . . 57
3.3.4. Funciones de primera clase . . . . . . . . . . . . . . . . . . . . . . . . 58
3.3.5. Funciones anónimas y funciones valor . . . . . . . . . . . . . . . . . . 58
3.3.6. Cierres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
3.4. Recursión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
3.4.1. Importancia de la pila del sistema en recursión. . . . . . . . . . . . . . 60
3.4.1.1. La pila de Java . . . . . . . . . . . . . . . . . . . . . . . . . 61
3.4.1.2. Contexto de pila . . . . . . . . . . . . . . . . . . . . . . . . 61
3.4.2. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.5. Currificación y Parcialización . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.5.1. Currificacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.5.2. Parcialización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
3.6. Orden Superior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.6.1. Funciones de orden superior . . . . . . . . . . . . . . . . . . . . . . . 65
3.7. Funciones polimórficas. Genericidad . . . . . . . . . . . . . . . . . . . . . . . 66
3.8. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.8.1. Ejercicio Resuelto . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.8.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
3.9. Programación funcional estricta y perezosa . . . . . . . . . . . . . . . . . . . 76
3.9.1. Funciones estrictas y no estrictas . . . . . . . . . . . . . . . . . . . . . 76
3.10. Estructuras de Datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
3.10.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
3.10.1.1. ¿Qué es una teoría?. Definición de Estructuras de Datos . . . 78
3.10.1.2. La abstracción en la programación . . . . . . . . . . . . . . 78
3.10.1.3. Datos, Tipos de Datos, Estructuras de Datos y Tipos Abstrac-
tos de Datos . . . . . . . . . . . . . . . . . . . . . . . . . . 79
3.10.2. Definición de Estructuras de Datos en Lenguajes Funcionales . . . . . 80
3.10.2.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.10.2.2. Definición . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
3.10.2.3. Los Naturales . . . . . . . . . . . . . . . . . . . . . . . . . 82
Ejercicios resueltos. . . . . . . . . . . . . . . . . . . . . . . . 86
3.10.3. Estructuras de datos lineales. Listas . . . . . . . . . . . . . . . . . . . 89
3.10.3.1. TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
3.10.3.2. Ejercicios sobre el TAD Lista . . . . . . . . . . . . . . . . . 94
3.10.4. Estructuras de datos no lineales . . . . . . . . . . . . . . . . . . . . . 95
3.10.4.1. Árboles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
3.10.4.2. Arboles Binarios . . . . . . . . . . . . . . . . . . . . . . . . 97
3.10.4.3. Arboles Binarios de Búsqueda . . . . . . . . . . . . . . . . 99
VI ÍNDICE GENERAL
3.10.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
3.11. Colecciones en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
3.11.1. El paquete scala.collection . . . . . . . . . . . . . . . . . . . . . . . . 103
3.11.2. Iteradores en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
3.11.2.1. Métodos definidos para el tipo Iterator en Scala. . . . . . . . 105
3.11.3. Colecciones inmutables . . . . . . . . . . . . . . . . . . . . . . . . . 107
3.11.3.1. Definición de rangos en Scala. La clase Range. . . . . . . . . 107
Métodos definidos para el tipo Range en Scala. . . . . . . . . . 108
3.11.3.2. Definición de tuplas en Scala. La clase Tuple . . . . . . . . . 108
3.11.3.3. Listas en Scala. La clase List . . . . . . . . . . . . . . . . . 110
Métodos definidos para el tipo List en Scala. . . . . . . . . . . 112
Ejercicios sobre listas . . . . . . . . . . . . . . . . . . . . . . . 112
3.11.3.4. Vectores en Scala. La clase Vector . . . . . . . . . . . . . . 114
3.11.3.5. Flujos en Scala. La clase Stream . . . . . . . . . . . . . . . 115
3.11.3.6. Conjuntos en Scala. La clase Set . . . . . . . . . . . . . . . 116
Recorriendo conjuntos . . . . . . . . . . . . . . . . . . . . . . 117
Métodos definidos para el tipo Set en Scala. . . . . . . . . . . . 118
3.11.3.7. Asociaciones en Scala. La clase Map . . . . . . . . . . . . . 119
Métodos definidos para el tipo Map en Scala. . . . . . . . . . . 120
3.11.3.8. Selección de una colección . . . . . . . . . . . . . . . . . . 120
3.11.3.9. Colecciones como funciones . . . . . . . . . . . . . . . . . 121
3.11.4. Expresiones for como una combinación elegante de funciones de orden
superior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
3.11.4.1. Traducción de expresiones for con un generador . . . . . . . 122
3.11.4.2. Traducción de expresiones for con un generador y un filtro . 123
3.11.4.3. Traducción de expresiones for con dos generadores . . . . . 123
3.11.4.4. Traducción de bucles for . . . . . . . . . . . . . . . . . . . 124
3.11.4.5. Definición de map, flatMap y filter con expresiones for . . . 125
3.11.4.6. Uso generalizado de for en estructuras de datos . . . . . . . 125
3.11.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
4. Programación Funcional Avanzada en Scala 129
4.1. Implícitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
4.1.1. Parámetros ímplicitos en funciones . . . . . . . . . . . . . . . . . . . 129
4.1.2. Clases implícitas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
4.2. Tipos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
4.2.1. Definición de tipos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
4.2.2. Parámetros de tipo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
4.2.2.1. Nombres de los parámetros de tipo. . . . . . . . . . . . . . . 132
4.2.3. Constructores de tipos. . . . . . . . . . . . . . . . . . . . . . . . . . . 133
4.2.4. Tipos compuestos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
4.2.5. Tipos estructurales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
4.2.6. Tipos de orden superior. . . . . . . . . . . . . . . . . . . . . . . . . . 134
4.2.7. Tipos existenciales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
4.3. Teoría de categorías . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
4.3.1. El patrón funcional Funtor . . . . . . . . . . . . . . . . . . . . . . . . 137
4.3.2. El patrón funcional Mónada . . . . . . . . . . . . . . . . . . . . . . . 138
4.3.2.1. Reglas que deben satisfacer las mónadas . . . . . . . . . . . 139
ÍNDICE GENERAL VII
4.3.2.2. Importancia de las propiedades de las mónadas en las expre-
siones for . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
4.3.2.3. Map en las mónadas . . . . . . . . . . . . . . . . . . . . . . 141
4.3.2.4. La importancia de las mónadas . . . . . . . . . . . . . . . . 142
4.3.2.5. La mónada Identidad . . . . . . . . . . . . . . . . . . . . . 142
4.3.2.6. Envolviendo el contexto con mónadas. La clase monádica Try 143
La mónada Try. . . . . . . . . . . . . . . . . . . . . . . . . . . 146
4.4. Manejo de errores sin usar excepciones . . . . . . . . . . . . . . . . . . . . . 149
4.4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
4.4.2. Tipo de datos Option . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
4.4.3. Tipo de datos Either . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
4.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
5. Tests en Scala 155
5.1. Afirmaciones Asserts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
5.1.1. Assert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
5.1.2. Ensuring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
5.2. Herramientas específicas para tests . . . . . . . . . . . . . . . . . . . . . . . . 159
5.2.1. ScalaTest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
5.3. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
6. Concurrencia en Scala. Modelo de actores 165
6.1. Programación Concurrente. Problemática . . . . . . . . . . . . . . . . . . . . 165
6.1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
6.1.1.1. Sistema Reactivo Vs Sistema Transformacional . . . . . . . 165
6.1.2. Speed-Up en programación concurrente . . . . . . . . . . . . . . . . . 166
6.1.3. Problemática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
6.1.3.1. Propiedades de los programas concurrentes . . . . . . . . . . 166
6.1.3.2. Bloqueos y secciones críticas . . . . . . . . . . . . . . . . . 167
Problemas del uso de bloqueos . . . . . . . . . . . . . . . . . . 167
6.1.3.3. Concurrencia en Java . . . . . . . . . . . . . . . . . . . . . 167
6.2. Modelo de actores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
6.2.1. Origen del Modelo de Actores . . . . . . . . . . . . . . . . . . . . . . 168
6.2.2. Filosofía del Modelo de Actores . . . . . . . . . . . . . . . . . . . . . 169
6.3. Actores en Scala. Librería scala.actors . . . . . . . . . . . . . . . . . . . . . . 170
6.3.1. Definición de actores . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
6.3.2. Estado de los actores . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
6.3.3. Mejora del rendimiento con react . . . . . . . . . . . . . . . . . . . . 173
6.4. Actores en Scala con Akka . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
6.4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
6.4.1.1. Diferencias entre Akka y la librería Actors de Scala. . . . . . 175
6.4.2. Definición y estado de los actores . . . . . . . . . . . . . . . . . . . . 177
6.5. Buenas prácticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
6.5.1. Ausencia de bloqueos . . . . . . . . . . . . . . . . . . . . . . . . . . 182
6.5.2. Comunicación exclusiva mediante mensajes . . . . . . . . . . . . . . . 182
6.5.3. Mensajes inmutables . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
6.5.4. Mensajes autocontenidos . . . . . . . . . . . . . . . . . . . . . . . . . 182
6.6. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
VIII ÍNDICE GENERAL
7. Conclusiones 185
8. Solución a los ejercicios propuestos 189
8.1. Evaluación en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
8.2. Introducción a la Programación Funcional . . . . . . . . . . . . . . . . . . . . 189
8.3. Estructuras de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
8.3.1. TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
8.3.2. TAD Arbol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
8.4. Colecciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
8.4.1. Tipo List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
8.4.2. Otras colecciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
8.5. Programación Funcional Avanzada . . . . . . . . . . . . . . . . . . . . . . . . 204
8.6. Tests en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
8.7. Concurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
Lista de tablas 210
Listados de algoritmos 211
Referencias 219
Glosario 221
Acrónimos 225
Capítulo 1
Scala
1.1. Introducción
El nombre Scala significa: diseñado para crecer con la demanda de sus usuarios (Scala-
ble language). Scala es un lenguaje de programación multi-paradigma diseñado para expresar
patrones comunes de programación de forma concisa, elegante y con tipos estáticos. Integra
elegantemente características de lenguajes funcionales y orientados a objetos, lo cual hace que
la escalabilidad sea una de las principales características del lenguaje. La implementación ac-
tual corre en la máquina virtual de Java y es compatible con las aplicaciones existentes en Java.
[7]
Su creador, Martin Odersky1
, y su equipo comenzaron el desarrollo de este nuevo lengua-
je de código abierto en el año 2001, en el laboratorio de métodos de programación en École
Polytechnique Fédérale de Lausanne (EPFL).
Scala hizo su aparición pública sobre la plataforma Máquina Virtual de Java (JVM) en enero
de 2004 y unos meses después haría 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 y las ventajas que ofrece han hecho ya
que cada vez más empresas apuesten por Scala ( Twitter, Linked-in, Foursquare, The Guardian,
...). Las características de Scala le hacen un lenguaje ideal para ser utilizado en centros de
computación, centros de hosting,..., en los que las aplicaciones se ejecutan de forma parale-
la en los supercomputadores, servidores,...que los conforman. Además, Scala es uno de los
lenguajes de programación que se utilizan para desarrollar aplicaciones para los dispositivos
Android.
“ Si le das a una persona un pescado, comerá un día. Si enseñas a pescar a una
persona, comerá toda su vida. Si das herramientas a una persona, puede construirse
1
Martin Odersky es profesor de la EPFL en Lausanne, Suiza y ha trabajado en los lenguajes de programación
durante la mayor parte de su carrera. Estudió programación estructurada y orientada a objetos como estudiante
de doctorado de Niklaus Wirth. Después, mientras trabajaba en IBM y en la Universidad de Yale se enamoró de
la programación funcional. Cuando apareció Java, comenzó a añadir construcciones de programación funcionales
para la nueva plataforma, lo cual dio lugar a Pizza y GJ y eventualmente a Java 5 con los genéricos. Durante ese
tiempo también desarrolló javac, el compilador de referencia actual para Java.
En los últimos 10 años, Martin ha centrado su trabajo en la unificación de los paradigmas de programación
funcional y programación orientada a objetos en el lenguaje Scala. Scala pasó rápidamente del laboratorio de
investigación a convertirse en una herramienta de código abierto y en un lenguaje industrial. Odersky ahora es el
encargado de supervisar el desarrollo de Scala como jefe del grupo de programación de la EPFL y como presidente
de la empresa Typesafe.
Página 1
una caña de pescar, ¡y muchas otras herramientas!. Incluso podrá construir una
máquina para fabricar más cañas y así podrá ayudar a otras personas a pescar.
Ahora debemos conceptualizar el diseño de un lenguaje de programación co-
mo un patrón para diseñar lenguajes de programación, como una herramienta para
hacer más herramientas del mismo tipo.
El diseño de un lenguaje de programación debe ser un patrón, un patrón de
crecimiento, un patrón para desarrollar el patrón para la definición de patrones que
los programadores puedan usar en su trabajo y para alcanzar su objetivo.” [1]
1.1.1. Scala. Un lenguaje 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 las exigencias 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 abstracción de componen-
tes, su sintaxis reducida, el soporte para la programación orientada a objetos y la programación
funcional han contribuido a que el lenguaje sea más escalable y haga de la escalabilidad una de
sus principales características.
1.1.2. Paradigmas de la programación
Dentro de la programación se pueden distinguir tres paradigmas:
Programación imperativa.
Programación lógica.
Programación funcional.
La programación imperativa pura está limitada por “el cuello de botella de Von Neuman”,
término que fue acuñado por John Backus en su conferencia de la concesión del Premio Turing
por la Asociación para la Maquinaria Computacional (ACM) en 1977. Según Backus:
“Seguramente debe haber una manera menos primitiva de realizar grandes cam-
bios en la memoria, que empujando tantas palabras hacia un lado y otro del cuello
de botella de Von Neumann. No sólo es un cuello de botella para el tráfico de datos,
sino que, más importante, es un cuello de botella intelectual que nos ha mantenido
atados al pensamiento de “una palabra a la vez” en lugar de fomentarnos el pensar
en unidades conceptuales mayores. Entonces la programación es básicamente la
planificación del enorme tráfico de palabras que cruzan el cuello de botella de Von
Neumann, y gran parte de ese tráfico no concierne a los propios datos, sino a dónde
encontrar éstos”
En la actualidad, la programación funcional y la programación orientada a objetos (POO)2
se preocupan mucho menos de “empujar un gran número de palabras hacia un lado y otro” que
otros lenguajes anteriores (como por ejemplo Fortran), pero internamente, esto sigue siendo
2
El paradigma de la POO se considera ortogonal a los paradigmas de programación funcional, programación
imperativa y programación lógica.
Página 2
lo que hacen durante gran parte del tiempo los computadores, incluso los supercomputadores
altamente paralelos3
.[26]
Del cuello de botella intelectual criticado por Backus subyace la necesidad de disponer de
otras técnicas para definir abstracciones de alto nivel como son los conjuntos, los polinomios,
las formas geométricas, las cadenas de caracteres, los documentos,...para lo que idealmente se
deberían de desarrollar teorías de conjuntos, formas, cadenas de caracteres, etc.
1.1.2.1. Scala. Un lenguaje multiparadigma
Scala ha sido el primero en incorporar y unificar la programación funcional y la programa-
ción orientada a objetos en un lenguaje estáticamente tipado, donde la clase de cada instancia
se conoce en tiempo de compilación, así como la disponibilidad de cualquier método de una
instancia dada. La pregunta es: ¿por qué se necesita 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.3. Preparación del sistema
1.1.3.1. Descargar Scala
Para disponer de la última versión de Scala sólo habrá que acceder a la sección “Download”
en la web oficial: http://www.scala-lang.org/download/
Para trabajar con Scala, sólo será necesario un editor de texto y un terminal.
También es posible trabajar con un Entorno de Desarrollo Integrado (IDE), ya que existen
plugins para Eclipse, IntelliJ IDEA o Netbeans.4
1.1.3.2. Herramientas de Scala
Las herramientas de línea de comandos de Scala se encuentran dentro de la carpeta bin de
la instalación.
El compilador de Scala: scalac
El compilador scalac transforma un programa de Scala en archivos .class que podrán ser
utilizados por la JVM. El nombre del archivo no tiene que corresponder con el nombre de la
clase.
Sintaxis: scalac [opciones ...] [archivos fuente ...]
Opciones:
La opción -classpath (-cp) indica al compilador donde encontrar los archivos fuente
La opción -d indica al compilador donde colocar los archivos .class
La opción -target indica al compilador qué versión de máquina virtual usar
3
Un estudio de referencia de base de datos, realizado a partir de 1996, encontró que tres de cada cuatro ciclos
de la unidad central de procesamiento (CPU) se dedican a la espera de memoria.
4
En los próximos capítulos se usará tanto el intérprete de Scala como el IDE Eclipse junto con el plugin
ScalaIDE disponible en la web http://scala-ide.org/.
Página 3
El intérprete de código: scala
La instrucción scala puede operar en tres modos:
Puede ejecutar clases compiladas
Puede ejecutar archivos fuente (scripts – Secuencia de instrucciones almacenadas en un
fichero que se ejecutan normalmente de forma interpretada. –)
Puede operar en forma interactiva, es decir como intérprete
Sintaxis: scala [opciones ...] [script u objeto] [argumentos]
Si no se especifica un script o un objeto, la herramienta funciona como un evaluador de
expresiones interactivo. En cambio, si se especifica un script, el comando scala lo compilará y
lo ejecutará. Si se especifica el nombre de una clase, scala la ejecuta.
Algunos comandos importantes:
:help muestra mensaje de ayuda
:quit termina la ejecución del intérprete
:load carga comandos de un archivo
Scala como lenguaje compilado
Como primer ejemplo, se puede observar la definición del programa estándar “Hola mun-
do”.
1 object HolaMundo {
2 def main(args: Array[String]) {
3 println("Hola, mundo!")
4 }
5 }
Algoritmo 1.1: Hola Mundo
La estructura de este programa debería resultar familiar para los lectores que ya conozcan Java.
Consiste de un método llamado main que toma los argumentos de la línea de comandos (un
vector, array en inglés, de objetos del tipo String) como parámetro. El cuerpo de este método
presenta una sola llamada al método predefinido println con el saludo como argumento. El
método main no devuelve un valor, por lo tanto no es necesario que se declare un tipo retorno.
Lo que es menos familiar es la declaración de objetos que contienen al método main. Esta
declaración introduce lo que es comúnmente conocido como singleton objects (objeto single-
ton), que es una clase con una sola instancia. Por lo tanto, dicha construcción declara tanto una
clase llamada HolaMundo, como una instancia de esa clase también llamada HolaMundo. Esta
instancia es creada bajo demanda, es decir, la primera vez que es utilizada.
Se puede advertir que el método main no está declarado como estático. Esto es así porque
los miembros estáticos (métodos o campos) no existen en Scala. En vez de definir miembros
estáticos, en Scala se declararán estos miembros en un objeto singleton.
Compilando el ejemplo
Para compilar el ejemplo utilizaremos scalac, el compilador de Scala. El comando scalac
funciona como la mayoría de los compiladores: toma un archivo fuente como argumento, al-
gunas opciones y produce uno o varios archivos objeto. Los archivos objeto que produce son
archivos class para la JVM.
Página 4
Si guardamos el programa anterior en un archivo llamado HolaMundo.scala, podemos com-
pilarlo ejecutando el siguiente comando:
$ scalac HolaMundo.scala
Esto generará algunos archivos class en el directorio actual. Uno de ellos se llamará Hola-
Mundo.class y contiene una clase que puede ser directamente ejecutada utilizando el comando
scala, como mostramos en la siguiente sección.
Ejecutando el ejemplo
Una vez compilado, un programa Scala puede ser ejecutado utilizando el comando scala. Su
uso es muy similar al comando java utilizado para ejecutar programas Java, y acepta las mismas
opciones. El ejemplo de arriba puede ser ejecutado utilizando el siguiente comando que, como
se puede comprobar, produce la salida esperada:
$ scala HolaMundo
Hola, mundo!
Scala como lenguaje interpretado desde un script
Como ya se ha introducido anteriormente, es posible ejecutar archivos fuente haciendo uso
del comando scala. A continuación se muestra como crear un script básico llamado hola.scala:
1 println("Hola mundo, desde un script!")
Ejecutando el ejemplo desde la línea de comandos se comprueba que el resultado obtenido
es el esperado:
$>scala hola.scala
Hola mundo, desde un script!
Desde la línea de comandos se le pueden pasar argumentos a los scripts mediante el vector
de argumentos args. En Scala, el acceso a los elementos de un vector se realiza especificando
el índice entre paréntesis (no entre corchetes como en Java)5
. Se ha definido el siguiente script,
llamado holaarg.scala, con el objetivo de probar el funcionamiento del paso de argumentos
desde la línea de comandos:
1 println("Hola, "+ args(0) +"!")
Si se ejecuta:
$> scala holaarg.scala pepe
En este comando, “pepe” se pasa como argumento desde la línea de comandos, el cual se
accede desde el script mediante args(0). La salida obtenida es la que se esperaba:
Hola, pepe!
Scala como lenguaje interpretado desde un intérprete
Para lanzar el intérprete de Scala, se tiene que abrir una ventana de terminal y teclear scala.
Aparecerá el prompt del intérprete de Scala a la espera de recibir expresiones. Funciona, al igual
que el intérprete de Scheme o Haskell, mediante el Bucle Leer-Evaluar-Imprimir (REPL).
El intérprete de Scala será muy utilizado durante el capítulo dedicado a la programación
funcional, por lo que se recomienda familiarizarse con el mismo.
5
Notación que es homogénea para las demás estructuras, incluso las estructuras definidas por el usuario.
Página 5
1.2. Conceptos básicos
1.2.1. Elementos de un lenguaje de programación
Un lenguaje de programación debe proveer:
expresiones primitivas que representen los elementos más simples
maneras de combinar expresiones
maneras de abstraer expresiones, lo cual introducirá un nombre para esta expresión y nos
permitirá hacer referencia a la expresión.
1.2.2. Elementos básicos en Scala
1.2.2.1. Tipos de datos básicos en Scala
En Scala se pueden encontrar los mismos tipos de datos básicos que en Java. El tamaño que
ocupa cada uno de ellos en memoria, así como la precisión de los tipos primitivos de Scala,
también se corresponden con los de Java. Aunque se hable de tipos de datos, en Scala todos los
tipos de datos son clases. En la tabla 1.1 se muestran los tipos básicos en Scala.
Tipo de dato Tamaño Rango Ejemplo
Byte 8 bits con signo [−128, 127] 38
Short 16 bits con signo [−32768, 32767] 23
Int 32 bits con signo [−231
, 231
− 1] 45
Long 64 bits con signo [−263
, 263
− 1] 3434115
Float 32 bits con signo [−3,4028 ∗ 1038
, 3,4028 ∗ 1038
1.38
Double 64 bits con signo [−1,7977 ∗ 10308
, 1,7977 ∗ 10308
] 54.37
Boolean true o false true
Char 16 bits con signo [0, 216
− 1] ’F’
String secuencia de caracteres Cadena de caracteres "hola mundo!"
Tabla 1.1: Tipos de datos primitivos y tamaño en Scala
Los tipos Byte, Short, Int y Long reciben el nombre de tipos enteros. Los tipos enteros junto
con los tipos Float y Double son llamados tipos numéricos.
Excepto String, que es del paquete java.lang, el resto se encuentran en el paquete Scala.
Todos se importan automáticamente.
Literales de tipos básicos en Scala
Las reglas sobre literales que usa Scala son bastante simples e intuitivas. A continuación se
verán las principales características de los literales básicos en Scala.
Literales enteros
Los mayoría de literales enteros que utilicemos serán de tipo Int. Los literales enteros
también podrán ser de tipo Long añadiendo el sufijo L o l al final del literal. A continuación se
muestran algunos ejemplos:
Página 6
scala> 5
res0: Int = 5
scala> 777L
res3: Long = 777
scala> 0xFFAFAFA5
res2: Int = -5263451
scala> 0777L
<console>:1: error: Non-zero integral values may not have a leading zero.
0777L
^
Los literales enteros no podrán tener el cero como primer dígito6
, excepto el entero 0 y
aquellos representados en notación hexadecimal. Los enteros representados en notación hexa-
decimal comienzan por 0x o 0X, pudiendo estar seguido por dígitos entre 0 y 9 o letras entre A
y F (en mayúscula o minúscula).
Literales en punto flotante
Los literales en punto flotante serán del tipo de datos Float cuando se añada el sufijo F o
f. En otro caso serán de tipo Double. Estos literales sí podrán contener uno o varios ceros como
primeros dígitos.
scala> 0.0
res0: Double = 0.0
scala> 01.2
res1: Double = 1.2
scala> 01.2F
res2: Float = 1.2
scala> 00045.34
res3: Double = 45.34
Literales lógicos
Los literales lógicos o booleanos son aquellos que pertenecen a la clase Boolean y que sólo
pueden tener uno de los dos valores booleanos: true o false.
Literales de tipo símbolo
Aunque el tipo de datos Symbol (scala.Symbol) no se considera un tipo básico de Scala,
merece la pena tenerlo en cuenta. Los símbolos serán cadenas de caracteres no vacías precedidas
del prefijo ’. La clase case Symbol está definida de la siguiente forma:
1 package scala
2 final case class Symbol private (name: String) {
3 override def toString: String = "’" + name
4 }
Ejemplo:
6
En versiones anteriores de Scala, cuando un entero comenzaba por cero era porque el número estaba en base
8 y, por tanto, sólo podía estar seguido de los dígitos comprendidos entre 0 y 7.
Página 7
scala> ’miSimbolo
res4: Symbol = ’miSimbolo
Literales de tipo carácter
Un literal de tipo carácter consiste en un carácter encerrado entre comillas simples que
representará un carácter representable o un carácter de escape7
. El tipo de datos de los literales
de tipo carácter es Char. El estándar de codificación de caracteres utilizado para representar los
caracteres es Unicode. También podremos indicar el código unicode del carácter que queramos
representar encerrado entre comillas simples. Veamos algunos ejemplos:
scala> ’u0050’
res5: Char = P
scala> ’S’
res6: Char = S
scala> ’n’
res7: Char =
Literales de tipo cadena de caracteres
Un literal del tipo cadena de caracteres será una secuencia de caracteres encerradas entre
comillas dobles cuyo tipo de datos será String. Los caracteres que conformen la cadena de
caracteres podrán ser tanto caracteres representables, como caracteres de escape. Por ejemplo:
scala> "Hola Mundo!"
res8: String = Hola Mundo!
scala> "Hola " Mundo!"
res9: String = Hola " Mundo!
Los literales de tipo cadena de caracteres también pueden ser multilínea en cuyo ca-
so estarán encerrados entre tres comillas dobles. En este caso, la cadena de caracteres podrá
estar formada por los caracteres representables, caracteres de escape o por caracteres no repre-
sentables como salto de línea o cualquier otro carácter especial como comillas dobles, barra
inversa,...con la única salvedad de que sólo podrá haber tres o más comillas dobles al final.
Veamos algún ejemplo:
scala> """y dijo:
| "mi nombre es Bond, James Bond"
| ///"""
res10: String =
y dijo:
"mi nombre es Bond, James Bond"
///
Caracteres de escape
Los caracteres de escape que se muestran en la tabla 1.2 son reconocidos tanto en los
literales de caracteres como en los literales de cadenas de caracteres.
7
También conocidos como secuencia de escape
Página 8
Carácter de escape Unicode Descripción
b u0008 Retroceso BS
t u0009 Tabulador horizontal HT
n u000A Salto de línea LF
f u000C Salto de página FF
r u000D Retorno de carro CR
" u0022 Comillas dobles
’ u0027 Comilla simple
 u005c Barra inversa
Tabla 1.2: Caracteres de escape reconocidos por Char y String
En versiones anteriores de Scala era posible definir cualquier carácter que tuviera un código
unicode entre 0 y 255, indicando el código en base octal. Esto se indicaba precediendo al código
octal de . Por ejemplo 150 representaría el carácter h. Esta representación está depreciada en
la actualidad, siendo sustituida por la representación en base hexadecimal de los caracteres.
scala> "150"
<console>:1: warning: Octal escape literals are deprecated, use u0068 instead.
"150"
^
res11: String = h
scala> "u0068"
res12: String = h
Expresiones de tipos básicos en Scala
Las expresiones en Scala pueden estar formadas por:
Un literal válido de cada uno de los tipo de datos básicos vistos en la tabla 1.1. Por
ejemplo: 1, 2.5, 3.74E10f, true, false, ’a’...
Un operador 8
junto con dos operandos (operadores binarios) o un operando (operadores
unarios) del tipo de datos compatible con el operador.
El último valor evaluado en un bloque (en la llamada a una función o método).
Expresiones aritméticas en Scala
Las expresiones aritméticas son aquellas que, tras ser evaluadas, devuelven un valor nu-
mérico. Los tipos de datos Byte, Short, Int y Long serán utilizados para representar valores
numéricos de tipo entero. Con los tipos de datos Float y Double se representarán valores de
tipo real.
El tipo de datos devuelto por defecto al evaluar una expresión que represente un número
entero es Int mientras que Double será el tipo de datos por defecto devuelto al evaluar una
expresión que represente un número real.
scala> 1.2
res0: Double = 1.2
scala> 5
res1: Int = 5
8
Los operadores se verán en la Subsubsección 1.2.2.2: Operadores « página 10 »
Página 9
Expresiones booleanas
Una expresión booleana puede estar compuesta por un literal, por uno de los operadores
lógicos mostrados en la tabla 1.6 o ser el valor devuelto por las operaciones usuales de com-
paración que se muestran en la tabla 1.5 de operadores relacionales en Scala. Una expresión
booleana también puede ser el resultado de la evaluación de cualquier expresión booleana co-
mo, por ejemplo, el resultado de una función o método.
1.2.2.2. Operadores
Los operadores se utilizarán para combinar expresiones de los tipos de datos básicos. En
Scala los operadores son en realidad métodos y, por tanto, se reducen a la llamada a un método
de un objeto de una de clases de los tipos de datos básicos. Es decir, 1 + 2 realmente invoca
al método + del objeto 1 con el parámetro 2: (1).+(2). Es más, en la clase Int hay diferentes
definiciones del método + (método sobrecargado) que difieren las unas de las otras en el tipo
del parámetro con el que se invocan y que nos permiten realizar la suma de objetos de diferentes
tipos enteros. Por ejemplo, existe otro método + en la clase Int que recibe como parámetro un
objeto de tipo Long y devuelve otro objeto de tipo Long.
Los operadores se pueden clasificar, atendiendo a su notación en:
Operadores infijos. Son operadores binarios en los que el método que se va a invocar
se ubica entre sus dos operandos. Un ejemplo de operador infijo podría ser el operador
aritmético + de los tipos numéricos.
Operadores prefijos. Son operadores unarios en los que el nombre del método se sitúa
delante del objeto que invocará al método. Como por ejemplo: !b, -5,...
Operadores postfijos9
. Son aquellos operadores unarios en los que el nombre del método
se sitúa detrás del objeto que invocará al método. Por ejemplo: 5 abs, 2345 toLong.
Infijos, Prefijos y Postfijos
En Scala los operadores no tienen una sintaxis especial10
, como ocurre en otros lenguajes
de programación como Haskell, por lo que cualquier método puede ser un operador. Lo que
convertirá un método en un operador será la forma de usar el mismo. Por ejemplo, si se escribe
1.+(2), + no será un operador. Pero si se escribe 1 + 2, + sí será un operador.
Existe la posibilidad de definir nuevos operadores prefijos definiendo métodos que comien-
cen por unary_ y estén seguidos por un identificador válido. Por ejemplo, si en un tipo de datos
se define un método unary_!, una instancia de este tipo de datos podrá invocar ! en notación
prefija. Scala transforma las llamadas a operadores prefijos en llamadas al método unary_. Por
ejemplo, la expresión !p es transformada por Scala en la invocación de p.unary_!.
Los operadores postfijos son métodos que no reciben parámetros y, por convenio, no es
necesario el uso de paréntesis11
9
Los operadores postfijos tienen que ser habilitados -visibles- importando scala.language.postfixOps o indican-
do al compilador la opción -language:postfixOps
10
Excepto los operadores prefijos en los que los identificadores que pueden ser usados en este tipo de operadores
son +, -, ! y ~.
11
Excepto si el método presenta efectos colaterales, como println(), en cuyo caso sí habrá que poner los parén-
tesis.
Página 10
Los operadores infijos son aquellos métodos que reciben un argumento. Los operadores
infijos se ubican entre sus dos operandos (como el operador +, en el que el primer operando
será el objeto que invoca al método y el segundo operando es el argumento que recibe).
Prioridad y Asociatividad de los operadores
Cuando en una expresión aparecen varios operadores, la prioridad de los operadores nos
indicará el orden de evaluación de las diferentes partes que componen la expresión, es decir, qué
partes de la expresión son evaluadas antes. Por ejemplo, el resultado de evaluar la expresión 100
- 40 * 2 es 20, no 120, ya que el operador * tiene mayor prioridad que el operador +. El resultado
de la anterior expresión es el mismo que si se evalúa la expresión 100 - (40 * 2). Si se quisiera
cambiar el orden de evaluación anterior se debería escribir la expresión: (100 - 40) * 2, cuyo
resultado sería 120.
Scala determina la prioridad de los operadores basándose en el primer carácter de los méto-
dos usados con notación de operador. La excepción a esta regla es el operador de menor priori-
dad en Scala: el operador de asignación (los operadores que terminan con el carácter ’=’). Así,
con la excepción ya comentada de los operadores de asignación, la precedencia de operadores
se determinará según el primer carácter, tal como se muestra en la tabla 1.3, donde encontramos
la prioridad de los operadores básicos, de forma que los operadores de mayor prioridad se en-
cuentran en la parte superior de la tabla y los operadores de menor prioridad en la parte inferior.
La prioridad de cualquier operador definido por el usuario se definirá por el primer carácter
empleando según esta misma tabla.
Tipo Operador Asociatividad
Postfijos (), [],... Izquierda
Unarios ! ˆ Derecha
Multiplicativos * / % Izquierda
Aditivos + - Izquierda
Binario : Izquierda
Binario = ! Izquierda
Desplazamiento >> >>> << Izquierda
Relación <><= >= Izquierda
Igualdad == != Izquierda
Bit a bit AND & Izquierda
Bit a bit XOR ˆ Izquierda
Bit a bit OR | Izquierda
AND lógico && Izquierda
OR lógico || Izquierda
Todas las letras Izquierda
Asignación = += -= *= /= %= >>= <<= &= ˆ= |= Derecha
Coma , Izquierda
Tabla 1.3: Prioridad y asociatividad de los operadores
Cuando se encuentran en una expresión varios operadores con la misma prioridad, será
otra de las características de los operadores, la asociatividad, la encargada de indicar cómo
se agrupan los operadores y, por tanto, como se evaluará la expresión. La asociatividad de un
operador en Scala se determina por el último carácter del operador. Si el último carácter de
Página 11
un operador binario es :, el operador tendrá asociatividad derecha, lo que quiere decir que
los métodos que terminen en : serán invocados por el operando situado en el lado derecho del
operador y se les pasará como parámetro el operando izquierdo. Es decir, si se tiene la expresión
x /: y, será equivalente a y./:(x). En cualquier otro caso, el operador presentará asociatividad
izquierda.
La asociatividad de un operador no influye en el orden de evaluación de los operandos,
que siempre será de izquierda a derecha. En la anterior expresión, x/:y, primero se evaluará el
operando x y después el operando y. Es decir, la expresión es tratada como el siguiente bloque:
1 {val t=x;y./:(x)}
Como se ha dicho anteriormente, la asociatividad determinará como se agrupan los opera-
dores en caso de que aparezcan en una expresión varios operadores con la misma prioridad. Si
los métodos terminan en ’:’, se agruparán de derecha a izquierda, mientras que en cualquier otro
caso se agruparán de izquierda a derecha. Si se tienen las expresiones a * b * c y x /: y /: z, se
evaluarán como (a * b) * c y x /: (y /: z), respectivamente. A pesar de conocer las reglas que
determinan la prioridad de los operadores y la asociatividad de los mismos, es aconsejable usar
paréntesis para aclarar cuales son los operadores que actúan sobre cada una de las expresiones.
Operadores aritméticos
Los operadores aritméticos son aquellos que operan con expresiones enteras o reales, es
decir, con objetos de tipo Short, Int, Double o Long. En Scala se pueden distinguir dos tipos de
operadores aritméticos: unarios y binarios. En la tabla 1.4 se muestran los diferentes operadores
aritméticos presentes en Scala.
Descripción Operador Tipo
Signo positivo + unario
Signo negativo - unario
Suma + binario
Resta - binario
División / binario
Producto * binario
Resto % binario
Tabla 1.4: Operadores aritméticos en Scala
Todos los operadores admiten expresiones enteras y reales. En el caso de los operadores
binarios, si los dos operandos son enteros o reales el resultado de la operación será un valor
entero o real respectivamente. Si uno de sus operandos es entero y el otro real entonces el
resultado devuelto será de tipo real.
Los operadores aritméticos también servirán para combinar expresiones de tipo carácter. En
este caso, Scala tomará el valor decimal del código unicode que represente el carácter de cada
uno de los operadores.
scala> 1.5 * 3
res2: Double = 4.5
scala> 5 * 3
res3: Int = 15
Página 12
Operadores relacionales
El uso de operadores relacionales permite comparar expresiones de tipos de datos com-
patibles, devolviendo un resultado de tipo booleano: true o false. Scala soporta los operadores
relacionales que se muestran en la tabla 1.5. Los operadores relacionales son binarios.
Descripción Operador
Menor <
Menor/Igual <=
Mayor >
Mayor/Igual >=
Distinto !=
Igual ==
Tabla 1.5: Operadores relacionales en Scala
Como se puede apreciar en el siguiente ejemplo, los resultados obtenidos después de com-
parar diferentes expresiones numéricas (reales o enteras) son los esperados:
scala> 12.0 * 3 == 3 * 12
res4: Boolean = true
scala> 3.0f < 7
res5: Boolean = true
scala> "cadena" == "cadena"
res6: Boolean = true
En cambio, si lo que pretende es comparar dos expresiones booleanas se deberá tener en
cuenta que el valor false se considera menor que el valor true.
scala> 13<10 < (11==11.0)
res7: Boolean = true
Cuando se comparen expresiones del tipo de datos Char o String se deberá tener en cuenta
que se basan en el valor decimal del código unicode de cada carácter12
. En el caso de expresiones
del tipo de datos String, cuando los caracteres situados en la primera posición de las cadenas que
se estén comparando sean iguales (mismo código unicode) se comparará el segundo carácter de
ambas cadenas y así sucesivamente hasta que se encuentre el primer par de caracteres distintos,
ubicados en la misma posición en ambas cadenas, o hasta que la cadena cuya longitud sea
menor se termine, en cuyo caso la cadena de menor longitud será menor que la cadena de mayor
longitud. A continuación se muestran unos ejemplos en el intérprete de Scala que pueden ayudar
a aclarar estos conceptos:
scala> "hola mundo" < "hola mundo scala!"
res8: Boolean = true
scala> ’a’<’b’
res9: Boolean = true
scala> "hola"<"hola"
res10: Boolean = false
scala> "hola"<="hola"
res11: Boolean = true
scala> "hola" <= "Hola"
res12: Boolean = false
12
En Scala, una expresión de tipo String y otra de tipo Char no se pueden comparar.
Página 13
En otros lenguajes de programación como Java, el operador relacional == se utiliza pa-
ra comparar tipos primitivos (comparando si los valores son iguales, como en Scala) y tipos
referenciados (comparando igualdad referencial). En Scala se puede comparar la igualdad refe-
rencial de tipos referenciados usando eq y neq.
Operadores lógicos
Los operadores lógicos permitirán combinar expresiones lógicas. Las expresiones lógicas
son todas aquellas expresiones que tras ser evaluadas se obtiene: verdadero o falso.
Scala soporta los operadores lógicos que se muestran en la tabla 1.6.
Descripción Operador Tipo
AND lógico && binario
OR lógico || binario
NOT lógico ! unario
Tabla 1.6: Operadores lógicos en Scala
Operadores bit a bit.
Los operadores bit a bit trabajan sobre cadenas de bits aplicando el operador en cada uno
de los bits de los operadores. Las tablas de verdad para los operadores &, | y ˆ son las que se
muestran en la tabla 1.7
p q p& q p ˆ q p | q
0 0 0 0 0
0 1 0 1 1
1 0 1 1 0
1 1 0 1 1
Tabla 1.7: Tabla de verdad de los operadores bit a bit &, | y ˆ .
Los operadores bit a bit soportados por Scala se muestran en la tabla 1.8.
Operador Descripción Ejemplo
& AND binario a & b
| OR binario a | b
ˆ XOR binario 45
˜ Complemento a uno (intercambio de bits) ˜a
<< Desplazamiento binario a la izquierda a <<2
>> Desplazamiento binario a la derecho a >>2
>>> Desplazamiento a la derecha con relleno de ceros a >>>2
Tabla 1.8: Operadores bit a bit.
Página 14
Operadores de igualdad.
Scala soporta los operadores de asignación mostrados en la tabla 1.9.
Operador Descripción Ejemplo
= Operador de asignación simple C = A + B
+= Suma y asignación A += B equivalente a A = A + B
-= Resta y asignación A -= B equivalente a A = A - B
*= Multiplicación y asignación A *= B equivalente a A = A * B
/= División y asignación A /= B equivalente a A = A / B
%= Módulo y asignación A %=B equivalente a A = A % B
<<= Desplazamiento a la izquierda y asignación A <<= 2 equivalente a A = A <<2
&= Bit a bit AND y asignación A &= 2 equivalente a A = A & 2
ˆ= Bit a bit XOR y asignación A ˆ= 2 equivalente a A = A ˆ 2
|= Bit a bit OR y asignación A |= 2 es equivalente a A = A | 2
Tabla 1.9: Operadores de asignación.
1.2.2.3. Nombrar expresiones
Es posible nombrar una expresión con la palabra reservada def y utilizar su nombre (identi-
ficador) en lugar de la expresión:
scala> def scale = 5
scale: Int
scala> 7 * scale
res4: Int = 35
scala> def pi = 3.141592653589793
pi: Double
scala> def radius = 10
radius: Int
scala> 2 * pi * radius
res5: Double = 62.83185307179586
def es una primitiva declarativa: le da un nombre a una expresión, pero no la evalúa.
scala> def r=8/0
r: Int
scala> r
java.lang.ArithmeticException: / by zero
at .r(<console>:7)
... 33 elided
Se puede observar que la definición de r no da error. El error se produce en el momento que
se evalúa por primera vez r.
Es lo contrario que la forma especial define de Scheme, que se utiliza para asociar nombres
con expresiones, y en primer lugar se evalúa la expresión. En realidad, nombrar una expresión
sólo aportará azúcar sintáctico, permitiendo crear funciones y evitando escribir la expresión
lambda asociada. Es decir, es lo mismo que crear una función en Scheme sin argumentos.
Alternativamente, se verá como Scala permite una definición de variables utilizando val o
var.
1.2.2.4. Variables
Las variables están encapsuladas en objetos, es decir, no pueden existir por si mismas. Las
variables son referencias a instancias de clases.
Página 15
Para definir una variable se requiere:
Definir su mutabilidad
Definir el identificador
Definir el tipo (opcional)
Definir un valor inicial
Sintaxis para definir una nueva variable:
<var | val> < identificador> : < Tipo> = < _ | Valor Inicial>
La nueva variable podrá ser un literal o el resultado de la evaluación de una expresión, una
función o el valor devuelto por un método. Pero también se podrán definir funciones, métodos,
bloques, etc.
Por ejemplo:
1 val x : Int = 1
Se usa la palabra reservada val para indicar que la referencia no puede reasignarse, mientras
que se utiliza var para indicar que sí puede reasignarse. La variable val es similar a una variable
final en Java: una vez inicializada, no se podrá reasignar. Una variable var, por el contrario, se
puede reasignar múltiples veces.
Todas las variables o referencias en Scala tienen un tipo, debido a que el lenguaje es estric-
tamente tipado. Sin embargo, los tipos pueden en muchos casos omitirse, porque el compilador
de Scala tiene inferencia de tipos. Por ejemplo, las siguientes definiciones son equivalentes:
scala> var x : Int = 1
scala> var x = 1
Otros ejemplos:
val msg = "Hola mundo!"
msg: java.lang-String = Hola mundo!
En este ejemplo se aprecia que Scala tiene inferencia de tipos, es decir, al haber inicializado
la variable msg con una cadena, Scala asocia el tipo de msg al tipo de datos String. Si se intenta
modificar el valor de msg, no se podrá y obteniéndose un error ya que se ha definido como val:
scala> msg = "Hasta luego!"
<console>:5: error: reassignment to val
msg = "Hasta luego!"
Si se imprime por pantalla el valor de msg:
scala> println(msg)
Hola mundo!
Si se quisiera reasignar el valor de una variable habría que utilizar la palabra reservada var:
scala> var saludo = "Hola mundo!"
saludo: java.lang.String = Hola mundo!
scala> saludo = "Hasta luego!"
saludo: java.lang.String = Hasta luego!
Aunque todas las declaraciones de variables podrían realizarse utilizando var, se recomienda
encarecidamente usar val cuando la variable no vaya a mutar.
Página 16
1.2.3. Uso del carácter punto y coma (;) en Scala
En Scala, el uso del punto y coma al final de una línea de programa es opcional en la
mayoría de las ocasiones, siendo recomendado omitir el mismo siempre y cuando su uso no sea
obligatorio. Se podría escribir:
1 def pi = 3.14159;
aunque muchos programadores omitirán el uso del (;) y simplemente escribirán:
1 def pi = 3.14159
¿Cuándo es obligatorio el uso del punto y coma?
El uso del punto y coma es obligatorio para separar instrucciones escritas en la misma línea.
1 def y = x - 1; y + y
Uso del punto y coma y operadores infijos en Scala.
Uno de los problemas derivados de omitir el uso del punto y coma en Scala es cómo escribir
expresiones que ocupen varias líneas. Si se escribiera:
Expresión larga
+ otra expresión larga
sería interpretado por Scala como dos expresiones. Si lo que se quiere es que se interprete como
una única expresión, se podría hacer de dos formas:
Se podría encerrar una expresión de varias líneas entre paréntesis, dando por hecho que
no se usará el punto y coma en éstas líneas:
(Expresión larga
+ otra expresión larga)
Se podría escribir el operador al final de la línea, indicándole así a Scala que la expresión
no está finalizada:
Expresión larga +
otra expresión larga
Por tanto, por norma general, los saltos de línea serán tratados como puntos y coma, salvo
que algunas de las siguientes condiciones sea cierta:
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
Página 17
1.3. Bloques en Scala
Un bloque en Scala estará encerrado entre llaves { ...}. Dentro de un bloque se podrán
encontrar definiciones o expresiones. El último elemento de un bloque será una expresión que
definirá el valor del bloque, la cual podrá estar precedida por definiciones auxiliares.
Los bloques también son expresiones en sí mismos, por lo que un bloque podrá aparecer en
cualquier lugar en el que pueda aparecer una expresión. Ejemplo:
1 { val x = f(3)
2 x * x
3 }
1.3.1. Visibilidad y bloques en Scala
Scala sigue las reglas de ámbito habituales en lenguajes como C o Java. Las definiciones
realizadas dentro de un bloque sólo serán visibles dentro del mismo y ocultan las definiciones
realizadas fuera del bloque que tengan el mismo nombre.
Las definiciones realizadas en bloques externos estarán visibles dentro de un bloque siempre
y cuando no sean ocultadas por otras definiciones con el mismo nombre en el bloque.
Ejemplo:
1 val x = 10
2 def f(y: Int)=y+1
3 val result = {
4 val x = f(3)
5 x * x
6 } + x
El resultado de la ejecución de este código será 26
1.4. Evaluación en Scala
1.4.1. Evaluación de expresiones
Para evaluar una expresión no primitiva:
1. Se toma el operador con mayor prioridad (ver la Sección 1.2.2.2: Prioridad y Asociati-
vidad de los operadores « página 11 »), o en caso de que todos los operadores tenga la
misma prioridad se toma el operador situado más a la izquierda,
2. Se evalúa sus operandos (de izquierda a derecha),
3. Se aplica el operador a los operandos.
Un nombre se evalúa reemplazando el mismo por su definición. El proceso de evaluación fina-
liza cuando se obtiene un valor.
Ejemplo:
(2 ∗ pi) ∗ radius
(2 ∗ 3,14159) ∗ radius
6,28318 ∗ radius
Página 18
6,28318 ∗ 10
62,8318
1.4.2. Evaluación de funciones
La evaluación de funciones parametrizadas es similar a la de operadores:
1. Se evalúan los parámetros de la función de izquierda a derecha,
2. Se reemplaza la llamada a la función por la definición de la misma y, al mismo tiempo,
3. Se reemplazan los parámetros formales de la función por el valor de sus argumentos.
Este sistema de evaluación de expresiones es llamado modelo de sustitución, cuya idea
gira en torno a que lo que hace una evaluación es reducir una expresión a su valor y puede ser
aplicado a todas las expresiones (siempre y cuando no tengan efectos laterales). El modelo de
sustitución está formalizado en el λ-cálculo. Esta estrategia de evaluación es conocida como
evaluación estricta o Call by Value.
Existe otra alternativa: no evaluar los argumentos antes de reemplazar la función por la
definición de la misma, llamada evaluación no estricta o Call by Name.
1.4.3. Sistema de Evaluación de Scala
Scala usa evaluación estricta normalmente, pero se puede indicar explícitamente que se
desea que algún argumento de una función use evaluación no estricta. Para esto último, se
pondrá => delante del tipo del parámetro que se desee evaluar siguiendo esta estrategia.
Ejemplo:
1 def miConst (x : Int, y: =>Int) = x
Algoritmo 1.2: Función con dos parámetros. El primero es evaluado por valor y el segundo por
nombre
1.4.3.1. Valores de las definiciones
Anteriormente se ha dicho que los parámetros de las funciones pueden ser pasados por valor
(evaluación estricta) o por nombre (evaluación no estricta).
La misma distinción se puede aplicar a las definiciones. La forma def es por nombre (eva-
luación no estricta) y la forma val es por valor (evaluación estricta).
1.4.3.2. Evaluación de Booleanos
En la tabla 1.10 aparecen representadas las reglas de reducción para expresiones booleanas.
Página 19
!true → false
!false → true
true && e → e
false && e → false
true || e → true
false || e → e
Tabla 1.10: Reglas de reducción para expresiones booleanas
En la tabla 1.10 se puede apreciar que && y || no siempre necesitan que el operando derecho
sea evaluado. En estos casos, se dirá que estas expresiones usan una evaluación en cortocir-
cuito.
1.4.4. Ámbito y visibilidad de las variables
El funcionamiento de los ámbitos en Scala es el siguiente:
Una invocación a una función crea un nuevo ámbito en el que se evalúa el cuerpo de la
función. Es el ámbito de evaluación de la función.
El ámbito de evaluación se crea dentro del ámbito en el que se definió la función a la que
se invoca.
Los argumentos de la función son variables no mutables locales de este nuevo ámbito que
quedan ligadas a los parámetros que se utilizan en la llamada.
En el nuevo ámbito se pueden definir variables locales.
En el nuevo ámbito se pueden obtener el valor de variables del ámbito padre.
Primer ejemplo:
Supongamos el siguiente código en Scala:
1 def f(x: Int, y: Int): Int = {
2 val z = 5
3 x+y+z
4 }
5 def g(z: Int): Int = {
6 val x = 10
7 z+x
8 }
9 f(g(3),g(5))
Se definen dos funciones f y g, dentro de cada una de las cuales hay declaradas distintas
variables locales. Las funciones f y g devuelven una suma de los parámetros con la variable
local. En la última línea de código se realiza una invocación a f con los resultados devueltos por
dos invocaciones a g .
¿Cuántos ámbitos locales se crean? ¿En qué orden?
1. En primer lugar se realizan las invocaciones a g . Cada una crea un ámbito local en el que
se evalúa la función. Las invocaciones devuelven 13 y 15, respectivamente.
Página 20
2. Después se realiza la invocación a f con esos valores 13 y 15. Esta invocación vuelve a
crear un ámbito local en el que se evalúa la expresión x+y+z , devolviendo 33.
Por ahora, todo es bastante normal. La diversión empezará cuando se construyan funciones
anónimas durante la ejecución de otras funciones, lo cual permitirá la definición de cierres.
1.5. Estructuras de control en Scala
Las estructuras de control permiten controlar el flujo de ejecución de las sentencias de un
programa, método, bloque, etc. Siguiendo el flujo natural, las sentencias que componen un pro-
grama se ejecutan secuencialmente una tras de otra según estén definidas13
(primero se ejecutará
la primera sentencia, después la segunda ..., y así hasta la última sentencia). Las sentencias de
control de flujo se emplean en los programas para ejecutar sentencias condicionalmente, repetir
un conjunto de sentencias o, en general, cambiar el flujo secuencial de ejecución. Las estructuras
de control se dividen en tres grandes categorías en función del flujo de ejecución:
Estructuras secuenciales
Estructuras condicionales
Estructuras iterativas
Hasta el momento se ha visto el flujo secuencial. Cada una de la sentencias que se utilizan
en Scala están separadas por el carácter punto y coma (véase el uso del carácter punto y coma
en Scala en la Subsección 1.2.3: Uso del carácter punto y coma (;) en Scala « página 17 »). El
uso de estructuras secuenciales puede ser suficiente para la resolución de programas sencillos,
pero para la resolución de programas de tipo general se necesitará controlar las sentencias que
se ejecutan haciendo uso de estructuras condicionales e iterativas.
1.5.1. Estructuras condicionales
Se utilizarán las estructuras condicionales para determinar si un bloque debe de ser ejecu-
tado o no, en función de una condición lógica o booleana.
1.5.1.1. La sentencia if
Una sentencia if consiste en una expresión booleana seguida de un bloque de expresiones.
Si la expresión booleana se evalúa a cierta, entonces se ejecutará el bloque de expresiones. En
caso contrario no se ejecutará el bloque de expresiones. La sintaxis de una sentencia if es:
1 if (expresion_booleana) {
2 // Sentencias que se ejecutaran si la
3 // expresion booleana se evalua a true
4 }
Algoritmo 1.3: Sintaxis sentencia if
Ejemplo:
13
Cuando se escribe un programa, se introduce la secuencia de sentencias dentro de un archivo. Sin sentencias
de control del flujo, el intérprete ejecuta las sentencias conforme aparecen en el programa de principio a fin.
Página 21
1 def mayorQueCinco(x: Int) = if (x > 5) { println(" El argumento
",x " es mayor que 5");}
Algoritmo 1.4: Expresión condicional if
La sentencia if / else
La sentencia if puede ir seguida de la una declaración else y un bloque de expresiones.
El bloque de expresiones que sigue a la declaración else se ejecutará en el caso de que la
expresión booleana del if sea falsa. Scala tiene la expresión condicional if-else para expresar la
elección entre dos alternativas (parecida a if-else de Java pero usada para expresiones, no para
instrucciones).
La sintaxis de una sentencia if / else es:
1 if (expresion_booleana) {
2 // Sentencias que se ejecutaran si la
3 // expresion booleana se evalua a true
4 } else {
5 // Sentencias que se ejecutaran si la
6 // expresion booleana se evalua a false
7 }
Algoritmo 1.5: Sintaxis sentencia if / else
Ejemplo:
1 def abs(x: Int) = if (x > 0) x else -x
Algoritmo 1.6: Expresiones condicionales. Función valor absoluto
La sentencia if...else if ...else
La expresión if puede ir seguida por una declaración else if, lo cual nos será de gran ayuda
para testear varias opciones haciendo uso de una única sentencia if.
Cuando se haga uso de la sentencia if...else if...else habrá que tener en cuenta algunos
puntos importantes:
Una sentencia if podrá tener cero o una declaración else, la cual estará declarada siempre
al final de una sentencia if.
Una sentencia if podrá tener cero o varias declaraciones else if, y siempre deberán prece-
der a la declaración else (si hubiera).
Si la evaluación de la expresión booleana de la sentencia if es true, ninguna de las otras
expresiones booleanas de las declaraciones else if serán evaluadas, y tampoco se ejecutará
el bloque de la declaración else.
En el algoritmo Algoritmo 1.7: Sintaxis sentencia if / else if / else « página 23 » se muestra
la sintaxis de una sentencia if / else if / else.
Página 22
1 if (expresion_booleana1) {
2 // Sentencias que se ejecutaran si la
3 // expresion booleana 1 se evalua a true
4 } else if (expresion_boolena2){
5 // Sentencias que se ejecutaran si la
6 // expresion booleana 2 se evalua a true
7 } else if (expresion_boolena3){
8 // Sentencias que se ejecutaran si la
9 // expresion booleana 3 se evalua a true
10 }
11 else {
12 // Sentencias que se ejecutaran si ninguna
13 // de las anteriores expresiones booleanas
14 // se evaluan a true
15 }
Algoritmo 1.7: Sintaxis sentencia if / else if / else
Estructuras condicionales anidadas
Scala permite que las sentencias if, if/else, if/else if/else se puedan anidar. Ejemplo:
1 var x = 30;
2 var y = 10;
3
4 if( x == 30 ){
5 if( y == 10 ){
6 println("X = 30 and Y = 10");
7 }
8 }
Algoritmo 1.8: Sentencias if anidadas
1.5.2. Estructuras iterativas
Hasta el momento se ha visto como el uso de la sentencia condicional permite dejar de
ejecutar algunas sentencias dispuestas en un programa, en función del resultado de la evaluación
de una expresión booleana. Pero el flujo del programa, en cualquier caso, siempre avanza hacia
adelante y nunca se vuelve a ejecutar una sentencia ejecutada anteriormente.
Las sentencias iterativas permitirán iterar un bloque de sentencias, es decir, ejecutar un
bloque de sentencias mientras la condición especificada sea cierta. A este tipo de sentencias se
les denomina bucles y al bloque de sentencias se les denominará cuerpo del bucle.
En la tabla 1.11 se puede observar los diferentes tipos de bucles que presenta Scala para dar
respuesta a las necesidades de iteración de los programadores.
Página 23
Bucle Descripción
while Repite una sentencia o un bloque de sentencias siempre que la condición
sea verdadera. Se evalúa la condición antes de ejecutar el cuerpo del bucle
do...while Igual que el bucle while pero la evaluación de la condición se produce
después de la ejecución del bucle.
for El cuerpo del bucle se ejecuta un número determinado de veces. Presenta
un sintaxis que abrevia el código que maneja la variable del bucle.
Tabla 1.11: Tipos de bucles en Scala
1.5.2.1. Bucles while
Un bucle while repetirá sucesivamente un bloque de sentencias mientras la condición da-
da sea cierta. Un bucle while tiene una condición de control o expresión lógica que ha de ir
encerrada entre paréntesis y es la encargada de controlar la secuencia de repetición.
El punto clave de los bucles while es el hecho de que la evaluación de la condición se realiza
antes de que se ejecute el cuerpo del bucle, por lo que si la condición se evalúa a falsa, el cuerpo
del bucle no se ejecutaría ninguna vez.
La sintaxis de un bucle while es:
1 while (condicion) {
2 // Sentencias que se ejecutaran mientras
3 // la condicion sea cierta
4 }
Algoritmo 1.9: Sintaxis bucles while
Hay que hacer notar que si la condición es cierta inicialmente, la sentencia while no termi-
nará nunca (bucle infinito) a menos que en el cuerpo de la misma se modifique de alguna forma
la condición de control del bucle.
Ejemplo:
1 def printUntil(x: Int) = {
2 //variable local
3 var s = 0;
4 while (s <= x){
5 println("Valor: " + s);
6 s += 1;
7 }
8 }
Algoritmo 1.10: Ejemplo de bucle while.
1.5.2.2. Bucles do...while
Al igual que los bucles while, los bucles do...while repetirán un bloque de sentencias hasta
que la condición de control del bucle sea falsa. Por tanto, al igual que en el bucle while, el cuerpo
del bucle se ejecuta mientras la expresión lógica sea cierta. Los bucles do...while también se
denominan post-prueba ya que, a diferencia de los bucles while, los bucles do...while evalúan
Página 24
la condición después de ejecutar el cuerpo del bucle, motivo por el cual este tipo de bucles
garantizan la ejecución del cuerpo del bucle al menos una vez.
La sintaxis de un bucle while es:
1 do {
2 // Sentencias que se ejecutaran mientras
3 // la condicion sea cierta
4 } while (condicion);
Algoritmo 1.11: Sintaxis bucles do... while.
Se puede observar que la expresión lógica o booleana aparece al final del bucle por lo que
el cuerpo del bucle se ejecutará al menos una vez. Si la condición se evalúa a cierta, el flujo
de control saltará hasta la declaración do y el cuerpo del bucle se ejecutará nuevamente. Este
proceso se repetirá hasta que la condición se evalúe a falsa.
Ejemplo:
1 def printUntil2(x: Int) = {
2 //variable local
3 var s = 0;
4 do {
5 println("Valor: " + s);
6 s += 1;
7 }while (s <= x)
8 }
Algoritmo 1.12: Ejemplo de bucle do... while.
1.5.2.3. Bucles for
Los bucles for son sentencias que nos permitirán escribir eficientemente bucles que ten-
gan que repetirse un número determinado de veces. En Scala podemos encontrar las siguientes
variantes de bucles for:
Bucles for con rangos
Bucles for con colecciones
Bucles for con filtros
A continuación se verán los conceptos básicos de este tipo de bucles aunque, la especificidad
de su implementación en Scala y su utilidad harán, al contrario de lo que se podría imaginar,
que este tipo de bucles sean también muy útiles en programación funcional, capítulo en el cual
se estudiarán con mayor profundidad (véase el Capítulo 3: Programación Funcional en Scala «
página 53 »).
Bucles for con rangos
La forma más fácil de definir bucles for es haciendo uso del tipo de datos Range definido
en Scala. El bucle se repetirá tantas veces como valores contenga el tipo de datos Range dado
(véase la Subsubsección 3.11.3.1: Definición de rangos en Scala. La clase Range « página 107
»).
La sintaxis más simple de bucles for con rangos es:
Página 25
1 for (a <- Range) {
2 // Sentencias que se ejecutaran tantas veces
3 // como valores contenga el tipo de datos
4 // Range dado
5 }
Algoritmo 1.13: Sintaxis bucles for con rangos.
Range puede ser un rango de números, normalmente representado de la forma i to j o i until j.
Más adelante veremos este tipo de datos en mayor profundidad.
El operador flecha izquierda (<-) recibe el nombre de generador, ya que es el encargado de
generar los diferentes valores del rango.
La variable de control de bucle (a) se inicializará con el primer valor del rango e irá
tomando, en cada iteración, los diferentes valores del mismo.
Ejemplo:
1 def printUntil3(x: Int) = {
2 //variable local
3 for (a <- 0 to x) {
4 println("Valor: " + a);
5 }
6 }
Algoritmo 1.14: Ejemplo de bucle for con rangos.
Dentro de los bucles for se pueden utilizar varios rangos, separando cada uno de ellos por el
carácter punto y coma (;). En este caso el bucle iterará sobre todas las posibles combinaciones
de los mismos. Ejemplo:
scala> for (x<- 1 to 3;y<- 4 to 6) {println("Valor x: "+x+" Valor y: "+y)}
Valor x: 1 Valor y: 4
Valor x: 1 Valor y: 5
Valor x: 1 Valor y: 6
Valor x: 2 Valor y: 4
Valor x: 2 Valor y: 5
Valor x: 2 Valor y: 6
Valor x: 3 Valor y: 4
Valor x: 3 Valor y: 5
Valor x: 3 Valor y: 6
Bucles for con colecciones
Los bucles for sirven para recorrer fácilmente los elementos de una colección.
La sintaxis de los bucles for con colecciones es:
1 for (a <- Collection) {
2 // Sentencias que se ejecutaran tantas veces
3 // como valores contenga el tipo de datos
4 // Collection dado
5 }
Algoritmo 1.15: Sintaxis bucles for con colecciones
Se puede iterar sobre los elementos de las distintas estructuras de datos definidas en la
librería Scala.collection de Scala como listas, conjuntos,etc., así como sobre los tipos de datos
Página 26
creados por el usuario14
La variable de control de bucle (a) se inicializará con el primer valor de la colección e
irá tomando el valor de los distintos elementos que componen la colección en las sucesivas
iteraciones del bucle.
Ejemplo:
1 def forLista = {
2 val numList = List(0,1,2,3,4,5,6,7,8,9,10);
3 for( a <- numList ){
4 println( "Valor de a: " + a );
5 }
6 }
Algoritmo 1.16: Ejemplo bucle for con colecciones.
En el ejemplo del algoritmo 1.16 se puede apreciar un bucle for recorriendo una colección
de números. Se ha creado esta colección usando el tipo de datos List de Scala. Las colecciones
en Scala se estudiarán en mayor profundidad en la Sección 3.11: Colecciones en Scala « página
102 ».
Bucles for con filtros
Los bucles for en Scala permiten emplear filtros para descartar la iteración sobre algunos
elementos de la colección que se desea iterar (rangos, listas, conjuntos,...) que no cumplan con
alguna propiedad. Para filtrar elementos se empleará una o más sentencias if.
La sintaxis de los bucles for con filtros es:
1 for (a <- Collection|Range...
2 if condicion1; if condicion2...) {
3 // Sentencias que se ejecutaran tantas veces
4 // como valores del tipo de datos
5 // Collection|Range dado satisfagan los filtros
6 }
Algoritmo 1.17: Sintaxis bucles for con filtros.
La variable de control de bucle (a) se inicializará con el primer valor de la colección/rango
que satisfaga las condiciones: condicion1 y condicion2, e irá tomando el valor de los distintos
elementos que componen la colección y que también satisfagan las condiciones impuestas como
filtros.
1 def forListaconFiltros = {
2 val numList = List(0,1,2,3,4,5,6,7,8,9,10);
3 for( a <- numList
4 if a <= 5;
5 if a != 3 ){
6 println( "Valor de a: " + a );
7 }
8 }
Algoritmo 1.18: Ejemplo bucle for con filtros
14
Estos tipos de datos creados por el usuario deberán presentar unas características especiales que serán estudia-
das con mayor detalle.
Página 27
En el ejemplo del algoritmo 1.18 se recorren los elementos de la lista numList que sean
menores o iguales a 5 y distintos de 3.
Bucles for con yield.
Los bucles for en Scala permiten almacenar los resultados de un bucle for en una variable o
que éstos sean el valor devuelto por una función. Para hacer esto se añadirá la palabra reservada
yield al final del cuerpo del bucle.
La sintaxis general de los bucles for con yield es:
1 for (a <- Collection|Range...
2 if condicion1; if condicion2...) {
3 // Sentencias que se ejecutaran tantas veces
4 // como valores del tipo de datos
5 // Collection dado satisfagan los filtros
6 }yield a
Algoritmo 1.19: Sintaxis bucles for con yield.
Yield generará un valor en cada iteración del bucle que será recordado por el bucle for15
.
Cuando el bucle finalice, devolverá una colección del mismo tipo de datos que la colección que
estamos iterando con los valores recordados. En los bucles for con yield se podrá también hacer
uso de filtros, haciendo de estos una herramienta mucho más potente.
En el algoritmo 1.20 podemos ver un ejemplo del uso de bucles for con yield y filtros.
1 def forListaconYield = {
2 val numList = List(0,1,2,3,4,5,6,7,8,9,10);
3 val lista= for( a <- numList
4 if a <= 5;
5 if a != 3 )yield a * 2
6 for (b<-lista){println ("Valor recordado por yield: "+b)}
7 }
Algoritmo 1.20: Ejemplo bucle for con yield
En el algoritmo 1.20 se utilizan dos bucles for. El primero de ellos generará una lista con el
doble de los números de la lista numList que satisfagan los filtros del bucle. El segundo bucle
for iterará sobre la lista resultante del primer bucle, imprimiendo los valores por pantalla.
1.6. Interacción con Java
Una de las características más importantes de Scala es que hace muy fácil la interacción
con el código escrito en Java. Todas las clases de la librería java.lang son importadas por de-
fecto, mientras que las otras necesitan ser importadas explícitamente. Scala hace muy fácil la
interactuación con código Java.
Como se verá a lo largo de los próximos capítulos, Scala es un lenguaje de programación
que ha sabido integrar algunas de las principales características presentes en los lenguajes de
programación más populares. Java no es la excepción, y comparte muchas cosas con éste. La
diferencia que se puede ver es que para cada uno de los conceptos de Java, Scala los aumenta,
15
Como si el bucle for tuviera un buffer que no se puede ver y al que en cada iteración se le añade un elemento
Página 28
refina y mejora. Poder aprender todas las características de Scala nos equipa con más y mejores
herramientas a la hora de escribir nuestros programas.
También es posible heredar de clases Java e implementar interfaces Java directamente en
Scala.
A continuación se presenta un ejemplo que demuestra esto. Se quiere obtener y formatear
la fecha actual de acuerdo a convenciones utilizadas en un país específico, por ejemplo Francia.
Las librerías de clases de Java definen clases de utilidades interesantes, como Date y Da-
teFormat. Ya que Scala interacciona fácilmente con Java, no es necesario implementar estas
clases equivalentes en las librerías de Scala, se pueden simplemente importar las clases de los
correspondientes paquetes de Java:
1 import java.util.{Date, Locale}
2 import java.text.DateFormat._
3 object FrenchDate {
4 def main(args: Array[String]) {
5 val ahora = new Date
6 val df = getDateInstance(LONG, Locale.FRANCE)
7 println(df format ahora)
8 }
9 }
Algoritmo 1.21: Fecha actual formateada.
Las declaraciones de importación de Scala parecen muy similares a las de Java, sin embargo,
las primeras son bastante más potentes. Se pueden importar múltiples clases desde el mismo
paquete al encerrarlas entre llaves, como se muestra en la primer linea. Otra diferencia es que
se pueden importar todos los nombres de un paquete o clase, utilizando el carácter guión bajo
(_) en lugar del asterisco (*). Eso es porque el asterisco es un identificador válido en Scala (por
ejemplo se puede nombrar a un método).
Por tanto, la declaración import en la segunda línea, importa todos los miembros de la clase
DateFormat. Esto hace que el método estático getDateInstance y el campo estático LONG sean
directamente visibles.
Dentro del método main, primero se crea una instancia de la clase Date que por defecto con-
tiene la fecha actual. A continuación se define un formateador de fechas, utilizando el método
estático getDateInstance que ha sido importado previamente. Finalmente, se imprime la fecha
actual formateada de acuerdo a la instancia de DateFormat que fue “localizada”. Esta última
línea muestra un ejemplo de un método que toma un solo argumento y que ha sido utilizado
como operador infijo (véase la Subsubsección 1.2.2.2: Operadores « página 10 »). Es decir, la
expresión:
df format ahora
es solamente otra manera más corta de escribir la expresión:
df.format(ahora)
1.6.1. Ejecución sobre la JVM
Una de las características más relevantes 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 de Java y
Página 29
su ecosistema (herramientas, librerías, IDE,...), 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.
El hecho de ser un lenguaje basado en la JVM también es la consecuencia por la que al-
gunos tipos de la JVM (como vectores) acaban siendo poco elegantes en Scala o que ciertas
características, como la recursividad de cola, no están implementadas en la JVM y hay que
simularlas.
1.7. Ejercicios
Ejercicio 1. Supongamos el siguiente código en Scala:
1 val x = 10
2 val y = 20
3 def g(y: Int): Int = {
4 x+y
5 }
6 def prueba(z: Int): Int = {
7 val x = 0
8 g(x+y+z)
9 }
10 prueba(3)
¿Qué devuelve prueba(3)? ¿En qué ámbito se evalúa la expresión x+y+z ? ¿Y la expresión
x+y ? ¿Qué valores tienen esas variables en el momento de la evaluación?
Página 30
Capítulo 2
Programación Orientada a Objetos en
Scala
2.1. Introducción a la programación orientada a objetos en
Scala
2.1.1. Características principales de la programación orientada a objetos
La popularidad de lenguajes como Java, C# o Ruby han hecho que la POO sea un paradigma
ampliamente aceptado entre la mayoría de desarrolladores. Aunque existen numerosos lengua-
jes 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 el envío de mensajes a objetos.
Todos los tipos definidos por el usuario son objetos.
2.1.2. Scala como lenguaje orientado a 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)1
.
De manera adicional a todas las características puras de un lenguaje orientado a objetos
presentadas anteriormente, Scala añade algunas innovaciones en el espacio de los lenguajes
orientados a objetos:
1
http://en.wikipedia.org/wiki/Smalltalk
Página 31
Composición modular de los elementos mezclados (mixin). Mecanismo que permite
la composición de clases para el diseño de componentes reutilizables, evitando los pro-
blemas 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 rasgos mezclados no dependen de ningún método y/o atributo de aque-
llas 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-type.
Abstracción de tipos. Existen dos mecanismos principales de abstracción en los lengua-
jes de programación: la parametrización y los miembros abstractos. Scala soporta ambos
estilos de abstracción de manera uniforme para tipos y valores.
2.2. Paquetes, clases, objetos y namespaces
2.2.1. Objetos Singleton
Scala no soporta la definición de atributos estáticos en las clases, incorporando en su lugar
el concepto de objeto singleton. 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 com-
panion object (objeto acompañante), mientras que la clase se denomina companion class (clase
acompañante) del objeto singleton. Inicialmente, sobre todo aquellos desarrolladores provenien-
tes 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 objetos singleton y las clases es que los pri-
meros no aceptan parámetros (no podemos instanciar un objeto singleton mediante la palabra
reservada new), mientras que las clases sí lo permiten. Cada uno de los objetos singleton es
implementado mediante una instancia de una clase synthetic 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.2.2. Módulos, objetos, paquetes y namespaces
Si se quiere hacer referencia a un método declarado dentro de un objeto (object) se tendrá
que hacer una llamada del tipo nombreObjeto.nombreMétodo(parámetros) ya que el método
nombreMétodo habrá sido definido dentro del objeto nombreObjeto.
En realidad, en Scala todos los valores serán objetos. El principal objetivo de un objeto es
el de otorgar un namespace a sus miembros, algunas veces llamado módulo.
Un objeto puede constar de cero o más miembros. Un miembro puede ser un método decla-
rado con la palabra reservada def, o puede ser otro objeto declarado con las palabras reservadas
val o object2
Para hacer referencia a los miembros de un objeto en Scala se utilizará la notación típica de
la POO (se indicará el namespace del objeto seguido de un puto y del nombre del miembro).
Ejemplo: MyModule.abs(42)
2
Los objetos también pueden contener otros tipos de objetos, que no se mencionarán en este capítulo.
Página 32
Si se observa la expresión 3 * 2, lo que se está haciendo realmente es llamar al miembro
(método) * del objeto 3, es decir (3.*(2)). En general, los métodos que toman un solo argumento
pueden ser usados con una sintaxis de infijo3
. De este modo, se puede comprobar como la
llamada a MyModule abs 42 devolvería la misma salida que la llamada MyModule.abs(42).
Es posible importar miembros de un namespace haciendo uso de la palabra reservada import.
Ejemplo: import MyModule.abs
Para importar todos los miembros de un namespace se usará el guión bajo. Ejemplo: import
MyModule._4
En Scala, al igual que en Java, se puede utilizar la palabra reservada package, la cual creará
un paquete (en realidad se crea un namespace). Por tanto, se puede crear un namespace tanto
con object como con package pero si se utiliza package no se creará un objeto, por lo que,
obviamente, no se podrá pasar como tal en otras llamadas. Además en un package no podrá
haber definiciones de miembros usando las palabras reservadas val o def.
2.2.3. Clases
Del mismo modo que en todos los lenguajes orientados a objetos, Scala permite la defini-
ción de clases en las que se pueden añadir métodos y atributos. A continuación se muestra la
definición de la clase MiPrimeraClase.
1 class MiPrimeraClase{
2 val a = 1
3 }
Algoritmo 2.1: Mi primera clase
Si se quiere instanciar un objeto de la clase anterior habrá que hacer uso de la palabra
reservada new.
1 val v = new MiPrimeraClase
Cuando se defina una variable en Scala se tendrá que especificar la mutabilidad de la misma,
pudiendo escoger entre:
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, se podrá indicar que una variable es de tipo var, consiguiendo con
esto que su valor pueda ser modificado durante todo su ciclo de vida.
Uno de los principales mecanismos utilizados para garantizar la robustez de un objeto es
la afirmación de 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 en
la misma clase, todo el código que podría modificar el estado del mismo estará localizado en
dicha clase.5
3
En estos casos, en Scala podremos omitir el uso del punto y los paréntesis. Para más información, véase la
Subsubsección 1.2.2.2: Operadores « página 10 »
4
Véase el algoritmo 1.21 (página 29) donde se podrán ver otros ejemplos de las declaraciones de importación
en Scala
5
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.
Página 33
El siguiente paso será incorporar funcionalidad a la clase que se ha creado. Para ello se
pueden definir métodos mediante el uso de la palabra reservada def:
1 class MiPrimeraClase{
2 var a = 1
3 def suma(b:Byte):Unit={
4 a += b
5 }
6 }
Una característica importante de los métodos en Scala es que todos los parámetros son
inmutables, es decir, vals. Por tanto, si se intenta modificar el valor de un parámetro en el
cuerpo de un método se obtendrá un error del compilación:
1 def sumaNoCompila(b:Byte) : Unit = {
2 b = 1 // Esto no compilara puesto que el
3 // parametro b es de tipo val
4 a += b
5 }
Algoritmo 2.2: Método suma que no compila
Los métodos presentan otra característica común entre los lenguajes orientados a objetos
como Java y es la asignación dinámica de métodos (dynamic method dispatch), lo que significa
que el código resultante de la llamada a un método depende del tipo del objeto que contiene el
método, algo que se determinará en tiempo de ejecución.
Otro aspecto relevante que se puede destacar 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 está compuesto por una
única expresión se puede prescindir del uso 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:
1 class MiPrimeraClase {
2 private var sum = 0
3 def add(b:Byte) { sum += b }
4 }
Algoritmo 2.3: MiPrimeraClase con método suma
2.2.4. Objetos funcionales
A continuación se analizará cómo se pueden construir objetos funcionales, es decir, inmu-
tables, mediante la definición de clases, algo que permitirá profundizar en cómo los aspectos
funcionales y los de orientación a objetos confluyen en el lenguaje.
Página 34
Números Racionales
Los números racionales son aquellos que pueden ser expresados como un cociente n
d
. Algu-
nas de sus características principales son:
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 ope-
rando que aparece a la derecha y posteriormente se realiza una operación de multiplica-
ción.
2.2.4.1. Constructores
Puesto que se ha decidido que en la solución que se va a realizar los números racionales
sean inmutables, se necesitará que los clientes de esta clase proporcionen toda la información
en el momento de creación de un objeto. Se podría comenzar el diseño del siguiente modo:
1 class Rational (n:Int,d:Int)
Los parámetros definidos tras el nombre de la clase son conocidos como parámetros de
clase. El compilador generará un constructor primario en cuya signatura aparecerán los dos pa-
rámetros escritos en la definición de la clase. Cualquier código que sea escrito dentro del cuerpo
de la clase que no forme parte de un atributo o de un método será incluido en el constructor pri-
mario indicado anteriormente.
2.2.4.2. Sobrescritura de métodos
Si se quisiera sobrescribir un método heredado de una clase padre en la jerarquía se tendría
que hacer uso de la palabra reservada override. Por ejemplo, si en la clase Rational se deseará
sobrescribir la implementación por defecto del método toString se podría actuar del siguiente
modo:
1 override def toString = n + "/" + d
2.2.4.3. Precondiciones
Una de las características de los números racionales es que no admiten el valor cero co-
mo denominador aunque, sin embargo, con la definición actual de la clase Rational se podría
escribir código como:
1 new Rational(11,0)
algo que violaría la definición actual de números racionales, dado que se está construyendo una
clase inmutable y toda la información debe estar disponible en el momento que se invoca al
constructor. Este último deberá asegurarse de que el denominador indicado no toma el valor
cero (0).
Página 35
La mejor aproximación para resolver este problema pasa por hacer uso de las precondicio-
nes. Este concepto, incluido en el lenguaje, representa un conjunto de restricciones que pueden
establecerse sobre los valores pasados a métodos o constructores y que deben ser satisfechas
por el cliente que realiza la llamada del método/constructor:
1 class Rational(n: Int, d: Int) {
2 require(d != 0)
3 override def toString = n +"/"+ d
4 }
Las restricciones se establecen mediante el uso del método require, el cual espera un argu-
mento booleano. En caso de que la condición exigida no se cumpla, el método require disparará
una excepción de tipo IllegalArgumentException.
2.2.4.4. Atributos y Métodos
A continuación, se definirá en la clase Rational un método público que reciba un número
racional como parámetro y retorne como resultado la suma de ambos operandos. Puesto que se
está construyendo una clase inmutable, el nuevo método deberá devolver la suma en un nuevo
número racional:
1 class Rational(n: Int, d: Int) {
2 require(d != 0)
3 override def toString = n +"/"+ d
4 // no compila: no podemos hacer that.d o that.n
5 // deben definirse como atributos
6 def add(that: Rational): Rational =
7 new Rational(n * that.d + that.n * d, d * that.d)
8 }
El código anterior muestra una primera aproximación a la solución aunque incorrecta, dado
que se producirá un error de compilación. Aunque los parámetros de clase n y d están el ámbito
del método add, sólo se puede acceder a su valor en el objeto sobre el que se realiza la llamada.
Para resolver el problema planteado en el fragmento de código anterior se tendrán que declarar
d y n como atributos de la clase Rational:
1 class Rational(n: Int, d: Int) {
2 require(d != 0)
3 val numer: Int = n // declaracion de atributos
4 val denom: Int = d
5 override def toString = numer +"/"+ denom
6 def add(that: Rational): Rational =
7 new Rational(numer * that.denom + that.numer * denom, denom *
that.denom)
8 }
Algoritmo 2.4: Números racionales
Nótese que en los fragmentos de código anteriores se está manteniendo la inmutabilidad
del diseño. En este caso, el operador de adición add devuelve 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.
Página 36
A continuación se incorporará a la clase Rational un método privado que determinará el
máximo común divisor de dos números enteros:
1 private def gcd(a:Int,b:Int):Int = if(b == 0) a else gcd(b,a %b)
Algoritmo 2.5: Cálculo del máximo común divisor de dos enteros
El código anterior muestra cómo se puede definir un método privado dentro de una clase en
Scala haciendo uso de private. En este caso, el método gcd se utilizará como método auxiliar
para calcular el máximo común divisor de dos números enteros.
2.2.4.5. Operadores
La implementación actual de la clase Rational es correcta, aunque podría haber sido defi-
nida de modo que su uso resultara mucho más intuitivo. Una de las posibles mejoras que se
podrían introducir sería la inclusión de los operadores comúnmente utilizados para la suma y el
producto:
1 def + (that: Rational): Rational = new Rational(numer *
that.denom + that.numer * denom, denom * that.denom)
2 def * (that: Rational): Rational = new Rational(numer *
that.numer, denom * that.denom)
De este modo se podría escribir código como el que a continuación se indica:
1 var a = new Rational(2,5)
2 var b = new Rational(1,5)
3 var sum = a + b
En el código anterior se aprecia como el método + es usado como un operador en notación
infija6
.
2.3. Jerarquía de clases en Scala
Una de las diferencias entre Scala y Java, como se adelantó en la Subsección 2.2.2: Módulos,
objetos, paquetes y namespaces « página 32 »(página 32), es que Scala es un lenguaje basado
en clases y, por tanto, todos los valores7
serán objetos instanciados de alguna clase.
Como se observa en la imagen 2.1, en la parte más alta de la jerarquía de clases en Scala
se encuentra el tipo Any, que es el tipo base de todos los tipos en Scala. El tipo Any tiene dos
subtipos:
El tipo AnyVal, para definir las clases que representan un valor (value classes), similar al
de los tipos primitivos definidos en lenguajes como Java (Int,Double,...). Las subclases
del tipo AnyVal se encuentran predefinidas.
El tipo AnyRef. El resto de clases harán referencia a un tipo. Las clases que se definan
siempre harán referencia, aunque sea de forma indirecta, a un tipo por defecto, el tipo
AnyRef, ya que todas las clases que se definan extenderán de forma implícita del trait
scala.ScalaObject. ScalaObject es el alias de java.lang.Object.
6
También podríamos escribir a.+(b) aunque en este caso el código resultante sería mucho menos legible. Para
más información, véase la Subsubsección 1.2.2.2: Operadores « página 10 »
7
Incluyendo valores numéricos, funciones...
Página 37
Figura 2.1: Jerarquía de Clases en Scala
En el lado opuesto, en la parte más baja de la jerarquía de clases en Scala, se encuentra el
tipo Nothing, subtipo de cualquier otro tipo. No hay valores para el tipo Nothing. Este tipo de
datos se utilizará para indicar una terminación anormal o para representar el elemento vacío en
nuestros tipos abstractos de datos.
El tipo Null es un subtipo de todas las clases que hereden de Object. Por tanto, no será
subtipo de los subtipos de AnyVal. También se verá como todas las clases que referencian a un
tipo tendrán, por defecto, un valor null de tipo Null[17].
2.3.1. Herencia en Scala
En la POO una forma de extender la funcionalidad de una clase es utilizando herencia
de clases. Con este mecanismo se puede extender una clase, agregando nuevos métodos, sin
necesidad de recompilar el código existente.
La herencia en Scala presenta las mismas limitaciones que en otros lenguajes de POO, como
Java. Las clases sólo podrán heredar su comportamiento de, a lo sumo, otra clase. Es decir, una
clase sólo podrá extender de otra clase.
Si se quisiera crear una clase para representar animales que tuviera una operación para
conocer el número de patas del animal, una posible implementación podría ser:
1 abstract class Animal {
2 def patas():Int
3 }
Algoritmo 2.6: Clase Abstracta Animal
donde la clase Animales es una clase abstracta cuyos miembros pueden no estar implementa-
dos, por lo que no se podrá instanciar.
Si ahora se quisieran definir las clases Perro y Pajaro, de tipo Animal, habría que exten-
der8
de la clase Animal e implementar el método patas, definido en la clase abstracta Animal:
8
Haciendo uso de la palabra reservada extends
Página 38
1 class Perro extends Animal{
2 def patas() = 4
3 }
4 class Pajaro extends Animal{
5 def patas() = 2
6 }
Algoritmo 2.7: Clases Perro y Pajaro de tipo Animal
Las clases Perro y Pajaro extienden de la clase Animal, por lo que un objeto de estas clases
podrá aparecer en cualquier parte en la que se requiera un objeto de tipo Animal.
Se dirá que:
Animal es la superclase de Perro y Pajaro.
Perro y Pajaro serán subclases de Animal.
Se llamarán clases base a las superclases, directas e indirectas, de una clase C. Por tanto,
Animal y Object serán las clases base de Perro y Gato.
2.3.1.1. Rasgos y herencia múltiple en Scala
En Java, como en Scala, una clase sólo puede heredar los métodos de una superclase. Pero,
¿qué se puede hacer si una clase se ajustara de forma natural a varias clases o se quisiera heredar
los métodos de varias clases?. Para resolver estas situaciones se hará uso de los rasgos (traits)
de Scala.
Las clases, los objetos y los rasgos en Scala pueden heredar sólo de una superclase, pero
también pueden heredar el comportamiento de cualquier número de rasgos.
Los rasgos son la unidad básica de reutilización de código en Scala. Un rasgo encapsula
definiciones de métodos y atributos que pueden ser reutilizados mediante un proceso de mezcla
(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 mezcla con un número indefinido de rasgos.
2.3.1.2. Funcionamiento de los rasgos
La definición de un rasgo es similar a la de una clase tradicional salvo que se utiliza la
palabra reservada trait en lugar de class.
1 trait MiPrimerTrait {
2 def printMessage(){
3 println("Este es mi primer trait")
4 }
5 }
Una vez definido, puede ser “mezclado” junto a una clase mediante el uso de las palabras
reservadas extends o with.
1 class MiPrimerMixin extends MiPrimerTrait{
2 override def toString = "Este es mi primer mixin en Scala"
3 }
Página 39
Cuando se utiliza la palabra reservada extends para realizar el proceso de mezcla se estará
heredando de manera implícita las superclases del rasgo. Los métodos heredados de un rasgo
se utilizan del mismo modo que se utilizan los métodos heredados de una clase. De manera
adicional, un rasgo también define un tipo.
En el caso de que se desee realizar un proceso de mezcla en el que una clase ya indica un
padre de manera explicita mediante el uso de extends, se tendrá que utilizar la palabra reservada
with. Si se quisieran incluir en el proceso de mezcla múltiples rasgos, únicamente se tendrían
que incluir más cláusulas with.
En este punto sería posible pensar que los rasgos son como interfaces Java con métodos
concretos, pero realmente pueden hacer muchas más cosas. Por ejemplo, los rasgos pueden
definir atributos y mantener un estado. Realmente en un rasgo se puede hacer lo mismo que en
una definición de clase con una sintaxis similar, aunque existen dos excepciones:
Un rasgo no puede tener parámetros de clase (los parámetros pasados al constructor pri-
mario de la clase).
Mientras que en las clases las llamadas a métodos de clases padre (super.xxx) son en-
lazadas de manera estática, en el caso de los rasgos dichas llamadas son enlazadas di-
námicamente. Si en una clase se escribe super.método(), se sabrá en todo momento cual
será la implementación del método invocada. Sin embargo, el mismo código escrito en un
rasgo 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 rasgo y una clase realizan un proceso de mezcla. Este curioso comportamien-
to de super es la clave que permite a los rasgos trabajar como stackable modifications
(modificaciones apilables), las cuales se verán con mayor detalle a continuación.
2.3.1.3. Rasgos como modificaciones apiladas
Ahora se analizará otro de los usos más populares de los rasgos: facilitar modificaciones
apilables en las clases. Los rasgos nos permitirán modificar los métodos de una clase y, adi-
cionalmente, nos permitirán apilarlas entre sí. Se apilarán modificaciones sobre una cola de
números enteros. Dicha cola tendrá dos operaciones: put (que añadirá números a la cola) y get
(que sacará los elementos de la cola).
Generalmente las colas siguen el comportamiento First In First Out – “primero en entrar,
primero en salir” – (FIFO), por lo que el método get tendría que retornar los elementos en el
mismo orden en el que fueron introducidos.
Dada una clase que implementa el comportamiento descrito en el párrafo anterior, se podría
definir un rasgo que llevara a cabo modificaciones como:
Multiplicar por dos 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 rasgos anteriores representan modificaciones dado que no definen una cola por sí
mismos, sino que llevan a cabo modificaciones sobre la cola subyacente con la que realizan el
proceso de mezcla. Los rasgos también son apilables: se podría escoger cualquier subconjunto
de los tres anteriores e incorporarlos a una clase, de manera que conseguiríamos una nueva clase
con la funcionalidad deseada. El siguiente fragmento de código representa una implementación
reducida del comportamiento de una cola FIFO:
Página 40
1 import scala.collection.mutable.ArrayBuffer
2 abstract class ColaEnteros {
3 def get(): Int
4 def put(x: Int)
5 }
6 class ColaEnterosBasica extends ColaEnteros {
7 private val buf = new ArrayBuffer[Int]
8 def get() = buf.remove(0)
9 def put(x: Int) { buf += x }
10 }
Ahora se realizarán un conjunto de modificaciones sobre la clase anterior. Para ello, se hará
uso de los rasgos. El siguiente fragmento de código muestra un rasgo que duplica el valor de
un elemento que se desea añadir a la cola:
1 trait Duplicado extends ColaEnteros{
2 abstract override def put(x:Int) { super.put(2*x) }
3 }
Nótese el uso de las palabras reservadas abstract override. Esta combinación de modifica-
dores sólo puede ser utilizada en los rasgos y no en las clases, e indica que el rasgo debe ser
integrado (mezclado) con una clase que presenta una implementación concreta del método en
cuestión.
A continuación se muestra un ejemplo de uso del rasgo anterior:
scala> class MiCola extends ColaEnterosBasica with Duplicado
defined class MiCola
scala> val cola = new MiCola
cola: MiCola = MiCola@91f017
scala> cola.put(10)
scala> cola.get()
res12: Int = 20
Para analizar el mecanismo de apilado de modificaciones, se implementarán en primer lugar
los dos rasgos restantes que han sido descritos anteriormente:
1 trait Incremento extends ColaEnteros{
2 abstract override def put(x:Int) { super.put(x + 1) }
3 }
4 trait Filtro extends ColaEnteros{
5 abstract override def put(x:Int) { if ( x >= 0 ) super.put(x)
}
6 }
Una vez disponibles las nuevas modificaciones, se podría generar una nueva cola del modo
que más nos interese:
scala> val cola = (new ColaEnterosBasica with Incremento with Filtro)
cola: BasicIntQueue with Incremento with Filtro...
scala> cola.put(-1); cola.put(0); cola.put(1)
scala> cola.get()
res15: Int = 1
scala> cola.get()
res16: Int = 2
Página 41
El orden en el que se mezclan los rasgos es importante . De manera resumida, cuando se
invoca a un método de una clase que presenta modificaciones apiladas, el método del rasgo
definido más a la derecha es el primero en ser invocado. Si dicho método invoca a super, éste
invocará al rasgo que se encuentra más a la izquierda y así sucesivamente. En el ejemplo ante-
rior, el método put del trait Filtro será invocado en primer lugar, por lo que aquellos números
menores que cero no serán incorporados a la cola. El método put del trait Incremento sumará el
valor uno a cada uno de los números (mayores o iguales que cero).
2.3.1.4. ¿Cuándo usar rasgos?
A continuación se presentan algunos criterios que podrán ayudar al programador a determi-
nar cuando usar rasgos:
Si el comportamiento no pretende ser reutilizado, entonces encapsularlo en una clase.
Si el comportamiento pretende ser reutilizado en múltiples clases no relacionadas, enton-
ces construir un rasgo (trait).
Si se desea que una clase Java herede de dicha funcionalidad, entonces se deberá utilizar
una clase abstracta.
Si la eficiencia es importante, se debería escoger 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 rasgos son compilados a
interfaces y esto podría penalizar el rendimiento.
Si tras haber analizado todas las opciones anteriores aún no se tiene claro qué aproxima-
ción se desea utilizar, se debería comenzar por el uso de rasgos (traits). Se podrá cambiar
en el futuro y, generalmente, se mantienen más opciones abiertas.
2.4. Patrones y clases case
Las clases case son un concepto relativamente novedoso. Permiten incorporar el mecanis-
mo de concordancia de patrones (pattern matching) sobre objetos sin la necesidad de código
repetitivo. De manera general, sólo se tendrá que prefijar la definición de una clase con la pa-
labra reservada case para indicar que la clase definida puede ser utilizada en la definición de
patrones.
2.4.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 (método de fábrica) con el nombre de la clase.
Gracias a esto se podrá 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:
1 val op = BinaryOperation(‘‘+’’, Number(1), v)
Página 42
Otra funcionalidad sintáctica incorporada por el compilador es que todos los argumentos en
la lista de parámetros incorporan de manera implícita el prefijo val, por lo que éstos últimos
serán atributos de clase. Por último, pero no por ello 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 gene-
rados son un poco más grandes9
y se tendrá 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.
2.4.2. Patrones: estructuras y tipos
La estructura general de un patrón en Scala presenta la siguiente estructura:
selector match {alternativas}
Incorporan un conjunto de alternativas en las que cada una de ellas comienza por la palabra
reservada case. Cada una de estas alternativas incorpora un patrón y una o más expresiones que
serán evaluadas en caso de que se produzca la concordancia del patrón. Se utiliza el símbolo
de flecha (=>) para separar el patrón de las expresiones. Como se ha podido comprobar, la
sintaxis de los patrones es sumamente sencilla por lo que, a continuación, se profundizará en
los diferentes tipos de patrones que se pueden construir en Scala.
2.4.2.1. Patrones comodín
El patrón (_) concuerda con cualquier objeto, por lo que podría ser utilizado como una
alternativa catch-all tal y como se muestra en el siguiente ejemplo:
1 expression match {
2 case BinaryOperation(op,leftSide,rightSide) =>
println(expression + " es una operacion binaria")
3 case _ =>
4 }
2.4.2.2. Patrones constantes
Un patrón constante concuerda única y exclusivamente consigo mismo. El siguiente frag-
mento de código muestra algunos ejemplos de patrones constantes:
1 def describe(x: Any) = x match {
2 case 5 => "cinco"
3 case true => "verdadero"
4 case "hola" => "hi!"
5 case Nil => "Es una lista vacia"
6 case _ => "cualquier otra accion"
7 }
9
Son más grandes porque se generan métodos adicionales y se incorporan atributos implícitos para cada uno de
los parámetros del constructor
Página 43
2.4.2.3. Patrones variables
Un patrón variable concuerda con cualquier objeto, del mismo modo que los patrones co-
modín (wildcard). A diferencia de los patrones comodín, Scala enlaza la variable al objeto, por
lo que posteriormente se podrá hacer uso de dicha variable para actuar sobre el objeto como se
puede apreciar en el siguiente ejemplo:
1 expr match {
2 case 0 => "valor cero"
3 case somethingElse => "no es cero: valor "+ somethingElse
4 }
2.4.2.4. Patrones constructores
Son en este tipo de construcciones donde los patrones se convierten en una herramienta
muy poderosa. Básicamente están formados por un nombre y un número indefinido de patrones.
Asumiendo que el nombre designa una clase de tipo case, este tipo de patrones comprobarán
primero si el objeto pertenece a dicha clase para, posteriormente, comprobar si los parámetros
del constructor concuerdan con el conjunto de patrones extra indicados.
La definición anterior puede no resultar demasiado explicativa, por lo que a continuación se
incluye un pequeño ejemplo en el que se realizan tres comprobaciones:
Comprueba que el objeto de primer nivel es de tipo BinaryOperation.
Si el objeto de primer nivel es del tipo BinaryOperation, se comprobará que su tercer
argumento es de tipo Number.
Si el tercer argumento del objeto de tipo BinaryOperation es de tipo Number, se verificará
que su atributo de clase es 0.
En caso de que fallara alguna de las anteriores comprobaciones, coincidiría con el patrón
comodín que aparece definido.
1 expr match {
2 case BinaryOperation("+", e, Number(0)) =>
println("comprobacion de patrones a gran profundidad")
3 case _ =>
4 }
2.4.2.5. Patrones de secuencia
Se pueden establecer patrones de concordancia sobre listas o vectores del mismo modo
que se han definido para las clases. Deberá utilizarse la misma sintaxis, aunque ahora se podrá
indicar cualquier número de elementos en el patrón.
El siguiente fragmento de código muestra un patrón que comprueba que la expresión coin-
cide con una lista del tipo List, compuesta exactamente por tres elementos y cuyo primer valor
sea 0:
1 expr match {
2 case List(0, _, _) => println("Concordancia de patrones!")
Página 44
3 case _ =>
4 }
2.4.2.6. Patrones tipados
Se puede utilizar este tipo de construcciones como reemplazo de las comprobaciones y
conversiones de tipos:
1 def tamanoGenerico(x: Any) = x match {
2 case s: String => s.length
3 case m: Map[_, _] => m.size
4 case _ => -1
5 }
El método tamanoGenerico devolverá la longitud de un objeto cualquiera. El patrón utili-
zado en el anterior ejemplo, “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.5. Polimorfismo en Scala
Cuando se habla de polimorfismo en programación, se hace referencia a que:
El tipo de la clase puede tener instancias de muchos tipos.
A una clase o función se le pueden aplicar argumentos de diferentes tipos.
Para lo cual se aplicarán principalmente dos técnicas fundamentales:
Subtipado. Instancias de una clase podrán ser pasadas a una clase base.
Genericidad. Instancias de una función o clase serán creadas parametrizando sus tipos.
La primera técnica, el subtipado, relacionada con el poliformismo en el paradigma de la
POO, se corresponde con el concepto de herencia visto en la Subsección 2.3.1: Herencia en
Scala « página 38 ».
La segunda técnica, la genericidad, se corresponde con la abstracción de los tipos en clases
y funciones, generalizando las mismas. En este caso, el término genericidad es más común-
mente utilizando dentro del paradigma de la programación funcional y está relacionado con la
abstracción de tipos en funciones para crear funciones polimórficas.
Cuando se generaliza el uso de las clases, algo relacionado con la POO, aunque se puede
hablar de genericidad, es más habitual referirse a estas clases como clases parametrizadas,
clases genéricas, constructores de tipos...
A continuación, se pondrá de manifiesto la importancia de la abstracción de tipos en las
clases implementando una versión muy simple de lista inmutable enlazada para los números
enteros, que podrá ser construida como:
Nil. Que representará la lista vacía.
Cons. Que contendrá un elemento y el resto de la lista.
Con estas indicaciones ya se podría comenzar a crear la primera lista de enteros:
Página 45
1 trait ListaInt
2 class Cons (val cabeza:Int, val cola:ListaInt) extends ListaInt
3 class Nil extends ListaInt
Algoritmo 2.8: Lista de Enteros
Según se ha definido la lista de enteros, ésta podrá ser:
Una lista vacía, new Nil
Un elemento de la lista, new Cons(x,xs)10
, compuesto por un elemento x de tipo Int (que
será la cabeza de la lista) y un elemento xs (la cola de la lista, que tendrá que ser de tipo
ListaInt).
Ahora también se necesitará definir una lista inmutable enlazada para valores booleanos que
siga las mimas especificaciones que la anterior lista:
1 trait ListaBool
2 class Cons (val cabeza:Bool, val cola:ListaBool) extends ListaBool
3 class Nil extends ListaBool
Algoritmo 2.9: Lista de Booleanos
Si se analizan los algoritmos 2.8 y 2.9, rápidamente se observa que las diferencias entre
ambos están relacionadas con los tipos de datos que pueden contener cada una de las listas.
Llegados a este punto, si se quisiera crear una lista de Double, Float,...sería necesario crear
una nueva clase para cada tipo. Esto provocaría que nuestro código final fuera muy extenso,
algo que dificultaría el mantenimiento del mismo ya que si, por ejemplo, se quisiera dotar de
alguna funcionalidad extra a las listas, habría que buscar y modificar una gran cantidad de
implementaciones de listas11
. Otra consecuencia relacionada con la extensión del código será el
hecho de que aumentaría notablemente la posibilidad de que éste contenga errores. Además, no
se estaría facilitando de ninguna forma la escalabilidad de nuestra solución.
Si se vuelven a observar los algoritmos 2.8 y 2.9, se puede comprobar que el comporta-
miento de ambas listas es similar y si se intentara abstraer el tipo de datos, lo que diferencia a
ambas, sería posible tener una única clase que valdría tanto para los tipos Int, Boolean, como
para cualquier otro tipo de datos definido.
Se podrán generalizar las clases definidas anteriormente parametrizando el tipo. Para ello
junto al nombre de la clase o rasgo, se indicará el nombre que se utilizará para hacer referencia
a los tipos de datos parametrizados en la clase o rasgo, en una lista separada por comas y
encerrada entre corchetes. Por ejemplo:
trait TraitParametrizado[T,U,S] //Trait con tres tipos de datos
//parametrizados:T,U y S
Entonces, en el algoritmo 2.10 se puede ver la implementación de una lista genérica que
presente el comportamiento descrito anteriormente para la listas de enteros y booleanos.
1 trait ListaGen[T] //Indicamos que el tipo del trait esta
parametrizado.
2 //T sera el nombre del tipo generico.
10
Se ha usado val delante de los parámetros de clase para convertir a los mismos en atributos de clase
11
Concretamente una por cada tipo de datos que nuestra lista pueda contener.
Página 46
3 class Cons[T] (val cabeza:T, val cola:ListaGen[T]) extends
ListaGen[T]
4 class Nil[T] extends ListaGen[T]
Algoritmo 2.10: Lista Genérica
Ahora si se quisiera añadir algunas funciones básicas de las listas a la solución anterior,
sólo se tendrían que definir las mismas una sola vez para que estén disponibles para booleanos,
enteros,...consiguiendo una solución escalable y mucho más fácil de mantener:
1 trait ListaGen[T]{
2 def isEmpty:Boolean
3 def head:T
4 def tail:ListaGen[T]
5 }
6 class Cons[T](val head:T, val tail:ListaGen[T]) extends ListaGen[T]{
7 def isEmpty = false
8 }
9 class Nil[T] extends ListaGen[T]{
10 def isEmpty = true
11 def head = throw new NoSuchElementException("Nil.head")
12 def tail = throw new NoSuchElementException("Nil.tail")
13 }
Algoritmo 2.11: Lista Genérica con funciones
Al igual que las clases, se pueden generalizar las funciones si se abstrae el tipo de datos. La
parametrización de funciones se verá en mayor profundidad en la sección dedicada a las funcio-
nes polimórficas, dentro del capítulo dedicada a la programación funcional, en la Sección 3.7:
Funciones polimórficas. Genericidad « página 66 ». A continuación se muestra un ejemplo
simple, una función que creará una lista con un único elemento pasado como parámetro:
1 def listaUnElemento[T](elem:T):ListaGen[T]=new Cons[T](elem,Nil)
Algoritmo 2.12: Ejemplo de función parametrizada
2.5.1. Acotación de tipos y varianza
La acotación de tipos y la varianza son conceptos relacionados a las técnicas para definir
clases y funciones polimórficas: la genericidad y el subtipado.
2.5.1.1. Acotación de tipos
Se pondrá de manifiesto la relevancia de la acotación de tipos definiendo una función son-
TodosPositivos para el siguiente tipo ListaInt (basado en el algoritmo 2.8):
1 trait ListaInt
2 case class Cons (val cabeza:Int, val cola:ListaInt) extends
ListaInt
3 case object Nil extends ListaInt
tal que:
Página 47
Tenga un parámetro del tipo ListaInt.
Devuelva como valor una lista del tipo ListaInt si todos los elementos de la lista son
positivos.
Lance una excepción en otro caso.
En una primera solución, se podría definir la función sonTodosPositivos como se muestra en
el algoritmo 2.13.
1 def sonTodosPositivos(xs:ListaInt):ListaInt = xs match{
2 case Nil => Nil
3 case Cons(h,ys) if h>=0 => Cons(h,sonTodosPositivos(ys))
4 case _ => throw new Error
5 }
Algoritmo 2.13: Función sonTodosPositivos para ListaInt
Observando bien la definición anterior, si se invoca sonTodosPositivos con Nil devolverá Nil,
mientras que si se invoca la función con una instancia de la clase Cons(h,ys) el valor devuelto
será una instancia de la clase Cons. Por tanto, la definición de la función sonTodosPositivos
podría haber sido más específica de modo que este comportamiento quedara reflejado. Para es-
pecificar este comportamiento se podría haber utilizado una cota superior para el tipo ListaInt,
lo que se expresaría:
1 def sonTodosPositivos[S<:ListaInt](xs:S):ListaInt = ...
2 }
Algoritmo 2.14: Función sonTodosPositivos para ListaInt con tipo acotado superiormente
donde “[S<:ListaInt]” indica que S podrá ser instanciado sólo por los tipos que hereden de
ListaInt. Generalmente, estableceremos dos tipos de cotas:
1. “S <: T”, que indicará que S es un subtipo de T.
2. “S >: T”, que indicará que S es un supertipo de T.
Del mismo modo es posible establecer una cota inferior. A continuación se muestra un
ejemplo de un parámetro acotado inferiormente en la función sonTodosPositivos:
1 def sonTodosPositivos[T >: ListaInt](xs:T):T = ...
2 }
Algoritmo 2.15: Función sonTodosPositivos para ListaInt con tipo acotado inferiormente
Cuando se introduce la cota inferior “[T >: ListaInt]”, se está introduciendo un parámetro
T que sólo podrá variar con los diferentes supertipos de ListaInt. Por tanto T podrá ser de tipo
ListaInt, AnyRef o Any, según está definida la jerarquía de clases en Scala (véase la sección
Sección 2.3: Jerarquía de clases en Scala « página 37 »).
De este modo, una posible definición de la función sonTodosPositivos utilizando tanto una
cota superior como una cota inferior sería:
1 def sonTodosPositivos[S <: ListaInt,T >: ListaInt](xs:S):T = xs
match{
2 case Nil => Nil
3 case Cons(h,ys) if h>=0 => Cons(h,sonTodosPositivos(ys))
Página 48
4 case _ => throw new Error
5 }
Algoritmo 2.16: Trait Function1
Es posible combinar cota superior y cota inferior restringiendo el tipo a un intervalo, por lo
que se podría encontrar una definición de tipo similar a “[S >: Cons <: ListaInt]” pudiendo ser
S, en este caso, de tipo Cons y ListInt
2.5.1.2. Varianza
La varianza definirá la relación existente entre los tipos parametrizados y los subtipos.
Para comprender mejor el concepto de varianza se tomarán como referencia los tipos de datos
definidos en los algoritmos 2.6 y 2.7 vistos en la Subsección 2.3.1: Herencia en Scala « página
38 ».
En ella se establecen las siguientes relaciones entre los tipos definidos:
Perro es subclase de Animal, Perro <: Animal
Pajaro es subclase de Animal, Pajaro <: Animal
Animal es la clase base de Perro y Pajaro, Animal >: Pajaro.
Teniendo en cuenta la relación establecida entre dichos tipos, surge una pregunta: ¿será
cierto que List[Pajaro] <: List[Animal]?
Intuitivamente se podría responder afirmativamente ya que tiene sentido que una lista de pá-
jaros sea un caso especial de una lista de animales. Se dirá que los tipos de datos que mantengan
esta relación tienen una varianza positiva o covarianza.
Ahora se plantea la duda de que si la covarianza es adecuada para todos los tipos que se
definan. La covarianza será apropiada para los tipos de datos inmutables12
y no será adecuada
para los tipos que permitan mutaciones de sus elementos.
Dada una clase C con un tipo parametrizado C[T] y dos tipos de datos A y B, que presentan
la siguiente relación A <: B, se plantea la duda sobre la varianza que presentará C, lo cual no
es una decisión binaria (covarianza o no) sino que se pueden encontrar tres posibles relaciones
entre C[A] y C[B]:
C[A] <: C[B], siendo C[A] un subtipo de C[B], por lo que diremos que C presenta una
relación de covarianza o varianza positiva.
C[A] >: C[B], lo cual representa la relación opuesta siendo C[B] un subtipo de C[A], lo
que representará un caso de contravarianza o varianza negativa.
C[A] no sea subtipo de C[B] y C[B] tampoco sea subtipo de C[A], que sería un caso de
invarianza.
Se representará la varianza de C cuando se defina la misma:
class C[+A]{...} si C tiene varianza positiva o convarianza.
class C[-A]{...} si C tiene varianza negativa o contravarianza.
class C[A]{...} siendo este un caso de invarianza de C.
12
Si los métodos definidos cumplen ciertas condiciones
Página 49
Se ha dicho que la covarianza será adecuada para los tipos inmutables cuyos miembros
cumplan unas ciertas propiedades.
Antes de empezar a enumerar estas propiedades se estudiará la relación que presentan los
tipos de funciones definidos a continuación:
1 type A = Animal => Perro
2 type B = Perro => Animal
Algoritmo 2.17: Tipos de funciones A y B
Para determinar la relación entre los tipos A y B no se tendrá más que aplicar el principio de
sustitución de Liskov13
y comprobar que A <: B, es decir A es subtipo de B.
Seguidamente se verá como se puede mantener la covarianza en la definición de nuestras
funciones o métodos. Si se tuvieran cuatro tipos A1, A2, B1 y B2 que presentan la siguiente
relación: A2 <: A1 y B1 <: B2 y las funciones A1 =>B1 y A2 =>B2, aplicando nuevamente
el principio de sustitución de Liskov se observa que la relación entre los tipos de ambas fun-
ciones es de covarianza, A1 =>B1 <: A2 =>B2. Si se presta atención, se puede observar que la
relación entre los argumentos es de contravarianza y la relación entre los tipos resultantes es de
covarianza.
Por tanto, las características que deben de presentar los miembros de las clases que presenten
paramétros de tipo covariantes serán:
Presentar una relación de contravarianza en los argumentos.
Presentar una relación de covarianza en los resultados.
Estas características se pueden ver representadas la definición del trait Function1:
1 trait Function1[-T,+U]{
2 def apply(x:T):U
3 }
Algoritmo 2.18: Trait Function1
Por tanto, se deberá de tener en cuenta que los parámetros tipados covariantes sólo pueden
aparecer como resultados de los métodos. Los parámetros tipados contravariantes sólo pueden
aparecer como argumentos de los métodos, mientras que los parámetros tipados invariantes
podrán aparecer en cualquier lugar.
Una vez vista la varianza, se podría mejorar el tipo de datos ListaGen definido en el algorit-
mo Algoritmo 2.11: Lista Genérica con funciones « página 47 ».
En primer lugar se podría pensar que no se necesita crear una instancia de la clase Nil cada
vez que se quiera representar la lista vacía, convirtiendo Nil en un objeto cuya superclase es
ListaGen:
13
El Principio de Sustitución de Liskov (LSP) es una definición particular de una relación de subtipificación,
llamada tipificación (fuerte) del comportamiento, que fue introducido inicialmente por Barbara Liskov en una
conferencia magistral en 1987 titulada La Abstracción de Datos y Jerarquía y que dice que: si S es un subtipo de
T, entonces los objetos de tipo T en un programa de computadora pueden ser sustituidos por objetos de tipo S (es
decir, los objetos de tipo S pueden sustituir objetos de tipo T), sin alterar ninguna de las propiedades deseables de
ese programa
Página 50
1 trait ListaGen[+T]{
2 def isEmpty:Boolean
3 def head:T
4 def tail:ListaGen[T]
5 }
6 class Cons[T](val cabeza:T, val cola:ListaGen[T]) extends
ListaGen[T]{
7 def isEmpty = false
8 }
9 object Nil extends ListaGen[Nothing]{
10 def isEmpty = true
11 def head:Nothing = throw new NoSuchElementException("Nil.head")
12 def tail:Nothing = throw new NoSuchElementException("Nil.tail")
13 }
Algoritmo 2.19: Lista genérica de enteros, covariante y con Nil como objeto
Teniendo en cuenta que los objetos no pueden ser parametrizados, se ha borrado el parámetro
[T] de Nil. A continuación, se ha cambiado el parámetro de tipo que presentaba ListaGen en
el objeto Nil ya que el parámetro T no está accesible para el objeto Nil y esto provocaría un
error. Por último, se ha especificado que el parámetro de tipo presente en el rasgo ListaGen es
covariante para poder realizar definiciones del tipo:
1 val x:ListaGen[String] = Nil
Página 51
Página 52
Capítulo 3
Programación Funcional en Scala
3.1. Introducción a la programación funcional
La programación funcional es un paradigma de programación que trata la
computación como la evaluación de funciones matemáticas evitando los estados
y los datos mutables.[24]
La programación funcional es un paradigma en el que se trata la computación como la eva-
luación de funciones matemáticas y se evitan los programas con estado y datos que puedan ser
modificados. Se adopta una visión más matemática en la que los programas están compuestos
por numerosas funciones que esperan una determinada entrada y producen una determinada
salida y, en muchas ocasiones, otras funciones.
Otra de las principales características de la programación funcional es la ausencia de efectos
colaterales, gracias a lo cual los programas desarrollados son mucho más sencillos de compren-
der y probar. Adicionalmente, se facilita la programación concurrente, evitando que se convierta
en un problema gracias a la ausencia de cambio.
3.1.1. Características de los Lenguajes de Programación Funcionales
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.
Closure (cierre).
Asignación simple.
Evaluación perezosa.
Inferencia de tipos.
Optimización de las llamadas de cola.
Efectos monádicos.
Página 53
3.1.2. Scala como lenguaje funcional
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.1
. Scala da soporte tanto a variables inmutables (también conocidas como valores) como a
variables que soportan estados no permanentes.
3.1.3. ¿Por qué la programación funcional?
La programación funcional permitirá crear programas modulares que estarán compuestos
por pequeños componentes que podrán ser entendidos y reutilizados independientemente del
propósito del programa. Por tanto, el significado del programa vendrá determinado por el propio
significado que le demos a estos componentes y por las reglas que determinen la composición
de estos componentes.
3.2. Sentido estricto y amplio de la programación funcional
En un sentido estricto, la programación funcional significa programar sin variables mu-
tables, asignaciones, bucles, y otras estructuras imperativas de control. (Pure Lisp, FP, XSLT,
Haskell 2
, ...)
En un sentido más amplio, la programación funcional significa centrarse en las funciones,
que podrán ser valores creados, consumidos y compuestos..., lo cual se hace más fácil con un
lenguaje de programación funcional. (Lisp, OCaml, Haskell, Scala...)
Por tanto, se puede afirmar que la programación funcional se basa en una premisa simple
pero con un gran alcance: desarrollar programas utilizando sólo funciones puras, es decir,
funciones que no tengan efectos colaterales. 3
La programación funcional impone restricciones con relación a cómo escribir los programas
pero no en qué programas se pueden escribir. A lo largo de este capítulo se verá como muchos
programas que se creían imposibles escribir sin evitar efectos colaterales tienen una versión
funcional pura. También se podrá comprobar cómo, aunque esté fuera del ámbito de la pro-
gramación funcional pura, en algunos casos será inevitable que aparezcan efectos colaterales,
mayormente ligados a las mejoras de rendimiento del programa, aunque se intentará que éstos
no sean observables. Por ejemplo, se mutarán datos declarados localmente en el cuerpo de una
función, teniendo en cuenta que esos datos no sean referenciados fuera de la misma.
Otra ventaja de la programación funcional radica en el hecho de que escribir programas
con funciones puras incrementará la modularidad de los mismos y, como consecuencia de la
modularidad, las funciones puras serán fáciles de verificar (test), reutilizar, paralelizar y gene-
ralizar. Así, se consideran a las funciones “ciudadanos de primera clase” dentro de un lenguaje
de programación funcional, lo que significará:
Pueden ser definidas en cualquier lugar.
Como cualquier otro valor, podrán ser pasadas como parámetros de funciones y ser de-
vueltas como resultado de las mismas.
Como otros valores, existirán un conjunto de operadores para componer funciones.
1
Un ejemplo de lenguaje funcional puro sería Haskell
2
Sin mónadas I/O o UnsafePerformIO
3
La reasignación de una variable, la modificación de una estructura de datos, lanzar una excepción, la impresión
por pantalla...se consideran efectos colaterales
Página 54
3.2.1. ¿Qué son las funciones puras?
Son aquellas que no producen ningún efecto observable en la ejecución del programa dis-
tinto al procesamiento esperado de las entradas para producir un resultado. A estas funciones
también las llamaremos sin efectos laterales.
Por ejemplo: La función suma (+) de enteros.
Se podrá formalizar la idea de función pura usando el concepto de transparencia referen-
cial. La transparencia referencial (TR) es un concepto que podemos aplicar a las expresiones,
además de a las funciones.
Diremos que una expresión será referencialmente transparente cuando podamos sustituir
dicha expresión por su resultado, sin cambiar el significado del programa. Del mismo modo,
se dice que una función es pura si el cuerpo de la función es referencialmente transparente,
asumiendo que las entradas también lo sean.
La transparencia referencial lleva a un modo de razonar en la evaluación de programas
llamado modelo de sustitución4
.
Cuando las expresiones son referencialmente transparentes, se puede imaginar el proceso
de computación como la resolución de una ecuación algebraica.
3.3. Funciones y cierres en Scala
Hasta el momento se han 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.
Cuando los programas crecen, se necesita hacer uso de un conjunto de abstracciones que
posibiliten dividir dicho programa en piezas más pequeñas y manejables, las cuales permitirán
una mejor comprensión del mismo. Scala ofrece varios mecanismos para definir funciones que
no están presentes en Java. Además de los métodos, que no son más que funciones miembro de
un objeto, se puede hacer uso de funciones anidadas, funciones anónimas y funciones valor.
3.3.1. Definición de funciones
Mediante el uso de la palabra reservada def se pueden definir funciones con argumentos.
La sintaxis es:
1 def <nombre_funcion>(<parametro1:tipo1>,...):<tipo_resultado> = {
2 <cuerpo de la funcion>
3 }
El tipo del resultado (el tipo del valor que devuelve la función) es opcional excepto en las
funciones recursivas, que siempre hay que indicarlo. Las llaves que delimitan el cuerpo de la
función también son opcionales si el cuerpo sólo tiene una sentencia.
Ejemplo:
1 def max(a:Int,b:Int):Int = if (a>=b) a else b
En la función max se podría haber omitido el tipo del resultado dejando que Scala lo infiriera.
Sin embargo, en una función recursiva hay que indicarlo explícitamente.
4
Para más información sobre la evaluación de expresiones en Scala, véase la Sección 1.4: Evaluación en Scala
« página 18 »
Página 55
Una vez se ha definido una función, se podrá invocar por su nombre.
Ejemplo:
scala> def max(a:Int,b:Int):Int= if (a>b) a else b
max: (a: Int, b: Int)Int
scala> max(32,5)
res1: Int = 32
3.3.2. Funciones anidadas
Es un buen estilo de programación dividir tareas en pequeñas funciones, aunque en muchas
ocasiones es deseable que esas pequeñas funciones no estén visibles para poder ser usadas
fuera del ámbito en el que se han creado, por lo que se tendrán escribir dentro del bloque de
otra función.
En el algoritmo 3.1 se muestra un ejemplo de la definición de pequeñas funciones dentro
del bloque de otra función. En concreto, se define la función sqrt que devolverá el cálculo
de la raíz cuadrada del número pasado como argumento utilizando el método de Newton de
aproximaciones sucesivas5
[32]. En el algoritmo 3.1 se ha tomado:
El valor 1 como aproximación inicial.
La función suficienteBuena6
como criterio de finalización del algoritmo.
1 def sqrt(x: Double) = {
2 def sqrtIter(valorEstimado: Double): Double = //Funcion
recursiva para calcular la raiz cuadrada
3 if (suficienteBuena(valorEstimado)) valorEstimado
4 else sqrtIter(mejorAproximacion(valorEstimado))
5
6 def mejorAproximacion(valorEstimado: Double) = //Funcion para
mejorar la aproximacion
7 (valorEstimado + x / valorEstimado) / 2
8
9 def suficienteBuena(valorEstimado: Double) = //Condicion de
terminacion del algoritmo
10 abs(cuadrado(valorEstimado) - x) < 0.001
11 sqrtIter(1.0)
12 }
Algoritmo 3.1: Cálculo de la raíz cuadrada de un número por el método de Newton
5
En términos generales, cada vez que se tenga una estimación valorEstimado del valor de la raíz cuadrada de un
número x (pasado como argumento a la función), se podrá hacer una pequeña manipulación para obtener una mejor
aproximación (una más cercana a la verdadera raíz cuadrada) realizando la media aritmética del valorEstimado
y x
valorEstimado ,es decir,
valorEstimado+ x
valorEstimado
2 . Al repetir este proceso, se obtendrán cada vez mejores
estimaciones de la raíz cuadrada. El algoritmo deberá detenerse cuando la estimación sea lo “suficientemente
buena”, algo que deberá ser un criterio bien definido.
6
Este criterio de detención no es muy bueno ya que no es muy preciso para números pequeños y cuando se
le pasa como argumento un número grande podría no terminar(ya las operaciones aritméticas son casi siempre
realizadas con una precisión limitada en los ordenadores). El criterio de detención se podría mejorar indicando
como condición de terminación la invariabilidad de los n primeros decimales.
Página 56
3.3.3. Diferencias entre métodos y funciones
En la mayoría de los casos se pueden tratar funciones y métodos indistintamente. Sin embar-
go, no son exactamente iguales, aunque los dos sirven para definir bloques de computación. La
diferencia sutil está en que un método es siempre un miembro de una clase (junto con campos
y tipos), mientras que una función no esta ligada a una clase y, como se ha visto anteriormente,
su naturaleza de objeto (de clase FunctionN) permite que sea pasada, asignada o devuelta en el
típico estilo funcional.
scala> def m(x: Int) = 2*x
m: (x: Int)Int
scala> val f = (x: Int) => 2*x
f: (Int) => Int =
Como se puede ver, el tipo para función y objeto difiere. Antes se ha comentado que los mé-
todos siempre pertenecen a clases. Para poder definir métodos, como se ha hecho en el anterior
ejemplo, el intérprete de Scala crea un objeto invisible que rodea todo lo definido desde la línea
de comandos.
¿Qué pasa si se intenta invocar toString sobre un método?
scala> m.toString
:7: error: missing arguments for method m in object $iw;
follow this method with ‘_’ if you want to treat it as a partially applied function
m.toString ^
scala> f.toString
res1: java.lang.String =
Efectivamente, el método no es un objeto, pero la función sí.
En Scala existe una convención por la cual la sintaxis nombre() se traduce en una llamada
al método apply de aquello referido con nombre. Esto significa que la expresión funcion(...)
realmente se traduce en funcion.apply(...), en virtud de que, como se ha visto, las funciones
son objetos con este método especial. Así que en el fondo, las llamadas a funciones en Scala
acaban siendo “por debajo” ejecuciones de algún método apply definido sobre algún objeto. La
creación de estos objetos ocurre de manera transparente; cuando en código se definen funciones,
Scala crea los objetos correspondientes de manera automática.
Aún hay algo más. En Scala se pueden asignar las funciones a variables (independientemen-
te de que se traten de variables mutables definidas con var como a variables inmutables definidas
con val). Reiterando, esto es una asignación de un objeto tipo FunctionN a dicha variable. Pero
también es posible asignar a una variable un método de una clase. Como por ejemplo:
scala> def m(x: Int) = 2*x
m: (x: Int)Int
scala> val f = m _
f: (Int) => Int = <function1>
scala> f.toString
res4: java.lang.String = <function1>
Como se puede observar, el tipo de f es el mismo que se vio anteriormente para una función.
Scala ha creado automáticamente un objeto cuya función apply llama al método m, y éste es el
objeto asignado a f. También se aprecia en este ejemplo la sintaxis utilizada para convertir m en
una función parcialmente aplicada:
scala> val f = m _
Si se hubiera escrito simplemente:
Página 57
scala> val f = m
Scala habría interpretado que lo que se trata de hacer es asignar a f el resultado de la invo-
cación de m. Pero lo que verdaderamente se intenta hacer es asignar m en sí. La sintaxis m _
dice “no evalúes m, devuelve como resultado de la expresión la función7
en sí”. En Scala, las
funciones parcialmente aplicadas son funciones a las que no se les ha proporcionado todos
sus argumentos (en el ejemplo que se ha visto, no se proporciona ninguno), y que por tanto no
tienen aún valor de retorno, sino carácter de función.
3.3.4. Funciones de primera clase
Scala incluye una de las características principales del paradigma funcional: first-class fun-
ctions o funciones de primera clase. No solamente se podrán definir funciones e invocarlas
sino que también será posible definirlas como literales para, posteriormente, pasarlas como va-
lores.
3.3.5. Funciones anónimas y funciones valor
Las funciones anónimas (también llamadas funciones literales, lambda funciones o simple-
mente lambdas8
) son compiladas en una clase que, cuando es instanciada, se convierte en una
función valor. Por lo tanto, la principal diferencia entre las funciones anónimas y las funciones
valor es que las primeras existen en el código fuente mientras que las segundas existen como
objetos en tiempo de ejecución.
A continuación se define un pequeño ejemplo de una función anónima que suma el valor 1
al número indicado:
1 val f = (x:Int) => x + 1
Las funciones valor son objetos propiamente dichos, por lo que se pueden almacenar en
variables o invocarlas mediante la notación de paréntesis habitual.
Las funciones anónimas se usan frecuentemente en programación funcional para pasarlas
como parámetros de otras funciones. Cuando se define una función anónima lo que el intérpre-
te define, de una forma totalmente transparente al programador, es un objeto con un método
llamado apply. 9
. Cuando se define una función anónima cómo (x : Int) => x + 1, lo que
realmente se crea es un objeto:
1 val f = new Function1[Int,Int] {
2 def apply (a:Int):Int = a + 1
3 }
Se observa que f tiene el tipo Function1[Int,Int], habitualmente escrito como Int => Int.
Se puede apreciar que el rasgo Function1 define un único método llamado apply. Cuando se
invoca a la función f con un valor, f(5), lo que realmente se hace es una llamada al método
apply de f:
1 f.apply(5)
7
Una vez se ha hecho la conversión de método en función
8
El nombre lambda viene del lambda cálculo,
9
En Scala los objetos que tienen un método apply pueden ser llamados como si fueran métodos
Página 58
La biblioteca estándar de Scala provee de distintos rasgos FunctionN para representar las
funciones como objetos de N argumentos.
3.3.6. Cierres
Las funciones anónimas que se han visto hasta este momento han hecho uso, única y exclu-
sivamente, de los parámetros pasados a la función. Sin embargo, se pueden definir funciones
anónimas en las que se hace uso de variables definidas en otro punto de nuestro programa:
1 (x:Int) = x * otro
La variable otro es conocida como una variable libre (free variable) puesto que la función
no le da un significado a la misma. Al contrario, la variable x es conocida como variable ligada
(bound variable) puesto que tiene un significado en el contexto de la función. Si se intenta
utilizar esta función en un contexto en el que no esté accesible una variable otro se obtendría un
error de compilación indicando que dicha variable no está disponible.
Las funciones valor creadas en tiempo de ejecución a partir de las funciones anónimas son
conocidas como cierres. El nombre se deriva del acto de “cerrar” la función anónima mediante
la captura en el ámbito de la función de los valores de sus variables libres. Una función valor
que no presenta variables libres, creada en tiempo de ejecución a partir de su función anónima
no es un cierre en el sentido más estricto de la definición dado que dicha función ya se encuentra
“cerrada” en el momento de su escritura. Veremos la verdadera importancia de los cierres dentro
de la programación funcional cuando se estudie la parcialización de funciones.
El fragmento de código anterior hace que se plantee la siguiente pregunta: ¿qué ocurre si la
variable otro es modificada después de que el cierre haya sido creado? La respuesta es sencilla:
en Scala, el cierre tiene visión sobre el cambio ocurrido. La regla anterior también se cumple
en sentido contrario: si un cierre modifica alguno de los valores capturados, estos últimos son
visibles fuera del ámbito del mismo.
3.4. Recursión
Una de las preguntas que surgen dentro del paradigma de la programación funcional es:
¿cómo se pueden escribir programas simples en los que se tengan que utilizar bucles sin reasig-
nación de variables? 10
Para escribir bucles en un lenguaje funcional se utilizará una función recursiva11
que estará
definida normalmente de forma local a otra función. Programando en un lenguaje funcional no
se utilizarán los bucles iterativos, ya que suelen utilizar vars y, principalmente, no determinan
un valor12
.
Aunque la recursión es mucho más que simular bucles ya que es una técnica esencial en
computación que permite diseñar algoritmos recursivos que dan soluciones elegantes y simples,
y generalmente bien estructuradas y modulares, a problemas de gran complejidad. Se dice que
un proceso es recursivo si se puede definir en términos de si mismo, y a dicha definición se
le denomina definición recursiva. La recursividad es una nueva forma de ver las acciones re-
petitivas permitiendo que un subprograma se llame a sí mismo para resolver una versión más
pequeña del problema original.
10
Sin utilizar bucles While, Do-While...
11
Cuando una función se llama así misma
12
La última expresión evaluada del bucle será la condición de salida del mismo
Página 59
Se puede diferenciar entre dos tipos de recursión:
Recursión directa o explícita cuando procedimiento se llama a sí mismo.
Recursión indirecta o implícita cuando un procedimiento P llama a otro Q, Q llama a
R, R llama a S, ..., y Z llama de nuevo a P
A la hora de definir una función recursiva para resolver un problema, además de definir la
relación de recurrencia, habrá que identificar el caso base, el cual permitirá conocer el valor
de la función. Esta regla es la condición de terminación.
Por tanto para utilizar recursión en la resolución de un problema habrá que asegurarse de
que se cumplen las siguientes condiciones.
Se debe poder definir en términos de una versión más pequeña del mismo problema.
En cada llamada recursiva debe disminuir el tamaño del problema.
El diseño de la solución del problema ha de ser tal que asegure la ejecución del caso base
y por tanto, el fin del proceso recursivo.
A continuación, se definirá la función recursiva sumaDesdeHasta, con dos argumentos en-
teros x e y, que devolverá como resultado la suma de los enteros comprendidos entre x e y.
Una posible definición de la función sumaDesdeHasta podría ser:
sumaDesdeHasta(x, y) =



0 si x > y
x + sumaDesdeHasta(x + 1, y) en otro caso
Esta es la definición recursiva de la función sumaDesdeHasta, ya que se define en términos
de si misma. La primera regla de la definición, o caso base, establece la condición de termina-
ción. Las definiciones recursivas permiten definir un conjunto infinito de objetos mediante una
sentencia finita.
Implementación de la función recursiva directa sumaDesdeHasta en Scala:
1 def sumaDesdeHasta(x:Int,y:Int):Int = if (x>y) 0 else x +
sumaDesdeHasta(x+1,y)
3.4.1. Importancia de la pila del sistema en recursión.
Mientras las funciones recursivas empleadas para resolver un problema no realicen un gran
número de llamadas recursivas hasta alcanzar el caso base no habrá ningún problema en apli-
car las soluciones recursivas vistas anteriormente. Cuando las funciones recursivas necesiten
realizar un gran número de llamadas recursivas hasta alcanzar el caso base, la función podría
no ser capaz de alcanzar la condición de terminación, devolviendo un error como consecuencia
del desbordamiento de pila: StackOverflowError. Por este motivo será determinante evaluar la
profundidad máxima requerida para que un algoritmo recursivo alcance la condición de termi-
nación con el objetivo de prever la cantidad de memoria necesaria.
El número de llamadas recursivas que una determinada función podrá realizar antes de que
se produzca un error por desbordamiento de pila dependerá del tamaño de la pila de Java,
definido por defecto en 1024kb13
.
13
En un sistema basado en Unix, podemos introducir desde la línea de comandos java -XX:+PrintFlagsFinal
-version | grep -i stack y conocer el tamaño de pila definido observando el valor de ThreadStackSize. El tamaño de
la pila se podrá cambiar desde la línea de comandos utilizando la opción -Xss
Página 60
3.4.1.1. La pila de Java
Según la descripción que Oracle nos ofrece de la pila de Java y de los contextos de pila,
cada thread (hilo de ejecución) tendrá una pila privada que se creará en el mismo momento que
el hilo de ejecución14
. En la pila de Java se guardará el estado del hilo de ejecución asociado
y la JVM sólo podrá realizar dos operaciones sobre dicha pila: almacenar y sacar contextos de
pila.
El método que el hilo de ejecución ejecuta en un momento dado recibe el nombre de méto-
do actual del hilo de ejecución. El contexto de pila para el método actual es llamado contexto
actual. La clase en la que se define el método actual será la clase actual, así como la agrupa-
ción de constantes actual (current constant pool) será las definida en la clase actual. Cuando
la JVM encuentra instrucciones que operen con los datos almacenados en el contexto de pila15
,
realizará dichas operaciones en el contexto actual.
3.4.1.2. Contexto de pila
Un contexto de pila consta de tres partes:
Variables locales
Pila de operandos16
Referencia al grupo de constantes17
El tamaño de las variables locales y de la pila de operandos18
dependerá, por tanto, de
las necesidades de cada método. Para cada método de una clase se determinará el tamaño del
contexto de pila necesario y se incluirá en el fichero de clase durante la compilación. Así, el
tamaño de cada contexto de pila dependerá de las variables locales, los parámetros del método
y del algoritmo empleado ya que determinará el tamaño de la pila de operandos.
Por tanto, cuando se invoca un método, se crea un nuevo contexto de pila que contendrá
información sobre ese método. Durante la ejecución del método, el código sólo podrá acceder a
los valores del contexto actual19
. Una vez finalizada la ejecución del método actual, la informa-
ción del contexto actual se sacará de la pila, por lo que el programa podrá continuar la ejecución
desde el mismo punto en el que se realizó la llamada a dicho método.
En resumen, cuando una función recursiva realiza una llamada a sí misma, la información
de la función se almacenará en la pila. Es decir, cada vez que la función se llame a sí misma,
una nueva copia de la información de la función será guardada en la pila por lo que se necesitará
un nuevo contexto de pila por cada nivel de recursión. Por tanto, por cada nivel de recursión
será necesaria más memoria. En la próxima sección se verá como la recursión de cola resuelve
el problema de la demanda de memoria de las funciones recursivas.
Considérese la siguiente definición de la función recursiva fact encargada de calcular el
factorial de un entero pasado como argumento:
14
Por tanto, en las aplicaciones en las que haya varios hilo de ejecucións, existirán varias pilas privadas asociadas
a dichos hilo de ejecucións, en concreto, tantas pilas como hilo de ejecucións.
15
Los datos almacenados en la pila de un hilo de ejecución son privados para dicho hilo de ejecución.
16
La pila de operandos será un espacio de memoria que será usado como área de trabajo dentro del contexto de
pila.
17
Contiene diferentes tipos de constates, desde literales numéricos que se conocen durante la compilación hasta
referencias a métodos que serán resueltas durante el tiempo de ejecución.
18
Se medirá en palabras (words). El tamaño de una palabra puede variar en las diferentes implementaciones de
la JVM aunque tendrá al menos 32bits por lo que se podrán almacenar datos del tipo long o double en una palabra.
19
El contexto actual estará situado en la cima de la pila privada al hilo de ejecución.
Página 61
1 def fact(n: Int):Int = {
2 var valor:Int=0
3 if (n == 0) {valor=1}
4 else {valor=n * fact(n - 1)}
5 valor
6 }
En la figura 3.1 se puede observar la evolución de la pila de Java cuando se evalúa la expre-
sión:
1 val valor = fact(4)
Figura 3.1: Evolución de la pila de Java cuando se evalúa la expresión fact(4).
3.4.2. Recursión de cola
Las funciones que se llaman a si mismas en la última sentencia de su cuerpo son llamadas
funciones recursivas de cola (tail recursive). El compilador de Scala detecta esta situación y la
optimiza, reemplazando 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.
A continuación se presenta una función recursiva que calcula la suma desde un natural dado
hasta otro natural dado:
1 def sumaDesdeHastaRec(x:Int,y:Int):Int={
2 @annotation.tailrec
3 def go(x:Int,y:Int,acc:Int):Int=
4 if (x>y) acc
5 else go(x+1,y,acc+x)
6 go(x,y,0)
7 }
Algoritmo 3.2: Recursión de cola
La anotación @annotation.tailrec le indica al compilador de Scala que la función definida a
continuación es recursiva de cola. Si se hace esta indicación y la función no fuera recursiva de
cola daría un error.
Cuando se emplea recursión de cola, el compilador de Scala traducirá el código al mismo
Página 62
grupo de bytecodes que si se hubiera utilizado un bucle while20
. El objetivo de utilizar esta
optimización para la recursión es el de evitar el error provocado por un desbordamiento de la
pila – StackOverflowError – para casos que generen un gran número de llamadas recursivas,
ya que los bucles iterativos no presentan este problema. En general, en recursión de cola, un
contexto de la pila será suficiente tanto para la función como para la llamada recursiva.
El uso de recursión de cola 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
recursión de cola. Scala únicamente optimiza llamadas recursivas a una función dentro de la
misma (recursión directa).
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:
1 def esPar(x:Int): Boolean = if(x==0) true else esImpar(x-1)
2 def esImpar(x:Int): Boolean = if(x==0) false else esPar(x-1)
Algoritmo 3.3: Recursión Indirecta
3.5. Currificación y Parcialización
3.5.1. Currificacion
Scala no incluye un excesivo número de instrucciones de control. Las típicas están definidas
y se puede llevar a cabo la definición de construcciones propias de manera sencilla.
Se analizará como se pueden definir 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: la currificación (currying21
).
Haskell B. Curry propuso trabajar sólo con funciones de un argumento. Aunque esto parece
una limitación, el truco consiste en que las funciones de más de un argumento son de orden
superior: toman un argumento y devuelven una función. La currificación hace que la parciali-
zación sea directa. El siguiente fragmento de código nos muestra una función tradicional que
recibe dos argumentos de tipo entero y retorna la suma de ambos:
1 def sumaPlana(x:Int, y:Int) = x + y
A continuación se muestra una función currificada similar a la descrita en el fragmento de
código anterior:
1 def sumaCurrificada(x:Int)(y:Int) = x + y
Cuando se evalúa la expresión sumaCurrificada(9)(2) se estarán realizando dos llamadas
tradicionales de manera consecutiva. La primera invocación recibe el parámetro x y devuelve
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 primera que lleva a cabo lo que haría la primera de las
invocaciones de la función sumaCurrificada anterior:
20
Siempre y cuando la llamada recursiva sea la última instrucción (esté en la posición de cola o tail position)
21
Se denomina así, currying en honor a su descubridor, Haskell B. Curry (aunque Moses Schönfinkel la propuso
antes, el término “Schöenfinkelization” no terminaría de popularizarse)
Página 63
1 def primera(x: Int) = (y: Int) => x + y
Invocando a la función anterior con el valor 1 se obtendría una nueva función:
1 def segunda = primera(1)
La invocación de la función segunda con el parámetro 2 devolvería como resultado 3.
scala>segunda(2)
res: Int = 3
3.5.2. Parcialización
La parcialización es la aplicación parcial de una función, es decir, una invocación que
recibe menos argumentos de los que espera. La aplicación parcial de una función producirá
como resultado una función anónima que será una versión especializada de la función original.
La parcialización de una función permitirá pasar sólo algunos argumentos a una función,
obteniendo como resultado una función anónima. Los argumentos que no le serán pasados a una
función se indicarán utilizando _ (guión bajo).Esta función anónima devuelta será un cierre.
En Scala es posible parcializar tanto funciones currificadas, como funciones no currificadas,
así como parcializar cualquier argumento de las mismas.
Para entender bien la parcialización se definirán dos funciones: suma y sumaC. Ambas
devolverán como resultado la suma de los dos parámetros enteros que se les pase. La única
diferencia entre ellas es que sumaC es una función currificada:
1 def suma(x:Int,y:Int):Int = x + y
2 def sumaC(x:Int)(y:Int) = x + y
Si se quisiera implementar una nueva función que sume 5 a un valor entero dado, se podría
definir cualquiera de las siguientes funciones parcializando la función suma.
1 val suma5:Int=>Int = suma(5,_:Int)
2 val suma5b = suma(_:Int,5)
Se puede apreciar que se ha definido la función suma5 parcializando sobre (y), que es el
segundo argumento de la función suma, mientras que la función suma5b se ha definido parcia-
lizando sobre el primer argumento de suma (x).
En cambio si se quisiera definir la misma función a partir de sumaC:
1 val suma5C:Int=>Int = sumaC(5)
2 val suma5Cb = sumaC (_:Int)(5)
En el ejemplo anterior se advierte que se ha definido la función suma5C parcializando sobre
el segundo argumento de la función sumaC (y) mientras que la función suma5Cb se ha definido
parcializando sobre el primer argumento de sumaC (x).
Finalmente, también es factible hacer una parcialización total de la función suma o sumaC,
como se puede ver en el siguiente ejemplo:
1 val sumaP:(Int,Int)=>Int=suma _
2 val sumaCP = sumaC _
Página 64
En este caso sí se puede observar una diferencia clara, atendiendo al tipo de datos devuelto
por ambas funciones. El tipo devuelto por sumaP es: (Int, Int) => Int y el tipo de sumaCP
es: Int => (Int => Int), como => tiene asociatividad derecha es equivalente a: Int =>
Int => Int. Mediante la parcialización total se pueden definir funciones a partir de métodos.
Aunque se pueda parcializar sobre cualquier argumento, aplicar la parcialización sobre los
primeros argumentos de funciones currificadas es una buena práctica que mejorará el entendi-
miento y la legibilidad del código.
La parcialización en los operadores
Como se estudió en la Subsubsección 1.2.2.2: Operadores « página 10 », los operadores
son en realidad métodos y, por tanto, se reducen a la llamada a un método de un objeto de una
de clases de los tipos de datos. Además, en la Subsección 3.3.3: Diferencias entre métodos y
funciones « página 57 », se vieron las principales diferencias entre métodos y funciones y cómo
en la mayoría de los casos se podrían tratar métodos y funciones indistintamente. Por tanto, se
podrán obtener funciones anónimas parcializando sobre los argumentos de los operadores:
1 def por3 = 3 * (_:Int)
2 def por4:Int=>Int = _ * 4
En el ejemplo anterior se han definido las funciones por3 y por4. Se puede observar dos
diferencias notables en la definición de ambas funciones.
Aunque ambas funciones han sido definidas parcializando el operador * definido en el tipo
de datos Int, la función por3 ha sido definida parcializando sobre el segundo operando y
la función por4 sobre el primer operando.
En la función por3 se ha especificado el tipo del operando parcializado mientras que en
la función se ha indicado el tipo de la función por4.
3.6. Orden Superior
3.6.1. Funciones de orden superior
En Scala, las funciones son objetos también, por lo que podrán ser pasadas como cualquier
otro valor, ser asignadas a variables, almacenadas en estructuras de datos...Por tanto, Scala per-
mite la definición de funciones de orden superior, es decir, funciones que toman otras funciones
como parámetros o que devuelven una función, y que permiten hacer mucho más conciso y
general el código escrito en Scala.
El orden superior es una técnica que permitirá abstraer funciones y pasarlas como paráme-
tros, lo que proporcionará al programador una forma flexible de componer programas.
A continuación se estudiará la importancia del orden superior en los lenguajes funcionales y
cómo facilita la reutilización de código, haciendo más simple la escalabilidad de los programas.
Para comenzar, se definirán dos funciones, sumaDesdeHasta y productoDesdeHasta, las
cuales calcularán la suma y el producto del intervalo comprendido entre los números enteros
pasados como parámetros, respectivamente.
Página 65
1 def sumaDesdeHasta(x:Int,y:Int):Int = if (x>y) 0
2 else x + sumaDesdeHasta(x+1,y)
3 def productoDesdeHasta(x:Int,y:Int):Int = if (x>y) 1
4 else x * productoDesdeHasta(x+1,y)
En el ejemplo anterior se puede observar la similitud entre ambas funciones. Lo único que
las diferencia es el valor devuelto cuando (x > y) (caso base) y la operación (suma o producto)
del caso recursivo.
Sería posible abstraer la operación(suma y producto) y el caso base, y pasarlos como argu-
mentos.
1 def operaN(f:(Int,Int)=>Int,e:Int)(x:Int,y:Int):Int= if (x>y) e
2 else f(x,operaN(f,e)(x+1,y))
Ahora si se invoca a la función:
scala> operaN(_+_,0)(1,10)
res15: Int = 55
Se obtiene el mismo resultado que si se invocara la función sumaDesdeHasta(1,10). Obsér-
vese también que la función operaN espera una función del tipo (Int,Int) =>Int en su argumento
f. En el ejemplo anterior se le ha pasado dicho argumento haciendo uso de la parcialización de
operadores (vista en la Sección 3.5.2: La parcialización en los operadores « página 65 »), con-
cretamente parcializando el operador + del tipo de datos Int.
También se podría calcular el producto con operaN:
scala> operaN(_*_,1)(1,5)
res16: Int = 120
Se puede comprobar que el resultado coincide con la llamada a la función productoDes-
deHasta(1,5). Pero además es posible realizar otros cálculos, como por ejemplo, calcular la
suma del cuadrado de los enteros comprendidos entre un intervalo dado:
scala>operaN((x,y)=>x*x+y,0)(1,5)
res17: 55
3.7. Funciones polimórficas. Genericidad
Al igual que se vio en la Sección 2.5: Polimorfismo en Scala « página 45 » cómo crear
clases genéricas parametrizando las mismas, ahora se estudiará como utilizar la genericidad
para construir funciones polimórficas.
Hasta el momento las funciones que se han definido son funciones monomórficas, es decir,
funciones que operan con un tipo de datos concreto. Por ejemplo, la función swapInt que dados
un par de elementos enteros devuelve una tupla con los elementos intercambiados:
1 def swapInt(a:Int,b:Int):(Int,Int)=(b,a)
En ocasiones, estos tipos resultan demasiado restrictivos y si se necesitara intercambiar un
par de elementos de tipo Float, Double,...habría que definir otra versión de la función swapInt
para este tipo concreto:
1 def swapFloat(a:Float,b:Float):(Float,Float)=(b,a)
Página 66
Una vez más se puede apreciar la similitud de las funciones swapInt y swapFloat. En estas
situaciones se desearía poder abstraer el tipo de la función ya que no influye en el cálculo de la
misma. Para poder abstraer el tipo de una función no se podrá recurrir a las funciones de orden
superior ya que éstas abstraen operaciones no tipos.
Para abstraer el tipo de una función se usará polimorfismo22
.El polimorfismo servirá para
introducir variables de tipo23
. Se dirá que una función cuyo tipo sea polimórfico será una
función polimórfica. Es posible asignar el nombre que se desee a las variables de tipo, así
[Animal, NumerosPrimos, . . . ] serían nombres válidos aunque por convenio se utilizarán
variables de una sola letra [A, B, C]
Para definir una función polimórfica en Scala se introducirá una lista de las variables de
tipo, separadas por comas y encerrada entre corchetes después del nombre de la función. Ej.
defswap[A, B](. . .) : . . .
Las variables de tipo que forman esta lista podrán ser referenciadas en el resto de la defini-
ción de la signatura de la función, así como en el cuerpo de la función (del mismo modo que
las variables pasadas como argumentos a una función pueden ser utilizadas en el cuerpo de la
misma).
Haciendo uso del polimorfismo, se definirá una función swapGen que, pasados cualesquiera
dos parámetros, devolverá una tupla con los parámetros intercambiados.
1 def swapGen[A](a:A,b:A):(A,A)=(b,a)
En este ejemplo se puede ver que se ha definido una variable de tipo, A. Todas las referencias
de tipo de la signatura de la función SwapGen hace referencia al tipo A por lo que se estaría
forzando a que el tipo de los dos argumentos pasados a la función sea el mismo.
Si los argumentos de la función no fueran del mismo tipo, Scala tratará de buscar un super-
tipo común para el parámetro de tipo A hasta llegar al tipo Any, supertipo de todos los tipos.
scala>swapGen("hola",2.3)
res4:(Any,Any) =(2.3,"hola")
A pesar de esta característica de Scala que permitiría que la función swapGen cumpla con
uno de los propósitos de la misma, el resultado no es el esperado ya que se esperaba una tupla
del tipo (Double,String) en lugar de una tupla del tipo (Any,Any). La función será más genérica
y estará definida con más rigor si se le indica a Scala que ambos tipos pueden ser distintos.
1 def swap[A,B](a:A,b:B):(B,A)=(b,a)
Ahora si se invoca la función swap con los parámetros anteriores si se obtendría una tupla
con los tipos esperados y con los parámetros invertidos.
scala>swap("hola",2.3)
res4:(Double,String) =(2.3,"hola")
22
Usaremos la palabra polimorfismo con un significado algo distinto al empleado en POO donde suele con-
notar algún tipo de subtipado. El poliformismo empleado en programación funcional también es llamado como
polimorfismo paramétrico y en otros paradigmas de programación se denomina genericidad
23
Las variables de tipo también suelen llamarse parámetros de tipo
Página 67
3.8. Ejercicios
3.8.1. Ejercicio Resuelto
Ejercicio 1 Desde 1990 el peso se categoriza mediante el cálculo del Índice de masa corpo-
ral (IMC)24
. El cálculo del IMC corporal es fácil ( peso(kg)
altura2(metros)
)
Este no es válido para personas con una altura inferior a 1,47 metros o superior a 1,98 m,
para menores de 18 años o para atletas de élite (que tienen mucha masa muscular).
Se pide definir una función imcVal que calcule el IMC de un individuo.
1 def square(x:Double)=x*x
2 def imcVal(weight:Double,height:Double):Double={
3 require(height>=1.47 && height <= 1.98)
4 weight/square(height)
5 }
Ejercicio 2 La clasificación desde 199025
para adultos según su IMC es: < 18.5 (bajo peso);
18.5-24.9 (normal-normopeso); 25.0-29.9 (sobrepeso); > 30 (obesidad).
Definir una función imcClas que dados el peso y la altura de un individuo adulto como
argumentos, nos devuelva su clasificación en función de su IMC.
Primera aproximación, función imcClas1:
1 def icmClas1(p:Double,a:Double):String={
2 val imcValor=imcVal(p,a)
3 if (imcValor < 18.5) "Bajo Peso"
4 else if (18.5<imcValor && imcValor<24.9) "Peso Normal"
5 else if (24.9<imcValor && imcValor<30) "Peso Normal"
6 else "Obesidad"
7 }
La función imcClas1 daría el resultado deseado pero no hace uso de las ventajas que ofrece
la programación funcional. Se podrían desacoplar las condiciones del If en funciones del tipo
Double => Boolean y así ganar tanto en legibilidad del código como en reusabilidad del
mismo. A continuación se verá una nueva aproximación a la solución final:
24
Ideado por el estadístico belga Adolphe Quetelet, por lo que también se conoce como índice de Quetelet.
25
http://www.ncbi.nlm.nih.gov/mesh?term=body%20mass%20index
Página 68
1 def bajoPeso(imcValue:Double):Boolean = 0<imcValue && imcValue < 18.5
2 def pesoNormal(imcValue:Double):Boolean = 18.5 <= imcValue &&
imcValue < 24.9
3 def sobrePeso(imcValue:Double):Boolean = 24.9 <= imcValue &&
imcValue < 30
4 def obesidad(imcValue:Double):Boolean = 30 <= imcValue && imcValue <
100
5
6 def icmClas2(p:Double,a:Double):String={
7 val imcValor=imcVal(p,a)
8 if (bajoPeso(imcValor)) "Bajo peso"
9 else if (pesoNormal(imcValor)) "Peso Normal"
10 else if (sobrePeso(imcValor)) "SobrePeso"
11 else "Obesidad"
12 }
Ahora se puede observar como la función encargada de clasificar los individuos según su
ICM queda mucho más clara. Además se podrán utilizar las “funciones auxiliares” definidas en
un futuro. Si se presta atención a dichas funciones auxiliares, se puede advertir que si se abstrae
el límite inferior y el límite superior del cada rango, junto con el valor que se está comparando
se podría crear una función de orden superior con la que poder definir todas.
1 def peso_estaEntre(low:Double,high:Double)(value:Double):Boolean =
low<value && value< high
2 def bajoPesoIMC:Double=>Boolean = peso_estaEntre(0,18.5)
3 def normalPesoIMC:Double=>Boolean = peso_estaEntre(18.5,25)
4 def sobrePesoIMC:Double=>Boolean = peso_estaEntre(25,29.9)
5 def obesidadIMC:Double=>Boolean = peso_estaEntre(30,100)
6
7 def imcClassificator(imcValue:Double):String = {
8 if (bajoPesoIMC(imcValue)) "Bajo peso"
9 else if (normalPesoIMC(imcValue)) "Peso normal"
10 else if (sobrePesoIMC(imcValue)) "Sobrepreso"
11 else "Obesidad"
12 }
13 def imcClas(weight:Double,height:Double) =
imcClasificator(imcVal(weight,height))
En la solución anterior se ha definido la función de orden superior peso_estaEntre, currifica-
da para facilitar la parcialización de la misma (dándole los límites inferior y superior del rango)
en cada una de las definiciones de las funciones bajoPesoIMC, normalPesoIMC,...y dejando la
variable value libre en cada una de los cierres generados con la definición de estas funciones.
Finalmente se define la función imcClas, objetivo del ejercicio:
1 def imcClas(weight:Double,height:Double):(String) =
imcClasificator(imcVal(weight,height)))
Ejercicio 3 Definir una función imc que dada la altura y el peso de un individuo devuelva
una tupla con su IMC y su clasificación.
Página 69
1 def imc(weight:Double,height:Double):(Double,String) = {
2 val imcValor=imcVal(weight,height)
3 (imcValor,imcClasificator(imcValor))
4 }
Ejercicio 4 En un estudio presentado en 2011, se ha observado que un tercio de las personas
con peso normal serían en realidad obesas si en lugar de éste, se midiera su grasa corporal total.
Aunque el exceso de adiposidad es el verdadero culpable de las complicaciones asociadas a
la obesidad y no el exceso de peso, los estudios que examinan los riegos para la salud asociados
a la obesidad en los que se mide realmente la adiposidad son menos usuales de lo deseado. El
porcentaje de grasa corporal puede medirse usando diferentes técnicas (análisis de impedancia
bioeléctrica...).
Cuando no es posible determinar el porcentaje de grasa corporal(BF %) se suele recurrir al
IMC para medir la adiposidad. Cómo se ha visto con anterioridad, el cálculo del IMC corporal
es fácil (peso/altura2
) aunque no refleja precisamente la grasa corporal ya que los cambios
en la composición del cuerpo que tienen lugar a lo largo de los diferentes periodos de la vida
(edad) o el sexo de la persona, son variables que influyen notablemente en el cálculo del BF.26
En 2011 se publicó un nuevo estimador denominado CUN-BAE27
del porcentaje de BF que
tenía en cuenta la edad, el sexo, la altura y el peso del individuo que se estaba estudiando:
1 BF % = - 44.988 + (0.503 * age) + (10.689 * sex) + (3.172 * BMI) -
(0.026 * BMI2) + (0.181 * BMI * sex) - (0.02 * BMI * age) -
(0.005 * BMI2 * sex) + (0.00021 * BMI2 * age)
Definir en Scala la función CUN-BAE:
1 //sex=1=>mujer, sex=0=>hombre
2 def cun_bae (age:Double, sex:Double, weight:Double,
height:Double):Double = {
3 require(sex==0 || sex==1)
4 def bf(age:Double, sex:Double, weight:Double, height:Double,
bmiValue:Double, bmiValue2:Double) : Double = {
5 - 44.988 + (0.503 * age) + (10.689 * sex) + (3.172 *
bmiValue) - (0.026 * bmiValue2) + (0.181 * bmiValue *
sex) - (0.02 * bmiValue * age) - (0.005 * bmiValue2 *
sex) + (0.00021 * bmiValue2 * age)}
6
7 val imc_val= imcVal(weight,height)
8 bf(age, sex, weight, height, imc_val, square(imc_val))
9
10
11 }
Ejercicio 5 Definir una función bfClas a la que se le pasarán como argumentos la edad, el
sexo, el peso y la altura de un individuo y nos devuelva su clasificación basado en la predicción
del porcentaje de grasa corporal calculado con la función CUN-BAE.
Clasificación basada en BF%
26
Más información sobre este tema: Body mass index classification misses subjects with increased cardiometa-
bolic risk factors related to elevated adiposity. Int J Obes 2011;in press.
27
Autores del predictor CUN-BAE: Gómez-Ambrosi J, Silva C, Galofré JC, Escalada J, Santos S, Millán D,
Vila N, Ibañez P, Gil MJ, Valentí V, Rotellar F, Ramírez B, Salvador J, Frühbeck G.)
Página 70
normal => < 20% en hombres y < 30% en mujeres
sobrepeso => 20%-25% en hombres y 30%-35% mujeres
obesos => > 25% en hombres y > 35% en mujeres
1 def normalPesoBF(sex:Double):Double=>Boolean =
peso_estaEntre(0+10*sex,20+10*sex)
2 def sobrepesoBF(sex:Double):Double=>Boolean =
peso_estaEntre(20+10*sex,25+10*sex)
3 def obesidadBF(sex:Double):Double=>Boolean =
peso_estaEntre(25+10*sex,100)
4
5 def bfClas(age:Double,sex:Double,weight:Double,height:Double):String
= {
6 def bfClassificator(bfValue:Double):String={
7 if (normalPesoBF(sex)(bfValue)) "Peso normal"
8 else if (sobrepesoBF(sex)(bfValue)) "Sobrepreso"
9 else "obesidad"
10 }
11 bfClassificator(cun_bae(age,sex,weight,height))
12 }
Ejercicio 6 Define una función bf que dada la edad, el sexo, la altura y el peso de un individuo
devuelva una tupla con su BF (calculado con el método Cun-Bae) y su clasificación.
1 def bf(age:Double,sex:Double,weight:Double,height:Double) =
(cun_bae(age,sex,weight,height),bfClas(age,sex,weight,height))
3.8.2. Ejercicios
Responder a las siguientes cuestiones:
Ejercicio 2. Considérese el siguiente fragmento de código:
1 var a = 1
2 a = a + 1
En sentido estrictamente matemático, ¿se podría pensar en a como una variable, teniendo en
cuenta la sentencia a = a + 1?
Sí
No
Ejercicio 3. Si se define una función fun tal que:
1 def fun(x: Int) = x + x
¿Es “fun” una función pura?
Sí
No
Página 71
Ejercicio 4. Considérese la función getTime la cual devuelve la hora actual. ¿Es getTime una
función pura?
Sí
No
Ejercicio 5. Considérese la función random la cual devuelve un número aleatorio. ¿Es random
una función pura?
Sí
No
Ejercicio 6. Si se define una función f tal que:
1 def f() = {
2 10
3 20
4 }
¿Es f una función pura?
Sí
No
Ejercicio 7. Si se define una función fun tal que:
1 def fun(f: Int=>Int, x: Int) = f(x)
¿Cuál es el tipo del valor devuelto por fun?
Int
Int =>Int
Ninguna de las respuestas anteriores es correcta.
¿Cuál será el resultado de evaluar la expresión fun(x =>x, 10)?
10
Error de compilación
Ejercicio 8. Considérense las siguientes soluciones para el cálculo del factorial de un número:
1 def fact1(n: Int) = {
2 var f = 1
3 var t = n
4 while (t > 0) {
5 f = f * t
6 t = t - 1
7 }
8 f
9 }
10
11 def fact2(n: Int):Int =
12 if (n == 0) 1 else n * fact2(n - 1)
Página 72
¿Cuál de las dos presenta un aspecto más “matemático”?
Razonando sobre la corrección de las dos funciones y teniendo en cuenta el flujo de
control y la secuencia de las operaciones:
• ¿Cuál de las dos versiones presenta un problema de corrección?
• ¿Cómo se solucionaría?
Ejercicio 9. Si se tiene la función fun definida tal que:
1 def fun(f: Int => String, g: String => Int, x: Int) = g(f(x))
¿Cuál es el tipo del valor devuelto por fun?
• String
• Int
¿Cuál el resultado de evaluar la expresión fun(x =>10, y =>"hola", 20)?
• Error de compilación
• 10
• 20
¿Y el resultado de evaluar fun(x =>"hola", y =>15, 20)?
• Error de compilación
• 15
• 20
¿Y el resultado de evaluar fun(x =>"hello", y =>10, "20")?
• Error de compilación
• 10
• 20
Ejercicio 10. Definida la función fun tal que:
1 def fun(x: Int, y: Int):Int=>Int =
2 z => (x + y) + z
¿Cuál será la salida de println(fun(1,2)(3))?
Dará error
3
5
6
Ejercicio 11. Dada la función fun tal que:
Página 73
1 def fun(x: Int):(Int, Int)=>Int =
2 (y, z) => x + y + z
¿Cuál de las siguientes expresiones dará error?
fun(1,2)(3)
fun(1)(2,3)
Ejercicio 12. Dadas las funciones fun1 y fun2 definidas a continuación:
1 def fun1():Int => Int = {
2 val y = 1
3 def add(x: Int) = x + y
4
5 add
6 }
7
8 def fun2() = {
9 val y = 2
10 val f = fun1()
11 println(f(10))
12 }
¿Qué se imprimirá por pantalla si se invoca a fun2()?
10
11
1
Ninguna de las anteriores respuestas es correcta
Ejercicio 13. Dado el siguiente fragmento de código:
1 def cuadrado(x: Int) = x*x
2 def cubo(x: Int) = x*x*x
3
4 def componer(f: Int=>Int, g: Int=>Int): Int=>Int =
5 x => f(g(x))
6
7 val f = componer(cuadrado, cubo)
8 val a=List(4,1,3,4,7,8,9,10)
¿Es cierta la igualdad a.map(f) == a.map(cuadrado).map(cubo)?
Sí
No
Ejercicio 14. Dadas las siguientes definiciones de:
Página 74
1 def hello() = {
2 println("hello")
3 10
4 }
5
6 def fun(x: => Int) = {
7 x + x
8 }
¿Cuál será el resultado de evaluar la expresión val t = fun(hello())?
• 10
• 20
• Error
¿Qué se mostraría por pantalla si se evalúa la expresión anterior?
Ejercicio 15. Definir una función que devuelva el número de dígitos que tiene un valor entero
pasado como argumento.
Ejercicio 16. Definir la función aprueba que tome como parámetro una lista de enteros con las
calificaciones de los alumnos y apruebe con un 5 a aquellos que tengan una calificación inferior.
Ejercicio 17. Definir la función eliminaBajos que tome como parámetros una lista de enteros
xs y un valor entero cota y devuelva una lista de enteros eliminando los elementos menores que
la cota.
Ejercicio 18. Definir una función que devuelva un entero con los dígitos del parámetro entero
en orden inverso, sabiendo que el valor del parámetro tiene que ser mayor que cero.
Ejercicio 19. Definir una función que indique si el valor entero pasado como argumento es
capicúa(true) o no(false).
Ejercicio 20. Definir una función para calcular los número de Fibonacci que sea recursiva de
cola.
Ejercicio 21. Definir la función descomponer que convierta un número entero, pasado por
parámetro, de segundos en horas, minutos y segundos.
Ejercicio 22. Definir una función mcd que calcule el máximo común divisor de dos números
pasados como argumentos y la función coprimos que nos diga si dos números pasados como
argumentos son coprimos.
Ejercicio 23. A la siguiente representación de números se le conoce como el “Triángulo de
Pascal”:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
. . .
Página 75
Los números de los extremos de cada fila son 1 y el resto de elementos se calculan sumando los
dos números situados justamente encima.
Se pide:
Escribir una función que tome la columna y la fila y nos devuelva el número que corres-
ponde a esa posición (para optimizar la función se deberá tener en cuenta que tanto la
columna, como la fila no podrán ser números negativos y que la columna debe de ser me-
nor o igual que la fila). Calcular los números del triángulo de Pascal de forma recursiva.
Ejercicio 24. Escribir una función que verifique que los paréntesis de una cadena pasado co-
mo parámetro están balanceados. Implementar la misma función si el parámetro es del tipo
List[Char](sin hacer uso de los métodos de List).
3.9. Programación funcional estricta y perezosa
3.9.1. Funciones estrictas y no estrictas
Se dirá que una expresión no termina si la evaluación de la misma se ejecuta de forma
indefinida o lanza un error en lugar de devolver un valor.
En la Sección 1.4: Evaluación en Scala « página 18 » se pudo ver cómo los lenguajes funcio-
nales pueden presentar diferentes estrategias a la hora de evaluar sus expresiones y que también
se empleaban para evaluar las funciones.
Se llamarán funciones estrictas a aquellas funciones que utilizan una estrategia de evalua-
ción estricta para evaluar sus argumentos. Por tanto, una de las características de las funciones
estrictas es que siempre evalúan sus argumentos antes de comenzar con la evaluación del cuerpo
de la función. Dependiendo del lenguaje de programación, la definición de funciones estrictas
puede ser una constante en programación funcional28
. Es más, muchos lenguajes funcionales no
proveen una forma de definir funciones que no sean estrictas. La gran mayoría de las funciones
definidas hasta el momento son estrictas, como por ejemplo la función valor absoluto definida
en el algoritmo 1.6, en la página 22, que se muestra a continuación:
1 def abs(x: Int) = if (x > 0) then x else -x
Algoritmo 3.4: Función estricta valor absoluto
Si se invoca la función abs con el argumento (75 - 20) nos devolverá el valor 55. En cambio,
si se invoca la función con el argumento (sys.error("fallo")) devolverá una excepción, ya qué la
expresión sys.error("fallo") se evaluará antes que el cuerpo de la función.
Las funciones no estrictas son aquellas que no evalúan sus argumentos, es decir, que siguen
la estrategia evaluación no estricta para la evaluación de sus argumentos. Por tanto, como se
vio previamente en la Sección 1.4: Evaluación en Scala « página 18 », una función puede ser
estricta o no en cada argumento ya que Scala provee una sintaxis (=>) especial para indicar qué
argumentos no serán evaluados y que el compilador se asegurará de que sean pasados al cuerpo
de la función sin haber sido evaluados previamente.
Ejemplo de función no estricta:
1 def hacerPar(x: => Int):(Int,Int) = (x,x)
Algoritmo 3.5: Ejemplo de función no estricta
28
No será una constante en lenguajes de programación funcional como Haskell, Lean o Miranda.
Página 76
Uno de los inconvenientes que presenta la estrategia de evaluación evaluación no estricta
y que, por tanto, presentan las funciones no estrictas, es que los argumentos se han de evaluar
cada vez que se hace referencia a ellos en el cuerpo de la función. Este comportamiento se
ha ejemplificado invocando la función hacerPar definida en el algoritmo 3.5 con el bloque
println("hola");1+41;:
scala> hacerPar { println("hola"); 1 + 41;}
hola
hola
res4: (Int, Int) = (42,42)
En el ejemplo anterior se puede observar como el efecto colateral de imprimir por pantalla
el mensaje “hola” se produce dos veces, una por cada vez que se tiene que evaluar dicho bloque,
ya que aparecen dos referencias a x en el cuerpo de la función.
Cuando se desea que no se tengan que evaluar los argumentos cada vez que sean referencia-
dos en el cuerpo de una función no estricta, entonces se tendrá que emplear una estrategia de
evaluación conocida como evaluación perezosa (Call by Need). Esta estrategia de evaluación,
que siguen lenguajes de programación funcional como Haskell, es tal que, una vez evalúe por
primera vez el argumento, el valor de dicho argumento será guardado para que, si posterior-
mente se hace una referencia al mismo valor, no sea necesario volver a evaluarlo. En Scala se
puede utilizar la palabra reservada lazy para indicar que la evaluación de una val se poster-
gue hasta encontrar la primera referencia. Después el valor será almacenado para evitar más
reevaluaciones.
1 def hacerPar2(x: => Int):(Int,Int) = {lazy val j=x;(j,j)}
Algoritmo 3.6: Ejemplo de función no estricta con estrategia Call By Need
Se puede ver cómo cambia el comportamiento de la función hacerPar2 si se invoca con el
mismo bloque que se invocó previamente la función hacerPar del algoritmo 3.5:
scala> hacerPar2{ println("hola"); 1 + 41;}
hola
res6: (Int, Int) = (42,42)
En realidad, aunque se han utilizado las estrategias de evaluación no estricta y evaluación
perezosa, las funciones hacerPar y hacerPar2 serían estrictas29
, ya que siempre evalúan todos
sus argumentos. Se puede ver un ejemplo de función no estricta, implementando la selectiva if
de la siguiente forma:
1 def if2[A](cond: Boolean, cierto: => A, falso: => A): A =
2 if (cond) cierto else falso
Algoritmo 3.7: Selectiva if como función no estricta
Se podría realizar la siguiente invocación de la función if2 (false, sys.error(“error”), 4),
obteniendo el resultado:
scala> if2(false, sys.error("error"), 4)
res8: Int = 4
Se puede apreciar que el segundo argumento no llega a evaluarse, por lo que no se produce
el error.
El uso de las diferentes estrategias de evaluación ofrece al programador una gran capacidad
a la hora de separar la descripción de una expresión y la evaluación de la misma, por ejemplo,
escribiendo expresiones de las que sólo se evalúe una parte.
29
Ambas funciones tiene como valor devuelto una tupla y las tuplas en Scala son estrictas.
Página 77
3.10. Estructuras de Datos
3.10.1. Introducción
3.10.1.1. ¿Qué es una teoría?. Definición de Estructuras de Datos
La teoría de tipos hace referencia al diseño, análisis y estudio de los sistemas de tipos,
por tanto, las teorías serán empleadas para definir las estructuras de datos y fundamentalmente
consisten en:
Uno o más tipos de datos.
Operaciones definidas entre esos tipos de datos.
Reglas que describen las relaciones entre los valores y las operaciones.
Normalmente, una teoría no describe mutaciones, por tanto, a la hora de definir una teoría
habrá que concentrarse en:
Definir teorías para los operadores.
Minimizar los cambios de estado
Tratar a los operadores como funciones, en ocasiones compuestos de funciones simples.
3.10.1.2. La abstracción en la programación
La abstracción es un mecanismo fundamental para la comprensión de fenómenos o situa-
ciones que implican gran cantidad de detalles. La idea de abstracción es uno de los conceptos
más potentes en el proceso de resolución de problemas. Se entiende por abstracción la capacidad
de manejar un objeto (tema o idea) como un concepto general, sin considerar la enorme canti-
dad de detalles que pueden estar asociados con dicho objeto. Sin abstracción no sería posible
manejar, ni siquiera entender, la gran complejidad de ciertos problemas.
En el proceso de programación, se puede extender el concepto de abstracción tanto a las
acciones que debe realizar un programa mediante la técnica “Divide y Vencerás”30
(resolviendo
cada subproblema en un subprograma independiente), como a los datos, mediante tipos abstrac-
tos de datos.
En el proceso de abstracción aparecen dos aspectos complementarios:
Identificar los detalles esenciales del problema.
Ignorar los aspectos no esenciales para la resolución del problema.
30
El término “Divide y Vencerás”, en su acepción más amplia, es algo más que una técnica de diseño de algo-
ritmos. De hecho, suele ser considerada una filosofía general para resolver problemas y de aquí que su nombre no
sólo forme parte del vocabulario informático, sino que también se utiliza en muchos otros ámbitos. En el ámbito
de la informática “Divide y Vencerás” es una técnica de diseño de algoritmos que consiste en resolver un problema
a partir de la solución de subproblemas del mismo tipo, pero de menor tamaño. Si los subproblemas son todavía
relativamente grandes se aplicará de nuevo esta técnica hasta alcanzar subproblemas lo suficientemente pequeños
como para ser solucionados directamente[31].
Página 78
3.10.1.3. Datos, Tipos de Datos, Estructuras de Datos y Tipos Abstractos de Datos
No se debe confundir los conceptos de: tipo de datos, estructura de datos y tipo abstracto
de datos. Todos ellos constituyen diferentes niveles en el proceso de abstracción referido a los
datos.
Los datos son las propiedades o atributos (cualidades o cantidades) sobre hechos u objetos
del problema que queremos resolver. Dependiendo de las propiedades, será tarea del programa-
dor determinar el tipo de datos más apropiado para definir cada una de ellas.
El tipo de datos, en un lenguaje de programación, define el conjunto de valores que una
determinada variable puede tomar, así como las operaciones aplicables sobre dicho conjunto.
Definen cómo se representa la información y cómo se interpreta.
Los tipos de datos, atendiendo a su naturaleza, pueden ser clasificados en:
Tipos de datos simples31
. Son aquellos que almacenan un único valor.
Tipos de datos compuestos. Son aquellos que almacenan una agregación o colección de
valores.
Los tipos de datos, atendiendo a su definición, pueden ser clasificados en:
Tipos de datos primitivos. Son aquellos que son proporcionados por el lenguaje de pro-
gramación y que, por tanto, no necesitan ser definidos por el usuario32
.
Tipos de datos definidos por el usuario. Aquellos tipos de datos definidos a partir de
los tipos de datos primitivos.
Los tipos de datos constituyen un primer nivel de abstracción, ya que no se tiene en cuenta
cómo se representa la información, ni cómo se manipula. Sólo se hará uso de las operaciones
proporcionadas para cada tipo de datos.
Se pueden definir las estructuras de datos como agrupaciones de datos que guardan alguna
relación entre si y sobre las que se definirán operaciones.
Las estructuras de datos las podemos clasificar:
Según su naturaleza:
• Homogéneas. Formadas por elementos del mismo tipo de datos.
• Heterogéneas. Formadas por elementos de varios tipos de datos.
Según su relación:
• Lineales. Las estructuras lineales se caracterizan por el hecho de que sus elementos
están en secuencia, relacionados en forma lineal, es decir, la relación que se esta-
blece entre los elementos de la estructura de datos es de sucesión y precedencia
(independientemente de cual sea el elemento de información). Ejemplos de estruc-
turas de datos lineales son las colas, las pilas,...33
31
También llamados tipos de datos elementales.
32
Los tipos de datos pueden ser primitivos y no elementales, como por ejemplo los conjuntos.
33
La lista doblemente enlazada es una estructura de datos lineal en la que cada elemento está relacionado con dos
elementos (tiene dos punteros): el sucesor y el predecesor. Si se elimina una de estas relaciones se transformaría
en una lista simple. En cambio, si en una estructura no lineal en la que cada elemento se relaciona con otros dos
elementos, como por ejemplo ocurre en un Árbol binario de búsqueda (ABB), eliminamos una de estas relaciones,
destruiríamos dicha estructura.
Página 79
• No lineales. Estructuras de datos que presentan una organización complementaria a
las estructuras de datos lineales en la que cada elemento puede estar relacionado con
múltiples diferentes elementos de la estructura mediante una organización no lineal.
Ej. Árboles, Grafos...
Al igual que ocurría con los tipos de datos, es posible usar estructuras de datos proporcio-
nadas por el lenguaje de programación o definir nuevas estructuras de datos.
Las operaciones permitidas sobre una estructura de datos es otra de las características pro-
pias de las mismas. Algunas operaciones típicas sobre estructuras de datos son:
Buscar y acceder a los elementos (por la posición que ocupan en la estructura o por la
información que contienen),
Insertar o borrar elementos,
Modificar las relaciones entre los elementos, etc.
Las estructuras de datos serían un nuevo nivel de abstracción sobre los datos, donde ya no
importará las operaciones definidas sobre los elementos de la estructura sino las operaciones
que implican a la estructura34
.
Un Tipo abstracto de datos (TADs) oculta al usuario los detalles de su implementación,
mostrando sólo la interfaz de acceso a los datos del tipo, también llamada signatura, junto a una
semántica de las operaciones[25].
Como se ha visto anteriormente, un tipo consta de:
Un conjunto de valores.
Un conjunto de operaciones.
Se tendrán que representar los distintos valores del tipo en función de otros tipos simples o
de otros tipos de datos compuestos ya creados. Además, habrá que implementar el conjunto de
operaciones que se hayan definido para el TADs35
.
La única forma de la que se dispondrá para usar los datos del tipo definido será a través de
las operaciones definidas en su interfaz, por lo que, cuando un programador haga uso del tipo
no conocerá su representación interna. Es más, se podría cambiar la representación del tipo de
datos sin que el funcionamiento del programa cliente se viera afectado.
Para indicar el funcionamiento de las operaciones sobre un tipo de datos se utilizará la
especificación algebraica con constructores, basada en describir el comportamiento mediante
ecuaciones36
[15].
3.10.2. Definición de Estructuras de Datos en Lenguajes Funcionales
3.10.2.1. Introducción
En la introducción a la programación funcional se dijo que unas de las principales caracte-
rísticas de la misma eran:
34
El programador que use las estructuras de datos, al igual que ocurría en el caso de los tipos de datos, descono-
cerá cómo han sido implementadas las operaciones con las que podrá interactuar con la estructura de datos o cómo
se representa internamente la misma.
35
Obviamente, cuando se implementen las operaciones sobre el TADs, sí se tendrá acceso a la representación
del mismo.
36
Basado en el modelo de sustitución – ver la Sección 1.4: Evaluación en Scala « página 18 » –
Página 80
Variables inmutables (no actualizar el valor de una variable).
Estructuras de datos inmutables
Cuando se plantea la definición de estructuras de datos funcionales, pueden surgir algunas
preguntas:
¿Qué estructuras de datos se pueden usar en programación funcional?
¿Cómo se podrá operar con estas estructuras de datos funcionales?
¿Cómo se definen estructuras de datos en Scala?
A lo largo de este capítulo se intentará dar respuesta a estas preguntas, haciendo uso de
los conceptos propios de la programación funcional vistos anteriormente, como son: funciones
puras, funciones anónimas, orden superior, polimorfismo ...
3.10.2.2. Definición
Para operar sobre estructuras de datos funcionales, se utilizarán funciones puras. Las estruc-
turas de datos funcionales serán inmutables.
En general, para crear una estructura de datos nueva, un tipo de datos nuevo, se usará la
palabra clave trait. Un trait es una interfaz abstracta que puede incluir la implementación de
algunos métodos37
. Si se añade la palabra clave sealed delante de trait se estará indicando que
todas las implementaciones del rasgo que se va a definir deberán ser declaradas en el mismo
archivo. Ejem. sealed trait List[T].
Para definir los constructores del tipo de datos (constructores de datos) se utilizará la pala-
bra reservada case con las que se representarán cada una de las posibles formas de la estructura
de datos. La declaración de un constructor de datos proporcionará una función para construir
cada una de las posibles formas que pueda tener nuestra tipo de datos.
Al igual que las funciones pueden ser polimórficas, las estructuras de datos también pueden
serlo. Para definir una estructura de datos polimórfica se deberá incluir una lista de variables de
tipo, encerrada entre corchetes, en la declaración de la estructura de datos. Posteriormente, se
podrá hacer uso de estas variables de tipo en la definición de los constructores de datos y en los
métodos.
Las operaciones que se quieran definir sobre el tipo de datos se definirán en un objeto acom-
pañante de la clase acompañante.
Compartición estructural (Data Sharing)
Cuando los tipos de datos son inmutables, ¿cómo se escribirán funciones que, por ejemplo,
añadan o eliminen un elemento de una lista?
La respuesta es simple, cuando se añade un elemento (por ejemplo 1) como primer elemento
de una lista xs, se devuelve una nueva lista (en este caso, Cons(1,xs))
Teniendo en cuenta que los tipos de datos son inmutables, no se necesitará realmente copiar
la lista xs. Se podrá reutilizar. Esta propiedad de los tipos de datos inmutables se denomina
compartición estructural o compartición de datos (data sharing o sharing).
37
Véase la Subsección 2.3.1: Herencia en Scala « página 38 » para ampliar la información sobre los rasgos
(traits) en Scala.
Página 81
La compartición estructural permitirá implementar funciones de una forma más eficiente.
Esta propiedad de los datos inmutables ofrece la posibilidad de devolver estructuras de datos
inmutables sin tener la preocupación de que posteriormente el código modifique sus datos y por
tanto, sin la necesidad de hacer “copias de seguridad” pesimistas de la estructura de datos38
.
3.10.2.3. Los Naturales
Se denotará por N al conjunto de números naturales, cuyos elementos son suma de un nú-
mero finito de unos:
N = {0, 1, 2, 3, 4, . . .}
Es importante recordar que N es cerrado para la suma y el producto, pero no lo es para la
resta o para la división (3 − 8 /∈ N, 4
5
/∈ N).
A continuación se muestra la especificación algebraica correspondiente a la especificación
de los números naturales:
especificación NATURALES
usa BOOLEANOS
tipos nat
operaciones
cero :−→ nat{constructora}
suc : nat −→ nat{constructora}
_ + _ : nat nat −→ nat
_ ∗ _ : nat nat −→ nat
_ˆ_ : nat nat −→ nat
_ === _ : nat nat −→ bool
_! == _ : nat nat −→ bool
_ ≤ _ : nat nat −→ bool
_ < _ : nat nat −→ bool
_ ≥ _ : nat nat −→ bool
_ > _ : nat nat −→ bool
max : nat nat −→ nat
min : nat nat −→ nat
_/_ : nat nat −→ nat
_ %_ : nat nat −→ nat
es_par : nat −→ bool
es_impar : nat −→ bool
variables
n,m: nat
ecucaciones
n + cero = n
n + suc(m) = suc(n + m)
38
Algo que se convierte en un problema cuando se desarrollan programas grandes, en los que los datos tienen
que ser pasados a través de diversos componentes, los cuales se ven obligados a hacer “copias de seguridad” de los
mismos.
Página 82
cero ∗ m = 0
suc(n) ∗ m = (n ∗ m) + m
cero − cero = cero
cero − m = error
suc(n) − cero = suc(n)
suc(n) − suc(m) = n − m
ceroˆcero = error
nˆcero = suc(cero)
nˆsuc(m) = n ∗ (nˆm)
cero === cero = cierto
cero === suc(m) = falso
suc(n) === cero = falso
suc(n) === suc(m) = n == m
n! == m =!(n === m)
cero ≤ m = cierto
suc(n) ≤ cero = falso
suc(n) ≤ suc(m) = n ≤ m
n < m = n ≤ m&&n = m
n ≥ m = m ≤ n
n > m = m < n
max(cero, m) = m
max(suc(n), cero) = suc(n)
max(sun(n), suc(m) = suc(max(n, m))
min(cero, m) = cero
min(suc(n), cero) = cero
min(suc(n), suc(m)) = suc(min(n, m))
n/cero = error
n/m = cero ⇐ n < m
n/m = suc((n − m)/m) ⇐ m = cero && m ≤ n
n %cero = error
n %m = n ⇐ n < m
n %m = (n − m) %m ⇐ m = cero && m ≤ n
es_par(cero) = cierto
es_par(suc(n)) = es_impar(n)
es_impar(cero) = falso
Página 83
es_impar(suc(n)) = es_par(n)
Se definirán los números naturales. Tendrán dos constructores: Cero y Suc, además de las
operaciones que aparecen en la especificación algebraica de los mismos:
1
2 trait Nat {
3
4 def + (y:Nat):Nat = y match {
5 case Cero => this
6 case Suc(m) => Suc(this+m)
7 }
8
9 def * (m:Nat):Nat = this match{
10 case Cero => Cero
11 case Suc(n) => (n * m) + m
12 }
13
14 def - (m:Nat):Nat = this match{
15 case Cero if m==Cero => Cero
16 case Cero => throw new Exception("Error en la resta")
17 case Suc(n) => m match {
18 case Cero => this
19 case Suc(t) => n - t
20 }
21 }
22
23 def ^ (m:Nat):Nat= m match{
24 case Cero if this==Cero=> throw new Exception("Indeterminacion
0^0")
25 case Cero => Suc(Cero)
26 case Suc(t)=> this * (this ^ t)
27 }
28
29 def === (m:Nat):Boolean = this match {
30 case Cero if m==Cero => true
31 case Suc(n) => m match {
32 case Suc(t) => n==t
33 case Cero => false
34 }
35 case _ => false
36 }
37
38 def !== (m:Nat):Boolean = !(this===m)
39
40 def <= (y:Nat):Boolean = this match{
41 case Cero => true
42 case Suc(n)=> y match{
43 case Cero => false
44 case Suc(m) => n <= m
45 }
Página 84
46 }
47
48 def < (y:Nat):Boolean= (this <= y) && (this !== y)
49
50 def >= (y:Nat):Boolean = y <= this
51
52 def > (y:Nat):Boolean = y < this
53
54 def / (y:Nat):Nat = y match{
55 case Cero => throw new Exception("Error division por cero")
56 case Suc(m) if this < y => Cero
57 case Suc(m) => Suc((this - y) / y)
58 }
59
60 def % (y:Nat):Nat = y match{
61 case Cero => throw new Exception("Error calculando resto en
division por cero")
62 case Suc(m) if this < y => this
63 case Suc(m) => (this - y) % y
64 }
65
66
67 }
68 case object Cero extends Nat
69 case class Suc(elem:Nat) extends Nat
70
71 object Nat{
72
73 def max (x:Nat,y:Nat):Nat = x match{
74 case Cero => y
75 case Suc(n) => y match{
76 case Cero => x
77 case Suc(m) => Suc(max(n,m))
78 }
79 }
80
81 def min(x:Nat,y:Nat):Nat = x match{
82 case Cero => Cero
83 case Suc(n) => y match{
84 case Cero => Cero
85 case Suc(m) => Suc(min(n,m))
86 }
87 }
88
89 def es_Par(x:Nat):Boolean = x match{
90 case Cero => true
91 case Suc(n) => es_Impar(n)
92 }
93
94 def es_Impar(x:Nat):Boolean = x match{
95 case Cero => false
Página 85
96 case Suc(n) => es_Par(n)
97 }
98
99 }
Ejercicios resueltos.
Completar la implementación de la especificación de los números naturales con:
1. La función toInt, que devolverá el valor de tipo Int correspondiente al número natural.
2. La función pred, que devolverá un valor de tipo Nat correspondiente al número natural
anterior al pasado como argumento a la función. Para facilitar la resolución del ejercicio
se podrá considerar que el predecesor de 0 es 0.
3. La función es_Primo, que nos indicará si el número natural pasado como argumento a la
función es un número primo.
4. La función mcd, que devolverá el máximo común divisor de dos números naturales pasa-
dos como argumentos.
5. La función coprimos, que nos indicará si dos números pasados como argumentos son
coprimos.
6. La función cuadrado, que devolverá el cuadrado del número natural pasado como argu-
mento.
7. El método de fábrica apply, con el que se podrá crear un número natural a partir de un
número del tipo Int
El algoritmo 3.8 muestra la implementación final de los números naturales con una posible
solución a los ejercicios anteriores.
1 trait Nat {
2
3 def + (y:Nat):Nat = y match {
4 case Cero => this
5 case Suc(m) => Suc(this+m)
6 }
7
8 def * (m:Nat):Nat = this match{
9 case Cero => Cero
10 case Suc(n) => (n * m) + m
11 }
12
13 def - (m:Nat):Nat = this match{
14 case Cero if m==Cero => Cero
15 case Cero => throw new Exception("Error en la resta")
16 case Suc(n) => m match {
17 case Cero => this
18 case Suc(t) => n - t
19 }
Página 86
20 }
21
22 def ^ (m:Nat):Nat= m match{
23 case Cero if this==Cero=> throw new Exception("Indeterminacion
0^0")
24 case Cero => Suc(Cero)
25 case Suc(t)=> this * (this ^ t)
26 }
27
28 def === (m:Nat):Boolean = this match {
29 case Cero if m==Cero => true
30 case Suc(n) => m match {
31 case Suc(t) => n==t
32 case Cero => false
33 }
34 case _ => false
35 }
36
37 def !== (m:Nat):Boolean = !(this===m)
38
39 def <= (y:Nat):Boolean = this match{
40 case Cero => true
41 case Suc(n)=> y match{
42 case Cero => false
43 case Suc(m) => n <= m
44 }
45 }
46
47 def < (y:Nat):Boolean= (this <= y) && (this !== y)
48
49 def >= (y:Nat):Boolean = y <= this
50
51 def > (y:Nat):Boolean = y < this
52
53 def / (y:Nat):Nat = y match{
54 case Cero => throw new Exception("Error division por cero")
55 case Suc(m) if this < y => Cero
56 case Suc(m) => Suc((this - y) / y)
57 }
58
59 def % (y:Nat):Nat = y match{
60 case Cero => throw new Exception("Error calculando resto en
division por cero")
61 case Suc(m) if this < y => this
62 case Suc(m) => (this - y) % y
63 }
64
65 //******** Ejercicios resueltos *********
66 val toInt:Int= this match{
67 case Cero => 0
68 case Suc(x)=> 1 + x.toInt
Página 87
69 }
70 }
71
72 case object Cero extends Nat
73 case class Suc(elem:Nat) extends Nat
74
75 object Nat{
76
77 def max (x:Nat,y:Nat):Nat = x match{
78 case Cero => y
79 case Suc(n) => y match{
80 case Cero => x
81 case Suc(m) => Suc(max(n,m))
82 }
83 }
84
85 def min(x:Nat,y:Nat):Nat = x match{
86 case Cero => Cero
87 case Suc(n) => y match{
88 case Cero => Cero
89 case Suc(m) => Suc(min(n,m))
90 }
91 }
92
93 def es_Par(x:Nat):Boolean = x match{
94 case Cero => true
95 case Suc(n) => es_Impar(n)
96 }
97
98 def es_Impar(x:Nat):Boolean = x match{
99 case Cero => false
100 case Suc(n) => es_Par(n)
101 }
102 //********* Ejercicios resueltos **************
103 def pred(x:Nat)= x match{
104 case Cero => Cero
105 case _ => x - Suc(Cero)
106 }
107 def suc(x:Nat)=Suc(x)
108 def esDivisible(x:Nat)(y:Nat):Boolean= (x % y) == Cero
109 private def compruebaDesdeHasta (x:Nat)
(y:Nat)(f:Nat=>Nat=>Boolean):Boolean=
110 if (x==y) true
111 else !f (y)(x) && compruebaDesdeHasta(suc(x))(y)(f)
112
113 def es_Primo(x:Nat):Boolean= x match{
114 case Cero => false
115 case Suc(Cero)=> false
116 case otro=>compruebaDesdeHasta(Suc(Suc(Cero)))(x)(esDivisible)
117 }
118
Página 88
119 @annotation.tailrec
120 def mcd (x:Nat)(y:Nat): Nat = if (y==Cero) x else mcd (y) (x % y)
121
122 def coprimos(x:Nat, y:Nat):Boolean = mcd(x)(y)== Suc(Cero)
123
124 def cuadrado(x:Nat):Nat =x^(Suc(Suc(Cero)))
125
126 def apply(ent:Int):Nat= ent match{
127 case 0 => Cero
128 case x => Suc(apply(x-1))
129 }
130 }
Algoritmo 3.8: Implementación final del tipo Naturales
3.10.3. Estructuras de datos lineales. Listas
3.10.3.1. TAD Lista
Las listas son estructuras inmutables homogéneas, es decir, que contienen datos del mismo
tipo. Además, son las estructuras inmutables de datos lineales más flexibles, puesto que su única
característica es imponer un orden entre los elementos39
almacenados en ellas[15].
A continuación, se definirá el tipo de datos Lista como un tipo recursivo:
Dado un alfabeto V, se define el conjunto de secuencias o cadenas de elementos de V,
denotado por V ∗
, como:
Nil V∗
∀s V∗
: ∀v V : v.s V ∗
Nil representará la lista vacía, mientras que el resto de listas se definirán como el añadido de
un elemento por la izquierda a una lista ya existente.[10]
El comportamiento de las listas es independiente del tipo de sus elementos, por lo que se
especificarán de forma paramétrica.
Se han escogido dos constructores:
1. La lista vacía (Nil)
2. La operación que añade un elemento por la izquierda a una lista (operación Cons40
).
Se implementarán también las operaciones típicas sobre listas:
esVacia nos indicará si una lista contiene algún elemento o no.
longitud devolverá un entero con el número de elementos de la lista.
_ ## _ añadirá un elemento al final de la lista
_ ++ _ devolverá el resultado de concatenar dos listas.
39
Los elementos de una lista pueden estar repetidos
40
Otras especificaciones de listas utilizan el constructor :: en lugar de Cons por lo que se añadirá la operación ::
para poder construir listas de la forma (1::2::3::4::Nil) aunque no se podrá utilizar :: como patrón
Página 89
izquierdo41
devolverá el elemento situado más a la izquierda de la lista.
elim-izq42
eliminará el elemento situado más a la izquierda de la lista.
derecho devolverá el elemento situado más a la derecha de la lista.
elim-der eliminará el elemento situado más a la derecha de la lista.
drop permite eliminar una cantidad de elementos del principio de la lista.
Así, la especificación de las listas será:
especificación LISTA[ELEM]
usa BOOLEANOS,NATURALES,ENTEROS
tipos lista
operaciones
Nil :−→ lista{constructora}
Cons(_, _) : elemento lista −→ lista{constructora}
esV acia : lista −→ bool
longitud : lista −→ int
_##_ : lista elemento −→ lista
_ + +_ : lista lista −→ lista
izquierdo : lista −→ elemento
elim − izq : lista −→ lista
derecho : lista −→ lista
elim − der : lista −→ lista
drop : lista nat −→ lista
variables
n,m: elemento
x,y,z: lista
a: nat
ecuaciones
izquierdo(Nil) = error
izquierdo(Cons(e, x)) = e
elim − izq(Nil) = error
elim − izq(Cons(e, x)) = x
derecho(Nil) = error
derecho(Cons(e, Nil)) = e
derecho(Cons(e, x)) = derecho(x)
elim − der(Nil) = error
elim − der(Cons(e, Nil)) = Nil
41
Se corresponde con la operación cabeza
42
Se corresponde con la operación cola
Página 90
elim − der(Cons(e, x)) = elim − der(x)
esV acia(Nil) = cierto
esV acia(Cons(e, x)) = falso
Nil + +y = y
Cons(e, x) + +y = Cons(e, x + +y)
longitudNil = 0
longitud(Cons(e, x)) = 1 + longitud(x)
Nil + +x = x
Cons(e, xs) + +y = Cons(e, xs + +y)
x##e = x + +Cons(e, Nil)
drop(x, 0) = x
drop(Nil, a) = Nil
drop(Cons(e, x), a) = drop(x, a − 1)
Una posible implementación en Scala de la especificación del tipo abstracto de datos Lista
definido anteriormente sería:
1
2 sealed trait Lista[+A]{
3 def cabeza: A
4 def cola: Lista[A]
5
6 /** Version recursiva para calcular la longitud de una lista */
7 def longitud:Int={
8 @annotation.tailrec
9 def length[A](xs:Lista[A],count:Int):Int= xs match{
10 case Nil => count
11 case Cons(_,xs) => length(xs,count+1)
12 }
13 length(this,0)
14 }
15
16 /** Devuelve el elemento situado mas a la izquierda de una lista */
17 def izquierdo:A= this match {
18 case Nil => throw new NoSuchElementException("Lista vacia!!")
19 case _ => cabeza
20 }
21
22 /** Elimina el elemento situado mas a la izquierda de una lista*/
23 def elim_izq:Lista[A]= this match {
24 case Nil => throw new NoSuchElementException("Lista vacia!!")
25 case _ => cola
26
27 }
Página 91
28 /** Devuelve el elemento situado mas a la derecha de una lista */
29 def derecho:A= this match{
30 case Nil => throw new Exception("Lista vacia!!, sin elementos")
31 case Cons(elem,Nil)=>elem
32 case Cons(e,xs)=>xs derecho
33 }
34 /** Elimina el elemento situado mas a la derecha de una lista */
35 def elim_der:Lista[A]= this match{
36 case Nil => Nil
37 case Cons(elem,Nil)=>Nil
38 case Cons(elem,xs)=>Cons(elem,xs elim_der)
39 }
40 def esVacia:Boolean = this match{
41 case Nil => true
42 case Cons(e,x) => false
43 }
44 /** Operador que nos permite construir listas del tipo
1::2::3::Nil */
45 def ::[U >: A](x: U): Lista[U] = Cons(x,this)
46
47 /** Operador que nos permite insertar un elemento al final de la
lista */
48 def ##[U >: A](x:U):Lista[U]=this++(Cons(x,Nil))
49
50 /** Operador para concatenar listas **/
51 def ++[U >: A](ys:Lista[U]):Lista[U]= this match{
52 case Nil => ys
53 case Cons(e,x) => Cons(e,x++ys)
54 }
55
56 case object Nil extends Lista[Nothing]{
57 def cabeza: Nothing = throw new NoSuchElementException("cabeza de
la lista vacia")
58 def cola:Nothing = throw new NoSuchElementException("cola lista
vacia")
59 }
60 final case class Cons[A] (cabeza:A, cola:Lista[A]) extends Lista[A]{
61 override def toString:String=cabeza+"::"+cola
62 }
63
64 object Lista {
65 def drop[A](ys:Lista[A])(n:Nat):Lista[A]= (ys,n) match {
66 case (Nil,_) => Nil
67 case (xs,Cero)=> xs
68 case (Cons(e,xs),n)=> drop (xs) (n-Nat(1))
69 }
70 def apply[A](as: A*): Lista[A] ={
71 if (as.isEmpty) Nil
72 else as.head :: apply(as.tail: _*)
73 }
74 }
Página 92
Algoritmo 3.9: Implementacion TAD Lista
A continuación se ampliarán las operaciones sobre el tipo Lista que se está implementando:
esta, dado un elemento, devolverá true si el elemento está en la lista y false si el elemento
no está en la lista.
posicion, devolverá un valor entero con la posición de la primera aparición de un elemento
dado en la lista. La primera posición de la lista será la posición 0 y la última posición será
n − 1, considerando n = length(xs). Si devuelve -1 indicará que el elemento no se
encuentra en la lista.
repeticiones, devolverá un valor entero con el número de repeticiones de un elemento
dado en la lista.
eliminarTodos, eliminará todas las apariciones de un elemento dado en la lista.
inversa, devolverá una lista con los elementos de la lista original en orden inverso.
esCapicua, devolverá true en caso de que la lista sea capicúa y false en caso contrario.
Las anteriores funciones se añadirán al objeto acompañante43
del tipo de datos Lista que se
está definiendo. Así, el tipo de datos Lista quedaría:
1 object Lista {
2
3 def drop[A](ys:Lista[A])(n:Nat):Lista[A]= (ys,n) match {
4 case (Nil,_) => Nil
5 case (xs,Cero)=> xs
6 case (Cons(e,xs),n)=> drop (xs) (n-Nat(1))
7 }
8
9
10
11 private def encuentra[A](elem:A,pos:Int,ys:Lista[A]):Int= ys match{
12 case Nil => -1
13 case Cons(e,_) if e== elem => pos
14 case Cons(_,xs) => encuentra(elem,pos+1,xs)
15 }
16
17 def esta[A](elem:A,ys:Lista[A]):Boolean= if (encuentra(elem,0,ys)
!= -1) true else false
18
19 def posicion[A](elem:A,ys:Lista[A]):Int = encuentra(elem,0,ys)
20
21 def repeticiones[A](elem:A,ys:Lista[A]):Int={
22 @annotation.tailrec
23 def go(ac:Int, xs:Lista[A]):Int = xs match {
43
Habitualmente se declarará un objeto acompañante que acompañará al tipo de datos que se esté definiendo y
a sus constructores. Un objeto acompañante es un objeto que tiene el mismo nombre que la clase acompañante
donde se definirán funciones para crear o trabajar con el tipo de datos. Los objeto acompañante tienen un soporte
especial dentro de Scala.
Página 93
24 case Nil => ac
25 case Cons(e,xs) if e==elem => go(ac+1,xs)
26 case Cons(_,xs) => go(ac,xs)
27
28 }
29 go(0,ys)
30 }
31
32 def eliminarTodos[A](elem:A,xs:Lista[A]):Lista[A]= {
33 @annotation.tailrec
34 def go(ac:Lista[A],xs:Lista[A]):Lista[A]= xs match{
35 case Nil => ac
36 case Cons(e,xs) if e== elem => go(ac,xs)
37 case Cons(algo,xs) => go(Cons(algo,ac),xs)
38
39 }
40 go(Nil,xs)
41 }
42 def inversa[A](xs:Lista[A]):Lista[A]={
43 @annotation.tailrec
44 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match {
45 case Nil => res
46 case Cons(elem,xss)=>go(xss,elem::res)
47 }
48 go(xs,Nil)
49
50 }
51
52 def esCapicua[A](xs:Lista[A]):Boolean= xs == inversa(xs)
53
54 def apply[A](as: A*): Lista[A] ={
55 if (as.isEmpty) Nil
56 else as.head :: apply(as.tail: _*)
57 }
58 }
Algoritmo 3.10: Object Lista ampliado
3.10.3.2. Ejercicios sobre el TAD Lista
Ejercicio 25. Definir la función last que devuelva el último elemento de una lista.
Ejercicio 26. Definir la función init que devuelva la lista formada por los elementos de la lista
original sin el último elemento.
Ejercicio 27. Definir la función take que permita seleccionar una cantidad de elementos inicia-
les de una lista.
Ejercicio 28. Definir la función splitAt que devolverá una dupla, combinando los resultados de
take y drop.
Ejercicio 29. Definir la función zipWith que aplicará cierta función a los elementos de dos listas
tomándolos de dos en dos. La longitud de la lista resultado coincidirá con la de menor longitud.
Página 94
Ejercicio 30. Definir la función zip que construirá una lista de pares a partir de dos listas.
Ejercicio 31. Definir la función unzip que dada una lista de pares devuelva una dupla formada
por dos listas. La primera de ella será el resultado de tomar el primer elemento de cada par de
elementos de la lista original y la segunda lista será el resultado de tomar el segundo elemento.
Ejercicio 32. Definir la función map la cual transformará una lista aplicando a cada elemento
de la misma una función.
Ejercicio 33. Definir la función filter que permitirá seleccionar los elementos de una lista que
cumplan un cierta propiedad.
Ejercicio 34. Definir la función takeWhile que tomará el mayor segmento inicial de una lista
que cumpla una cierta propiedad.
Ejercicio 35. Definir la función dropWhile la cual eliminará de una lista el mayor segmento
inicial de elementos que verifiquen un propiedad.
Ejercicio 36. Definir la función foldr, función recursiva que recorre los elementos de una lista
de derecha a izquierda y que tenga el siguiente comportamiento:
Si el argumento es la lista vacía, devolverá el argumento correspondiente al caso base.
En otro caso, se opera mediante una función u operador pasado como argumento a la
función, la cabeza de la lista con una llamada recursiva con la cola de la misma.
Ejercicio 37. Definir la función foldl con un comportamiento similar a la función foldr, pero
realizando el plegado de la lista de izquierda a derecha.
3.10.4. Estructuras de datos no lineales
3.10.4.1. Árboles
Los árboles son estructuras no lineales, como los conjuntos o los grafos, en los que la
secuencialidad que caracteriza a las estructuras lineales no existe, aunque en los árboles existe
una estructura jerárquica, de manera que un elemento tiene un solo predecesor, pero varios
sucesores.
Un árbol impone una estructura jerárquica sobre una colección de objetos, con un único
punto de entrada y una serie de caminos que van abriéndose en cada punto hacia sus sucesores.
Desde un punto de vista formal (teoría de conjuntos), un árbol se puede considerar como una
estructura A = (N, ), constituida por un conjunto, N, cuyos elementos se denominan nodos, y
una relación de orden parcial transitiva, , definida sobre N, y caracterizada por la existencia
de:
Un elemento mínimo (anterior a todos los demás) único, la raíz.
∃! r ∈ N (∀n ∈ N, n = r, r n)
Un predecesor único para cada nodo p distinto de la raíz, es decir, un nodo, q, tal que q
p y para cualquier nodo s con las mismas características se cumple s q.
∀ n ∈ N n = r −→ (∃! m ∈ N, ((m n) (∀ s ∈ N, s = m s n −→ s m)))
Página 95
La terminología utilizada con los árboles, en relación con los nodos que los forma, es la
siguiente:
Raíz - Elemento mínimo de un árbol, es decir, único nodo que no tiene padre.
Padre - Predecesor máximo de un nodo.
Hijo - Cualquiera de los sucesores directos de un nodo.
Hermanos - Nodos que comparten el mismo padre.
Nodo terminal u hoja - Nodos sin hijos, es decir, que no tiene sucesores.
Nivel - El nivel de un nodo está definido por el número de conexiones entre el nodo y la
raíz.
Nodo intermedio - Cualquier nodo del árbol predecesor de una hoja, y sucesor de la raíz
Un árbol en el que en cada nodo o bien todos o ninguno de los hijos existe, se llama árbol
completo.
Existen otros conceptos relacionados con el tamaño del árbol que definen las características
del mismo:
Orden: es el número potencial de hijos que puede tener cada elemento del árbol. De este
modo, se dice que un árbol en el que cada nodo puede apuntar a otros dos es de orden
dos, si puede apuntar a tres será de orden tres, etc.
Grado: el número de hijos que tiene el elemento con más hijos dentro del árbol.
Nivel: se define para cada elemento del árbol como la distancia a la raíz, medida en nodos.
El nivel de la raíz es cero y el de sus hijos uno. Así sucesivamente.
Altura: la altura de un árbol se define como el nivel del nodo de mayor nivel + 1. Como
cada nodo de un árbol puede considerarse a su vez como la raíz de un árbol, también se
puede hablar de altura de ramas.
Esta estructura se puede considerar una estructura recursiva teniendo en cuenta que cada
nodo del árbol, junto con todos sus descendientes, y manteniendo la ordenación original, cons-
tituye también un árbol o subárbol del árbol principal, característica esta que permite definicio-
nes simples de árbol, más apropiadas desde el punto de vista de la teoriza de tipos abstractos de
datos, y, ajustadas, cada una de ellas, al uso que se vaya a hacer de la noción de árbol.
Las dos definiciones más comunes son las de árbol general y la de árbol de orden N, que se
pueden dar en los términos siguientes:
Un árbol general con nodos de un tipo T es un par (r, LA) formado por un nodo r (la
raíz) y una lista (si se considera relevante el orden de los subárboles) o un conjunto(si
éste es irrelevante) LA (bosque), posiblemente vacío, de árboles generales del mismo
tipo (subárboles de la raíz). Se puede apreciar que aquí no existe el árbol vacío, sino la
secuencia vacía de árboles generales.
Un árbol de orden N (con N ≥ 2), con nodos de tipo T, es un árbol vacío () o un par (r,
LA) formado por un nodo r (la raíz) y una tupla LA (bosque) formada por N árboles del
mismo tipo (subárboles de la raíz). Este último caso suele escribirse explícitamente de la
forma (r, A1, ..., AN ).
Página 96
3.10.4.2. Arboles Binarios
El árbol binario es el caso más simple de árbol de orden N, cuando N vale 2. Su especifica-
ción se puede hacer considerando un valor constante, el árbol nulo, y un constructor de árboles
a partir de un elemento y dos árboles.
especificación ARBOL_BINARIO[ELEM]
usa BOOLEANOS,ENTEROS
tipos arbolB
operaciones
V acio :−→ arbolB{constructora}
Nodo(_, _, _) : elemento arbolB arbolB −→ arbolB{constructora}
hijoIzqdo : arbolB −→ arbolB
hijoDcho : arbolB −→ arbolB
raiz : arbolB −→ elemento
esV acio : arbolB −→ bool
numeroNodos : arbolB −→ int
igualForma : arbolB arbolB −→ bool
altura : arbolB ←→ int
variables
n,m: elemento
x,y,z: arbolB
ecuaciones
hijoIzqdo(V acio) = error
hijoIzqdo(Nodo(e, i, d)) = i
hijoDcho(V acio) = error
hijoDcho(Nodo(e, i, d)) = d
raiz(V acio) = error
raiz(e, i, d) = e
esV acio(V acio)) = cierto
esV acio(Nodo(e, i, d)) = falso
numeroNodos(V acio) = 0
numeroNodos(Nodo(e, i, d)) = 1 + numeroNodos(i) + numeroNodos(d)
igualForma(V acio, a) = esV acio(a)
igualForma(Nodo(e, i, d), Nodo(e2, i2, d2)) = igualForma(i, i2)&&igualForma(d, d2)
altura(V acio) = 0
altura(e, i, d) = 1 + max(altura(i), altura(d))
Al igual que ocurría con las estructuras de datos lineales, esta especificación desempeña un
papel básico que ayudará a la futura construcción de otras especificaciones de árboles.
El algoritmo 3.11 muestra una posible implementación de la especificación anterior en Sca-
la.
Página 97
1 trait ArbolB[+T] {
2 def hijoIzqdo:ArbolB[T] = this match {
3 case Vacio => Vacio
4 case Nodo(_,i,_)=>i
5 }
6 def hijoDcho:ArbolB[T] = this match {
7 case Vacio => Vacio
8 case Nodo(_,_,d)=>d
9 }
10 def raiz:T= this match{
11 case Vacio => throw new Exception("Solicitando raiz de arbol
vacio!!!")
12 case Nodo(e,_,_)=>e
13 }
14 def esVacio:Boolean= this match{
15 case Vacio => true
16 case _ => false
17 }
18 def numeroNodos:Int = this match{
19 case Vacio => 0
20 case Nodo(e,i,d)=> 1 + (i numeroNodos) + (d numeroNodos)
21 }
22 def altura:Int = this match{
23 case Vacio => 0
24 case Nodo(e,i,d)=> 1 + (i altura).max(d altura)
25 }
26
27
28 }
29 case object Vacio extends ArbolB[Nothing]{
30 override def toString = "."
31 }
32 case class Nodo[T] (valor:T,i:ArbolB[T],d:ArbolB[T]) extends
ArbolB[T]{
33 override def toString = "T(" + valor.toString + " " + i.toString +
" " + d.toString + ")"
34 }
35 object ArbolB {
36 def igualForma[T](a1:ArbolB[T],a2:ArbolB[T]):Boolean= a1 match{
37 case Vacio => a2 esVacio
38 case Nodo(e,i,d)=> igualForma(i,a2 hijoIzqdo) && igualForma(d,a2
hijoDcho)
39 }
40 }
41 object Nodo {
42
43 def apply[T](value: T): Nodo[T] = Nodo(value, Vacio, Vacio)
44 }
Algoritmo 3.11: Implementación básica de Arboles Binarios
Página 98
3.10.4.3. Arboles Binarios de Búsqueda
Son árboles de orden 2 en los que se cumple que para cada nodo, el valor de la clave de la
raíz del subárbol izquierdo es menor que el valor de la clave del nodo y que el valor de la clave
raíz del subárbol derecho es mayor que el valor de la clave del nodo.
El repertorio de operaciones que se pueden realizar sobre un ABB es parecido al que ya se
ha definido sobre otras estructuras de datos, más alguna otra propia de árboles:
Buscar un elemento en un árbol.
Insertar un elemento en un árbol.
Borrar un elemento.
Movimientos a través del árbol:
• Sub-árbol izquierdo.
• Sub-árbol derecho.
• Raiz.
Información:
• Comprobar si un árbol está vacío.
• Calcular el número de nodos.
• Comprobar si el nodo es hoja.
• Calcular el nodo con mayor clave
• Calcular el nodo con menor clave.
• Calcular el padre de un elemento.
A continuación, se muestra una posible implementación del TADs ABB a la que se ha
llamado Arbol:
1 trait ArbolBB[+T] {
2 def esVacio:Boolean
3 def add[U >: T < % Ordered[U]](x: U): ArbolBB[U]
4 def del[U >: T < % Ordered[U]](x: U): ArbolBB[U]
5 def search[U >: T< % Ordered[U]](x:U):Boolean
6 def raiz:ArbolBB[T]=this
7 def subIzq:ArbolBB[T]
8 def subDer:ArbolBB[T]
9 def maximo:T
10 def minimo:T
11 def predecesor[U >: T< % Ordered[U]](x:U):ArbolBB[U]
12 }
13
14 case class Nodo[+T](valor: T, left: ArbolBB[T], right: ArbolBB[T])
extends ArbolBB[T] {
15 override def toString = "T(" + valor.toString + " " +
left.toString + " " + right.toString + ")"
16 def esVacio=false
17 def subIzq = left
Página 99
18 def subDer = right
19 def add[U >: T < % Ordered[U]](x: U): ArbolBB[U] = {
20 x.compare(valor) match {
21 case i if i < 0 => Nodo(valor, left.add(x), right)
22 case i if i > 0 => Nodo(valor, left, right.add(x))
23 case _ => Nodo(x,left,right)
24 }
25 }
26
27 def del[U >: T < % Ordered[U]](x: U): ArbolBB[U] = {
28
29 def valorSuc[U >: T](x:ArbolBB[U],direct:Boolean):U={
30 x match{
31 case Nodo(s,Vacio,_)if direct==true=>s //Si buscamos el menor
de los elementos mayores
32 case Nodo(s,_,Vacio)if direct==false=>s // Si buscamos el
mayor de los elementos menores
33 case Nodo(_,_,right1) if direct==false
=>valorSuc(right1,direct) // Si buscamos el mayor de los
elementos menores
34 case Nodo(_,left1,_) if direct==true =>valorSuc(left1,direct)
//Si buscamos el menor de los elementos mayores
35 }
36 }
37 x.compare(valor) match{
38 case i if i < 0 => Nodo(valor,left.del(x),right) // Seguimos
buscando el elemento por el subarbol izdo.
39 case s if s > 0 => Nodo(valor,left,right.del(x)) // Seguimos
buscando el elemento por el subarbol dcho.
40 case _ =>this match{ // Hemos encuentrado el elemento a eliminar
41 case Nodo(r,Vacio,Vacio)=> Vacio
42 case Nodo(r,i,Vacio)=>i
43 case Nodo(r,Vacio,d)=>d
44 case Nodo(r,i,d)=> val e:U=valorSuc(d,true);
Nodo(e,i,d.del(e))
45 //Hemos buscados como sucesor, el menor de los
46 //elementos mayores
47 }
48 }
49
50 }
51 def search[U >: T< % Ordered[U]](x:U):Boolean={
52 x.compare(valor) match {
53 case i if i <0 => left.search(x)
54 case i if i >0 => right.search(x)
55 case _ => true
56 }
57 }
58 def maximo:T= this match{
59 case Nodo(r,_,Vacio)=>r
60 case Nodo(r,_,d)=>d maximo
Página 100
61 }
62 def minimo:T= this match{
63 case Nodo(r,Vacio,_)=>r
64 case Nodo(r,i,_)=>i minimo
65
66 }
67 def predecesor[U >: T< % Ordered[U]](x:U):ArbolBB[U]={
68 if (search(x)==true) buscaPadre(x,this,Vacio) else Vacio //
Comprobamos que existe el elemento x
69 // y si existe,
comenzamos busqueda
del padre
70 }
71 private def buscaPadre[U >: T< % Ordered[U]] (elemen:U,
tree:ArbolBB[U], padre:ArbolBB[U]) :ArbolBB[U]= {
72 tree match{ // El argumento padre contendra el padre(predecesor)
73 // de elemen (Vacio si elemen es la raiz)
74 case Nodo(r,i,d) if r>elemen =>buscaPadre(elemen,i,Nodo(r,i,d))
75 case Nodo(r,i,d) if r<elemen=>buscaPadre(elemen,d,Nodo(r,i,d))
76 case _ => padre
77 }
78 }
79
80 }
81 case object Vacio extends ArbolBB[Nothing] {
82 override def toString = "."
83 def add [U >: Nothing < % Ordered[U]] (x:U):ArbolBB[U]=
Nodo(x,Vacio,Vacio)
84 def del [U >: Nothing < % Ordered[U]] (x:U):ArbolBB[U]=Vacio
85 def search[U >: Nothing < % Ordered[U]](x:U):Boolean =false
86 def subIzq=throw new Error("Vacio.subIzq")
87 def subDer=throw new Error("Vacio.subDer")
88 def maximo=throw new Error("Vacio.maximo")
89 def minimo=throw new Error("Vacio.minimo")
90 def predecesor[U >: Nothing < % Ordered[U]](x: U):ArbolBB[U]=Vacio
91 def esVacio=true
92
93
94 }
95
96 object Nodo {
97
98 def apply[T](value: T): Nodo[T] = Nodo(value, Vacio, Vacio)
99 }
Algoritmo 3.12: TAD Arbol Binario de Búsqueda
3.10.5. Ejercicios
Ejercicio 38. Definir una función listaToArbol que devolverá un árbol binario a partir de una
lista xs, de tipo List, pasada como argumento.
Página 101
Ejercicio 39. Desde el punto de vista estructural (sin tener en cuenta el valor de los nodos)
diremos que un árbol binario A1 es isomorfismo de otro árbol binario A2 si y sólo si ambos
tienen el mismo número de nodos y además los nodos presentan la misma estructura. Definir
una función isomorfimos que devuelva un valor true si A1 es isomorfismo del árbol A2 pasado
como parámetro.
Ejercicio 40. Se dice que un árbol binario es simétrico siempre que al trazar un línea vertical
que pase por su raíz, el subárbol izquierdo sea un espejo del subárbol derecho. Definir una fun-
ción esSimetrico que, haciendo uso de la función isomorfismo definida en el anterior ejercicio,
determine si un árbol cumple esta propiedad.
3.11. Colecciones en Scala
Además de poder construir estructuras de datos, es posible utilizar algunas de las estructuras
de datos que Scala ofrece. Las colecciones44
se pueden utilizar haciendo uso de la librería
collection45
, en la cual se encuentran los diferentes tipos de colecciones parametrizadas que
provee el lenguaje para resolver la mayoría de los problemas que se puedan presentar. Las
colecciones son estructuras de datos en las que se pueden agrupar uno o más datos de un tipo
de datos (colecciones homogéneas) o de diferentes tipos de datos (colecciones heterogéneas).
Se pueden entender como contenedores de valores, los cuales pueden estar dispuestos de forma
secuencial, es decir, estructuras lineales que pueden tener un número arbitrario de elementos
como Listas, Tuplas, Vectores, Conjuntos, ...o cuyo número de elementos puede estar limitado
como, por ejemplo, se verá en el tipo de datos Option. Desde Scala se tiene acceso completo a la
biblioteca de colecciones de Java, aunque no se podrían utilizar las funciones de orden superior
(map, filter y reduce), presentes en las estructuras de datos de la librería collection de Scala y
que, como se verá, ayudarán a manejar y operar sobre los datos de una colección, definiendo
expresiones cortas y expresivas.
Las colecciones pueden presentar una evaluación anticipada o una evaluación estricta o
perezosa de sus elementos(ver la Sección 3.9: Programación funcional estricta y perezosa «
página 76 »). Los elementos de las colecciones que presentan una evaluación perezosa, como
por ejemplo el tipo de datos Range, no se construyen hasta que son referenciados por primera
vez.
Otra de las características importantes que definirán a una colección estará basada en la
mutabilidad de la misma, es decir, si la colección será mutable o inmutable. Esta característica
definirá si los elementos de una colección podrán cambiar una vez son asignados (colecciones
mutables) o si los elementos no pueden cambiar (la referencia que apunta al elemento de la
colección no podrá cambiar nunca). Se deberá tener en cuenta que, aunque la colección sea
inmutable, los elementos que formen la colección pueden ser mutables.[30]
Finalmente, habrá que decidir si se quiere que los elementos de la colección sean evaluados
secuencialmente o en paralelo, para lo que será imprescindible evaluar previamente las opera-
ciones que se desean aplicar a la colección para determinar si es posible, o no, la ejecución en
paralelo.
Cada una de las características mencionadas anteriormente pueden ser de utilidad en la reso-
lución de problemas. Así, es posible mejorar el tiempo de ejecución de una tarea haciendo que
44
El término “colecciones” se popularizó a raíz de la librería collections de Java.
45
Es usual que los lenguajes de programación incluyan librerías que ofrezcan a los programadores diferentes
estructuras de datos en las que agrupar elementos, (al menos listas y asociacións)[29]
Página 102
Figura 3.2: Diagrama UML de los tipos principales dentro del paquete scala.collection
una colección se evalúe de forma paralela o, en otras ocasiones, se podrá mejorar el rendimiento
utilizando evaluación perezosa.[6]
3.11.1. El paquete scala.collection
Si se observa la Application Programming Interface (API) de Scala se puede apreciar que
hay varios paquetes que empiezan por scala.collection, incluido el propio paquete llamado sca-
la.collection.
Como se puede ver en la figura 3.2, en la parte más alta de la jerarquía se encuentra el
tipo de datos Traversable[A], que agrupa a todos los tipos de datos que pueden ser usados
para representar cualquier cosa y que tienen la propiedad de poder ser recorridos. Los subtipos
de Traversable deberán definir el método foreach, un método que se utilizará para recorrer la
estructura de datos. El método foreach46
es un iterador interno que recibirá una función que
operará sobre los elementos del tipo A como parámetro y que se aplicará a cada uno de los
elementos de la estructura.
Justo debajo de Traversable se encuentra Iterable[A], que como su propio nombre indica,
agrupa las estructuras de datos que disponen de iteradores. Este tipo y sus subtipos dispondrán
de un método, iterator, que devolverá un Iterator[A]. De modo que los subtipos de Iterable[A]
tendrán que implementar el método iterator, el cuál devolverá un iterador sobre los elementos
de la estructura de datos. Esta clase mejora sustancialmente el rendimiento de su superclase
Traversable ya que permitirá, a los métodos que necesiten recorrer una colección, parar el re-
corrido antes de lo que permitiría pararlo la clase Traversable. Iterator dispone de dos métodos
que ayudarán a la hora de recorrer las colecciones: next y hasNext. El método hasNext devol-
verá true si y sólo si en la colección aún quedan elementos por recorrer. El método next avanza
sobre los elementos de la colección y devolverá el siguiente valor de la colección o lanzará una
excepción en el caso de que no queden elementos por recorrer en la colección.
Dentro de las librerías de Scala se pueden diferenciar tres subtipos principales de Itera-
ble[A]:
Set[A]. Dentro del que se puede destacar dos subtipos: BitSet y SortedSet[A]. El tipo de
46
La librería incluye algunas excepciones predefinidas para detener de forma anticipada la iteración por los
elementos de la estructura. De este modo se podrá evitar que, para ciertas operaciones, se produzca una pérdida
innecesaria de ciclos de computación.
Página 103
datos BitSet está optimizado para trabajar con conjuntos de enteros, almacenando un bit
por cada entero que forme parte del conjunto, de modo que si el valor se encuentra en el
conjunto se activa el bit, en otro caso no se activará. El tipo de datos SortedSet[A] es una
versión especializada de Set que se podrá emplear cuando el tipo de datos de los valores
del conjunto A presente un orden natural.
Seq[A]. Define los métodos length y apply, así como representa las colecciones que tienen
una estructura secuencial. Los dos subtipos de Seq[A] son: IndexedSeq y LinearSeq. La
principal diferencia entre ambos reside en cómo se accede a cada uno de los elementos de
estas colecciones. Mientras que los subtipos de IndexedSeq permitirán un acceso eficien-
te a cualquier elemento de la colección, los subtipos de LinearSeq son más ineficientes
para accesos aleatorios a sus elementos ya que tienen que recorrer todos los elementos
anteriores, pero son adecuados cuando se utilizan junto con algoritmos que acceden se-
cuencialmente a los elementos. LinearSeq servirá para indicar que la colección puede ser
descompuesta en el primer elemento de la colección (la cabeza) y el resto (la cola). Las
estructuras de datos conocidas, que mejor se ajustan a este tipo de datos, son las pilas
ya que se puede acceder rápidamente al último elemento agregado pero se tendrían que
desapilar todos los elementos de la misma para llegar al primer elemento añadido. El tipo
IndexedSeq será ideal para todos aquellos algoritmos de propósito general que no tengan
que descomponer la estructura de datos en cabeza y cola.
Map[A,B]. Será utilizada para definir asociaciones47
de pares (clave,valor)48
. En esta es-
tructura de datos sólo habrá un valor por cada clave y las claves son únicas. Estas estruc-
turas de datos ofrecen una solución a aquellas aplicaciones que usan conjuntos de datos
que pueden variar en el tiempo y sobre las que no son frecuentes realizar las operacio-
nes de unión, intersección o diferencia, sino inserciones, eliminaciones y consultas, éstas
últimas serán realizadas por clave.
Si no existiesen limitaciones de espacio, se podría implementar un diccionario simple-
mente utilizando la clave de un elemento como índice en una tabla de búsqueda directa
(utilizando un vector, por ejemplo). Si no existiesen limitaciones de tiempo, se podría
implementar utilizando una lista enlazada con un requerimiento de espacio mínimo. Uti-
lizando éstas soluciones obtendríamos tablas de direccionamiento directo, la cuales sólo
podrán ser aplicadas cuando el conjunto de claves posibles es razonadamente pequeño.
Para solucionar las limitaciones de las tablas de direccionamiento directo se utilizan las
tablas hash. Una tabla hash es una estructura de datos que intenta encontrar un balance
entre estos dos extremos.Cuando el número de claves que se almacenan efectivamente
es pequeño comparado con el número total de claves posibles, una tabla hash ofrece una
alternativa eficiente a una tabla de búsqueda directa, utilizando solamente un vector de
tamaño proporcional a la cantidad de claves almacenadas. En lugar de utilizar la clave
como un índice directamente, el índice se calcula a partir de la clave utilizando una fun-
ción hash. Al utilizar una tabla hash el número de claves efectivas es menor al número
de claves posibles (existirán necesariamente claves que tengan el mismo número ) por lo
que se podrán producir colisiones que habrá que tratar49
. .
A continuación se verán algunas de las colecciones inmutables más importantes dentro del
ecosistema de la librería scala.collection[14].
47
También reciben el nombre de diccionarios.
48
Una asociación es un par (clave/valor). Un diccionario estará formado por un conjunto de asociaciones
49
Una función de hash adecuada podrá minimizar el número de colisiones pero no evitarlas completamente.
Página 104
3.11.2. Iteradores en Scala
A pesar de que el tipo Iterator no es una colección, si que nos proporciona una forma de
recorrer cada uno de los elementos que forman la estructura. Las operaciones básicas que se
realizan con los iteradores son next() y hasNext(). El método next() de un iterador se empleará
para obtener el elemento siguiente del iterador de la colección y avanzar el iterador a un nuevo
elemento. El método hasNext del iterador se utilizará para conocer si existen elementos en la
colección que aún no se han recorrido.
Una forma habitual de utilizar estas funciones es en un bucle while, como se muestra en el
ejemplo:
scala> val it = Iterator("Ejemplo","metodos","next","hasNext","Iterator")
it: Iterator[String] = non-empty iterator
scala> while (it.hasNext){
| println(it.next())
| }
Ejemplo
metodos
next
hasNext
Iterator
Algoritmo 3.13: Métodos next y hashNext en un bucle while
3.11.2.1. Métodos definidos para el tipo Iterator en Scala.
Como las colecciones que se van a estudiar son subclases del tipo Iterable[A]50
, tendrán
que definir el método iterator que devuelva un Iterator[A]. En la tabla 3.1 se reflejan algunas
de las operaciones más usuales dentro del tipo Iterator, que además estarán presentes en el tipo
Iterable[A] y que, por tanto, podremos emplear con las colecciones.
Métodos disponibles en el Tipo Iterator
def hasNext: Boolean Indica si hay otro elemento en el iterador.
def next(): A Devuelve el siguiente elemento del itera-
dor.
def ++(that: =>Iterator[A]): Iterator[A] Concatena dos iteradores
def ++[B >: A](that :=>GenTraversa-
bleOnce[B]): Iterator[B]
Concatena este iterador con otro
def contains(elem: Any): Boolean Indica si el elemento elem está entre los
elementos del iterador.
def count(p: (A) =>Boolean): Int Devuelve el número de elementos del ite-
rador que satisfacen la condición P.
def drop(n: Int): Iterator[A] Avanza el iterador n posiciones. Si la lon-
gitud del iterador es menor que n, avanza-
rá todo el iterador.
50
Es la clase base de todas las colecciones que definen el método iterator.
Página 105
def dropWhile(p: (A) =>Boolean): Itera-
tor[A]
Saltará todos los elementos del iterador
que verifiquen la condición p hasta que
encuentre el primer elemento en el itera-
dor que no verifique dicha condición. El
valor devuelto será un iterador con todos
los elementos restantes.
def duplicate: (Iterator[A], Iterator[A]) Devolverá una dupla con dos iteradores
que iteraran sobre los mismos elementos
que el iterador (y en el mismo orden).
def exists(p: (A) =>Boolean): Boolean Indicará si algún elemento del iterador sa-
tisface la condición p.
def filter(p: (A) =>Boolean): Iterator[A] Devolverá un iterador sobre los elemen-
tos de este iterador que satisfagan la con-
dición p.
def find(p: (A) =>Boolean): Option[A] Devolverá, si existe, el primer valor del
iterador que satisfaga la condición p.
def flatMap[B](f: (A) =>GenTraversa-
bleOnce[B]): Iterator[B]
Crea un nuevo iterador aplicando la fun-
ción f a los elementos de este iterador y
concatenando los resultados.
def forall(p: (A) =>Boolean): Boolean Indicará si todos los elementos del itera-
dor satisfacen la condición p.
def foreach(f: (A) =>Unit): Unit Aplica una función a todos los elementos
del iterador.
def isEmpty: Boolean Devolverá true si el método hasNext de-
vuelve false y viceversa.
def length: Int Devuelve el número de elementos del ite-
rador, situándose el mismo al final.
def map[B](f: (A) =>B): Iterator[B] Devuelve un nuevo iterador cuyos valores
son el resultado de aplicar la función f a
este iterador.
def max:A51
Encuentra el elemento de mayor valor en
el iterador. El iterador se encontrará al
final del mismo después de invocar este
método.
def min:A52
Encuentra el elemento de menor valor en
el iterador. El iterador se encontrará al
final del mismo después de invocar este
método.
def nonEmpty: Boolean Devolverá el mismo valor que el método
hasNext.
def product: A53
Devolverá el producto de los elementos
del iterador.
def size: Int Devolverá el número de elementos del ite-
rador.
51
Signatura completa: def max[B >: A](implicit cmp: Ordering[B]): A
52
Signatura completa: def min[B >: A](implicit cmp: Ordering[B]): A
53
Signatura completa: def product[B >: A](implicit num: Numeric[B]): B
Página 106
def sum: A54
Devolverá la suma de los elementos del
iterador.
def take(n: Int): Iterator[A] Devolverá un iterador con los n primeros
elementos de este iterador.
def zip[B](that: Iterator[B]): Iterator[(A,
B)]
Devolverá un nuevo iterador que conten-
drá duplas con los elementos que se co-
rresponden de este iterador y el iterador
that. El número de elementos del iterador
devuelto será igual al menor número de
elementos de este iterador y del iterador
that.
Tabla 3.1: Métodos del tipo Iterator
3.11.3. Colecciones inmutables
3.11.3.1. Definición de rangos en Scala. La clase Range.
Range es una subclase de IndexedSeq y es el tipo más simple de secuencia que se utiliza-
rá para representar rangos de enteros. Se utilizará principalmente para generar otros tipos de
secuencias o para iterar en bucles for. Una de las propiedades de esta colección es que pre-
senta evaluación perezosa de sus elementos por lo que no serán instanciados y, por tanto, no
consumirán memoria, hasta que se acceda a ellos.
Los operadores más habituales que se emplearán con los rangos son:
El operador ...to ...que se utilizará para crear rangos en los que se incluya la cota supe-
rior dada. En el algoritmo 3.14 se puede ver un ejemplo de la utilización de este operador.
El operador ...until ...que será empleado para definir rangos en los que se excluya la
cota superior dada. En el algoritmo 3.14 se puede ver un ejemplo de la utilización de este
operador.
by. Se empleará para cambiar el paso del rango. En el algoritmo 3.14 se puede ver un
ejemplo de la utilización de este operador.
Por tanto, para definir un rango habrá que indicar la cota inferior, la cota superior y el paso
del rango.
1 scala> val simpleRange = 1 to 10
2 simpleRange: scala.collection.immutable.Range.Inclusive = Range(1,
2, 3, 4, 5, 6, 7, 8, 9, 10)
3
4 scala> val rangeSimple = 1 until 10
5 rangeSimple: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5,
6, 7, 8, 9)
6
7 scala> val stepRange= 1 to 10 by 2
8 stepRange: scala.collection.immutable.Range = Range(1, 3, 5, 7, 9)
Algoritmo 3.14: Definicion de rangos
54
Signatura completa: def sum[B >: A](implicit num: Numeric[B]): B
Página 107
Se podrá acceder a cualquiera de estros tres valores (cota inferior, cota superior y paso)
utilizando los campos start, end y step del objeto del tipo Range que define el rango como se
muestra en el algoritmo 3.15.
1 scala> stepRange.step
2 res8: Int = 2
3
4 scala> rangeSimple.start
5 res9: Int = 1
6
7 scala> rangeSimple.end
8 res10: Int = 10
Algoritmo 3.15: Acceso a las cotas y valor de paso de los rangos
En el algoritmo 3.16 se puede apreciar que una de las aplicaciones de este tipo de datos
será la de generar los elementos de otras estructuras de datos, aunque algunas estructuras tienen
definido el método range que realizará la misma función. Se podrá invocar el método range con
dos parámetros (cota inferior y cota superior) o con tres parámetros (cota inferior, cota superior,
valor de paso) teniendo en cuenta, en ambos casos, que la cota superior quedará excluida del
rango definido.
1
2 scala> val miLista=(1 to 10).toList
3 miLista: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
4
5 scala> val miVector=(1 to 10).toVector
6 miVector: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
7
8 scala> List.range(1,10) // Definimos una lista
9 res18: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)
10
11 scala> Vector.range(0,25,5)
12 res19: scala.collection.immutable.Vector[Int] = Vector(0, 5, 10, 15,
20)
Algoritmo 3.16: Acceso a las cotas y valor de paso de los rangos
Métodos definidos para el tipo Range en Scala.
Además de los métodos vistos anteriormente, en el tipo de datos Range están disponibles
los métodos más usuales de los tipos iterables vistos en la tabla 3.1.
3.11.3.2. Definición de tuplas en Scala. La clase Tuple
Las tuplas son un tipo de datos algebraico que se utilizará para definir pequeñas colecciones
de dos o más elementos heterogéneos. Tuple será de gran utilidad en multitud de ocasiones en
las que se quieran agrupar varios elementos heterogéneos, es decir, elementos de distintos tipos
de datos en una estructura55
. Las tuplas permitirán agrupar hasta un máximo de 22 elementos,
55
Algo que no se podrá hacer con las colecciones que sean subtipos de Iterable como List, Vector...
Página 108
por lo que habrá tuplas del tipo Tuple2, Tuple3, ..., Tuple22. Tuple no hereda de Iterable al no
agrupar elementos homogéneos, sino que es un subtipo de Product.
Como se muestra en el algoritmo 3.17, para crear un tupla sólo se tendrá que encerrar entre
paréntesis los elementos que formen a la misma y separarlos por comas. El tipo de una tupla
dependerá del número de elementos que contenga y del tipo de los mismos.
scala> val miTupla = (1, 2.0, 3.4755F, "hola", true)
miTupla: (Int, Double, Float, String, Boolean) = (1,2.0,3.4755,hola,true)
Algoritmo 3.17: Definición de tuplas
Para cada uno de los tipos de tuplas (Tuple2, ..., Tuple22), Scala define un número adecuado
de métodos para acceder a los elementos de cada tupla. Así, para acceder al primer elemento de
una tupla se usará el método _1, para acceder al segundo elemento utilizaremos el método _2
y así sucesivamente. También se podría utilizar concordancia de patrones (Pattern Matching)
para asignar los valores de la tupla a variables, haciendo uso del guión bajo (_) para descartar
aquellos elementos de la tupla que no se deseen. Se puede ver un ejemplo del uso de estos
métodos y de concordancia de patrones con tuplas en el algoritmo 3.18.
scala> miTupla._1
res0: Int = 1
scala> miTupla._2
res1: Double = 2.0
scala> miTupla._3
res2: Float = 3.4755
scala> miTupla._4
res3: String = hola
scala> miTupla._5
res4: Boolean = true
scala> val (a,b,c,d,e) = miTupla
a: Int = 1
b: Double = 2.0
c: Float = 3.4755
d: String = hola
e: Boolean = true
scala> val (x,_,y,_,z) = miTupla
x: Int = 1
y: Float = 3.4755
z: Boolean = true
Algoritmo 3.18: Acceso a los elementos de una tupla.
Aunque Tuple no es una colección, es posible tratarla como una colección cuando sea nece-
sario, creando un iterador utilizando el método productIterator, como se muestra en el algoritmo
3.19. Incluso es posible transformar la tupla en una colección.
scala> miTupla.productIterator.foreach(i=>println("Valor: "+ i))
Valor: 1
Valor: 2.0
Valor: 3.4755
Valor: hola
Valor: true
scala> miTupla.productIterator.toList
res8: List[Any] = List(1, 2.0, 3.4755, hola, true)
Algoritmo 3.19: Iterando sobre los elementos de las tuplas
Las tuplas de dos elementos (instancias del tipo Tuple2) disponen del método swap que
permitirá intercambiar la posición de los elementos de la tupla como se puede apreciar en el
algoritmo 3.20
Página 109
scala> val tupla = ("hola","hello")
tupla: (String, String) = (hola,hello)
scala> tupla.swap
res9: (String, String) = (hello,hola)
Algoritmo 3.20: Método swap en tuplas de dos elementos
3.11.3.3. Listas en Scala. La clase List
Las listas constituyen una de las estructuras más empleadas en programación funcional por
lo que era de esperar que la librería estándar de Scala incluyera una implementación de las
mismas. Scala implementa en la clase List la estructura de datos de listas enlazadas inmutables
donde se representan colecciones ordenadas y homogéneas de elementos. Esta estructura de
datos es un subtipo de LinearSeq[A] y presentará un rendimiento excelente si añadimos y eli-
minamos elementos siempre de la cabeza de la lista o queremos descomponer nuestra estructura
de datos en cabeza y cola, ya que esta operación de descomposición presentará una complejidad
del orden O(1). Para otros usos, el rendimiento se verá más afectado por lo que es preferible
elegir esta clase cuando se utilicen algoritmos que accedan secuencialmente a sus elementos,
comportamiento que se ajusta más a la especificación de List.
Aunque la clase List suele ser la primera elección por defecto de los programadores que co-
nocen lenguajes como Java o Haskell, veremos como la clase Vector, o incluso la clase Stream,
se puede ajustar más a sus necesidades que la clase List.
La clase List en Scala está definida con una clase abstracta (abstract class) y tiene dos im-
plementaciones (una por cada constructor) que implementan los miembros abstractos isEmpty,
head y tail:
Nil para la lista vacía
::, llamado cons (abreviatura de construir), para construir listas a partir de un elemento y
una lista
List implementa compartición estructural de la cola de la lista por lo que muchas opera-
ciones tendrán un coste nulo o constante de memoria, como ya se vio en la Sección 3.10.2.2:
Compartición estructural (Data Sharing) « página 81 »
Ejemplo:
1 val lista1 = List(3, 2, 1)
2 val with4 = 4 :: lista1 // re-usa lista1, coste --> una instancia de
::
3 val with42 = 42 :: lista1 // re-usa lista1, coste --> una instancia
de ::
4 val shorter = lista1.tail // coste nulo. Usa la misma instancia
2::1::Nil de lista
List se caracteriza por su persistencia y por la compartición estructural, lo cual se tradu-
cirá en beneficios de rendimiento y ahorro de memoria en muchos escenarios si se usa con
corrección[9], siempre y cuando se añadan elementos al principio de la lista o se descomponga
la lista en cabeza y cola. Cuando se necesite actualizar un elemento que se encuentre en la parte
central de la lista será necesario generar toda la lista de elementos previos al elemento que se
desea modificar.
Página 110
Otra de las características del tipo de datos List, es que es covariante, lo cual significa que
para cada par de tipos S y T, si S es un subtipo de T, List[S] será subtipo de List[T]. Como se vio
en la Sección 2.3: Jerarquía de clases en Scala « página 37 », Nothing se encuentra en la parte
más baja de la jerarquía de clases de Scala y es un subtipo de cualquier otro tipo en Scala. Como
las listas son covariantes, List[Nothing] será subtipo de cualquier List[T] (independientemente
de T). Por esta razón, el objeto que representa la lista vacía, List(), puede ser visto como un ob-
jeto de cualquier lista List[T], independientemente del tipo de ésta . Por este motivo, se pueden
escribir asignaciones como la que se muestra en el algoritmo 3.11.3.3.
1 //List() es tambien del tipo List(Int)
2 val intList:List[Int]=List()
Antes de continuar profundizando en el manejo de las listas, se verá como se pueden crear
listas enlazadas homogéneas e inmutables con la clase List, así como las funciones más carac-
terísticas de la misma.
Creación de listas
Como ya se ha visto anteriormente es posible diferenciar:
1. La lista vacía, lista que almacena cero elementos de cualquier tipo.
Usando el constructor de listas vacías Nil. Nil es esencialmente una instancia de
List[Nothing]. Ejemplo:
scala> val lista = Nil
lista: scala.collection.immutable.Nil.type = List()
Indicando la clase List, sin argumentos56
. Ejemplo:
scala> val lista2 = List()
lista2: List[Nothing] = List()
2. Listas que contienen n elementos de un mismo tipo.
Usando el constructor de listas ::, el cual permite añadir un nuevo elemento al prin-
cipio de una lista. Así, haciendo uso de Nil y del constructor, asociativo a la derecha,
:: (cons) se podrán construir listas de forma similar a como se construyen en Lisp.
Ejemplo:
scala> val lista3=1::2::3::4::5::Nil
lista3: List[Int] = List(1, 2, 3, 4, 5)
Indicando la clase List, cuyos argumentos serán los elementos de la lista.
scala> val lista4=List(1,2,3,4,5)
lista4: List[Int] = List(1, 2, 3, 4, 5)
Es posible descomponer una lista en dos partes, la cabeza (head) y la cola (tail). La cabeza
de la lista corresponderá al primer elemento de la misma y la cola estará formada por el resto
de elementos de la lista.
List es una colección que presenta evaluación impaciente, es decir, cuando se construye una
lista se calcula tanto la cabeza como la cola de la misma. Dentro de la librería collection de
56
Haciendo uso del método apply del objeto acompañante de List
Página 111
Scala se puede encontrar otro tipo de implementación de lista enlazada, Stream, que presen-
ta evaluación perezosa y que se verá en la Subsubsección 3.11.3.5: Flujos en Scala. La clase
Stream « página 115 ».
Métodos definidos para el tipo List en Scala.
Además de los métodos vistos anteriormente para crear listas, en el tipo de datos List se
pueden aplicar las funciones más usuales de los tipos iterables vistas en la tabla 3.1. Otras
operaciones características de las listas se encuentran definidas en la tabla 3.2
Métodos del Tipo List
def def +(elem: A): List[A] Añade un elemento al final de la lista.
def :::(prefix: List[A]): List[A] Devuelve el resultado de añadir los ele-
mentos de la lista prefix delante de la lista.
def apply(n: Int): A Devuelve el elemento n-ésimo de la lista.
def distinct: List[A] Devuelve una lista con los elementos sin
repeticiones.
def dropRight(n: Int): List[A] Devuelve una lista con los elementos de
esta lista excepto los n últimos.
def equals(that: Any): Boolean Compara la lista con cualquier otra se-
cuencia.
def init: List[A] Devuelve una lista con los elementos de
esta lista exceptuando el último.
def intersect(that: Seq[A]): List[A] Realiza la intersección entre los elemen-
tos de esta lista y de otra secuencia.
def iterator: Iterator[A] Devuelve un iterador sobre los elementos
de la lista.
def last: A Devolverá el último elemento de la lista.
def reverse: List[A] Devuelve una lista con los elementos de
esta lista en orden inverso.
def sorted[B >: A]: List[A] Ordenará la lista acorde a un orden.
def takeRight(n: Int): List[A] Devuelve los últimos n elementos.
Tabla 3.2: Métodos del tipo List
Dentro de la clase List se pueden encontrar muchas más funciones que serán de gran utilidad
a la hora de manejar listas. Se recomienda ver la API de Scala, en scala-lang.org donde se
pueden consultar las diferentes funciones disponibles para la clase List: isEmpty,length, ++,
drop, dropWhile, take, takeWhile, map, foldRight, foldLeft,....
Ejercicios sobre listas
Responder a las siguientes cuestiones teniendo en cuenta listas del tipo List[+A] visto ante-
riormente:
Ejercicio 41. Si se define la función fun tal que:
Página 112
1 def fun(xs: List[Int], ys: List[Int]):List[Int] = (xs, ys) match {
2 case (Nil, ys) => ys
3 case (xs, Nil) => xs
4 case (h::t, ys) => h :: fun(t, ys)
5 }
¿Cuál será el resultado de evaluar la expresión fun(List(1,2), List(3,4,5 ))?
(List(1,2), List(3,4,5))
List(List(1,2), List(3,4,5))
List(1,2,3,4,5)
Ejercicio 42. Suponiendo que se tiene el siguiente programa:
1 val a = List(4,3,2,1)
2
3 val b = a.sorted
4
5 println(a)
¿Qué resultado se obtendrá tras su ejecución?
List(4, 3, 2, 1)
List(1, 2, 3, 4)
Ninguna de las respuestas es correcta
Ejercicio 43. Si se define la variable inmutable a como:
1 val a = List(1, 2, 3, 4, 5)
¿Cuál será el resultado de evaluar la expresión a.map(x =>x * 2).filter(x =>x <5).reduce((x, y)
=>x * y)?
8
16
Error al intentar reasignar un valor a una variable inmutable
List(2, 4)
Ejercicio 44. Definir la función miMax, que dada una lista de enteros devuelva el del mayor
valor:
Ejercicio 45. Definir las funciones suma y producto tales que, que dada una lista de enteros
como parámetro devuelvan, respectivamente, la suma y el producto de todos sus elementos.
scala> suma(List(1,2,3,4,5))
res0: Int = 15
scala> producto(List(1,2,3,4,5))
res2: Int = 120
Página 113
Ejercicio 46. Definir en Scala la función diferencias que devuelva la diferencia que hay entre
cada número adyacente. Si la lista está formada por un único elemento devolverá la lista vacía.
Ejercicio 47. Definir una función aplana que dada una lista de listas de elementos, nos devuelva
una lista con los elementos de cada una de las listas.
1 aplana(List(List(1,2,3),List(4,5,6),List(7,8,9))
¿Se podría haber solucionado el problema utilizando foldRight o foldLeft ?
Escribir la solución o razonar por qué no se pueden utilizar.
Ejercicio 48. Definir una función aEntero al que dada una lista de enteros pasada por paráme-
tros, devuelva el número resultante de unir todos ellos.
Ejercicio 49. Definir la función aLista que tome como parámetro un número entero y devuelva
una lista con cada uno de sus dígitos.
Ejercicio 50. Definir la función miLength que calcule el número de elementos de una lista.
Ejercicio 51. Implementar una función que devuelva el penúltimo elemento de una lista pasada
como parámetro. En caso de que la lista esté vacía o tenga un sólo elemento se lanzará un error.
Ejercicio 52. Implementar una función que examine una lista de enteros y determine si es un
palíndromo.
Ejercicio 53. Definir una función que devuelva el valor del elemento enésimo, pasado por
parámetro, de una lista.
scala> enesimo(2,List(1,1,2,3,4,5,6,7))
res0: Int = 2
Nota: Por convenio el primer elemento de una lista es el 0-ésimo.
Ejercicio 54. Definir una función que elimine los elementos duplicados consecutivos en una
lista.
Ejercicio 55. Definir una función que duplique todos los elementos de una lista.
Ejercicio 56. Definir una función que repita N veces todos los elementos de una lista.
Ejercicio 57. Definir una función que agrupe los elementos duplicados de una lista, pasada por
parámetro, en sublistas dentro de una lista.
3.11.3.4. Vectores en Scala. La clase Vector
La estructura de datos Vector es un subtipo de IndexedSeq y es la estructura de datos que
mejor se ajusta a la mayoría de algoritmos de propósito general. Las operaciones de acceso a
un elemento aleatorio en un Vector presentan una complejidad del orden O(log32(N)), lo cual,
usando índices de 32 bits, representa un tiempo de acceso constante pequeño. El tipo de datos
Vector es inmutable y presenta unas características de compartición estructural razonables.
El hecho de que Vector tenga un factor de ramificación de 32 hace que presente diversas
ventajas:
Los tiempos de búsqueda y de actualización de un elemento son excelentes, así como los
tiempos de las operaciones para añadir un elemento al principio o al final de la estructura
de datos.
Decente coherencia en caché, aumentando los aciertos57
en la misma ya que los elementos
57
Un acierto en caché se produce cuando el dato que requiere el procesador se encuentra en caché.
Página 114
que se encuentren próximos en nuestra colección también deberán de estar próximos en
memoria.
La eficiencia de este tipo de datos, junto con el hecho de que sea una estructura inmutable,
y por tanto, una estructura que puede ser compartida por diferente hilos de ejecución sin temor
a que efectos colaterales puedan dar lugar a resultados no deseados, convierten a Vector en la
secuencia más potente disponible en la biblioteca de Scala.
Como se muestra en el algoritmo, se pueden crear vectores de forma análoga a como se
creaban listas, excepto usando el operador :: de listas.
scala> val simpleVector = Vector(1,2,3,4)
simpleVector: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3, 4)
Algoritmo 3.21: Definición de Vector.
En el algoritmo 3.22 se muestra como, en lugar del operador ::, las estructuras de datos de
tipo Vector presentan los operadores +: y :+ para añadir un elemento al principio del vector o
al final del mismo respectivamente58
.
scala> simpleVector :+ 5 // Agregamos un elemento al final del vector
res5: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3, 4, 5)
scala> 0 +: simpleVector // Agregams un elemento al principio del vector
res6: scala.collection.immutable.Vector[Int] = Vector(0, 1, 2, 3, 4)
Algoritmo 3.22: Agregar elementos a un Vector
Las operaciones más frecuentes sobre vectores son similares a las operaciones vistas ante-
riormente sobre listas59
.
3.11.3.5. Flujos en Scala. La clase Stream
Stream es un subtipo de LinearSeq que se utilizará para definir colecciones de elementos
cuando se desee que presenten evaluación perezosa. Haciendo uso de los flujos de datos se
podrán representar colecciones con un número infinito de elementos, sin que se produzca un
desbordamiento de memoria60
. Los flujos de datos guardan el valor de los elementos que ya han
sido calculados, lo cual es una ventaja a la hora de acceder a estos elementos de forma eficiente
ya que no se han de recalcular estos elementos pero, a su vez, puede representar un problema
de memoria el hecho de que coexistan un número elevado de elementos.
La clase Stream de Scala es similar a la clase List de Scala vista anteriormente. Stream está
compuesta por el flujo de datos vacío (Stream.empty) y por sucesivas operaciones cons que
utilizarán el método #:: (en lugar del método :: que se usaba en la colección List). También se
podrá definir un flujo de datos haciendo uso del método apply de su objeto acompañante, como
se puede comprobar en el algoritmo 3.23.
58
Una buena regla nemotécnica para distinguir ambos operadores podría ser tener en cuenta que el símbolo dos
puntos (:) siempre se encuentra próximo a la secuencia mientras que el símbolo + se encuentra próximo al elemento
59
En la API de Scala podremos encontrar muchas más funciones que serán de gran utilidad a la hora de manejar
vectores
60
En lugar de almacenar los elementos, Stream almacena funciones objeto para calcular tanto la cabeza (head)
del flujo de datos como el resto de los elementos (tail).
Página 115
scala> val miStream=Stream.range(1,10)
miStream: scala.collection.immutable.Stream[Int] = Stream(1, ?)
scala> miStream(3)
res0: Int = 4
scala> println(miStream)
Stream(1, 2, 3, 4, ?)
scala> val otroStream = 1 #:: 2 #:: 3 #:: 5 #:: 7 #:: Stream.empty
otroStream: scala.collection.immutable.Stream[Int] = Stream(1, ?)
scala> otroStream(2)
res2: Int = 3
scala> println(otroStream)
Stream(1, 2, 3, ?)
Algoritmo 3.23: Definición de Streams
Un uso muy apropiado de Stream sería el de calcular el siguiente valor de la secuencia
haciendo uso de los valores previamente calculados. Una aplicación en la que se puede ver
perfectamente la utilización de Stream en este sentido sería el cálculo de la serie de Fibonacci,
en el que el siguiente valor de la secuencia se calcula sumando los dos valores anteriores. En
el algoritmo 3.24 se muestra una posible implementación de la serie de Fibonacci utilizando
Stream y el cálculo de los diez primeros valores de la serie.
scala> val serieFibonacci = {
| def fib(a:Int,b:Int):Stream[Int] = a #:: fib(b,a+b)
| fib (0,1)
| }
serieFibonacci: Stream[Int] = Stream(0, ?)
scala> serieFibonacci take 10 toList
res5: List[Int] = List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
Algoritmo 3.24: Cálculo de la serie de Fibonacci utilizando Stream
Los programadores que han tenido un contacto previo con la programación funcional en
lenguajes como Haskell suelen utilizar List por defecto para representar listas. Hay que tener en
cuenta que, como se ha comentado anteriormente, List presenta evaluación impaciente o estricta
mientras que la listas en Haskell presentan evaluación perezosa. Además Stream es estricta en
el elemento, una vez producido, mientras que las listas en Haskell no lo son. Cuando se quiera
crear una lista con evaluación perezosa habrá que usar Stream en Scala en lugar de List[6].
Las operaciones más frecuentes sobre flujos son similares a las operaciones vistas anterior-
mente sobre listas. 61
.
3.11.3.6. Conjuntos en Scala. La clase Set
El concepto de conjunto es matemático. Hay dos diferencias fundamentales entre las se-
cuencias (tipo Seq) y los conjuntos (tipo Set). La diferencia principal que podemos encontrar
entre ambos tipos es que los conjuntos no permiten la existencia de elementos duplicados. Es
decir, si en un conjunto compuesto por diferentes elementos se trata de añadir un elemento que
es igual62
a otro existente en el conjunto, dicho elemento no se añadirá quedando el tamaño del
conjunto igual que antes de realizar la operación de añadir. Por tanto, se elegirá un conjunto
cuando se desee que la estructura de datos no presente elementos duplicados o cuando se quiera
comprobar si un elemento está en la misma.
61
En la API de Scala se pueden encontrar muchas más funciones que serán de gran utilidad a la hora de manejar
flujos
62
En términos del método ==.
Página 116
Otra de las diferencias fundamentales que existen entre los tipos Set y Seq es que no se
puede garantizar el orden entre los elementos de los conjuntos63
.
La forma más fácil de definir conjuntos será haciendo uso del método apply definido en el
objeto acompañante, al que se le pasará como argumento los elementos del mismo:
Ejemplo:
1 scala> val numeros = Set(1,2,3,4,5) //Definimos un nuevo conjunto
2 numeros: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)
3
4 scala> numeros + 7 //Agregamos el 7 al conjunto definido anteriormente
5 res0: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 7, 3, 4)
6
7 scala> numeros + 4 //Agregamos el 4 al conjunto numeros
8 res1: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)
9
10 scala> res0 + 4 //Agregamos el 4 al conjunto resultante tras agregar el 7
11 res2: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 7, 3, 4)
12
13 scala> res0 + -25 // Agregamos el -25 al conjunto resultante tras agregar el 7
14 res3: scala.collection.immutable.Set[Int] = Set(5, 1, 2, -25, 7, 3, 4)
15
16 scala> res3 - 1 //Eliminamos el 1 del conjunto resultante de agregar -25 tras agregar el 7
17 res4: scala.collection.immutable.Set[Int] = Set(5, 2, -25, 7, 3, 4)
18
19 scala> res4 + 123 //Agregamos el 123 al conjunto resultante de eliminar el 1
20 res5: scala.collection.immutable.Set[Int] = Set(5, 2, -25, 7, 3, 123, 4)
Algoritmo 3.25: Definición y propiedades fundamentales de los conjuntos
En el algoritmo 3.25 se puede ver como se cumplen las propiedades anteriormente definidas:
Por defecto, cuando se define una colección del tipo Set, se obtiene una estructura de
datos inmutable.
El conjunto resultante tras añadir el número 4 a un conjunto en el que este número ya
existe es el conjunto original, sin modificaciones.
Tras realizar diferentes operaciones de añadir/eliminar elementos de un conjunto se com-
prueba que el orden del conjunto no es previsible, ni se puede asegurar.
Otra de las diferencias que se pueden apreciar con respecto a la clase List es referente al
acceso a los elementos. En este tipo de datos, en lugar de obtener un valor ubicado en una
posición de nuestra colección (como ocurría en el caso de los subtipos de LinearSeq), se utilizará
el método apply (definido en cualquiera de los subtipos de Set[A]) para saber si un elemento
está o no está en un conjunto.
Recorriendo conjuntos
La forma más fácil de recorrer los elementos de un conjunto será haciendo uso del bucle for.
No se podrán utilizar otros bucles, como por ejemplo while, ya que no se accede a los elementos
del conjunto por la posición de los mismos64
.
Ejemplo:
63
Excepto si se trata de un SortedSet ya que el orden de los elementos de este tipo coincidirá con el orden natural
de los elementos, aunque se puede considerar este caso como la excepción que confirma la regla.
64
Para esto, se hará uso de un iterador.
Página 117
scala> for (x <- numeros){println(x)}
5
1
2
3
4
Algoritmo 3.26: Recorriendo los elementos de un conjunto
Métodos definidos para el tipo Set en Scala.
Además de los métodos vistos anteriormente para crear conjuntos, en el tipo de datos Set
se pueden aplicar las funciones más usuales de los tipos iterables vistas en la tabla 3.1. Otras
operaciones características de los conjuntos se encuentran definidas en la tabla 3.3
Métodos del Tipo Set
def def +(elem: A): Set[A] Crea un conjunto nuevo que contendrá el
elemento elem además de los elementos
de este conjunto, excepto si elem ya se en-
cuentra en este conjunto.
def -(elem: A): Set[A] Devuelve el resultado de eliminar el ele-
mento elem del conjunto.
def apply(elem: A): Boolean Indica si un elemento pertenece al conjun-
to
def & (that: Set[A]): Set[A] Devuelve un conjunto con los elementos
de este conjunto y los elementos del con-
junto that.
def &∼ (that: Set[A]): Set[A] Devuelve la diferencia entre este conjunto
y el conjunto pasado como argumento.
def ++(elems: Set[A]): Set[A] Concatena los elementos de este conjunto
con los elementos del conjunto elems .
def diff(that: Set[A]): Set[A] Devuelve la diferencia entre este conjunto
y el conjunto pasado como argumento.
def intersect(that: Set[A]): Set[A] Realiza la intersección entre los elemen-
tos de este conjunto y del conjunto pasado
como argumento.
def iterator: Iterator[A] Devolverá un iterador sobre los elementos
del conjunto.
def last: A Devolverá el último elemento del conjun-
to.
def isEmpty: Boolean Devolverá true si el conjunto no tiene nin-
gún elemento.
def subsetOf(that: Set[A]): Boolean Devolverá true si este conjunto es un sub-
conjunto del conjunto pasado como argu-
mento.
Tabla 3.3: Métodos del tipo Set
El tipo Set presenta algunos métodos muy interesantes que dan respuesta a las propieda-
des de este tipo de estructuras de datos. En la API de Scala se pueden encontrar muchas más
funciones que pueden ser de gran utilidad a la hora de manejar conjuntos.
Página 118
3.11.3.7. Asociaciones en Scala. La clase Map
Las asociaciones son una de las estructuras de datos más utilizadas en el desarrollo de pro-
gramas ya que presentan una búsqueda eficiente de valores en base a sus claves. Su nombre, al
igual que ocurría en el caso de los conjuntos, tiene su origen en las matemáticas donde elemen-
tos de un conjunto son asociados con elementos de otro conjunto65
. El tipo de datos Map[A,B]
asocia elementos del tipo paramétrico A, también llamado tipo de las claves, a otro tipo de datos
B (el tipo de los valores). El tipo de las claves y el tipo de los valores pueden ser iguales, aunque
en la mayoría de las ocasiones serán de tipos diferentes.
Como se veía que ocurría en el caso de los tipos de datos para conjuntos, la forma más fácil
de definir asociaciones será haciendo uso del método apply definido en el objeto acompañante,
al que se le pasará como argumento los elementos del mismo. Los siguientes ejemplos pueden
ayudar a entender la utilidad de las asociaciones:
scala> val simpleMap:Map[Int,String]=Map(1->"uno",2->"dos",3->"tres")
simpleMap: Map[Int,String] = Map(1 -> uno, 2 -> dos, 3 -> tres)
Algoritmo 3.27: Definicion de Maps.
En el algoritmo 3.27 se ha definido un Map simple. Los tipos de los parámetros son Int y
String. Los elementos que se han pasado a la asociación son tuplas en las que el primer elemento
es la clave y el segundo elemento es el valor. A la hora de definir los valores se ha empleado
una sintaxis clara, utilizando el operador ->, aunque como se verá en el algoritmo 3.28, también
se podría haber definido la asociación introduciendo las tuplas directamente (sin necesidad de
utilizar el operador ->).
scala> val miMapa:Map[String,String]=Map(("hola","hello"),("adios","bye"))
miMapa: Map[String,String] = Map(hola -> hello, adios -> bye)
Algoritmo 3.28: Definicion de Maps con tuplas.
En el algoritmo 3.29 se puede apreciar como, para acceder a los elementos de una asocia-
ción, se hace uso de las claves. Así, cuando se consulte por el valor del elemento cuya claves es
2, se obtiene como resultado “dos”, del tipo String, como era de esperar.
scala> simpleMap(2)
res0: String = dos
Algoritmo 3.29: Recuperar valores en Maps
Otra similitud entre los conjuntos y las asociaciones es la forma en la que pueden añadir o
eliminar elementos de estas estructuras de datos. En las asociaciones se hará uso de los operado-
res + o -, para añadir o eliminar elementos respectivamente, como se puede ver en el algoritmo
3.30.
simpleMap + (4->"cuatro")
res1: scala.collection.immutable.Map[Int,String] = Map(1 -> uno, 2 -> dos, 3 -> tres,
4 -> cuatro)
scala> simpleMap - 3
res3: scala.collection.immutable.Map[Int,String] = Map(1 -> uno, 2 -> dos)
Algoritmo 3.30: Agregar y eliminar elementos en un Map
65
Una función matemática hace exactamente esto, asociar elementos de un conjunto con elementos de otro
conjunto.
Página 119
Otra de las similitudes que comparten los tipos datos Set y Map es el hecho de que, por
defecto, cuando se define un Map o un Set se obtendrá una estructura de datos inmutables66
.
Para recorrer las asociaciones se podrán utilizar funciones de orden superior o el bucle for,
al igual que con el resto de las colecciones. Para las asociaciones habrá que prestar especial
atención ya que se deberá tener en cuenta que los elementos de una asociación se almacenan
como tuplas de pares (clave,valor). En el algoritmo 3.31 se muestra un ejemplo simple en el que
se recorren los elementos de la asociación definida anteriormente.
scala> for ((clave,valor)<-simpleMap) yield clave*2
res4: scala.collection.immutable.Iterable[Int] = List(2, 4, 6)
Algoritmo 3.31: Recorrer los elementos de un Map
Métodos definidos para el tipo Map en Scala.
Además de los métodos vistos anteriormente para crear asociaciones, en el tipo de datos
Map se pueden aplicar las funciones más usuales de los tipos iterables vistas en la tabla 3.1.
Otras operaciones características de las asociaciones están definidas en la tabla 3.4
Se han destacado algunos métodos que no han sido vistos en colecciones anteriores o que
son propios del tipo Map. Al igual que ocurría con las demás colecciones, en la API de Scala se
pueden encontrar muchas más funciones que pueden ser de gran utilidad a la hora de manejar
asociaciones.
3.11.3.8. Selección de una colección
Uno de los aspectos más importantes a tener en cuenta a la hora de decidir que colección
escoger será el rendimiento que ofrece. Como se ha visto anteriormente, el rendimiento es
una característica que está muy relacionada con la implementación de la colección y con las
operaciones que se vayan a realizar sobre la misma. En la tabla 3.5 se muestra el rendimiento
de las operaciones más importantes que se pueden realizar sobre las secuencias.
Para representar la complejidad de cada operación se han utilizado las siguientes abreviatu-
ras:
RC. Es una operación rápida que tomará un tiempo constante. La complejidad de estas
operaciones es de O(1).
Lin. Es una operación lineal y tomará un tiempo proporcional a la dimensión de la colec-
ción. La complejidad de estas operaciones es de O(n).
DC. Es una operación que tomará un tiempo constante, asumiendo ciertas características
en la colección como el tamaño de un vector o la distribución de las claves hash.
-. Operación no soportada.
Log. La operación tomará un tiempo proporcional al logaritmo del tamaño de la colec-
ción. La complejidad de estas operaciones será de O(log(N)).
En la tabla 3.6 se muestra la eficiencia de las operaciones más comunes en las estructuras
de datos Map y Set.
66
No obstante, existe una versión mutable tanto para los conjuntos como las asociaciones. Para utilizar la versión
mutable se tendrá que importar la librería scala.collection.mutable y luego se tendrá que hacer referencia a estas
estructuras mutables como mutable.Map o mutable.Set.
Página 120
Métodos del Tipo Map
def ++(xs: Map[(A, B)]): Map[A, B] Devuelve un nuevo Map que contendrá
los pares (clave,valor) de esta asociación
y los de la asociación pasada como argu-
mento.
def get(key: A): Option[B] Devuelve el valor asociado a una clave
pasada como argumento.
def apply(key: A): B Nos devuelve el valor asociado a la clave
pasada como argumento. En caso de no
existir nos devolvería el resultado del mé-
todo default.
def default(key: A): B Define el valor por defecto que devolve-
rá la asociación cuando la clave buscada
no se encuentre. El método devolverá una
excepción pero puede ser sobreescrito en
las diferentes subclases.
def keys: Iterable[A] Devuelve un iterador sobre todas las cla-
ves.
def remove(key: A): Option[B] Elimina el par (clave,valor) cuya clave
coincida con el argumento pasado.
def iterator: Iterator[A] Nos devolverá un iterador sobre los ele-
mentos de la colección.
def last: A Nos devolverá el último elemento del
map.
def isEmpty: Boolean Nos devolverá true si la asociación no
contiene ningún elemento.
Tabla 3.4: Métodos del tipo Map
3.11.3.9. Colecciones como funciones
Todas las colecciones que se han visto pueden ser utilizadas como funciones ya que heredan
de PartialFunction, el cual hereda del tipo estándar de función en Scala. El comportamiento
que tendrán las colecciones como funciones dependerá de la implementación del método apply
de la colección67
.
Por ejemplo, el tipo de datos Set[T] se podrá utilizar como una función T =>Boolean, el
tipo de datos Seq[T] puede ser utilizado como una función Int =>T o el tipo Map[C,V] como
una función C =>V.
Por tanto, se podrán utilizar las colecciones en lugares en los que se esperaría que apareciera
una función.
scala> val conj=Set(1,2,3)
conj: scala.collection.immutable.Set[Int] = Set(1, 2, 3)
scala> 1 to 2 map conj
res1: scala.collection.immutable.IndexedSeq[Boolean] = Vector(true, true)
67
Distinto del método apply del objeto acompañante de la colección y que se utiliza para construir las mismas.
Página 121
Eficiencia de las operaciones sobre secuencias
head tail apply actualización insertar(por la cabeza) insertar(por la cola)
List RC RC Lin Lin C Lin
Vector DC DC DC DC DC DC
Stream RC RC Lin Lin C Lin
Range RC RC RC - - -
Tabla 3.5: Secuencias. Eficiencia de las operaciones.
Eficiencia de las operaciones en conjuntos y maps
buscar añadir eliminar minimo
HashSet & HashMap DC DC DC Lin
TreeSet & TreeMap Log Log Log Log
Tabla 3.6: Set y Map. Eficiencia de las operaciones
3.11.4. Expresiones for como una combinación elegante de funciones de
orden superior
En general, no se permite el uso de estructuras de control iterativas en el ámbito de la pro-
gramación funcional ya que se incumpliría uno de los principios de la programación funcional:
el uso de variables inmutables. En la Subsubsección 1.5.2.3: Bucles for « página 25 » se intro-
dujeron los bucles for como una estructura de control iterativa aunque ya se advirtió que este
tipo de bucles en Scala presentaban unas características muy especiales que los convertirían en
una herramienta muy útil en la programación funcional en Scala.
Existen dos motivos por los que en Scala, los bucles for son una herramienta muy útil dentro
del paradigma de la programación funcional:
1. La traducción que realiza el compilador de Scala de esta estructura de control. En realidad,
el compilador de Scala expresa los bucles for que almacenan un resultado con yield en
términos de las funciones de orden superior map, flatMap y filter. Los bucles for que no
almacenen resultados serán traducidos por el compilador en términos de las funciones de
orden superior foreach y filter.
2. Anteriormente se ha visto como combinando las funciones de orden superior map, flat-
map y filter se pueden resolver problemas complejos, aunque el nivel de abstracción de
las mismas pueden dar lugar a que el código resultante sea difícil de interpretar. Las ex-
presiones for nos proporcionarán expresiones más claras para resolver estos problemas.
3.11.4.1. Traducción de expresiones for con un generador
Un bucle for con un generador presentará la siguiente construcción:
for (x <- expr1) yield expr2
que será traducida por el compilador a la siguiente expresión:
expr1 map (x=>expr2)
con la cual, como se puede comprobar en el algoritmo 3.32, se obtendrá el mismo resultado que
con el bucle for.
Página 122
scala> val miLista = 1::2::3::4::5::Nil
miLista: List[Int] = List(1, 2, 3, 4, 5)
scala> for (x <- miLista) yield(x*2)
reso: List[Int] = List(2, 4, 6, 8, 10)
scala> miLista map (x => x*2)
res1: List[Int] = List(2, 4, 6, 8, 10)
Algoritmo 3.32: Ejemplo traducción bucle for con un generador
3.11.4.2. Traducción de expresiones for con un generador y un filtro
Si en nuestro bucle for, además de un generador, aparece un filtro:
for (x <- expr1 if expr2) yield expr3
el compilador lo traducirá a la siguiente expresión:
for (x <- expr1 filter (x => expr2)) yield expr3
con lo que la traducción final quedará de la siguiente manera:
expr1 filter (x => expr2) map (x => expr3)
En el algoritmo 3.33 se puede comprobar como, tanto el bucle for, como la expresión a la
que es traducida, obtienen los mismos resultados.
scala> def esPar(x:Int):Boolean = (x % 2)==0
esPar: (x: Int)Boolean
scala> for (x <- miLista; if esPar(x)) yield (x,"es par")
res2: List[(Int, String)] = List((2,es par), (4,es par))
scala> miLista filter (esPar) map (x=>(x,"es Par"))
res3: List[(Int, String)] = List((2,es Par), (4,es Par))
Algoritmo 3.33: Ejemplo traducción de expresiones for con un generador y un filtro
3.11.4.3. Traducción de expresiones for con dos generadores
Si ahora se definiera un bucle for con dos generadores anidados:
for (x <- expr1; y <- expr2) yield expr3
que sería traducido por el compilador a la siguiente expresión:
expr1 flatmap (x => for (y <- expr2) yield (expr3))
en la que se puede observar que hay otra expresión for dentro de la función que se le pasa a
flatmap, que también será traducida por el compilador generando finalmente la expresión:
expr1 flatMap (x => expr2 map (y => expr3))
En el algoritmo 3.34 se puede comprobar que la expresión final generada por el compilador
y el bucle for con dos generadores son equivalentes
scala> val auxLista= ’a’::’b’::’c’::Nil
auxLista: List[Char] = List(a, b, c)
scala> for (x <- auxLista; y <- miLista) yield (x, y*y)
res4: List[(Char, Int)] = List((a,1), (a,4), (a,9), (a,16), (a,25), (b,1), (b,4), (b,9),
Página 123
(b,16), (b,25), (c,1), (c,4), (c,9), (c,16), (c,25))
scala> auxLista flatMap (x=> miLista map (y=>(x,y*y)))
res33: List[(Char, Int)] = List((a,1), (a,4), (a,9), (a,16), (a,25), (b,1), (b,4), (b,9),
(b,16), (b,25), (c,1), (c,4), (c,9), (c,16), (c,25))
Algoritmo 3.34: Ejemplo de traducción de expresiones for con dos generadores
3.11.4.4. Traducción de bucles for
La traducción de los bucles que for sigue el mismo patrón que el visto anteriormente en la
traducción de expresiones for. La diferencia es que en lugar de utilizar las funciones de orden
superior map y flatMap se utilizará foreach.
De este modo, un bucle for definido de la siguiente forma:
for (x <- expr1; if expr2; y <- expr3) {cuerpo del bucle}
será traducido por el compilador a la siguiente expresión intermedia:
expr1 filter (expr2) foreach(x => for (y <- expr3)) {cuerpo del
bucle}
la cual, será finalmente traducida a la siguiente expresión:
expr1 filter (expr2) foreach(x => expr3 foreach (y=> {cuerpo del
bucle}))
En el algoritmo 3.35 se muestra como efectivamente se obtiene el mismo resultado utilizan-
do bucles for y su traducción equivalente. Para ilustrar el ejemplo se han definido dos listas de
enteros, miLista y tmpLista. Se desea obtener el resultado total de sumar las parejas de elemen-
tos pares de ambas listas. Se puede apreciar ver como el resultado total es acumulado en una
variable mutable.
scala> var a = 0
a: Int = 0
scala> val tmpLista = 6::7::8::9::10::Nil
tmpLista: List[Int] = List(6, 7, 8, 9, 10)
scala> for (x<-miLista;if esPar(x);y<-tmpLista;if esPar(y))
{println(x + " + " + y+" = "+(x+y));a+=x+y};println("Total = "+a)
2 + 6 = 8
2 + 8 = 10
2 + 10 = 12
4 + 6 = 10
4 + 8 = 12
4 + 10 = 14
Total = 66
scala> a=0
a: Int = 0
scala> miLista filter esPar foreach(x=> tmpLista filter esPar foreach (y=>
{println(x + " + " + y+" = "+(x+y));a+=x+y}));println("Total = "+a)
2 + 6 = 8
2 + 8 = 10
2 + 10 = 12
4 + 6 = 10
4 + 8 = 12
4 + 10 = 14
Total = 66
Algoritmo 3.35: Ejemplo traducción bucle for genérico
Página 124
3.11.4.5. Definición de map, flatMap y filter con expresiones for
A continuación se mostrará como es posible definir las funciones de orden superior map,
flatMap y filter de los tipos de datos definidos, haciendo uso de las expresiones for:
def map[A, B](xs: List[A], f: A => B): List[B] =
for (x <- xs) yield f(x)
def flatMap[A, B](xs: List[A], f: A => List[B]): List[B] =
for (x <- xs; y <- f(x)) yield y
def filter[A](xs: List[A], p: A => Boolean): List[A] =
for (x <- xs if p(x)) yield x
3.11.4.6. Uso generalizado de for en estructuras de datos
Como se ha visto, el compilador necesita de las funciones de orden superior map, flatMap
y filter para la traducción de las expresiones for que devuelven algún valor, mientras que para
los bucles for que no devuelven ningún valor, sólo necesita que estén definidas las funciones
de orden superior foreach y filter. Por tanto, podremos recorrer todas las colecciones que im-
plementen estas funciones (como Stream, List, Vector, ...) haciendo uso de los bucles for. Pero
además también se pueden disfrutar de todas las ventajas de las expresiones for vistas anterior-
mente en las estructuras de datos definidas por el usuario siempre y cuando implementen las
funciones map, flatMap, filter y foreach. Si las estructuras de datos no definen todas estas fun-
ciones, pero si algunas de ellas, también será posible disfrutar de algunas de estas ventajas. A
continuación se muestran las diferentes posibilidades que existen, dependiendo de las funciones
de orden superior definidas:
Si sólo se define la función map, se podrá definir expresiones for que tengan un único
generador.
Si se definen las funciones map y flatMap se podrán definir expresiones for que tengan
varios generadores.
Si se encuentra definida la función foreach, podremos definir bucles for que no devuelvan
ningún valor, independientemente del número de generadores que tengan.
Si se ha definido la función filter, se podrán definir expresiones for que incluyan filtros
como se ha visto anteriormente
En la siguiente sección se verá un concepto muy relacionado con la programación funcional,
las mónadas. Las mónadas y las funciones de orden superior map, flatMap y filter están muy
relacionadas ya que se definirán en las mónadas estas funciones que, además, caracterizarán las
mismas junto con el método unit. Por tanto, se pueden ver las funciones de orden superior map,
flatMap y filter como una versión orientada a objetos de la definición del concepto de mónada
característico de la programación funcional.
Además, como se ha visto anteriormente, las expresiones for son una traducción de las
funciones map, flatMap y filter que permitirán escribir de forma más clara y comprensible
ciertos algoritmos y que, por tanto, también podrán utilizarse con las mónadas.
Por todo esto, es posible imaginar que los bucles for juegan un papel muy importante dentro
de Scala, más allá de ser simples estructuras de control o iteradores de colecciones, ya que
siempre que para cualquier tipo de datos en el que se encuentren definidas las funciones map,
flatMap y filter se podrán utilizar expresiones for.
Página 125
3.11.5. Ejercicios
Ejercicio 58. Implementar una función construirMap la cual devuelva un valor del tipo Map a
partir de cualquier tipo de secuencia dada y de una función que permitirá crear las claves de la
asociación.
1 def contruirMap[A,B](datos: Seq[A], f: A=>B): Map[B,A]
Ejercicio 59. Implementar la función palabrasDistintas que indique las palabras distintas hay
en una variable de tipo String pasada como argumento a la función:
1 def palabrasDistintas(str:String):Int
Ejercicio 60. Implementar una clase Persona para almacenar datos de personas (nombre, ape-
llidos, teléfono, dirección, email) y una clase Agenda que nos permita almacenar a nuestros
contactos y acceder rápidamente a los datos de los mismos si buscamos por el nombre del
contacto. En principio se supondrá que no habrá dos contactos que tengan el mismo nombre.
Defina la función add que nos permita añadir un nuevo contacto de tipo Persona pasado
como argumento a nuestra agenda.
Dentro de la clase Agenda, definir la función contactos que nos devuelva una lista con el
nombre de todos los contactos que hay almacenados en la agenda.
Definir una función telefonos dentro de la clase Agenda que devuelva una lista con todos
los pares (Nombre,Teléfono) correspondientes a los contactos almacenados en la agenda.
Modificar la clase Agenda para poder almacenar a más de un contacto con el mismo
nombre. A la hora de imprimir por pantalla el resultado de las funciones contactos y
telefonos se deberá poder diferenciar a cada uno de los contactos de la agenda. De modo
que si después de imprimir por pantalla el resultado de las funciones contactos y telefonos
se obtenía antes:
List(Anton, Lourdes)
List((Anton,11111111), (Lourdes,22222222))
Ahora se deberá obtener:
List(Anton Oliva Olmo, Lourdes de Marcos Soria)
List((Anton Oliva Olmo,11111111),
(Lourdes de Marcos Soria,22222222))
Implementar una función en la clase Agenda que nos devuelva una lista con todos los con-
tactos que tengan el mismo nombre que el valor de tipo String pasado como argumento.
En caso de no haber ningún contacto nos devolverá la lista vacía.
Implementar una función vecinos en la clase Agenda que devuelva una lista con todos los
contactos que vivan en la calle pasada como argumento.
Página 126
Implementar una función vecinos en la clase Agenda que devuelva un Map cuya clave
será la calle y tendrá como valor la lista de vecinos que viven en esa calle.
Mejorar la versión de la función vecinos para que no muestre como resultado las direc-
ciones en las que sólo exista un contacto.
Ejercicio 61. Implementar una función que identifique las palabras distintas hay en una variable
de tipo String pasada como argumento a la función, y además informe de cuantas apariciones
hay de cada palabra en la variable pasada como argumento.
Página 127
Página 128
Capítulo 4
Programación Funcional Avanzada en
Scala
4.1. Implícitos
4.1.1. Parámetros ímplicitos en funciones
En la Sección 3.5: Currificación y Parcialización « página 63 », se estudió la aplicación
parcial de funciones y se vio como una función podía ser invocada sin necesidad de especificar
todos los argumentos, obteniendo otra función como valor que podía ser invocada especificando
los parámetros restantes.
Una de las aplicaciones que tendrán los parámetros implícitos será la de poder aplicar
funciones sin la necesidad de especificar todos sus parámetros1
.
Para definir valores, variables o parámetros implícitos en funciones se utilizará la palabra
reservada implicit. Las variables o valores implícitos definidos se utilizarán como “valores por
defecto”2
de los parámetros definidos como implícitos de las funciones que se invoquen dentro
del namespace (espacio de nombres), donde estas variables o valores implícitas son definidas. El
parámetro implícito3
de las funciones se especificará después de la definición de los parámetros
no implícitos o normales de la misma.
En el algoritmo 4.1 se define la función hacerLista con un parámetro implícito dentro del
objeto IntOps. El parámetro implícito definido es de tipo lista de enteros List[Int].
object IntOps {
def hacerLista(x:Int)(implicit xs:List[Int]):List[Int] = {x :: xs}
}
Algoritmo 4.1: Definición de parámetros implícitos
A continuación, se verá en el REPL de Scala como la invocación de la función hacerLista,
sin especificar el parámetro implícito, provoca un error:
scala> val lista5=IntOps.hacerLista(5)(List())
lista5: List[Int] = List(5)
scala> IntOps.hacerLista(3)
<console>:9: error: could not find implicit value for parameter xs: List[Int]
IntOps.hacerLista(3)
1
Se tendrán que especificar todos los parámetros que no se declaren como implícitos.
2
Si éstos no son aportados específicamente cuando se invoque la función.
3
Normalmente se definirá un parámetro implícito aunque se pueden definir un grupo de parámetros implícitos
separados por comas.
Página 129
^
En el algoritmo 4.2 se ha definido la clase ListaSingleInt. Dentro de la clase se ha definido un
valor implícito de tipo lista: la lista vacía (List()). También se ha definido la función hacerLista
que se encarga de hacer una llamada a la operación hacerLista del objeto IntOps, pasándole
el valor de clase x como argumento. Por tanto, la función IntOps.hacerLista tomará el valor
implícito xs cuando no se haya especificado ningún otro valor para el parámetro implícito xs.
case class ListaSingleInt(x:Int){
implicit val xs = List()
def hacerLista = IntOps.hacerLista(x)
}
Algoritmo 4.2: Definición de valores implícitos
A continuación, se muestra como es posible crear listas de un sólo elemento sin necesidad
de especificar un segundo parámetro utilizando la clase ListaSingleInt:
scala> val lista3 = ListaSingleInt(3).hacerLista
lista3: List[Int] = List(3)
Los valores implícitos son muy usados en Scala. La mayoría aportan funcionalidad a las
funciones, como por ejemplo el orden por defecto en las colecciones. Esta funcionalidad puede
ser sobreescrita por las clases que utilicen estas funciones.
Se deberá de tener en cuenta que el código puede llegar a ser difícil de leer y de entender si
se hace un uso excesivo de los parámetros implícitos. Se puede evitar que esto ocurra limitando
el uso de los mismos dentro del código, por ejemplo al aporte de funcionalidad a determinadas
funciones.
4.1.2. Clases implícitas
Otra característica de los implícitos en Scala es el de servir para declarar conversiones im-
plícitas entre clases. Una clase implícita servirá para realizar una conversión automática de un
tipo (de una clase A), a otro tipo (de otra clase B).
El compilador de Scala utiliza las clases implícitas cuando una instancia invoca un método
o valor desconocido. En este momento buscará dentro del espacio de nombres (namespace) en
busca de una clase implícita que tome como argumento la propia instancia e implemente el
método o valor invocado. Si el compilador encuentra una coincidencia incorporará una conver-
sión automática a la clase implícita que haga accesibles las invocaciones de este método o valor
desde el propio tipo.
En el algoritmo 4.3 se define la clase implícita Listas en la que se implementa la operación
hacerSingletonLista de un elemento entero.
object UtilInts {
implicit class Listas(x:Int) {
def hacerSingletonLista = List(x)
}
}
Algoritmo 4.3: Definición de clases implícitas
Por tanto, para que el compilador sea capaz de realizar la conversión automáticamente de
un elemento de tipo Int, a un elemento de tipo Listas, pudiendo así hacer accesible la operación
hacerSingletonLista a todos los enteros, se tendrá que importar el objeto UtilInts para que esté
disponible dentro del ámbito del REPL después de haber definido el mismo:
Página 130
scala> object UtilInts {
| implicit class Listas(x:Int) {
| def hacerSingletonLista = List(x)
| }
| }
defined object UtilInts
scala> import UtilInts._
import UtilInts._
scala> println(3.hacerSingletonLista)
List(3)
Como se ha visto, las clases implícitas facilitan la incorporación de métodos o valores a los
tipos de datos pero para que Scala pueda realizar esta tarea, las clases implícitas deberán de
cumplir unas reglas:
Las clases implícitas se tienen que definir dentro de otra clase, objeto o rasgo. Como se ha
visto, las clases implícitas que se definen dentro de objetos se pueden importar fácilmente.
Las clases implícitas sólo podrán tomar un valor cuyo tipo no podrá ser el de una clase
implícita..
El nombre de la clase implícita no podrá ser igual al de otro objeto, clase o rasgo definido
dentro del ámbito de la clase implícita4
Todas estas reglas se cumplen en el ejemplo anterior.
Se recomienda definir las clases implícitas dentro de objetos ya que:
Se pueden incorporar fácilmente dentro del ámbito como se ha visto en el anterior ejem-
plo, importando parte o todos los objetos miembros.
Ninguna clase, objeto o rasgo podrán heredar de un objeto, algo que aportará seguridad
de que no se incorporen conversiones de forma automática.
4.2. Tipos en Scala
Scala es un lenguaje tipado estáticamente. El sistema de tipos es un componente muy im-
portante del lenguaje que, combinando ideas de la programación funcional y la POO, intenta ser
comprensible, completo y consistente. El sistema de tipos de Scala ofrece un conjunto de opti-
mizaciones y de restricciones que permitirán mejorar el tiempo de ejecución de los programas
y prevenir errores de programación.
Informando suficientemente al compilador, éste será capaz de detectar una gran cantidad de
errores.
Un tipo representa un conjunto de información conocida para el compilador como puede ser
“qué clase se usó para instanciar una variable” o “qué métodos están disponibles para una varia-
ble” , información que se puede aportar al compilador o que éste puede inferir inspeccionando
el código.
4
Por lo que no se podrá definir una clase implícita haciendo uso de case class ya que las clases case crean
automáticamente un objeto acompañante, dentro del cual se define automáticamente el método de fábrica apply.
Página 131
4.2.1. Definición de tipos.
En Scala se pueden definir tipos de dos modos:
Definiendo una clase, objeto o rasgo se creará automáticamente un tipo asociado a la cla-
se, objeto o rasgo definido. Se podrá hacer referencia a estos tipos con el mismo nombre
que la clase o rasgo, mientras que para referirnos al tipo de un objeto5
utilizaremos el
miembro type del objeto. A continuación se muestra un ejemplo:
object MiObjeto
def miFuncion(x:MiObjeto.type)
Definiendo directamente el tipo usando la palabra reservada type.. Utilizando type se po-
drán crear tanto tipos concretos, como tipos abstractos6
. Con type sólo se podrán definir
tipos dentro de un contexto, es decir, dentro de un objeto, clase o rasgo. Ejemplo:
abstract class Suma {
type MiTipo // MiTipo es un tipo abstracto
def suma(x:MiTipo,y:MiTipo):MiTipo
}
class SumaInt extends Suma {
type MiTipo = Int
def suma(x:Int,y:Int):Int = x+y
}
def sumarInt = new SumaInt()
val l1= List(1,2,3,4)
val valor= l1 foldRight (0) (sumarInt.suma)
4.2.2. Parámetros de tipo.
Los parámetros de tipo se definen antes de que se haya definido los parámetros de la fun-
ción, clase, etc. encerrando los parámetros de tipo dentro de corchetes. Una vez se definen los
parámetros de tipo, se podrán utilizar estos parámetros tanto en los argumentos como dentro de
la definición de las funciones, clases,etc. La importancia de los parámetros de tipo para crear
funciones polimórficas o clases genéricas fue vista en la sección dedicada al polimorfismo en
Scala, en la Sección 2.5: Polimorfismo en Scala « página 45 ».
Los parámetros de tipo pueden presentar restricciones que limiten los posibles valores que
puedan tomar, acotando los mismos. Otra de las características de los parámetros de tipo es la
varianza que define la relación entre los tipos parametrizados y sus subtipos. La varianza y la
acotación de tipos fue estudiada en detalle en la Subsección 2.5.1: Acotación de tipos y varianza
« página 47 ».
4.2.2.1. Nombres de los parámetros de tipo.
La decisión sobre qué nombre utilizar para los parámetros de tipo es un tema que ha ge-
nerado siempre un gran debate entre los programadores. De un lado hay quien apoya la idea
5
No es habitual referirnos al tipo de un objeto ya que si conocemos el objeto podemos hacer referencia al
mismo sin necesidad de tener que preguntar por el tipo del mismo.
6
Los contextos en los que se definan tipos abstractos no se podrán instanciar.
Página 132
de otorgar nombres descriptivos a los parámetros de tipo, mientras que por otro lado hay quien
apoya utilizar nombres cortos.
Entre la comunidad de programadores de Scala se ha llegado a un acuerdo:
Utilizar nombres cortos, definidos con una letra o dos letras (A,B,A1,T2,...), normalmente
para definir los elementos que pueden existir en un contenedor y que no presentan ninguna
relación con el contenedor.
Utilizar nombre largos para definir aquellos tipos de datos que si están relacionados con el
contenedor. Por ejemplo, si observamos el tipo That aparece en la definición del método
scan de List para hacer referencia al tipo del contenedor en el que se almacenarán los
resultados de la operación op, que serán del tipo B.
def scan[B >: A, That](z: B)(op: (B, B) => B)(implicit cbf:
CanBuildFrom[List[A], B, That]): That
4.2.3. Constructores de tipos.
En muchas ocasiones se verá el término de constructor de tipos en referencia a las clases
parametrizadas. El término constructor de tipos se utiliza para enfatizar en el hecho de que los
parámetros de tipo se utilizan para crear tipos específicos.
Por ejemplo, se podría decir que List sería el constructor de tipos de los tipos específicos
List[Int] o List[Boolean].
4.2.4. Tipos compuestos.
Es posible definir nuevos tipos, tipos compuestos, como resultado de la combinación de
otros tipos. El compilador se asegurará, en la medida de la información que disponga, que los
tipos sean compatibles. Para combinar tipos utilizaremos la palabra reservada with. Por ejemplo:
class MiClase
trait MiTrait
trait Rasgo
type MiTipo = MiClase with MiTrait
type MapInt = Map[Int,_]
type MapIntRasgo = MapInt with Rasgo
En el anterior ejemplo se puede apreciar el uso del guión bajo (_) en la expresión type
MapInt = Map[Int,_], en la que actúa como comodín, haciendo referencia a un tipo existencial
desconocido en el momento de la definición.
En la Subsubsección 2.3.1.1: Rasgos y herencia múltiple en Scala « página 39 », dedicada
a estudiar los rasgos y la herencia múltiple en Scala, se vieron la bondades del uso de tipos
compuestos.
4.2.5. Tipos estructurales
En Scala se pueden definir tipos estructurales haciendo uso de la palabra reservada ty-
pe y encerrando entre llaves las definiciones de métodos y variables que deseemos que estén
presentes en el tipo definido.
Página 133
La definición de tipos estructurales permitirá definir tipos abstractos, es decir, Scala permite
especificar ciertas características que deben de cumplir los objetos: métodos que deben estar
definidos, tipos, etc. Los tipos estructurales sólo tendrán en cuenta la estructura, por lo que
podrán ser anónimos aunque normalmente serán tipos nominales.
1 trait ComidaAnimalMap
2 type Animal
3 type ComidaAnimal = {def comida(animal:Any):Unit}
4
5 def addAnimal(animal : Animal, as:List[Animal]):List[Animal] =
animal::as
6
7
8 }
Algoritmo 4.4: Ejemplo de tipos estructurales.
En la línea 3 del algoritmo 4.4 se observa cómo la única condición que se impone a un objeto
para satisfacer el tipo ComidaAnimal es que haya implementado el método comida. Scala no
permite hacer referencia a tipos abstractos o a parámetros de tipo dentro de la definición de un
tipo estructural por lo que no se puede utilizar el tipo Animal. En su lugar se ha utilizado un
tipo conocido como el tipo Any. Esto hará que la clase que implemente este método tenga que
hacer cast de la variable y transformarla en el tipo correcto.
Otro inconveniente que puede presentar el uso de tipos estructurales es el hecho de que,
para verificar que una instancia del rasgo ComidaAnimal implementa el método comida, se
recomienda importar scala.language.reflectiveCalls, aunque esto añadirá un sobrecoste en de-
terminadas situaciones.
4.2.6. Tipos de orden superior.
Igual que se ha visto que existen las funciones de orden superior, como funciones que re-
ciben otras funciones como argumentos o que devuelven una función, existen tipos de orden
superior. Los tipos de orden superior son aquellos que utilizan otros tipos para construir un tipo
nuevo7
.
Los tipos de orden superior se utilizarán para simplificar la definición de otros tipos o para
hacer que tipos complejos se ajusten a parámetros de tipo definidos. Ejemplo:
type Funcion[T] = Function1[T,Unit]
En el ejemplo anterior el tipo Funcion tomará un parámetro de tipo para construir una nueva
función del tipo Function1[T,Unit]. Por tanto, podremos usar el tipo Funcion para simplificar
la signatura de las funciones que reciben un único parámetro y no devuelven ningún resultado.
Otra característica del tipo Funcion es que no será un tipo completo hasta que se asigne un valor
al parámetro de tipo8
.
Como se ha descrito anteriormente, el tipo Funcion se puede utilizar para simplificar la
definición de funciones que toman un argumento y no devuelven ningún valor. Ejemplo:
def sumaTres:Funcion[Int]={x=>println(x+3)}
7
Los tipos de orden superior pueden recibir uno o más tipos como parámetros
8
Al igual que las clases parametrizadas eran consideradas constructores de tipos, los tipos de orden superior
también son considerados constructores de tipos ya que pueden ser utilizados para definir tipos específicos.
Página 134
El tipo de Funcion[Int] será traducido por el compilador al tipo (Int) → Unit.
Hasta ahora se ha visto como los tipos de orden superior simplificarán la definición de tipos
complejos. Además, se podrán usar para hacer que tipos complejos se ajusten a parámetros de
tipo simples. Ejemplo:
def ajusteTipo[M[_]](f:M[Int]) = f
def masTres = ajusteTipo[Funcion](sumaTres)
En el algoritmo 4.5 se puede ver como se comporta el ejemplo anterior en el REPL de Scala.
scala> def sumaTres:Funcion[Int]={x=>println(x+3)}
sumaDos: Funcion[Int]
scala> def ajusteTipo[M[_]](f:M[Int]) = f
ajusteTipo: [M[_]](f: M[Int])M[Int]
scala> ajusteTipo[Funcion](sumaTres)
res2: Funcion[Int] = <function1>
Algoritmo 4.5: Ejemplo de tipos de orden superior.
En el algoritmo 4.5 se observa como el método ajusteTipo recibe un parámetro de tipo M
parametrizado por un tipo desconocido. Como se vio en la Subsección 4.2.4: Tipos compuestos
« página 133 », el _ es utilizado como identificador de un tipo existencial desconocido. Si se
hubiera intentado evaluar la expresión ajusteTipo[Function1](sumaTres) se habría obtenido un
error ya que Function1 toma dos parámetros de tipo y el método ajusteTipo espera un tipo con
un único parámetro de tipo.
4.2.7. Tipos existenciales
Los tipos existenciales representan una forma de abstracción sobre tipos permitiendo indicar
en el código la existencia de un tipo sin especificar de qué tipo se trata.
Serán de especial utilidad en aquellas ocasiones en las que no se conoce el tipo o simple-
mente no es necesario indicarlo porque no aporte información relevante en ese contexto.
Formalmente los tipos existenciales se definen haciendo uso de la palabra reservada forSo-
me:
type forSome lista de tipos y vals abstractos
Los tipos existenciales se emplearán sobre todo para acceder a algunas clases Java para
las que los tipos conocidos previamente no aportan una solución como, por ejemplo, Collec-
tion<?>. En general, los tipos de datos existenciales son muy importantes para mantener la
compatibilidad entre los tipos en Java y los tipos en Scala en tres situaciones:
Cuando en Java se usa el comodín para expresar la varianza en los tipos genéricos.
Los parámetros de tipo de los genéricos son eliminados por el proceso borradura por
lo que, por ejemplo, a nivel de código de la Máquina Virtual de Java, para la JVM es
imposible distinguir entre estas dos listas basándose en la información de tipos conocida:
List[Perros] y List[Pajaros].
Los tipos simples9
, como todos los tipos existentes en las bibliotecas de Java (antes de la
versión 5 de Java), en la que todos los parámetros de tipo eran objetos del tipo Object.
9
Tipos de datos sin parámetros de tipo
Página 135
Normalmente los tipos existenciales se indicarán haciendo uso del guión bajo _, de modo
que cuando Scala encuentre el guión bajo en el lugar en el que debería aparecer un tipo colocará
un tipo existencial en dicho lugar10
.
Como se muestra en la tabla 4.1 es posible utilizar cotas superiores y/o cotas inferiores en
los tipos existenciales.
Def. abreviada Def. formal Significado
List[_] List[T] forSome type T Lista de cualquier tipo
List[_ <: Perro] List[T] forSometype T <: Perro Lista de cualquier tipo que sea sub-
tipo del tipo Perro
List[_ >: Animal] List[T] forSometype T >: Ani-
mal
Lista de cualquier tipo que sea su-
pertipo del tipo Animal
Tabla 4.1: Tipos existenciales en Scala
4.3. Teoría de categorías
La teoría de categorías es una teoría que trata de axiomatizar de forma abstracta diversas
estructuras matemáticas como una sola, mediante el uso de objetos y morfismos. Al mismo
tiempo trata de mostrar una nueva forma de ver las matemáticas sin incluir las nociones de
elementos, pertenencia, entre otras.[34]
La definición general de categoría contiene tres entidades:
1. Una clase que actúa como contenedor de objetos.
2. Una clase de morfismos, también llamados aplicaciones que generalizan el concepto de
función f : A → B (f:A=>B en Scala).
3. Una operación binaria, llamada composición de morfismos, que tiene la propiedad de
∀f, g | f : A → B, g : B → C; ∃ g ◦ f : A → C. Además la composición de
morfismos satisface dos axiomas:
Existencia de un único morfismo identidad, IDx en el que el dominio y el codominio
son iguales. La composición con el morfismo identidad tiene la siguiente propiedad:
f ◦ IDx = IDx ◦ f
Propiedad asociativa en la composición, ∀f : A → B, g : C → A, h : D → C :
(f ◦ g) ◦ h = f ◦ (g ◦ h)
La teoría de las categorías modela muchos aspectos de la programación aunque en la ma-
yoría de las ocasiones pasa desapercibida para los programadores.
Una buena forma aprovechar la aplicación de la teoría de las categorías en programación
es a través de los patrones de diseño. La teoría de las categorías define conceptos abstractos de
bajo nivel que pueden ser expresados directamente en un lenguaje de programación funcional
como Scala, además de ofrecer librerías de soporte como Scalaz11
.
A continuación se verán dos categorías muy usadas en el desarrollo software: Funtores y
Mónadas.
10
Cada guión bajo será convertido a un parámetro de tipo en una sentencia forSome por lo que si se utiliza dos
veces el guión bajo en el mismo tipo Scala pondrá una sentencia forSome con dos tipos
11
https://github.com/scalaz/scalaz.
Página 136
4.3.1. El patrón funcional Funtor
Los funtores representan una categoría dentro de la teoría de las categorías. Los funtores
representan transformaciones de una categoría en otra categoría que también deben transformar
y preservar los morfismos.
En programación, los funtores se entenderán como patrones funcionales, en los que a un
contenedor que referencia a un conjunto de objetos se le quiere dotar de la posibilidad de aplicar
una función a cualquier objeto dentro del mismo, algo que se realizará sin alterar la estructura
del propio contenedor12
. Dicho de otro modo, los funtores permitirán aplicar funciones puras
(f : A → B), a los elementos de un contenedor en el que existan uno o más valores del tipo A.
Es decir, los funtores representan la abstracción de la función map que se ha visto anteriormente.
Además, los funtores serán clases, objetos, rasgos...en los que habitualmente sólo habrá un
método (el método map) que será el encargado de aplicar la función deseada a los objetos del
contenedor. Los funtores, como objetos, podrán ser pasados como parámetros y utilizados en el
mismo lugar en el que pueda aparecer una función para ser aplicada a un conjunto de objetos.
A continuación se muestra una posible implementación en el siguiente rasgo:
trait Funtor[F[_]] {
def map[A,B](fa: F[A])(f: A => B): F[B]
}
Se puede observar que el rasgo Funtor está parametrizado en el tipo. A continuación se
muestra una implementación concreta para tres tipos concretos, Seq, Option y Set:
object seqFuntor extends Funtor[Seq] {
def map[A,B](seq: Seq[A])(f: A => B): Seq[B] = seq map f
}
object setFuntor extends Funtor[Set] {
def map[A,B](set: Set[A])(f: A => B): Set[B] = set map f
}
object optFuntor extends Funtor[Option] {
def map[A,B](opt: Option[A])(f: A => B): Option[B] = opt map f
}
A continuación, se verá un ejemplo en el que se hará uso de las implementaciones seqFuntor,
setFuntor y optFuntor13
:
scala> def multD(x:Int):Double= x*2.5
multD: (x: Int)Double
scala> seqFuntor.map(List(1,2,3))(doble)
res26: Seq[Int] = List(2, 4, 6)
scala> seqFuntor.map(List())(doble)
res27: Seq[Int] = List()
scala> setFuntor.map(Set(1,2,3))(triple)
res29: Set[Int] = Set(3, 6, 9)
scala> optFuntor.map(Some(5))(cuadrado)
res30: Option[Int] = Some(25)
scala> optFuntor.map(Some(5))(multD)
res31: Option[Double] = Some(12.5)
12
Normalmente se devolverá una nuevo contenedor con los resultados de aplicar la función a los elementos del
contenedor original.
13
Se consideran definidas anteriormente las funciones doble, triple y cuadrado cuyo dominio y codominio es
Int
Página 137
scala> optFuntor.map(None)(multD)
res32: Option[Double] = None
Con la abstracción Funtor es posible crear un conjunto de funciones que se puedan implementar
haciendo uso de map, como por ejemplo la función distribuir:
1 def distribuir[A,B](fab: F[(A, B)]): (F[A], F[B]) = (map(fab)(_._1),
map(fab)(_._2))
Algoritmo 4.6: Función distribuir usando funtores.
Si se hablara en términos de teoría de categorías, otras categorías serán los objetos y los
morfismos serán las funciones entre las categorías.
Los funtores presentan dos propiedades fundamentales que son consecuencia directa de las
propiedades y axiomas de la teoría de categorías:
1. Un funtor F preserva la identidad, esto significará que la identidad del dominio se corres-
ponderá con la identidad del codominio.
2. Un funtor F preserva la composición. F(f ◦ g) = F(f) ◦ F(g)
Una vez definidos los funtores, se puede observar que las clases de la biblioteca collection de
Scala presentan las características propias de los funtores.
A pesar de que los funtores son mucho más importantes en la teoría de categorías que las
mónadas, su importancia en la programación es anecdótica si se compara con las mónadas, el
siguiente patrón de programación funcional que se estudiará.
4.3.2. El patrón funcional Mónada
Las mónadas representan otra categoría dentro de la teoría de categorías. Los fundamentos
matemáticos que definen a las mónadas serán importantes a la hora de definir el patrón mónada.
El término mónadas tiene su origen en el término griego monas, usado por los filósofos de la
antigua Grecia, que quiere decir algo así como "la Divinidad por la que otras cosas son creadas".
En un programación funcional pura, una mónada es una estructura que representa las
computaciones en un conjunto de objetos. Si el patrón funcional funtor servía para abstraer
la aplicación de una función a los elementos referenciados por un contenedor, el patrón mónada
se usará para realizar un aplanado de la aplicación de determinados funtores. Más concretamen-
te, el patrón mónada será el encargado de establecer como crear los contenedores para manejar
la creación y la combinación de mónadas, la aplicación de funciones a miembros de ese con-
tenedor, como varias funciones son aplicadas a los elementos del contenedor y como múltiples
contenedores pueden ser aplanados en un contenedor único. Como se verá a continuación, el
patrón mónada aparece muy frecuentemente dentro de la programación funcional.
Al igual que ocurría en el caso de los funtores, las mónadas serán clases, objetos, rasgos...en
los que habitualmente sólo habrá un método (el método flatMap) que será el encargado de apli-
car la función deseada a los objetos del contenedor. Las mónadas se suelen utilizar en progra-
mación funcional para abstraer el comportamiento en la ejecución de un programa. Algunas
mónadas se utilizan para manejar la concurrencia, las excepciones o los efectos colaterales, por
ejemplo.
Durante la sección dedicada al estudio de las colecciones en Scala se ha podido apreciar que
es muy común que aparezcan definidas las operaciones map y flatMap dentro de las estructuras
Página 138
de datos. De hecho, hay un nombre para definir a estas estructuras que, además, cumplen unas
reglas algebraicas. Se llamarán Mónadas14
.
Una mónada será un tipo paramétrico M[T] con dos operaciones: flatMap15
y unit que deben
satisfacer algunas reglas.
Por tanto, una posible definición de mónadas en Scala podría ser:
trait M[T] {
def flatMap[U](f: T => M[U]): M[U]
def unit[T](x: T): M[T]
}
Algoritmo 4.7: Definición básica del trait Mónada.
Dentro del rasgo M (que representa la mónada) se encuentra el método flatMap que toma un
tipo U como parámetro y una función que mapea T a una mónada de U, devolviendo la misma
mónada aplicada a U. Después, se encontrará el método unit que toma un elemento del tipo T y
devuelve una instancia de la mónada de T.
Por tanto, sabiendo que la función flatMap se encuentra definida para algunas de las estruc-
turas vistas anteriormente, algunos ejemplos de mónadas serían:
List es una mónada donde unit(x)=List(x)
Set es una mónada donde unit(x)=Set(x)
Option es una mónada donde unit(x)=Some(x)
4.3.2.1. Reglas que deben satisfacer las mónadas
Pero como se ha dicho anteriormente, estas operaciones deben de satisfacer ciertas propie-
dades:
1. Asociatividad: m flatMap f flatMap g == m flatMap (x => f(x) flatMap g)
2. Unit es identidad por la izquierda: unit(x) flatmap f == f(x)
3. Unit es identidad por la derecha: m flatmap unit == m
Se comprueba que, efectivamente, la clase Option satisface las reglas descritas anteriormen-
te. Para las demostraciones, se tendrá que recordar la definición del método flatMap dentro de
la clase Option:
abstract class Option[+T] {
def flatMap[U](f: T => Option[U]): Option[U] = this match {
case Some(x) => f(x)
case None => None
}
}
En primer lugar, se comprueba que se cumple la propiedad unit es identidad por la izquierda:
Habrá que demostrar: Some(x) flatMap f == f(x)
14
Haskell, un lenguaje de programación funcional en el que se enfatiza en la pureza funcional, fue pionero en el
uso de mónadas al separar la entrada y salida (IO) de lo que es puramente código.
15
Dentro de la programación funcional es posible reconocer esta operación como bind
Página 139
Some(x) flatMap f
== Some(x) match {
case Some(x) => f(x)
case None => None
}
== f(x)
A continuación, se verifica que se cumple la regla unit es identidad por la derecha:
Se tendrá que demostrar: opt flatMap Some == opt
opt flatMap Some
== opt match {
case Some(x) => Some(x)
case None => None
}
== opt
Finalmente se demuestra la propiedad de la asociatividad:
En este caso hay que demostrar:
opt flatMap f flatMap g == opt flatMap (x => f(x) flatMap g)
opt flatMap f flatMap g
== opt match { case Some(x) => f(x)
case None => None }
== opt match {
case Some(x) =>
f(x) match { case Some(y) => g(y)
case None => None }
case None =>
None match {
case Some(y) => g(y)
case None => None }
}
== opt match {
case Some(x) =>
f(x) match {
case Some(y) => g(y)
case None => None }
case None => None
}
== opt match {
case Some(x) => f(x) flatMap g
case None => None
}
== opt flatMap (x => f(x) flatMap g)
Página 140
4.3.2.2. Importancia de las propiedades de las mónadas en las expresiones for
El hecho de que se verifiquen las anteriores reglas tendrá una importancia especial en la
definición de las expresiones for.
1. La asociatividad permitirá anidar los generadores dentro de un bucle for:
for (y <- for (x <- m; y <- f(x)) yield y
z <- g(y)) yield z
==
for (x <- m;
y <- f(x);
z <- g(y)) yield z
2. La regla derecha de unit implica que:
for (y <- m) yield y
==
m
3. La regla derecha de unit no tiene un análogo dentro de las expresiones for.
4.3.2.3. Map en las mónadas
Hasta el momento no se ha referido el hecho de definir la función map para las mónadas ya
que ésta puede ser definida en términos de fmap:
m map f == m flatmap (x=> unit(f(x)))
== m flatmap (f andThen unit)
Se podría completar la definición del rasgo definido anteriormente para representar móna-
das, con la definición de la función map y la función map2. Se aprovechará la ocasión para dar
una definición más amplia para el rasgo Mónada, utilizando tipos de orden superior:
trait Monada[M[_]]{
def unit[A](a:A):M[A]
def flatMap[A,B] (ma:M[A])(f:A=>M[B]):M[B]
def map[A,B](ma:M[A])(f:A=>B):M[B]= flatMap (ma) (x=>
unit(f(x)))
def map2[A,B,C](ma:M[A], mb:M[B])(f:(A,B)=>M[C]):M[C] =
flatMap (ma) (x => map (mb) (y=> f(x,y)))
}
}
De este modo, al implementar los métodos flatMap16
y unit necesarios para definir una
mónada estarán disponibles también los métodos map y map2. Ahora el rasgo podría heredar
de Funtor al incorporar una definición de map.
Después de haber visto los funtores y las mónadas, se puede decir que “todas las mónadas
son funtores pero no todos los funtores son mónadas”.
16
En esta ocasión se han utilizado dos parámetros de tipo para la definición de la función flatMap, algo que
difiere de la definición vista en el algoritmo4.7 ya que en la signatura del rasgo Monada se ha utilizado el _ para
indicar que Monada recibe un parámetro existencial desconocido.
Página 141
4.3.2.4. La importancia de las mónadas
Las mónadas son importantes dentro de la programación ya que son capaces de envolver la
información que rodea a un valor del mismo modo que envolvería el propio valor, minimizando
el acoplamiento entre ambos.
Inspirándose en el uso de este patrón en Haskell, el patrón Mónada también se utiliza recu-
rrentemente en Scala. De hecho, ya se han visto algunos ejemplos de clases monádicas en Scala
como puedan ser los tipos List o Either17
. Ambas clases monádicas presentan características
comunes:
Implementan el método flatMap y la construcción haciendo uso del método de fábrica
apply en lugar de unit.
Pueden utilizarse en expresiones for.
Permiten aplicar una secuencia de funciones y manejar fallos de diferentes formas (nor-
malmente devolviendo una subclase del tipo).
4.3.2.5. La mónada Identidad
A continuación se verá una de las principales características de las mónadas ejemplificada
en la mónada identidad, la cual sólo se encargará de envolver los datos. Para ello, se definirá la
clase parametrizada Id que recibirá un único parámetro, el valor que se desea envolver. Dentro
de la clase Id se definirán los métodos map y flatMap.
case class Id[A](value:A){
def map[B](f:A=>B):Id[B]=Id(f(value))
def flatMap[B](f:A=>Id[B]):Id[B]=f(value)
}
A continuación, se implementará el objeto MonadaID[Id]. Es importante recordad que sólo
se tienen que definir los métodos unit y flatMap:
object MonadaID extends Monada[Id]{
def unit[A](a:A):Id[A]=Id(a)
def flatMap[A,B](ma:Id[A])(f:A=>Id[B]):Id[B]=ma flatMap f
}
Se aprecia que la Id es un simple envoltorio, sin añadir ningún tipo de información adicional.
Si se observa la función flatMap, ésta desempeña una tarea simple en la mónada identidad,
la sustitución de variables. En el algoritmo 4.8 se puede ver este comportamiento.
17
Tomando como operaciones unit(x)=List(x) en las listas o, en el caso de los conjuntos, unit(x)=Set(x).
Página 142
import scala.language.higherKinds
scala> trait Monada[M[_]]{
def unit[A](a:A):M[A]
def flatMap[A,B] (ma:M[A])(f:A=>M[B]):M[B]
def map[A,B](ma:M[A])(f:A=>B):M[B]= flatMap (ma) (x=> unit(f(x)))
}
defined trait Monada
scala> case class Id[A](value:A){
def map[B](f:A=>B):Id[B]=Id(f(value))
def flatMap[B](f:A=>Id[B]):Id[B]=f(value)
}
defined class Id
scala> object MonadaID extends Monada[Id]{
def unit[A](a:A):Id[A]=Id(a)
def flatMap[A,B](ma:Id[A])(f:A=>Id[B]):Id[B]=ma flatMap f
}
defined object MonadaID
scala> for (x<- Id("Hola ");
y<- Id("mundo!")) yield x+y
res0: Id[String] = Id(Hola mundo!)
Algoritmo 4.8: Monada Identidad
Es posible observar en la expresión for del algoritmo 4.8 como:
Se puede utilizar la clase monádica Id en expresiones for.
Las variables x e y toman los valores “Hola ” y “mundo!”, respectivamente. Estos valores
son substituidos en la expresión x+y.
Ahora se puede afirmar que es posible utilizar las mónadas para envolver valores, o algo
más contundente, la mónada es un tipo de lenguaje de programación que soporta la sustitución
de variables[5]
4.3.2.6. Envolviendo el contexto con mónadas. La clase monádica Try
Se ilustrará uno de los principales usos de las mónadas, envolver el contexto en el que se
ejecuta el programa, desarrollando un juego muy simple18
.
Se desarrollará un juego, JailBreak, en el que el héroe tendrá que rescatar a su amigo de la
cárcel. Para ello necesitará conseguir la llave maestra que abre la celda en la que está encerrado
su amigo. La misión de conseguir la llave maestra será muy peligrosa, ya que está custodiada
por la guardia. Se sabe que la guardia posee 5 llaves: dos llaves amarillas, dos llaves verdes y una
llave roja. La llave roja abre el teatro. Las dos llaves amarillas son indistinguibles entre si. Una
llave amarilla sirve para abrir la celda y la otra llave amarilla abre la puerta de los vestuarios.
Igualmente, cada una de las llaves verdes, indistinguibles entre si, abre una puerta diferente,
una abrirá la celda mientras que la otra abrirá el gimnasio. Cada día se reparten las llaves de
modo que el director tiene dos llaves, una verde y otra amarilla, pero no sabe que puerta abre
cada una. El guardia se queda con las otras tres llaves en un llavero y éstas serán las llaves que
nuestro héroe deberá conseguir. Si el guardián que vigila la llave descubre a el héroe, la partida
habrá finalizado. Aunque si el héroe tiene fortuna, podrá coger el llavero con las tres llaves
aprovechando que los guardas se distraen durante la mitad del tiempo que custodian la llave,
aunque se desconoce el momento en el que están distraídos. Una vez conseguida la llave, podrá
salvar a su amigo siempre y cuando la llave maestra que abre la celda se encuentre en el llavero.
18
Adaptación del juego publicado en [19]
Página 143
Una vez más, el héroe deberá ser afortunado ya que ni el director, ni el guardia encargado de
custodiar las llaves saben qué puertas abre cada una de las llaves indistinguibles que poseen.
El algoritmo 4.9 muestra la definición de algunos tipos que se utilizarán en la resolución del
juego.
1 trait Llave{
2 val maestra:Boolean
3 }
4 case class Amarilla(maestra:Boolean) extends Llave
5 case object Roja extends Llave{
6 val maestra=false;
7 }
8 case class Verde(maestra:Boolean) extends Llave
9
10 case class Liberado(amigo:String) {
11 override def toString():String= amigo+ " eres Libre"
12
13 }
14 case class GameOverException(i:String) extends Exception(i)
Algoritmo 4.9: Juego JailBreak. Definición tipos necesarios.
En el algoritmo 4.10 se define el rasgo que servirá para definir los métodos que posterior-
mente serán implementados por nuestra clase JailBreak.
1
2 trait Game {
3
4 def rescatar(llaves:List[Llave]):Liberado
5 def conseguirLlave():List[Llave]
6 }
Algoritmo 4.10: Juego JailBreak. Trait Game.
Conocida la interfaz, el algoritmo 4.11 muestra una posible resolución del juego planteado
sin tener en cuenta la implementación de la clase JailBreak que heredará del rasgo Game.
1 val jailBreakGame = new JailBreak()
2 val llaves = jailBreakGame.conseguirLlave()
3 val resultado=jailBreakGame.rescatar(llaves)
Algoritmo 4.11: Juego JailBreak. Solución al juego.
En principio, sin más información, parece una solución bastante simple pero acertada. Co-
mo se ha dicho anteriormente, no se ha tenido en cuenta la implementación de los métodos
conseguirLlave y rescatar que la clase JailBreak tiene que implementar, ya que se encuentran
definidos en el rasgo Game.
Finalmente, en el algoritmo 4.12 se muestra la implementación de los métodos conseguir-
Llave y rescatar que hace la clase JailBreak.
Página 144
1 class JailBreak extends Game{
2 def conseguirLlave():List[Llave]={
3 if (descubierto()) throw new GameOverException("Game
Over: Has sido atrapado")
4 List(Amarilla(Random.nextBoolean()), Roja,
Verde(Random.nextBoolean()))
5 }
6
7 def rescatar(llaves:List[Llave]):Liberado={
8 if (!llaveCorrecta(llaves)) throw new
GameOverException("Game Over: Buen intento! No tienes
la llave maestra")
9 Liberado("Luis")
10 }
11
12 // Metodos auxiliares
13 private def descubierto():Boolean=Random.nextBoolean()
14 private def llaveCorrecta(llaves:List[Llave])=
15 llaves.foldLeft(false)((acu,llave)=>acu||llave.maestra)
16 }
17 }
Algoritmo 4.12: Juego JailBreak. Definición de la clase JailBreak.
A continuación se analizarán algunos de los detalles relevantes de la definición de la clase
JailBreak hecha en el algoritmo 4.12:
Si se presta atención a la definición del método conseguirLlave, se observa que si el héroe
es descubierto, se lanzará una excepción del tipo GameOverException, lo cual provocará
un fallo en la ejecución del programa.
Si no se encuentra la llaveCorrecta en el llavero que nuestro héroe ha conseguido, el mé-
todo rescatar lanzará una excepción del tipo GameOverException, provocando un fallo
en la ejecución del programa.
Siguiendo la definición del problema, existe un 50 % de posibilidades de que el guarda
descubra al héroe, así como un 50 % de posibilidades de que la llave maestra sea la ama-
rilla y un 50 % de posibilidades de que la llave maestra sea la verde. Estas probabilidades
se han implementando utilizando el método nextBoolean() del objeto Random definido
en scala.util.Random19
, el cual generará aleatoriamente valores booleanos.
El método auxiliar llaveCorrecta es utilizado para determinar si se encuentra la llave
correcta en el llavero, empleando un plegado de listas.
Por tanto, el algoritmo 4.11 estará bloqueado en la línea 2 mientras se ejecuta el método
conseguirLlave. Si el método finaliza como se espera, el algoritmo volverá a estar bloqueado en
la línea 3 mientras se ejecuta el método rescatar.
Aunque cuando se ve el algoritmo 4.11 no se puede imaginar que el código puede presentar
estos problemas, ya que nada hace pensar que esta resolución simple pueda fallar, se ha visto
como puede bloquearse la ejecución del mismo y provocar un funcionamiento incorrecto del
programa.
19
Biblioteca que habrá que importar para la compilación del programa.
Página 145
A continuación se muestra ejemplificado el funcionamiento del programa en el REPL de
Scala, tras haber introducido previamente las definiciones vistas en los algoritmos 4.9, 4.10 y
4.12 que como se ha indicado puede presentar un comportamiento no deseado:
scala> val jugar= new JailBreak()
jugar: JailBreak = JailBreak@32a53d86
scala> val llaves = jugar.conseguirLlave()
llaves: List[Llave] = List(Amarilla(true), Roja, Verde(true))
scala> val resultado = jugar.rescatar(llaves)
resultado: Liberado = Luis eres Libre
scala> jugar.conseguirLlave()
java.lang.Exception: Game Over: Has sido atrapado
at JailBreak.conseguirLlave(<console>:24)
... 33 elided
scala> val masterkeys=jugar.conseguirLlave()
masterkeys: List[Llave] = List(Amarilla(false), Roja, Verde(false))
scala> val resultado2=jugar.rescatar(masterkeys)
java.lang.Exception: Game Over: Buen intento! No tienes la llave maestra
at JailBreak.rescatar(<console>:29)
... 33 elided
La mónada Try.
Para solucionar este problema, se utilizará alguna de las clases monádicas existentes. En
la Sección 4.4: Manejo de errores sin usar excepciones « página 149 » se verán las clases
monádicas Option y Either en el ámbito de manejar posibles errores sin necesidad de lanzar
excepciones. Para las situaciones en las que se tenga que lidiar con excepciones existe otra
mónada, la mónada Try20
, la cual nos permitirá capturar excepciones en un envoltorio, informar
de la existencia de estos efectos colaterales y elevar este contexto para su posterior tratamiento.
abstract class Try[T]
case class Success[T](elem: T) extends Try[T]
case class Failure(t: Throwable) extends Try[Nothing]
object Try {
def apply[T](r: =>T): Try[T] = {
try { Success(r) }
catch { case t => Failure(t) }
}
Se puede observar que existen dos subclases de la clase abstracta Try, Success y Failure, que
representan las situaciones de éxito y fallo en la ejecución de un bloque, método...Se puede
ver como el método de fábrica apply del objeto acompañante de Try, recibe un parámetro que
sigue la estrategia de evaluación evaluación no estricta. El motivo es que se pretende capturar
el resultado de la evaluación de este parámetro y si se hubiera pasado por valor (evaluación
estricta) no se podría capturar su comportamiento, el cual se captura posteriormente dentro de
los bloques try/catch.
A continuación, se verán las modificaciones necesarias que habrá que realizar al código
de los ejemplos anteriores para hacer uso de la clase monádica Try. En el algoritmo 4.13 se
muestra como quedaría la definición del rasgo Game realizada previamente en el algoritmo 4.10,
20
Estrictamente hablando, Try no sería una mónada ya que si se comprueban las reglas que han de cumplir las
mónadas se puede comprobar como falla la regla derecha de unit.
Página 146
mientras que el algoritmo 4.14 muestra como quedaría la clase JailBreak, vista anteriormente
en el algoritmo 4.12, adaptada al uso de Try.
1
2 trait Game {
3
4 def rescatar(llaves:List[Llave]):Try[Liberado]
5 def conseguirLlave():Try[List[Llave]]
6 }
Algoritmo 4.13: Juego JailBreak. Trait Game incluyendo Try.
Ahora, simplemente al ver el rasgo Game, se puede observar que los métodos rescatar y
conseguirLlave presentan efectos colaterales que son tratados por la mónada Try.
1 class JailBreak extends Game{
2 def conseguirLlave():Try[List[Llave]]={Try({
3 if (descubierto()) throw new GameOverException("Has sido
atrapado")
4 List(Amarilla(Random.nextBoolean()),Roja,
5 Verde(Random.nextBoolean())) })
6 }
7
8 def rescatar(llaves:List[Llave]):Try[Liberado]={Try({
9 if (!llaveCorrecta(llaves)) throw new
GameOverException("Buen intento! No tienes la llave
maestra")
10 Liberado("Luis")})
11 }
12 ...
13 }
Algoritmo 4.14: Juego JailBreak. Trait Game incluyendo Try.
En el algoritmo 4.14 se observan modificaciones en la implementación de los métodos con-
seguirLlave y rescatar. El cuerpo de ambos métodos ha sido encerrado entre llaves, formando
un bloque, el cual se pasa como parámetro al método de fábrica apply del objeto acompañante
de Try.
El código de la solución al juego vista en el algoritmo 4.11 también se verá afectada por el
uso de Try, quedando como se muestra en el algoritmo 4.15.
1 val jailBreakGame = new JailBreak()
2 val llaves:Try[List[Llave]]= jailBreakGame.conseguirLlave()
3 val resultado= llaves match {
4 case Success(lla)=>jailBreakGame.rescatar(lla)
5 case failure @ Failure(t)=>failure
6 }
Algoritmo 4.15: Juego JailBreak. Solución al juego.
Ahora se observa que para la definición de la variable inmutable resultado se utiliza concor-
dancia de patrones, definiendo dos posibles situaciones (una por cada posible valor de Try).
Try define algunas funciones de orden superior que pueden ser de gran utilidad a la hora de
manejar esta mónada, entre las que se encuentran las siguientes funciones:
Página 147
def flatMap[S](f: T => Try[S]):Try[S]
def map[S](f: T => S):Try[S]
def flatten[U <: Try[T]]:Try[U]
def filter (p: T => Boolean): Try[T]
Dado que se utilizará la mónada Try, en la que se encuentra definida la función flatMap, en
primer lugar se deberá de ver como está definida esta función:
def flatMap[S](f: T=>Try[S]): Try[S] = this match {
case Success(value) =>try { f(value) }
catch { case t => Failure(t) }
case failure @ Failure(t) => failure
}
Ahora que es conocida la definición de la función flatMap, es posible refinar un poco el
código del algoritmo 4.15, obteniendo el algoritmo 4.16.
1 val jailBreakGame = new JailBreak()
2 val llaves:Try[List[Llave]] = jailBreakGame.conseguirLlave()
3 val resultado = llaves flatMap ( llavero =>
jailBreakGame.rescatar (llavero) )
Algoritmo 4.16: Juego JailBreak. Solución al juego utilizando flatMap.
Como se aprecia en el algoritmo 4.17, se podría refinar el algoritmo 4.16 algo más, aun-
que habrá muchos programadores que opten por la implementación del algoritmo 4.16, en el
que la variable llaves permite una legibilidad más clara del código. Evidentemente, con ambas
soluciones se obtendrá un resultado correcto.
1 val jailBreakGame = new JailBreak()
2 val resultado= jailBreakGame.conseguirLlave().flatMap (llavero
=> jailBreakGame.rescatar(llavero))
Algoritmo 4.17: Juego JailBreak. Solución al juego utilizando flatMap 2.
Si se vuelve a introducir la definición del rasgo Game y de la clase JailBreak, se verá ahora
que el juego, como era de esperar, presenta el comportamiento esperado:
scala> val resultado9 =
jailBreakGame.conseguirLlave().flatMap (llavero => jailBreakGame.rescatar(llavero))
resultado9: scala.util.Try[Liberado] = Failure(GameOverException: Has sido atrapado)
scala> val resultado10 =
jailBreakGame.conseguirLlave().flatMap (llavero => jailBreakGame.rescatar(llavero))
resultado10: scala.util.Try[Liberado] = Success(Luis eres Libre)
scala> val resultado11 =
jailBreakGame.conseguirLlave().flatMap (llavero => jailBreakGame.rescatar(llavero))
resultado11: scala.util.Try[Liberado] = Failure(GameOverException: Buen intento! No tienes la
llave maestra)
Otra de las características que se han destacado de las mónadas es la posibilidad de ser
utilizadas en expresiones for. En el algoritmo 4.18 de expresa el código del algoritmo 4.17
usando expresiones for.
Página 148
1 val jailBreakGame = new JailBreak()
2 for (llaves <- jailBreakGame.conseguirLlave();
3 resultado <- jailBreakGame.rescatar(llaves)) yield liberado
Algoritmo 4.18: Juego JailBreak. Solución al juego utilizando expresiones for.
4.4. Manejo de errores sin usar excepciones
4.4.1. Introducción
Como se vio en la Sección 3.2: Sentido estricto y amplio de la programación funcional «
página 54 », lanzar excepciones y parar la ejecución de un programa son efectos colaterales que
no se pueden producir en las funciones puras ya que no cumplirían la transparencia referencial,
sin la cual, la programación funcional no tendría sentido.
Cuando se lanza una excepción no se produce un valor en sí, sino que el flujo del programa
salta hasta el bloque catch que captura a la misma por lo que el valor final dependerá del con-
texto en el que la excepción sea tratada. Haciendo uso de throw y catch será imposible sustituir
los términos de las expresiones por sus definiciones.
Para tratar el uso de situaciones anómalas en el código, así como el manejo de errores,
se utilizará una técnica basada en devolver un valor especial indicando que ha ocurrido una
situación especial.
Para manejar situaciones excepcionales existen diferentes opciones:
Lanzar excepciones.
Incluir un argumento en la llamada a nuestras funciones indicando qué hacer cuando se
produzcan estos casos especiales.
Usar el tipo de datos Option.
Usar el tipo de datos Either.
Se entenderán mejor estas opciones viendo el siguiente ejemplo, en el que la función media
calcula la media de una lista de enteros:
1
2 def media(xs: List[Int]): Int =
3 if (xs.isEmpty) throw new ArithmeticException("media de una lista
vacia!")
4 else xs.sum / xs.length
Algoritmo 4.19: Excepciones.Función media con excepciones
Por los motivos descritos anteriormente, se evitará el uso de excepciones para tratar casos
excepcionales. Ahora se mostrará una solución basada en la inclusión de un argumento para
indicar qué hacer en caso de que se den estas situaciones especiales:
1 def media_1(xs: List[Int], siVacia: Int): Int =
2 if (xs.isEmpty) siVacia
3 else xs.sum / xs.length
Algoritmo 4.20: Excepciones.Función media con argumento para caso especial
Página 149
Esta opción presenta una ventaja, ya que se ha convertido la función media en una función
total. Pero también se observan dos grandes inconvenientes:
Se requiere saber como manejar estos casos especiales a la hora de realizar las llamadas.
Limita el valor devuelto a un valor del tipo devuelto por la función, en este caso Int.
4.4.2. Tipo de datos Option
Por su simplicidad es una de las soluciones más usadas. Algo que se puede apreciar en la
biblioteca estándar de Scala, por ejemplo:
La búsqueda de una clave en un Map devuelve un valor de tipo Option
Las funciones headOption y lastOption devuelven, respectivamente, un tipo Option que
contendrá el primer y el último elemento de una estructura iterable (como por ejemplo las
listas) en caso de que la lista no esté vacía.
La solución basada en el tipo de datos Option servirá para representar, de forma explícita, el
hecho de que en alguna situación especial el valor devuelto no esté definido. Para comprender
mejor esta alternativa se muestra el tipo de datos Option:
1 sealed trait Option[+A]
2 case class Some[+A](get: A) extends Option[A]
3 case object None extends Option[Nothing]
Algoritmo 4.21: Tipo de datos Option
Como se puede ver, Option presenta dos casos:
1. Some. Cuando el valor devuelto esté definido.
2. None. Para esas situaciones especiales en las que el valor devuelto no está definido.
Se verá como quedaría el ejemplo con el uso del tipo de datos Option:
1 def media_2(xs: List[Int], siVacia: Int): Option[Int] =
2 if (xs.isEmpty) None
3 else Some(xs.sum / xs.length)
Algoritmo 4.22: Excepciones. Función media con tipo de datos Option
Ahora se puede ver que la función media devuelve un valor del tipo de datos de retorno (en
nuestro ejemplo Option[Int]) para cada una de las entradas posibles, por lo que la función media
será una función total. Este es otro de los usos del tipo de datos Option, convertir funciones
parciales en funciones totales.
Un inconveniente que presenta el tipo de datos Option para el manejo de errores y casos
especiales es que no aporta información sobre el error o situación especial, simplemente se
devuelve None.
Página 150
4.4.3. Tipo de datos Either
Si se necesitara obtener más información sobre el error que se ha producido para su posterior
tratamiento, el tipo de datos Option no se ajustará a estas nuevas necesidades, ya que sólo indica
que se ha producido un error pero no aporta más información.
Para las situaciones en las que se necesite obtener más información sobre el tipo de error
que se ha producido se utilizará el tipo de datos Either. Para comprender mejor esta opción para
el manejo de errores y casos especiales, se verá en primer lugar la definición del tipo de datos
Either:
1 sealed trait Either[+E, +A]
2 case class Left[+E](value: E) extends Either[E, Nothing]
3 case class Right[+A](value: A) extends Either[Nothing, A]
Algoritmo 4.23: Tipo de datos Either
Either, al igual que Option, presenta dos casos pero en esta ocasión ambos casos definen un
valor. Otra diferencia entre ambos es que el tipo de datos Either representa, de forma general,
valores que pueden ser de dos tipos disjuntos. Cuando se utiliza el tipo de datos Either para el
manejo de errores, por convención, el constructor Left se utiliza para indicar el error, fallo...que
se ha producido.
A continuación se muestra como quedaría el ejemplo haciendo uso del tipo de datos Either:
1 def media_3(xs: List[Int], siVacia: Int): Either[String,Int] =
2 if (xs.isEmpty)
3 Left("media de una lista vacia!")
4 else
5 Right(xs.sum / xs.length)
Algoritmo 4.24: Excepciones.Función media con tipo de datos Either
En ocasiones, sobre todo a la hora de depurar, se puede necesitar más información sobre
el error producido. Para ello puede ser de gran ayuda incluir la excepción en el valor devuelto.
Ejemplo:
1 def division(x: Double, y: Double): Either[Exception, Double] =
2 try {
3 Right(x / y)
4 } catch {
5 case e: Exception => Left(e)
6 }
Algoritmo 4.25: Excepciones.Division con Either
4.5. Ejercicios
Ejercicio 62. Se van a realizar búsquedas dentro de una base de datos de familias. El resultado
de la búsqueda puede ser un valor y también puede ser que no tenga éxito. Implementar la
mónada Maybe que capture este comportamiento.
Ejercicio 63. Dada la siguiente definición de la clase y el objeto persona:
Página 151
1 object Persona {
2
3 val personas = List("P", "MP", "MMP", "FMP", "FP", "MFP", "FFP")
map { Persona(_) }
4
5 private val madres = Map(
6 Persona("P") -> Persona("MP"),
7 Persona("MP") -> Persona("MMP"),
8 Persona("FP") -> Persona("MFP"))
9
10 private val padres = Map(
11 Persona("P") -> Persona("FP"),
12 Persona("MP") -> Persona("FMP"),
13 Persona("FP") -> Persona("FFP"))
14
15 def madre(p: Persona): Maybe[Persona] = relacion(p, madres)
16
17 def padre(p: Persona): Maybe[Persona] = relacion(p, padres)
18
19 private def relacion(p: Persona, relacionMap: Map[Persona,
Persona]) = relacionMap.get(p) match {
20 case Some(m) => Just(m)
21 case None => MaybeNot
22 }
23 }
24
25 case class Persona(nombre: String) {
26 def madre: Maybe[Persona] = Persona.madre(this)
27 def padre: Maybe[Persona] = Persona.padre(this)
28 }
Se pide:
1. Definir la función abueloMaterno que devuelva la persona que es el abuelo materno de la
persona pasada como argumento. Utilizar flatMap y concordancia de patrones.
2. Definir la función abuelaMaterna que devuelva la persona que es el abuelo materno de la
persona pasada como argumento.
3. Definir la función abueloPaterno que devuelva la persona que es el abuelo materno de la
persona pasada como argumento.
4. Definir la función abuelaPaterna que devuelva la persona que es el abuelo materno de la
persona pasada como argumento.
Ejercicio 64. Definir la función abuelos que devuelva una dupla con los dos abuelos de la
persona pasada como argumento. Utilizar la función flatMap.
Ejercicio 65. Definir la función abuelos que devuelva una dupla con los dos abuelos de la
persona pasada como argumento. Utilizar bucles for.
Página 152
Ejercicio 66. Del mismo modo que se ha utilizado en la definición de las funciones de los
ejercicios anteriores la mónada Maybe, se podría emplear con las operaciones matemáticas.
Definir la función divSec que reciba dos números de tipo Double, dividendo y divisor, como
parámetros y devuelva el resultado de la división en caso de que el divisor sea distinto de cero.
Ejercicio 67. Definir la función sqrtSec que reciba un número de tipo Double como parámetro
y devuelva el resultado de realizar la raíz cuadrada al número pasado como argumento en caso
de que el argumento sea positivo.
Ejercicio 68. Definir la función sqrtdivSec que devuelva la raíz cuadra del resultado de divi-
dir dos argumentos de tipo Double pasados como argumentos. Utilizar las funciones definidas
anteriormente.
Página 153
Página 154
Capítulo 5
Tests en Scala
El desarrollo guiado por pruebas de software (Test Driven Development (TDD)), es una
práctica fundamental en el desarrollo del software para construir un producto de calidad. Ade-
más, los tests son muy importantes en otros aspectos relacionados con las metodologías de
desarrollo ágiles1
. Se sabe que el software se comportará según hayamos codificado el mismo,
así que realizar tests al software se convierte en una tarea fundamental para comprobar que el
comportamiento del software se corresponde con la especificación del mismo. Por todo esto,
todo desarrollador debe de tener presente la importancia de las pruebas unitarias.
Para comprobar el funcionamiento del software se dispondrá de dos opciones fundamental-
mente:
1. Afirmaciones (Asserts).
2. Herramientas específicas de tests.
5.1. Afirmaciones Asserts
Para hacer las afirmaciones que debe cumplir el código, se hará uso de uno de los dos méto-
dos (assert y ensuring) definidos en el objeto singleton Predef, cuyos miembros son importados
automáticamente en todos los ficheros fuente.
5.1.1. Assert
Se podrán realizar llamadas al método predefinido assert en cualquier parte del código. Con
el método assert se podrán crear dos tipos de expresiones que se diferenciarán en la información
que aportarán en caso de no cumplirse la afirmación evaluada:
assert(condición). Esta expresión lanzará una excepción del tipo AssertionError si no se
cumple la condición.
assert(condición,explicación). En este caso si la condición no se cumple, lanzará una ex-
cepción del tipo AssertionError que contendrá la explicación dada. La explicación puede
ser un string, un objeto,...ya que su tipo es Any.
A continuación se muestra cómo se pueden definir afirmaciones. Para ello se definirá un
método que calcule el enésimo término de la serie de Fibonacci2
.
1
Procesos de integración continua
2
La serie de Fibonacci o sucesión de Fibonacci es la sucesión infinita de números naturales cuyos dos primeros
términos son 0 y 1 y el resto de términos se calculan como la suma de los dos anteriores.
Página 155
1 def fibonacci(x:Int):Int= x match{
2 case 0 => 0
3 case 1 => 1
4 case _ => fibonacci(x-2)+fibonacci(x-1)
5 }
Algoritmo 5.1: Fibonacci sin recursión de cola
En un primer intento se afirmará que el término fibonaccix−2 siempre es menor que el
término fibonaccix−1, es decir, fibonaccix−2 ≤ fibonaccix−1∀x : x ≥ 2, lo cual se podría
comprobar con la siguiente sentencia assert3
:
1 def fibonacci(x:Int):Int= x match{
2 case 0 => 0
3 case 1 => 1
4 case _ => {assert(fibonacci(x-2) < fibonacci(x-1))
5 fibonacci(x-2) + fibonacci(x-1)}
6 }
Algoritmo 5.2: Fibonacci sin recursión de cola. Assert que falla
Si se prueba la definición...
fibonacci: (x: Int)Int
scala> fibonacci(1)
res0: Int = 1
scala> fibonacci(2)
res1: Int = 1
scala> fibonacci(3)
java.lang.AssertionError: assertion failed
at scala.Predef$.assert(Predef.scala:151)
at .fibonacci(<console>:10)
... 33 elided
Algoritmo 5.3: Excepcion AssertError
Se puede comprobar que se lanza una excepción del tipo AssertionError cuyo mensaje as-
sertion failed no aportará mayor información.
Se muestra una segunda aproximación en la que se construirá la afirmación con la misma
condición pero se añadirá una explicación:
1 def fibonacci(x:Int):Int= x match{
2 case 0 => 0
3 case 1 => 1
4 case _ => {assert(fibonacci(x-2) < fibonacci(x-1), "El fibonacci
de "(x-2)" no es menor que el fibonacci de "(x-1))
5 fibonacci(x-2)+fibonacci(x-1)}
6 }
Algoritmo 5.4: Fibonacci sin recursión de cola. Assert que falla + explicación
Si se introduce en el REPL de Scala la nueva definición, se obtendrá:
3
Se puede observar que la definición de Fibonacci es poco eficiente ya que se podría desbordar la pila de
llamadas recursivas si se quisiera calcular un término muy alto de la serie. Además es exponencial en el número
de sumas realizadas.
Página 156
fibonacci: (x: Int)Int
scala> fibonacci(3)
java.lang.AssertionError: assertion failed: El fibonacci de 1 no es menor que el fibonacci
de 2
at scala.Predef$.assert(Predef.scala:165)
at .fibonacci(<console>:11)
... 33 elided
Algoritmo 5.5: Excepcion AssertError con información
Ahora se podrá comprobar cómo la información que acompaña a la excepción que se lanza
sí que es de utilidad. La afirmación es correcta excepto para los dos primeros términos. Como se
sabía, se partía de una premisa que no era cierta. Por tanto, se modificará la afirmación realizada
previamente, de modo que ahora si esté definida correctamente4
:
1 def fibonacci(x:Int):Int= x match{
2 case 0 => 0
3 case 1 => 1
4 case _ => {assert(fibonacci(x-2) <= fibonacci(x-1)," El
fibonacci de "+(x-2)+" no es menor que el fibonacci de
"+(x-1));
5 fibonacci(x-2)+fibonacci(x-1)}
6 }
Algoritmo 5.6: Fibonacci sin recursión de cola. Assert que no falla
5.1.2. Ensuring
En ocasiones sólo se querrán realizar las comprobaciones al final del método, justo antes
de que devuelva un valor5
. El método ensuring podrá ser utilizado con cualquier tipo, ya que
se realiza una conversión implícita. El método ensuring tomará como argumento una condición
booleana en forma de predicado. Si ensuring devuelve true, entonces el método devolverá el
valor, en otro caso se lanzará una excepción el tipo AssertError. Volviendo al ejemplo que
calcula los términos de la serie de Fibonacci, en un primer intento se hará una afirmación que,
según hemos definido nuestro método, debería fallar:
1 def fibonacci(x:Int):Int={ x match{
2 case 0 => 0
3 case 1 => 1
4 case _ => fibonacci(x-2)+ fibonacci(x-1)
5 }
6 }ensuring(x >= 1)
Algoritmo 5.7: Fibonacci sin recursión de cola. Ensuring que falla
Se puede apreciar que se ha afirmado que siempre se llamará al método con un valor mayor
o igual que uno, lo cual debería de fallar al calcular fibonacci2
6
. De vuelta al REPL se observa
que, con esta afirmación, se lanza una excepción del tipo AssertionError:
4
Obsérvese que ahora se usa ≤.
5
La realización de comprobaciones al final del método se le llama postcondiciones técnicamente.
6
Ya que se producirá una llamada recursiva a fibonacci0 que provocará el lanzamiento de la excepción
Página 157
scala> fibonacci(1)
res8: Int = 1
scala> fibonacci(2)
java.lang.AssertionError: assertion failed
at scala.Predef$.assert(Predef.scala:151)
Algoritmo 5.8: Ensuring.Excepcion AssertError
A continuación se corregirá el código, poniendo una afirmación que sí se sabe que se cumplirá
siempre7
:
1 def fibonacci(x:Int):Int={
2 x match{
3 case 0 => 0
4 case 1 => 1
5 case _ => fibonacci(x-2)+ fibonacci(x-1)
6 }
7 }ensuring(fibonacci(x-2) <= fibonacci(x-1))
Algoritmo 5.9: Fibonacci sin recursión de cola. Ensuring que no falla
Ahora que ya se ha comprendido el uso de ensuring, se verá un ejemplo más realista en el
que usar este método. Para ello se recordará la definición del método inversa de una lista que
aparece en el algoritmo 3.10 (página 93):
1 def inversa[A](xs:Lista[A]):Lista[A]={
2 @annotation.tailrec
3 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match {
4 case Nil => res
5 case Cons(elem,xss)=>go(xss,elem::res)
6 }
7 go(xs,Nil)
8
9 }
Algoritmo 5.10: Inversa de una lista. Recursión de cola
Una afirmación que se puede hacer es que la longitud de la lista original y la longitud de la
inversa de la misma han de ser iguales:
1 def inversa[A](xs:Lista[A]):Lista[A]={
2 @annotation.tailrec
3 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match {
4 case Nil => res
5 case Cons(elem,xss)=>go(xss,elem::res)
6 }
7 go(xs,Nil)
8
9 }ensuring( xs.length == _.length)
Algoritmo 5.11: Ensuring en el cálculo de la inversa de una lista
7
Para asegurar que nuestra función no falla se debería de poner como primera linea de código require(x>=0)
ya que la serie de Fibonacci sólo está definida para números naturales y se trata de una precondición, no de una
postcondición.
Página 158
En la expresión (xs.length) == _.length, el guión bajo hace referencia al argumento pasado
al predicado, en este caso, el resultado del tipo Lista[A] del método inversa.
5.2. Herramientas específicas para tests
Existen muchas opciones a la hora de escoger las herramientas que se usarán para realizar
tests al código en Scala. En primer lugar, se podrá optar por utilizar las herramientas de tests
propias de Java como JUnit8
o TestNG9
, o también se podrá optar por utilizar nuevas herramien-
tas de tests desarrolladas para Scala como ScalaTest, specs10
o ScalaCheck11
.
El uso de estas herramientas para comprobar el comportamiento de nuestro software ayudará
a mejorar el diseño del mismo, desacoplando la especificación del programa de los casos de
prueba y haciendo que el código sea más coherente, legible y fácil de comprender que si se
intentara incluir los casos de prueba en el mismo12
, sobre todo cuando el software es complejo
y tiene una dimensión considerable.
A continuación se verán las opciones que ofrece el framework ScalaTest y cómo se podrán
implementar los casos de pruebas en alguna de las Suites que se ofrecen.
5.2.1. ScalaTest
ScalaTest es un framework de tests para Scala. ScalaTest no pertenece a la librería estándar
de Scala, por lo que antes de empezar a realizar pruebas al código habrá que instalarlo en nuestro
sistema13
.
El framework de ScalaTest ofrece diversos estilos para realizar pruebas al software, cada uno
de ellos desarrollado para cubrir las diferentes necesidades de los programadores. La elección
de cada tipo sólo determinará cómo se quieren expresar los tests, no el tipo de test que se puede
realizar.
La forma más fácil de implementar el primer conjunto de pruebas es crear un clase que
extienda de org.scalatest.Suite. El rasgo Suite incluye un método execute, el cual es sobreescrito
en cada uno de los estilos que el framework ScalaTest ofrece a los programadores. En la tabla
5.1 se pueden ver los distintos estilos disponibles en ScalaTest:
8
http://junit.org
9
http://testng.org
10
http://www.scalatest.org/
11
http://www.scalatest.org/
12
Utilizando afirmaciones, por ejemplo.
13
Habrá que descargarse el framework de la web www.artimia.com y descomprimirlo en la misma carpeta
en la que se tenga instalado Scala en el sistema. Además, se recomienda descargar el plugin ScalaTest del IDE de
Scala para Eclipse.
Página 159
Suites en ScalaTest
FunSuite
Facilita la codificación de los nombres descriptivos de las
pruebas.
Simplifica el desarrollo de tests.
Genera artefactos de salida útiles para la comunicación.
FlatSpec Estructura similar a XUnit en la que los nombres de los tests han
de ser del tipo “X should Y”,“A must B”
FunSpec
Ideal para desarrolladores acostumbrados a Ruby’s RSpec.
Excelente opción de uso general, bien estructurada, que ha-
ce uso de describe y de it para escribir pruebas.
WordSpec
Ideal para portar tests en specsN a Scala
Buena opción para los desarrolladores que quieren mante-
ner un alto grado de disciplina a la hora de especificar sus
tests.
FreeSpec Ofrece absoluta libertad a la hora de decidir cómo escribir tests.
Spec Permite escribir tests como métodos que serán representados me-
diante funciones. Se reducirán los tiempos de compilación, algo
que resultará muy adecuado para proyectos con grandes cantida-
des de pruebas.
PropSpec Ideal para desarrolladores que escriban tests especialmente orien-
tados a verificar propiedades.
FeatureSpec Diseñada con la intención de facilitar el proceso de aceptación de
requisitos entre los programadores y la parte interesada.
Tabla 5.1: Suites del framework ScalaTest
En el primer ejemplo se creará una clase que extienda de org.scalatest.Suite:
Página 160
1
2 import org.scalatest.Suite
3 class EjemploSuite extends Suite {
4 def testAssert() {
5 val v1 = true
6 val v2 = true
7 assert(v1 == v2)
8 }
9 def testAssertResult() {
10 val v1 = 15
11 val v2 = 22
12 assertResult(37) {
13 v1 + v2
14 }
15 }
16 def testIntercept() {
17 val v1 = 9
18 val v2 = 0
19 intercept[ArithmeticException] {
20 v1 / v2
21 }
22 }
23 def testExpect() {
24 val v1 = 12
25 expect(12)(valor1)
26 }
27
28 }
Algoritmo 5.12: ScalaTest. Ejemplo que extiende de org.scalatest.Suite
En este ejemplo se hace uso, además de assert, de:
expect, definiendo el valor esperado como primer parámetro y la validación en el segun-
do.
intercept, indicando entre corchetes la excepción que esperamos capturar y como segun-
do parámetro el código en el que esperamos que la produzca.
La función currificada assertResult. Se definirá el valor que se espera y como segundo
parámetro la verificación.
Una vez visto el primer ejemplo, se estudiará otra de las soluciones que el framework Scala-
Test ofrece: el rasgo FunSuite, en el cual se pueden definir los casos de prueba como funciones
(en lugar de métodos), motivo por el cual FunSuite tiene el prefijo “Fun”14
. El método test defi-
nido en FunSuite será el que se invoque para definir los casos de prueba, pasándole como primer
argumento un string con el nombre del test y como segundo argumento un bloque en el que es-
tará definido el código propio de la prueba. El segundo argumento correspondiente al código de
la prueba será una función, la cual será evaluada por el método test siguiendo la estrategia de
paso de parámetros por necesidad o evaluación perezosa (véase la Subsección 1.4.3: Sistema de
Evaluación de Scala « página 19 »).
14
En referencia a función
Página 161
A continuación se muestra un ejemplo simple del uso de FunSuite, definiendo algunos casos
de prueba para el tipo abstracto de datos Lista definido en el algoritmo 3.9(página 91):
1 import org.scalatest.FunSuite
2 import Lista._
3
4 class prueba extends FunSuite {
5 val miLista:Lista[Int]= Cons(1,Nil)
6 test("Una lista vacia deberia de tener longitud 0") {
7 assert(Nil.length == 0)
8 }
9 test("La longitud de miLista debe ser 1"){
10 assert( miLista length === 1)
11 }
12
13 test("Invocamos Head en una lista vacia") {
14 intercept[NoSuchElementException] {
15 Nil head
16 }
17 }
18 test("Invocamos el metodo !! con un elemento que no existe"){
19 intercept[IllegalArgumentException]{
20 miLista !! 2
21 }
22 }
23 test("Expect 1") {
24 expect(1) (miLista length)
25 }
26
27 }
Algoritmo 5.13: ScalaTest. Ejemplo de la suite FunSuite
En este ejemplo se puede apreciar el uso del triple igual (===) en el test “El tamaño de
MiLista deber ser 1”. La diferencia entre el uso del operador de igualdad habitual y el uso del
triple igual radica en la información que obtenemos en caso de que la afirmación falle. Cuando
una afirmación realizada con assert falla, sólo se sabrá que se ha lanzado una excepción del
tipo AssertionError y un mensaje indicando el número de línea en el que se ha producido el
fallo, pero no se sabrá qué valores son los que han producido el fallo. Se podrían conocer los
valores que producen el fallo, incluyendo una aplicación en el assert que nos proporcione dicha
información, como se vio en el algoritmo 5.4, pero resulta más apropiado el uso del triple igual
para obtener esta información. Igualmente, el triple igual no indicará cual era el valor esperado
y cual ha sido el valor obtenido, simplemente indicará los valores que no cumplen la igualdad.
Para conocer esta distinción se hará uso de expect.
5.3. Ejercicios
Ejercicio 69. Utilizar la suite FunSuite de ScalaTest para comprobar que la suma de números
enteros es conmutativa, es decir, ∀a, b/a, b ∈ Z : a + b = b + a.
Ejercicio 70. Utilizar la suite FunSuite de ScalaTest para comprobar que la suma de números
Página 162
enteros es distributiva con respecto al producto, es decir, ∀a, b/a, b ∈ Z : a∗(b+c) = a∗b+a∗c.
Ejercicio 71. Utilizar la suite FunSuite de ScalaTest para comprobar que la mónada Maybe
cumple con las reglas de las mónadas. Antes de comenzar con la solución del ejercicio, se
recordará la implementación de la clase monádica Maybe realizada en el ejercicio 62:
1 sealed trait Maybe[+A] {
2
3 def flatMap[B](f: A => Maybe[B]): Maybe[B]
4 def map[B](f:A=>B):Maybe[B]
5 }
6
7 case class Just[+A](a: A) extends Maybe[A] {
8 override def flatMap[B](f: A => Maybe[B]) = f(a)
9 override def map[B](f:A=>B):Maybe[B]=Just(f(a))
10 }
11
12 case object MaybeNot extends Maybe[Nothing] {
13 override def flatMap[B](f: Nothing => Maybe[B]) = MaybeNot
14 override def map[B](f:Nothing=>B):Maybe[B]=MaybeNot
15 }
Ejercicio 72. Utilizar la suite FunSuite de ScalaTest para comprobar que se obtiene el mis-
mo resultado al calcular los abuelos de una persona utilizando las funciones definidas en los
ejercicios 64 y 65.
Ejercicio 73. Utilizar la suite FunSuite de ScalaTest para comprobar que se obtiene el mismo
resultado al calcular los abuelos maternos de una persona utilizando las funciones definidas en
el ejercicio 63.
Página 163
Página 164
Capítulo 6
Concurrencia en Scala. Modelo de actores
6.1. Programación Concurrente. Problemática
6.1.1. Introducción
La programación concurrente aúna el conjunto de metodologías, lenguajes, técnicas y he-
rramientas de programación necesarias para la construcción de programas reactivos, es decir,
programas que interaccionan continuamente con el entorno, recibiendo estímulos del mismo y
produciendo salidas en respuesta a los mismos.
Durante la fase de diseño de un software que deba interaccionar con un sistema reactivo se
tendrán que indicar aquellos eventos, acciones, estímulos ...que tengan lugar de forma inde-
pendiente, paralela, concurrente ...
“Un programa concurrente no va a ser más que el resultado de la intercalación no-
determinista de las instrucciones de los procesos secuenciales que lo componen”
[11]
6.1.1.1. Sistema Reactivo Vs Sistema Transformacional
Sistema Reactivo Sistema Transformacional
Interacciona continuamente con el en-
torno, recibiendo estímulos del mismo y
produciendo salidas en respuesta a los
mismos.
Toma unos datos de entrada y devuelve
una salida.
El orden de los eventos en el sistema no
es predecible, viene determinado externa-
mente.
El orden de entrada de los datos está
preestablecido.
La ejecución de los sistemas reactivos no
tiene por qué terminar.
Su ejecución debe finalizar.
Tabla 6.1: Sistema Reactivo Vs Sistema Transformacional
Página 165
6.1.2. Speed-Up en programación concurrente
La programación concurrente es utilizada para mejorar los tiempos de ejecución en máqui-
nas multiprocesadoras. La ganancia en velocidad (speed-up) de un programa secuencial con
respecto a su versión paralela se define como: G = TiempoSecuencial
TiempoParalelo
donde, teóricamente, G de-
bería ser igual o muy próximo al número de procesadores usados, aunque en la realidad existen
diversos inconvenientes que harán que esto no sea así:
Balanceo de carga.
Distribución de las tareas y recolección de los resultados.
La importancia de aprovechar al máximo el uso de los procesadores se ha hecho mayor
desde 2005, momento en el cual se puede observar un punto de inflexión en la tendencia de
fabricación de microprocesadores ya que se apuesta por el aumento del número de núcleos de
los procesadores, en lugar de aumentar la velocidad de los procesadores. El número de núcleos
de los procesadores se ha aumentado fundamentalmente de dos maneras:
Multiplicando el número de núcleos que se encuentran integrados en un único chip, ac-
cediendo todos ellos a la memoria compartida.
Creando núcleos virtualizados que comparten un único núcleo físico de ejecución, el cual
puede ejecutar muchas hebras lógicas de ejecución.
Hay diferentes formas de aprovechar esta nueva tendencia:
Multitarea. Teniendo en ejecución varios programas de forma paralela.1
Multihebra. Ejecutando varias partes de un mismo programa de forma paralela.
La diferencia entre ambos radica en que mientras las tareas que realizan los programas en
multitarea no comparten información, es decir, se ejecuta cada una de forma independiente, las
tareas que lleva a cabo cada hebra de un programa multihebra se ejecutan de forma colabo-
rativa, es decir, necesitan sincronizar sus acciones para obtener el resultado buscado, lo cual
implicará que el desarrollo de estos programas se realice de distinta forma que los programas
secuenciales.
6.1.3. Problemática
6.1.3.1. Propiedades de los programas concurrentes
Cuando se desarrollan programas concurrentes habrá que asegurarse de que cumplen un
conjunto de propiedades, las cuales se dividen en dos grupos (Owicky y Lampart, 82):
1. Seguridad. Aseguran que no ocurrirá nada indeseable durante la ejecución del programa.
2. Vivacidad. Aseguran que algo (bueno) ocurrirá durante la ejecución del programa.
Las propiedades de seguridad son equivalentes a las propiedades de corrección parcial de
los programas secuenciales. Vienen a decir que, si el programa acaba, lo hace con un resultado
correcto.2
1
Algo que se hace desde las primeras versiones de Linux.
2
Suelen poder asegurarse a costa de perder parte de la concurrencia.
Página 166
Exclusión mutua: nunca debe de haber más de un proceso en una sección crítica.3
Sincronización: los procesos deben de estar sincronizados en su ejecución. En un cruce
de caminos, un semáforo no podrá cambiar su color a verde hasta que el otro esté en rojo.
Ausencia de bloqueo Deadlocks: bloqueo permanente de varios procesos, hilos, he-
bras,...que se encuentran a la espera de un recurso en un sistema concurrente que no
será liberado nunca.
Las propiedades de vivacidad estarían relacionadas con la corrección total de los progra-
mas secuenciales: el programa acaba y lo hace con un resultado correcto.
Livelock: todos los procesos están realizando una tarea inútil esperando que suceda una
acción que nunca sucederá.
Postergación indefinida: el sistema en su conjunto sigue progresando pero hay un pro-
ceso o varios que se encuentran bloqueados porque los demás le quitan el acceso a los
recursos compartidos.
Las propiedades de vivacidad son mucho más difíciles de asegurar, ya que el sistema podrá
detectar si los procesos se encuentran bloqueados pero es más difícil determinar si lo que están
haciendo es útil o no (livelock).[11]
6.1.3.2. Bloqueos y secciones críticas
La forma tradicional de afrontar estos problemas ha sido mediante el uso de Mutex/Locks
o semáforos. La diferencia entre ambos es que los semáforos permiten el acceso a múltiples
hebras definidas previamente que pueden tener acceso a las secciones críticas, mientras los
mutex sólo permitirán/denegarán el acceso a un único hilo.
Problemas del uso de bloqueos
El uso de bloqueos es el origen de uno de los mayores problemas en concurrencia: Dead-
Locks.
Son perjudiciales para la utilización de la CPU ya que si una hebra se bloquea la CPU
quedará inactiva excepto si existen otras hebras en espera de ejecución. Además tiene un
gran coste despertar y volver a ejecutar una hebra bloqueada. Como consecuencia, los
programas se ejecutarán más lentamente.
6.1.3.3. Concurrencia en Java
A pesar de que Java ofrece a los programadores suficiente soporte para la creación de apli-
caciones concurrentes, este soporte puede convertirse en el peor enemigo cuando se diseñan
aplicaciones complejas y de un tamaño considerable, principalmente debido a las dificultades
que entraña el modelo de concurrencia basado en procesos sincronizados interactuando me-
diante memoria compartida, así como la complejidad que supone garantizar las propiedades de
seguridad y vivacidad.
3
Fragmento de código donde puede modificarse un recurso compartido y, por tanto, el resto de procesos deberán
ver como una acción atómica
Página 167
Java introdujo la palabra reservada synchronized para hacer referencia a las secciones críti-
cas de los programas concurrentes, indicando que se debe acceder a ellos en exclusión mutua.
Para garantizar el acceso en exclusión mutua a estas zonas críticas Java emplea un mecanis-
mo de bloqueos que garantizan que una sola hebra puede acceder a estas zonas críticas a la vez,
ofreciendo de este modo una forma fácil para compartir datos entre muchas hebras.
En la versión 5 de Java se introdujo la biblioteca java.util.concurrent con la que los progra-
madores tienen a su disposición una librería con un nivel mayor de abstracción de concurrencia,
la cual, usada de forma correcta, debería de ayudar a obtener programas concurrentes con mu-
chos menos errores que si se utilizara la primitiva synchronized, aunque al estar basada en el
modelo de datos compartidos y bloqueos no resuelve los problemas principales de este modelo.
Algunos de los problemas a los que los programadores que desarrollen programas concu-
rrentes en Java, especialmente aquellos que vayan creciendo tanto en tamaño como en comple-
jidad, deberán enfrentarse son:
Se deberá de razonar, siempre que se acceda o se modifique algún dato, si otras hebras
puedan estar accediendo o modificando simultáneamente el mismo dato y cuáles son los
bloqueos que pueden estar activos.
Cada vez que se realice una llamada a un método se deberá de tener en cuenta los bloqueos
que pueda tener y esperar que no se produzcan DeadLock.
El programa puede crear nuevos bloqueos durante la ejecución del mismo, por lo que
estos problemas no pueden ser resueltos durante la compilación.
No se puede confiar en los resultados de las pruebas de la aplicación ya que se podrían
realizar miles de pruebas obteniendo resultados satisfactorios y que falle la primera vez
que se ejecute en la máquina del cliente.
Cuando dos o más hebras pueden acceder simultáneamente a una sección crítica e intentan
modificar algún valor en la misma se dirá que se dan condiciones de carrera ya que el
planificador puede cambiar de hebra en cualquier momento, por lo que no se conocerá el
orden en el que las hebras accederán a los datos y, por tanto, el resultado final dependerá
del planificador.
La clave del modelo basado en actores que incorpora Scala es el modelo de no comparti-
ción, ofreciendo un espacio seguro (el método act de cada actor) en el que se podrá razonar de
manera secuencial. Expresándolo de manera diferente, los actores permiten escribir programas
multihilo como un conjunto independiente de programas monohilo. La simplificación anterior
se cumple siempre y cuando el único mecanismo de comunicación entre actores sea el paso de
mensajes.
6.2. Modelo de actores
6.2.1. Origen del Modelo de Actores
El uso de un modelo basado en actores para dar solución a los problemas de los sistemas
reactivos no es una novedad. El modelo de actores fue desarrollado y publicado por primera vez
por Carl Hewitt, Bishop y Steiger en 1973[12] cuando buscaban crear un modelo con el que
poder formular los programas de sus investigaciones en Inteligencia Artificial.
Página 168
En 1986, Ericsson comenzó el desarrollo del lenguaje de programación Erlang, un lenguaje
de programación funcional puro que basaba su modelo de concurrencia en actores en lugar de
hilos por primera vez. De hecho, la popularidad alcanzada por Erlang en determinados ámbi-
tos empresariales ha 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.
Philipp Harler advirtió el éxito que el modelo de actores había otorgado a Earlang y en 2006
añadió la librería estándar que da soporte a este modelo en Scala. Jonas Bonér, influenciado
tanto por Earlang como por el modelo de actores de Scala, creó Akka en 2009.
6.2.2. Filosofía del Modelo de Actores
El modelo de actores ofrece una solución diferente al problema de la concurrencia, por lo
que habrá que aprender a estructurar los programas para usar actores. Los actores representan
objetos y el modelo de actores describe las interacciones entre dichos objetos inspirándose en
cómo los humanos se relacionan4
. Por este motivo, para comprender el modelo de actores será
de gran ayuda pensar en los actores como personas, en lugar de verlos como objetos abstractos
con unos determinados métodos que podemos invocar.
Formalmente, como fue definido por Hewit, Bishop y Steiger un actor[12]:
Es un objeto con una identidad.
Tiene un comportamiento.
Sólo puede interactuar mediante el paso asíncrono de mensajes.
En lugar de procesos interactuando mediante memoria compartida, el modelo de actores
ofrece una solución basada en buzones y paso de mensajes (eventos) asíncronos. Los mensajes
son almacenados en estos buzones y posteriormente recuperados para su procesamiento por los
actores. En lugar de compartir variables en memoria, el uso de estos buzones permite aislar cada
uno de los procesos.
Los actores son entidades independientes con identidad propia, un estado que puede variar
durante la ejecución y 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.
El comportamiento de los actores se definirá en una función a la que se le pasará cada uno de
los mensajes recibidos y en la que se indicarán las acciones a llevar a cabo en respuesta a cada
mensaje en un momento concreto de la ejecución. Los actores procesan los mensajes en tiempo
de ejecución por lo que no hay una comprobación previa de los mismos, para poder conocer si
un Actor podrá procesar un determinado mensaje, durante el tiempo de compilación.
Como se ha dicho anteriormente, los actores procesan los mensajes de forma asíncrona, pu-
diendo atender a los mismos en un orden distinto al de llegada al buzón. De hecho, un actor
eliminará del buzón el primer mensaje que pueda procesar. Si no pudiera procesar ningún men-
saje, el actor quedaría suspendido hasta que el estado del buzón de entrada cambiase. Un actor
sólo puede procesar un mensaje a la vez, aunque sí puede recibir distintos mensajes simultánea-
mente en su buzón.[23]
Se verá como los actores, además de intercambiar mensajes, pueden crear nuevos actores
o incluso cambiar su propio comportamiento durante su ejecución. Los cambios en el compor-
tamiento de los actores podrán ser determinados por la variación de alguna de las variables de
4
En referencia al intercambio de mensajes entre humanos
Página 169
estado que sean leídas por la lógica de la función de comportamiento o bien porque se cambie
durante la ejecución del programa la propia función que define el comportamiento.
En esta aproximación a la concurrencia basada en el modelo de actores se verá como, si-
guiendo unas buenas prácticas en el desarrollo de programas concurrentes, se podrá asegurar
que se cumplen las propiedades de seguridad de los programas concurrentes, ya que no existirán
secciones críticas a las que sean necesario acceder en exclusión mutua, ni secciones sincroni-
zadas por lo que los problemas derivados de las mismas (deadlocks, pérdida de actualizaciones
de datos,...) no deberían de darse en este modelo. Los actores están pensados para trabajar de
manera concurrente, no de modo secuencial.
6.3. Actores en Scala. Librería scala.actors
6.3.1. Definición de actores
Para comenzar, se implementará un actor en Scala, para ello no habrá más que extender
scala.actors.Actor5
e implementar el método act, el cual definirá cómo procesará el actor los
mensajes. El siguiente fragmento de código ilustra un actor sumamente simple que no realiza
nada con su buzón:
1 import scala.actors._
2 object PrimerActor extends Actor{
3 def act(){
4 for(i <- 1 to 11){
5 println("Ejecutando mi primer actor!")
6 Thread.sleep(1000)
7 }
8 }
9 }
Algoritmo 6.1: Mi Primer Actor
Si se desea ejecutar un actor no tenemos más que invocar a su método start():
scala> PrimerActor.start()
res0: scala.actors.Actor = PrimerActor$@4c2b880a
scala> Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Ejecutando Primer Actor
Se puede apreciar como la salida Ejecutando Primer Actor se intercala con el prompt de
Scala ya que la hebra del actor se ejecuta independientemente de la hebra del shell. Los actores
siempre se ejecutarán independientemente unos de otros.
Otro mecanismo diferente que permitiría instanciar un actor sería hacer uso del método
actor, muy útil y disponible en scala.actors.Actor:
5
Hasta la versión 2.12 de Scala en la que la biblioteca scala.actors desaparecerá.
Página 170
scala> import scala.actors.Actor._
scala> val otroActor = actor {
for (i <- 1 to 11)
println("Otro actor.")
Thread.sleep(1000)
}
Hasta el momento se ha visto como se puede crear un actor y ejecutarlo de manera indepen-
diente pero, ¿cómo se puede conseguir que dos actores trabajen de manera conjunta? Tal y como
se ha descrito en la sección anterior, los actores se comunican mediante el paso de mensajes.
Para enviar un mensaje se hará uso del operador !.
A continuación se definirá un actor que hará uso de su buzón, simplemente esperará un
mensaje e imprimirá aquello que ha recibido:
1 val echoActor = actor {
2 while (true) {
3 receive {
4 case msg => println ("Mensaje recibido " + msg)
5 }
6 }
7 }
Algoritmo 6.2: Procesando primer mensaje
Cuando un actor envía un mensaje no se bloquea y cuando lo recibe no es interrumpido.
El mensaje enviado queda a la espera en el buzón del receptor hasta que este último ejecute la
instrucción receive6
. El siguiente fragmento de código ilustra el comportamiento descrito:
scala> echoActor ! "Primer Mensaje"
Mensaje recibido Primer Mensaje
scala> echoActor ! 11
Mensaje recibido 11
Algoritmo 6.3: Mi primer mensaje
Un actor sólo podrá procesar los mensajes que coincidan con alguna de las definiciones
case definidas en la función parcial receive. Receive devuelve el valor de la última expresión
evaluada, correspondiente al bloque de código ejecutado por la función parcial.
6.3.2. Estado de los actores
El estado de los actores en Scala puede ser implementado haciendo uso de variables reasig-
nables o acarreando el mismo en la pila de llamadas. A continuación se implementará un sen-
cillo contador haciendo uso de un actor, cuyo estado se pueda modificar mediante el paso de
mensajes:
6
receiveWithin es una variante a receive en la que se puede indicar el tiempo de espera en milisegundos, una
vez superado el tiempo de espera devolverá TIMEOUT
Página 171
1 import scala.actors._
2
3 class Contador extends Actor {
4 private var count = 0
5 def act()={
6 while(true) {
7 receive {
8 case "incr" => {
9 count += 1
10 println ("El valor del contador es "+count)}
11
12 case "decr"=> {
13 count -= 1
14 println("El valor del contador es " + count)}
15 }
16 }
17 }
18 }
Algoritmo 6.4: Actor con estado definido con variable mutable
El comportamiento de este actor está definido en la función parcial receive donde se pue-
de encontrar una única sentencia case. Si el mensaje coincide con el string “incr” entonces
se incrementará la variable definida. En cambio, si el mensaje coincide con el string “decr”
decrementará la variable una unidad.
defined class Contador
scala> val counter = new Contador ()
counter: Contador = Contador@40d0dd48
scala> counter.start()
res0: scala.actors.Actor = Contador@40d0dd48
scala> counter ! "hola"
scala> counter ! "adios"
scala> counter ! "incr"
El valor del contador es 1
scala> counter ! "incr"
El valor del contador es 2
scala> counter ! "decr"
El valor del contador es 1
Algoritmo 6.5: Ejecutando Contador con estado mutable
Se observa que el estado se ha definido utilizando una variable privada counter que apunta
a un estado no permanente, algo que no se ajusta a la programación funcional desde un punto
de vista estricto. Si se quisiera definir el contador con un estado inmutable, se podría hacer con
una solución similar a la aportada en el siguiente código:
Página 172
1 import scala.actors._
2
3 class Contador extends Actor {
4 def act()= run(0)
5 private def run(cnt:Int):Unit = {
6 receive {
7 case "incr" => {
8 val newcount = cnt + 1
9 println ("El valor del contador es "+newcount)
10 run(newcount)}
11
12 case "decr"=> {
13 val newcount = cnt - 1
14 println("El valor del contador es " + newcount)
15 run(newcount)}
16 }
17 }
18 }
Algoritmo 6.6: Actor con estado inmutable
Se puede apreciar que se ha utilizado una solución similar a la empleada para evitar la
reasignación de variables en bucles y optimizar el uso de la pila, la recursión de cola (ver la
Subsección 3.4.2: Recursión de cola « página 62 »). A continuación se comprobará que el
funcionamiento del contador se ajusta al comportamiento definido:
defined class Contador
scala> val counter = new Contador ()
counter: Contador = Contador@40d0dd48
scala> counter.start()
res0: scala.actors.Actor = Contador@40d0dd48
scala> counter ! "hola"
scala> counter ! "adios"
scala> counter ! "incr"
El valor del contador es 1
scala> counter ! "incr"
El valor del contador es 2
scala> counter ! "decr"
El valor del contador es 1
Algoritmo 6.7: Ejecutando Contador con estado inmutable
En esta definición de Contador ya no hay estados mutables y, por tanto, el estado se man-
tiene en la pila de mensajes y es pasado al método privado run cada vez que se procesa un
mensaje.
6.3.3. Mejora del rendimiento con react
Como se ha dicho anteriormente, cada actor tiene su propia hebra por lo que todos los
métodos act de los actores tendrán su turno. Sería ideal poder implementar todos los actores
con su propio método act y ejecutar cada uno en su propia hebra pero las máquinas virtuales
Página 173
típicas de Java, capaces de almacenar millones de objetos, sólo pueden manejar unos pocos
cientos de hebras7
.
Para intentar dar una solución a esta problemática, Scala incorpora react, una alternativa al
método receive8
.
En la tabla 6.2 aparecen reflejadas las principales diferencias entre receive y react.
Receive Vs React
Toma una función
parcial
Toma una función
parcial
Devuelve el valor de
la última expresión
evaluada
No devuelve ningún
valor
Preserva la pila de
llamadas de la hebra
actual
No preserva pila de
llamadas
La biblioteca no
puede reutilizar la
hebra
La biblioteca reuti-
lizará la hebra para
otro actor
Tabla 6.2: Receive Vs React
La biblioteca scala.actors.Actor incorpora la función loop que ayudará a crear los actores
basados en eventos. La función Actor.loop ejecutará un bloque de código de forma ininterrum-
pida incluso si en el mismo código se realiza una llamada a react().
En el algoritmo 6.8 se puede ver un ejemplo de la función loop incluida en la biblioteca
scala.actors.Actor.
1 object LoopActor extends Actor{
2 def act(){
3 loop {
4 react{
5 case str : String => println(str)
6 case msg => println("Mensaje sin caso especial")
7 }
8 }
9 }
10 }
Algoritmo 6.8: Método act con loop y react
7
Los cambios de contexto de una hebra a otro suelen tener asociado un coste de cientos o miles ciclos de
procesador
8
En la práctica, los programas necesitarán al menos unos pocos métodos receive pero intentaremos utilizar
react cuando nos sea posible para preservar las hebras.
Página 174
6.4. Actores en Scala con Akka
6.4.1. Introducción
Akka es un framework que simplifica la construcción de aplicaciones concurrentes y dis-
tribuidas en la JVM. Soporta múltiples modelos de programación concurrente, aunque hace
especial énfasis en la concurrencia basada en el modelo de actores. Akka está escrita en Scala
y, desde la versión 2.10, ha sido incorporada a la biblioteca estándar de Scala.
Akka ofrece procesamiento escalable en tiempo real de transacciones.Algunas de las carac-
terísticas que han llevado a incorporar el modelo de actores de Akka en la biblioteca estándar9
son[2]:
Akka está desarrollada en Scala.
La concurrencia en Akka está basada en el modelo de actores
Tolerancia a fallos. Excelente para diseñar sistemas con una gran tolerancia a fallos, sin
paradas y con capacidad para auto-repararse.
Transparencia local. Akka ha sido diseñada para trabajar en entornos distribuidos. Las
interacciones entre actores serán a través del paso de mensajes asíncronos.
Soporte para clusters.
Persistencia. Akka ofrece la posibilidad de que los actores puedan volver a un estado ante-
rior incluso después de ser reiniciados o iniciados nuevamente tras una parada volviendo
a reproducir los mensajes que el actor hubiera recibido.
6.4.1.1. Diferencias entre Akka y la librería Actors de Scala.
Las principales diferencias entre Akka y la biblioteca scala.actors son:
En Akka existe un sólo tipo de Actor. A diferencia con la biblioteca de actores de
Scala en la que se encuentran disponibles diferentes tipos de actores (Reactor, ReplyActor,
DaemonActor ...), en Akka todos los actores heredarán la funcionalidad de la clase Actor.
Por ejemplo, con la biblioteca de Scala se podría tener la siguiente definición:
1 class MyServ extends ReplyReactor
Algoritmo 6.9: Diferencias entre Akka y la librería de Scala. Instanciación en Scala
En Akka se definirá de la siguiente forma:
1 class MyServ extends Actor
Algoritmo 6.10: Diferencias entre Akka y la librería de Scala. Instanciación en Scala
Instanciación de actores. En Akka solo se podrá acceder a los actores haciendo uso de
la interfaz ActorRef. Se podrán obtener instancias de ActorRef invocando el método actor
del objeto ActorDSL o haciendo uso del método actorOf en una instancia de ActorRef-
Factory. Por ejemplo, con la biblioteca scala.actors se podría haber definido:
9
Sustituyendo la implementación original del modelo de actores que será finalmente eliminada en la versión
2.12 de Scala
Página 175
1 val myActor = new MyActor(arg1, arg2)
2 myActor.start()
Algoritmo 6.11: Diferencias entre Akka y la librería de Scala. Instanciación en Scala I
1 object MyActor extends Actor {
2 // MyActor definition
3 }
4 MyActor.start()
Algoritmo 6.12: Diferencias entre Akka y la librería de Scala. Instanciación en Scala II
Ahora, en Akka, se definirán de la siguiente manera10
:
1 ActorDSL.actor(new MyActor(arg1, arg2)
Algoritmo 6.13: Diferencias entre Akka y la librería de Scala. Instanciación en Akka I
1 class MyActor extends Actor {
2 // Definicion MyActor
3 }
4 object MyActor {
5 val ref = ActorDSL.actor(new MyActor)
6 }
Algoritmo 6.14: Diferencias entre Akka y la librería de Scala. Instanciación en Akka II
Se deberá de tener en cuenta que los actores, en Akka, siempre se inician cuando se ins-
tancian, por lo que si se desea que no se inicien en el momento en el que son instanciados,
habrá que especificarlo.
Eliminación en Akka del método act. En Scala el comportamiento de los actores se
define implementando el método act. En Akka hay un único manejador de mensajes, una
función parcial devuelta por el método recieve que se aplicará a cada uno de los mensajes
existentes en el buzón de un actor. Si se quisiera incluir algún código de iniciación, habría
que incluirlo en el método preStart. Ejemplo:
1 def act() {
2 // codigo de iniciacion
3 loop {
4 react { //cuerpo }
5 }
6 }
Algoritmo 6.15: Eliminación de act. Ejemplo en Scala
10
Todas las referencias al objeto MyActor ahora deberán de hacerse a MyActor.ref
Página 176
1 override def preStart() {
2 // codigo de iniciacion
3 }
4 def receive = {
5 // body
6 }
Algoritmo 6.16: Eliminación de act. Ejemplo en Akka
Cambio de la signatura de métodos. Habrá que tener en cuenta que, aunque conserven
la misma funcionalidad, la signatura de algunos de los métodos de Actor y ActorRef cam-
bian en Akka. Como, por ejemplo, el método exit() de Scala, ahora en Akka se utilizará
context.stop(self)
Éstas son algunas de las diferencias entre el modelo de actores de Scala y Akka. [13] y [2]
6.4.2. Definición y estado de los actores
Una vez se han visto las principales características de Akka, así como las diferencias más
destacables entre el modelo de actores de Scala y el de Akka, es el momento de tener una
primera toma de contacto con el rasgo Actor de Akka antes de definir el primer actor.
El rasgo Actor define un método abstracto receive11
que devuelve algo del tipo Receive:
1 Type Receive = PartialFunction[Any,Unit]
2
3 trait Actor {
4
5 def receive:Receive
6
7 ...
8
9 }
Algoritmo 6.17: Akka.Trait Actor
A continuación, se implementará en Akka un actor con un comportamiento igual al visto en
la Subsección 6.3.2: Estado de los actores « página 171 » y sabiendo que el estado de un actor
en Akka, al igual que ocurría en el modelo de actores de Scala, se puede implementar tanto con
variables privadas, objetos mutables, etc., así como llevando el mismo en la pila de llamadas.
En primer lugar se implementará en Akka un actor similar al implementado en el algoritmo
6.4 (página 172) en el que el valor del contador se guarda en una variable privada.
11
receive es una función parcial de Any a Unit y describe la respuesta del actor a un mensaje.
Página 177
1 import akka.actor.Actor
2
3 class Contador extends Actor {
4 private var count = 0
5 def receive = {
6 case "incr" => {
7 count += 1
8 println ("El valor del contador es "+count)}
9 case "decr" => {
10 count -= 1
11 println("El valor del contador es " + count)}
12 }
13 }
Algoritmo 6.18: Akka.Actor con estado definido con variable mutable
Se puede observar que las diferencias entre el algoritmo 6.18 y el algoritmo 6.4 se ajustan
a las descritas anteriormente en la Subsubsección 6.4.1.1: Diferencias entre Akka y la librería
Actors de Scala « página 175 ».
A continuación se implementará en Akka un actor similar al implementado en el algoritmo
6.6 (página 173), es decir, se implementará un actor capaz de mantener su estado sin necesidad
de utilizar variables reasignables que puedan dar lugar a comportamientos no deseados. En una
primera aproximación se podría escribir:
1 import akka.actor.Actor
2
3 class Contador extends Actor {
4 def receive = run(0)
5 private def run(cnt:Int):Receive = {
6 case "incr" => {
7 val newcount = cnt + 1
8 println ("El valor del contador es "+newcount)
9 run(newcount)}
10
11 case "decr"=> {
12 val newcount = cnt - 1
13 println("El valor del contador es " + newcount)
14 run(newcount)}
15 }
16 }
Algoritmo 6.19: Akka.Actor con estado inmutable
Esta solución no aprovecha el potencial de Akka. Como se vio en la Subsección 6.2.2:
Filosofía del Modelo de Actores « página 169 », los actores pueden cambiar la función que
define su comportamiento durante la ejecución del programa, algo que se puede aprovechar
para modificar el estado del actor sin necesidad de utilizar variables.
El tipo Actor sólo posee el método receive, el cual, como ya se ha visto, define el compor-
tamiento de un actor. El encargado de llevar a cabo la ejecución del comportamiento descrito
es el ActorContext asociado a cada actor. Para comprender todo esto mejor, se muestra el rasgo
ActorContext y cómo se refleja en el rasgo Actor:
Página 178
1 trait ActorContext {
2 def become(behavior:Receive, discardOld:Boolean = true):Unit
3 def unbecome():Unit
4 }
5
6 trait Actor {
7 implicit val context: ActorContext
8 def receive:Receive
9
10 ...
11
12 }
Algoritmo 6.20: Akka.Trait Actor y Trait ActorContext
Cada actor tiene asociada una pila de comportamientos. En la cima de esta pila se encuentra
siempre el comportamiento activo en ese momento. Los métodos become y unbecome están
relacionados con las operaciones tradicionales sobre pilas. Habitualmente, utilizaremos el mé-
todo become para cambiar la función que se encuentra en la cima de la pila y que define el
comportamiento actual de un actor.
Para acceder al contexto dentro de un actor sólo habrá que escribir context.
Ahora se modificará el algoritmo para que haga uso del método become para actualizar el
estado del actor.
1 import akka.actor.Actor
2
3 class Contador extends Actor {
4 def receive = run(0)
5 private def run(cnt:Int):Receive = {
6 case "incr" => {
7 val newcount = cnt + 1
8 println ("El valor del contador es "+newcount)
9 context.become(run(newcount))}
10
11 case "decr"=> {
12 val newcount = cnt - 1
13 println("El valor del contador es " + newcount)
14 context.become(run(newcount))}
15 }
16 }
Algoritmo 6.21: Akka.Actor con estado inmutable (become)
Puede parecer que otra vez se ha utilizado recursión de cola para que nuestro actor sea
un objeto inmutable ya que se realiza una llamada a la función run dentro de si misma, pero
esta llamada es asíncrona ya que context.become evaluará el nuevo comportamiento cuando se
procese el siguiente mensaje.
Otra las acciones que los actores pueden hacer es intercambiar mensajes. Hasta el momento
se ha visto cómo se pueden mandar mensajes de un actor a un segundo actor, pero no cómo este
segundo actor puede responder al actor que mandó el mensaje.
Si se observa el comportamiento de la última versión de nuestro actor Contador, rápidamen-
te se puede notar la ausencia de un método que permita saber cuál es el valor del contador en
Página 179
un momento dado y, al mismo tiempo, permita prescindir de las sentencias println en nuestro
actor Contador que como se sabe, presenta efectos colaterales no deseables en las funciones.
Cada uno de los actores posee una dirección única que es determinada por el tipo ActorRef.
A continuación se muestra la clase abstracta ActorRef y el rasgo Actor para tener una idea de
cómo se puede utilizar:
1
2 trait Actor {
3 implicit val self: ActorRef
4 implicit val context: ActorContext
5 def receive: Receive
6 def sender: ActorRef
7
8 ...
9
10 }
11 abstract class ActorRef {
12 def !(msg:Any)(implicit sender: ActorRef =
Actor.noSender):Unit
13 ...
14 }
Algoritmo 6.22: Akka.Trait Actor y Clase Abstracta ActorRef
Cada actor conoce su propia dirección del tipo ActorRef, disponible de forma implícita
haciendo uso de self. Cuando se envía un mensaje a otro actor, la dirección del remitente se
envía de forma implícita haciendo referencia a la variable inmutable self. El actor que recibe el
mensaje dispondrá del valor de la dirección del remitente invocando sender, que le devolverá
una dirección del tipo ActorRef.
Ahora ya se puede incorporar un nuevo comportamiento al actor Contador que devuelva el
valor del mismo al actor que haga la petición:
1 import akka.actor.Actor
2
3 class Contador extends Actor {
4
5 private def run(cnt:Int):Receive = {
6
7 case "incr" => context.become(run(cnt + 1))
8 case "decr"=> context.become(run(cnt -1))
9 case "get"=> sender ! cnt
10
11 }
12 def receive = run(0)
13 }
Algoritmo 6.23: Akka.Mensjaes bidireccionales
Ahora si se recubre un mensaje con el string “get” se enviará al remitente el valor del
contador.
Según se describe en la Subsección 6.2.2: Filosofía del Modelo de Actores « página 169
», la última de las acciones fundamentales que pueden realizar los actores es la de crear nue-
vos actores, así como parar a los mismos. ActorContext presenta métodos para realizar dichas
Página 180
operaciones. En el siguiente ejemplo se ven dichos métodos:
1 trait ActorContext {
2 def become(behavior:Receive, discardOld:Boolean = true):Unit
3 def unbecome():Unit
4 def actorOf(p:Props, name: String): ActorRef
5 def stop(a: ActorRef): Unit
6 }
Algoritmo 6.24: Akka.Trait ActorContext II
En primer lugar, para crear actores se utilizará el método actorOf, cuyo primer parámetro
de tipo Props servirá para describir cómo crear el actor y el segundo parámetro servirá para
asignarle un nombre. Los actores son siempre creados por otros actores, por lo que formarán un
sistema jerárquico.
Un actor puede parar otros actores haciendo uso del método stop, el cual frecuentemente irá
acompañado de self12
.
Mostraremos un ejemplo definiendo un nuevo actor que haga uso del actor Contador y que
reciba la cuenta del contador después de varios mensajes:
1 import akka.actor.Actor
2 import akka.actor.Props
3
4 class MainContador extends Actor {
5 val counter = context.actorOf(Props[Contador],"counter")
6
7 counter ! "incr"
8 counter ! "incr"
9 counter ! "incr"
10 counter ! "decr"
11 counter ! "decr"
12 counter ! "get"
13
14 def receive = {
15 case msg:Int =>
16 println("El valor del contador es:"+msg)
17 context.stop(self)
18 }
19 }
Algoritmo 6.25: Akka.Crear Actores
Ya se han visto las principales acciones de los actores. La biblioteca Akka es mucho más
potente, ofreciendo al programador diversas herramientas que facilitarán el diseño de programas
concurrentes complejos.[19]
6.5. Buenas prácticas
Llegados a este punto, ya se conocen los fundamentos básicos para escribir actores. El punto
fuerte de los métodos vistos hasta este momento es que ofrecen un modelo de programación
concurrente basado en actores por lo que, en la medida que se puedan escribir siguiendo este
12
Lo que significará que el actor desea parar su ejecución
Página 181
estilo, el código será más sencillo de depurar y tendrá menos interbloqueos y condiciones de
carrera. Los siguientes apartados describen, de manera breve, algunas directrices que permitirán
adoptar un estilo de programación basado en actores.
6.5.1. Ausencia de bloqueos
Un actor no debería bloquearse mientras se encuentra procesando un mensaje. El proble-
ma radica en que mientras un actor se bloquea, otro actor podría realizar una petición sobre el
primero. Si el actor se bloquea en la primera petición no se dará cuenta de una segunda solici-
tud. En el peor de los casos, se podría producir un interbloqueo en el que varios actores están
esperando a otros actores que a su vez están bloqueados.
En lugar de bloquearse, el actor debería esperar la llegada de un mensaje indicando que
la acción está lista para ser ejecutada. Esta nueva disposición, por norma general, implicará la
participación de otros actores.
6.5.2. Comunicación exclusiva mediante mensajes
La clave de los actores es el modelo de no compartición, ofreciendo un espacio seguro (el
método act de cada actor) en el que se podría razonar de manera secuencial. Expresándolo de
manera diferente, los actores permiten escribir programas multihilo como un conjunto indepen-
diente de programas monohilo. La simplificación anterior se cumple siempre y cuando el único
mecanismo de comunicación entre actores sea el paso de mensajes.
6.5.3. Mensajes inmutables
Puesto que el modelo de actores provee un entorno monohilo dentro de cada método act, no
habrá que preocuparse de si los objetos que se utilizan dentro de la implementación de dicho
método son thread-safe. Este es el motivo por el que el modelo de actores es llamado shared-
nothing, los datos están confinados en un único hilo en lugar de ser compartidos por varios.
La excepción a esta regla reside en la información de los mensajes intercambiados entre ac-
tores, dado que es compartida por varios de ellos. Por tanto, habrá que prestar especial atención
al hecho de que los mensajes intercambiados entre actores sean thread-safe.
6.5.4. Mensajes autocontenidos
Cuando se devuelve un valor al finalizar la ejecución de un método, el fragmento de código
que realiza la llamada se encuentra en una posición idónea para recordar lo que se estaba ha-
ciendo anteriormente a la ejecución del método, recoger el resultado y actuar en consecuencia.
Sin embargo, en el modelo de actores las cosas se vuelven un poco más complicadas. Cuan-
do un actor realiza una petición a otro actor, el primero de ellos no es consciente del tiempo que
tardará la respuesta, instantes en los que dicho actor no debería bloquearse, sino que debería
continuar ejecutando otro trabajo hasta que la respuesta a su petición le sea enviada. ¿Puede el
actor recordar qué estaba haciendo en el momento en que se envió la petición inicial?
Se podrían adoptar dos soluciones para intentar resolver el problema planteado en el párrafo
anterior:
Un mecanismo para simplificar la lógica de los actores sería incluir información redun-
dante en los mensajes. Si la petición es un objeto inmutable, el coste de incluir una refe-
rencia a la solicitud en el valor de retorno no sería costoso.
Página 182
Otro mecanismo adicional que permitiría incrementar la redundancia en los mensajes
sería la utilización de una clase diferente para cada una de las clases de mensajes de los
que se dispongan.
6.6. Ejercicios
Ejercicio 74. Implementar un sistema concurrente en el que dos actores jueguen al pin-pong
de forma que el primero de ellos inicie el juego con la palabra “ping” y el adversario responda
con la palabra “pong”. Después de tres intercambios de mensajes el juego finalizará.
Página 183
Página 184
Capítulo 7
Conclusiones
Scala presenta las características del paradigma de la programación funcional y el paradigma
de la POO, ofreciendo un amplio conjunto de mecanismos para la resolución de problemas, lo
que permite a los desarrolladores seleccionar la técnica que mejor se adapte a las características
del problema que están tratando de resolver.
En primer lugar, se ha presentado Scala como lenguaje de programación, introduciendo los
elementos básicos presentes en cualquier lenguaje de programación como son los tipos básicos,
la definición de variables (tanto mutables como inmutables) o los diferentes tipos de operadores
existentes para esos tipos básicos. Se ha explicado cómo crear nuevos operadores y cómo definir
características propias de los operadores como son la prioridad y la asociatividad, las cuales
determinarán el resultado final de evaluación de una expresión.
Se ha utilizado Scala para presentar otros conceptos básicos de la programación como son la
visibilidad y el ámbito de uso de las variables o las sentencias de control selectivas e iterativas.
Se ha visto las diferentes técnicas de evaluación y cómo se pueden definir variables que
respondan a cada una de esas técnicas de evaluación. Se han tratado las ventajas e inconve-
nientes de cada una de las estrategias de evaluación y se ha presentado la evaluación estricta,
o por valor, como la estrategia de evaluación utilizada por defecto en el paso de parámetros a
métodos en Scala. También se ha indicado cómo se puede cambiar la estrategia de evaluación
de los parámetros de las funciones y utilizar evaluación no estricta o por nombre, separando de
esta forma los conceptos de definición y evaluación de una función, es decir desacoplando el
“cómo y cuándo”. Igualmente, se ha visto cómo se puede utilizar la estrategia de evaluación
perezosa en Scala, estrategia usada por defecto en otros lenguajes de programación funcional
como Haskell.
Por tanto, se puede apreciar que Scala es un lenguaje que puede ser utilizado para estudiar
los conceptos fundamentales de los lenguajes de programación.
Cuando se aprende un lenguaje de POO, las clases son uno de los primeros conceptos que
son estudiados y, por tanto, algo que se tenía que definir ya que Scala es un lenguaje orientado
a objetos puro, en el que todo son objetos: las funciones, los valores...
Las clases son el núcleo de las aplicaciones en Scala, por lo que se ha explicado la jerar-
quía de clases en Scala y se han cubierto los conceptos fundamentales presentes en un lenguaje
orientado a objetos. Se ha visto el polimorfismo en el paradigma de la POO en referencia al
subtipado y cómo la herencia simple y el uso de los rasgos, que permiten un mecanismo si-
milar a la herencia múltiple en nuestras clases dentro de Scala, aumentan, de este modo, la
reusabilidad del código. Se aborda otro concepto relacionado con el polimorfismo en la POO,
la generalización del uso de las clases en referencia a las clases genéricas o parametrizadas.
Se muestra como un parámetro de tipo de una clase (clase parametrizada) otorga otra ca-
Página 185
racterística a la clase, ya que, además de definir la clase un tipo propio, tiene la posibilidad
de construir un nuevo tipo cada vez que sea instanciada con un tipo distinto1
. Por ejemplo,
List[String] y List[Int] presentan el mismo tipo List aunque hayan sido instanciadas con tipos
diferentes.
Los tipos pueden hacer más restrictivas las clases y funciones, indicando cotas, o hacerlas
menos restrictivas cuando se especifica la varianza de un tipo.
Además, se pone de manifiesto cómo se pueden aprovechar las características de un tipo
especial de clases presentes en Scala, las case class (como por ejemplo en la concordancia de
patrones) o las ventajas que ofrecen este tipo de clases como la creación de un método de fábrica
con el nombre de la clase automáticamente.
En Scala, cada instancia de clase y cada literal corresponden a un tipo. Pero en Scala, las
clases son algo más que meros contenedores de valores y métodos. Por ejemplo, los rasgos
otorgan una característica especial a las clases ya que, si se tiene definida una clase y diferentes
rasgos, ésta puede ser instanciada con uno o varios de los rasgos definidos, lo que hará, por
ejemplo, que un mismo tipo pueda presentar diferentes comportamientos.
Tras este análisis de Scala como lenguaje orientado a objetos puro se puede observar que
Scala es un lenguaje en el que se ven reflejados los conceptos relacionados con la programación
orientada a objetos.
Seguidamente se ha definido el concepto de programación funcional, se ha explicado exac-
tamente en qué consiste la programación funcional, se han argumentado motivos por los que
es aconsejable el uso de la programación funcional y cómo hay que cambiar la forma de razo-
nar cuando se aborda un problema desde el punto de vista del paradigma de la programación
funcional y la evaluación de expresiones.
Se ha utilizado el lenguaje para ver conceptos fundamentales asociados al paradigma de la
programación funcional como la definición de funciones, definición de operadores funcionales,
la currificación de funciones, parcialización...o cómo se pueden expresar algoritmos iterativos
haciendo uso de la recursión. Profundizando un poco más, se muestra cómo trata Scala las
funciones y la importancia del método de fábrica apply.
También se ha estudiado el polimorfismo dentro del paradigma de la programación funcio-
nal y cómo la aplicación de la genericidad en las funciones multiplica la reutilización de código.
Se ha introducido el concepto de funciones de alto nivel y se muestra cómo Scala trata a las fun-
ciones como tipos de primera clase. También se ha visto cómo escribir funciones polimórficas,
así cómo las funciones polimórficas pueden definir restricciones sobre que tipos pueden usar
las mismas.
Otro aspecto importante dentro de la programación es la definición de estructuras algebrai-
cas de datos y cómo pueden ser implementadas en un lenguaje funcional como Scala. También
se ha puesto de manifiesto una de las características de Scala como es la potencia de usar la
concordancia de patrones, sobre todo en la definición de una estructura de datos lineal como la
lista enlazada o de una estructura de datos no lineal, como los árboles.
Se ha explicado la técnica de la compartición estructural, utilizada para optimizar los recur-
sos empleados por las estructuras de datos inmutables.
Posteriormente se han introducido las colecciones definidas en Scala, una de las razones
más importantes para utilizar este lenguaje ya que ofrecen soluciones muy elegantes para una
gran cantidad de problemas. Se ha visto cómo se pueden manejar las colecciones: crear, filtrar,
aplicar o realizar otro tipo de operaciones sobre los elementos de una colección. Se ha prestado
especial atención a las colecciones inmutables de Scala, colecciones que presentan característi-
cas propias de la programación funcional como la imposibilidad de modificar el tamaño o los
1
A estos tipos se los conocen con el nombre de constructores de tipos.
Página 186
elementos de las mismas. Las colecciones inmutables se importan, por defecto, automáticamen-
te dentro de los espacios de nombres en Scala2
.
Es muy común que los lenguajes funcionales nos ofrezcan la posibilidad de iterar o aplicar
los elementos de una colección pero el hecho de un sistema de tipos estáticos de la colección de
origen y de la colección obtenida como resultado ya es algo menos usual.
Los lenguajes funcionales que nos ofrecen la posibilidad de definir tipos seguros en las
funciones de orden superior que manipulan las colecciones permitirán desarrollar un código
muy expresivo y que minimice los errores de conversión de tipos en tiempo de ejecución, lo que
se traducirá en una gran productividad para los programadores.
También se han explicado las virtudes y desventajas que presentan cada uno de las coleccio-
nes inmutables estudiadas y la importancia de escoger una u otra en función de las operaciones
que se vayan a realizar sobre las mismas.
Llegados a este punto se puede entender el hecho de que Scala se presente como un lenguaje
de programación adecuado para explicar los conceptos básicos de la programación funcional.
A continuación se han estudiado los tipos dentro de la programación funcional avanzada
aunque anteriormente se explica como se pueden crear tipos propios, definiendo clases, ras-
gos...Pero en Scala los tipos son algo más que una clase. Se hace referencia a los constructores
de tipos3
y a los tipos compuestos4
y se introducen los tipos estructurales como una forma de
definir tipos abstractos (tipos en los que se han especificado ciertas características), los tipos
de orden superior como tipos que utilizan otros tipos para construir un tipo nuevo y los tipos
existenciales que nos permitirán indicar la existencia de un tipo sin especificar de qué tipo se
trata.
Se ha presentado otra característica del lenguaje como es el método de búsqueda de Scala y
la importancia de los implícitos. Se ha mostrado que tanto los parámetros implícitos de las fun-
ciones, como las clases implícitas comparten el mismo mecanismo de búsqueda, aunque tengan
diferentes aplicaciones. Las clases se utilizan para hacer conversiones entre tipos, mientras que
en las funciones se utiliza para evitar la declaración de todos los argumentos de las mismas.
Se expone cómo Scala busca la declaración de los parámetros implícitos en el propio espacio
de nombres y cómo cuando no es capaz de resolver un método, busca una posible conversión
existente dentro de su espacio de nombres.
También se ha hecho hincapié en la importancia de hacer un uso responsable de los implí-
citos.
Dentro de la programación funcional avanzada, se ha explicado un patrón muy importante
como es el patrón funtor en Scala5
y como simplifica la aplicación de una función a los elemen-
tos de un contenedor. Dentro de la programación funcional, se ha visto que la aplicación del
patrón funtor devolverá un contenedor igual al contenedor de los elementos previos a la aplica-
ción de la función. Otro de los patrones importantes en la programación funcional es el patrón
mónada, el cual presenta una serie de reglas que han de cumplir las clases monádicas para poder
ser utilizadas como mónadas. Se ha visto como para definir clases monádicas en Scala sólo se
tendrán que definir los métodos unit y flatMap6
.
Se ha comprobado como el uso de ambos patrones, funtores y mónadas, nos permiten definir
combinadores que serán aplicables a una gran cantidad de tipos de datos, tipos que en principio
2
Scala también presenta un conjunto de colecciones mutables pero estas no han sido estudiadas por carecer de
interés dentro de la programación funcional.
3
Clases parametrizadas
4
Resultado de la combinación de otros tipos ya conocidos
5
Como implementación de la operación map de Scala, también llamada fmap dentro de la literatura relativa a
la programación funcional
6
También conocidos como identity y bind dentro de la literatura.
Página 187
podría parecer que no tienen nada en común.
Las colecciones monádicas en Scala nos ofrecerán una forma segura de encadenar operacio-
nes y de manejar situaciones sensibles y complejas como las condiciones de error o el manejo
de excepciones.
Las utilidades de las mónadas se ha ejemplifican con uno de los usos de las mismas, como
envoltorio (con la mónada identidad) o la importancia de utilizar la clase monádica Try para
manejar situaciones en las que aparecen excepciones.
Se ha estudiado cómo la utilización de las mónadas Either y Option nos pueden ayudar a
la hora de manejar situaciones especiales durante el desarrollo de las estructuras de datos sin
necesidad de lanzar excepciones. Se ha visto cómo estos tipos algebraicos de datos nos ofrecen
una manera simple de razonar a la hora de manejar situaciones especiales y cómo se pueden
aprovechar sus características modulares y de composición. El uso de estas mónadas, junto con
funciones de orden superior, ayudan a tratar con errores de una manera especial. Algo que sería
imposible si se tuvieran que lanzar excepciones. De este modo, el uso de excepciones se dejaría
para situaciones irrecuperables del programa.
A pesar de no haber desarrollado programas complejos, los principios definidos son aplica-
bles tanto en la construcción de programas complejos, como en programas más simples.
Tras este recorrido por la programación funcional y después de haber estudiado los funtores
y las mónadas en Scala, es notable que Scala es un lenguaje de programación que se puede
utilizar para explicar los conceptos relacionados con la programación funcional avanzada.
También se han presentado, de forma resumida, algunas de las diferentes posibilidades que
nos ofrece Scala para realizar comprobaciones al código escrito. Se analiza cómo se pueden
utilizar algunas de estas posibilidades para hacer pruebas a las estructuras de datos y así poder
constatar que los resultados obtenidos son los esperados o que las estructuras cumplen con las
propiedades que se han definido sobre ellas.
Adicionalmente, se ha examinado brevemente la solución que propone Scala a la proble-
mática de la concurrencia, el modelo de actores, una solución basada en el paso asíncrono de
mensajes inmutables. Inicialmente se ha explicado las bases del modelo de actores en Scala, la
biblioteca scala.actors, para después introducir los principios y explicar las diferencias de una
solución mucho más completa, la biblioteca Akka. A través de la concurrencia se han visto tam-
bién las posibles soluciones al manejo de estados en la programación funcional y cómo cambiar
el comportamiento de una clase.
Definitivamente es posible aseverar que Scala es un lenguaje en el que se pueden explicar
tanto los conceptos básicos presentes en los lenguajes de programación, como los conceptos re-
lativos a la POO o los conceptos relacionados con el paradigma de la programación funcional,
todos ellos presentes en un mismo lenguaje de programación. Además, Scala dispone de dife-
rentes soluciones para las necesidades que pueda tener el programador a la hora de desarrollar
aplicaciones concurrentes o realizar pruebas al código escrito.
Todas las características descritas previamente, junto que el código generado por el com-
pilador se ejecuta en la AcrónimosJVM y la posibilidad de interacción que presenta con Java
hacen que Scala sea un lenguaje de programación muy interesante para el programador.
Por tanto, se podría afirmar que Scala es un lenguaje de programación que podría ser utili-
zado como una alternativa dentro del ámbito de la educación.
Página 188
Capítulo 8
Solución a los ejercicios propuestos
8.1. Evaluación en Scala
Solución del ejercicio 1 Si ejecutamos el código en el intérprete veremos que devuelve 33.
Razonemos la solución:
1. En el ámbito principal del intérprete se define el valor de las variables x (10) e y (20), así
como la función prueba.
2. La ejecución de prueba(3) crea un nuevo ámbito de evaluación, dentro del ámbito princi-
pal, en el que se ejecuta el cuerpo de la función.
3. En el ámbito de evaluación se liga el valor de z (argumento de prueba ) con el valor 3
(parámetro con el que se realiza la llamada).
4. En este nuevo ámbito se ejecuta el cuerpo de la función: se crea la variable local x con
el valor 0 y se evalúa la expresión x+y+z . El valor de x y z están definidos en el propio
ámbito local (0 y 3). El valor de y se obtiene del entorno padre: 20. El resultado de la
expresión es 23.
5. Se invoca a la función g, con el valor 23 como parámetro y . Para evaluar esta invocación
se crea un nuevo ámbito local dentro del ámbito global en donde está definida la función.
6. Dentro de este nuevo ámbito se evalúa la expresión x+y devolviéndose 33 como resultado
ya que x valdrá 10 (definida en el ámbito global).
8.2. Introducción a la Programación Funcional
Solución del ejercicio 2 No.
Solución del ejercicio 3 Sí.
Solución del ejercicio 4 No.
Página 189
Solución del ejercicio 5 No.
Solución del ejercicio 6 Sí.
Solución del ejercicio 7
Int
10
Solución del ejercicio 8
fact2
• Se aprecian dos problemas en la función fact2: las llamadas a la función con núme-
ros negativos provocarán un comportamiento incorrecto de la función. Las llamadas
a fact2 con números muy grandes puede provocar desbordamiento de pila.
• El primer problema se podría solucionar añadiendo a nuestro código la precondición
n > 0. El segundo problema se podría solucionar haciendo una versión recursiva de
cola.
1 def fact2(n: Int):Int = {
2 require (n>=0)
3 @annotation.tailrec
4 def go(x:Int,acu:Int):Int= if (x == 0) acu else
go((x-1),(acu*x))
5 go(n,1)
6 }
Solución del ejercicio 9
Int.
Error de compilación.
15.
Error de compilación.
Solución del ejercicio 10 6.
Solución del ejercicio 11 fun(1,2)(3).
Página 190
Solución del ejercicio 12 11.
Solución del ejercicio 13 Sí.
Solución del ejercicio 14
20.
hello
hello
Solución del ejercicio 15 Una posible solución sería:
1 def numeroDigitos(x:Int):Int = {
2 require(x>0)
3 x match{
4 case x if (x<10) => 1
5 case other => 1 + numeroDigitos(other / 10)
6 }
7 }
Solución del ejercicio 16 Una posible solución sería:
1 def aprueba(calificaciones: List[Int]): List[Int] =
2 calificaciones.map(x=> if (x<5) 5 else x)
Solución del ejercicio 17 Una posible solución podría ser:
1 def eliminaBajos(xs: List[Int],cota: Int): List[Int] =
2 xs.filter(valor => valor >= cota)
Solución del ejercicio 18 Una posible solución sería:
1 def invierteDigitos(x:Int):Int ={
2 require(x>=0)
3 @annotation.tailrec
4 def invierte(digito:Int,acum:Int):Int={
5 digito match{
6 case digito if (digito<10) => acum+digito
7 case other => invierte((other / 10),((acum+digito % 10)*10))
8 }
9
10 }
11 invierte(x,0)
12
13 }
Página 191
Solución del ejercicio 19 Una posible solución sería:
1 def esCapicua(x:Int):Boolean = x==invierteDigitos(x)
Solución del ejercicio 20 Una posible solución sería:
1 def fibonacci(x:Int):Int={
2 @annotation.tailrec
3 def go(fib2:Int,fib1:Int,y:Int):Int={
4 if (x==y) fib1
5 else go(fib1,fib2+fib1,y+1)
6 }
7 if (x==0) 0
8 else go(0,1,1)
9 }
Solución del ejercicio 21 Una posible solución sería:
1 type TotalSegundos=Int
2 type Horas =Int
3 type Minutos = Int
4 type Segundos = Int
5
6 def descomponer(seg:TotalSegundos):(Horas,Minutos,Segundos)={
7 val horas = seg / 3600
8 val resto = seg % 3600
9 val minutos = resto / 60
10 val segundos = resto % 60
11 (horas,minutos,segundos)
12 }
Solución del ejercicio 22 Una posible solución sería:
1 def mcd (x: Int)(y:Int): Int = {
2 if (y==0) x else mcd (y) (x % y)
3 }
4 def coprimos(x:Int, y:Int):Boolean ={
5 mcd(x)(y)== 1
6 }
Solución del ejercicio 23 Una posible solución sería:
1 def pascal(c: Int, r: Int): Int = {
2 require(c>=0 & r>=0 & r>=c)
3
4 if (c == 0 || c == r) 1
5 else pascal(c - 1, r - 1) + pascal(c, r - 1)
6 }
Página 192
Solución del ejercicio 24
Una posible implementación de la función balanceado si el parámetro es una cadena
podría ser:
1 def balanceado(str:String):Boolean =(str count ( p => p == ’(’
)) == (str count (p => p==’)’))
Si el parámetro es del tipo List[Char]:
1 def balanceado(str:List[Char]):Boolean ={
2 @annotation.tailrec
3 def cuenta(carac:Char,cadena:List[Char],acc:Int):Int = cadena
match {
4 case Nil => acc
5 case a::xs if (a!=carac) => cuenta(carac,xs,acc)
6 case a::xs => cuenta(carac,xs,acc+1)
7 }
8 cuenta(’(’,str,0) == cuenta (’)’,str,0)
9 }
8.3. Estructuras de datos
8.3.1. TAD Lista
Solución del ejercicio 25
1 def last[A](xs:Lista[A]):A=xs derecho
Solución del ejercicio 26
1 def init[A](xs:Lista[A]):Lista[A]=xs elim_der
Solución del ejercicio 27
1 def take[A](xs:Lista[A])(x:Int):Lista[A]= x match{
2 case 0 => Nil
3 case _ => if (xs esVacia) Nil else (xs cabeza) :: take(xs
cola)(x-1)
4 }
Solución del ejercicio 28
Página 193
1 def splitAt[A] (xs:Lista[A]) (x:Int): (Lista[A],Lista[A])
=(take(xs)(x),drop(xs)(Nat(x)))
Solución del ejercicio 29
1 def zipWith[A,B,C] (xs:Lista[A]) (ys:Lista[B])
(f:A=>B=>C):Lista[C] = {
2 @annotation.tailrec
3 def go(xs:Lista[A],ys:Lista[B],zs:Lista[C]):Lista[C]= xs
match {
4 case Nil => zs
5 case otro => ys match{
6 case Nil => zs
7 case other => go(xs.cola,ys.cola,zs ## f(xs cabeza)(ys
cabeza))
8 }
9 }
10 go(xs,ys,Nil)
11 }
Solución del ejercicio 30
1 def zip[A,B](xs:Lista[A])(ys:Lista[B]):Lista[(A,B)]=zipWith
(xs) (ys) (x=>y=>(x,y))
Solución del ejercicio 31
1 /** Definicion de unzip sin considerar la implementacion de
lista */
2 def unzip[A,B](xs:Lista[(A,B)]):(Lista[A],Lista[B])={
3 def go(lista:Lista[(A,B)], res1:Lista[A], res2:Lista[B]):
(Lista[A],Lista[B])= lista match{
4 case Nil => (res1,res2)
5 case other=>go(lista.cola,res1 ## other.cabeza._1,res2 ##
other.cabeza._2)
6 }
7 go(xs,Nil,Nil)
8 }
9 /** Definicion de unzip teniendo en cuenta la implementacion
de lista */
10 def unzip1[A,B](xs:Lista[(A,B)]):(Lista[A],Lista[B])={
11 def go(lista:Lista[(A,B)], res1:Lista[A], res2:Lista[B]):
(Lista[A],Lista[B])= lista match{
12 case Nil => (res1,res2)
13 case Cons((a,b),xs)=>go(lista.cola,res1 ## a,res2 ## b)
14 }
15 go(xs,Nil,Nil)
16 }
Página 194
Solución del ejercicio 32
1 def map[A,B](xs:Lista[A])(f:A=>B):Lista[B]= xs match {
2 case Nil => Nil
3 case Cons(x,xss)=>f(x) :: map(xss)(f)
4 }
Solución del ejercicio 33
1 def filter[A](xs:Lista[A])(f:A=>Boolean):Lista[A]={
2 @annotation.tailrec
3 def go(xs:Lista[A],res:Lista[A],f:A=>Boolean):Lista[A]= xs
match{
4 case Nil => res
5 case Cons(el,xss)=>if (f(el)) go(xss,res ## el,f) else
go(xss,res,f)
6 }
7 go(xs,Nil,f)
8 }
Solución del ejercicio 34
1 def takeWhile[A](xs:Lista[A])(f:A=>Boolean):Lista[A]={
2 @annotation.tailrec
3 def go[A](xs:Lista[A],res:Lista[A],f:A=>Boolean):Lista[A]=
xs match{
4 case Nil => res
5 case other => if (f(other cabeza)) go(other.cola, res ##
(other cabeza),f) else res
6 }
7 go(xs,Nil,f)
8 }
Solución del ejercicio 35
1 def dropWhile[A](xs:Lista[A])(f:A=>Boolean):Lista[A]= xs match{
2 case Nil => Nil
3 case Cons(el,xss)=>if(f(el)) dropWhile(xss)(f) else xs
4 }
Solución del ejercicio 36
1 def foldr[A,B](xs:Lista[A])(base:B)(f:A=>B=>B):B=xs match{
2 case Nil => base
3 case Cons(ele,xss)=>f(ele)(foldr(xss)(base)(f))
4 }
Página 195
Solución del ejercicio 37
1 @annotation.tailrec
2 def foldl[A,B](xs:Lista[A])(base:B)(f:B=>A=>B):B=xs match{
3 case Nil => base
4 case Cons(ele,xss)=>foldl(xss)(f(base)(ele))(f)
5 }
8.3.2. TAD Arbol
Solución del ejercicio 38 Una posible solución podría ser:
1 def listaToArbol[T < % Ordered[T]](ls: List[T]) : ArbolBB[T] = {
2 ls.reverse match {
3 case Nil => Vacio
4 case head :: xs => listaToArbol(xs.reverse).add(head)
5 }
6 }
Hacer la inversa de las listas servirá para que los nodos se introduzcan en el mismo orden en el
que aparecen en la lista.
Solución del ejercicio 39 Una posible solución podría ser:
1 def isomorfismo[U >: T](A2: ArbolBB[U]) : Boolean = this match {
2 case Vacio => A2 == Vacio
3 case Nodo(raiz,izq,der) => A2 match {
4 case Nodo(ov,a2izq,a2der) => izq.isomorfismo(a2izq) &&
der.isomorfismo(a2der)
5 case _ => false
6 }
7 }
Esta función irá dentro del trait ArbolBB.
Solución del ejercicio 40 Una posible solución podría ser:
1 def esSimetrico : Boolean = this match {
2 case Vacio => true
3 case Nodo(raiz ,izq,der) => izq.isomorfismo(der)
4 }
Esta función irá dentro del trait ArbolBB.
Página 196
8.4. Colecciones
8.4.1. Tipo List
Solución del ejercicio 41 List(1,2,3,4,5).
Solución del ejercicio 42 List(4, 3, 2, 1).
Solución del ejercicio 43 8.
Solución del ejercicio 44 Una posible solución podría ser:
1 def miMax(xs:List[Int]) : Int = xs.foldLeft(0)( (x,y)=> if (x>y) x
else y)
Solución del ejercicio 45 Una posible solución sería:
1 def suma(xs:List[Int]):Int=xs.foldRight(0)(_ + _)
2 def producto(xs:List[Int]):Int=xs.foldRight(1)(_ * _)
Solución del ejercicio 46 Una posible solución sería:
1 def diferencias(xs:List[Int]):List[Int] = xs match {
2 case Nil => Nil
3 case x :: Nil => Nil
4 case x :: y :: rest => (y-x) :: diferencias(y :: rest)
5 }
Solución del ejercicio 47 Una posible solución sería:
1 def aplana[T] (xs:List[List[T]]):List[T] = xs.fold(Nil)(_ ++ _)
No si usa Nil como caso base. Si se emplea List[T]() como caso base sí se podría utilizar
foldLeft y foldRight:
1 def aplana[T] (xs:List[List[T]]):List[T] =
xs.foldLeft(List[T]()) (_ ++ _)
Si se usa Nil como caso base no se podrían utilizar foldRight y foldLeft, ya que son dos
funciones parametrizadas. Por tanto, al definir el valor del caso base se inferirá el tipo del
mismo (que a su vez debe de ser el tipo devuelto y el tipo del segundo parámetro de la
operación a realizar por foldRight), lo cual daría un error ya que el tipo de los elementos
de xs es List[T]. La función fold define una cota superior1
para su parámetro en relación
al parámetro T del tipo List[T], por lo que el tipo del caso base (Nil) se inferirá List[T].
1
Como se vio en la Subsección 2.5.1: Acotación de tipos y varianza « página 47 ».
Página 197
Solución del ejercicio 48 Una posible solución sería:
1 def aEntero(l:List[Int]):Int = {
2 def pegaDigitos(x:Int,y:Int)={
3 def pega(x:Int,y:Int,cont:Int):Int={
4 cont match{
5 case 0 => x + y
6 case other => pega(x*10,y,cont-1)
7 }
8 }
9 pega(x,y,numeroDigitos(y))
10 }
11 def aEnt(lis:List[Int],acum:Int):Int={
12 lis match{
13
14 case (x::Nil) => pegaDigitos(acum,x)
15 case (y::t)=>aEnt(t,pegaDigitos(acum,y))
16 case other => 0 //Situacion error
17 }
18 }
19 aEnt(l,0)
20 }
Solución del ejercicio 49 Una posible solución sería:
1 def aLista(x:Int):List[Int]= {
2 def aList(x:Int):List[Int]={
3 x match{
4 case x if x<10 => x::Nil
5 case other => other % 10 :: aList(other/10)
6
7 }
8
9 }
10 aList(invierteDigitos(x))
11 }
Solución del ejercicio 50 Una posible solución sería:
1 def miLength[A](xs:List[A]):Int= xs match {
2 case Nil => 0
3 case h::t => 1 + miLength(t)
4 }
Solución del ejercicio 51 Una posible solución sería:
Página 198
1 def penultimo(xs: List[Int]) : Int = xs match {
2 | case x :: y :: Nil => x
3 | case x :: y :: rest => penultimo(y :: rest)
4 | case _ => throw new Error("No existe penultimo")
5 | }
Solución del ejercicio 52 Una posible solución sería:
1 def esPalindromo[T](list: List[T]) : Boolean = {
2 list match {
3 case Nil => true
4 case head :: Nil => true
5 case _ =>
6 (list.head == list.last) &&
7 esPalindromo(list.tail.reverse.tail.reverse)
8 }
9 }
Solución del ejercicio 53 Una posible solución podría basarse en la definición vista del
método apply en la clase List:
1 def enesimo[A](n:Int,xs:List[A]):A = xs(n)
Si se tienen en cuenta las precondiciones de la función, n ≥ 0, sería posible definir la función:
1 def enesimo[A](n:Int,xs:List[A]):A = {require(n>=0); xs(n)}
Solución del ejercicio 54 Una posible solución podría ser:
1 def eliminaDuplicados[A](xs:List[A]):List[A]={
2 xs match {
3 case Nil => Nil
4 case x :: Nil => xs
5 case x :: y :: rest if (x==y) =>
6 eliminaDuplicados(y::rest)
7 case x :: y :: rest =>
8 x :: eliminaDuplicados(y::rest)
9 }
10 }
Aunque también se podría haber optado por utilizar plegado de listas:
1 def eliminaDuplicados[A](xs:List[A]):List[A] = xs.foldLeft
(List[A]())
2 ( (acc:List[A],elem:A) => acc match {
3 case Nil=> List(elem);
4 case _ => if (acc.head != elem) elem :: acc else acc
5 }).reverse
Página 199
Solución del ejercicio 55
1 def duplica[A](xs:List[A]):List[A]= xs flatMap (x=>List(x,x))
Solución del ejercicio 56
1 def repiteN[A](n:Int,xs:List[A]):List[A] = xs flatMap (x => (1
to n) map (_ => x))
Solución del ejercicio 57
1 def agrupaDuplicados[A](xs:List[A]):List[List[A]] = {
2 xs match {
3 case Nil => List(Nil)
4 case elem :: Nil => List(List(elem))
5 case x :: y :: rest if x != y =>
6 List(List(x)) ::: agrupaDuplicados(y :: rest)
7 case x :: y :: rest => // x e y son iguales
8 agrupaDuplicados(rest) match {
9 case List(Nil) => List(List(x,y))//No habia mas elementos
en xs
10 case restList :: restXs if (restList.head == x) => //Habia
mas elementos
11 //iguales a x e y
12 List(List(x,y) ::: restList) ::: restXs
13 case rest => List(List(x,y)) ::: rest //Habia mas elementos
en xs pero
14 //distintos a x e y
15 }
16 }
17 }
8.4.2. Otras colecciones
Solución del ejercicio 58 Una posible solución al ejercicio propuesto podría ser:
1 def construirMap[A,B](datos: Seq[A], f: A=>B): Map[B,A] = {
2 @annotation.tailrec
3 def go (datos: Seq[A], f: A=>B, acc: Map[B,A]): Map[B,A] =
4 datos match {
5 case Nil => acc
6 case _ => go(datos.tail,f,acc + (f(datos.head)->datos.head))
7 }
8 go(datos,f,Map())
9 }
Página 200
Solución del ejercicio 59 Una posible solución al problema planteado podría utilizar con-
juntos:
1 def palabrasDistintas(str:String):Set[String] =
2 str.split(" +").map(_.filter(_.isLetter).toLowerCase).filter(_ !=
"").toSet
Solución del ejercicio 60 La agenda de teléfonos es un buen ejemplo para utilizar Maps, en
el que la clave puede ser el nombre y el valor los datos del contacto.
Una posible solución podría ser:
1 case class Persona(nombre:String,
2 apellidos:String,
3 tfno:String,
4 direccion:String,
5 email:String)
6 case class Agenda(agenda:Map[String,Persona]){
7 def add(contacto:Persona):Agenda = Agenda(agenda +
(contacto.nombre -> contacto))
8 }
Se define la función contactos:
1 case class Agenda(agenda:Map[String,Persona]){
2 def add(contacto:Persona):Agenda = Agenda(agenda +
(contacto.nombre -> contacto))
3 def contactos():List[String] = agenda.keys.toList
4 }
Se define la función telefonos:
1 case class Agenda(agenda:Map[String,Persona]){
2 def add(contacto:Persona):Agenda = Agenda(agenda +
(contacto.nombre -> contacto))
3 def contactos():List[String] = agenda.keys.toList
4 def telefonos():List[(String,String)]=
contactos.zip(agenda.values.map { persona => persona.tfno
})
5 }
Una posible solución para que la clase Agenda pueda guardar más de un contacto con el
mismo nombre y seguir utilizando el nombre como clave podría ser:
Página 201
1 case class Agenda(agenda:Map[String,List[Persona]]){
2 def contactos():List[Persona] = {
3 (for ((contacto,listaContacto) <- agenda;
4 persona <- listaContacto) yield persona).toList;
5 }
6
7 def add(contacto:Persona):Agenda = Agenda({
8 if (agenda.contains(contacto.nombre))
9 agenda +
(contacto.nombre->agenda(contacto.nombre).::(contacto))
10 else agenda + (contacto.nombre -> List(contacto))})
11
12 def telefonos():List[(Persona,String)] =
contactos.zip(agenda.values.flatMap( listaPerson =>
listaPerson.map(persona=> persona.tfno)))
13 }
Además de los cambios realizados en la clase Agenda también se ha sobreescrito el méto-
do toString de la clase Persona para que muestre el nombre y el apellido de cada contacto:
1 case class Persona(nombre:String,
2 apellidos:String,
3 tfno:String,
4 direccion:String,
5 email:String){
6 override def toString():String = nombre +" " + apellidos
7 }
Se podría sobrecargar el método contactos, con la implementación de la función que
devuelva la lista vacía si no hay ningún contacto en la agenda que se corresponda con
el nombre pasado como parámetro o una lista con todas los contactos almacenados en la
agenda que tengan ese nombre.
1 def contactos(nombre:String):List[Persona]=
agenda.getOrElse(nombre, List())
Una posible implementación de la función vecinos de una dirección dada podría ser:
1 def vecinos(place:String):List[Persona]={
2 contactos().filter { x => x.direccion==place}
3 }
Una posible implementación de la función vecinos sería:
1 def vecinos():Map[String,List[Persona]]=contactos().flatMap({
2 x => Map(x.direccion->vecinos(x.direccion))}).toSet.toMap
La función vecinos() mejorada podría ser:
Página 202
1 def vecinos():Map[String,List[Persona]]=contactos().flatMap({
2 x => Map(x.direccion->vecinos(x.direccion))}).toSet.toMap
Solución del ejercicio 61 Una posible solución al problema planteado podría utilizar con-
juntos:
1 def numeroPalabrasDistintas(str:String):Map[String,Int] = {
2 val listaPalabras = str.split("
").map(_.filter(_.isLetter).toLowerCase).filter( _ != ""
).toList
3 listaPalabras.zip( (0 until listaPalabras.size).map(x =>
listaPalabras.count( _ == listaPalabras(x)))).toSet.toMap
4 }
Algoritmo 8.1: Función numeroPalabrasDistintas utilizando zip y map
A continuación se verá la solución aportada con más detalle:
En primer lugar, se observa en la línea 2 del algoritmo 8.1 cómo se ha asignado a la
variable listaPalabras el valor resultante de crear la lista de palabras existentes en str.
El resultado de la evaluación de la expresión escrita en la línea 3 del algoritmo 8.1 va a
ser analizado con más detalle:
1. Se ha iterado sobre cada palabra (listaPalabras(x)) utilizando el método map de la
colección Rango, obteniendo un Vector con las veces que se repite cada palabra con
resultado de la evaluación de la expresión:
1 (0 until listaPalabras.size).map(x=>
listaPalabras.count(_ == listaPalabras(x)))
2. Posteriormente, haciendo uso del método zip de List, hemos creado un vector con
las tuplas formadas por listaPalabras y el vector resultante de la anterior acción.
1 listaPalabras.zip((0 until
listaPalabras.size).map(x=> listaPalabras.count(_
== listaPalabras(x))))
3. Como las tuplas pueden encontrarse repetidas, tanto en listaPalabras como en el
vector con la cuenta de la repetición de cada palabra, se eliminan las tuplas repetidas
transformando el anterior resultado en un conjunto haciendo uso del método toSet
de List.
4. Finalmente se transforma el anterior resultado en un Map utilizando el método to-
Map de Set.
Otra posible solución podría haber sido:
Página 203
1 def numeroPalabrasDistintas(str:String):Map[String,Int] = {
2 val listaPalabras = str.split("
").map(_.filter(_.isLetter).toLowerCase).filter(_ != "").toList
3 listaPalabras.foldLeft(Map[String,Int]())((m,w) => {
4 if(m.contains(w)) m + (w -> (m(w)+1))
5 else m + (w -> 1)
6 })
Algoritmo 8.2: Función numeroPalabrasDistintas utilizando plegado de Listas
Los literales m y w hacen referencia al Map que actúa como acumulador y a los elementos
de la lista, respectivamente.
Nótese que las expresiones definidas en las líneas 2 y 3 del algoritmo 8.2 podrían unirse,
obteniéndose un código que puede resultar más incómodo de leer y entender:
1 def numeroPalabrasDistintas(str: String): Map[String,Int] =
2 str.split(" ").map(_.filter(_.isLetter).toLowerCase).filter(_ !=
"").toList.foldLeft(Map[String,Int]())((m,w) => {
3 if(m.contains(w)) m + (w -> (m(w)+1))
4 else m + (w -> 1)
5 })
Algoritmo 8.3: Función numeroPalabrasDistintas en una expresión
8.5. Programación Funcional Avanzada
Solución del ejercicio 62 La mónada Maybe es similar a la mónada Option, la cual se vio
que cumplía las reglas de las mónadas en la Subsubsección 4.3.2.1: Reglas que deben satisfacer
las mónadas « página 139 » y cuyo comportamiento se vio en la Subsección 4.4.2: Tipo de datos
Option « página 150 ». Por tanto, en primer lugar se definirá la clase Maybe:
1 sealed trait Maybe[+A] {
2
3 def flatMap[B](f: A => Maybe[B]): Maybe[B]
4 def map[B](f:A=>B):Maybe[B]
5 }
6
7 case class Just[+A](a: A) extends Maybe[A] {
8 override def flatMap[B](f: A => Maybe[B]) = f(a)
9 override def map[B](f:A=>B):Maybe[B]=Just(f(a))
10 }
11
12 case object MaybeNot extends Maybe[Nothing] {
13 override def flatMap[B](f: Nothing => Maybe[B]) = MaybeNot
14 override def map[B](f:Nothing=>B):Maybe[B]=MaybeNot
15 }
También se podría haber aprovechado la definición del rasgo Monada realizado en la Sub-
subsección 4.3.2.3: Map en las mónadas « página 141 » para crear la mónada Maybe:
Página 204
1 object MonadaMaybe extends Monada[Maybe]{
2 def unit[A](x:A):Maybe[A]=Just(x)
3 def flatMap[A,B] (ma:Maybe[A]) (f:A=> Maybe[B]):Maybe[B]= ma
flatMap f
4 }
En realidad, se puede considerar la clase Maybe como una clase monádica cuyo método unit
es el método de fábrica apply del objeto acompañante que se crea automáticamente junto con
las case class y el método flatMap también se encuentra definido.
Solución del ejercicio 63
1. Una posible solución basada en la función flatMap:
1 def abueloMaterno(p: Persona): Maybe[Persona] =
2 p.madre flatMap { _.padre }
También se podría haber definido la función abueloMaterno sin utilizar flatMap:
1 def abueloMaternoMatch(p: Persona): Maybe[Persona] =
2 p.madre match {
3 case Just(m) => m.padre
4 case MaybeNot => MaybeNot
5 }
2. Se usará en la solución la función flatMap:
1 def abuelaMaterna(p: Persona): Maybe[Persona] =
2 p.madre flatMap { _.madre }
3. Para resolver el problema se utilizará la función flatMap:
1 def abueloPaterno(p: Persona): Maybe[Persona] =
2 p.padre flatMap { _.padre }
4. Una posible solución usando flatMap podría ser:
1 def abuelaPaterna(p: Persona): Maybe[Persona] =
2 p.padre flatMap { _.madre }
Solución del ejercicio 64 Una posible solución al ejercicio, utilizando flatMap, podría ser:
Página 205
1 def abuelos(p: Person): Maybe[(Persona, Persona)] =
2 p.madre flatMap { m =>
3 m.padre flatMap { fm =>
4 p.padre flatMap { f =>
5 f.padre flatMap { ff =>
6 Just(fm, ff)
7 }
8 }
9 }
10 }
Solución del ejercicio 65 Una posible solución al ejercicio, utilizando bucles for, podría ser:
1 def abuelosFor(p: Persona): Maybe[(Persona, Persona)] =
2 for(
3 m <- p.madre;
4 fm <- m.padre;
5 f <- p.padre;
6 ff <- f.padre)
7 yield (fm, ff)
Solución del ejercicio 66
1 def divSec(dividendo: Double, divisor: Double):Maybe[Double] =
divisor match {
2 case 0 => MaybeNot
3 case _ => Just(dividendo/divisor)
4 }
Solución del ejercicio 67 La función sqrtSec podría implementarse:
1 def sqrtSec(num: Double): Maybe[Double] = num match {
2 case n if n<0 => MaybeNot
3 case _ =>Just(scala.math.sqrt(d))
4 }
Solución del ejercicio 68 Una posible solución al problema sería:
1 def sqrtDivSec(div:Double,dsor:Double):Maybe[Double] =
2 divSec(div,dsor) flatMap (x=> sqrtSec(x))
8.6. Tests en Scala
Solución del ejercicio 69 Se definirá la clase Test para realizar las comprobaciones:
Página 206
1 import org.scalatest.FunSuite
2
3 class Test extends FunSuite{
4 val a:Int=5
5 val b:Int=7
6 val c:Int=10
7 test("Propiedad conmutativa numeros enteros"){
8 assert (a+b==b+a)
9 }
Solución del ejercicio 70 Se agregará la siguiente función a nuestra clase Test:
1 test("Propiedad distributiva de la suma con respecto al producto de
enteros"){
2 assert(a*(b+c)==a*b+a*c)
3 }
Solución del ejercicio 71
A continuación se definirá una nueva clase Test para realizar las comprobaciones:
1 import org.scalatest.FunSuite
2
3 class Test extends FunSuite {
4 test("Regla izquierda de Unit"){
5 Persona.personas foreach { p =>
6
7 // Regla izquierda Unit
8
9 assert((Just(p) flatMap { _.madre }) == p.madre)
10 }
11 }
12 val maybes = MaybeNot +: (Persona.personas map { Just(_) })
13 // Regla derecha Unit
14 test("Regla derecha de Unit"){
15
16
17 maybes foreach { m =>
18 assert((m flatMap { Just(_) }) == m)
19 }
20 }
21 // Asociatividad
22 test("Asociatividad"){
23 maybes foreach { m =>
24 assert(
25 (m flatMap { _.madre } flatMap { _.padre }) ==
26 (m flatMap { _.madre flatMap { _.padre } }))
27 }
28 }
29 }
Página 207
Solución del ejercicio 72 Se añadirá una nueva función a la suite:
1 test("Mismos abuelos con metodo abuelos y abuelosFor"){
2 Persona.personas foreach { p =>
3 assert(Persona.abuelos(p)==Persona.abuelosFor(p))
4 }
5 }
Solución del ejercicio 73 Para la solución de este ejercicio se añadirá una nueva función a la
suite:
1 test("Mismo abuelo materno con metodo abueloMaterno y
abueloMaternoMatch"){
2 Persona.personas foreach { p =>
3 assert(Persona.abuelos(p)==Persona.abuelosFor(p))
4 }
5 }
8.7. Concurrencia
Solución del ejercicio 74 Una posible solución podría ser:
Página 208
1 package pinpong
2
3 import akka.actor.{Actor, ActorLogging, Props}
4
5 class PingActor extends Actor with ActorLogging {
6 import PingActor._
7
8 var counter = 0
9 val pongActor = context.actorOf(PongActor.props, "pongActor")
10
11 def receive = {
12 case Iniciar =>
13 log.info("In PingActor - starting ping-pong")
14 pongActor ! PingMessage("ping")
15 case PongActor.PongMessage(text) =>
16 log.info("En PingActor - mensaje recibido: {}", text)
17 counter += 1
18 if (counter == 3) context.system.shutdown()
19 else sender() ! PingMessage("ping")
20 }
21 }
22
23 object PingActor {
24 val props = Props[PingActor]
25 case object Iniciar
26 case class PingMessage(text: String)
27 }
Algoritmo 8.4: Juego Ping-Pong. Actor Ping
1 package pinpong
2
3 import akka.actor.{Actor, ActorLogging, Props}
4
5 class PongActor extends Actor with ActorLogging {
6 import PongActor._
7
8 def receive = {
9 case PingActor.PingMessage(text) =>
10 log.info("En PongActor - mensaje recibido: {}", text)
11 sender() ! PongMessage("pong")
12 }
13 }
14
15 object PongActor {
16 val props = Props[PongActor]
17 case class PongMessage(text: String)
18 }
Algoritmo 8.5: Juego Ping-Pong. Actor Pong
Página 209
1 package pinpong
2
3 import akka.actor.ActorSystem
4
5 object ApplicationMain extends App {
6 val system = ActorSystem("MyActorSystem")
7 val pingActor = system.actorOf(PingActor.props, "pingActor")
8 pingActor ! PingActor.Iniciar
9
10 system.awaitTermination()
11 }
Algoritmo 8.6: Juego Ping-Pong. Main
Página 210
Lista de Tablas
1.1. Tipos de datos primitivos y tamaño en Scala . . . . . . . . . . . . . . . . . . . 6
1.2. Caracteres de escape reconocidos por Char y String . . . . . . . . . . . . . . . 9
1.3. Prioridad y asociatividad de los operadores . . . . . . . . . . . . . . . . . . . 11
1.4. Operadores aritméticos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.5. Operadores relacionales en Scala . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.6. Operadores lógicos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.7. Tabla de verdad de los operadores bit a bit &, | y ˆ . . . . . . . . . . . . . . . . 14
1.8. Operadores bit a bit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.9. Operadores de asignación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.10. Reglas de reducción para expresiones booleanas . . . . . . . . . . . . . . . . . 20
1.11. Tipos de bucles en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.1. Métodos del tipo Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
3.2. Métodos del tipo List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
3.3. Métodos del tipo Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
3.4. Métodos del tipo Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
3.5. Secuencias. Eficiencia de las operaciones. . . . . . . . . . . . . . . . . . . . . 122
3.6. Set y Map. Eficiencia de las operaciones . . . . . . . . . . . . . . . . . . . . . 122
4.1. Tipos existenciales en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
5.1. Suites del framework ScalaTest . . . . . . . . . . . . . . . . . . . . . . . . . . 160
6.1. Sistema Reactivo Vs Sistema Transformacional . . . . . . . . . . . . . . . . . 165
6.2. Receive Vs React . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
Página 211
Página 212
Lista de Algoritmos
1.1. Hola Mundo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2. Función con dos parámetros. El primero es evaluado por valor y el segundo por
nombre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.3. Sintaxis sentencia if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.4. Expresión condicional if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.5. Sintaxis sentencia if / else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.6. Expresiones condicionales. Función valor absoluto . . . . . . . . . . . . . . . 22
1.7. Sintaxis sentencia if / else if / else . . . . . . . . . . . . . . . . . . . . . . . . 23
1.8. Sentencias if anidadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.9. Sintaxis bucles while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.10. Ejemplo de bucle while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.11. Sintaxis bucles do... while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.12. Ejemplo de bucle do... while. . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.13. Sintaxis bucles for con rangos. . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.14. Ejemplo de bucle for con rangos. . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.15. Sintaxis bucles for con colecciones . . . . . . . . . . . . . . . . . . . . . . . . 26
1.16. Ejemplo bucle for con colecciones. . . . . . . . . . . . . . . . . . . . . . . . . 27
1.17. Sintaxis bucles for con filtros. . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.18. Ejemplo bucle for con filtros . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.19. Sintaxis bucles for con yield. . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.20. Ejemplo bucle for con yield . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.21. Fecha actual formateada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.1. Mi primera clase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.2. Método suma que no compila . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.3. MiPrimeraClase con método suma . . . . . . . . . . . . . . . . . . . . . . . . 34
2.4. Números racionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.5. Cálculo del máximo común divisor de dos enteros . . . . . . . . . . . . . . . . 37
2.6. Clase Abstracta Animal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.7. Clases Perro y Pajaro de tipo Animal . . . . . . . . . . . . . . . . . . . . . . . 39
2.8. Lista de Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.9. Lista de Booleanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.10. Lista Genérica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.11. Lista Genérica con funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.12. Ejemplo de función parametrizada . . . . . . . . . . . . . . . . . . . . . . . . 47
2.13. Función sonTodosPositivos para ListaInt . . . . . . . . . . . . . . . . . . . . . 48
2.14. Función sonTodosPositivos para ListaInt con tipo acotado superiormente . . . . 48
2.15. Función sonTodosPositivos para ListaInt con tipo acotado inferiormente . . . . 48
2.16. Trait Function1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.17. Tipos de funciones A y B . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Página 213
2.18. Trait Function1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.19. Lista genérica de enteros, covariante y con Nil como objeto . . . . . . . . . . . 51
3.1. Cálculo de la raíz cuadrada de un número por el método de Newton . . . . . . 56
3.2. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.3. Recursión Indirecta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.4. Función estricta valor absoluto . . . . . . . . . . . . . . . . . . . . . . . . . . 76
3.5. Ejemplo de función no estricta . . . . . . . . . . . . . . . . . . . . . . . . . . 76
3.6. Ejemplo de función no estricta con estrategia Call By Need . . . . . . . . . . . 77
3.7. Selectiva if como función no estricta . . . . . . . . . . . . . . . . . . . . . . . 77
3.8. Implementación final del tipo Naturales . . . . . . . . . . . . . . . . . . . . . 86
3.9. Implementacion TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
3.10. Object Lista ampliado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.11. Implementación básica de Arboles Binarios . . . . . . . . . . . . . . . . . . . 98
3.12. TAD Arbol Binario de Búsqueda . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.13. Métodos next y hashNext en un bucle while . . . . . . . . . . . . . . . . . . . 105
3.14. Definicion de rangos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
3.15. Acceso a las cotas y valor de paso de los rangos . . . . . . . . . . . . . . . . . 108
3.16. Acceso a las cotas y valor de paso de los rangos . . . . . . . . . . . . . . . . . 108
3.17. Definición de tuplas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
3.18. Acceso a los elementos de una tupla. . . . . . . . . . . . . . . . . . . . . . . . 109
3.19. Iterando sobre los elementos de las tuplas . . . . . . . . . . . . . . . . . . . . 109
3.20. Método swap en tuplas de dos elementos . . . . . . . . . . . . . . . . . . . . . 110
3.21. Definición de Vector. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
3.22. Agregar elementos a un Vector . . . . . . . . . . . . . . . . . . . . . . . . . . 115
3.23. Definición de Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
3.24. Cálculo de la serie de Fibonacci utilizando Stream . . . . . . . . . . . . . . . . 116
3.25. Definición y propiedades fundamentales de los conjuntos . . . . . . . . . . . . 117
3.26. Recorriendo los elementos de un conjunto . . . . . . . . . . . . . . . . . . . . 118
3.27. Definicion de Maps. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.28. Definicion de Maps con tuplas. . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.29. Recuperar valores en Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.30. Agregar y eliminar elementos en un Map . . . . . . . . . . . . . . . . . . . . . 119
3.31. Recorrer los elementos de un Map . . . . . . . . . . . . . . . . . . . . . . . . 120
3.32. Ejemplo traducción bucle for con un generador . . . . . . . . . . . . . . . . . 123
3.33. Ejemplo traducción de expresiones for con un generador y un filtro . . . . . . . 123
3.34. Ejemplo de traducción de expresiones for con dos generadores . . . . . . . . . 123
3.35. Ejemplo traducción bucle for genérico . . . . . . . . . . . . . . . . . . . . . . 124
4.1. Definición de parámetros implícitos . . . . . . . . . . . . . . . . . . . . . . . 129
4.2. Definición de valores implícitos . . . . . . . . . . . . . . . . . . . . . . . . . 130
4.3. Definición de clases implícitas . . . . . . . . . . . . . . . . . . . . . . . . . . 130
4.4. Ejemplo de tipos estructurales. . . . . . . . . . . . . . . . . . . . . . . . . . . 134
4.5. Ejemplo de tipos de orden superior. . . . . . . . . . . . . . . . . . . . . . . . 135
4.6. Función distribuir usando funtores. . . . . . . . . . . . . . . . . . . . . . . . . 138
4.7. Definición básica del trait Mónada. . . . . . . . . . . . . . . . . . . . . . . . . 139
4.8. Monada Identidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
4.9. Juego JailBreak. Definición tipos necesarios. . . . . . . . . . . . . . . . . . . . 144
4.10. Juego JailBreak. Trait Game. . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
4.11. Juego JailBreak. Solución al juego. . . . . . . . . . . . . . . . . . . . . . . . . 144
Página 214
4.12. Juego JailBreak. Definición de la clase JailBreak. . . . . . . . . . . . . . . . . 145
4.13. Juego JailBreak. Trait Game incluyendo Try. . . . . . . . . . . . . . . . . . . . 147
4.14. Juego JailBreak. Trait Game incluyendo Try. . . . . . . . . . . . . . . . . . . . 147
4.15. Juego JailBreak. Solución al juego. . . . . . . . . . . . . . . . . . . . . . . . . 147
4.16. Juego JailBreak. Solución al juego utilizando flatMap. . . . . . . . . . . . . . . 148
4.17. Juego JailBreak. Solución al juego utilizando flatMap 2. . . . . . . . . . . . . . 148
4.18. Juego JailBreak. Solución al juego utilizando expresiones for. . . . . . . . . . . 149
4.19. Excepciones.Función media con excepciones . . . . . . . . . . . . . . . . . . 149
4.20. Excepciones.Función media con argumento para caso especial . . . . . . . . . 149
4.21. Tipo de datos Option . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
4.22. Excepciones. Función media con tipo de datos Option . . . . . . . . . . . . . . 150
4.23. Tipo de datos Either . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
4.24. Excepciones.Función media con tipo de datos Either . . . . . . . . . . . . . . 151
4.25. Excepciones.Division con Either . . . . . . . . . . . . . . . . . . . . . . . . . 151
5.1. Fibonacci sin recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . 156
5.2. Fibonacci sin recursión de cola. Assert que falla . . . . . . . . . . . . . . . . . 156
5.3. Excepcion AssertError . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
5.4. Fibonacci sin recursión de cola. Assert que falla + explicación . . . . . . . . . 156
5.5. Excepcion AssertError con información . . . . . . . . . . . . . . . . . . . . . 157
5.6. Fibonacci sin recursión de cola. Assert que no falla . . . . . . . . . . . . . . . 157
5.7. Fibonacci sin recursión de cola. Ensuring que falla . . . . . . . . . . . . . . . 157
5.8. Ensuring.Excepcion AssertError . . . . . . . . . . . . . . . . . . . . . . . . . 158
5.9. Fibonacci sin recursión de cola. Ensuring que no falla . . . . . . . . . . . . . . 158
5.10. Inversa de una lista. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . 158
5.11. Ensuring en el cálculo de la inversa de una lista . . . . . . . . . . . . . . . . . 158
5.12. ScalaTest. Ejemplo que extiende de org.scalatest.Suite . . . . . . . . . . . . . 161
5.13. ScalaTest. Ejemplo de la suite FunSuite . . . . . . . . . . . . . . . . . . . . . 162
6.1. Mi Primer Actor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
6.2. Procesando primer mensaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
6.3. Mi primer mensaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
6.4. Actor con estado definido con variable mutable . . . . . . . . . . . . . . . . . 172
6.5. Ejecutando Contador con estado mutable . . . . . . . . . . . . . . . . . . . . . 172
6.6. Actor con estado inmutable . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
6.7. Ejecutando Contador con estado inmutable . . . . . . . . . . . . . . . . . . . 173
6.8. Método act con loop y react . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
6.9. Diferencias entre Akka y la librería de Scala. Instanciación en Scala . . . . . . 175
6.10. Diferencias entre Akka y la librería de Scala. Instanciación en Scala . . . . . . 175
6.11. Diferencias entre Akka y la librería de Scala. Instanciación en Scala I . . . . . 176
6.12. Diferencias entre Akka y la librería de Scala. Instanciación en Scala II . . . . . 176
6.13. Diferencias entre Akka y la librería de Scala. Instanciación en Akka I . . . . . 176
6.14. Diferencias entre Akka y la librería de Scala. Instanciación en Akka II . . . . . 176
6.15. Eliminación de act. Ejemplo en Scala . . . . . . . . . . . . . . . . . . . . . . 176
6.16. Eliminación de act. Ejemplo en Akka . . . . . . . . . . . . . . . . . . . . . . 177
6.17. Akka.Trait Actor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
6.18. Akka.Actor con estado definido con variable mutable . . . . . . . . . . . . . . 178
6.19. Akka.Actor con estado inmutable . . . . . . . . . . . . . . . . . . . . . . . . . 178
6.20. Akka.Trait Actor y Trait ActorContext . . . . . . . . . . . . . . . . . . . . . . 179
6.21. Akka.Actor con estado inmutable (become) . . . . . . . . . . . . . . . . . . . 179
Página 215
6.22. Akka.Trait Actor y Clase Abstracta ActorRef . . . . . . . . . . . . . . . . . . 180
6.23. Akka.Mensjaes bidireccionales . . . . . . . . . . . . . . . . . . . . . . . . . . 180
6.24. Akka.Trait ActorContext II . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
6.25. Akka.Crear Actores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
8.1. Función numeroPalabrasDistintas utilizando zip y map . . . . . . . . . . . . . 203
8.2. Función numeroPalabrasDistintas utilizando plegado de Listas . . . . . . . . . 204
8.3. Función numeroPalabrasDistintas en una expresión . . . . . . . . . . . . . . . 204
8.4. Juego Ping-Pong. Actor Ping . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
8.5. Juego Ping-Pong. Actor Pong . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
8.6. Juego Ping-Pong. Main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Página 216
Bibliografía
[1] Growing a Language, Burlington, Massachusetts, Octubre 1998. 2
[2] Akka Scala Documentation. 2.3.12 edition, julio 2015. 175, 177
[3] Bagwell. Learning scala. http://www.scala-lang.org/node/1305, septiem-
bre 2012. [Online; accedido 15-Abril-2013].
[4] Richard Bird. Introducción a la Programación funcional con Haskell. Prentice Hall,
segunda edición edition, 2000. http://goo.gl/cGTpJ.[Online; accedido 15-Abril-
2013].
[5] Paul Chiusano and Rúnar Bjarnason. Functional Programming in Scala. MEAP, 2013.
143
[6] Joshua D. Suereth. Scala in Depth. Mannaging Publications, 2012. 103, 116
[7] Desconocido. Scala (lenguaje de programación). http://es.wikipedia.org/
wiki/Scala_(lenguaje_de_programaci%C3%B3n). [Online; accedido 15-
Abril-2013]. 1
[8] Desconocido. Scala to a haskell programmer. goo.gl/k0Ks4V, septiembre 2012. [On-
line; accedido 15-Abril-2013].
[9] EPFL. Scala api. http://www.scala-lang.org/api. [Online; accedido 15-
Abril-2015]. 110
[10] Xavier Franch Gutiérrez. Estructuras de Datos. Especificación, diseño e implementación.
Ediciones UPC, tercera edition, abril 1999. 89
[11] Daniel Garrido Márquez. Apuntes programación concurrente. 165, 167
[12] Hewitt, Bishop, and Steiger. A Universal Actor Formalism for Artificial Intelligence. IJ-
CAI, 1973. 168, 169
[13] Vojin Jovanovic and Philipp Haller. The scala actors migration gui-
de. http://http://docs.scala-lang.org/overviews/core/
actors-migration-guide.html. [Online; accedido 25-Agosto-2013]. 177
[14] Mark C. Lewis. Introduction to the art of programming using Scala. CRC Press, Agosto
2012. 104
[15] Narciso Martí Oliet, Yolanda Ortega Mallén, and Alberto Verdejo. Estructuras de Datos
y Métodos Algorítmicos. Garceta, segunda edition, 2013. 80, 89
Página 217
[16] Jesper Nordenberg. My Take on Haskell vs Scala. http://jnordenberg.
blogspot.com.es/2012/05/my-take-on-haskell-vs-scala.html,
mayo 2012. [Online; accedido 15-Abril-2013].
[17] M. Odersky. Scala functional programming. Coursera. [Online; accedido 29-Agosto-
2015]. 38
[18] M. Odersky. Scala by Example. PROGRAMMING METHODS LABORATORY, EPFL,
SWITZERLAND, noviembre 2010. http://es.scribd.com/doc/47368246/
Scala-By-Example-Martin-Odersky.
[19] M. Odersky and Roland Kuhn. Scala. principles of reactive programming. Coursera.
[Online; accedido 29-Agosto-2015]. 143, 181
[20] M. Odersky, L. Spoon, and B. Venners. Programming in Scala: A Comprehensive Step-
by-Step Guide. Artima Inc., segunda edición edition, 2010.
[21] Miguel Pastor. Scala: Primeros pasos. http://miguelinlas3.blogspot.com.
es/2011/01/scala-primeros-pasos.html, enero 2011. [Online; accedido 15-
Abril-2013].
[22] Alex Payne and Dean Wampler. Programming Scala. O’Reilly Media, septiembre 2009.
[23] David Pollak. Beginning Scala. Apress, 2009. 169
[24] Nilanjan Raychaudhuri. Scala in Action. Manning, abril 2010. 53
[25] Blas C. Ruiz, Francisco Gutiérrez, Pablo Guerrero, and José E. Gallardo. Razonando con
Haskell. Universidad de Málaga, segunda edition, abril 2004. 80
[26] ScalaTest. Scalatest user guide. http://es.wikipedia.org/wiki/
Arquitectura_de_von_Neumann. [Online; accedido 30-Agosto-2015]. 3
[27] ScalaTest. Scalatest user guide. http://www.scalatest.org/user_guide.
[Online; accedido 30-Agosto-2015].
[28] Michel Schinz and Philipp Haller. A scala tutorial for java pro-
grammers. http://docs.scala-lang.org/es/tutorials/
scala-for-java-programmers.html, mayo 2011. [Online; accedido 15-
Julio-2013].
[29] Jason Swartz. Learning Scala. Practical Functional Programming for the JVM. O’Really,
segunda edition, Diciembre 2014. 102
[30] Tutorialspoint. Scala tutorial. http://www.tutorialspoint.com/scala/
index.htm. [Online; accedido 10-Septiembre-2015]. 102
[31] Antonio Vallecillo Moreno and R. Guerequeta García. Técnicas de diseño de algoritmos.
Universidad de Málaga, segunda edition, 2000. 78
[32] Wikipedia. Método de newton. https://es.wikipedia.org/wiki/M%C3%
A9todo_de_Newton. [Online; accedido 12-Diciembre-2015]. 56
Página 218
[33] Wikipedia. Programacion funcional. https://es.wikipedia.org/wiki/
Programaci%C3%B3n_funcional. [Online; accedido 29-Agosto-2015].
[34] Wikipedia. Teoría de las categorías. https://es.wikipedia.org/wiki/
Teoria_de_categorias. [Online; accedido 1-Octubre-2015]. 136
Página 219
Página 220
Glosario
.NET
.NET representa la evolución del Component Object Model (COM), la plataforma de
desarrollo de Microsoft anterior a .NET y sobre la cual se basaba el desarrollo de apli-
caciones Visual Basic 6 (entre otros tantos lenguajes y versiones). Es una plataforma de
desarrollo y ejecución de aplicaciones. Esto quiere decir que no sólo nos brinda todas las
herramientas y servicios que se necesitan para desarrollar modernas aplicaciones empre-
sariales y de misión crítica, sino que también nos provee de mecanismos robustos, seguros
y eficientes para asegurar que la ejecución de las mismas sea óptima.
Android
Sistema operativo basado en el núcleo Linux. Fue diseñado principalmente para disposi-
tivos móviles con pantalla táctil, como teléfonos inteligentes o tabletas; y también para
relojes inteligentes, televisores y automóviles.
buffer
Espacio de la memoria en un disco o en un instrumento digital reservado para el almace-
namiento temporal de información, mientras que está esperando ser procesada.
bytecode
El bytecode es un código intermedio más abstracto que el código máquina. Habitualmente
es tratado como un archivo binario que contiene un programa ejecutable similar a un
módulo objeto, que es un archivo binario producido por el compilador cuyo contenido es
el código objeto o código máquina.
evaluación estricta
Estrategia de evaluación que evalúa los argumentos antes de reemplazar la función por la
definición de la misma. También conocida como Call by Value o paso por valor.
evaluación no estricta
Estrategia de evaluación que no evalúa los argumentos antes de reemplazar la función por
la definición de la misma. También conocida como Call by Name o paso por referencia.
evaluación perezosa
Estrategia de evaluación que retrasa el cálculo de una expresión hasta que su valor sea
necesario, y que también evita repetir la evaluación en caso de ser necesaria en posteriores
ocasiones. También conocida como Call by Need o paso por necesidad.
Página 221
Haskell
Lenguaje de programación funcional puro, fuertemente tipado. Su nombre se debe al
lógico estadounidense Haskell Curry.
literal
Notación que representa un valor dentro del lenguaje de programación.
llamada de cola
Llamada a una subrutina que se realiza como la última acción de un procedimiento. Si esta
llamada de cola se realiza a la misma subrutina que desde donde se realiza, la subrutina
se llamará recursiva de cola (que es un caso especial de recursión). .
programación declarativa
La programación declarativa, en contraposición a la programación imperativa es un pa-
radigma de programación que está basado en el desarrollo de programas especificando
o “declarando” un conjunto de condiciones, proposiciones, afirmaciones, restricciones,
ecuaciones o transformaciones que describen el problema y detallan su solución.
programación funcional
Es un tipo de paradigma de programación dentro del paradigma de programación decla-
rativa que se basa en el concepto de función (que no es más que una evolución de los
predicados), de corte más matemático. Los lenguajes funcionales se basan en el cálculo
lambda..
programación imperativa
Es un paradigma de programación que describe la programación en términos del estado
del programa y sentencias que cambian dicho estado. Los programas imperativos son un
conjunto de instrucciones que le indican al computador cómo realizar una tarea.
programación lógica
Es un tipo de paradigma de programación dentro del paradigma de programación decla-
rativa que gira en torno al concepto de predicado, o relación entre elementos. La mayoría
de los lenguajes de programación lógica se basan en la teoría lógica de primer orden,
aunque también incorporan algunos comportamientos de orden superior como la lógica
difusa (aunque ésta no siempre tiene que ser de orden superior).
prueba unitaria
Una prueba unitaria es una forma de comprobar el correcto funcionamiento de un módulo
de código. Esto sirve para asegurar que cada uno de los módulos funcione correctamente
por separado. Luego, con las pruebas de integración, se podrá asegurar el correcto fun-
cionamiento del sistema o subsistema en cuestión.
Scheme
Lenguaje de programación funcional (no puro) interpretado. Es un dialecto de Lisp, muy
expresivo y soporta varios paradigmas. Estuvo influenciado por el cálculo lambda.
Página 222
script
Secuencia de instrucciones almacenadas en un fichero que se ejecutan normalmente de
forma interpretada..
sistema de tipos
Un sistema de tipos define cómo un lenguaje de programación clasifica los valores y las
expresiones en tipos, cómo se pueden manipular estos tipos y cómo interactúan.
unicode
Unicode es un estándar de codificación de caracteres diseñado para facilitar el tratamiento
informático, transmisión y visualización de textos de múltiples lenguajes y disciplinas
técnicas, además de textos clásicos de lenguas muertas. El término Unicode proviene de
los tres objetivos perseguidos: universalidad, uniformidad y unicidad.
vector
Colección de un número determinado de elementos de un mismo tipo de datos ubicados
en una zona de almacenamiento continuo, adecuado para situaciones en las que el acceso
a los datos se realice de forma aleatoria e impredecible.
Página 223
Página 224
Acrónimos
ABB
Árbol binario de búsqueda.
ACM
Asociación para la Maquinaria Computacional.
API
Application Programming Interface.
asociación
maps.
cierre
closure.
clase acompañante
companion class.
COM
Component Object Model.
CPU
unidad central de procesamiento.
EPFL
École Polytechnique Fédérale de Lausanne.
espacio de nombres
namespace.
FIFO
First In First Out – “primero en entrar, primero en salir” –.
flujo de datos
stream.
hilo de ejecución
thread.
Página 225
IDE
Entorno de Desarrollo Integrado.
IMC
Índice de masa corporal.
JVM
Máquina Virtual de Java.
modificaciones apilables
stackable modifications.
método de fábrica
factory method.
objeto acompañante
companion object.
objeto singleton
singleton objects.
POO
programación orientada a objetos.
REPL
Bucle Leer-Evaluar-Imprimir.
TADs
Tipo abstracto de datos.
TDD
Test Driven Development.
Página 226

Más contenido relacionado

PDF
Fundamentos de programacion piensa en c
PDF
ED Unidad 2: Recursividad, ordenamiento y búsqueda de datos
PPTX
Elementos basicos de un programa
PDF
Algoritmos programacion-python
PDF
Guia estudiantes mec mat 2021_2022_esp.docx
PDF
10 Tips para desarrollar tu lógica de programación
PDF
Prueba de Caja Blanca
PDF
Apuntes de c
Fundamentos de programacion piensa en c
ED Unidad 2: Recursividad, ordenamiento y búsqueda de datos
Elementos basicos de un programa
Algoritmos programacion-python
Guia estudiantes mec mat 2021_2022_esp.docx
10 Tips para desarrollar tu lógica de programación
Prueba de Caja Blanca
Apuntes de c

La actualidad más candente (20)

PPTX
4 Introducción al lenguaje Scala
PPTX
PPS
Modelo objeto semántico
PPTX
2 1 vistas arquitectonicas
PDF
Alfabetos-Lenguajes y Automatas 1
PPTX
Funciones de un administrador de base de datos
DOCX
control de concurrencia
PDF
Entidad relacion extendido resumen
PPTX
Topicos Avanzados de Programacion - Unidad 3 componentes y librerias
PPSX
Proyecto de software
PDF
Programación Orientada a Eventos Java
PDF
Diagramas UML: Componentes y despliegue
PDF
Uml clase 04_uml_clases
PPTX
Estrategias de aplicaciones para las pruebas de integración
PPTX
Los tipos de usuarios en una base de datos
PPT
Código intermedio
PPSX
Javascript
PDF
Consultas básicas en sql server
PPTX
Spring boot Introduction
PPTX
Unidad 4 a HERENCIA, CLASES ABSTRACTAS, INTERFACES Y POLIMORFISMO . UML
4 Introducción al lenguaje Scala
Modelo objeto semántico
2 1 vistas arquitectonicas
Alfabetos-Lenguajes y Automatas 1
Funciones de un administrador de base de datos
control de concurrencia
Entidad relacion extendido resumen
Topicos Avanzados de Programacion - Unidad 3 componentes y librerias
Proyecto de software
Programación Orientada a Eventos Java
Diagramas UML: Componentes y despliegue
Uml clase 04_uml_clases
Estrategias de aplicaciones para las pruebas de integración
Los tipos de usuarios en una base de datos
Código intermedio
Javascript
Consultas básicas en sql server
Spring boot Introduction
Unidad 4 a HERENCIA, CLASES ABSTRACTAS, INTERFACES Y POLIMORFISMO . UML
Publicidad

Destacado (15)

PPTX
Introducción a Scala
PPTX
Scala en la Practica
PPTX
Pf con scala
PDF
Curso de Scala: Trabajando con variables
PDF
Scala - just good for Java shops?
PPTX
JavaFX and Scala - Like Milk and Cookies
PDF
Introducción a scala
PDF
Koreference
ODP
Scala+swing
PPTX
JavaFX 2 and Scala - Like Milk and Cookies (33rd Degrees)
PPT
Seven ways to kill your presentation
PPT
Scala
Introducción a Scala
Scala en la Practica
Pf con scala
Curso de Scala: Trabajando con variables
Scala - just good for Java shops?
JavaFX and Scala - Like Milk and Cookies
Introducción a scala
Koreference
Scala+swing
JavaFX 2 and Scala - Like Milk and Cookies (33rd Degrees)
Seven ways to kill your presentation
Scala
Publicidad

Similar a Programación Funcional en Scala (20)

PDF
Scala Overview
PPTX
Introducción a Scala
PDF
Apache spark meetup
PDF
Procesamiento de datos a gran escala con Apache Spark
PDF
Descubriendo scala
PDF
Programacion en Phyton desde ce..........................ro
PDF
Trabajo práctico sobre Clojure, Evaluación de un Lenguaje de Programación
PDF
Iniciación a python
PPTX
Codemotion 2014 Scala @real life
PPTX
Scala @ Real Life Codemotion 2014
PDF
Introduccion a la programacion en c prev
PPTX
Nivel de programacion web
PDF
Scala desde c# y JavaScript
PDF
MANUAL DE LENGUAJE DE PROGRAMACION
PDF
Programación funcional, una nueva forma de resolver problemas.
PDF
Algoritmos
PDF
Introduccion al desarrollo de software
PPTX
Conceptos básicos de un lenguaje de programación
PDF
Apuntes de introduccion a la programación
Scala Overview
Introducción a Scala
Apache spark meetup
Procesamiento de datos a gran escala con Apache Spark
Descubriendo scala
Programacion en Phyton desde ce..........................ro
Trabajo práctico sobre Clojure, Evaluación de un Lenguaje de Programación
Iniciación a python
Codemotion 2014 Scala @real life
Scala @ Real Life Codemotion 2014
Introduccion a la programacion en c prev
Nivel de programacion web
Scala desde c# y JavaScript
MANUAL DE LENGUAJE DE PROGRAMACION
Programación funcional, una nueva forma de resolver problemas.
Algoritmos
Introduccion al desarrollo de software
Conceptos básicos de un lenguaje de programación
Apuntes de introduccion a la programación

Último (20)

PPTX
Introducción al Diseño de Máquinas Metodos.pptx
PDF
fulguracion-medicina-legal-418035-downloable-2634665.pdf lesiones por descarg...
PPTX
MARITIMO Y LESGILACION DEL MACO TRANSPORTE
PPT
tema DISEÑO ORGANIZACIONAL UNIDAD 1 A.ppt
DOC
informacion acerca de la crianza tecnificada de cerdos
PPTX
Presentación - Taller interpretación iso 9001-Solutions consulting learning.pptx
PPT
357161027-seguridad-industrial-diapositivas-ppt.ppt
PDF
Pensamiento Politico Siglo XXI Peru y Mundo.pdf
PDF
prg2_t01_p01_Fundamentos POO - parte1.pdf
PPTX
Manual ISO9001_2015_IATF_16949_2016.pptx
PDF
Curso Introductorio de Cristales Liquidos
PDF
presentacion sobre los polimeros, como se conforman
PPTX
NILS actividad 4 PRESENTACION.pptx pppppp
PPTX
1 CONTAMINACION AMBIENTAL EN EL PLANETA.pptx
PPT
PRIMEROS AUXILIOS EN EL SECTOR EMPRESARIAL
PPT
Sustancias Peligrosas de empresas para su correcto manejo
PPT
TRABAJOS EN ALTURA PARA OBRAS DE INGENIERIA
PPTX
Seminario de telecomunicaciones para ingeniería
PDF
Primera formulación de cargos de la SEC en contra del CEN
PDF
FIJA NUEVO TEXTO DE LA ORDENANZA GENERAL DE LA LEY GENERAL DE URBANISMO Y CON...
Introducción al Diseño de Máquinas Metodos.pptx
fulguracion-medicina-legal-418035-downloable-2634665.pdf lesiones por descarg...
MARITIMO Y LESGILACION DEL MACO TRANSPORTE
tema DISEÑO ORGANIZACIONAL UNIDAD 1 A.ppt
informacion acerca de la crianza tecnificada de cerdos
Presentación - Taller interpretación iso 9001-Solutions consulting learning.pptx
357161027-seguridad-industrial-diapositivas-ppt.ppt
Pensamiento Politico Siglo XXI Peru y Mundo.pdf
prg2_t01_p01_Fundamentos POO - parte1.pdf
Manual ISO9001_2015_IATF_16949_2016.pptx
Curso Introductorio de Cristales Liquidos
presentacion sobre los polimeros, como se conforman
NILS actividad 4 PRESENTACION.pptx pppppp
1 CONTAMINACION AMBIENTAL EN EL PLANETA.pptx
PRIMEROS AUXILIOS EN EL SECTOR EMPRESARIAL
Sustancias Peligrosas de empresas para su correcto manejo
TRABAJOS EN ALTURA PARA OBRAS DE INGENIERIA
Seminario de telecomunicaciones para ingeniería
Primera formulación de cargos de la SEC en contra del CEN
FIJA NUEVO TEXTO DE LA ORDENANZA GENERAL DE LA LEY GENERAL DE URBANISMO Y CON...

Programación Funcional en Scala

  • 3. UNIVERSIDAD DE MÁLAGA ESCUELA TÉCNICA SUPERIOR DE INGENIERÍA INFORMÁTICA INGENIERO EN INFORMÁTICA PROGRAMACIÓN FUNCIONAL EN SCALA (FUNCTIONAL PROGRAMMING IN SCALA) Realizado por RUBÉN PÉREZ LUJANO Dirigido por JOSÉ ENRIQUE GALLARDO RUIZ Departamento LENGUAJES Y CIENCIAS DE LA COMPUTACIÓN MÁLAGA, SEPTIEMBRE 2016
  • 5. Dedicado a la memoria de mi padre
  • 7. Agradecimientos Después de recorrer un largo camino ha llegado el momento de parar, coger aire, mirar hacia atrás un instante y dar las gracias a todas esas personas que en algún momento han formado parte del mismo, que han compartido los mejores momentos, que me han ayudado a mirar hacia delante a pesar de las adversidades y junto a las que me gustaría continuar recorriendo el camino de la vida. Quisiera comenzar dando las gracias a D. José Enrique Gallardo Ruiz, tutor del proyecto, por su comprensión, dedicación y por el gran esfuerzo realizado, así como a D. Blas Carlos Ruiz Jiménez, un gran profesor y la persona con la que comencé mi proyecto. Gracias a Irene, mi mujer, por saber sacarme una sonrisa en esos días grises, por tenderme la mano y ayudar a levantarme en los días más oscuros y por darme ánimos para continuar cuando más duro se hacía el camino. Gracias por hacerme el hombre más feliz del mundo. Gracias por apostar por mí. Tú siempre has sido y serás mi apuesta. Gracias a Carmen, mi madre, por los valores que me ha transmitido, por todo lo que me ha enseñado durante la vida y por creer en mí hasta el final. Gracias por esa canción inolvidable. Gracias a Lidia, mi hermana, por su cariño, su ayuda, sus bromas y por confiar ciegamente en mí. Gracias por todos y cada uno de los momentos que hemos vivido. Gracias a mis titos, Juan y Paqui, por ofrecer siempre todo su apoyo y demostrarme que siempre podré contar con ellos. Gracias a mis amigos Nacho y Jesús, con los que he compartido algunos de los mejores momentos de este camino. Gracias por vuestros consejos, por esas tardes de risas en el salón. Gracias a Dña. Lidia Fuentes por ofrecerme esa beca en el momento que más lo necesitaba y a Dña. Mariam Cobaleda por todos los buenos consejos que me dio. Gracias a todos los miembros de los centros de día para personas mayores de Estepona y Coín por acogerme en vuestra familia y por todos los buenos momentos que compartimos. Para finalizar, aunque no los mencione de una forma explícita, quiero dar las gracias a mis compañeros de universidad y a mis compañeros de piso. A todos, eternamente agradecido.
  • 9. Introducción La elección de un lenguaje para introducir la programación a los alumnos de las actuales ingenierías en informática es una decisión trascendental; esta elección está ligada a la pregunta: ¿qué características se deben exigir a un “primer” lenguaje para describir de forma limpia y sencilla los conceptos de la programación? Hoy en día es comúnmente aceptado entre los profesionales de la enseñanza (y entre los alum- nos) que hay dos paradigmas esenciales que simplifican los conceptos de la programación, y que debe conocer un futuro informático: el funcional y el orientado a objetos. Sin embargo es difícil encontrar un lenguaje que integre ambos paradigmas de forma sencilla si se parte de la base de que tal lenguaje será el primer contacto de un estudiante con la programación. Además de esto, se quiere una buena elección desde el punto de vista del programador profesional; es decir, tal lenguaje debe facilitar de forma “natural” el aprendizaje de los principales lenguajes con los que se enfrentará el futuro informático profesional. Entre la oferta actual de lenguajes hay uno que cada vez toma más adeptos en el mundo edu- cativo: el lenguaje Scala. Scala es un lenguaje de programación multiparadigma diseñado para expresar patrones comunes de programación en forma concisa, elegante y con tipos seguros. Integra sutilmente características de lenguajes funcionales y orientados a objetos. La imple- mentación actual corre en la máquina virtual de Java y es compatible con las aplicaciones Java existentes; por ello el uso de Scala como un primer lenguaje será un puente importante con el mundo de la programación profesional. El trabajo en Scala surge a partir de un esfuerzo de investigación para desarrollar un mejor soporte de los lenguajes de programación para la composición de software. Hay dos hipótesis que se desea validar con el experimento Scala. Primera, se postula que un lenguaje de progra- mación para la composición de software necesita ser escalable en el sentido de que los mismos conceptos pueden describir tanto partes pequeñas como grandes. Por tanto, los autores se han concentrado en los mecanismos para la abstracción, composición y descomposición en vez de añadir un conjunto grande de primitivas que pueden ser útiles para los componentes a algún nivel de escala, pero no a otro nivel. Segundo, se postula, que el soporte escalable para los componentes puede ser previsto por un lenguaje de programación que unifica y generaliza la programación orientada a objetos y la funcional. Para los lenguajes con tipos estáticos, de los que Scala es un ejemplo, estos dos paradigmas estaban hasta ahora en gran medida separados. El principal objetivo de este PFC es desarrollar un material didáctico a modo de una guía donde el programador (tanto el alumno como el profesor) pueda ver de forma clara y conci- sa, tras una primera toma de contacto con el lenguaje, las ventajas de utilizar un lenguaje de programación multiparadigma como Scala en la resolución de los diferentes problemas que se puedan plantear. En el Capítulo 1: Scala « página 1 » se realiza una breve introducción a Scala, se presenta el lenguaje y se analizan conceptos básicos de los lenguajes de programación como los tipos de datos básicos, los operadores, las estructuras de control o la evaluación en Scala. Página I
  • 10. II En el Capítulo 2: Programación Orientada a Objetos en Scala « página 31 » se realiza un análisis de Scala como un lenguaje orientado a objetos puro, se presenta la jerarquía de clases y conceptos como polimorfismo, genericidad, acotación de tipos y varianza en Scala. En las primeras secciones del Capítulo 3: Programación Funcional en Scala « página 53 » se describe el paradigma funcional utilizando Scala y se enseñan conceptos de la programación a través del estilo funcional. Posteriormente, se detalla la implementación en Scala de las es- tructuras básicas de la programación, aprovechando dichas estructuras para el aprendizaje de la programación funcional. Para finalizar el capítulo se hace un repaso por las colecciones que Scala ofrece al programador. Es conocido que es posible unificar los estilos imperativos y orientado a objetos con el fun- cional puro a través de mónadas, pero el uso de éstas no es apropiado para un curso introductorio a la programación. Después de haber estudiado previamente los aspectos fundamentales de la programación funcional, en el Capítulo 4: Programación Funcional Avanzada en Scala « página 129 » se analizan conceptos más complejos de este paradigma, como la programación monádica a través de las mónadas más populares del lenguaje. En el Capítulo 5: Tests en Scala « página 155 » se presentan brevemente algunas de las soluciones más populares para realizar pruebas al código realizado en Scala. Scala propone una solución basada en el paso asíncrono de mensajes inmutables para resol- ver la problemática de la concurrencia. El modelo de actores de Scala, así como la biblioteca Akka, se presentan en el Capítulo 6: Concurrencia en Scala. Modelo de actores « página 165 ». En el Capítulo 7: Conclusiones « página 185 » se justifica razonadamente el uso de Scala como lenguaje de programación adecuado para ser utilizado dentro del ámbito de la docencia. Finalmente, en el Capítulo 8: Solución a los ejercicios propuestos « página 189 » se pueden encontrar las soluciones de los ejercicios propuestos a lo largo de la guía.
  • 11. Índice general 1. Scala 1 1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1.1. Scala. Un lenguaje escalable . . . . . . . . . . . . . . . . . . . . . . . 2 1.1.2. Paradigmas de la programación . . . . . . . . . . . . . . . . . . . . . 2 1.1.2.1. Scala. Un lenguaje multiparadigma . . . . . . . . . . . . . . 3 1.1.3. Preparación del sistema . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1.3.1. Descargar Scala . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1.3.2. Herramientas de Scala . . . . . . . . . . . . . . . . . . . . . 3 El compilador de Scala: scalac . . . . . . . . . . . . . . . . . . 3 El intérprete de código: scala . . . . . . . . . . . . . . . . . . . 4 Scala como lenguaje compilado . . . . . . . . . . . . . . . . . 4 Scala como lenguaje interpretado desde un script . . . . . . . . 5 Scala como lenguaje interpretado desde un intérprete . . . . . . 5 1.2. Conceptos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.2.1. Elementos de un lenguaje de programación . . . . . . . . . . . . . . . 6 1.2.2. Elementos básicos en Scala . . . . . . . . . . . . . . . . . . . . . . . 6 1.2.2.1. Tipos de datos básicos en Scala . . . . . . . . . . . . . . . . 6 1.2.2.2. Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Operadores de igualdad. . . . . . . . . . . . . . . . . . . . . . 15 1.2.2.3. Nombrar expresiones . . . . . . . . . . . . . . . . . . . . . 15 1.2.2.4. Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 1.2.3. Uso del carácter punto y coma (;) en Scala . . . . . . . . . . . . . . . . 17 1.3. Bloques en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 1.3.1. Visibilidad y bloques en Scala . . . . . . . . . . . . . . . . . . . . . . 18 1.4. Evaluación en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 1.4.1. Evaluación de expresiones . . . . . . . . . . . . . . . . . . . . . . . . 18 1.4.2. Evaluación de funciones . . . . . . . . . . . . . . . . . . . . . . . . . 19 1.4.3. Sistema de Evaluación de Scala . . . . . . . . . . . . . . . . . . . . . 19 1.4.3.1. Valores de las definiciones . . . . . . . . . . . . . . . . . . . 19 1.4.3.2. Evaluación de Booleanos . . . . . . . . . . . . . . . . . . . 19 1.4.4. Ámbito y visibilidad de las variables . . . . . . . . . . . . . . . . . . . 20 1.5. Estructuras de control en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.5.1. Estructuras condicionales . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.5.1.1. La sentencia if . . . . . . . . . . . . . . . . . . . . . . . . . 21 La sentencia if / else . . . . . . . . . . . . . . . . . . . . . . . 22 La sentencia if...else if ...else . . . . . . . . . . . . . . . . . 22 Estructuras condicionales anidadas . . . . . . . . . . . . . . . . 23 1.5.2. Estructuras iterativas . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Página III
  • 12. IV ÍNDICE GENERAL 1.5.2.1. Bucles while . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.5.2.2. Bucles do...while . . . . . . . . . . . . . . . . . . . . . . . 24 1.5.2.3. Bucles for . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Bucles for con rangos . . . . . . . . . . . . . . . . . . . . . . . 25 Bucles for con colecciones . . . . . . . . . . . . . . . . . . . . 26 Bucles for con filtros . . . . . . . . . . . . . . . . . . . . . . . 27 Bucles for con yield. . . . . . . . . . . . . . . . . . . . . . . . 28 1.6. Interacción con Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 1.6.1. Ejecución sobre la JVM . . . . . . . . . . . . . . . . . . . . . . . . . 29 1.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 2. Programación Orientada a Objetos en Scala 31 2.1. Introducción a la programación orientada a objetos en Scala . . . . . . . . . . 31 2.1.1. Características principales de la programación orientada a objetos . . . 31 2.1.2. Scala como lenguaje orientado a objetos . . . . . . . . . . . . . . . . . 31 2.2. Paquetes, clases, objetos y namespaces . . . . . . . . . . . . . . . . . . . . . . 32 2.2.1. Objetos Singleton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 2.2.2. Módulos, objetos, paquetes y namespaces . . . . . . . . . . . . . . . . 32 2.2.3. Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.2.4. Objetos funcionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 2.2.4.1. Constructores . . . . . . . . . . . . . . . . . . . . . . . . . 35 2.2.4.2. Sobrescritura de métodos . . . . . . . . . . . . . . . . . . . 35 2.2.4.3. Precondiciones . . . . . . . . . . . . . . . . . . . . . . . . . 35 2.2.4.4. Atributos y Métodos . . . . . . . . . . . . . . . . . . . . . . 36 2.2.4.5. Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 2.3. Jerarquía de clases en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 2.3.1. Herencia en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 2.3.1.1. Rasgos y herencia múltiple en Scala . . . . . . . . . . . . . 39 2.3.1.2. Funcionamiento de los rasgos . . . . . . . . . . . . . . . . . 39 2.3.1.3. Rasgos como modificaciones apiladas . . . . . . . . . . . . 40 2.3.1.4. ¿Cuándo usar rasgos? . . . . . . . . . . . . . . . . . . . . . 42 2.4. Patrones y clases case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 2.4.1. Clases case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 2.4.2. Patrones: estructuras y tipos . . . . . . . . . . . . . . . . . . . . . . . 43 2.4.2.1. Patrones comodín . . . . . . . . . . . . . . . . . . . . . . . 43 2.4.2.2. Patrones constantes . . . . . . . . . . . . . . . . . . . . . . 43 2.4.2.3. Patrones variables . . . . . . . . . . . . . . . . . . . . . . . 44 2.4.2.4. Patrones constructores . . . . . . . . . . . . . . . . . . . . . 44 2.4.2.5. Patrones de secuencia . . . . . . . . . . . . . . . . . . . . . 44 2.4.2.6. Patrones tipados . . . . . . . . . . . . . . . . . . . . . . . . 45 2.5. Polimorfismo en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 2.5.1. Acotación de tipos y varianza . . . . . . . . . . . . . . . . . . . . . . 47 2.5.1.1. Acotación de tipos . . . . . . . . . . . . . . . . . . . . . . . 47 2.5.1.2. Varianza . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
  • 13. ÍNDICE GENERAL V 3. Programación Funcional en Scala 53 3.1. Introducción a la programación funcional . . . . . . . . . . . . . . . . . . . . 53 3.1.1. Características de los Lenguajes de Programación Funcionales . . . . . 53 3.1.2. Scala como lenguaje funcional . . . . . . . . . . . . . . . . . . . . . . 54 3.1.3. ¿Por qué la programación funcional? . . . . . . . . . . . . . . . . . . 54 3.2. Sentido estricto y amplio de la programación funcional . . . . . . . . . . . . . 54 3.2.1. ¿Qué son las funciones puras? . . . . . . . . . . . . . . . . . . . . . . 55 3.3. Funciones y cierres en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.3.1. Definición de funciones . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.3.2. Funciones anidadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.3.3. Diferencias entre métodos y funciones . . . . . . . . . . . . . . . . . . 57 3.3.4. Funciones de primera clase . . . . . . . . . . . . . . . . . . . . . . . . 58 3.3.5. Funciones anónimas y funciones valor . . . . . . . . . . . . . . . . . . 58 3.3.6. Cierres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 3.4. Recursión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 3.4.1. Importancia de la pila del sistema en recursión. . . . . . . . . . . . . . 60 3.4.1.1. La pila de Java . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.4.1.2. Contexto de pila . . . . . . . . . . . . . . . . . . . . . . . . 61 3.4.2. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 3.5. Currificación y Parcialización . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.5.1. Currificacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.5.2. Parcialización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 3.6. Orden Superior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 3.6.1. Funciones de orden superior . . . . . . . . . . . . . . . . . . . . . . . 65 3.7. Funciones polimórficas. Genericidad . . . . . . . . . . . . . . . . . . . . . . . 66 3.8. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 3.8.1. Ejercicio Resuelto . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 3.8.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 3.9. Programación funcional estricta y perezosa . . . . . . . . . . . . . . . . . . . 76 3.9.1. Funciones estrictas y no estrictas . . . . . . . . . . . . . . . . . . . . . 76 3.10. Estructuras de Datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 3.10.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 3.10.1.1. ¿Qué es una teoría?. Definición de Estructuras de Datos . . . 78 3.10.1.2. La abstracción en la programación . . . . . . . . . . . . . . 78 3.10.1.3. Datos, Tipos de Datos, Estructuras de Datos y Tipos Abstrac- tos de Datos . . . . . . . . . . . . . . . . . . . . . . . . . . 79 3.10.2. Definición de Estructuras de Datos en Lenguajes Funcionales . . . . . 80 3.10.2.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . 80 3.10.2.2. Definición . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 3.10.2.3. Los Naturales . . . . . . . . . . . . . . . . . . . . . . . . . 82 Ejercicios resueltos. . . . . . . . . . . . . . . . . . . . . . . . 86 3.10.3. Estructuras de datos lineales. Listas . . . . . . . . . . . . . . . . . . . 89 3.10.3.1. TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 3.10.3.2. Ejercicios sobre el TAD Lista . . . . . . . . . . . . . . . . . 94 3.10.4. Estructuras de datos no lineales . . . . . . . . . . . . . . . . . . . . . 95 3.10.4.1. Árboles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 3.10.4.2. Arboles Binarios . . . . . . . . . . . . . . . . . . . . . . . . 97 3.10.4.3. Arboles Binarios de Búsqueda . . . . . . . . . . . . . . . . 99
  • 14. VI ÍNDICE GENERAL 3.10.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 3.11. Colecciones en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 3.11.1. El paquete scala.collection . . . . . . . . . . . . . . . . . . . . . . . . 103 3.11.2. Iteradores en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 3.11.2.1. Métodos definidos para el tipo Iterator en Scala. . . . . . . . 105 3.11.3. Colecciones inmutables . . . . . . . . . . . . . . . . . . . . . . . . . 107 3.11.3.1. Definición de rangos en Scala. La clase Range. . . . . . . . . 107 Métodos definidos para el tipo Range en Scala. . . . . . . . . . 108 3.11.3.2. Definición de tuplas en Scala. La clase Tuple . . . . . . . . . 108 3.11.3.3. Listas en Scala. La clase List . . . . . . . . . . . . . . . . . 110 Métodos definidos para el tipo List en Scala. . . . . . . . . . . 112 Ejercicios sobre listas . . . . . . . . . . . . . . . . . . . . . . . 112 3.11.3.4. Vectores en Scala. La clase Vector . . . . . . . . . . . . . . 114 3.11.3.5. Flujos en Scala. La clase Stream . . . . . . . . . . . . . . . 115 3.11.3.6. Conjuntos en Scala. La clase Set . . . . . . . . . . . . . . . 116 Recorriendo conjuntos . . . . . . . . . . . . . . . . . . . . . . 117 Métodos definidos para el tipo Set en Scala. . . . . . . . . . . . 118 3.11.3.7. Asociaciones en Scala. La clase Map . . . . . . . . . . . . . 119 Métodos definidos para el tipo Map en Scala. . . . . . . . . . . 120 3.11.3.8. Selección de una colección . . . . . . . . . . . . . . . . . . 120 3.11.3.9. Colecciones como funciones . . . . . . . . . . . . . . . . . 121 3.11.4. Expresiones for como una combinación elegante de funciones de orden superior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 3.11.4.1. Traducción de expresiones for con un generador . . . . . . . 122 3.11.4.2. Traducción de expresiones for con un generador y un filtro . 123 3.11.4.3. Traducción de expresiones for con dos generadores . . . . . 123 3.11.4.4. Traducción de bucles for . . . . . . . . . . . . . . . . . . . 124 3.11.4.5. Definición de map, flatMap y filter con expresiones for . . . 125 3.11.4.6. Uso generalizado de for en estructuras de datos . . . . . . . 125 3.11.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 4. Programación Funcional Avanzada en Scala 129 4.1. Implícitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 4.1.1. Parámetros ímplicitos en funciones . . . . . . . . . . . . . . . . . . . 129 4.1.2. Clases implícitas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 4.2. Tipos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 4.2.1. Definición de tipos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 4.2.2. Parámetros de tipo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 4.2.2.1. Nombres de los parámetros de tipo. . . . . . . . . . . . . . . 132 4.2.3. Constructores de tipos. . . . . . . . . . . . . . . . . . . . . . . . . . . 133 4.2.4. Tipos compuestos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 4.2.5. Tipos estructurales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 4.2.6. Tipos de orden superior. . . . . . . . . . . . . . . . . . . . . . . . . . 134 4.2.7. Tipos existenciales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 4.3. Teoría de categorías . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 4.3.1. El patrón funcional Funtor . . . . . . . . . . . . . . . . . . . . . . . . 137 4.3.2. El patrón funcional Mónada . . . . . . . . . . . . . . . . . . . . . . . 138 4.3.2.1. Reglas que deben satisfacer las mónadas . . . . . . . . . . . 139
  • 15. ÍNDICE GENERAL VII 4.3.2.2. Importancia de las propiedades de las mónadas en las expre- siones for . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 4.3.2.3. Map en las mónadas . . . . . . . . . . . . . . . . . . . . . . 141 4.3.2.4. La importancia de las mónadas . . . . . . . . . . . . . . . . 142 4.3.2.5. La mónada Identidad . . . . . . . . . . . . . . . . . . . . . 142 4.3.2.6. Envolviendo el contexto con mónadas. La clase monádica Try 143 La mónada Try. . . . . . . . . . . . . . . . . . . . . . . . . . . 146 4.4. Manejo de errores sin usar excepciones . . . . . . . . . . . . . . . . . . . . . 149 4.4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 4.4.2. Tipo de datos Option . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 4.4.3. Tipo de datos Either . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 4.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 5. Tests en Scala 155 5.1. Afirmaciones Asserts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 5.1.1. Assert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 5.1.2. Ensuring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 5.2. Herramientas específicas para tests . . . . . . . . . . . . . . . . . . . . . . . . 159 5.2.1. ScalaTest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 5.3. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162 6. Concurrencia en Scala. Modelo de actores 165 6.1. Programación Concurrente. Problemática . . . . . . . . . . . . . . . . . . . . 165 6.1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 6.1.1.1. Sistema Reactivo Vs Sistema Transformacional . . . . . . . 165 6.1.2. Speed-Up en programación concurrente . . . . . . . . . . . . . . . . . 166 6.1.3. Problemática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 6.1.3.1. Propiedades de los programas concurrentes . . . . . . . . . . 166 6.1.3.2. Bloqueos y secciones críticas . . . . . . . . . . . . . . . . . 167 Problemas del uso de bloqueos . . . . . . . . . . . . . . . . . . 167 6.1.3.3. Concurrencia en Java . . . . . . . . . . . . . . . . . . . . . 167 6.2. Modelo de actores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 6.2.1. Origen del Modelo de Actores . . . . . . . . . . . . . . . . . . . . . . 168 6.2.2. Filosofía del Modelo de Actores . . . . . . . . . . . . . . . . . . . . . 169 6.3. Actores en Scala. Librería scala.actors . . . . . . . . . . . . . . . . . . . . . . 170 6.3.1. Definición de actores . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 6.3.2. Estado de los actores . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 6.3.3. Mejora del rendimiento con react . . . . . . . . . . . . . . . . . . . . 173 6.4. Actores en Scala con Akka . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 6.4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 6.4.1.1. Diferencias entre Akka y la librería Actors de Scala. . . . . . 175 6.4.2. Definición y estado de los actores . . . . . . . . . . . . . . . . . . . . 177 6.5. Buenas prácticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 6.5.1. Ausencia de bloqueos . . . . . . . . . . . . . . . . . . . . . . . . . . 182 6.5.2. Comunicación exclusiva mediante mensajes . . . . . . . . . . . . . . . 182 6.5.3. Mensajes inmutables . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 6.5.4. Mensajes autocontenidos . . . . . . . . . . . . . . . . . . . . . . . . . 182 6.6. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
  • 16. VIII ÍNDICE GENERAL 7. Conclusiones 185 8. Solución a los ejercicios propuestos 189 8.1. Evaluación en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 8.2. Introducción a la Programación Funcional . . . . . . . . . . . . . . . . . . . . 189 8.3. Estructuras de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 8.3.1. TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 8.3.2. TAD Arbol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 8.4. Colecciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 8.4.1. Tipo List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 8.4.2. Otras colecciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 8.5. Programación Funcional Avanzada . . . . . . . . . . . . . . . . . . . . . . . . 204 8.6. Tests en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 8.7. Concurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208 Lista de tablas 210 Listados de algoritmos 211 Referencias 219 Glosario 221 Acrónimos 225
  • 17. Capítulo 1 Scala 1.1. Introducción El nombre Scala significa: diseñado para crecer con la demanda de sus usuarios (Scala- ble language). Scala es un lenguaje de programación multi-paradigma diseñado para expresar patrones comunes de programación de forma concisa, elegante y con tipos estáticos. Integra elegantemente características de lenguajes funcionales y orientados a objetos, lo cual hace que la escalabilidad sea una de las principales características del lenguaje. La implementación ac- tual corre en la máquina virtual de Java y es compatible con las aplicaciones existentes en Java. [7] Su creador, Martin Odersky1 , y su equipo comenzaron el desarrollo de este nuevo lengua- je de código abierto en el año 2001, en el laboratorio de métodos de programación en École Polytechnique Fédérale de Lausanne (EPFL). Scala hizo su aparición pública sobre la plataforma Máquina Virtual de Java (JVM) en enero de 2004 y unos meses después haría 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 y las ventajas que ofrece han hecho ya que cada vez más empresas apuesten por Scala ( Twitter, Linked-in, Foursquare, The Guardian, ...). Las características de Scala le hacen un lenguaje ideal para ser utilizado en centros de computación, centros de hosting,..., en los que las aplicaciones se ejecutan de forma parale- la en los supercomputadores, servidores,...que los conforman. Además, Scala es uno de los lenguajes de programación que se utilizan para desarrollar aplicaciones para los dispositivos Android. “ Si le das a una persona un pescado, comerá un día. Si enseñas a pescar a una persona, comerá toda su vida. Si das herramientas a una persona, puede construirse 1 Martin Odersky es profesor de la EPFL en Lausanne, Suiza y ha trabajado en los lenguajes de programación durante la mayor parte de su carrera. Estudió programación estructurada y orientada a objetos como estudiante de doctorado de Niklaus Wirth. Después, mientras trabajaba en IBM y en la Universidad de Yale se enamoró de la programación funcional. Cuando apareció Java, comenzó a añadir construcciones de programación funcionales para la nueva plataforma, lo cual dio lugar a Pizza y GJ y eventualmente a Java 5 con los genéricos. Durante ese tiempo también desarrolló javac, el compilador de referencia actual para Java. En los últimos 10 años, Martin ha centrado su trabajo en la unificación de los paradigmas de programación funcional y programación orientada a objetos en el lenguaje Scala. Scala pasó rápidamente del laboratorio de investigación a convertirse en una herramienta de código abierto y en un lenguaje industrial. Odersky ahora es el encargado de supervisar el desarrollo de Scala como jefe del grupo de programación de la EPFL y como presidente de la empresa Typesafe. Página 1
  • 18. una caña de pescar, ¡y muchas otras herramientas!. Incluso podrá construir una máquina para fabricar más cañas y así podrá ayudar a otras personas a pescar. Ahora debemos conceptualizar el diseño de un lenguaje de programación co- mo un patrón para diseñar lenguajes de programación, como una herramienta para hacer más herramientas del mismo tipo. El diseño de un lenguaje de programación debe ser un patrón, un patrón de crecimiento, un patrón para desarrollar el patrón para la definición de patrones que los programadores puedan usar en su trabajo y para alcanzar su objetivo.” [1] 1.1.1. Scala. Un lenguaje 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 las exigencias 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 abstracción de componen- tes, su sintaxis reducida, el soporte para la programación orientada a objetos y la programación funcional han contribuido a que el lenguaje sea más escalable y haga de la escalabilidad una de sus principales características. 1.1.2. Paradigmas de la programación Dentro de la programación se pueden distinguir tres paradigmas: Programación imperativa. Programación lógica. Programación funcional. La programación imperativa pura está limitada por “el cuello de botella de Von Neuman”, término que fue acuñado por John Backus en su conferencia de la concesión del Premio Turing por la Asociación para la Maquinaria Computacional (ACM) en 1977. Según Backus: “Seguramente debe haber una manera menos primitiva de realizar grandes cam- bios en la memoria, que empujando tantas palabras hacia un lado y otro del cuello de botella de Von Neumann. No sólo es un cuello de botella para el tráfico de datos, sino que, más importante, es un cuello de botella intelectual que nos ha mantenido atados al pensamiento de “una palabra a la vez” en lugar de fomentarnos el pensar en unidades conceptuales mayores. Entonces la programación es básicamente la planificación del enorme tráfico de palabras que cruzan el cuello de botella de Von Neumann, y gran parte de ese tráfico no concierne a los propios datos, sino a dónde encontrar éstos” En la actualidad, la programación funcional y la programación orientada a objetos (POO)2 se preocupan mucho menos de “empujar un gran número de palabras hacia un lado y otro” que otros lenguajes anteriores (como por ejemplo Fortran), pero internamente, esto sigue siendo 2 El paradigma de la POO se considera ortogonal a los paradigmas de programación funcional, programación imperativa y programación lógica. Página 2
  • 19. lo que hacen durante gran parte del tiempo los computadores, incluso los supercomputadores altamente paralelos3 .[26] Del cuello de botella intelectual criticado por Backus subyace la necesidad de disponer de otras técnicas para definir abstracciones de alto nivel como son los conjuntos, los polinomios, las formas geométricas, las cadenas de caracteres, los documentos,...para lo que idealmente se deberían de desarrollar teorías de conjuntos, formas, cadenas de caracteres, etc. 1.1.2.1. Scala. Un lenguaje multiparadigma Scala ha sido el primero en incorporar y unificar la programación funcional y la programa- ción orientada a objetos en un lenguaje estáticamente tipado, donde la clase de cada instancia se conoce en tiempo de compilación, así como la disponibilidad de cualquier método de una instancia dada. La pregunta es: ¿por qué se necesita 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.3. Preparación del sistema 1.1.3.1. Descargar Scala Para disponer de la última versión de Scala sólo habrá que acceder a la sección “Download” en la web oficial: http://www.scala-lang.org/download/ Para trabajar con Scala, sólo será necesario un editor de texto y un terminal. También es posible trabajar con un Entorno de Desarrollo Integrado (IDE), ya que existen plugins para Eclipse, IntelliJ IDEA o Netbeans.4 1.1.3.2. Herramientas de Scala Las herramientas de línea de comandos de Scala se encuentran dentro de la carpeta bin de la instalación. El compilador de Scala: scalac El compilador scalac transforma un programa de Scala en archivos .class que podrán ser utilizados por la JVM. El nombre del archivo no tiene que corresponder con el nombre de la clase. Sintaxis: scalac [opciones ...] [archivos fuente ...] Opciones: La opción -classpath (-cp) indica al compilador donde encontrar los archivos fuente La opción -d indica al compilador donde colocar los archivos .class La opción -target indica al compilador qué versión de máquina virtual usar 3 Un estudio de referencia de base de datos, realizado a partir de 1996, encontró que tres de cada cuatro ciclos de la unidad central de procesamiento (CPU) se dedican a la espera de memoria. 4 En los próximos capítulos se usará tanto el intérprete de Scala como el IDE Eclipse junto con el plugin ScalaIDE disponible en la web http://scala-ide.org/. Página 3
  • 20. El intérprete de código: scala La instrucción scala puede operar en tres modos: Puede ejecutar clases compiladas Puede ejecutar archivos fuente (scripts – Secuencia de instrucciones almacenadas en un fichero que se ejecutan normalmente de forma interpretada. –) Puede operar en forma interactiva, es decir como intérprete Sintaxis: scala [opciones ...] [script u objeto] [argumentos] Si no se especifica un script o un objeto, la herramienta funciona como un evaluador de expresiones interactivo. En cambio, si se especifica un script, el comando scala lo compilará y lo ejecutará. Si se especifica el nombre de una clase, scala la ejecuta. Algunos comandos importantes: :help muestra mensaje de ayuda :quit termina la ejecución del intérprete :load carga comandos de un archivo Scala como lenguaje compilado Como primer ejemplo, se puede observar la definición del programa estándar “Hola mun- do”. 1 object HolaMundo { 2 def main(args: Array[String]) { 3 println("Hola, mundo!") 4 } 5 } Algoritmo 1.1: Hola Mundo La estructura de este programa debería resultar familiar para los lectores que ya conozcan Java. Consiste de un método llamado main que toma los argumentos de la línea de comandos (un vector, array en inglés, de objetos del tipo String) como parámetro. El cuerpo de este método presenta una sola llamada al método predefinido println con el saludo como argumento. El método main no devuelve un valor, por lo tanto no es necesario que se declare un tipo retorno. Lo que es menos familiar es la declaración de objetos que contienen al método main. Esta declaración introduce lo que es comúnmente conocido como singleton objects (objeto single- ton), que es una clase con una sola instancia. Por lo tanto, dicha construcción declara tanto una clase llamada HolaMundo, como una instancia de esa clase también llamada HolaMundo. Esta instancia es creada bajo demanda, es decir, la primera vez que es utilizada. Se puede advertir que el método main no está declarado como estático. Esto es así porque los miembros estáticos (métodos o campos) no existen en Scala. En vez de definir miembros estáticos, en Scala se declararán estos miembros en un objeto singleton. Compilando el ejemplo Para compilar el ejemplo utilizaremos scalac, el compilador de Scala. El comando scalac funciona como la mayoría de los compiladores: toma un archivo fuente como argumento, al- gunas opciones y produce uno o varios archivos objeto. Los archivos objeto que produce son archivos class para la JVM. Página 4
  • 21. Si guardamos el programa anterior en un archivo llamado HolaMundo.scala, podemos com- pilarlo ejecutando el siguiente comando: $ scalac HolaMundo.scala Esto generará algunos archivos class en el directorio actual. Uno de ellos se llamará Hola- Mundo.class y contiene una clase que puede ser directamente ejecutada utilizando el comando scala, como mostramos en la siguiente sección. Ejecutando el ejemplo Una vez compilado, un programa Scala puede ser ejecutado utilizando el comando scala. Su uso es muy similar al comando java utilizado para ejecutar programas Java, y acepta las mismas opciones. El ejemplo de arriba puede ser ejecutado utilizando el siguiente comando que, como se puede comprobar, produce la salida esperada: $ scala HolaMundo Hola, mundo! Scala como lenguaje interpretado desde un script Como ya se ha introducido anteriormente, es posible ejecutar archivos fuente haciendo uso del comando scala. A continuación se muestra como crear un script básico llamado hola.scala: 1 println("Hola mundo, desde un script!") Ejecutando el ejemplo desde la línea de comandos se comprueba que el resultado obtenido es el esperado: $>scala hola.scala Hola mundo, desde un script! Desde la línea de comandos se le pueden pasar argumentos a los scripts mediante el vector de argumentos args. En Scala, el acceso a los elementos de un vector se realiza especificando el índice entre paréntesis (no entre corchetes como en Java)5 . Se ha definido el siguiente script, llamado holaarg.scala, con el objetivo de probar el funcionamiento del paso de argumentos desde la línea de comandos: 1 println("Hola, "+ args(0) +"!") Si se ejecuta: $> scala holaarg.scala pepe En este comando, “pepe” se pasa como argumento desde la línea de comandos, el cual se accede desde el script mediante args(0). La salida obtenida es la que se esperaba: Hola, pepe! Scala como lenguaje interpretado desde un intérprete Para lanzar el intérprete de Scala, se tiene que abrir una ventana de terminal y teclear scala. Aparecerá el prompt del intérprete de Scala a la espera de recibir expresiones. Funciona, al igual que el intérprete de Scheme o Haskell, mediante el Bucle Leer-Evaluar-Imprimir (REPL). El intérprete de Scala será muy utilizado durante el capítulo dedicado a la programación funcional, por lo que se recomienda familiarizarse con el mismo. 5 Notación que es homogénea para las demás estructuras, incluso las estructuras definidas por el usuario. Página 5
  • 22. 1.2. Conceptos básicos 1.2.1. Elementos de un lenguaje de programación Un lenguaje de programación debe proveer: expresiones primitivas que representen los elementos más simples maneras de combinar expresiones maneras de abstraer expresiones, lo cual introducirá un nombre para esta expresión y nos permitirá hacer referencia a la expresión. 1.2.2. Elementos básicos en Scala 1.2.2.1. Tipos de datos básicos en Scala En Scala se pueden encontrar los mismos tipos de datos básicos que en Java. El tamaño que ocupa cada uno de ellos en memoria, así como la precisión de los tipos primitivos de Scala, también se corresponden con los de Java. Aunque se hable de tipos de datos, en Scala todos los tipos de datos son clases. En la tabla 1.1 se muestran los tipos básicos en Scala. Tipo de dato Tamaño Rango Ejemplo Byte 8 bits con signo [−128, 127] 38 Short 16 bits con signo [−32768, 32767] 23 Int 32 bits con signo [−231 , 231 − 1] 45 Long 64 bits con signo [−263 , 263 − 1] 3434115 Float 32 bits con signo [−3,4028 ∗ 1038 , 3,4028 ∗ 1038 1.38 Double 64 bits con signo [−1,7977 ∗ 10308 , 1,7977 ∗ 10308 ] 54.37 Boolean true o false true Char 16 bits con signo [0, 216 − 1] ’F’ String secuencia de caracteres Cadena de caracteres "hola mundo!" Tabla 1.1: Tipos de datos primitivos y tamaño en Scala Los tipos Byte, Short, Int y Long reciben el nombre de tipos enteros. Los tipos enteros junto con los tipos Float y Double son llamados tipos numéricos. Excepto String, que es del paquete java.lang, el resto se encuentran en el paquete Scala. Todos se importan automáticamente. Literales de tipos básicos en Scala Las reglas sobre literales que usa Scala son bastante simples e intuitivas. A continuación se verán las principales características de los literales básicos en Scala. Literales enteros Los mayoría de literales enteros que utilicemos serán de tipo Int. Los literales enteros también podrán ser de tipo Long añadiendo el sufijo L o l al final del literal. A continuación se muestran algunos ejemplos: Página 6
  • 23. scala> 5 res0: Int = 5 scala> 777L res3: Long = 777 scala> 0xFFAFAFA5 res2: Int = -5263451 scala> 0777L <console>:1: error: Non-zero integral values may not have a leading zero. 0777L ^ Los literales enteros no podrán tener el cero como primer dígito6 , excepto el entero 0 y aquellos representados en notación hexadecimal. Los enteros representados en notación hexa- decimal comienzan por 0x o 0X, pudiendo estar seguido por dígitos entre 0 y 9 o letras entre A y F (en mayúscula o minúscula). Literales en punto flotante Los literales en punto flotante serán del tipo de datos Float cuando se añada el sufijo F o f. En otro caso serán de tipo Double. Estos literales sí podrán contener uno o varios ceros como primeros dígitos. scala> 0.0 res0: Double = 0.0 scala> 01.2 res1: Double = 1.2 scala> 01.2F res2: Float = 1.2 scala> 00045.34 res3: Double = 45.34 Literales lógicos Los literales lógicos o booleanos son aquellos que pertenecen a la clase Boolean y que sólo pueden tener uno de los dos valores booleanos: true o false. Literales de tipo símbolo Aunque el tipo de datos Symbol (scala.Symbol) no se considera un tipo básico de Scala, merece la pena tenerlo en cuenta. Los símbolos serán cadenas de caracteres no vacías precedidas del prefijo ’. La clase case Symbol está definida de la siguiente forma: 1 package scala 2 final case class Symbol private (name: String) { 3 override def toString: String = "’" + name 4 } Ejemplo: 6 En versiones anteriores de Scala, cuando un entero comenzaba por cero era porque el número estaba en base 8 y, por tanto, sólo podía estar seguido de los dígitos comprendidos entre 0 y 7. Página 7
  • 24. scala> ’miSimbolo res4: Symbol = ’miSimbolo Literales de tipo carácter Un literal de tipo carácter consiste en un carácter encerrado entre comillas simples que representará un carácter representable o un carácter de escape7 . El tipo de datos de los literales de tipo carácter es Char. El estándar de codificación de caracteres utilizado para representar los caracteres es Unicode. También podremos indicar el código unicode del carácter que queramos representar encerrado entre comillas simples. Veamos algunos ejemplos: scala> ’u0050’ res5: Char = P scala> ’S’ res6: Char = S scala> ’n’ res7: Char = Literales de tipo cadena de caracteres Un literal del tipo cadena de caracteres será una secuencia de caracteres encerradas entre comillas dobles cuyo tipo de datos será String. Los caracteres que conformen la cadena de caracteres podrán ser tanto caracteres representables, como caracteres de escape. Por ejemplo: scala> "Hola Mundo!" res8: String = Hola Mundo! scala> "Hola " Mundo!" res9: String = Hola " Mundo! Los literales de tipo cadena de caracteres también pueden ser multilínea en cuyo ca- so estarán encerrados entre tres comillas dobles. En este caso, la cadena de caracteres podrá estar formada por los caracteres representables, caracteres de escape o por caracteres no repre- sentables como salto de línea o cualquier otro carácter especial como comillas dobles, barra inversa,...con la única salvedad de que sólo podrá haber tres o más comillas dobles al final. Veamos algún ejemplo: scala> """y dijo: | "mi nombre es Bond, James Bond" | ///""" res10: String = y dijo: "mi nombre es Bond, James Bond" /// Caracteres de escape Los caracteres de escape que se muestran en la tabla 1.2 son reconocidos tanto en los literales de caracteres como en los literales de cadenas de caracteres. 7 También conocidos como secuencia de escape Página 8
  • 25. Carácter de escape Unicode Descripción b u0008 Retroceso BS t u0009 Tabulador horizontal HT n u000A Salto de línea LF f u000C Salto de página FF r u000D Retorno de carro CR " u0022 Comillas dobles ’ u0027 Comilla simple u005c Barra inversa Tabla 1.2: Caracteres de escape reconocidos por Char y String En versiones anteriores de Scala era posible definir cualquier carácter que tuviera un código unicode entre 0 y 255, indicando el código en base octal. Esto se indicaba precediendo al código octal de . Por ejemplo 150 representaría el carácter h. Esta representación está depreciada en la actualidad, siendo sustituida por la representación en base hexadecimal de los caracteres. scala> "150" <console>:1: warning: Octal escape literals are deprecated, use u0068 instead. "150" ^ res11: String = h scala> "u0068" res12: String = h Expresiones de tipos básicos en Scala Las expresiones en Scala pueden estar formadas por: Un literal válido de cada uno de los tipo de datos básicos vistos en la tabla 1.1. Por ejemplo: 1, 2.5, 3.74E10f, true, false, ’a’... Un operador 8 junto con dos operandos (operadores binarios) o un operando (operadores unarios) del tipo de datos compatible con el operador. El último valor evaluado en un bloque (en la llamada a una función o método). Expresiones aritméticas en Scala Las expresiones aritméticas son aquellas que, tras ser evaluadas, devuelven un valor nu- mérico. Los tipos de datos Byte, Short, Int y Long serán utilizados para representar valores numéricos de tipo entero. Con los tipos de datos Float y Double se representarán valores de tipo real. El tipo de datos devuelto por defecto al evaluar una expresión que represente un número entero es Int mientras que Double será el tipo de datos por defecto devuelto al evaluar una expresión que represente un número real. scala> 1.2 res0: Double = 1.2 scala> 5 res1: Int = 5 8 Los operadores se verán en la Subsubsección 1.2.2.2: Operadores « página 10 » Página 9
  • 26. Expresiones booleanas Una expresión booleana puede estar compuesta por un literal, por uno de los operadores lógicos mostrados en la tabla 1.6 o ser el valor devuelto por las operaciones usuales de com- paración que se muestran en la tabla 1.5 de operadores relacionales en Scala. Una expresión booleana también puede ser el resultado de la evaluación de cualquier expresión booleana co- mo, por ejemplo, el resultado de una función o método. 1.2.2.2. Operadores Los operadores se utilizarán para combinar expresiones de los tipos de datos básicos. En Scala los operadores son en realidad métodos y, por tanto, se reducen a la llamada a un método de un objeto de una de clases de los tipos de datos básicos. Es decir, 1 + 2 realmente invoca al método + del objeto 1 con el parámetro 2: (1).+(2). Es más, en la clase Int hay diferentes definiciones del método + (método sobrecargado) que difieren las unas de las otras en el tipo del parámetro con el que se invocan y que nos permiten realizar la suma de objetos de diferentes tipos enteros. Por ejemplo, existe otro método + en la clase Int que recibe como parámetro un objeto de tipo Long y devuelve otro objeto de tipo Long. Los operadores se pueden clasificar, atendiendo a su notación en: Operadores infijos. Son operadores binarios en los que el método que se va a invocar se ubica entre sus dos operandos. Un ejemplo de operador infijo podría ser el operador aritmético + de los tipos numéricos. Operadores prefijos. Son operadores unarios en los que el nombre del método se sitúa delante del objeto que invocará al método. Como por ejemplo: !b, -5,... Operadores postfijos9 . Son aquellos operadores unarios en los que el nombre del método se sitúa detrás del objeto que invocará al método. Por ejemplo: 5 abs, 2345 toLong. Infijos, Prefijos y Postfijos En Scala los operadores no tienen una sintaxis especial10 , como ocurre en otros lenguajes de programación como Haskell, por lo que cualquier método puede ser un operador. Lo que convertirá un método en un operador será la forma de usar el mismo. Por ejemplo, si se escribe 1.+(2), + no será un operador. Pero si se escribe 1 + 2, + sí será un operador. Existe la posibilidad de definir nuevos operadores prefijos definiendo métodos que comien- cen por unary_ y estén seguidos por un identificador válido. Por ejemplo, si en un tipo de datos se define un método unary_!, una instancia de este tipo de datos podrá invocar ! en notación prefija. Scala transforma las llamadas a operadores prefijos en llamadas al método unary_. Por ejemplo, la expresión !p es transformada por Scala en la invocación de p.unary_!. Los operadores postfijos son métodos que no reciben parámetros y, por convenio, no es necesario el uso de paréntesis11 9 Los operadores postfijos tienen que ser habilitados -visibles- importando scala.language.postfixOps o indican- do al compilador la opción -language:postfixOps 10 Excepto los operadores prefijos en los que los identificadores que pueden ser usados en este tipo de operadores son +, -, ! y ~. 11 Excepto si el método presenta efectos colaterales, como println(), en cuyo caso sí habrá que poner los parén- tesis. Página 10
  • 27. Los operadores infijos son aquellos métodos que reciben un argumento. Los operadores infijos se ubican entre sus dos operandos (como el operador +, en el que el primer operando será el objeto que invoca al método y el segundo operando es el argumento que recibe). Prioridad y Asociatividad de los operadores Cuando en una expresión aparecen varios operadores, la prioridad de los operadores nos indicará el orden de evaluación de las diferentes partes que componen la expresión, es decir, qué partes de la expresión son evaluadas antes. Por ejemplo, el resultado de evaluar la expresión 100 - 40 * 2 es 20, no 120, ya que el operador * tiene mayor prioridad que el operador +. El resultado de la anterior expresión es el mismo que si se evalúa la expresión 100 - (40 * 2). Si se quisiera cambiar el orden de evaluación anterior se debería escribir la expresión: (100 - 40) * 2, cuyo resultado sería 120. Scala determina la prioridad de los operadores basándose en el primer carácter de los méto- dos usados con notación de operador. La excepción a esta regla es el operador de menor priori- dad en Scala: el operador de asignación (los operadores que terminan con el carácter ’=’). Así, con la excepción ya comentada de los operadores de asignación, la precedencia de operadores se determinará según el primer carácter, tal como se muestra en la tabla 1.3, donde encontramos la prioridad de los operadores básicos, de forma que los operadores de mayor prioridad se en- cuentran en la parte superior de la tabla y los operadores de menor prioridad en la parte inferior. La prioridad de cualquier operador definido por el usuario se definirá por el primer carácter empleando según esta misma tabla. Tipo Operador Asociatividad Postfijos (), [],... Izquierda Unarios ! ˆ Derecha Multiplicativos * / % Izquierda Aditivos + - Izquierda Binario : Izquierda Binario = ! Izquierda Desplazamiento >> >>> << Izquierda Relación <><= >= Izquierda Igualdad == != Izquierda Bit a bit AND & Izquierda Bit a bit XOR ˆ Izquierda Bit a bit OR | Izquierda AND lógico && Izquierda OR lógico || Izquierda Todas las letras Izquierda Asignación = += -= *= /= %= >>= <<= &= ˆ= |= Derecha Coma , Izquierda Tabla 1.3: Prioridad y asociatividad de los operadores Cuando se encuentran en una expresión varios operadores con la misma prioridad, será otra de las características de los operadores, la asociatividad, la encargada de indicar cómo se agrupan los operadores y, por tanto, como se evaluará la expresión. La asociatividad de un operador en Scala se determina por el último carácter del operador. Si el último carácter de Página 11
  • 28. un operador binario es :, el operador tendrá asociatividad derecha, lo que quiere decir que los métodos que terminen en : serán invocados por el operando situado en el lado derecho del operador y se les pasará como parámetro el operando izquierdo. Es decir, si se tiene la expresión x /: y, será equivalente a y./:(x). En cualquier otro caso, el operador presentará asociatividad izquierda. La asociatividad de un operador no influye en el orden de evaluación de los operandos, que siempre será de izquierda a derecha. En la anterior expresión, x/:y, primero se evaluará el operando x y después el operando y. Es decir, la expresión es tratada como el siguiente bloque: 1 {val t=x;y./:(x)} Como se ha dicho anteriormente, la asociatividad determinará como se agrupan los opera- dores en caso de que aparezcan en una expresión varios operadores con la misma prioridad. Si los métodos terminan en ’:’, se agruparán de derecha a izquierda, mientras que en cualquier otro caso se agruparán de izquierda a derecha. Si se tienen las expresiones a * b * c y x /: y /: z, se evaluarán como (a * b) * c y x /: (y /: z), respectivamente. A pesar de conocer las reglas que determinan la prioridad de los operadores y la asociatividad de los mismos, es aconsejable usar paréntesis para aclarar cuales son los operadores que actúan sobre cada una de las expresiones. Operadores aritméticos Los operadores aritméticos son aquellos que operan con expresiones enteras o reales, es decir, con objetos de tipo Short, Int, Double o Long. En Scala se pueden distinguir dos tipos de operadores aritméticos: unarios y binarios. En la tabla 1.4 se muestran los diferentes operadores aritméticos presentes en Scala. Descripción Operador Tipo Signo positivo + unario Signo negativo - unario Suma + binario Resta - binario División / binario Producto * binario Resto % binario Tabla 1.4: Operadores aritméticos en Scala Todos los operadores admiten expresiones enteras y reales. En el caso de los operadores binarios, si los dos operandos son enteros o reales el resultado de la operación será un valor entero o real respectivamente. Si uno de sus operandos es entero y el otro real entonces el resultado devuelto será de tipo real. Los operadores aritméticos también servirán para combinar expresiones de tipo carácter. En este caso, Scala tomará el valor decimal del código unicode que represente el carácter de cada uno de los operadores. scala> 1.5 * 3 res2: Double = 4.5 scala> 5 * 3 res3: Int = 15 Página 12
  • 29. Operadores relacionales El uso de operadores relacionales permite comparar expresiones de tipos de datos com- patibles, devolviendo un resultado de tipo booleano: true o false. Scala soporta los operadores relacionales que se muestran en la tabla 1.5. Los operadores relacionales son binarios. Descripción Operador Menor < Menor/Igual <= Mayor > Mayor/Igual >= Distinto != Igual == Tabla 1.5: Operadores relacionales en Scala Como se puede apreciar en el siguiente ejemplo, los resultados obtenidos después de com- parar diferentes expresiones numéricas (reales o enteras) son los esperados: scala> 12.0 * 3 == 3 * 12 res4: Boolean = true scala> 3.0f < 7 res5: Boolean = true scala> "cadena" == "cadena" res6: Boolean = true En cambio, si lo que pretende es comparar dos expresiones booleanas se deberá tener en cuenta que el valor false se considera menor que el valor true. scala> 13<10 < (11==11.0) res7: Boolean = true Cuando se comparen expresiones del tipo de datos Char o String se deberá tener en cuenta que se basan en el valor decimal del código unicode de cada carácter12 . En el caso de expresiones del tipo de datos String, cuando los caracteres situados en la primera posición de las cadenas que se estén comparando sean iguales (mismo código unicode) se comparará el segundo carácter de ambas cadenas y así sucesivamente hasta que se encuentre el primer par de caracteres distintos, ubicados en la misma posición en ambas cadenas, o hasta que la cadena cuya longitud sea menor se termine, en cuyo caso la cadena de menor longitud será menor que la cadena de mayor longitud. A continuación se muestran unos ejemplos en el intérprete de Scala que pueden ayudar a aclarar estos conceptos: scala> "hola mundo" < "hola mundo scala!" res8: Boolean = true scala> ’a’<’b’ res9: Boolean = true scala> "hola"<"hola" res10: Boolean = false scala> "hola"<="hola" res11: Boolean = true scala> "hola" <= "Hola" res12: Boolean = false 12 En Scala, una expresión de tipo String y otra de tipo Char no se pueden comparar. Página 13
  • 30. En otros lenguajes de programación como Java, el operador relacional == se utiliza pa- ra comparar tipos primitivos (comparando si los valores son iguales, como en Scala) y tipos referenciados (comparando igualdad referencial). En Scala se puede comparar la igualdad refe- rencial de tipos referenciados usando eq y neq. Operadores lógicos Los operadores lógicos permitirán combinar expresiones lógicas. Las expresiones lógicas son todas aquellas expresiones que tras ser evaluadas se obtiene: verdadero o falso. Scala soporta los operadores lógicos que se muestran en la tabla 1.6. Descripción Operador Tipo AND lógico && binario OR lógico || binario NOT lógico ! unario Tabla 1.6: Operadores lógicos en Scala Operadores bit a bit. Los operadores bit a bit trabajan sobre cadenas de bits aplicando el operador en cada uno de los bits de los operadores. Las tablas de verdad para los operadores &, | y ˆ son las que se muestran en la tabla 1.7 p q p& q p ˆ q p | q 0 0 0 0 0 0 1 0 1 1 1 0 1 1 0 1 1 0 1 1 Tabla 1.7: Tabla de verdad de los operadores bit a bit &, | y ˆ . Los operadores bit a bit soportados por Scala se muestran en la tabla 1.8. Operador Descripción Ejemplo & AND binario a & b | OR binario a | b ˆ XOR binario 45 ˜ Complemento a uno (intercambio de bits) ˜a << Desplazamiento binario a la izquierda a <<2 >> Desplazamiento binario a la derecho a >>2 >>> Desplazamiento a la derecha con relleno de ceros a >>>2 Tabla 1.8: Operadores bit a bit. Página 14
  • 31. Operadores de igualdad. Scala soporta los operadores de asignación mostrados en la tabla 1.9. Operador Descripción Ejemplo = Operador de asignación simple C = A + B += Suma y asignación A += B equivalente a A = A + B -= Resta y asignación A -= B equivalente a A = A - B *= Multiplicación y asignación A *= B equivalente a A = A * B /= División y asignación A /= B equivalente a A = A / B %= Módulo y asignación A %=B equivalente a A = A % B <<= Desplazamiento a la izquierda y asignación A <<= 2 equivalente a A = A <<2 &= Bit a bit AND y asignación A &= 2 equivalente a A = A & 2 ˆ= Bit a bit XOR y asignación A ˆ= 2 equivalente a A = A ˆ 2 |= Bit a bit OR y asignación A |= 2 es equivalente a A = A | 2 Tabla 1.9: Operadores de asignación. 1.2.2.3. Nombrar expresiones Es posible nombrar una expresión con la palabra reservada def y utilizar su nombre (identi- ficador) en lugar de la expresión: scala> def scale = 5 scale: Int scala> 7 * scale res4: Int = 35 scala> def pi = 3.141592653589793 pi: Double scala> def radius = 10 radius: Int scala> 2 * pi * radius res5: Double = 62.83185307179586 def es una primitiva declarativa: le da un nombre a una expresión, pero no la evalúa. scala> def r=8/0 r: Int scala> r java.lang.ArithmeticException: / by zero at .r(<console>:7) ... 33 elided Se puede observar que la definición de r no da error. El error se produce en el momento que se evalúa por primera vez r. Es lo contrario que la forma especial define de Scheme, que se utiliza para asociar nombres con expresiones, y en primer lugar se evalúa la expresión. En realidad, nombrar una expresión sólo aportará azúcar sintáctico, permitiendo crear funciones y evitando escribir la expresión lambda asociada. Es decir, es lo mismo que crear una función en Scheme sin argumentos. Alternativamente, se verá como Scala permite una definición de variables utilizando val o var. 1.2.2.4. Variables Las variables están encapsuladas en objetos, es decir, no pueden existir por si mismas. Las variables son referencias a instancias de clases. Página 15
  • 32. Para definir una variable se requiere: Definir su mutabilidad Definir el identificador Definir el tipo (opcional) Definir un valor inicial Sintaxis para definir una nueva variable: <var | val> < identificador> : < Tipo> = < _ | Valor Inicial> La nueva variable podrá ser un literal o el resultado de la evaluación de una expresión, una función o el valor devuelto por un método. Pero también se podrán definir funciones, métodos, bloques, etc. Por ejemplo: 1 val x : Int = 1 Se usa la palabra reservada val para indicar que la referencia no puede reasignarse, mientras que se utiliza var para indicar que sí puede reasignarse. La variable val es similar a una variable final en Java: una vez inicializada, no se podrá reasignar. Una variable var, por el contrario, se puede reasignar múltiples veces. Todas las variables o referencias en Scala tienen un tipo, debido a que el lenguaje es estric- tamente tipado. Sin embargo, los tipos pueden en muchos casos omitirse, porque el compilador de Scala tiene inferencia de tipos. Por ejemplo, las siguientes definiciones son equivalentes: scala> var x : Int = 1 scala> var x = 1 Otros ejemplos: val msg = "Hola mundo!" msg: java.lang-String = Hola mundo! En este ejemplo se aprecia que Scala tiene inferencia de tipos, es decir, al haber inicializado la variable msg con una cadena, Scala asocia el tipo de msg al tipo de datos String. Si se intenta modificar el valor de msg, no se podrá y obteniéndose un error ya que se ha definido como val: scala> msg = "Hasta luego!" <console>:5: error: reassignment to val msg = "Hasta luego!" Si se imprime por pantalla el valor de msg: scala> println(msg) Hola mundo! Si se quisiera reasignar el valor de una variable habría que utilizar la palabra reservada var: scala> var saludo = "Hola mundo!" saludo: java.lang.String = Hola mundo! scala> saludo = "Hasta luego!" saludo: java.lang.String = Hasta luego! Aunque todas las declaraciones de variables podrían realizarse utilizando var, se recomienda encarecidamente usar val cuando la variable no vaya a mutar. Página 16
  • 33. 1.2.3. Uso del carácter punto y coma (;) en Scala En Scala, el uso del punto y coma al final de una línea de programa es opcional en la mayoría de las ocasiones, siendo recomendado omitir el mismo siempre y cuando su uso no sea obligatorio. Se podría escribir: 1 def pi = 3.14159; aunque muchos programadores omitirán el uso del (;) y simplemente escribirán: 1 def pi = 3.14159 ¿Cuándo es obligatorio el uso del punto y coma? El uso del punto y coma es obligatorio para separar instrucciones escritas en la misma línea. 1 def y = x - 1; y + y Uso del punto y coma y operadores infijos en Scala. Uno de los problemas derivados de omitir el uso del punto y coma en Scala es cómo escribir expresiones que ocupen varias líneas. Si se escribiera: Expresión larga + otra expresión larga sería interpretado por Scala como dos expresiones. Si lo que se quiere es que se interprete como una única expresión, se podría hacer de dos formas: Se podría encerrar una expresión de varias líneas entre paréntesis, dando por hecho que no se usará el punto y coma en éstas líneas: (Expresión larga + otra expresión larga) Se podría escribir el operador al final de la línea, indicándole así a Scala que la expresión no está finalizada: Expresión larga + otra expresión larga Por tanto, por norma general, los saltos de línea serán tratados como puntos y coma, salvo que algunas de las siguientes condiciones sea cierta: 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 Página 17
  • 34. 1.3. Bloques en Scala Un bloque en Scala estará encerrado entre llaves { ...}. Dentro de un bloque se podrán encontrar definiciones o expresiones. El último elemento de un bloque será una expresión que definirá el valor del bloque, la cual podrá estar precedida por definiciones auxiliares. Los bloques también son expresiones en sí mismos, por lo que un bloque podrá aparecer en cualquier lugar en el que pueda aparecer una expresión. Ejemplo: 1 { val x = f(3) 2 x * x 3 } 1.3.1. Visibilidad y bloques en Scala Scala sigue las reglas de ámbito habituales en lenguajes como C o Java. Las definiciones realizadas dentro de un bloque sólo serán visibles dentro del mismo y ocultan las definiciones realizadas fuera del bloque que tengan el mismo nombre. Las definiciones realizadas en bloques externos estarán visibles dentro de un bloque siempre y cuando no sean ocultadas por otras definiciones con el mismo nombre en el bloque. Ejemplo: 1 val x = 10 2 def f(y: Int)=y+1 3 val result = { 4 val x = f(3) 5 x * x 6 } + x El resultado de la ejecución de este código será 26 1.4. Evaluación en Scala 1.4.1. Evaluación de expresiones Para evaluar una expresión no primitiva: 1. Se toma el operador con mayor prioridad (ver la Sección 1.2.2.2: Prioridad y Asociati- vidad de los operadores « página 11 »), o en caso de que todos los operadores tenga la misma prioridad se toma el operador situado más a la izquierda, 2. Se evalúa sus operandos (de izquierda a derecha), 3. Se aplica el operador a los operandos. Un nombre se evalúa reemplazando el mismo por su definición. El proceso de evaluación fina- liza cuando se obtiene un valor. Ejemplo: (2 ∗ pi) ∗ radius (2 ∗ 3,14159) ∗ radius 6,28318 ∗ radius Página 18
  • 35. 6,28318 ∗ 10 62,8318 1.4.2. Evaluación de funciones La evaluación de funciones parametrizadas es similar a la de operadores: 1. Se evalúan los parámetros de la función de izquierda a derecha, 2. Se reemplaza la llamada a la función por la definición de la misma y, al mismo tiempo, 3. Se reemplazan los parámetros formales de la función por el valor de sus argumentos. Este sistema de evaluación de expresiones es llamado modelo de sustitución, cuya idea gira en torno a que lo que hace una evaluación es reducir una expresión a su valor y puede ser aplicado a todas las expresiones (siempre y cuando no tengan efectos laterales). El modelo de sustitución está formalizado en el λ-cálculo. Esta estrategia de evaluación es conocida como evaluación estricta o Call by Value. Existe otra alternativa: no evaluar los argumentos antes de reemplazar la función por la definición de la misma, llamada evaluación no estricta o Call by Name. 1.4.3. Sistema de Evaluación de Scala Scala usa evaluación estricta normalmente, pero se puede indicar explícitamente que se desea que algún argumento de una función use evaluación no estricta. Para esto último, se pondrá => delante del tipo del parámetro que se desee evaluar siguiendo esta estrategia. Ejemplo: 1 def miConst (x : Int, y: =>Int) = x Algoritmo 1.2: Función con dos parámetros. El primero es evaluado por valor y el segundo por nombre 1.4.3.1. Valores de las definiciones Anteriormente se ha dicho que los parámetros de las funciones pueden ser pasados por valor (evaluación estricta) o por nombre (evaluación no estricta). La misma distinción se puede aplicar a las definiciones. La forma def es por nombre (eva- luación no estricta) y la forma val es por valor (evaluación estricta). 1.4.3.2. Evaluación de Booleanos En la tabla 1.10 aparecen representadas las reglas de reducción para expresiones booleanas. Página 19
  • 36. !true → false !false → true true && e → e false && e → false true || e → true false || e → e Tabla 1.10: Reglas de reducción para expresiones booleanas En la tabla 1.10 se puede apreciar que && y || no siempre necesitan que el operando derecho sea evaluado. En estos casos, se dirá que estas expresiones usan una evaluación en cortocir- cuito. 1.4.4. Ámbito y visibilidad de las variables El funcionamiento de los ámbitos en Scala es el siguiente: Una invocación a una función crea un nuevo ámbito en el que se evalúa el cuerpo de la función. Es el ámbito de evaluación de la función. El ámbito de evaluación se crea dentro del ámbito en el que se definió la función a la que se invoca. Los argumentos de la función son variables no mutables locales de este nuevo ámbito que quedan ligadas a los parámetros que se utilizan en la llamada. En el nuevo ámbito se pueden definir variables locales. En el nuevo ámbito se pueden obtener el valor de variables del ámbito padre. Primer ejemplo: Supongamos el siguiente código en Scala: 1 def f(x: Int, y: Int): Int = { 2 val z = 5 3 x+y+z 4 } 5 def g(z: Int): Int = { 6 val x = 10 7 z+x 8 } 9 f(g(3),g(5)) Se definen dos funciones f y g, dentro de cada una de las cuales hay declaradas distintas variables locales. Las funciones f y g devuelven una suma de los parámetros con la variable local. En la última línea de código se realiza una invocación a f con los resultados devueltos por dos invocaciones a g . ¿Cuántos ámbitos locales se crean? ¿En qué orden? 1. En primer lugar se realizan las invocaciones a g . Cada una crea un ámbito local en el que se evalúa la función. Las invocaciones devuelven 13 y 15, respectivamente. Página 20
  • 37. 2. Después se realiza la invocación a f con esos valores 13 y 15. Esta invocación vuelve a crear un ámbito local en el que se evalúa la expresión x+y+z , devolviendo 33. Por ahora, todo es bastante normal. La diversión empezará cuando se construyan funciones anónimas durante la ejecución de otras funciones, lo cual permitirá la definición de cierres. 1.5. Estructuras de control en Scala Las estructuras de control permiten controlar el flujo de ejecución de las sentencias de un programa, método, bloque, etc. Siguiendo el flujo natural, las sentencias que componen un pro- grama se ejecutan secuencialmente una tras de otra según estén definidas13 (primero se ejecutará la primera sentencia, después la segunda ..., y así hasta la última sentencia). Las sentencias de control de flujo se emplean en los programas para ejecutar sentencias condicionalmente, repetir un conjunto de sentencias o, en general, cambiar el flujo secuencial de ejecución. Las estructuras de control se dividen en tres grandes categorías en función del flujo de ejecución: Estructuras secuenciales Estructuras condicionales Estructuras iterativas Hasta el momento se ha visto el flujo secuencial. Cada una de la sentencias que se utilizan en Scala están separadas por el carácter punto y coma (véase el uso del carácter punto y coma en Scala en la Subsección 1.2.3: Uso del carácter punto y coma (;) en Scala « página 17 »). El uso de estructuras secuenciales puede ser suficiente para la resolución de programas sencillos, pero para la resolución de programas de tipo general se necesitará controlar las sentencias que se ejecutan haciendo uso de estructuras condicionales e iterativas. 1.5.1. Estructuras condicionales Se utilizarán las estructuras condicionales para determinar si un bloque debe de ser ejecu- tado o no, en función de una condición lógica o booleana. 1.5.1.1. La sentencia if Una sentencia if consiste en una expresión booleana seguida de un bloque de expresiones. Si la expresión booleana se evalúa a cierta, entonces se ejecutará el bloque de expresiones. En caso contrario no se ejecutará el bloque de expresiones. La sintaxis de una sentencia if es: 1 if (expresion_booleana) { 2 // Sentencias que se ejecutaran si la 3 // expresion booleana se evalua a true 4 } Algoritmo 1.3: Sintaxis sentencia if Ejemplo: 13 Cuando se escribe un programa, se introduce la secuencia de sentencias dentro de un archivo. Sin sentencias de control del flujo, el intérprete ejecuta las sentencias conforme aparecen en el programa de principio a fin. Página 21
  • 38. 1 def mayorQueCinco(x: Int) = if (x > 5) { println(" El argumento ",x " es mayor que 5");} Algoritmo 1.4: Expresión condicional if La sentencia if / else La sentencia if puede ir seguida de la una declaración else y un bloque de expresiones. El bloque de expresiones que sigue a la declaración else se ejecutará en el caso de que la expresión booleana del if sea falsa. Scala tiene la expresión condicional if-else para expresar la elección entre dos alternativas (parecida a if-else de Java pero usada para expresiones, no para instrucciones). La sintaxis de una sentencia if / else es: 1 if (expresion_booleana) { 2 // Sentencias que se ejecutaran si la 3 // expresion booleana se evalua a true 4 } else { 5 // Sentencias que se ejecutaran si la 6 // expresion booleana se evalua a false 7 } Algoritmo 1.5: Sintaxis sentencia if / else Ejemplo: 1 def abs(x: Int) = if (x > 0) x else -x Algoritmo 1.6: Expresiones condicionales. Función valor absoluto La sentencia if...else if ...else La expresión if puede ir seguida por una declaración else if, lo cual nos será de gran ayuda para testear varias opciones haciendo uso de una única sentencia if. Cuando se haga uso de la sentencia if...else if...else habrá que tener en cuenta algunos puntos importantes: Una sentencia if podrá tener cero o una declaración else, la cual estará declarada siempre al final de una sentencia if. Una sentencia if podrá tener cero o varias declaraciones else if, y siempre deberán prece- der a la declaración else (si hubiera). Si la evaluación de la expresión booleana de la sentencia if es true, ninguna de las otras expresiones booleanas de las declaraciones else if serán evaluadas, y tampoco se ejecutará el bloque de la declaración else. En el algoritmo Algoritmo 1.7: Sintaxis sentencia if / else if / else « página 23 » se muestra la sintaxis de una sentencia if / else if / else. Página 22
  • 39. 1 if (expresion_booleana1) { 2 // Sentencias que se ejecutaran si la 3 // expresion booleana 1 se evalua a true 4 } else if (expresion_boolena2){ 5 // Sentencias que se ejecutaran si la 6 // expresion booleana 2 se evalua a true 7 } else if (expresion_boolena3){ 8 // Sentencias que se ejecutaran si la 9 // expresion booleana 3 se evalua a true 10 } 11 else { 12 // Sentencias que se ejecutaran si ninguna 13 // de las anteriores expresiones booleanas 14 // se evaluan a true 15 } Algoritmo 1.7: Sintaxis sentencia if / else if / else Estructuras condicionales anidadas Scala permite que las sentencias if, if/else, if/else if/else se puedan anidar. Ejemplo: 1 var x = 30; 2 var y = 10; 3 4 if( x == 30 ){ 5 if( y == 10 ){ 6 println("X = 30 and Y = 10"); 7 } 8 } Algoritmo 1.8: Sentencias if anidadas 1.5.2. Estructuras iterativas Hasta el momento se ha visto como el uso de la sentencia condicional permite dejar de ejecutar algunas sentencias dispuestas en un programa, en función del resultado de la evaluación de una expresión booleana. Pero el flujo del programa, en cualquier caso, siempre avanza hacia adelante y nunca se vuelve a ejecutar una sentencia ejecutada anteriormente. Las sentencias iterativas permitirán iterar un bloque de sentencias, es decir, ejecutar un bloque de sentencias mientras la condición especificada sea cierta. A este tipo de sentencias se les denomina bucles y al bloque de sentencias se les denominará cuerpo del bucle. En la tabla 1.11 se puede observar los diferentes tipos de bucles que presenta Scala para dar respuesta a las necesidades de iteración de los programadores. Página 23
  • 40. Bucle Descripción while Repite una sentencia o un bloque de sentencias siempre que la condición sea verdadera. Se evalúa la condición antes de ejecutar el cuerpo del bucle do...while Igual que el bucle while pero la evaluación de la condición se produce después de la ejecución del bucle. for El cuerpo del bucle se ejecuta un número determinado de veces. Presenta un sintaxis que abrevia el código que maneja la variable del bucle. Tabla 1.11: Tipos de bucles en Scala 1.5.2.1. Bucles while Un bucle while repetirá sucesivamente un bloque de sentencias mientras la condición da- da sea cierta. Un bucle while tiene una condición de control o expresión lógica que ha de ir encerrada entre paréntesis y es la encargada de controlar la secuencia de repetición. El punto clave de los bucles while es el hecho de que la evaluación de la condición se realiza antes de que se ejecute el cuerpo del bucle, por lo que si la condición se evalúa a falsa, el cuerpo del bucle no se ejecutaría ninguna vez. La sintaxis de un bucle while es: 1 while (condicion) { 2 // Sentencias que se ejecutaran mientras 3 // la condicion sea cierta 4 } Algoritmo 1.9: Sintaxis bucles while Hay que hacer notar que si la condición es cierta inicialmente, la sentencia while no termi- nará nunca (bucle infinito) a menos que en el cuerpo de la misma se modifique de alguna forma la condición de control del bucle. Ejemplo: 1 def printUntil(x: Int) = { 2 //variable local 3 var s = 0; 4 while (s <= x){ 5 println("Valor: " + s); 6 s += 1; 7 } 8 } Algoritmo 1.10: Ejemplo de bucle while. 1.5.2.2. Bucles do...while Al igual que los bucles while, los bucles do...while repetirán un bloque de sentencias hasta que la condición de control del bucle sea falsa. Por tanto, al igual que en el bucle while, el cuerpo del bucle se ejecuta mientras la expresión lógica sea cierta. Los bucles do...while también se denominan post-prueba ya que, a diferencia de los bucles while, los bucles do...while evalúan Página 24
  • 41. la condición después de ejecutar el cuerpo del bucle, motivo por el cual este tipo de bucles garantizan la ejecución del cuerpo del bucle al menos una vez. La sintaxis de un bucle while es: 1 do { 2 // Sentencias que se ejecutaran mientras 3 // la condicion sea cierta 4 } while (condicion); Algoritmo 1.11: Sintaxis bucles do... while. Se puede observar que la expresión lógica o booleana aparece al final del bucle por lo que el cuerpo del bucle se ejecutará al menos una vez. Si la condición se evalúa a cierta, el flujo de control saltará hasta la declaración do y el cuerpo del bucle se ejecutará nuevamente. Este proceso se repetirá hasta que la condición se evalúe a falsa. Ejemplo: 1 def printUntil2(x: Int) = { 2 //variable local 3 var s = 0; 4 do { 5 println("Valor: " + s); 6 s += 1; 7 }while (s <= x) 8 } Algoritmo 1.12: Ejemplo de bucle do... while. 1.5.2.3. Bucles for Los bucles for son sentencias que nos permitirán escribir eficientemente bucles que ten- gan que repetirse un número determinado de veces. En Scala podemos encontrar las siguientes variantes de bucles for: Bucles for con rangos Bucles for con colecciones Bucles for con filtros A continuación se verán los conceptos básicos de este tipo de bucles aunque, la especificidad de su implementación en Scala y su utilidad harán, al contrario de lo que se podría imaginar, que este tipo de bucles sean también muy útiles en programación funcional, capítulo en el cual se estudiarán con mayor profundidad (véase el Capítulo 3: Programación Funcional en Scala « página 53 »). Bucles for con rangos La forma más fácil de definir bucles for es haciendo uso del tipo de datos Range definido en Scala. El bucle se repetirá tantas veces como valores contenga el tipo de datos Range dado (véase la Subsubsección 3.11.3.1: Definición de rangos en Scala. La clase Range « página 107 »). La sintaxis más simple de bucles for con rangos es: Página 25
  • 42. 1 for (a <- Range) { 2 // Sentencias que se ejecutaran tantas veces 3 // como valores contenga el tipo de datos 4 // Range dado 5 } Algoritmo 1.13: Sintaxis bucles for con rangos. Range puede ser un rango de números, normalmente representado de la forma i to j o i until j. Más adelante veremos este tipo de datos en mayor profundidad. El operador flecha izquierda (<-) recibe el nombre de generador, ya que es el encargado de generar los diferentes valores del rango. La variable de control de bucle (a) se inicializará con el primer valor del rango e irá tomando, en cada iteración, los diferentes valores del mismo. Ejemplo: 1 def printUntil3(x: Int) = { 2 //variable local 3 for (a <- 0 to x) { 4 println("Valor: " + a); 5 } 6 } Algoritmo 1.14: Ejemplo de bucle for con rangos. Dentro de los bucles for se pueden utilizar varios rangos, separando cada uno de ellos por el carácter punto y coma (;). En este caso el bucle iterará sobre todas las posibles combinaciones de los mismos. Ejemplo: scala> for (x<- 1 to 3;y<- 4 to 6) {println("Valor x: "+x+" Valor y: "+y)} Valor x: 1 Valor y: 4 Valor x: 1 Valor y: 5 Valor x: 1 Valor y: 6 Valor x: 2 Valor y: 4 Valor x: 2 Valor y: 5 Valor x: 2 Valor y: 6 Valor x: 3 Valor y: 4 Valor x: 3 Valor y: 5 Valor x: 3 Valor y: 6 Bucles for con colecciones Los bucles for sirven para recorrer fácilmente los elementos de una colección. La sintaxis de los bucles for con colecciones es: 1 for (a <- Collection) { 2 // Sentencias que se ejecutaran tantas veces 3 // como valores contenga el tipo de datos 4 // Collection dado 5 } Algoritmo 1.15: Sintaxis bucles for con colecciones Se puede iterar sobre los elementos de las distintas estructuras de datos definidas en la librería Scala.collection de Scala como listas, conjuntos,etc., así como sobre los tipos de datos Página 26
  • 43. creados por el usuario14 La variable de control de bucle (a) se inicializará con el primer valor de la colección e irá tomando el valor de los distintos elementos que componen la colección en las sucesivas iteraciones del bucle. Ejemplo: 1 def forLista = { 2 val numList = List(0,1,2,3,4,5,6,7,8,9,10); 3 for( a <- numList ){ 4 println( "Valor de a: " + a ); 5 } 6 } Algoritmo 1.16: Ejemplo bucle for con colecciones. En el ejemplo del algoritmo 1.16 se puede apreciar un bucle for recorriendo una colección de números. Se ha creado esta colección usando el tipo de datos List de Scala. Las colecciones en Scala se estudiarán en mayor profundidad en la Sección 3.11: Colecciones en Scala « página 102 ». Bucles for con filtros Los bucles for en Scala permiten emplear filtros para descartar la iteración sobre algunos elementos de la colección que se desea iterar (rangos, listas, conjuntos,...) que no cumplan con alguna propiedad. Para filtrar elementos se empleará una o más sentencias if. La sintaxis de los bucles for con filtros es: 1 for (a <- Collection|Range... 2 if condicion1; if condicion2...) { 3 // Sentencias que se ejecutaran tantas veces 4 // como valores del tipo de datos 5 // Collection|Range dado satisfagan los filtros 6 } Algoritmo 1.17: Sintaxis bucles for con filtros. La variable de control de bucle (a) se inicializará con el primer valor de la colección/rango que satisfaga las condiciones: condicion1 y condicion2, e irá tomando el valor de los distintos elementos que componen la colección y que también satisfagan las condiciones impuestas como filtros. 1 def forListaconFiltros = { 2 val numList = List(0,1,2,3,4,5,6,7,8,9,10); 3 for( a <- numList 4 if a <= 5; 5 if a != 3 ){ 6 println( "Valor de a: " + a ); 7 } 8 } Algoritmo 1.18: Ejemplo bucle for con filtros 14 Estos tipos de datos creados por el usuario deberán presentar unas características especiales que serán estudia- das con mayor detalle. Página 27
  • 44. En el ejemplo del algoritmo 1.18 se recorren los elementos de la lista numList que sean menores o iguales a 5 y distintos de 3. Bucles for con yield. Los bucles for en Scala permiten almacenar los resultados de un bucle for en una variable o que éstos sean el valor devuelto por una función. Para hacer esto se añadirá la palabra reservada yield al final del cuerpo del bucle. La sintaxis general de los bucles for con yield es: 1 for (a <- Collection|Range... 2 if condicion1; if condicion2...) { 3 // Sentencias que se ejecutaran tantas veces 4 // como valores del tipo de datos 5 // Collection dado satisfagan los filtros 6 }yield a Algoritmo 1.19: Sintaxis bucles for con yield. Yield generará un valor en cada iteración del bucle que será recordado por el bucle for15 . Cuando el bucle finalice, devolverá una colección del mismo tipo de datos que la colección que estamos iterando con los valores recordados. En los bucles for con yield se podrá también hacer uso de filtros, haciendo de estos una herramienta mucho más potente. En el algoritmo 1.20 podemos ver un ejemplo del uso de bucles for con yield y filtros. 1 def forListaconYield = { 2 val numList = List(0,1,2,3,4,5,6,7,8,9,10); 3 val lista= for( a <- numList 4 if a <= 5; 5 if a != 3 )yield a * 2 6 for (b<-lista){println ("Valor recordado por yield: "+b)} 7 } Algoritmo 1.20: Ejemplo bucle for con yield En el algoritmo 1.20 se utilizan dos bucles for. El primero de ellos generará una lista con el doble de los números de la lista numList que satisfagan los filtros del bucle. El segundo bucle for iterará sobre la lista resultante del primer bucle, imprimiendo los valores por pantalla. 1.6. Interacción con Java Una de las características más importantes de Scala es que hace muy fácil la interacción con el código escrito en Java. Todas las clases de la librería java.lang son importadas por de- fecto, mientras que las otras necesitan ser importadas explícitamente. Scala hace muy fácil la interactuación con código Java. Como se verá a lo largo de los próximos capítulos, Scala es un lenguaje de programación que ha sabido integrar algunas de las principales características presentes en los lenguajes de programación más populares. Java no es la excepción, y comparte muchas cosas con éste. La diferencia que se puede ver es que para cada uno de los conceptos de Java, Scala los aumenta, 15 Como si el bucle for tuviera un buffer que no se puede ver y al que en cada iteración se le añade un elemento Página 28
  • 45. refina y mejora. Poder aprender todas las características de Scala nos equipa con más y mejores herramientas a la hora de escribir nuestros programas. También es posible heredar de clases Java e implementar interfaces Java directamente en Scala. A continuación se presenta un ejemplo que demuestra esto. Se quiere obtener y formatear la fecha actual de acuerdo a convenciones utilizadas en un país específico, por ejemplo Francia. Las librerías de clases de Java definen clases de utilidades interesantes, como Date y Da- teFormat. Ya que Scala interacciona fácilmente con Java, no es necesario implementar estas clases equivalentes en las librerías de Scala, se pueden simplemente importar las clases de los correspondientes paquetes de Java: 1 import java.util.{Date, Locale} 2 import java.text.DateFormat._ 3 object FrenchDate { 4 def main(args: Array[String]) { 5 val ahora = new Date 6 val df = getDateInstance(LONG, Locale.FRANCE) 7 println(df format ahora) 8 } 9 } Algoritmo 1.21: Fecha actual formateada. Las declaraciones de importación de Scala parecen muy similares a las de Java, sin embargo, las primeras son bastante más potentes. Se pueden importar múltiples clases desde el mismo paquete al encerrarlas entre llaves, como se muestra en la primer linea. Otra diferencia es que se pueden importar todos los nombres de un paquete o clase, utilizando el carácter guión bajo (_) en lugar del asterisco (*). Eso es porque el asterisco es un identificador válido en Scala (por ejemplo se puede nombrar a un método). Por tanto, la declaración import en la segunda línea, importa todos los miembros de la clase DateFormat. Esto hace que el método estático getDateInstance y el campo estático LONG sean directamente visibles. Dentro del método main, primero se crea una instancia de la clase Date que por defecto con- tiene la fecha actual. A continuación se define un formateador de fechas, utilizando el método estático getDateInstance que ha sido importado previamente. Finalmente, se imprime la fecha actual formateada de acuerdo a la instancia de DateFormat que fue “localizada”. Esta última línea muestra un ejemplo de un método que toma un solo argumento y que ha sido utilizado como operador infijo (véase la Subsubsección 1.2.2.2: Operadores « página 10 »). Es decir, la expresión: df format ahora es solamente otra manera más corta de escribir la expresión: df.format(ahora) 1.6.1. Ejecución sobre la JVM Una de las características más relevantes 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 de Java y Página 29
  • 46. su ecosistema (herramientas, librerías, IDE,...), 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. El hecho de ser un lenguaje basado en la JVM también es la consecuencia por la que al- gunos tipos de la JVM (como vectores) acaban siendo poco elegantes en Scala o que ciertas características, como la recursividad de cola, no están implementadas en la JVM y hay que simularlas. 1.7. Ejercicios Ejercicio 1. Supongamos el siguiente código en Scala: 1 val x = 10 2 val y = 20 3 def g(y: Int): Int = { 4 x+y 5 } 6 def prueba(z: Int): Int = { 7 val x = 0 8 g(x+y+z) 9 } 10 prueba(3) ¿Qué devuelve prueba(3)? ¿En qué ámbito se evalúa la expresión x+y+z ? ¿Y la expresión x+y ? ¿Qué valores tienen esas variables en el momento de la evaluación? Página 30
  • 47. Capítulo 2 Programación Orientada a Objetos en Scala 2.1. Introducción a la programación orientada a objetos en Scala 2.1.1. Características principales de la programación orientada a objetos La popularidad de lenguajes como Java, C# o Ruby han hecho que la POO sea un paradigma ampliamente aceptado entre la mayoría de desarrolladores. Aunque existen numerosos lengua- jes 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 el envío de mensajes a objetos. Todos los tipos definidos por el usuario son objetos. 2.1.2. Scala como lenguaje orientado a 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)1 . De manera adicional a todas las características puras de un lenguaje orientado a objetos presentadas anteriormente, Scala añade algunas innovaciones en el espacio de los lenguajes orientados a objetos: 1 http://en.wikipedia.org/wiki/Smalltalk Página 31
  • 48. Composición modular de los elementos mezclados (mixin). Mecanismo que permite la composición de clases para el diseño de componentes reutilizables, evitando los pro- blemas 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 rasgos mezclados no dependen de ningún método y/o atributo de aque- llas 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-type. Abstracción de tipos. Existen dos mecanismos principales de abstracción en los lengua- jes de programación: la parametrización y los miembros abstractos. Scala soporta ambos estilos de abstracción de manera uniforme para tipos y valores. 2.2. Paquetes, clases, objetos y namespaces 2.2.1. Objetos Singleton Scala no soporta la definición de atributos estáticos en las clases, incorporando en su lugar el concepto de objeto singleton. 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 com- panion object (objeto acompañante), mientras que la clase se denomina companion class (clase acompañante) del objeto singleton. Inicialmente, sobre todo aquellos desarrolladores provenien- tes 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 objetos singleton y las clases es que los pri- meros no aceptan parámetros (no podemos instanciar un objeto singleton mediante la palabra reservada new), mientras que las clases sí lo permiten. Cada uno de los objetos singleton es implementado mediante una instancia de una clase synthetic 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.2.2. Módulos, objetos, paquetes y namespaces Si se quiere hacer referencia a un método declarado dentro de un objeto (object) se tendrá que hacer una llamada del tipo nombreObjeto.nombreMétodo(parámetros) ya que el método nombreMétodo habrá sido definido dentro del objeto nombreObjeto. En realidad, en Scala todos los valores serán objetos. El principal objetivo de un objeto es el de otorgar un namespace a sus miembros, algunas veces llamado módulo. Un objeto puede constar de cero o más miembros. Un miembro puede ser un método decla- rado con la palabra reservada def, o puede ser otro objeto declarado con las palabras reservadas val o object2 Para hacer referencia a los miembros de un objeto en Scala se utilizará la notación típica de la POO (se indicará el namespace del objeto seguido de un puto y del nombre del miembro). Ejemplo: MyModule.abs(42) 2 Los objetos también pueden contener otros tipos de objetos, que no se mencionarán en este capítulo. Página 32
  • 49. Si se observa la expresión 3 * 2, lo que se está haciendo realmente es llamar al miembro (método) * del objeto 3, es decir (3.*(2)). En general, los métodos que toman un solo argumento pueden ser usados con una sintaxis de infijo3 . De este modo, se puede comprobar como la llamada a MyModule abs 42 devolvería la misma salida que la llamada MyModule.abs(42). Es posible importar miembros de un namespace haciendo uso de la palabra reservada import. Ejemplo: import MyModule.abs Para importar todos los miembros de un namespace se usará el guión bajo. Ejemplo: import MyModule._4 En Scala, al igual que en Java, se puede utilizar la palabra reservada package, la cual creará un paquete (en realidad se crea un namespace). Por tanto, se puede crear un namespace tanto con object como con package pero si se utiliza package no se creará un objeto, por lo que, obviamente, no se podrá pasar como tal en otras llamadas. Además en un package no podrá haber definiciones de miembros usando las palabras reservadas val o def. 2.2.3. Clases Del mismo modo que en todos los lenguajes orientados a objetos, Scala permite la defini- ción de clases en las que se pueden añadir métodos y atributos. A continuación se muestra la definición de la clase MiPrimeraClase. 1 class MiPrimeraClase{ 2 val a = 1 3 } Algoritmo 2.1: Mi primera clase Si se quiere instanciar un objeto de la clase anterior habrá que hacer uso de la palabra reservada new. 1 val v = new MiPrimeraClase Cuando se defina una variable en Scala se tendrá que especificar la mutabilidad de la misma, pudiendo escoger entre: 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, se podrá indicar que una variable es de tipo var, consiguiendo con esto que su valor pueda ser modificado durante todo su ciclo de vida. Uno de los principales mecanismos utilizados para garantizar la robustez de un objeto es la afirmación de 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 en la misma clase, todo el código que podría modificar el estado del mismo estará localizado en dicha clase.5 3 En estos casos, en Scala podremos omitir el uso del punto y los paréntesis. Para más información, véase la Subsubsección 1.2.2.2: Operadores « página 10 » 4 Véase el algoritmo 1.21 (página 29) donde se podrán ver otros ejemplos de las declaraciones de importación en Scala 5 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. Página 33
  • 50. El siguiente paso será incorporar funcionalidad a la clase que se ha creado. Para ello se pueden definir métodos mediante el uso de la palabra reservada def: 1 class MiPrimeraClase{ 2 var a = 1 3 def suma(b:Byte):Unit={ 4 a += b 5 } 6 } Una característica importante de los métodos en Scala es que todos los parámetros son inmutables, es decir, vals. Por tanto, si se intenta modificar el valor de un parámetro en el cuerpo de un método se obtendrá un error del compilación: 1 def sumaNoCompila(b:Byte) : Unit = { 2 b = 1 // Esto no compilara puesto que el 3 // parametro b es de tipo val 4 a += b 5 } Algoritmo 2.2: Método suma que no compila Los métodos presentan otra característica común entre los lenguajes orientados a objetos como Java y es la asignación dinámica de métodos (dynamic method dispatch), lo que significa que el código resultante de la llamada a un método depende del tipo del objeto que contiene el método, algo que se determinará en tiempo de ejecución. Otro aspecto relevante que se puede destacar 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 está compuesto por una única expresión se puede prescindir del uso 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: 1 class MiPrimeraClase { 2 private var sum = 0 3 def add(b:Byte) { sum += b } 4 } Algoritmo 2.3: MiPrimeraClase con método suma 2.2.4. Objetos funcionales A continuación se analizará cómo se pueden construir objetos funcionales, es decir, inmu- tables, mediante la definición de clases, algo que permitirá profundizar en cómo los aspectos funcionales y los de orientación a objetos confluyen en el lenguaje. Página 34
  • 51. Números Racionales Los números racionales son aquellos que pueden ser expresados como un cociente n d . Algu- nas de sus características principales son: 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 ope- rando que aparece a la derecha y posteriormente se realiza una operación de multiplica- ción. 2.2.4.1. Constructores Puesto que se ha decidido que en la solución que se va a realizar los números racionales sean inmutables, se necesitará que los clientes de esta clase proporcionen toda la información en el momento de creación de un objeto. Se podría comenzar el diseño del siguiente modo: 1 class Rational (n:Int,d:Int) Los parámetros definidos tras el nombre de la clase son conocidos como parámetros de clase. El compilador generará un constructor primario en cuya signatura aparecerán los dos pa- rámetros escritos en la definición de la clase. Cualquier código que sea escrito dentro del cuerpo de la clase que no forme parte de un atributo o de un método será incluido en el constructor pri- mario indicado anteriormente. 2.2.4.2. Sobrescritura de métodos Si se quisiera sobrescribir un método heredado de una clase padre en la jerarquía se tendría que hacer uso de la palabra reservada override. Por ejemplo, si en la clase Rational se deseará sobrescribir la implementación por defecto del método toString se podría actuar del siguiente modo: 1 override def toString = n + "/" + d 2.2.4.3. Precondiciones Una de las características de los números racionales es que no admiten el valor cero co- mo denominador aunque, sin embargo, con la definición actual de la clase Rational se podría escribir código como: 1 new Rational(11,0) algo que violaría la definición actual de números racionales, dado que se está construyendo una clase inmutable y toda la información debe estar disponible en el momento que se invoca al constructor. Este último deberá asegurarse de que el denominador indicado no toma el valor cero (0). Página 35
  • 52. La mejor aproximación para resolver este problema pasa por hacer uso de las precondicio- nes. Este concepto, incluido en el lenguaje, representa un conjunto de restricciones que pueden establecerse sobre los valores pasados a métodos o constructores y que deben ser satisfechas por el cliente que realiza la llamada del método/constructor: 1 class Rational(n: Int, d: Int) { 2 require(d != 0) 3 override def toString = n +"/"+ d 4 } Las restricciones se establecen mediante el uso del método require, el cual espera un argu- mento booleano. En caso de que la condición exigida no se cumpla, el método require disparará una excepción de tipo IllegalArgumentException. 2.2.4.4. Atributos y Métodos A continuación, se definirá en la clase Rational un método público que reciba un número racional como parámetro y retorne como resultado la suma de ambos operandos. Puesto que se está construyendo una clase inmutable, el nuevo método deberá devolver la suma en un nuevo número racional: 1 class Rational(n: Int, d: Int) { 2 require(d != 0) 3 override def toString = n +"/"+ d 4 // no compila: no podemos hacer that.d o that.n 5 // deben definirse como atributos 6 def add(that: Rational): Rational = 7 new Rational(n * that.d + that.n * d, d * that.d) 8 } El código anterior muestra una primera aproximación a la solución aunque incorrecta, dado que se producirá un error de compilación. Aunque los parámetros de clase n y d están el ámbito del método add, sólo se puede acceder a su valor en el objeto sobre el que se realiza la llamada. Para resolver el problema planteado en el fragmento de código anterior se tendrán que declarar d y n como atributos de la clase Rational: 1 class Rational(n: Int, d: Int) { 2 require(d != 0) 3 val numer: Int = n // declaracion de atributos 4 val denom: Int = d 5 override def toString = numer +"/"+ denom 6 def add(that: Rational): Rational = 7 new Rational(numer * that.denom + that.numer * denom, denom * that.denom) 8 } Algoritmo 2.4: Números racionales Nótese que en los fragmentos de código anteriores se está manteniendo la inmutabilidad del diseño. En este caso, el operador de adición add devuelve 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. Página 36
  • 53. A continuación se incorporará a la clase Rational un método privado que determinará el máximo común divisor de dos números enteros: 1 private def gcd(a:Int,b:Int):Int = if(b == 0) a else gcd(b,a %b) Algoritmo 2.5: Cálculo del máximo común divisor de dos enteros El código anterior muestra cómo se puede definir un método privado dentro de una clase en Scala haciendo uso de private. En este caso, el método gcd se utilizará como método auxiliar para calcular el máximo común divisor de dos números enteros. 2.2.4.5. Operadores La implementación actual de la clase Rational es correcta, aunque podría haber sido defi- nida de modo que su uso resultara mucho más intuitivo. Una de las posibles mejoras que se podrían introducir sería la inclusión de los operadores comúnmente utilizados para la suma y el producto: 1 def + (that: Rational): Rational = new Rational(numer * that.denom + that.numer * denom, denom * that.denom) 2 def * (that: Rational): Rational = new Rational(numer * that.numer, denom * that.denom) De este modo se podría escribir código como el que a continuación se indica: 1 var a = new Rational(2,5) 2 var b = new Rational(1,5) 3 var sum = a + b En el código anterior se aprecia como el método + es usado como un operador en notación infija6 . 2.3. Jerarquía de clases en Scala Una de las diferencias entre Scala y Java, como se adelantó en la Subsección 2.2.2: Módulos, objetos, paquetes y namespaces « página 32 »(página 32), es que Scala es un lenguaje basado en clases y, por tanto, todos los valores7 serán objetos instanciados de alguna clase. Como se observa en la imagen 2.1, en la parte más alta de la jerarquía de clases en Scala se encuentra el tipo Any, que es el tipo base de todos los tipos en Scala. El tipo Any tiene dos subtipos: El tipo AnyVal, para definir las clases que representan un valor (value classes), similar al de los tipos primitivos definidos en lenguajes como Java (Int,Double,...). Las subclases del tipo AnyVal se encuentran predefinidas. El tipo AnyRef. El resto de clases harán referencia a un tipo. Las clases que se definan siempre harán referencia, aunque sea de forma indirecta, a un tipo por defecto, el tipo AnyRef, ya que todas las clases que se definan extenderán de forma implícita del trait scala.ScalaObject. ScalaObject es el alias de java.lang.Object. 6 También podríamos escribir a.+(b) aunque en este caso el código resultante sería mucho menos legible. Para más información, véase la Subsubsección 1.2.2.2: Operadores « página 10 » 7 Incluyendo valores numéricos, funciones... Página 37
  • 54. Figura 2.1: Jerarquía de Clases en Scala En el lado opuesto, en la parte más baja de la jerarquía de clases en Scala, se encuentra el tipo Nothing, subtipo de cualquier otro tipo. No hay valores para el tipo Nothing. Este tipo de datos se utilizará para indicar una terminación anormal o para representar el elemento vacío en nuestros tipos abstractos de datos. El tipo Null es un subtipo de todas las clases que hereden de Object. Por tanto, no será subtipo de los subtipos de AnyVal. También se verá como todas las clases que referencian a un tipo tendrán, por defecto, un valor null de tipo Null[17]. 2.3.1. Herencia en Scala En la POO una forma de extender la funcionalidad de una clase es utilizando herencia de clases. Con este mecanismo se puede extender una clase, agregando nuevos métodos, sin necesidad de recompilar el código existente. La herencia en Scala presenta las mismas limitaciones que en otros lenguajes de POO, como Java. Las clases sólo podrán heredar su comportamiento de, a lo sumo, otra clase. Es decir, una clase sólo podrá extender de otra clase. Si se quisiera crear una clase para representar animales que tuviera una operación para conocer el número de patas del animal, una posible implementación podría ser: 1 abstract class Animal { 2 def patas():Int 3 } Algoritmo 2.6: Clase Abstracta Animal donde la clase Animales es una clase abstracta cuyos miembros pueden no estar implementa- dos, por lo que no se podrá instanciar. Si ahora se quisieran definir las clases Perro y Pajaro, de tipo Animal, habría que exten- der8 de la clase Animal e implementar el método patas, definido en la clase abstracta Animal: 8 Haciendo uso de la palabra reservada extends Página 38
  • 55. 1 class Perro extends Animal{ 2 def patas() = 4 3 } 4 class Pajaro extends Animal{ 5 def patas() = 2 6 } Algoritmo 2.7: Clases Perro y Pajaro de tipo Animal Las clases Perro y Pajaro extienden de la clase Animal, por lo que un objeto de estas clases podrá aparecer en cualquier parte en la que se requiera un objeto de tipo Animal. Se dirá que: Animal es la superclase de Perro y Pajaro. Perro y Pajaro serán subclases de Animal. Se llamarán clases base a las superclases, directas e indirectas, de una clase C. Por tanto, Animal y Object serán las clases base de Perro y Gato. 2.3.1.1. Rasgos y herencia múltiple en Scala En Java, como en Scala, una clase sólo puede heredar los métodos de una superclase. Pero, ¿qué se puede hacer si una clase se ajustara de forma natural a varias clases o se quisiera heredar los métodos de varias clases?. Para resolver estas situaciones se hará uso de los rasgos (traits) de Scala. Las clases, los objetos y los rasgos en Scala pueden heredar sólo de una superclase, pero también pueden heredar el comportamiento de cualquier número de rasgos. Los rasgos son la unidad básica de reutilización de código en Scala. Un rasgo encapsula definiciones de métodos y atributos que pueden ser reutilizados mediante un proceso de mezcla (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 mezcla con un número indefinido de rasgos. 2.3.1.2. Funcionamiento de los rasgos La definición de un rasgo es similar a la de una clase tradicional salvo que se utiliza la palabra reservada trait en lugar de class. 1 trait MiPrimerTrait { 2 def printMessage(){ 3 println("Este es mi primer trait") 4 } 5 } Una vez definido, puede ser “mezclado” junto a una clase mediante el uso de las palabras reservadas extends o with. 1 class MiPrimerMixin extends MiPrimerTrait{ 2 override def toString = "Este es mi primer mixin en Scala" 3 } Página 39
  • 56. Cuando se utiliza la palabra reservada extends para realizar el proceso de mezcla se estará heredando de manera implícita las superclases del rasgo. Los métodos heredados de un rasgo se utilizan del mismo modo que se utilizan los métodos heredados de una clase. De manera adicional, un rasgo también define un tipo. En el caso de que se desee realizar un proceso de mezcla en el que una clase ya indica un padre de manera explicita mediante el uso de extends, se tendrá que utilizar la palabra reservada with. Si se quisieran incluir en el proceso de mezcla múltiples rasgos, únicamente se tendrían que incluir más cláusulas with. En este punto sería posible pensar que los rasgos son como interfaces Java con métodos concretos, pero realmente pueden hacer muchas más cosas. Por ejemplo, los rasgos pueden definir atributos y mantener un estado. Realmente en un rasgo se puede hacer lo mismo que en una definición de clase con una sintaxis similar, aunque existen dos excepciones: Un rasgo no puede tener parámetros de clase (los parámetros pasados al constructor pri- mario de la clase). Mientras que en las clases las llamadas a métodos de clases padre (super.xxx) son en- lazadas de manera estática, en el caso de los rasgos dichas llamadas son enlazadas di- námicamente. Si en una clase se escribe super.método(), se sabrá en todo momento cual será la implementación del método invocada. Sin embargo, el mismo código escrito en un rasgo 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 rasgo y una clase realizan un proceso de mezcla. Este curioso comportamien- to de super es la clave que permite a los rasgos trabajar como stackable modifications (modificaciones apilables), las cuales se verán con mayor detalle a continuación. 2.3.1.3. Rasgos como modificaciones apiladas Ahora se analizará otro de los usos más populares de los rasgos: facilitar modificaciones apilables en las clases. Los rasgos nos permitirán modificar los métodos de una clase y, adi- cionalmente, nos permitirán apilarlas entre sí. Se apilarán modificaciones sobre una cola de números enteros. Dicha cola tendrá dos operaciones: put (que añadirá números a la cola) y get (que sacará los elementos de la cola). Generalmente las colas siguen el comportamiento First In First Out – “primero en entrar, primero en salir” – (FIFO), por lo que el método get tendría que retornar los elementos en el mismo orden en el que fueron introducidos. Dada una clase que implementa el comportamiento descrito en el párrafo anterior, se podría definir un rasgo que llevara a cabo modificaciones como: Multiplicar por dos 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 rasgos anteriores representan modificaciones dado que no definen una cola por sí mismos, sino que llevan a cabo modificaciones sobre la cola subyacente con la que realizan el proceso de mezcla. Los rasgos también son apilables: se podría escoger cualquier subconjunto de los tres anteriores e incorporarlos a una clase, de manera que conseguiríamos una nueva clase con la funcionalidad deseada. El siguiente fragmento de código representa una implementación reducida del comportamiento de una cola FIFO: Página 40
  • 57. 1 import scala.collection.mutable.ArrayBuffer 2 abstract class ColaEnteros { 3 def get(): Int 4 def put(x: Int) 5 } 6 class ColaEnterosBasica extends ColaEnteros { 7 private val buf = new ArrayBuffer[Int] 8 def get() = buf.remove(0) 9 def put(x: Int) { buf += x } 10 } Ahora se realizarán un conjunto de modificaciones sobre la clase anterior. Para ello, se hará uso de los rasgos. El siguiente fragmento de código muestra un rasgo que duplica el valor de un elemento que se desea añadir a la cola: 1 trait Duplicado extends ColaEnteros{ 2 abstract override def put(x:Int) { super.put(2*x) } 3 } Nótese el uso de las palabras reservadas abstract override. Esta combinación de modifica- dores sólo puede ser utilizada en los rasgos y no en las clases, e indica que el rasgo debe ser integrado (mezclado) con una clase que presenta una implementación concreta del método en cuestión. A continuación se muestra un ejemplo de uso del rasgo anterior: scala> class MiCola extends ColaEnterosBasica with Duplicado defined class MiCola scala> val cola = new MiCola cola: MiCola = MiCola@91f017 scala> cola.put(10) scala> cola.get() res12: Int = 20 Para analizar el mecanismo de apilado de modificaciones, se implementarán en primer lugar los dos rasgos restantes que han sido descritos anteriormente: 1 trait Incremento extends ColaEnteros{ 2 abstract override def put(x:Int) { super.put(x + 1) } 3 } 4 trait Filtro extends ColaEnteros{ 5 abstract override def put(x:Int) { if ( x >= 0 ) super.put(x) } 6 } Una vez disponibles las nuevas modificaciones, se podría generar una nueva cola del modo que más nos interese: scala> val cola = (new ColaEnterosBasica with Incremento with Filtro) cola: BasicIntQueue with Incremento with Filtro... scala> cola.put(-1); cola.put(0); cola.put(1) scala> cola.get() res15: Int = 1 scala> cola.get() res16: Int = 2 Página 41
  • 58. El orden en el que se mezclan los rasgos es importante . De manera resumida, cuando se invoca a un método de una clase que presenta modificaciones apiladas, el método del rasgo definido más a la derecha es el primero en ser invocado. Si dicho método invoca a super, éste invocará al rasgo que se encuentra más a la izquierda y así sucesivamente. En el ejemplo ante- rior, el método put del trait Filtro será invocado en primer lugar, por lo que aquellos números menores que cero no serán incorporados a la cola. El método put del trait Incremento sumará el valor uno a cada uno de los números (mayores o iguales que cero). 2.3.1.4. ¿Cuándo usar rasgos? A continuación se presentan algunos criterios que podrán ayudar al programador a determi- nar cuando usar rasgos: Si el comportamiento no pretende ser reutilizado, entonces encapsularlo en una clase. Si el comportamiento pretende ser reutilizado en múltiples clases no relacionadas, enton- ces construir un rasgo (trait). Si se desea que una clase Java herede de dicha funcionalidad, entonces se deberá utilizar una clase abstracta. Si la eficiencia es importante, se debería escoger 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 rasgos son compilados a interfaces y esto podría penalizar el rendimiento. Si tras haber analizado todas las opciones anteriores aún no se tiene claro qué aproxima- ción se desea utilizar, se debería comenzar por el uso de rasgos (traits). Se podrá cambiar en el futuro y, generalmente, se mantienen más opciones abiertas. 2.4. Patrones y clases case Las clases case son un concepto relativamente novedoso. Permiten incorporar el mecanis- mo de concordancia de patrones (pattern matching) sobre objetos sin la necesidad de código repetitivo. De manera general, sólo se tendrá que prefijar la definición de una clase con la pa- labra reservada case para indicar que la clase definida puede ser utilizada en la definición de patrones. 2.4.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 (método de fábrica) con el nombre de la clase. Gracias a esto se podrá 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: 1 val op = BinaryOperation(‘‘+’’, Number(1), v) Página 42
  • 59. Otra funcionalidad sintáctica incorporada por el compilador es que todos los argumentos en la lista de parámetros incorporan de manera implícita el prefijo val, por lo que éstos últimos serán atributos de clase. Por último, pero no por ello 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 gene- rados son un poco más grandes9 y se tendrá 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. 2.4.2. Patrones: estructuras y tipos La estructura general de un patrón en Scala presenta la siguiente estructura: selector match {alternativas} Incorporan un conjunto de alternativas en las que cada una de ellas comienza por la palabra reservada case. Cada una de estas alternativas incorpora un patrón y una o más expresiones que serán evaluadas en caso de que se produzca la concordancia del patrón. Se utiliza el símbolo de flecha (=>) para separar el patrón de las expresiones. Como se ha podido comprobar, la sintaxis de los patrones es sumamente sencilla por lo que, a continuación, se profundizará en los diferentes tipos de patrones que se pueden construir en Scala. 2.4.2.1. Patrones comodín El patrón (_) concuerda con cualquier objeto, por lo que podría ser utilizado como una alternativa catch-all tal y como se muestra en el siguiente ejemplo: 1 expression match { 2 case BinaryOperation(op,leftSide,rightSide) => println(expression + " es una operacion binaria") 3 case _ => 4 } 2.4.2.2. Patrones constantes Un patrón constante concuerda única y exclusivamente consigo mismo. El siguiente frag- mento de código muestra algunos ejemplos de patrones constantes: 1 def describe(x: Any) = x match { 2 case 5 => "cinco" 3 case true => "verdadero" 4 case "hola" => "hi!" 5 case Nil => "Es una lista vacia" 6 case _ => "cualquier otra accion" 7 } 9 Son más grandes porque se generan métodos adicionales y se incorporan atributos implícitos para cada uno de los parámetros del constructor Página 43
  • 60. 2.4.2.3. Patrones variables Un patrón variable concuerda con cualquier objeto, del mismo modo que los patrones co- modín (wildcard). A diferencia de los patrones comodín, Scala enlaza la variable al objeto, por lo que posteriormente se podrá hacer uso de dicha variable para actuar sobre el objeto como se puede apreciar en el siguiente ejemplo: 1 expr match { 2 case 0 => "valor cero" 3 case somethingElse => "no es cero: valor "+ somethingElse 4 } 2.4.2.4. Patrones constructores Son en este tipo de construcciones donde los patrones se convierten en una herramienta muy poderosa. Básicamente están formados por un nombre y un número indefinido de patrones. Asumiendo que el nombre designa una clase de tipo case, este tipo de patrones comprobarán primero si el objeto pertenece a dicha clase para, posteriormente, comprobar si los parámetros del constructor concuerdan con el conjunto de patrones extra indicados. La definición anterior puede no resultar demasiado explicativa, por lo que a continuación se incluye un pequeño ejemplo en el que se realizan tres comprobaciones: Comprueba que el objeto de primer nivel es de tipo BinaryOperation. Si el objeto de primer nivel es del tipo BinaryOperation, se comprobará que su tercer argumento es de tipo Number. Si el tercer argumento del objeto de tipo BinaryOperation es de tipo Number, se verificará que su atributo de clase es 0. En caso de que fallara alguna de las anteriores comprobaciones, coincidiría con el patrón comodín que aparece definido. 1 expr match { 2 case BinaryOperation("+", e, Number(0)) => println("comprobacion de patrones a gran profundidad") 3 case _ => 4 } 2.4.2.5. Patrones de secuencia Se pueden establecer patrones de concordancia sobre listas o vectores del mismo modo que se han definido para las clases. Deberá utilizarse la misma sintaxis, aunque ahora se podrá indicar cualquier número de elementos en el patrón. El siguiente fragmento de código muestra un patrón que comprueba que la expresión coin- cide con una lista del tipo List, compuesta exactamente por tres elementos y cuyo primer valor sea 0: 1 expr match { 2 case List(0, _, _) => println("Concordancia de patrones!") Página 44
  • 61. 3 case _ => 4 } 2.4.2.6. Patrones tipados Se puede utilizar este tipo de construcciones como reemplazo de las comprobaciones y conversiones de tipos: 1 def tamanoGenerico(x: Any) = x match { 2 case s: String => s.length 3 case m: Map[_, _] => m.size 4 case _ => -1 5 } El método tamanoGenerico devolverá la longitud de un objeto cualquiera. El patrón utili- zado en el anterior ejemplo, “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.5. Polimorfismo en Scala Cuando se habla de polimorfismo en programación, se hace referencia a que: El tipo de la clase puede tener instancias de muchos tipos. A una clase o función se le pueden aplicar argumentos de diferentes tipos. Para lo cual se aplicarán principalmente dos técnicas fundamentales: Subtipado. Instancias de una clase podrán ser pasadas a una clase base. Genericidad. Instancias de una función o clase serán creadas parametrizando sus tipos. La primera técnica, el subtipado, relacionada con el poliformismo en el paradigma de la POO, se corresponde con el concepto de herencia visto en la Subsección 2.3.1: Herencia en Scala « página 38 ». La segunda técnica, la genericidad, se corresponde con la abstracción de los tipos en clases y funciones, generalizando las mismas. En este caso, el término genericidad es más común- mente utilizando dentro del paradigma de la programación funcional y está relacionado con la abstracción de tipos en funciones para crear funciones polimórficas. Cuando se generaliza el uso de las clases, algo relacionado con la POO, aunque se puede hablar de genericidad, es más habitual referirse a estas clases como clases parametrizadas, clases genéricas, constructores de tipos... A continuación, se pondrá de manifiesto la importancia de la abstracción de tipos en las clases implementando una versión muy simple de lista inmutable enlazada para los números enteros, que podrá ser construida como: Nil. Que representará la lista vacía. Cons. Que contendrá un elemento y el resto de la lista. Con estas indicaciones ya se podría comenzar a crear la primera lista de enteros: Página 45
  • 62. 1 trait ListaInt 2 class Cons (val cabeza:Int, val cola:ListaInt) extends ListaInt 3 class Nil extends ListaInt Algoritmo 2.8: Lista de Enteros Según se ha definido la lista de enteros, ésta podrá ser: Una lista vacía, new Nil Un elemento de la lista, new Cons(x,xs)10 , compuesto por un elemento x de tipo Int (que será la cabeza de la lista) y un elemento xs (la cola de la lista, que tendrá que ser de tipo ListaInt). Ahora también se necesitará definir una lista inmutable enlazada para valores booleanos que siga las mimas especificaciones que la anterior lista: 1 trait ListaBool 2 class Cons (val cabeza:Bool, val cola:ListaBool) extends ListaBool 3 class Nil extends ListaBool Algoritmo 2.9: Lista de Booleanos Si se analizan los algoritmos 2.8 y 2.9, rápidamente se observa que las diferencias entre ambos están relacionadas con los tipos de datos que pueden contener cada una de las listas. Llegados a este punto, si se quisiera crear una lista de Double, Float,...sería necesario crear una nueva clase para cada tipo. Esto provocaría que nuestro código final fuera muy extenso, algo que dificultaría el mantenimiento del mismo ya que si, por ejemplo, se quisiera dotar de alguna funcionalidad extra a las listas, habría que buscar y modificar una gran cantidad de implementaciones de listas11 . Otra consecuencia relacionada con la extensión del código será el hecho de que aumentaría notablemente la posibilidad de que éste contenga errores. Además, no se estaría facilitando de ninguna forma la escalabilidad de nuestra solución. Si se vuelven a observar los algoritmos 2.8 y 2.9, se puede comprobar que el comporta- miento de ambas listas es similar y si se intentara abstraer el tipo de datos, lo que diferencia a ambas, sería posible tener una única clase que valdría tanto para los tipos Int, Boolean, como para cualquier otro tipo de datos definido. Se podrán generalizar las clases definidas anteriormente parametrizando el tipo. Para ello junto al nombre de la clase o rasgo, se indicará el nombre que se utilizará para hacer referencia a los tipos de datos parametrizados en la clase o rasgo, en una lista separada por comas y encerrada entre corchetes. Por ejemplo: trait TraitParametrizado[T,U,S] //Trait con tres tipos de datos //parametrizados:T,U y S Entonces, en el algoritmo 2.10 se puede ver la implementación de una lista genérica que presente el comportamiento descrito anteriormente para la listas de enteros y booleanos. 1 trait ListaGen[T] //Indicamos que el tipo del trait esta parametrizado. 2 //T sera el nombre del tipo generico. 10 Se ha usado val delante de los parámetros de clase para convertir a los mismos en atributos de clase 11 Concretamente una por cada tipo de datos que nuestra lista pueda contener. Página 46
  • 63. 3 class Cons[T] (val cabeza:T, val cola:ListaGen[T]) extends ListaGen[T] 4 class Nil[T] extends ListaGen[T] Algoritmo 2.10: Lista Genérica Ahora si se quisiera añadir algunas funciones básicas de las listas a la solución anterior, sólo se tendrían que definir las mismas una sola vez para que estén disponibles para booleanos, enteros,...consiguiendo una solución escalable y mucho más fácil de mantener: 1 trait ListaGen[T]{ 2 def isEmpty:Boolean 3 def head:T 4 def tail:ListaGen[T] 5 } 6 class Cons[T](val head:T, val tail:ListaGen[T]) extends ListaGen[T]{ 7 def isEmpty = false 8 } 9 class Nil[T] extends ListaGen[T]{ 10 def isEmpty = true 11 def head = throw new NoSuchElementException("Nil.head") 12 def tail = throw new NoSuchElementException("Nil.tail") 13 } Algoritmo 2.11: Lista Genérica con funciones Al igual que las clases, se pueden generalizar las funciones si se abstrae el tipo de datos. La parametrización de funciones se verá en mayor profundidad en la sección dedicada a las funcio- nes polimórficas, dentro del capítulo dedicada a la programación funcional, en la Sección 3.7: Funciones polimórficas. Genericidad « página 66 ». A continuación se muestra un ejemplo simple, una función que creará una lista con un único elemento pasado como parámetro: 1 def listaUnElemento[T](elem:T):ListaGen[T]=new Cons[T](elem,Nil) Algoritmo 2.12: Ejemplo de función parametrizada 2.5.1. Acotación de tipos y varianza La acotación de tipos y la varianza son conceptos relacionados a las técnicas para definir clases y funciones polimórficas: la genericidad y el subtipado. 2.5.1.1. Acotación de tipos Se pondrá de manifiesto la relevancia de la acotación de tipos definiendo una función son- TodosPositivos para el siguiente tipo ListaInt (basado en el algoritmo 2.8): 1 trait ListaInt 2 case class Cons (val cabeza:Int, val cola:ListaInt) extends ListaInt 3 case object Nil extends ListaInt tal que: Página 47
  • 64. Tenga un parámetro del tipo ListaInt. Devuelva como valor una lista del tipo ListaInt si todos los elementos de la lista son positivos. Lance una excepción en otro caso. En una primera solución, se podría definir la función sonTodosPositivos como se muestra en el algoritmo 2.13. 1 def sonTodosPositivos(xs:ListaInt):ListaInt = xs match{ 2 case Nil => Nil 3 case Cons(h,ys) if h>=0 => Cons(h,sonTodosPositivos(ys)) 4 case _ => throw new Error 5 } Algoritmo 2.13: Función sonTodosPositivos para ListaInt Observando bien la definición anterior, si se invoca sonTodosPositivos con Nil devolverá Nil, mientras que si se invoca la función con una instancia de la clase Cons(h,ys) el valor devuelto será una instancia de la clase Cons. Por tanto, la definición de la función sonTodosPositivos podría haber sido más específica de modo que este comportamiento quedara reflejado. Para es- pecificar este comportamiento se podría haber utilizado una cota superior para el tipo ListaInt, lo que se expresaría: 1 def sonTodosPositivos[S<:ListaInt](xs:S):ListaInt = ... 2 } Algoritmo 2.14: Función sonTodosPositivos para ListaInt con tipo acotado superiormente donde “[S<:ListaInt]” indica que S podrá ser instanciado sólo por los tipos que hereden de ListaInt. Generalmente, estableceremos dos tipos de cotas: 1. “S <: T”, que indicará que S es un subtipo de T. 2. “S >: T”, que indicará que S es un supertipo de T. Del mismo modo es posible establecer una cota inferior. A continuación se muestra un ejemplo de un parámetro acotado inferiormente en la función sonTodosPositivos: 1 def sonTodosPositivos[T >: ListaInt](xs:T):T = ... 2 } Algoritmo 2.15: Función sonTodosPositivos para ListaInt con tipo acotado inferiormente Cuando se introduce la cota inferior “[T >: ListaInt]”, se está introduciendo un parámetro T que sólo podrá variar con los diferentes supertipos de ListaInt. Por tanto T podrá ser de tipo ListaInt, AnyRef o Any, según está definida la jerarquía de clases en Scala (véase la sección Sección 2.3: Jerarquía de clases en Scala « página 37 »). De este modo, una posible definición de la función sonTodosPositivos utilizando tanto una cota superior como una cota inferior sería: 1 def sonTodosPositivos[S <: ListaInt,T >: ListaInt](xs:S):T = xs match{ 2 case Nil => Nil 3 case Cons(h,ys) if h>=0 => Cons(h,sonTodosPositivos(ys)) Página 48
  • 65. 4 case _ => throw new Error 5 } Algoritmo 2.16: Trait Function1 Es posible combinar cota superior y cota inferior restringiendo el tipo a un intervalo, por lo que se podría encontrar una definición de tipo similar a “[S >: Cons <: ListaInt]” pudiendo ser S, en este caso, de tipo Cons y ListInt 2.5.1.2. Varianza La varianza definirá la relación existente entre los tipos parametrizados y los subtipos. Para comprender mejor el concepto de varianza se tomarán como referencia los tipos de datos definidos en los algoritmos 2.6 y 2.7 vistos en la Subsección 2.3.1: Herencia en Scala « página 38 ». En ella se establecen las siguientes relaciones entre los tipos definidos: Perro es subclase de Animal, Perro <: Animal Pajaro es subclase de Animal, Pajaro <: Animal Animal es la clase base de Perro y Pajaro, Animal >: Pajaro. Teniendo en cuenta la relación establecida entre dichos tipos, surge una pregunta: ¿será cierto que List[Pajaro] <: List[Animal]? Intuitivamente se podría responder afirmativamente ya que tiene sentido que una lista de pá- jaros sea un caso especial de una lista de animales. Se dirá que los tipos de datos que mantengan esta relación tienen una varianza positiva o covarianza. Ahora se plantea la duda de que si la covarianza es adecuada para todos los tipos que se definan. La covarianza será apropiada para los tipos de datos inmutables12 y no será adecuada para los tipos que permitan mutaciones de sus elementos. Dada una clase C con un tipo parametrizado C[T] y dos tipos de datos A y B, que presentan la siguiente relación A <: B, se plantea la duda sobre la varianza que presentará C, lo cual no es una decisión binaria (covarianza o no) sino que se pueden encontrar tres posibles relaciones entre C[A] y C[B]: C[A] <: C[B], siendo C[A] un subtipo de C[B], por lo que diremos que C presenta una relación de covarianza o varianza positiva. C[A] >: C[B], lo cual representa la relación opuesta siendo C[B] un subtipo de C[A], lo que representará un caso de contravarianza o varianza negativa. C[A] no sea subtipo de C[B] y C[B] tampoco sea subtipo de C[A], que sería un caso de invarianza. Se representará la varianza de C cuando se defina la misma: class C[+A]{...} si C tiene varianza positiva o convarianza. class C[-A]{...} si C tiene varianza negativa o contravarianza. class C[A]{...} siendo este un caso de invarianza de C. 12 Si los métodos definidos cumplen ciertas condiciones Página 49
  • 66. Se ha dicho que la covarianza será adecuada para los tipos inmutables cuyos miembros cumplan unas ciertas propiedades. Antes de empezar a enumerar estas propiedades se estudiará la relación que presentan los tipos de funciones definidos a continuación: 1 type A = Animal => Perro 2 type B = Perro => Animal Algoritmo 2.17: Tipos de funciones A y B Para determinar la relación entre los tipos A y B no se tendrá más que aplicar el principio de sustitución de Liskov13 y comprobar que A <: B, es decir A es subtipo de B. Seguidamente se verá como se puede mantener la covarianza en la definición de nuestras funciones o métodos. Si se tuvieran cuatro tipos A1, A2, B1 y B2 que presentan la siguiente relación: A2 <: A1 y B1 <: B2 y las funciones A1 =>B1 y A2 =>B2, aplicando nuevamente el principio de sustitución de Liskov se observa que la relación entre los tipos de ambas fun- ciones es de covarianza, A1 =>B1 <: A2 =>B2. Si se presta atención, se puede observar que la relación entre los argumentos es de contravarianza y la relación entre los tipos resultantes es de covarianza. Por tanto, las características que deben de presentar los miembros de las clases que presenten paramétros de tipo covariantes serán: Presentar una relación de contravarianza en los argumentos. Presentar una relación de covarianza en los resultados. Estas características se pueden ver representadas la definición del trait Function1: 1 trait Function1[-T,+U]{ 2 def apply(x:T):U 3 } Algoritmo 2.18: Trait Function1 Por tanto, se deberá de tener en cuenta que los parámetros tipados covariantes sólo pueden aparecer como resultados de los métodos. Los parámetros tipados contravariantes sólo pueden aparecer como argumentos de los métodos, mientras que los parámetros tipados invariantes podrán aparecer en cualquier lugar. Una vez vista la varianza, se podría mejorar el tipo de datos ListaGen definido en el algorit- mo Algoritmo 2.11: Lista Genérica con funciones « página 47 ». En primer lugar se podría pensar que no se necesita crear una instancia de la clase Nil cada vez que se quiera representar la lista vacía, convirtiendo Nil en un objeto cuya superclase es ListaGen: 13 El Principio de Sustitución de Liskov (LSP) es una definición particular de una relación de subtipificación, llamada tipificación (fuerte) del comportamiento, que fue introducido inicialmente por Barbara Liskov en una conferencia magistral en 1987 titulada La Abstracción de Datos y Jerarquía y que dice que: si S es un subtipo de T, entonces los objetos de tipo T en un programa de computadora pueden ser sustituidos por objetos de tipo S (es decir, los objetos de tipo S pueden sustituir objetos de tipo T), sin alterar ninguna de las propiedades deseables de ese programa Página 50
  • 67. 1 trait ListaGen[+T]{ 2 def isEmpty:Boolean 3 def head:T 4 def tail:ListaGen[T] 5 } 6 class Cons[T](val cabeza:T, val cola:ListaGen[T]) extends ListaGen[T]{ 7 def isEmpty = false 8 } 9 object Nil extends ListaGen[Nothing]{ 10 def isEmpty = true 11 def head:Nothing = throw new NoSuchElementException("Nil.head") 12 def tail:Nothing = throw new NoSuchElementException("Nil.tail") 13 } Algoritmo 2.19: Lista genérica de enteros, covariante y con Nil como objeto Teniendo en cuenta que los objetos no pueden ser parametrizados, se ha borrado el parámetro [T] de Nil. A continuación, se ha cambiado el parámetro de tipo que presentaba ListaGen en el objeto Nil ya que el parámetro T no está accesible para el objeto Nil y esto provocaría un error. Por último, se ha especificado que el parámetro de tipo presente en el rasgo ListaGen es covariante para poder realizar definiciones del tipo: 1 val x:ListaGen[String] = Nil Página 51
  • 69. Capítulo 3 Programación Funcional en Scala 3.1. Introducción a la programación funcional La programación funcional es un paradigma de programación que trata la computación como la evaluación de funciones matemáticas evitando los estados y los datos mutables.[24] La programación funcional es un paradigma en el que se trata la computación como la eva- luación de funciones matemáticas y se evitan los programas con estado y datos que puedan ser modificados. Se adopta una visión más matemática en la que los programas están compuestos por numerosas funciones que esperan una determinada entrada y producen una determinada salida y, en muchas ocasiones, otras funciones. Otra de las principales características de la programación funcional es la ausencia de efectos colaterales, gracias a lo cual los programas desarrollados son mucho más sencillos de compren- der y probar. Adicionalmente, se facilita la programación concurrente, evitando que se convierta en un problema gracias a la ausencia de cambio. 3.1.1. Características de los Lenguajes de Programación Funcionales 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. Closure (cierre). Asignación simple. Evaluación perezosa. Inferencia de tipos. Optimización de las llamadas de cola. Efectos monádicos. Página 53
  • 70. 3.1.2. Scala como lenguaje funcional 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.1 . Scala da soporte tanto a variables inmutables (también conocidas como valores) como a variables que soportan estados no permanentes. 3.1.3. ¿Por qué la programación funcional? La programación funcional permitirá crear programas modulares que estarán compuestos por pequeños componentes que podrán ser entendidos y reutilizados independientemente del propósito del programa. Por tanto, el significado del programa vendrá determinado por el propio significado que le demos a estos componentes y por las reglas que determinen la composición de estos componentes. 3.2. Sentido estricto y amplio de la programación funcional En un sentido estricto, la programación funcional significa programar sin variables mu- tables, asignaciones, bucles, y otras estructuras imperativas de control. (Pure Lisp, FP, XSLT, Haskell 2 , ...) En un sentido más amplio, la programación funcional significa centrarse en las funciones, que podrán ser valores creados, consumidos y compuestos..., lo cual se hace más fácil con un lenguaje de programación funcional. (Lisp, OCaml, Haskell, Scala...) Por tanto, se puede afirmar que la programación funcional se basa en una premisa simple pero con un gran alcance: desarrollar programas utilizando sólo funciones puras, es decir, funciones que no tengan efectos colaterales. 3 La programación funcional impone restricciones con relación a cómo escribir los programas pero no en qué programas se pueden escribir. A lo largo de este capítulo se verá como muchos programas que se creían imposibles escribir sin evitar efectos colaterales tienen una versión funcional pura. También se podrá comprobar cómo, aunque esté fuera del ámbito de la pro- gramación funcional pura, en algunos casos será inevitable que aparezcan efectos colaterales, mayormente ligados a las mejoras de rendimiento del programa, aunque se intentará que éstos no sean observables. Por ejemplo, se mutarán datos declarados localmente en el cuerpo de una función, teniendo en cuenta que esos datos no sean referenciados fuera de la misma. Otra ventaja de la programación funcional radica en el hecho de que escribir programas con funciones puras incrementará la modularidad de los mismos y, como consecuencia de la modularidad, las funciones puras serán fáciles de verificar (test), reutilizar, paralelizar y gene- ralizar. Así, se consideran a las funciones “ciudadanos de primera clase” dentro de un lenguaje de programación funcional, lo que significará: Pueden ser definidas en cualquier lugar. Como cualquier otro valor, podrán ser pasadas como parámetros de funciones y ser de- vueltas como resultado de las mismas. Como otros valores, existirán un conjunto de operadores para componer funciones. 1 Un ejemplo de lenguaje funcional puro sería Haskell 2 Sin mónadas I/O o UnsafePerformIO 3 La reasignación de una variable, la modificación de una estructura de datos, lanzar una excepción, la impresión por pantalla...se consideran efectos colaterales Página 54
  • 71. 3.2.1. ¿Qué son las funciones puras? Son aquellas que no producen ningún efecto observable en la ejecución del programa dis- tinto al procesamiento esperado de las entradas para producir un resultado. A estas funciones también las llamaremos sin efectos laterales. Por ejemplo: La función suma (+) de enteros. Se podrá formalizar la idea de función pura usando el concepto de transparencia referen- cial. La transparencia referencial (TR) es un concepto que podemos aplicar a las expresiones, además de a las funciones. Diremos que una expresión será referencialmente transparente cuando podamos sustituir dicha expresión por su resultado, sin cambiar el significado del programa. Del mismo modo, se dice que una función es pura si el cuerpo de la función es referencialmente transparente, asumiendo que las entradas también lo sean. La transparencia referencial lleva a un modo de razonar en la evaluación de programas llamado modelo de sustitución4 . Cuando las expresiones son referencialmente transparentes, se puede imaginar el proceso de computación como la resolución de una ecuación algebraica. 3.3. Funciones y cierres en Scala Hasta el momento se han 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. Cuando los programas crecen, se necesita hacer uso de un conjunto de abstracciones que posibiliten dividir dicho programa en piezas más pequeñas y manejables, las cuales permitirán una mejor comprensión del mismo. Scala ofrece varios mecanismos para definir funciones que no están presentes en Java. Además de los métodos, que no son más que funciones miembro de un objeto, se puede hacer uso de funciones anidadas, funciones anónimas y funciones valor. 3.3.1. Definición de funciones Mediante el uso de la palabra reservada def se pueden definir funciones con argumentos. La sintaxis es: 1 def <nombre_funcion>(<parametro1:tipo1>,...):<tipo_resultado> = { 2 <cuerpo de la funcion> 3 } El tipo del resultado (el tipo del valor que devuelve la función) es opcional excepto en las funciones recursivas, que siempre hay que indicarlo. Las llaves que delimitan el cuerpo de la función también son opcionales si el cuerpo sólo tiene una sentencia. Ejemplo: 1 def max(a:Int,b:Int):Int = if (a>=b) a else b En la función max se podría haber omitido el tipo del resultado dejando que Scala lo infiriera. Sin embargo, en una función recursiva hay que indicarlo explícitamente. 4 Para más información sobre la evaluación de expresiones en Scala, véase la Sección 1.4: Evaluación en Scala « página 18 » Página 55
  • 72. Una vez se ha definido una función, se podrá invocar por su nombre. Ejemplo: scala> def max(a:Int,b:Int):Int= if (a>b) a else b max: (a: Int, b: Int)Int scala> max(32,5) res1: Int = 32 3.3.2. Funciones anidadas Es un buen estilo de programación dividir tareas en pequeñas funciones, aunque en muchas ocasiones es deseable que esas pequeñas funciones no estén visibles para poder ser usadas fuera del ámbito en el que se han creado, por lo que se tendrán escribir dentro del bloque de otra función. En el algoritmo 3.1 se muestra un ejemplo de la definición de pequeñas funciones dentro del bloque de otra función. En concreto, se define la función sqrt que devolverá el cálculo de la raíz cuadrada del número pasado como argumento utilizando el método de Newton de aproximaciones sucesivas5 [32]. En el algoritmo 3.1 se ha tomado: El valor 1 como aproximación inicial. La función suficienteBuena6 como criterio de finalización del algoritmo. 1 def sqrt(x: Double) = { 2 def sqrtIter(valorEstimado: Double): Double = //Funcion recursiva para calcular la raiz cuadrada 3 if (suficienteBuena(valorEstimado)) valorEstimado 4 else sqrtIter(mejorAproximacion(valorEstimado)) 5 6 def mejorAproximacion(valorEstimado: Double) = //Funcion para mejorar la aproximacion 7 (valorEstimado + x / valorEstimado) / 2 8 9 def suficienteBuena(valorEstimado: Double) = //Condicion de terminacion del algoritmo 10 abs(cuadrado(valorEstimado) - x) < 0.001 11 sqrtIter(1.0) 12 } Algoritmo 3.1: Cálculo de la raíz cuadrada de un número por el método de Newton 5 En términos generales, cada vez que se tenga una estimación valorEstimado del valor de la raíz cuadrada de un número x (pasado como argumento a la función), se podrá hacer una pequeña manipulación para obtener una mejor aproximación (una más cercana a la verdadera raíz cuadrada) realizando la media aritmética del valorEstimado y x valorEstimado ,es decir, valorEstimado+ x valorEstimado 2 . Al repetir este proceso, se obtendrán cada vez mejores estimaciones de la raíz cuadrada. El algoritmo deberá detenerse cuando la estimación sea lo “suficientemente buena”, algo que deberá ser un criterio bien definido. 6 Este criterio de detención no es muy bueno ya que no es muy preciso para números pequeños y cuando se le pasa como argumento un número grande podría no terminar(ya las operaciones aritméticas son casi siempre realizadas con una precisión limitada en los ordenadores). El criterio de detención se podría mejorar indicando como condición de terminación la invariabilidad de los n primeros decimales. Página 56
  • 73. 3.3.3. Diferencias entre métodos y funciones En la mayoría de los casos se pueden tratar funciones y métodos indistintamente. Sin embar- go, no son exactamente iguales, aunque los dos sirven para definir bloques de computación. La diferencia sutil está en que un método es siempre un miembro de una clase (junto con campos y tipos), mientras que una función no esta ligada a una clase y, como se ha visto anteriormente, su naturaleza de objeto (de clase FunctionN) permite que sea pasada, asignada o devuelta en el típico estilo funcional. scala> def m(x: Int) = 2*x m: (x: Int)Int scala> val f = (x: Int) => 2*x f: (Int) => Int = Como se puede ver, el tipo para función y objeto difiere. Antes se ha comentado que los mé- todos siempre pertenecen a clases. Para poder definir métodos, como se ha hecho en el anterior ejemplo, el intérprete de Scala crea un objeto invisible que rodea todo lo definido desde la línea de comandos. ¿Qué pasa si se intenta invocar toString sobre un método? scala> m.toString :7: error: missing arguments for method m in object $iw; follow this method with ‘_’ if you want to treat it as a partially applied function m.toString ^ scala> f.toString res1: java.lang.String = Efectivamente, el método no es un objeto, pero la función sí. En Scala existe una convención por la cual la sintaxis nombre() se traduce en una llamada al método apply de aquello referido con nombre. Esto significa que la expresión funcion(...) realmente se traduce en funcion.apply(...), en virtud de que, como se ha visto, las funciones son objetos con este método especial. Así que en el fondo, las llamadas a funciones en Scala acaban siendo “por debajo” ejecuciones de algún método apply definido sobre algún objeto. La creación de estos objetos ocurre de manera transparente; cuando en código se definen funciones, Scala crea los objetos correspondientes de manera automática. Aún hay algo más. En Scala se pueden asignar las funciones a variables (independientemen- te de que se traten de variables mutables definidas con var como a variables inmutables definidas con val). Reiterando, esto es una asignación de un objeto tipo FunctionN a dicha variable. Pero también es posible asignar a una variable un método de una clase. Como por ejemplo: scala> def m(x: Int) = 2*x m: (x: Int)Int scala> val f = m _ f: (Int) => Int = <function1> scala> f.toString res4: java.lang.String = <function1> Como se puede observar, el tipo de f es el mismo que se vio anteriormente para una función. Scala ha creado automáticamente un objeto cuya función apply llama al método m, y éste es el objeto asignado a f. También se aprecia en este ejemplo la sintaxis utilizada para convertir m en una función parcialmente aplicada: scala> val f = m _ Si se hubiera escrito simplemente: Página 57
  • 74. scala> val f = m Scala habría interpretado que lo que se trata de hacer es asignar a f el resultado de la invo- cación de m. Pero lo que verdaderamente se intenta hacer es asignar m en sí. La sintaxis m _ dice “no evalúes m, devuelve como resultado de la expresión la función7 en sí”. En Scala, las funciones parcialmente aplicadas son funciones a las que no se les ha proporcionado todos sus argumentos (en el ejemplo que se ha visto, no se proporciona ninguno), y que por tanto no tienen aún valor de retorno, sino carácter de función. 3.3.4. Funciones de primera clase Scala incluye una de las características principales del paradigma funcional: first-class fun- ctions o funciones de primera clase. No solamente se podrán definir funciones e invocarlas sino que también será posible definirlas como literales para, posteriormente, pasarlas como va- lores. 3.3.5. Funciones anónimas y funciones valor Las funciones anónimas (también llamadas funciones literales, lambda funciones o simple- mente lambdas8 ) son compiladas en una clase que, cuando es instanciada, se convierte en una función valor. Por lo tanto, la principal diferencia entre las funciones anónimas y las funciones valor es que las primeras existen en el código fuente mientras que las segundas existen como objetos en tiempo de ejecución. A continuación se define un pequeño ejemplo de una función anónima que suma el valor 1 al número indicado: 1 val f = (x:Int) => x + 1 Las funciones valor son objetos propiamente dichos, por lo que se pueden almacenar en variables o invocarlas mediante la notación de paréntesis habitual. Las funciones anónimas se usan frecuentemente en programación funcional para pasarlas como parámetros de otras funciones. Cuando se define una función anónima lo que el intérpre- te define, de una forma totalmente transparente al programador, es un objeto con un método llamado apply. 9 . Cuando se define una función anónima cómo (x : Int) => x + 1, lo que realmente se crea es un objeto: 1 val f = new Function1[Int,Int] { 2 def apply (a:Int):Int = a + 1 3 } Se observa que f tiene el tipo Function1[Int,Int], habitualmente escrito como Int => Int. Se puede apreciar que el rasgo Function1 define un único método llamado apply. Cuando se invoca a la función f con un valor, f(5), lo que realmente se hace es una llamada al método apply de f: 1 f.apply(5) 7 Una vez se ha hecho la conversión de método en función 8 El nombre lambda viene del lambda cálculo, 9 En Scala los objetos que tienen un método apply pueden ser llamados como si fueran métodos Página 58
  • 75. La biblioteca estándar de Scala provee de distintos rasgos FunctionN para representar las funciones como objetos de N argumentos. 3.3.6. Cierres Las funciones anónimas que se han visto hasta este momento han hecho uso, única y exclu- sivamente, de los parámetros pasados a la función. Sin embargo, se pueden definir funciones anónimas en las que se hace uso de variables definidas en otro punto de nuestro programa: 1 (x:Int) = x * otro La variable otro es conocida como una variable libre (free variable) puesto que la función no le da un significado a la misma. Al contrario, la variable x es conocida como variable ligada (bound variable) puesto que tiene un significado en el contexto de la función. Si se intenta utilizar esta función en un contexto en el que no esté accesible una variable otro se obtendría un error de compilación indicando que dicha variable no está disponible. Las funciones valor creadas en tiempo de ejecución a partir de las funciones anónimas son conocidas como cierres. El nombre se deriva del acto de “cerrar” la función anónima mediante la captura en el ámbito de la función de los valores de sus variables libres. Una función valor que no presenta variables libres, creada en tiempo de ejecución a partir de su función anónima no es un cierre en el sentido más estricto de la definición dado que dicha función ya se encuentra “cerrada” en el momento de su escritura. Veremos la verdadera importancia de los cierres dentro de la programación funcional cuando se estudie la parcialización de funciones. El fragmento de código anterior hace que se plantee la siguiente pregunta: ¿qué ocurre si la variable otro es modificada después de que el cierre haya sido creado? La respuesta es sencilla: en Scala, el cierre tiene visión sobre el cambio ocurrido. La regla anterior también se cumple en sentido contrario: si un cierre modifica alguno de los valores capturados, estos últimos son visibles fuera del ámbito del mismo. 3.4. Recursión Una de las preguntas que surgen dentro del paradigma de la programación funcional es: ¿cómo se pueden escribir programas simples en los que se tengan que utilizar bucles sin reasig- nación de variables? 10 Para escribir bucles en un lenguaje funcional se utilizará una función recursiva11 que estará definida normalmente de forma local a otra función. Programando en un lenguaje funcional no se utilizarán los bucles iterativos, ya que suelen utilizar vars y, principalmente, no determinan un valor12 . Aunque la recursión es mucho más que simular bucles ya que es una técnica esencial en computación que permite diseñar algoritmos recursivos que dan soluciones elegantes y simples, y generalmente bien estructuradas y modulares, a problemas de gran complejidad. Se dice que un proceso es recursivo si se puede definir en términos de si mismo, y a dicha definición se le denomina definición recursiva. La recursividad es una nueva forma de ver las acciones re- petitivas permitiendo que un subprograma se llame a sí mismo para resolver una versión más pequeña del problema original. 10 Sin utilizar bucles While, Do-While... 11 Cuando una función se llama así misma 12 La última expresión evaluada del bucle será la condición de salida del mismo Página 59
  • 76. Se puede diferenciar entre dos tipos de recursión: Recursión directa o explícita cuando procedimiento se llama a sí mismo. Recursión indirecta o implícita cuando un procedimiento P llama a otro Q, Q llama a R, R llama a S, ..., y Z llama de nuevo a P A la hora de definir una función recursiva para resolver un problema, además de definir la relación de recurrencia, habrá que identificar el caso base, el cual permitirá conocer el valor de la función. Esta regla es la condición de terminación. Por tanto para utilizar recursión en la resolución de un problema habrá que asegurarse de que se cumplen las siguientes condiciones. Se debe poder definir en términos de una versión más pequeña del mismo problema. En cada llamada recursiva debe disminuir el tamaño del problema. El diseño de la solución del problema ha de ser tal que asegure la ejecución del caso base y por tanto, el fin del proceso recursivo. A continuación, se definirá la función recursiva sumaDesdeHasta, con dos argumentos en- teros x e y, que devolverá como resultado la suma de los enteros comprendidos entre x e y. Una posible definición de la función sumaDesdeHasta podría ser: sumaDesdeHasta(x, y) =    0 si x > y x + sumaDesdeHasta(x + 1, y) en otro caso Esta es la definición recursiva de la función sumaDesdeHasta, ya que se define en términos de si misma. La primera regla de la definición, o caso base, establece la condición de termina- ción. Las definiciones recursivas permiten definir un conjunto infinito de objetos mediante una sentencia finita. Implementación de la función recursiva directa sumaDesdeHasta en Scala: 1 def sumaDesdeHasta(x:Int,y:Int):Int = if (x>y) 0 else x + sumaDesdeHasta(x+1,y) 3.4.1. Importancia de la pila del sistema en recursión. Mientras las funciones recursivas empleadas para resolver un problema no realicen un gran número de llamadas recursivas hasta alcanzar el caso base no habrá ningún problema en apli- car las soluciones recursivas vistas anteriormente. Cuando las funciones recursivas necesiten realizar un gran número de llamadas recursivas hasta alcanzar el caso base, la función podría no ser capaz de alcanzar la condición de terminación, devolviendo un error como consecuencia del desbordamiento de pila: StackOverflowError. Por este motivo será determinante evaluar la profundidad máxima requerida para que un algoritmo recursivo alcance la condición de termi- nación con el objetivo de prever la cantidad de memoria necesaria. El número de llamadas recursivas que una determinada función podrá realizar antes de que se produzca un error por desbordamiento de pila dependerá del tamaño de la pila de Java, definido por defecto en 1024kb13 . 13 En un sistema basado en Unix, podemos introducir desde la línea de comandos java -XX:+PrintFlagsFinal -version | grep -i stack y conocer el tamaño de pila definido observando el valor de ThreadStackSize. El tamaño de la pila se podrá cambiar desde la línea de comandos utilizando la opción -Xss Página 60
  • 77. 3.4.1.1. La pila de Java Según la descripción que Oracle nos ofrece de la pila de Java y de los contextos de pila, cada thread (hilo de ejecución) tendrá una pila privada que se creará en el mismo momento que el hilo de ejecución14 . En la pila de Java se guardará el estado del hilo de ejecución asociado y la JVM sólo podrá realizar dos operaciones sobre dicha pila: almacenar y sacar contextos de pila. El método que el hilo de ejecución ejecuta en un momento dado recibe el nombre de méto- do actual del hilo de ejecución. El contexto de pila para el método actual es llamado contexto actual. La clase en la que se define el método actual será la clase actual, así como la agrupa- ción de constantes actual (current constant pool) será las definida en la clase actual. Cuando la JVM encuentra instrucciones que operen con los datos almacenados en el contexto de pila15 , realizará dichas operaciones en el contexto actual. 3.4.1.2. Contexto de pila Un contexto de pila consta de tres partes: Variables locales Pila de operandos16 Referencia al grupo de constantes17 El tamaño de las variables locales y de la pila de operandos18 dependerá, por tanto, de las necesidades de cada método. Para cada método de una clase se determinará el tamaño del contexto de pila necesario y se incluirá en el fichero de clase durante la compilación. Así, el tamaño de cada contexto de pila dependerá de las variables locales, los parámetros del método y del algoritmo empleado ya que determinará el tamaño de la pila de operandos. Por tanto, cuando se invoca un método, se crea un nuevo contexto de pila que contendrá información sobre ese método. Durante la ejecución del método, el código sólo podrá acceder a los valores del contexto actual19 . Una vez finalizada la ejecución del método actual, la informa- ción del contexto actual se sacará de la pila, por lo que el programa podrá continuar la ejecución desde el mismo punto en el que se realizó la llamada a dicho método. En resumen, cuando una función recursiva realiza una llamada a sí misma, la información de la función se almacenará en la pila. Es decir, cada vez que la función se llame a sí misma, una nueva copia de la información de la función será guardada en la pila por lo que se necesitará un nuevo contexto de pila por cada nivel de recursión. Por tanto, por cada nivel de recursión será necesaria más memoria. En la próxima sección se verá como la recursión de cola resuelve el problema de la demanda de memoria de las funciones recursivas. Considérese la siguiente definición de la función recursiva fact encargada de calcular el factorial de un entero pasado como argumento: 14 Por tanto, en las aplicaciones en las que haya varios hilo de ejecucións, existirán varias pilas privadas asociadas a dichos hilo de ejecucións, en concreto, tantas pilas como hilo de ejecucións. 15 Los datos almacenados en la pila de un hilo de ejecución son privados para dicho hilo de ejecución. 16 La pila de operandos será un espacio de memoria que será usado como área de trabajo dentro del contexto de pila. 17 Contiene diferentes tipos de constates, desde literales numéricos que se conocen durante la compilación hasta referencias a métodos que serán resueltas durante el tiempo de ejecución. 18 Se medirá en palabras (words). El tamaño de una palabra puede variar en las diferentes implementaciones de la JVM aunque tendrá al menos 32bits por lo que se podrán almacenar datos del tipo long o double en una palabra. 19 El contexto actual estará situado en la cima de la pila privada al hilo de ejecución. Página 61
  • 78. 1 def fact(n: Int):Int = { 2 var valor:Int=0 3 if (n == 0) {valor=1} 4 else {valor=n * fact(n - 1)} 5 valor 6 } En la figura 3.1 se puede observar la evolución de la pila de Java cuando se evalúa la expre- sión: 1 val valor = fact(4) Figura 3.1: Evolución de la pila de Java cuando se evalúa la expresión fact(4). 3.4.2. Recursión de cola Las funciones que se llaman a si mismas en la última sentencia de su cuerpo son llamadas funciones recursivas de cola (tail recursive). El compilador de Scala detecta esta situación y la optimiza, reemplazando 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. A continuación se presenta una función recursiva que calcula la suma desde un natural dado hasta otro natural dado: 1 def sumaDesdeHastaRec(x:Int,y:Int):Int={ 2 @annotation.tailrec 3 def go(x:Int,y:Int,acc:Int):Int= 4 if (x>y) acc 5 else go(x+1,y,acc+x) 6 go(x,y,0) 7 } Algoritmo 3.2: Recursión de cola La anotación @annotation.tailrec le indica al compilador de Scala que la función definida a continuación es recursiva de cola. Si se hace esta indicación y la función no fuera recursiva de cola daría un error. Cuando se emplea recursión de cola, el compilador de Scala traducirá el código al mismo Página 62
  • 79. grupo de bytecodes que si se hubiera utilizado un bucle while20 . El objetivo de utilizar esta optimización para la recursión es el de evitar el error provocado por un desbordamiento de la pila – StackOverflowError – para casos que generen un gran número de llamadas recursivas, ya que los bucles iterativos no presentan este problema. En general, en recursión de cola, un contexto de la pila será suficiente tanto para la función como para la llamada recursiva. El uso de recursión de cola 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 recursión de cola. Scala únicamente optimiza llamadas recursivas a una función dentro de la misma (recursión directa). 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: 1 def esPar(x:Int): Boolean = if(x==0) true else esImpar(x-1) 2 def esImpar(x:Int): Boolean = if(x==0) false else esPar(x-1) Algoritmo 3.3: Recursión Indirecta 3.5. Currificación y Parcialización 3.5.1. Currificacion Scala no incluye un excesivo número de instrucciones de control. Las típicas están definidas y se puede llevar a cabo la definición de construcciones propias de manera sencilla. Se analizará como se pueden definir 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: la currificación (currying21 ). Haskell B. Curry propuso trabajar sólo con funciones de un argumento. Aunque esto parece una limitación, el truco consiste en que las funciones de más de un argumento son de orden superior: toman un argumento y devuelven una función. La currificación hace que la parciali- zación sea directa. El siguiente fragmento de código nos muestra una función tradicional que recibe dos argumentos de tipo entero y retorna la suma de ambos: 1 def sumaPlana(x:Int, y:Int) = x + y A continuación se muestra una función currificada similar a la descrita en el fragmento de código anterior: 1 def sumaCurrificada(x:Int)(y:Int) = x + y Cuando se evalúa la expresión sumaCurrificada(9)(2) se estarán realizando dos llamadas tradicionales de manera consecutiva. La primera invocación recibe el parámetro x y devuelve 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 primera que lleva a cabo lo que haría la primera de las invocaciones de la función sumaCurrificada anterior: 20 Siempre y cuando la llamada recursiva sea la última instrucción (esté en la posición de cola o tail position) 21 Se denomina así, currying en honor a su descubridor, Haskell B. Curry (aunque Moses Schönfinkel la propuso antes, el término “Schöenfinkelization” no terminaría de popularizarse) Página 63
  • 80. 1 def primera(x: Int) = (y: Int) => x + y Invocando a la función anterior con el valor 1 se obtendría una nueva función: 1 def segunda = primera(1) La invocación de la función segunda con el parámetro 2 devolvería como resultado 3. scala>segunda(2) res: Int = 3 3.5.2. Parcialización La parcialización es la aplicación parcial de una función, es decir, una invocación que recibe menos argumentos de los que espera. La aplicación parcial de una función producirá como resultado una función anónima que será una versión especializada de la función original. La parcialización de una función permitirá pasar sólo algunos argumentos a una función, obteniendo como resultado una función anónima. Los argumentos que no le serán pasados a una función se indicarán utilizando _ (guión bajo).Esta función anónima devuelta será un cierre. En Scala es posible parcializar tanto funciones currificadas, como funciones no currificadas, así como parcializar cualquier argumento de las mismas. Para entender bien la parcialización se definirán dos funciones: suma y sumaC. Ambas devolverán como resultado la suma de los dos parámetros enteros que se les pase. La única diferencia entre ellas es que sumaC es una función currificada: 1 def suma(x:Int,y:Int):Int = x + y 2 def sumaC(x:Int)(y:Int) = x + y Si se quisiera implementar una nueva función que sume 5 a un valor entero dado, se podría definir cualquiera de las siguientes funciones parcializando la función suma. 1 val suma5:Int=>Int = suma(5,_:Int) 2 val suma5b = suma(_:Int,5) Se puede apreciar que se ha definido la función suma5 parcializando sobre (y), que es el segundo argumento de la función suma, mientras que la función suma5b se ha definido parcia- lizando sobre el primer argumento de suma (x). En cambio si se quisiera definir la misma función a partir de sumaC: 1 val suma5C:Int=>Int = sumaC(5) 2 val suma5Cb = sumaC (_:Int)(5) En el ejemplo anterior se advierte que se ha definido la función suma5C parcializando sobre el segundo argumento de la función sumaC (y) mientras que la función suma5Cb se ha definido parcializando sobre el primer argumento de sumaC (x). Finalmente, también es factible hacer una parcialización total de la función suma o sumaC, como se puede ver en el siguiente ejemplo: 1 val sumaP:(Int,Int)=>Int=suma _ 2 val sumaCP = sumaC _ Página 64
  • 81. En este caso sí se puede observar una diferencia clara, atendiendo al tipo de datos devuelto por ambas funciones. El tipo devuelto por sumaP es: (Int, Int) => Int y el tipo de sumaCP es: Int => (Int => Int), como => tiene asociatividad derecha es equivalente a: Int => Int => Int. Mediante la parcialización total se pueden definir funciones a partir de métodos. Aunque se pueda parcializar sobre cualquier argumento, aplicar la parcialización sobre los primeros argumentos de funciones currificadas es una buena práctica que mejorará el entendi- miento y la legibilidad del código. La parcialización en los operadores Como se estudió en la Subsubsección 1.2.2.2: Operadores « página 10 », los operadores son en realidad métodos y, por tanto, se reducen a la llamada a un método de un objeto de una de clases de los tipos de datos. Además, en la Subsección 3.3.3: Diferencias entre métodos y funciones « página 57 », se vieron las principales diferencias entre métodos y funciones y cómo en la mayoría de los casos se podrían tratar métodos y funciones indistintamente. Por tanto, se podrán obtener funciones anónimas parcializando sobre los argumentos de los operadores: 1 def por3 = 3 * (_:Int) 2 def por4:Int=>Int = _ * 4 En el ejemplo anterior se han definido las funciones por3 y por4. Se puede observar dos diferencias notables en la definición de ambas funciones. Aunque ambas funciones han sido definidas parcializando el operador * definido en el tipo de datos Int, la función por3 ha sido definida parcializando sobre el segundo operando y la función por4 sobre el primer operando. En la función por3 se ha especificado el tipo del operando parcializado mientras que en la función se ha indicado el tipo de la función por4. 3.6. Orden Superior 3.6.1. Funciones de orden superior En Scala, las funciones son objetos también, por lo que podrán ser pasadas como cualquier otro valor, ser asignadas a variables, almacenadas en estructuras de datos...Por tanto, Scala per- mite la definición de funciones de orden superior, es decir, funciones que toman otras funciones como parámetros o que devuelven una función, y que permiten hacer mucho más conciso y general el código escrito en Scala. El orden superior es una técnica que permitirá abstraer funciones y pasarlas como paráme- tros, lo que proporcionará al programador una forma flexible de componer programas. A continuación se estudiará la importancia del orden superior en los lenguajes funcionales y cómo facilita la reutilización de código, haciendo más simple la escalabilidad de los programas. Para comenzar, se definirán dos funciones, sumaDesdeHasta y productoDesdeHasta, las cuales calcularán la suma y el producto del intervalo comprendido entre los números enteros pasados como parámetros, respectivamente. Página 65
  • 82. 1 def sumaDesdeHasta(x:Int,y:Int):Int = if (x>y) 0 2 else x + sumaDesdeHasta(x+1,y) 3 def productoDesdeHasta(x:Int,y:Int):Int = if (x>y) 1 4 else x * productoDesdeHasta(x+1,y) En el ejemplo anterior se puede observar la similitud entre ambas funciones. Lo único que las diferencia es el valor devuelto cuando (x > y) (caso base) y la operación (suma o producto) del caso recursivo. Sería posible abstraer la operación(suma y producto) y el caso base, y pasarlos como argu- mentos. 1 def operaN(f:(Int,Int)=>Int,e:Int)(x:Int,y:Int):Int= if (x>y) e 2 else f(x,operaN(f,e)(x+1,y)) Ahora si se invoca a la función: scala> operaN(_+_,0)(1,10) res15: Int = 55 Se obtiene el mismo resultado que si se invocara la función sumaDesdeHasta(1,10). Obsér- vese también que la función operaN espera una función del tipo (Int,Int) =>Int en su argumento f. En el ejemplo anterior se le ha pasado dicho argumento haciendo uso de la parcialización de operadores (vista en la Sección 3.5.2: La parcialización en los operadores « página 65 »), con- cretamente parcializando el operador + del tipo de datos Int. También se podría calcular el producto con operaN: scala> operaN(_*_,1)(1,5) res16: Int = 120 Se puede comprobar que el resultado coincide con la llamada a la función productoDes- deHasta(1,5). Pero además es posible realizar otros cálculos, como por ejemplo, calcular la suma del cuadrado de los enteros comprendidos entre un intervalo dado: scala>operaN((x,y)=>x*x+y,0)(1,5) res17: 55 3.7. Funciones polimórficas. Genericidad Al igual que se vio en la Sección 2.5: Polimorfismo en Scala « página 45 » cómo crear clases genéricas parametrizando las mismas, ahora se estudiará como utilizar la genericidad para construir funciones polimórficas. Hasta el momento las funciones que se han definido son funciones monomórficas, es decir, funciones que operan con un tipo de datos concreto. Por ejemplo, la función swapInt que dados un par de elementos enteros devuelve una tupla con los elementos intercambiados: 1 def swapInt(a:Int,b:Int):(Int,Int)=(b,a) En ocasiones, estos tipos resultan demasiado restrictivos y si se necesitara intercambiar un par de elementos de tipo Float, Double,...habría que definir otra versión de la función swapInt para este tipo concreto: 1 def swapFloat(a:Float,b:Float):(Float,Float)=(b,a) Página 66
  • 83. Una vez más se puede apreciar la similitud de las funciones swapInt y swapFloat. En estas situaciones se desearía poder abstraer el tipo de la función ya que no influye en el cálculo de la misma. Para poder abstraer el tipo de una función no se podrá recurrir a las funciones de orden superior ya que éstas abstraen operaciones no tipos. Para abstraer el tipo de una función se usará polimorfismo22 .El polimorfismo servirá para introducir variables de tipo23 . Se dirá que una función cuyo tipo sea polimórfico será una función polimórfica. Es posible asignar el nombre que se desee a las variables de tipo, así [Animal, NumerosPrimos, . . . ] serían nombres válidos aunque por convenio se utilizarán variables de una sola letra [A, B, C] Para definir una función polimórfica en Scala se introducirá una lista de las variables de tipo, separadas por comas y encerrada entre corchetes después del nombre de la función. Ej. defswap[A, B](. . .) : . . . Las variables de tipo que forman esta lista podrán ser referenciadas en el resto de la defini- ción de la signatura de la función, así como en el cuerpo de la función (del mismo modo que las variables pasadas como argumentos a una función pueden ser utilizadas en el cuerpo de la misma). Haciendo uso del polimorfismo, se definirá una función swapGen que, pasados cualesquiera dos parámetros, devolverá una tupla con los parámetros intercambiados. 1 def swapGen[A](a:A,b:A):(A,A)=(b,a) En este ejemplo se puede ver que se ha definido una variable de tipo, A. Todas las referencias de tipo de la signatura de la función SwapGen hace referencia al tipo A por lo que se estaría forzando a que el tipo de los dos argumentos pasados a la función sea el mismo. Si los argumentos de la función no fueran del mismo tipo, Scala tratará de buscar un super- tipo común para el parámetro de tipo A hasta llegar al tipo Any, supertipo de todos los tipos. scala>swapGen("hola",2.3) res4:(Any,Any) =(2.3,"hola") A pesar de esta característica de Scala que permitiría que la función swapGen cumpla con uno de los propósitos de la misma, el resultado no es el esperado ya que se esperaba una tupla del tipo (Double,String) en lugar de una tupla del tipo (Any,Any). La función será más genérica y estará definida con más rigor si se le indica a Scala que ambos tipos pueden ser distintos. 1 def swap[A,B](a:A,b:B):(B,A)=(b,a) Ahora si se invoca la función swap con los parámetros anteriores si se obtendría una tupla con los tipos esperados y con los parámetros invertidos. scala>swap("hola",2.3) res4:(Double,String) =(2.3,"hola") 22 Usaremos la palabra polimorfismo con un significado algo distinto al empleado en POO donde suele con- notar algún tipo de subtipado. El poliformismo empleado en programación funcional también es llamado como polimorfismo paramétrico y en otros paradigmas de programación se denomina genericidad 23 Las variables de tipo también suelen llamarse parámetros de tipo Página 67
  • 84. 3.8. Ejercicios 3.8.1. Ejercicio Resuelto Ejercicio 1 Desde 1990 el peso se categoriza mediante el cálculo del Índice de masa corpo- ral (IMC)24 . El cálculo del IMC corporal es fácil ( peso(kg) altura2(metros) ) Este no es válido para personas con una altura inferior a 1,47 metros o superior a 1,98 m, para menores de 18 años o para atletas de élite (que tienen mucha masa muscular). Se pide definir una función imcVal que calcule el IMC de un individuo. 1 def square(x:Double)=x*x 2 def imcVal(weight:Double,height:Double):Double={ 3 require(height>=1.47 && height <= 1.98) 4 weight/square(height) 5 } Ejercicio 2 La clasificación desde 199025 para adultos según su IMC es: < 18.5 (bajo peso); 18.5-24.9 (normal-normopeso); 25.0-29.9 (sobrepeso); > 30 (obesidad). Definir una función imcClas que dados el peso y la altura de un individuo adulto como argumentos, nos devuelva su clasificación en función de su IMC. Primera aproximación, función imcClas1: 1 def icmClas1(p:Double,a:Double):String={ 2 val imcValor=imcVal(p,a) 3 if (imcValor < 18.5) "Bajo Peso" 4 else if (18.5<imcValor && imcValor<24.9) "Peso Normal" 5 else if (24.9<imcValor && imcValor<30) "Peso Normal" 6 else "Obesidad" 7 } La función imcClas1 daría el resultado deseado pero no hace uso de las ventajas que ofrece la programación funcional. Se podrían desacoplar las condiciones del If en funciones del tipo Double => Boolean y así ganar tanto en legibilidad del código como en reusabilidad del mismo. A continuación se verá una nueva aproximación a la solución final: 24 Ideado por el estadístico belga Adolphe Quetelet, por lo que también se conoce como índice de Quetelet. 25 http://www.ncbi.nlm.nih.gov/mesh?term=body%20mass%20index Página 68
  • 85. 1 def bajoPeso(imcValue:Double):Boolean = 0<imcValue && imcValue < 18.5 2 def pesoNormal(imcValue:Double):Boolean = 18.5 <= imcValue && imcValue < 24.9 3 def sobrePeso(imcValue:Double):Boolean = 24.9 <= imcValue && imcValue < 30 4 def obesidad(imcValue:Double):Boolean = 30 <= imcValue && imcValue < 100 5 6 def icmClas2(p:Double,a:Double):String={ 7 val imcValor=imcVal(p,a) 8 if (bajoPeso(imcValor)) "Bajo peso" 9 else if (pesoNormal(imcValor)) "Peso Normal" 10 else if (sobrePeso(imcValor)) "SobrePeso" 11 else "Obesidad" 12 } Ahora se puede observar como la función encargada de clasificar los individuos según su ICM queda mucho más clara. Además se podrán utilizar las “funciones auxiliares” definidas en un futuro. Si se presta atención a dichas funciones auxiliares, se puede advertir que si se abstrae el límite inferior y el límite superior del cada rango, junto con el valor que se está comparando se podría crear una función de orden superior con la que poder definir todas. 1 def peso_estaEntre(low:Double,high:Double)(value:Double):Boolean = low<value && value< high 2 def bajoPesoIMC:Double=>Boolean = peso_estaEntre(0,18.5) 3 def normalPesoIMC:Double=>Boolean = peso_estaEntre(18.5,25) 4 def sobrePesoIMC:Double=>Boolean = peso_estaEntre(25,29.9) 5 def obesidadIMC:Double=>Boolean = peso_estaEntre(30,100) 6 7 def imcClassificator(imcValue:Double):String = { 8 if (bajoPesoIMC(imcValue)) "Bajo peso" 9 else if (normalPesoIMC(imcValue)) "Peso normal" 10 else if (sobrePesoIMC(imcValue)) "Sobrepreso" 11 else "Obesidad" 12 } 13 def imcClas(weight:Double,height:Double) = imcClasificator(imcVal(weight,height)) En la solución anterior se ha definido la función de orden superior peso_estaEntre, currifica- da para facilitar la parcialización de la misma (dándole los límites inferior y superior del rango) en cada una de las definiciones de las funciones bajoPesoIMC, normalPesoIMC,...y dejando la variable value libre en cada una de los cierres generados con la definición de estas funciones. Finalmente se define la función imcClas, objetivo del ejercicio: 1 def imcClas(weight:Double,height:Double):(String) = imcClasificator(imcVal(weight,height))) Ejercicio 3 Definir una función imc que dada la altura y el peso de un individuo devuelva una tupla con su IMC y su clasificación. Página 69
  • 86. 1 def imc(weight:Double,height:Double):(Double,String) = { 2 val imcValor=imcVal(weight,height) 3 (imcValor,imcClasificator(imcValor)) 4 } Ejercicio 4 En un estudio presentado en 2011, se ha observado que un tercio de las personas con peso normal serían en realidad obesas si en lugar de éste, se midiera su grasa corporal total. Aunque el exceso de adiposidad es el verdadero culpable de las complicaciones asociadas a la obesidad y no el exceso de peso, los estudios que examinan los riegos para la salud asociados a la obesidad en los que se mide realmente la adiposidad son menos usuales de lo deseado. El porcentaje de grasa corporal puede medirse usando diferentes técnicas (análisis de impedancia bioeléctrica...). Cuando no es posible determinar el porcentaje de grasa corporal(BF %) se suele recurrir al IMC para medir la adiposidad. Cómo se ha visto con anterioridad, el cálculo del IMC corporal es fácil (peso/altura2 ) aunque no refleja precisamente la grasa corporal ya que los cambios en la composición del cuerpo que tienen lugar a lo largo de los diferentes periodos de la vida (edad) o el sexo de la persona, son variables que influyen notablemente en el cálculo del BF.26 En 2011 se publicó un nuevo estimador denominado CUN-BAE27 del porcentaje de BF que tenía en cuenta la edad, el sexo, la altura y el peso del individuo que se estaba estudiando: 1 BF % = - 44.988 + (0.503 * age) + (10.689 * sex) + (3.172 * BMI) - (0.026 * BMI2) + (0.181 * BMI * sex) - (0.02 * BMI * age) - (0.005 * BMI2 * sex) + (0.00021 * BMI2 * age) Definir en Scala la función CUN-BAE: 1 //sex=1=>mujer, sex=0=>hombre 2 def cun_bae (age:Double, sex:Double, weight:Double, height:Double):Double = { 3 require(sex==0 || sex==1) 4 def bf(age:Double, sex:Double, weight:Double, height:Double, bmiValue:Double, bmiValue2:Double) : Double = { 5 - 44.988 + (0.503 * age) + (10.689 * sex) + (3.172 * bmiValue) - (0.026 * bmiValue2) + (0.181 * bmiValue * sex) - (0.02 * bmiValue * age) - (0.005 * bmiValue2 * sex) + (0.00021 * bmiValue2 * age)} 6 7 val imc_val= imcVal(weight,height) 8 bf(age, sex, weight, height, imc_val, square(imc_val)) 9 10 11 } Ejercicio 5 Definir una función bfClas a la que se le pasarán como argumentos la edad, el sexo, el peso y la altura de un individuo y nos devuelva su clasificación basado en la predicción del porcentaje de grasa corporal calculado con la función CUN-BAE. Clasificación basada en BF% 26 Más información sobre este tema: Body mass index classification misses subjects with increased cardiometa- bolic risk factors related to elevated adiposity. Int J Obes 2011;in press. 27 Autores del predictor CUN-BAE: Gómez-Ambrosi J, Silva C, Galofré JC, Escalada J, Santos S, Millán D, Vila N, Ibañez P, Gil MJ, Valentí V, Rotellar F, Ramírez B, Salvador J, Frühbeck G.) Página 70
  • 87. normal => < 20% en hombres y < 30% en mujeres sobrepeso => 20%-25% en hombres y 30%-35% mujeres obesos => > 25% en hombres y > 35% en mujeres 1 def normalPesoBF(sex:Double):Double=>Boolean = peso_estaEntre(0+10*sex,20+10*sex) 2 def sobrepesoBF(sex:Double):Double=>Boolean = peso_estaEntre(20+10*sex,25+10*sex) 3 def obesidadBF(sex:Double):Double=>Boolean = peso_estaEntre(25+10*sex,100) 4 5 def bfClas(age:Double,sex:Double,weight:Double,height:Double):String = { 6 def bfClassificator(bfValue:Double):String={ 7 if (normalPesoBF(sex)(bfValue)) "Peso normal" 8 else if (sobrepesoBF(sex)(bfValue)) "Sobrepreso" 9 else "obesidad" 10 } 11 bfClassificator(cun_bae(age,sex,weight,height)) 12 } Ejercicio 6 Define una función bf que dada la edad, el sexo, la altura y el peso de un individuo devuelva una tupla con su BF (calculado con el método Cun-Bae) y su clasificación. 1 def bf(age:Double,sex:Double,weight:Double,height:Double) = (cun_bae(age,sex,weight,height),bfClas(age,sex,weight,height)) 3.8.2. Ejercicios Responder a las siguientes cuestiones: Ejercicio 2. Considérese el siguiente fragmento de código: 1 var a = 1 2 a = a + 1 En sentido estrictamente matemático, ¿se podría pensar en a como una variable, teniendo en cuenta la sentencia a = a + 1? Sí No Ejercicio 3. Si se define una función fun tal que: 1 def fun(x: Int) = x + x ¿Es “fun” una función pura? Sí No Página 71
  • 88. Ejercicio 4. Considérese la función getTime la cual devuelve la hora actual. ¿Es getTime una función pura? Sí No Ejercicio 5. Considérese la función random la cual devuelve un número aleatorio. ¿Es random una función pura? Sí No Ejercicio 6. Si se define una función f tal que: 1 def f() = { 2 10 3 20 4 } ¿Es f una función pura? Sí No Ejercicio 7. Si se define una función fun tal que: 1 def fun(f: Int=>Int, x: Int) = f(x) ¿Cuál es el tipo del valor devuelto por fun? Int Int =>Int Ninguna de las respuestas anteriores es correcta. ¿Cuál será el resultado de evaluar la expresión fun(x =>x, 10)? 10 Error de compilación Ejercicio 8. Considérense las siguientes soluciones para el cálculo del factorial de un número: 1 def fact1(n: Int) = { 2 var f = 1 3 var t = n 4 while (t > 0) { 5 f = f * t 6 t = t - 1 7 } 8 f 9 } 10 11 def fact2(n: Int):Int = 12 if (n == 0) 1 else n * fact2(n - 1) Página 72
  • 89. ¿Cuál de las dos presenta un aspecto más “matemático”? Razonando sobre la corrección de las dos funciones y teniendo en cuenta el flujo de control y la secuencia de las operaciones: • ¿Cuál de las dos versiones presenta un problema de corrección? • ¿Cómo se solucionaría? Ejercicio 9. Si se tiene la función fun definida tal que: 1 def fun(f: Int => String, g: String => Int, x: Int) = g(f(x)) ¿Cuál es el tipo del valor devuelto por fun? • String • Int ¿Cuál el resultado de evaluar la expresión fun(x =>10, y =>"hola", 20)? • Error de compilación • 10 • 20 ¿Y el resultado de evaluar fun(x =>"hola", y =>15, 20)? • Error de compilación • 15 • 20 ¿Y el resultado de evaluar fun(x =>"hello", y =>10, "20")? • Error de compilación • 10 • 20 Ejercicio 10. Definida la función fun tal que: 1 def fun(x: Int, y: Int):Int=>Int = 2 z => (x + y) + z ¿Cuál será la salida de println(fun(1,2)(3))? Dará error 3 5 6 Ejercicio 11. Dada la función fun tal que: Página 73
  • 90. 1 def fun(x: Int):(Int, Int)=>Int = 2 (y, z) => x + y + z ¿Cuál de las siguientes expresiones dará error? fun(1,2)(3) fun(1)(2,3) Ejercicio 12. Dadas las funciones fun1 y fun2 definidas a continuación: 1 def fun1():Int => Int = { 2 val y = 1 3 def add(x: Int) = x + y 4 5 add 6 } 7 8 def fun2() = { 9 val y = 2 10 val f = fun1() 11 println(f(10)) 12 } ¿Qué se imprimirá por pantalla si se invoca a fun2()? 10 11 1 Ninguna de las anteriores respuestas es correcta Ejercicio 13. Dado el siguiente fragmento de código: 1 def cuadrado(x: Int) = x*x 2 def cubo(x: Int) = x*x*x 3 4 def componer(f: Int=>Int, g: Int=>Int): Int=>Int = 5 x => f(g(x)) 6 7 val f = componer(cuadrado, cubo) 8 val a=List(4,1,3,4,7,8,9,10) ¿Es cierta la igualdad a.map(f) == a.map(cuadrado).map(cubo)? Sí No Ejercicio 14. Dadas las siguientes definiciones de: Página 74
  • 91. 1 def hello() = { 2 println("hello") 3 10 4 } 5 6 def fun(x: => Int) = { 7 x + x 8 } ¿Cuál será el resultado de evaluar la expresión val t = fun(hello())? • 10 • 20 • Error ¿Qué se mostraría por pantalla si se evalúa la expresión anterior? Ejercicio 15. Definir una función que devuelva el número de dígitos que tiene un valor entero pasado como argumento. Ejercicio 16. Definir la función aprueba que tome como parámetro una lista de enteros con las calificaciones de los alumnos y apruebe con un 5 a aquellos que tengan una calificación inferior. Ejercicio 17. Definir la función eliminaBajos que tome como parámetros una lista de enteros xs y un valor entero cota y devuelva una lista de enteros eliminando los elementos menores que la cota. Ejercicio 18. Definir una función que devuelva un entero con los dígitos del parámetro entero en orden inverso, sabiendo que el valor del parámetro tiene que ser mayor que cero. Ejercicio 19. Definir una función que indique si el valor entero pasado como argumento es capicúa(true) o no(false). Ejercicio 20. Definir una función para calcular los número de Fibonacci que sea recursiva de cola. Ejercicio 21. Definir la función descomponer que convierta un número entero, pasado por parámetro, de segundos en horas, minutos y segundos. Ejercicio 22. Definir una función mcd que calcule el máximo común divisor de dos números pasados como argumentos y la función coprimos que nos diga si dos números pasados como argumentos son coprimos. Ejercicio 23. A la siguiente representación de números se le conoce como el “Triángulo de Pascal”: 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 . . . Página 75
  • 92. Los números de los extremos de cada fila son 1 y el resto de elementos se calculan sumando los dos números situados justamente encima. Se pide: Escribir una función que tome la columna y la fila y nos devuelva el número que corres- ponde a esa posición (para optimizar la función se deberá tener en cuenta que tanto la columna, como la fila no podrán ser números negativos y que la columna debe de ser me- nor o igual que la fila). Calcular los números del triángulo de Pascal de forma recursiva. Ejercicio 24. Escribir una función que verifique que los paréntesis de una cadena pasado co- mo parámetro están balanceados. Implementar la misma función si el parámetro es del tipo List[Char](sin hacer uso de los métodos de List). 3.9. Programación funcional estricta y perezosa 3.9.1. Funciones estrictas y no estrictas Se dirá que una expresión no termina si la evaluación de la misma se ejecuta de forma indefinida o lanza un error en lugar de devolver un valor. En la Sección 1.4: Evaluación en Scala « página 18 » se pudo ver cómo los lenguajes funcio- nales pueden presentar diferentes estrategias a la hora de evaluar sus expresiones y que también se empleaban para evaluar las funciones. Se llamarán funciones estrictas a aquellas funciones que utilizan una estrategia de evalua- ción estricta para evaluar sus argumentos. Por tanto, una de las características de las funciones estrictas es que siempre evalúan sus argumentos antes de comenzar con la evaluación del cuerpo de la función. Dependiendo del lenguaje de programación, la definición de funciones estrictas puede ser una constante en programación funcional28 . Es más, muchos lenguajes funcionales no proveen una forma de definir funciones que no sean estrictas. La gran mayoría de las funciones definidas hasta el momento son estrictas, como por ejemplo la función valor absoluto definida en el algoritmo 1.6, en la página 22, que se muestra a continuación: 1 def abs(x: Int) = if (x > 0) then x else -x Algoritmo 3.4: Función estricta valor absoluto Si se invoca la función abs con el argumento (75 - 20) nos devolverá el valor 55. En cambio, si se invoca la función con el argumento (sys.error("fallo")) devolverá una excepción, ya qué la expresión sys.error("fallo") se evaluará antes que el cuerpo de la función. Las funciones no estrictas son aquellas que no evalúan sus argumentos, es decir, que siguen la estrategia evaluación no estricta para la evaluación de sus argumentos. Por tanto, como se vio previamente en la Sección 1.4: Evaluación en Scala « página 18 », una función puede ser estricta o no en cada argumento ya que Scala provee una sintaxis (=>) especial para indicar qué argumentos no serán evaluados y que el compilador se asegurará de que sean pasados al cuerpo de la función sin haber sido evaluados previamente. Ejemplo de función no estricta: 1 def hacerPar(x: => Int):(Int,Int) = (x,x) Algoritmo 3.5: Ejemplo de función no estricta 28 No será una constante en lenguajes de programación funcional como Haskell, Lean o Miranda. Página 76
  • 93. Uno de los inconvenientes que presenta la estrategia de evaluación evaluación no estricta y que, por tanto, presentan las funciones no estrictas, es que los argumentos se han de evaluar cada vez que se hace referencia a ellos en el cuerpo de la función. Este comportamiento se ha ejemplificado invocando la función hacerPar definida en el algoritmo 3.5 con el bloque println("hola");1+41;: scala> hacerPar { println("hola"); 1 + 41;} hola hola res4: (Int, Int) = (42,42) En el ejemplo anterior se puede observar como el efecto colateral de imprimir por pantalla el mensaje “hola” se produce dos veces, una por cada vez que se tiene que evaluar dicho bloque, ya que aparecen dos referencias a x en el cuerpo de la función. Cuando se desea que no se tengan que evaluar los argumentos cada vez que sean referencia- dos en el cuerpo de una función no estricta, entonces se tendrá que emplear una estrategia de evaluación conocida como evaluación perezosa (Call by Need). Esta estrategia de evaluación, que siguen lenguajes de programación funcional como Haskell, es tal que, una vez evalúe por primera vez el argumento, el valor de dicho argumento será guardado para que, si posterior- mente se hace una referencia al mismo valor, no sea necesario volver a evaluarlo. En Scala se puede utilizar la palabra reservada lazy para indicar que la evaluación de una val se poster- gue hasta encontrar la primera referencia. Después el valor será almacenado para evitar más reevaluaciones. 1 def hacerPar2(x: => Int):(Int,Int) = {lazy val j=x;(j,j)} Algoritmo 3.6: Ejemplo de función no estricta con estrategia Call By Need Se puede ver cómo cambia el comportamiento de la función hacerPar2 si se invoca con el mismo bloque que se invocó previamente la función hacerPar del algoritmo 3.5: scala> hacerPar2{ println("hola"); 1 + 41;} hola res6: (Int, Int) = (42,42) En realidad, aunque se han utilizado las estrategias de evaluación no estricta y evaluación perezosa, las funciones hacerPar y hacerPar2 serían estrictas29 , ya que siempre evalúan todos sus argumentos. Se puede ver un ejemplo de función no estricta, implementando la selectiva if de la siguiente forma: 1 def if2[A](cond: Boolean, cierto: => A, falso: => A): A = 2 if (cond) cierto else falso Algoritmo 3.7: Selectiva if como función no estricta Se podría realizar la siguiente invocación de la función if2 (false, sys.error(“error”), 4), obteniendo el resultado: scala> if2(false, sys.error("error"), 4) res8: Int = 4 Se puede apreciar que el segundo argumento no llega a evaluarse, por lo que no se produce el error. El uso de las diferentes estrategias de evaluación ofrece al programador una gran capacidad a la hora de separar la descripción de una expresión y la evaluación de la misma, por ejemplo, escribiendo expresiones de las que sólo se evalúe una parte. 29 Ambas funciones tiene como valor devuelto una tupla y las tuplas en Scala son estrictas. Página 77
  • 94. 3.10. Estructuras de Datos 3.10.1. Introducción 3.10.1.1. ¿Qué es una teoría?. Definición de Estructuras de Datos La teoría de tipos hace referencia al diseño, análisis y estudio de los sistemas de tipos, por tanto, las teorías serán empleadas para definir las estructuras de datos y fundamentalmente consisten en: Uno o más tipos de datos. Operaciones definidas entre esos tipos de datos. Reglas que describen las relaciones entre los valores y las operaciones. Normalmente, una teoría no describe mutaciones, por tanto, a la hora de definir una teoría habrá que concentrarse en: Definir teorías para los operadores. Minimizar los cambios de estado Tratar a los operadores como funciones, en ocasiones compuestos de funciones simples. 3.10.1.2. La abstracción en la programación La abstracción es un mecanismo fundamental para la comprensión de fenómenos o situa- ciones que implican gran cantidad de detalles. La idea de abstracción es uno de los conceptos más potentes en el proceso de resolución de problemas. Se entiende por abstracción la capacidad de manejar un objeto (tema o idea) como un concepto general, sin considerar la enorme canti- dad de detalles que pueden estar asociados con dicho objeto. Sin abstracción no sería posible manejar, ni siquiera entender, la gran complejidad de ciertos problemas. En el proceso de programación, se puede extender el concepto de abstracción tanto a las acciones que debe realizar un programa mediante la técnica “Divide y Vencerás”30 (resolviendo cada subproblema en un subprograma independiente), como a los datos, mediante tipos abstrac- tos de datos. En el proceso de abstracción aparecen dos aspectos complementarios: Identificar los detalles esenciales del problema. Ignorar los aspectos no esenciales para la resolución del problema. 30 El término “Divide y Vencerás”, en su acepción más amplia, es algo más que una técnica de diseño de algo- ritmos. De hecho, suele ser considerada una filosofía general para resolver problemas y de aquí que su nombre no sólo forme parte del vocabulario informático, sino que también se utiliza en muchos otros ámbitos. En el ámbito de la informática “Divide y Vencerás” es una técnica de diseño de algoritmos que consiste en resolver un problema a partir de la solución de subproblemas del mismo tipo, pero de menor tamaño. Si los subproblemas son todavía relativamente grandes se aplicará de nuevo esta técnica hasta alcanzar subproblemas lo suficientemente pequeños como para ser solucionados directamente[31]. Página 78
  • 95. 3.10.1.3. Datos, Tipos de Datos, Estructuras de Datos y Tipos Abstractos de Datos No se debe confundir los conceptos de: tipo de datos, estructura de datos y tipo abstracto de datos. Todos ellos constituyen diferentes niveles en el proceso de abstracción referido a los datos. Los datos son las propiedades o atributos (cualidades o cantidades) sobre hechos u objetos del problema que queremos resolver. Dependiendo de las propiedades, será tarea del programa- dor determinar el tipo de datos más apropiado para definir cada una de ellas. El tipo de datos, en un lenguaje de programación, define el conjunto de valores que una determinada variable puede tomar, así como las operaciones aplicables sobre dicho conjunto. Definen cómo se representa la información y cómo se interpreta. Los tipos de datos, atendiendo a su naturaleza, pueden ser clasificados en: Tipos de datos simples31 . Son aquellos que almacenan un único valor. Tipos de datos compuestos. Son aquellos que almacenan una agregación o colección de valores. Los tipos de datos, atendiendo a su definición, pueden ser clasificados en: Tipos de datos primitivos. Son aquellos que son proporcionados por el lenguaje de pro- gramación y que, por tanto, no necesitan ser definidos por el usuario32 . Tipos de datos definidos por el usuario. Aquellos tipos de datos definidos a partir de los tipos de datos primitivos. Los tipos de datos constituyen un primer nivel de abstracción, ya que no se tiene en cuenta cómo se representa la información, ni cómo se manipula. Sólo se hará uso de las operaciones proporcionadas para cada tipo de datos. Se pueden definir las estructuras de datos como agrupaciones de datos que guardan alguna relación entre si y sobre las que se definirán operaciones. Las estructuras de datos las podemos clasificar: Según su naturaleza: • Homogéneas. Formadas por elementos del mismo tipo de datos. • Heterogéneas. Formadas por elementos de varios tipos de datos. Según su relación: • Lineales. Las estructuras lineales se caracterizan por el hecho de que sus elementos están en secuencia, relacionados en forma lineal, es decir, la relación que se esta- blece entre los elementos de la estructura de datos es de sucesión y precedencia (independientemente de cual sea el elemento de información). Ejemplos de estruc- turas de datos lineales son las colas, las pilas,...33 31 También llamados tipos de datos elementales. 32 Los tipos de datos pueden ser primitivos y no elementales, como por ejemplo los conjuntos. 33 La lista doblemente enlazada es una estructura de datos lineal en la que cada elemento está relacionado con dos elementos (tiene dos punteros): el sucesor y el predecesor. Si se elimina una de estas relaciones se transformaría en una lista simple. En cambio, si en una estructura no lineal en la que cada elemento se relaciona con otros dos elementos, como por ejemplo ocurre en un Árbol binario de búsqueda (ABB), eliminamos una de estas relaciones, destruiríamos dicha estructura. Página 79
  • 96. • No lineales. Estructuras de datos que presentan una organización complementaria a las estructuras de datos lineales en la que cada elemento puede estar relacionado con múltiples diferentes elementos de la estructura mediante una organización no lineal. Ej. Árboles, Grafos... Al igual que ocurría con los tipos de datos, es posible usar estructuras de datos proporcio- nadas por el lenguaje de programación o definir nuevas estructuras de datos. Las operaciones permitidas sobre una estructura de datos es otra de las características pro- pias de las mismas. Algunas operaciones típicas sobre estructuras de datos son: Buscar y acceder a los elementos (por la posición que ocupan en la estructura o por la información que contienen), Insertar o borrar elementos, Modificar las relaciones entre los elementos, etc. Las estructuras de datos serían un nuevo nivel de abstracción sobre los datos, donde ya no importará las operaciones definidas sobre los elementos de la estructura sino las operaciones que implican a la estructura34 . Un Tipo abstracto de datos (TADs) oculta al usuario los detalles de su implementación, mostrando sólo la interfaz de acceso a los datos del tipo, también llamada signatura, junto a una semántica de las operaciones[25]. Como se ha visto anteriormente, un tipo consta de: Un conjunto de valores. Un conjunto de operaciones. Se tendrán que representar los distintos valores del tipo en función de otros tipos simples o de otros tipos de datos compuestos ya creados. Además, habrá que implementar el conjunto de operaciones que se hayan definido para el TADs35 . La única forma de la que se dispondrá para usar los datos del tipo definido será a través de las operaciones definidas en su interfaz, por lo que, cuando un programador haga uso del tipo no conocerá su representación interna. Es más, se podría cambiar la representación del tipo de datos sin que el funcionamiento del programa cliente se viera afectado. Para indicar el funcionamiento de las operaciones sobre un tipo de datos se utilizará la especificación algebraica con constructores, basada en describir el comportamiento mediante ecuaciones36 [15]. 3.10.2. Definición de Estructuras de Datos en Lenguajes Funcionales 3.10.2.1. Introducción En la introducción a la programación funcional se dijo que unas de las principales caracte- rísticas de la misma eran: 34 El programador que use las estructuras de datos, al igual que ocurría en el caso de los tipos de datos, descono- cerá cómo han sido implementadas las operaciones con las que podrá interactuar con la estructura de datos o cómo se representa internamente la misma. 35 Obviamente, cuando se implementen las operaciones sobre el TADs, sí se tendrá acceso a la representación del mismo. 36 Basado en el modelo de sustitución – ver la Sección 1.4: Evaluación en Scala « página 18 » – Página 80
  • 97. Variables inmutables (no actualizar el valor de una variable). Estructuras de datos inmutables Cuando se plantea la definición de estructuras de datos funcionales, pueden surgir algunas preguntas: ¿Qué estructuras de datos se pueden usar en programación funcional? ¿Cómo se podrá operar con estas estructuras de datos funcionales? ¿Cómo se definen estructuras de datos en Scala? A lo largo de este capítulo se intentará dar respuesta a estas preguntas, haciendo uso de los conceptos propios de la programación funcional vistos anteriormente, como son: funciones puras, funciones anónimas, orden superior, polimorfismo ... 3.10.2.2. Definición Para operar sobre estructuras de datos funcionales, se utilizarán funciones puras. Las estruc- turas de datos funcionales serán inmutables. En general, para crear una estructura de datos nueva, un tipo de datos nuevo, se usará la palabra clave trait. Un trait es una interfaz abstracta que puede incluir la implementación de algunos métodos37 . Si se añade la palabra clave sealed delante de trait se estará indicando que todas las implementaciones del rasgo que se va a definir deberán ser declaradas en el mismo archivo. Ejem. sealed trait List[T]. Para definir los constructores del tipo de datos (constructores de datos) se utilizará la pala- bra reservada case con las que se representarán cada una de las posibles formas de la estructura de datos. La declaración de un constructor de datos proporcionará una función para construir cada una de las posibles formas que pueda tener nuestra tipo de datos. Al igual que las funciones pueden ser polimórficas, las estructuras de datos también pueden serlo. Para definir una estructura de datos polimórfica se deberá incluir una lista de variables de tipo, encerrada entre corchetes, en la declaración de la estructura de datos. Posteriormente, se podrá hacer uso de estas variables de tipo en la definición de los constructores de datos y en los métodos. Las operaciones que se quieran definir sobre el tipo de datos se definirán en un objeto acom- pañante de la clase acompañante. Compartición estructural (Data Sharing) Cuando los tipos de datos son inmutables, ¿cómo se escribirán funciones que, por ejemplo, añadan o eliminen un elemento de una lista? La respuesta es simple, cuando se añade un elemento (por ejemplo 1) como primer elemento de una lista xs, se devuelve una nueva lista (en este caso, Cons(1,xs)) Teniendo en cuenta que los tipos de datos son inmutables, no se necesitará realmente copiar la lista xs. Se podrá reutilizar. Esta propiedad de los tipos de datos inmutables se denomina compartición estructural o compartición de datos (data sharing o sharing). 37 Véase la Subsección 2.3.1: Herencia en Scala « página 38 » para ampliar la información sobre los rasgos (traits) en Scala. Página 81
  • 98. La compartición estructural permitirá implementar funciones de una forma más eficiente. Esta propiedad de los datos inmutables ofrece la posibilidad de devolver estructuras de datos inmutables sin tener la preocupación de que posteriormente el código modifique sus datos y por tanto, sin la necesidad de hacer “copias de seguridad” pesimistas de la estructura de datos38 . 3.10.2.3. Los Naturales Se denotará por N al conjunto de números naturales, cuyos elementos son suma de un nú- mero finito de unos: N = {0, 1, 2, 3, 4, . . .} Es importante recordar que N es cerrado para la suma y el producto, pero no lo es para la resta o para la división (3 − 8 /∈ N, 4 5 /∈ N). A continuación se muestra la especificación algebraica correspondiente a la especificación de los números naturales: especificación NATURALES usa BOOLEANOS tipos nat operaciones cero :−→ nat{constructora} suc : nat −→ nat{constructora} _ + _ : nat nat −→ nat _ ∗ _ : nat nat −→ nat _ˆ_ : nat nat −→ nat _ === _ : nat nat −→ bool _! == _ : nat nat −→ bool _ ≤ _ : nat nat −→ bool _ < _ : nat nat −→ bool _ ≥ _ : nat nat −→ bool _ > _ : nat nat −→ bool max : nat nat −→ nat min : nat nat −→ nat _/_ : nat nat −→ nat _ %_ : nat nat −→ nat es_par : nat −→ bool es_impar : nat −→ bool variables n,m: nat ecucaciones n + cero = n n + suc(m) = suc(n + m) 38 Algo que se convierte en un problema cuando se desarrollan programas grandes, en los que los datos tienen que ser pasados a través de diversos componentes, los cuales se ven obligados a hacer “copias de seguridad” de los mismos. Página 82
  • 99. cero ∗ m = 0 suc(n) ∗ m = (n ∗ m) + m cero − cero = cero cero − m = error suc(n) − cero = suc(n) suc(n) − suc(m) = n − m ceroˆcero = error nˆcero = suc(cero) nˆsuc(m) = n ∗ (nˆm) cero === cero = cierto cero === suc(m) = falso suc(n) === cero = falso suc(n) === suc(m) = n == m n! == m =!(n === m) cero ≤ m = cierto suc(n) ≤ cero = falso suc(n) ≤ suc(m) = n ≤ m n < m = n ≤ m&&n = m n ≥ m = m ≤ n n > m = m < n max(cero, m) = m max(suc(n), cero) = suc(n) max(sun(n), suc(m) = suc(max(n, m)) min(cero, m) = cero min(suc(n), cero) = cero min(suc(n), suc(m)) = suc(min(n, m)) n/cero = error n/m = cero ⇐ n < m n/m = suc((n − m)/m) ⇐ m = cero && m ≤ n n %cero = error n %m = n ⇐ n < m n %m = (n − m) %m ⇐ m = cero && m ≤ n es_par(cero) = cierto es_par(suc(n)) = es_impar(n) es_impar(cero) = falso Página 83
  • 100. es_impar(suc(n)) = es_par(n) Se definirán los números naturales. Tendrán dos constructores: Cero y Suc, además de las operaciones que aparecen en la especificación algebraica de los mismos: 1 2 trait Nat { 3 4 def + (y:Nat):Nat = y match { 5 case Cero => this 6 case Suc(m) => Suc(this+m) 7 } 8 9 def * (m:Nat):Nat = this match{ 10 case Cero => Cero 11 case Suc(n) => (n * m) + m 12 } 13 14 def - (m:Nat):Nat = this match{ 15 case Cero if m==Cero => Cero 16 case Cero => throw new Exception("Error en la resta") 17 case Suc(n) => m match { 18 case Cero => this 19 case Suc(t) => n - t 20 } 21 } 22 23 def ^ (m:Nat):Nat= m match{ 24 case Cero if this==Cero=> throw new Exception("Indeterminacion 0^0") 25 case Cero => Suc(Cero) 26 case Suc(t)=> this * (this ^ t) 27 } 28 29 def === (m:Nat):Boolean = this match { 30 case Cero if m==Cero => true 31 case Suc(n) => m match { 32 case Suc(t) => n==t 33 case Cero => false 34 } 35 case _ => false 36 } 37 38 def !== (m:Nat):Boolean = !(this===m) 39 40 def <= (y:Nat):Boolean = this match{ 41 case Cero => true 42 case Suc(n)=> y match{ 43 case Cero => false 44 case Suc(m) => n <= m 45 } Página 84
  • 101. 46 } 47 48 def < (y:Nat):Boolean= (this <= y) && (this !== y) 49 50 def >= (y:Nat):Boolean = y <= this 51 52 def > (y:Nat):Boolean = y < this 53 54 def / (y:Nat):Nat = y match{ 55 case Cero => throw new Exception("Error division por cero") 56 case Suc(m) if this < y => Cero 57 case Suc(m) => Suc((this - y) / y) 58 } 59 60 def % (y:Nat):Nat = y match{ 61 case Cero => throw new Exception("Error calculando resto en division por cero") 62 case Suc(m) if this < y => this 63 case Suc(m) => (this - y) % y 64 } 65 66 67 } 68 case object Cero extends Nat 69 case class Suc(elem:Nat) extends Nat 70 71 object Nat{ 72 73 def max (x:Nat,y:Nat):Nat = x match{ 74 case Cero => y 75 case Suc(n) => y match{ 76 case Cero => x 77 case Suc(m) => Suc(max(n,m)) 78 } 79 } 80 81 def min(x:Nat,y:Nat):Nat = x match{ 82 case Cero => Cero 83 case Suc(n) => y match{ 84 case Cero => Cero 85 case Suc(m) => Suc(min(n,m)) 86 } 87 } 88 89 def es_Par(x:Nat):Boolean = x match{ 90 case Cero => true 91 case Suc(n) => es_Impar(n) 92 } 93 94 def es_Impar(x:Nat):Boolean = x match{ 95 case Cero => false Página 85
  • 102. 96 case Suc(n) => es_Par(n) 97 } 98 99 } Ejercicios resueltos. Completar la implementación de la especificación de los números naturales con: 1. La función toInt, que devolverá el valor de tipo Int correspondiente al número natural. 2. La función pred, que devolverá un valor de tipo Nat correspondiente al número natural anterior al pasado como argumento a la función. Para facilitar la resolución del ejercicio se podrá considerar que el predecesor de 0 es 0. 3. La función es_Primo, que nos indicará si el número natural pasado como argumento a la función es un número primo. 4. La función mcd, que devolverá el máximo común divisor de dos números naturales pasa- dos como argumentos. 5. La función coprimos, que nos indicará si dos números pasados como argumentos son coprimos. 6. La función cuadrado, que devolverá el cuadrado del número natural pasado como argu- mento. 7. El método de fábrica apply, con el que se podrá crear un número natural a partir de un número del tipo Int El algoritmo 3.8 muestra la implementación final de los números naturales con una posible solución a los ejercicios anteriores. 1 trait Nat { 2 3 def + (y:Nat):Nat = y match { 4 case Cero => this 5 case Suc(m) => Suc(this+m) 6 } 7 8 def * (m:Nat):Nat = this match{ 9 case Cero => Cero 10 case Suc(n) => (n * m) + m 11 } 12 13 def - (m:Nat):Nat = this match{ 14 case Cero if m==Cero => Cero 15 case Cero => throw new Exception("Error en la resta") 16 case Suc(n) => m match { 17 case Cero => this 18 case Suc(t) => n - t 19 } Página 86
  • 103. 20 } 21 22 def ^ (m:Nat):Nat= m match{ 23 case Cero if this==Cero=> throw new Exception("Indeterminacion 0^0") 24 case Cero => Suc(Cero) 25 case Suc(t)=> this * (this ^ t) 26 } 27 28 def === (m:Nat):Boolean = this match { 29 case Cero if m==Cero => true 30 case Suc(n) => m match { 31 case Suc(t) => n==t 32 case Cero => false 33 } 34 case _ => false 35 } 36 37 def !== (m:Nat):Boolean = !(this===m) 38 39 def <= (y:Nat):Boolean = this match{ 40 case Cero => true 41 case Suc(n)=> y match{ 42 case Cero => false 43 case Suc(m) => n <= m 44 } 45 } 46 47 def < (y:Nat):Boolean= (this <= y) && (this !== y) 48 49 def >= (y:Nat):Boolean = y <= this 50 51 def > (y:Nat):Boolean = y < this 52 53 def / (y:Nat):Nat = y match{ 54 case Cero => throw new Exception("Error division por cero") 55 case Suc(m) if this < y => Cero 56 case Suc(m) => Suc((this - y) / y) 57 } 58 59 def % (y:Nat):Nat = y match{ 60 case Cero => throw new Exception("Error calculando resto en division por cero") 61 case Suc(m) if this < y => this 62 case Suc(m) => (this - y) % y 63 } 64 65 //******** Ejercicios resueltos ********* 66 val toInt:Int= this match{ 67 case Cero => 0 68 case Suc(x)=> 1 + x.toInt Página 87
  • 104. 69 } 70 } 71 72 case object Cero extends Nat 73 case class Suc(elem:Nat) extends Nat 74 75 object Nat{ 76 77 def max (x:Nat,y:Nat):Nat = x match{ 78 case Cero => y 79 case Suc(n) => y match{ 80 case Cero => x 81 case Suc(m) => Suc(max(n,m)) 82 } 83 } 84 85 def min(x:Nat,y:Nat):Nat = x match{ 86 case Cero => Cero 87 case Suc(n) => y match{ 88 case Cero => Cero 89 case Suc(m) => Suc(min(n,m)) 90 } 91 } 92 93 def es_Par(x:Nat):Boolean = x match{ 94 case Cero => true 95 case Suc(n) => es_Impar(n) 96 } 97 98 def es_Impar(x:Nat):Boolean = x match{ 99 case Cero => false 100 case Suc(n) => es_Par(n) 101 } 102 //********* Ejercicios resueltos ************** 103 def pred(x:Nat)= x match{ 104 case Cero => Cero 105 case _ => x - Suc(Cero) 106 } 107 def suc(x:Nat)=Suc(x) 108 def esDivisible(x:Nat)(y:Nat):Boolean= (x % y) == Cero 109 private def compruebaDesdeHasta (x:Nat) (y:Nat)(f:Nat=>Nat=>Boolean):Boolean= 110 if (x==y) true 111 else !f (y)(x) && compruebaDesdeHasta(suc(x))(y)(f) 112 113 def es_Primo(x:Nat):Boolean= x match{ 114 case Cero => false 115 case Suc(Cero)=> false 116 case otro=>compruebaDesdeHasta(Suc(Suc(Cero)))(x)(esDivisible) 117 } 118 Página 88
  • 105. 119 @annotation.tailrec 120 def mcd (x:Nat)(y:Nat): Nat = if (y==Cero) x else mcd (y) (x % y) 121 122 def coprimos(x:Nat, y:Nat):Boolean = mcd(x)(y)== Suc(Cero) 123 124 def cuadrado(x:Nat):Nat =x^(Suc(Suc(Cero))) 125 126 def apply(ent:Int):Nat= ent match{ 127 case 0 => Cero 128 case x => Suc(apply(x-1)) 129 } 130 } Algoritmo 3.8: Implementación final del tipo Naturales 3.10.3. Estructuras de datos lineales. Listas 3.10.3.1. TAD Lista Las listas son estructuras inmutables homogéneas, es decir, que contienen datos del mismo tipo. Además, son las estructuras inmutables de datos lineales más flexibles, puesto que su única característica es imponer un orden entre los elementos39 almacenados en ellas[15]. A continuación, se definirá el tipo de datos Lista como un tipo recursivo: Dado un alfabeto V, se define el conjunto de secuencias o cadenas de elementos de V, denotado por V ∗ , como: Nil V∗ ∀s V∗ : ∀v V : v.s V ∗ Nil representará la lista vacía, mientras que el resto de listas se definirán como el añadido de un elemento por la izquierda a una lista ya existente.[10] El comportamiento de las listas es independiente del tipo de sus elementos, por lo que se especificarán de forma paramétrica. Se han escogido dos constructores: 1. La lista vacía (Nil) 2. La operación que añade un elemento por la izquierda a una lista (operación Cons40 ). Se implementarán también las operaciones típicas sobre listas: esVacia nos indicará si una lista contiene algún elemento o no. longitud devolverá un entero con el número de elementos de la lista. _ ## _ añadirá un elemento al final de la lista _ ++ _ devolverá el resultado de concatenar dos listas. 39 Los elementos de una lista pueden estar repetidos 40 Otras especificaciones de listas utilizan el constructor :: en lugar de Cons por lo que se añadirá la operación :: para poder construir listas de la forma (1::2::3::4::Nil) aunque no se podrá utilizar :: como patrón Página 89
  • 106. izquierdo41 devolverá el elemento situado más a la izquierda de la lista. elim-izq42 eliminará el elemento situado más a la izquierda de la lista. derecho devolverá el elemento situado más a la derecha de la lista. elim-der eliminará el elemento situado más a la derecha de la lista. drop permite eliminar una cantidad de elementos del principio de la lista. Así, la especificación de las listas será: especificación LISTA[ELEM] usa BOOLEANOS,NATURALES,ENTEROS tipos lista operaciones Nil :−→ lista{constructora} Cons(_, _) : elemento lista −→ lista{constructora} esV acia : lista −→ bool longitud : lista −→ int _##_ : lista elemento −→ lista _ + +_ : lista lista −→ lista izquierdo : lista −→ elemento elim − izq : lista −→ lista derecho : lista −→ lista elim − der : lista −→ lista drop : lista nat −→ lista variables n,m: elemento x,y,z: lista a: nat ecuaciones izquierdo(Nil) = error izquierdo(Cons(e, x)) = e elim − izq(Nil) = error elim − izq(Cons(e, x)) = x derecho(Nil) = error derecho(Cons(e, Nil)) = e derecho(Cons(e, x)) = derecho(x) elim − der(Nil) = error elim − der(Cons(e, Nil)) = Nil 41 Se corresponde con la operación cabeza 42 Se corresponde con la operación cola Página 90
  • 107. elim − der(Cons(e, x)) = elim − der(x) esV acia(Nil) = cierto esV acia(Cons(e, x)) = falso Nil + +y = y Cons(e, x) + +y = Cons(e, x + +y) longitudNil = 0 longitud(Cons(e, x)) = 1 + longitud(x) Nil + +x = x Cons(e, xs) + +y = Cons(e, xs + +y) x##e = x + +Cons(e, Nil) drop(x, 0) = x drop(Nil, a) = Nil drop(Cons(e, x), a) = drop(x, a − 1) Una posible implementación en Scala de la especificación del tipo abstracto de datos Lista definido anteriormente sería: 1 2 sealed trait Lista[+A]{ 3 def cabeza: A 4 def cola: Lista[A] 5 6 /** Version recursiva para calcular la longitud de una lista */ 7 def longitud:Int={ 8 @annotation.tailrec 9 def length[A](xs:Lista[A],count:Int):Int= xs match{ 10 case Nil => count 11 case Cons(_,xs) => length(xs,count+1) 12 } 13 length(this,0) 14 } 15 16 /** Devuelve el elemento situado mas a la izquierda de una lista */ 17 def izquierdo:A= this match { 18 case Nil => throw new NoSuchElementException("Lista vacia!!") 19 case _ => cabeza 20 } 21 22 /** Elimina el elemento situado mas a la izquierda de una lista*/ 23 def elim_izq:Lista[A]= this match { 24 case Nil => throw new NoSuchElementException("Lista vacia!!") 25 case _ => cola 26 27 } Página 91
  • 108. 28 /** Devuelve el elemento situado mas a la derecha de una lista */ 29 def derecho:A= this match{ 30 case Nil => throw new Exception("Lista vacia!!, sin elementos") 31 case Cons(elem,Nil)=>elem 32 case Cons(e,xs)=>xs derecho 33 } 34 /** Elimina el elemento situado mas a la derecha de una lista */ 35 def elim_der:Lista[A]= this match{ 36 case Nil => Nil 37 case Cons(elem,Nil)=>Nil 38 case Cons(elem,xs)=>Cons(elem,xs elim_der) 39 } 40 def esVacia:Boolean = this match{ 41 case Nil => true 42 case Cons(e,x) => false 43 } 44 /** Operador que nos permite construir listas del tipo 1::2::3::Nil */ 45 def ::[U >: A](x: U): Lista[U] = Cons(x,this) 46 47 /** Operador que nos permite insertar un elemento al final de la lista */ 48 def ##[U >: A](x:U):Lista[U]=this++(Cons(x,Nil)) 49 50 /** Operador para concatenar listas **/ 51 def ++[U >: A](ys:Lista[U]):Lista[U]= this match{ 52 case Nil => ys 53 case Cons(e,x) => Cons(e,x++ys) 54 } 55 56 case object Nil extends Lista[Nothing]{ 57 def cabeza: Nothing = throw new NoSuchElementException("cabeza de la lista vacia") 58 def cola:Nothing = throw new NoSuchElementException("cola lista vacia") 59 } 60 final case class Cons[A] (cabeza:A, cola:Lista[A]) extends Lista[A]{ 61 override def toString:String=cabeza+"::"+cola 62 } 63 64 object Lista { 65 def drop[A](ys:Lista[A])(n:Nat):Lista[A]= (ys,n) match { 66 case (Nil,_) => Nil 67 case (xs,Cero)=> xs 68 case (Cons(e,xs),n)=> drop (xs) (n-Nat(1)) 69 } 70 def apply[A](as: A*): Lista[A] ={ 71 if (as.isEmpty) Nil 72 else as.head :: apply(as.tail: _*) 73 } 74 } Página 92
  • 109. Algoritmo 3.9: Implementacion TAD Lista A continuación se ampliarán las operaciones sobre el tipo Lista que se está implementando: esta, dado un elemento, devolverá true si el elemento está en la lista y false si el elemento no está en la lista. posicion, devolverá un valor entero con la posición de la primera aparición de un elemento dado en la lista. La primera posición de la lista será la posición 0 y la última posición será n − 1, considerando n = length(xs). Si devuelve -1 indicará que el elemento no se encuentra en la lista. repeticiones, devolverá un valor entero con el número de repeticiones de un elemento dado en la lista. eliminarTodos, eliminará todas las apariciones de un elemento dado en la lista. inversa, devolverá una lista con los elementos de la lista original en orden inverso. esCapicua, devolverá true en caso de que la lista sea capicúa y false en caso contrario. Las anteriores funciones se añadirán al objeto acompañante43 del tipo de datos Lista que se está definiendo. Así, el tipo de datos Lista quedaría: 1 object Lista { 2 3 def drop[A](ys:Lista[A])(n:Nat):Lista[A]= (ys,n) match { 4 case (Nil,_) => Nil 5 case (xs,Cero)=> xs 6 case (Cons(e,xs),n)=> drop (xs) (n-Nat(1)) 7 } 8 9 10 11 private def encuentra[A](elem:A,pos:Int,ys:Lista[A]):Int= ys match{ 12 case Nil => -1 13 case Cons(e,_) if e== elem => pos 14 case Cons(_,xs) => encuentra(elem,pos+1,xs) 15 } 16 17 def esta[A](elem:A,ys:Lista[A]):Boolean= if (encuentra(elem,0,ys) != -1) true else false 18 19 def posicion[A](elem:A,ys:Lista[A]):Int = encuentra(elem,0,ys) 20 21 def repeticiones[A](elem:A,ys:Lista[A]):Int={ 22 @annotation.tailrec 23 def go(ac:Int, xs:Lista[A]):Int = xs match { 43 Habitualmente se declarará un objeto acompañante que acompañará al tipo de datos que se esté definiendo y a sus constructores. Un objeto acompañante es un objeto que tiene el mismo nombre que la clase acompañante donde se definirán funciones para crear o trabajar con el tipo de datos. Los objeto acompañante tienen un soporte especial dentro de Scala. Página 93
  • 110. 24 case Nil => ac 25 case Cons(e,xs) if e==elem => go(ac+1,xs) 26 case Cons(_,xs) => go(ac,xs) 27 28 } 29 go(0,ys) 30 } 31 32 def eliminarTodos[A](elem:A,xs:Lista[A]):Lista[A]= { 33 @annotation.tailrec 34 def go(ac:Lista[A],xs:Lista[A]):Lista[A]= xs match{ 35 case Nil => ac 36 case Cons(e,xs) if e== elem => go(ac,xs) 37 case Cons(algo,xs) => go(Cons(algo,ac),xs) 38 39 } 40 go(Nil,xs) 41 } 42 def inversa[A](xs:Lista[A]):Lista[A]={ 43 @annotation.tailrec 44 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match { 45 case Nil => res 46 case Cons(elem,xss)=>go(xss,elem::res) 47 } 48 go(xs,Nil) 49 50 } 51 52 def esCapicua[A](xs:Lista[A]):Boolean= xs == inversa(xs) 53 54 def apply[A](as: A*): Lista[A] ={ 55 if (as.isEmpty) Nil 56 else as.head :: apply(as.tail: _*) 57 } 58 } Algoritmo 3.10: Object Lista ampliado 3.10.3.2. Ejercicios sobre el TAD Lista Ejercicio 25. Definir la función last que devuelva el último elemento de una lista. Ejercicio 26. Definir la función init que devuelva la lista formada por los elementos de la lista original sin el último elemento. Ejercicio 27. Definir la función take que permita seleccionar una cantidad de elementos inicia- les de una lista. Ejercicio 28. Definir la función splitAt que devolverá una dupla, combinando los resultados de take y drop. Ejercicio 29. Definir la función zipWith que aplicará cierta función a los elementos de dos listas tomándolos de dos en dos. La longitud de la lista resultado coincidirá con la de menor longitud. Página 94
  • 111. Ejercicio 30. Definir la función zip que construirá una lista de pares a partir de dos listas. Ejercicio 31. Definir la función unzip que dada una lista de pares devuelva una dupla formada por dos listas. La primera de ella será el resultado de tomar el primer elemento de cada par de elementos de la lista original y la segunda lista será el resultado de tomar el segundo elemento. Ejercicio 32. Definir la función map la cual transformará una lista aplicando a cada elemento de la misma una función. Ejercicio 33. Definir la función filter que permitirá seleccionar los elementos de una lista que cumplan un cierta propiedad. Ejercicio 34. Definir la función takeWhile que tomará el mayor segmento inicial de una lista que cumpla una cierta propiedad. Ejercicio 35. Definir la función dropWhile la cual eliminará de una lista el mayor segmento inicial de elementos que verifiquen un propiedad. Ejercicio 36. Definir la función foldr, función recursiva que recorre los elementos de una lista de derecha a izquierda y que tenga el siguiente comportamiento: Si el argumento es la lista vacía, devolverá el argumento correspondiente al caso base. En otro caso, se opera mediante una función u operador pasado como argumento a la función, la cabeza de la lista con una llamada recursiva con la cola de la misma. Ejercicio 37. Definir la función foldl con un comportamiento similar a la función foldr, pero realizando el plegado de la lista de izquierda a derecha. 3.10.4. Estructuras de datos no lineales 3.10.4.1. Árboles Los árboles son estructuras no lineales, como los conjuntos o los grafos, en los que la secuencialidad que caracteriza a las estructuras lineales no existe, aunque en los árboles existe una estructura jerárquica, de manera que un elemento tiene un solo predecesor, pero varios sucesores. Un árbol impone una estructura jerárquica sobre una colección de objetos, con un único punto de entrada y una serie de caminos que van abriéndose en cada punto hacia sus sucesores. Desde un punto de vista formal (teoría de conjuntos), un árbol se puede considerar como una estructura A = (N, ), constituida por un conjunto, N, cuyos elementos se denominan nodos, y una relación de orden parcial transitiva, , definida sobre N, y caracterizada por la existencia de: Un elemento mínimo (anterior a todos los demás) único, la raíz. ∃! r ∈ N (∀n ∈ N, n = r, r n) Un predecesor único para cada nodo p distinto de la raíz, es decir, un nodo, q, tal que q p y para cualquier nodo s con las mismas características se cumple s q. ∀ n ∈ N n = r −→ (∃! m ∈ N, ((m n) (∀ s ∈ N, s = m s n −→ s m))) Página 95
  • 112. La terminología utilizada con los árboles, en relación con los nodos que los forma, es la siguiente: Raíz - Elemento mínimo de un árbol, es decir, único nodo que no tiene padre. Padre - Predecesor máximo de un nodo. Hijo - Cualquiera de los sucesores directos de un nodo. Hermanos - Nodos que comparten el mismo padre. Nodo terminal u hoja - Nodos sin hijos, es decir, que no tiene sucesores. Nivel - El nivel de un nodo está definido por el número de conexiones entre el nodo y la raíz. Nodo intermedio - Cualquier nodo del árbol predecesor de una hoja, y sucesor de la raíz Un árbol en el que en cada nodo o bien todos o ninguno de los hijos existe, se llama árbol completo. Existen otros conceptos relacionados con el tamaño del árbol que definen las características del mismo: Orden: es el número potencial de hijos que puede tener cada elemento del árbol. De este modo, se dice que un árbol en el que cada nodo puede apuntar a otros dos es de orden dos, si puede apuntar a tres será de orden tres, etc. Grado: el número de hijos que tiene el elemento con más hijos dentro del árbol. Nivel: se define para cada elemento del árbol como la distancia a la raíz, medida en nodos. El nivel de la raíz es cero y el de sus hijos uno. Así sucesivamente. Altura: la altura de un árbol se define como el nivel del nodo de mayor nivel + 1. Como cada nodo de un árbol puede considerarse a su vez como la raíz de un árbol, también se puede hablar de altura de ramas. Esta estructura se puede considerar una estructura recursiva teniendo en cuenta que cada nodo del árbol, junto con todos sus descendientes, y manteniendo la ordenación original, cons- tituye también un árbol o subárbol del árbol principal, característica esta que permite definicio- nes simples de árbol, más apropiadas desde el punto de vista de la teoriza de tipos abstractos de datos, y, ajustadas, cada una de ellas, al uso que se vaya a hacer de la noción de árbol. Las dos definiciones más comunes son las de árbol general y la de árbol de orden N, que se pueden dar en los términos siguientes: Un árbol general con nodos de un tipo T es un par (r, LA) formado por un nodo r (la raíz) y una lista (si se considera relevante el orden de los subárboles) o un conjunto(si éste es irrelevante) LA (bosque), posiblemente vacío, de árboles generales del mismo tipo (subárboles de la raíz). Se puede apreciar que aquí no existe el árbol vacío, sino la secuencia vacía de árboles generales. Un árbol de orden N (con N ≥ 2), con nodos de tipo T, es un árbol vacío () o un par (r, LA) formado por un nodo r (la raíz) y una tupla LA (bosque) formada por N árboles del mismo tipo (subárboles de la raíz). Este último caso suele escribirse explícitamente de la forma (r, A1, ..., AN ). Página 96
  • 113. 3.10.4.2. Arboles Binarios El árbol binario es el caso más simple de árbol de orden N, cuando N vale 2. Su especifica- ción se puede hacer considerando un valor constante, el árbol nulo, y un constructor de árboles a partir de un elemento y dos árboles. especificación ARBOL_BINARIO[ELEM] usa BOOLEANOS,ENTEROS tipos arbolB operaciones V acio :−→ arbolB{constructora} Nodo(_, _, _) : elemento arbolB arbolB −→ arbolB{constructora} hijoIzqdo : arbolB −→ arbolB hijoDcho : arbolB −→ arbolB raiz : arbolB −→ elemento esV acio : arbolB −→ bool numeroNodos : arbolB −→ int igualForma : arbolB arbolB −→ bool altura : arbolB ←→ int variables n,m: elemento x,y,z: arbolB ecuaciones hijoIzqdo(V acio) = error hijoIzqdo(Nodo(e, i, d)) = i hijoDcho(V acio) = error hijoDcho(Nodo(e, i, d)) = d raiz(V acio) = error raiz(e, i, d) = e esV acio(V acio)) = cierto esV acio(Nodo(e, i, d)) = falso numeroNodos(V acio) = 0 numeroNodos(Nodo(e, i, d)) = 1 + numeroNodos(i) + numeroNodos(d) igualForma(V acio, a) = esV acio(a) igualForma(Nodo(e, i, d), Nodo(e2, i2, d2)) = igualForma(i, i2)&&igualForma(d, d2) altura(V acio) = 0 altura(e, i, d) = 1 + max(altura(i), altura(d)) Al igual que ocurría con las estructuras de datos lineales, esta especificación desempeña un papel básico que ayudará a la futura construcción de otras especificaciones de árboles. El algoritmo 3.11 muestra una posible implementación de la especificación anterior en Sca- la. Página 97
  • 114. 1 trait ArbolB[+T] { 2 def hijoIzqdo:ArbolB[T] = this match { 3 case Vacio => Vacio 4 case Nodo(_,i,_)=>i 5 } 6 def hijoDcho:ArbolB[T] = this match { 7 case Vacio => Vacio 8 case Nodo(_,_,d)=>d 9 } 10 def raiz:T= this match{ 11 case Vacio => throw new Exception("Solicitando raiz de arbol vacio!!!") 12 case Nodo(e,_,_)=>e 13 } 14 def esVacio:Boolean= this match{ 15 case Vacio => true 16 case _ => false 17 } 18 def numeroNodos:Int = this match{ 19 case Vacio => 0 20 case Nodo(e,i,d)=> 1 + (i numeroNodos) + (d numeroNodos) 21 } 22 def altura:Int = this match{ 23 case Vacio => 0 24 case Nodo(e,i,d)=> 1 + (i altura).max(d altura) 25 } 26 27 28 } 29 case object Vacio extends ArbolB[Nothing]{ 30 override def toString = "." 31 } 32 case class Nodo[T] (valor:T,i:ArbolB[T],d:ArbolB[T]) extends ArbolB[T]{ 33 override def toString = "T(" + valor.toString + " " + i.toString + " " + d.toString + ")" 34 } 35 object ArbolB { 36 def igualForma[T](a1:ArbolB[T],a2:ArbolB[T]):Boolean= a1 match{ 37 case Vacio => a2 esVacio 38 case Nodo(e,i,d)=> igualForma(i,a2 hijoIzqdo) && igualForma(d,a2 hijoDcho) 39 } 40 } 41 object Nodo { 42 43 def apply[T](value: T): Nodo[T] = Nodo(value, Vacio, Vacio) 44 } Algoritmo 3.11: Implementación básica de Arboles Binarios Página 98
  • 115. 3.10.4.3. Arboles Binarios de Búsqueda Son árboles de orden 2 en los que se cumple que para cada nodo, el valor de la clave de la raíz del subárbol izquierdo es menor que el valor de la clave del nodo y que el valor de la clave raíz del subárbol derecho es mayor que el valor de la clave del nodo. El repertorio de operaciones que se pueden realizar sobre un ABB es parecido al que ya se ha definido sobre otras estructuras de datos, más alguna otra propia de árboles: Buscar un elemento en un árbol. Insertar un elemento en un árbol. Borrar un elemento. Movimientos a través del árbol: • Sub-árbol izquierdo. • Sub-árbol derecho. • Raiz. Información: • Comprobar si un árbol está vacío. • Calcular el número de nodos. • Comprobar si el nodo es hoja. • Calcular el nodo con mayor clave • Calcular el nodo con menor clave. • Calcular el padre de un elemento. A continuación, se muestra una posible implementación del TADs ABB a la que se ha llamado Arbol: 1 trait ArbolBB[+T] { 2 def esVacio:Boolean 3 def add[U >: T < % Ordered[U]](x: U): ArbolBB[U] 4 def del[U >: T < % Ordered[U]](x: U): ArbolBB[U] 5 def search[U >: T< % Ordered[U]](x:U):Boolean 6 def raiz:ArbolBB[T]=this 7 def subIzq:ArbolBB[T] 8 def subDer:ArbolBB[T] 9 def maximo:T 10 def minimo:T 11 def predecesor[U >: T< % Ordered[U]](x:U):ArbolBB[U] 12 } 13 14 case class Nodo[+T](valor: T, left: ArbolBB[T], right: ArbolBB[T]) extends ArbolBB[T] { 15 override def toString = "T(" + valor.toString + " " + left.toString + " " + right.toString + ")" 16 def esVacio=false 17 def subIzq = left Página 99
  • 116. 18 def subDer = right 19 def add[U >: T < % Ordered[U]](x: U): ArbolBB[U] = { 20 x.compare(valor) match { 21 case i if i < 0 => Nodo(valor, left.add(x), right) 22 case i if i > 0 => Nodo(valor, left, right.add(x)) 23 case _ => Nodo(x,left,right) 24 } 25 } 26 27 def del[U >: T < % Ordered[U]](x: U): ArbolBB[U] = { 28 29 def valorSuc[U >: T](x:ArbolBB[U],direct:Boolean):U={ 30 x match{ 31 case Nodo(s,Vacio,_)if direct==true=>s //Si buscamos el menor de los elementos mayores 32 case Nodo(s,_,Vacio)if direct==false=>s // Si buscamos el mayor de los elementos menores 33 case Nodo(_,_,right1) if direct==false =>valorSuc(right1,direct) // Si buscamos el mayor de los elementos menores 34 case Nodo(_,left1,_) if direct==true =>valorSuc(left1,direct) //Si buscamos el menor de los elementos mayores 35 } 36 } 37 x.compare(valor) match{ 38 case i if i < 0 => Nodo(valor,left.del(x),right) // Seguimos buscando el elemento por el subarbol izdo. 39 case s if s > 0 => Nodo(valor,left,right.del(x)) // Seguimos buscando el elemento por el subarbol dcho. 40 case _ =>this match{ // Hemos encuentrado el elemento a eliminar 41 case Nodo(r,Vacio,Vacio)=> Vacio 42 case Nodo(r,i,Vacio)=>i 43 case Nodo(r,Vacio,d)=>d 44 case Nodo(r,i,d)=> val e:U=valorSuc(d,true); Nodo(e,i,d.del(e)) 45 //Hemos buscados como sucesor, el menor de los 46 //elementos mayores 47 } 48 } 49 50 } 51 def search[U >: T< % Ordered[U]](x:U):Boolean={ 52 x.compare(valor) match { 53 case i if i <0 => left.search(x) 54 case i if i >0 => right.search(x) 55 case _ => true 56 } 57 } 58 def maximo:T= this match{ 59 case Nodo(r,_,Vacio)=>r 60 case Nodo(r,_,d)=>d maximo Página 100
  • 117. 61 } 62 def minimo:T= this match{ 63 case Nodo(r,Vacio,_)=>r 64 case Nodo(r,i,_)=>i minimo 65 66 } 67 def predecesor[U >: T< % Ordered[U]](x:U):ArbolBB[U]={ 68 if (search(x)==true) buscaPadre(x,this,Vacio) else Vacio // Comprobamos que existe el elemento x 69 // y si existe, comenzamos busqueda del padre 70 } 71 private def buscaPadre[U >: T< % Ordered[U]] (elemen:U, tree:ArbolBB[U], padre:ArbolBB[U]) :ArbolBB[U]= { 72 tree match{ // El argumento padre contendra el padre(predecesor) 73 // de elemen (Vacio si elemen es la raiz) 74 case Nodo(r,i,d) if r>elemen =>buscaPadre(elemen,i,Nodo(r,i,d)) 75 case Nodo(r,i,d) if r<elemen=>buscaPadre(elemen,d,Nodo(r,i,d)) 76 case _ => padre 77 } 78 } 79 80 } 81 case object Vacio extends ArbolBB[Nothing] { 82 override def toString = "." 83 def add [U >: Nothing < % Ordered[U]] (x:U):ArbolBB[U]= Nodo(x,Vacio,Vacio) 84 def del [U >: Nothing < % Ordered[U]] (x:U):ArbolBB[U]=Vacio 85 def search[U >: Nothing < % Ordered[U]](x:U):Boolean =false 86 def subIzq=throw new Error("Vacio.subIzq") 87 def subDer=throw new Error("Vacio.subDer") 88 def maximo=throw new Error("Vacio.maximo") 89 def minimo=throw new Error("Vacio.minimo") 90 def predecesor[U >: Nothing < % Ordered[U]](x: U):ArbolBB[U]=Vacio 91 def esVacio=true 92 93 94 } 95 96 object Nodo { 97 98 def apply[T](value: T): Nodo[T] = Nodo(value, Vacio, Vacio) 99 } Algoritmo 3.12: TAD Arbol Binario de Búsqueda 3.10.5. Ejercicios Ejercicio 38. Definir una función listaToArbol que devolverá un árbol binario a partir de una lista xs, de tipo List, pasada como argumento. Página 101
  • 118. Ejercicio 39. Desde el punto de vista estructural (sin tener en cuenta el valor de los nodos) diremos que un árbol binario A1 es isomorfismo de otro árbol binario A2 si y sólo si ambos tienen el mismo número de nodos y además los nodos presentan la misma estructura. Definir una función isomorfimos que devuelva un valor true si A1 es isomorfismo del árbol A2 pasado como parámetro. Ejercicio 40. Se dice que un árbol binario es simétrico siempre que al trazar un línea vertical que pase por su raíz, el subárbol izquierdo sea un espejo del subárbol derecho. Definir una fun- ción esSimetrico que, haciendo uso de la función isomorfismo definida en el anterior ejercicio, determine si un árbol cumple esta propiedad. 3.11. Colecciones en Scala Además de poder construir estructuras de datos, es posible utilizar algunas de las estructuras de datos que Scala ofrece. Las colecciones44 se pueden utilizar haciendo uso de la librería collection45 , en la cual se encuentran los diferentes tipos de colecciones parametrizadas que provee el lenguaje para resolver la mayoría de los problemas que se puedan presentar. Las colecciones son estructuras de datos en las que se pueden agrupar uno o más datos de un tipo de datos (colecciones homogéneas) o de diferentes tipos de datos (colecciones heterogéneas). Se pueden entender como contenedores de valores, los cuales pueden estar dispuestos de forma secuencial, es decir, estructuras lineales que pueden tener un número arbitrario de elementos como Listas, Tuplas, Vectores, Conjuntos, ...o cuyo número de elementos puede estar limitado como, por ejemplo, se verá en el tipo de datos Option. Desde Scala se tiene acceso completo a la biblioteca de colecciones de Java, aunque no se podrían utilizar las funciones de orden superior (map, filter y reduce), presentes en las estructuras de datos de la librería collection de Scala y que, como se verá, ayudarán a manejar y operar sobre los datos de una colección, definiendo expresiones cortas y expresivas. Las colecciones pueden presentar una evaluación anticipada o una evaluación estricta o perezosa de sus elementos(ver la Sección 3.9: Programación funcional estricta y perezosa « página 76 »). Los elementos de las colecciones que presentan una evaluación perezosa, como por ejemplo el tipo de datos Range, no se construyen hasta que son referenciados por primera vez. Otra de las características importantes que definirán a una colección estará basada en la mutabilidad de la misma, es decir, si la colección será mutable o inmutable. Esta característica definirá si los elementos de una colección podrán cambiar una vez son asignados (colecciones mutables) o si los elementos no pueden cambiar (la referencia que apunta al elemento de la colección no podrá cambiar nunca). Se deberá tener en cuenta que, aunque la colección sea inmutable, los elementos que formen la colección pueden ser mutables.[30] Finalmente, habrá que decidir si se quiere que los elementos de la colección sean evaluados secuencialmente o en paralelo, para lo que será imprescindible evaluar previamente las opera- ciones que se desean aplicar a la colección para determinar si es posible, o no, la ejecución en paralelo. Cada una de las características mencionadas anteriormente pueden ser de utilidad en la reso- lución de problemas. Así, es posible mejorar el tiempo de ejecución de una tarea haciendo que 44 El término “colecciones” se popularizó a raíz de la librería collections de Java. 45 Es usual que los lenguajes de programación incluyan librerías que ofrezcan a los programadores diferentes estructuras de datos en las que agrupar elementos, (al menos listas y asociacións)[29] Página 102
  • 119. Figura 3.2: Diagrama UML de los tipos principales dentro del paquete scala.collection una colección se evalúe de forma paralela o, en otras ocasiones, se podrá mejorar el rendimiento utilizando evaluación perezosa.[6] 3.11.1. El paquete scala.collection Si se observa la Application Programming Interface (API) de Scala se puede apreciar que hay varios paquetes que empiezan por scala.collection, incluido el propio paquete llamado sca- la.collection. Como se puede ver en la figura 3.2, en la parte más alta de la jerarquía se encuentra el tipo de datos Traversable[A], que agrupa a todos los tipos de datos que pueden ser usados para representar cualquier cosa y que tienen la propiedad de poder ser recorridos. Los subtipos de Traversable deberán definir el método foreach, un método que se utilizará para recorrer la estructura de datos. El método foreach46 es un iterador interno que recibirá una función que operará sobre los elementos del tipo A como parámetro y que se aplicará a cada uno de los elementos de la estructura. Justo debajo de Traversable se encuentra Iterable[A], que como su propio nombre indica, agrupa las estructuras de datos que disponen de iteradores. Este tipo y sus subtipos dispondrán de un método, iterator, que devolverá un Iterator[A]. De modo que los subtipos de Iterable[A] tendrán que implementar el método iterator, el cuál devolverá un iterador sobre los elementos de la estructura de datos. Esta clase mejora sustancialmente el rendimiento de su superclase Traversable ya que permitirá, a los métodos que necesiten recorrer una colección, parar el re- corrido antes de lo que permitiría pararlo la clase Traversable. Iterator dispone de dos métodos que ayudarán a la hora de recorrer las colecciones: next y hasNext. El método hasNext devol- verá true si y sólo si en la colección aún quedan elementos por recorrer. El método next avanza sobre los elementos de la colección y devolverá el siguiente valor de la colección o lanzará una excepción en el caso de que no queden elementos por recorrer en la colección. Dentro de las librerías de Scala se pueden diferenciar tres subtipos principales de Itera- ble[A]: Set[A]. Dentro del que se puede destacar dos subtipos: BitSet y SortedSet[A]. El tipo de 46 La librería incluye algunas excepciones predefinidas para detener de forma anticipada la iteración por los elementos de la estructura. De este modo se podrá evitar que, para ciertas operaciones, se produzca una pérdida innecesaria de ciclos de computación. Página 103
  • 120. datos BitSet está optimizado para trabajar con conjuntos de enteros, almacenando un bit por cada entero que forme parte del conjunto, de modo que si el valor se encuentra en el conjunto se activa el bit, en otro caso no se activará. El tipo de datos SortedSet[A] es una versión especializada de Set que se podrá emplear cuando el tipo de datos de los valores del conjunto A presente un orden natural. Seq[A]. Define los métodos length y apply, así como representa las colecciones que tienen una estructura secuencial. Los dos subtipos de Seq[A] son: IndexedSeq y LinearSeq. La principal diferencia entre ambos reside en cómo se accede a cada uno de los elementos de estas colecciones. Mientras que los subtipos de IndexedSeq permitirán un acceso eficien- te a cualquier elemento de la colección, los subtipos de LinearSeq son más ineficientes para accesos aleatorios a sus elementos ya que tienen que recorrer todos los elementos anteriores, pero son adecuados cuando se utilizan junto con algoritmos que acceden se- cuencialmente a los elementos. LinearSeq servirá para indicar que la colección puede ser descompuesta en el primer elemento de la colección (la cabeza) y el resto (la cola). Las estructuras de datos conocidas, que mejor se ajustan a este tipo de datos, son las pilas ya que se puede acceder rápidamente al último elemento agregado pero se tendrían que desapilar todos los elementos de la misma para llegar al primer elemento añadido. El tipo IndexedSeq será ideal para todos aquellos algoritmos de propósito general que no tengan que descomponer la estructura de datos en cabeza y cola. Map[A,B]. Será utilizada para definir asociaciones47 de pares (clave,valor)48 . En esta es- tructura de datos sólo habrá un valor por cada clave y las claves son únicas. Estas estruc- turas de datos ofrecen una solución a aquellas aplicaciones que usan conjuntos de datos que pueden variar en el tiempo y sobre las que no son frecuentes realizar las operacio- nes de unión, intersección o diferencia, sino inserciones, eliminaciones y consultas, éstas últimas serán realizadas por clave. Si no existiesen limitaciones de espacio, se podría implementar un diccionario simple- mente utilizando la clave de un elemento como índice en una tabla de búsqueda directa (utilizando un vector, por ejemplo). Si no existiesen limitaciones de tiempo, se podría implementar utilizando una lista enlazada con un requerimiento de espacio mínimo. Uti- lizando éstas soluciones obtendríamos tablas de direccionamiento directo, la cuales sólo podrán ser aplicadas cuando el conjunto de claves posibles es razonadamente pequeño. Para solucionar las limitaciones de las tablas de direccionamiento directo se utilizan las tablas hash. Una tabla hash es una estructura de datos que intenta encontrar un balance entre estos dos extremos.Cuando el número de claves que se almacenan efectivamente es pequeño comparado con el número total de claves posibles, una tabla hash ofrece una alternativa eficiente a una tabla de búsqueda directa, utilizando solamente un vector de tamaño proporcional a la cantidad de claves almacenadas. En lugar de utilizar la clave como un índice directamente, el índice se calcula a partir de la clave utilizando una fun- ción hash. Al utilizar una tabla hash el número de claves efectivas es menor al número de claves posibles (existirán necesariamente claves que tengan el mismo número ) por lo que se podrán producir colisiones que habrá que tratar49 . . A continuación se verán algunas de las colecciones inmutables más importantes dentro del ecosistema de la librería scala.collection[14]. 47 También reciben el nombre de diccionarios. 48 Una asociación es un par (clave/valor). Un diccionario estará formado por un conjunto de asociaciones 49 Una función de hash adecuada podrá minimizar el número de colisiones pero no evitarlas completamente. Página 104
  • 121. 3.11.2. Iteradores en Scala A pesar de que el tipo Iterator no es una colección, si que nos proporciona una forma de recorrer cada uno de los elementos que forman la estructura. Las operaciones básicas que se realizan con los iteradores son next() y hasNext(). El método next() de un iterador se empleará para obtener el elemento siguiente del iterador de la colección y avanzar el iterador a un nuevo elemento. El método hasNext del iterador se utilizará para conocer si existen elementos en la colección que aún no se han recorrido. Una forma habitual de utilizar estas funciones es en un bucle while, como se muestra en el ejemplo: scala> val it = Iterator("Ejemplo","metodos","next","hasNext","Iterator") it: Iterator[String] = non-empty iterator scala> while (it.hasNext){ | println(it.next()) | } Ejemplo metodos next hasNext Iterator Algoritmo 3.13: Métodos next y hashNext en un bucle while 3.11.2.1. Métodos definidos para el tipo Iterator en Scala. Como las colecciones que se van a estudiar son subclases del tipo Iterable[A]50 , tendrán que definir el método iterator que devuelva un Iterator[A]. En la tabla 3.1 se reflejan algunas de las operaciones más usuales dentro del tipo Iterator, que además estarán presentes en el tipo Iterable[A] y que, por tanto, podremos emplear con las colecciones. Métodos disponibles en el Tipo Iterator def hasNext: Boolean Indica si hay otro elemento en el iterador. def next(): A Devuelve el siguiente elemento del itera- dor. def ++(that: =>Iterator[A]): Iterator[A] Concatena dos iteradores def ++[B >: A](that :=>GenTraversa- bleOnce[B]): Iterator[B] Concatena este iterador con otro def contains(elem: Any): Boolean Indica si el elemento elem está entre los elementos del iterador. def count(p: (A) =>Boolean): Int Devuelve el número de elementos del ite- rador que satisfacen la condición P. def drop(n: Int): Iterator[A] Avanza el iterador n posiciones. Si la lon- gitud del iterador es menor que n, avanza- rá todo el iterador. 50 Es la clase base de todas las colecciones que definen el método iterator. Página 105
  • 122. def dropWhile(p: (A) =>Boolean): Itera- tor[A] Saltará todos los elementos del iterador que verifiquen la condición p hasta que encuentre el primer elemento en el itera- dor que no verifique dicha condición. El valor devuelto será un iterador con todos los elementos restantes. def duplicate: (Iterator[A], Iterator[A]) Devolverá una dupla con dos iteradores que iteraran sobre los mismos elementos que el iterador (y en el mismo orden). def exists(p: (A) =>Boolean): Boolean Indicará si algún elemento del iterador sa- tisface la condición p. def filter(p: (A) =>Boolean): Iterator[A] Devolverá un iterador sobre los elemen- tos de este iterador que satisfagan la con- dición p. def find(p: (A) =>Boolean): Option[A] Devolverá, si existe, el primer valor del iterador que satisfaga la condición p. def flatMap[B](f: (A) =>GenTraversa- bleOnce[B]): Iterator[B] Crea un nuevo iterador aplicando la fun- ción f a los elementos de este iterador y concatenando los resultados. def forall(p: (A) =>Boolean): Boolean Indicará si todos los elementos del itera- dor satisfacen la condición p. def foreach(f: (A) =>Unit): Unit Aplica una función a todos los elementos del iterador. def isEmpty: Boolean Devolverá true si el método hasNext de- vuelve false y viceversa. def length: Int Devuelve el número de elementos del ite- rador, situándose el mismo al final. def map[B](f: (A) =>B): Iterator[B] Devuelve un nuevo iterador cuyos valores son el resultado de aplicar la función f a este iterador. def max:A51 Encuentra el elemento de mayor valor en el iterador. El iterador se encontrará al final del mismo después de invocar este método. def min:A52 Encuentra el elemento de menor valor en el iterador. El iterador se encontrará al final del mismo después de invocar este método. def nonEmpty: Boolean Devolverá el mismo valor que el método hasNext. def product: A53 Devolverá el producto de los elementos del iterador. def size: Int Devolverá el número de elementos del ite- rador. 51 Signatura completa: def max[B >: A](implicit cmp: Ordering[B]): A 52 Signatura completa: def min[B >: A](implicit cmp: Ordering[B]): A 53 Signatura completa: def product[B >: A](implicit num: Numeric[B]): B Página 106
  • 123. def sum: A54 Devolverá la suma de los elementos del iterador. def take(n: Int): Iterator[A] Devolverá un iterador con los n primeros elementos de este iterador. def zip[B](that: Iterator[B]): Iterator[(A, B)] Devolverá un nuevo iterador que conten- drá duplas con los elementos que se co- rresponden de este iterador y el iterador that. El número de elementos del iterador devuelto será igual al menor número de elementos de este iterador y del iterador that. Tabla 3.1: Métodos del tipo Iterator 3.11.3. Colecciones inmutables 3.11.3.1. Definición de rangos en Scala. La clase Range. Range es una subclase de IndexedSeq y es el tipo más simple de secuencia que se utiliza- rá para representar rangos de enteros. Se utilizará principalmente para generar otros tipos de secuencias o para iterar en bucles for. Una de las propiedades de esta colección es que pre- senta evaluación perezosa de sus elementos por lo que no serán instanciados y, por tanto, no consumirán memoria, hasta que se acceda a ellos. Los operadores más habituales que se emplearán con los rangos son: El operador ...to ...que se utilizará para crear rangos en los que se incluya la cota supe- rior dada. En el algoritmo 3.14 se puede ver un ejemplo de la utilización de este operador. El operador ...until ...que será empleado para definir rangos en los que se excluya la cota superior dada. En el algoritmo 3.14 se puede ver un ejemplo de la utilización de este operador. by. Se empleará para cambiar el paso del rango. En el algoritmo 3.14 se puede ver un ejemplo de la utilización de este operador. Por tanto, para definir un rango habrá que indicar la cota inferior, la cota superior y el paso del rango. 1 scala> val simpleRange = 1 to 10 2 simpleRange: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 3 4 scala> val rangeSimple = 1 until 10 5 rangeSimple: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5, 6, 7, 8, 9) 6 7 scala> val stepRange= 1 to 10 by 2 8 stepRange: scala.collection.immutable.Range = Range(1, 3, 5, 7, 9) Algoritmo 3.14: Definicion de rangos 54 Signatura completa: def sum[B >: A](implicit num: Numeric[B]): B Página 107
  • 124. Se podrá acceder a cualquiera de estros tres valores (cota inferior, cota superior y paso) utilizando los campos start, end y step del objeto del tipo Range que define el rango como se muestra en el algoritmo 3.15. 1 scala> stepRange.step 2 res8: Int = 2 3 4 scala> rangeSimple.start 5 res9: Int = 1 6 7 scala> rangeSimple.end 8 res10: Int = 10 Algoritmo 3.15: Acceso a las cotas y valor de paso de los rangos En el algoritmo 3.16 se puede apreciar que una de las aplicaciones de este tipo de datos será la de generar los elementos de otras estructuras de datos, aunque algunas estructuras tienen definido el método range que realizará la misma función. Se podrá invocar el método range con dos parámetros (cota inferior y cota superior) o con tres parámetros (cota inferior, cota superior, valor de paso) teniendo en cuenta, en ambos casos, que la cota superior quedará excluida del rango definido. 1 2 scala> val miLista=(1 to 10).toList 3 miLista: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 4 5 scala> val miVector=(1 to 10).toVector 6 miVector: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 7 8 scala> List.range(1,10) // Definimos una lista 9 res18: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9) 10 11 scala> Vector.range(0,25,5) 12 res19: scala.collection.immutable.Vector[Int] = Vector(0, 5, 10, 15, 20) Algoritmo 3.16: Acceso a las cotas y valor de paso de los rangos Métodos definidos para el tipo Range en Scala. Además de los métodos vistos anteriormente, en el tipo de datos Range están disponibles los métodos más usuales de los tipos iterables vistos en la tabla 3.1. 3.11.3.2. Definición de tuplas en Scala. La clase Tuple Las tuplas son un tipo de datos algebraico que se utilizará para definir pequeñas colecciones de dos o más elementos heterogéneos. Tuple será de gran utilidad en multitud de ocasiones en las que se quieran agrupar varios elementos heterogéneos, es decir, elementos de distintos tipos de datos en una estructura55 . Las tuplas permitirán agrupar hasta un máximo de 22 elementos, 55 Algo que no se podrá hacer con las colecciones que sean subtipos de Iterable como List, Vector... Página 108
  • 125. por lo que habrá tuplas del tipo Tuple2, Tuple3, ..., Tuple22. Tuple no hereda de Iterable al no agrupar elementos homogéneos, sino que es un subtipo de Product. Como se muestra en el algoritmo 3.17, para crear un tupla sólo se tendrá que encerrar entre paréntesis los elementos que formen a la misma y separarlos por comas. El tipo de una tupla dependerá del número de elementos que contenga y del tipo de los mismos. scala> val miTupla = (1, 2.0, 3.4755F, "hola", true) miTupla: (Int, Double, Float, String, Boolean) = (1,2.0,3.4755,hola,true) Algoritmo 3.17: Definición de tuplas Para cada uno de los tipos de tuplas (Tuple2, ..., Tuple22), Scala define un número adecuado de métodos para acceder a los elementos de cada tupla. Así, para acceder al primer elemento de una tupla se usará el método _1, para acceder al segundo elemento utilizaremos el método _2 y así sucesivamente. También se podría utilizar concordancia de patrones (Pattern Matching) para asignar los valores de la tupla a variables, haciendo uso del guión bajo (_) para descartar aquellos elementos de la tupla que no se deseen. Se puede ver un ejemplo del uso de estos métodos y de concordancia de patrones con tuplas en el algoritmo 3.18. scala> miTupla._1 res0: Int = 1 scala> miTupla._2 res1: Double = 2.0 scala> miTupla._3 res2: Float = 3.4755 scala> miTupla._4 res3: String = hola scala> miTupla._5 res4: Boolean = true scala> val (a,b,c,d,e) = miTupla a: Int = 1 b: Double = 2.0 c: Float = 3.4755 d: String = hola e: Boolean = true scala> val (x,_,y,_,z) = miTupla x: Int = 1 y: Float = 3.4755 z: Boolean = true Algoritmo 3.18: Acceso a los elementos de una tupla. Aunque Tuple no es una colección, es posible tratarla como una colección cuando sea nece- sario, creando un iterador utilizando el método productIterator, como se muestra en el algoritmo 3.19. Incluso es posible transformar la tupla en una colección. scala> miTupla.productIterator.foreach(i=>println("Valor: "+ i)) Valor: 1 Valor: 2.0 Valor: 3.4755 Valor: hola Valor: true scala> miTupla.productIterator.toList res8: List[Any] = List(1, 2.0, 3.4755, hola, true) Algoritmo 3.19: Iterando sobre los elementos de las tuplas Las tuplas de dos elementos (instancias del tipo Tuple2) disponen del método swap que permitirá intercambiar la posición de los elementos de la tupla como se puede apreciar en el algoritmo 3.20 Página 109
  • 126. scala> val tupla = ("hola","hello") tupla: (String, String) = (hola,hello) scala> tupla.swap res9: (String, String) = (hello,hola) Algoritmo 3.20: Método swap en tuplas de dos elementos 3.11.3.3. Listas en Scala. La clase List Las listas constituyen una de las estructuras más empleadas en programación funcional por lo que era de esperar que la librería estándar de Scala incluyera una implementación de las mismas. Scala implementa en la clase List la estructura de datos de listas enlazadas inmutables donde se representan colecciones ordenadas y homogéneas de elementos. Esta estructura de datos es un subtipo de LinearSeq[A] y presentará un rendimiento excelente si añadimos y eli- minamos elementos siempre de la cabeza de la lista o queremos descomponer nuestra estructura de datos en cabeza y cola, ya que esta operación de descomposición presentará una complejidad del orden O(1). Para otros usos, el rendimiento se verá más afectado por lo que es preferible elegir esta clase cuando se utilicen algoritmos que accedan secuencialmente a sus elementos, comportamiento que se ajusta más a la especificación de List. Aunque la clase List suele ser la primera elección por defecto de los programadores que co- nocen lenguajes como Java o Haskell, veremos como la clase Vector, o incluso la clase Stream, se puede ajustar más a sus necesidades que la clase List. La clase List en Scala está definida con una clase abstracta (abstract class) y tiene dos im- plementaciones (una por cada constructor) que implementan los miembros abstractos isEmpty, head y tail: Nil para la lista vacía ::, llamado cons (abreviatura de construir), para construir listas a partir de un elemento y una lista List implementa compartición estructural de la cola de la lista por lo que muchas opera- ciones tendrán un coste nulo o constante de memoria, como ya se vio en la Sección 3.10.2.2: Compartición estructural (Data Sharing) « página 81 » Ejemplo: 1 val lista1 = List(3, 2, 1) 2 val with4 = 4 :: lista1 // re-usa lista1, coste --> una instancia de :: 3 val with42 = 42 :: lista1 // re-usa lista1, coste --> una instancia de :: 4 val shorter = lista1.tail // coste nulo. Usa la misma instancia 2::1::Nil de lista List se caracteriza por su persistencia y por la compartición estructural, lo cual se tradu- cirá en beneficios de rendimiento y ahorro de memoria en muchos escenarios si se usa con corrección[9], siempre y cuando se añadan elementos al principio de la lista o se descomponga la lista en cabeza y cola. Cuando se necesite actualizar un elemento que se encuentre en la parte central de la lista será necesario generar toda la lista de elementos previos al elemento que se desea modificar. Página 110
  • 127. Otra de las características del tipo de datos List, es que es covariante, lo cual significa que para cada par de tipos S y T, si S es un subtipo de T, List[S] será subtipo de List[T]. Como se vio en la Sección 2.3: Jerarquía de clases en Scala « página 37 », Nothing se encuentra en la parte más baja de la jerarquía de clases de Scala y es un subtipo de cualquier otro tipo en Scala. Como las listas son covariantes, List[Nothing] será subtipo de cualquier List[T] (independientemente de T). Por esta razón, el objeto que representa la lista vacía, List(), puede ser visto como un ob- jeto de cualquier lista List[T], independientemente del tipo de ésta . Por este motivo, se pueden escribir asignaciones como la que se muestra en el algoritmo 3.11.3.3. 1 //List() es tambien del tipo List(Int) 2 val intList:List[Int]=List() Antes de continuar profundizando en el manejo de las listas, se verá como se pueden crear listas enlazadas homogéneas e inmutables con la clase List, así como las funciones más carac- terísticas de la misma. Creación de listas Como ya se ha visto anteriormente es posible diferenciar: 1. La lista vacía, lista que almacena cero elementos de cualquier tipo. Usando el constructor de listas vacías Nil. Nil es esencialmente una instancia de List[Nothing]. Ejemplo: scala> val lista = Nil lista: scala.collection.immutable.Nil.type = List() Indicando la clase List, sin argumentos56 . Ejemplo: scala> val lista2 = List() lista2: List[Nothing] = List() 2. Listas que contienen n elementos de un mismo tipo. Usando el constructor de listas ::, el cual permite añadir un nuevo elemento al prin- cipio de una lista. Así, haciendo uso de Nil y del constructor, asociativo a la derecha, :: (cons) se podrán construir listas de forma similar a como se construyen en Lisp. Ejemplo: scala> val lista3=1::2::3::4::5::Nil lista3: List[Int] = List(1, 2, 3, 4, 5) Indicando la clase List, cuyos argumentos serán los elementos de la lista. scala> val lista4=List(1,2,3,4,5) lista4: List[Int] = List(1, 2, 3, 4, 5) Es posible descomponer una lista en dos partes, la cabeza (head) y la cola (tail). La cabeza de la lista corresponderá al primer elemento de la misma y la cola estará formada por el resto de elementos de la lista. List es una colección que presenta evaluación impaciente, es decir, cuando se construye una lista se calcula tanto la cabeza como la cola de la misma. Dentro de la librería collection de 56 Haciendo uso del método apply del objeto acompañante de List Página 111
  • 128. Scala se puede encontrar otro tipo de implementación de lista enlazada, Stream, que presen- ta evaluación perezosa y que se verá en la Subsubsección 3.11.3.5: Flujos en Scala. La clase Stream « página 115 ». Métodos definidos para el tipo List en Scala. Además de los métodos vistos anteriormente para crear listas, en el tipo de datos List se pueden aplicar las funciones más usuales de los tipos iterables vistas en la tabla 3.1. Otras operaciones características de las listas se encuentran definidas en la tabla 3.2 Métodos del Tipo List def def +(elem: A): List[A] Añade un elemento al final de la lista. def :::(prefix: List[A]): List[A] Devuelve el resultado de añadir los ele- mentos de la lista prefix delante de la lista. def apply(n: Int): A Devuelve el elemento n-ésimo de la lista. def distinct: List[A] Devuelve una lista con los elementos sin repeticiones. def dropRight(n: Int): List[A] Devuelve una lista con los elementos de esta lista excepto los n últimos. def equals(that: Any): Boolean Compara la lista con cualquier otra se- cuencia. def init: List[A] Devuelve una lista con los elementos de esta lista exceptuando el último. def intersect(that: Seq[A]): List[A] Realiza la intersección entre los elemen- tos de esta lista y de otra secuencia. def iterator: Iterator[A] Devuelve un iterador sobre los elementos de la lista. def last: A Devolverá el último elemento de la lista. def reverse: List[A] Devuelve una lista con los elementos de esta lista en orden inverso. def sorted[B >: A]: List[A] Ordenará la lista acorde a un orden. def takeRight(n: Int): List[A] Devuelve los últimos n elementos. Tabla 3.2: Métodos del tipo List Dentro de la clase List se pueden encontrar muchas más funciones que serán de gran utilidad a la hora de manejar listas. Se recomienda ver la API de Scala, en scala-lang.org donde se pueden consultar las diferentes funciones disponibles para la clase List: isEmpty,length, ++, drop, dropWhile, take, takeWhile, map, foldRight, foldLeft,.... Ejercicios sobre listas Responder a las siguientes cuestiones teniendo en cuenta listas del tipo List[+A] visto ante- riormente: Ejercicio 41. Si se define la función fun tal que: Página 112
  • 129. 1 def fun(xs: List[Int], ys: List[Int]):List[Int] = (xs, ys) match { 2 case (Nil, ys) => ys 3 case (xs, Nil) => xs 4 case (h::t, ys) => h :: fun(t, ys) 5 } ¿Cuál será el resultado de evaluar la expresión fun(List(1,2), List(3,4,5 ))? (List(1,2), List(3,4,5)) List(List(1,2), List(3,4,5)) List(1,2,3,4,5) Ejercicio 42. Suponiendo que se tiene el siguiente programa: 1 val a = List(4,3,2,1) 2 3 val b = a.sorted 4 5 println(a) ¿Qué resultado se obtendrá tras su ejecución? List(4, 3, 2, 1) List(1, 2, 3, 4) Ninguna de las respuestas es correcta Ejercicio 43. Si se define la variable inmutable a como: 1 val a = List(1, 2, 3, 4, 5) ¿Cuál será el resultado de evaluar la expresión a.map(x =>x * 2).filter(x =>x <5).reduce((x, y) =>x * y)? 8 16 Error al intentar reasignar un valor a una variable inmutable List(2, 4) Ejercicio 44. Definir la función miMax, que dada una lista de enteros devuelva el del mayor valor: Ejercicio 45. Definir las funciones suma y producto tales que, que dada una lista de enteros como parámetro devuelvan, respectivamente, la suma y el producto de todos sus elementos. scala> suma(List(1,2,3,4,5)) res0: Int = 15 scala> producto(List(1,2,3,4,5)) res2: Int = 120 Página 113
  • 130. Ejercicio 46. Definir en Scala la función diferencias que devuelva la diferencia que hay entre cada número adyacente. Si la lista está formada por un único elemento devolverá la lista vacía. Ejercicio 47. Definir una función aplana que dada una lista de listas de elementos, nos devuelva una lista con los elementos de cada una de las listas. 1 aplana(List(List(1,2,3),List(4,5,6),List(7,8,9)) ¿Se podría haber solucionado el problema utilizando foldRight o foldLeft ? Escribir la solución o razonar por qué no se pueden utilizar. Ejercicio 48. Definir una función aEntero al que dada una lista de enteros pasada por paráme- tros, devuelva el número resultante de unir todos ellos. Ejercicio 49. Definir la función aLista que tome como parámetro un número entero y devuelva una lista con cada uno de sus dígitos. Ejercicio 50. Definir la función miLength que calcule el número de elementos de una lista. Ejercicio 51. Implementar una función que devuelva el penúltimo elemento de una lista pasada como parámetro. En caso de que la lista esté vacía o tenga un sólo elemento se lanzará un error. Ejercicio 52. Implementar una función que examine una lista de enteros y determine si es un palíndromo. Ejercicio 53. Definir una función que devuelva el valor del elemento enésimo, pasado por parámetro, de una lista. scala> enesimo(2,List(1,1,2,3,4,5,6,7)) res0: Int = 2 Nota: Por convenio el primer elemento de una lista es el 0-ésimo. Ejercicio 54. Definir una función que elimine los elementos duplicados consecutivos en una lista. Ejercicio 55. Definir una función que duplique todos los elementos de una lista. Ejercicio 56. Definir una función que repita N veces todos los elementos de una lista. Ejercicio 57. Definir una función que agrupe los elementos duplicados de una lista, pasada por parámetro, en sublistas dentro de una lista. 3.11.3.4. Vectores en Scala. La clase Vector La estructura de datos Vector es un subtipo de IndexedSeq y es la estructura de datos que mejor se ajusta a la mayoría de algoritmos de propósito general. Las operaciones de acceso a un elemento aleatorio en un Vector presentan una complejidad del orden O(log32(N)), lo cual, usando índices de 32 bits, representa un tiempo de acceso constante pequeño. El tipo de datos Vector es inmutable y presenta unas características de compartición estructural razonables. El hecho de que Vector tenga un factor de ramificación de 32 hace que presente diversas ventajas: Los tiempos de búsqueda y de actualización de un elemento son excelentes, así como los tiempos de las operaciones para añadir un elemento al principio o al final de la estructura de datos. Decente coherencia en caché, aumentando los aciertos57 en la misma ya que los elementos 57 Un acierto en caché se produce cuando el dato que requiere el procesador se encuentra en caché. Página 114
  • 131. que se encuentren próximos en nuestra colección también deberán de estar próximos en memoria. La eficiencia de este tipo de datos, junto con el hecho de que sea una estructura inmutable, y por tanto, una estructura que puede ser compartida por diferente hilos de ejecución sin temor a que efectos colaterales puedan dar lugar a resultados no deseados, convierten a Vector en la secuencia más potente disponible en la biblioteca de Scala. Como se muestra en el algoritmo, se pueden crear vectores de forma análoga a como se creaban listas, excepto usando el operador :: de listas. scala> val simpleVector = Vector(1,2,3,4) simpleVector: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3, 4) Algoritmo 3.21: Definición de Vector. En el algoritmo 3.22 se muestra como, en lugar del operador ::, las estructuras de datos de tipo Vector presentan los operadores +: y :+ para añadir un elemento al principio del vector o al final del mismo respectivamente58 . scala> simpleVector :+ 5 // Agregamos un elemento al final del vector res5: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3, 4, 5) scala> 0 +: simpleVector // Agregams un elemento al principio del vector res6: scala.collection.immutable.Vector[Int] = Vector(0, 1, 2, 3, 4) Algoritmo 3.22: Agregar elementos a un Vector Las operaciones más frecuentes sobre vectores son similares a las operaciones vistas ante- riormente sobre listas59 . 3.11.3.5. Flujos en Scala. La clase Stream Stream es un subtipo de LinearSeq que se utilizará para definir colecciones de elementos cuando se desee que presenten evaluación perezosa. Haciendo uso de los flujos de datos se podrán representar colecciones con un número infinito de elementos, sin que se produzca un desbordamiento de memoria60 . Los flujos de datos guardan el valor de los elementos que ya han sido calculados, lo cual es una ventaja a la hora de acceder a estos elementos de forma eficiente ya que no se han de recalcular estos elementos pero, a su vez, puede representar un problema de memoria el hecho de que coexistan un número elevado de elementos. La clase Stream de Scala es similar a la clase List de Scala vista anteriormente. Stream está compuesta por el flujo de datos vacío (Stream.empty) y por sucesivas operaciones cons que utilizarán el método #:: (en lugar del método :: que se usaba en la colección List). También se podrá definir un flujo de datos haciendo uso del método apply de su objeto acompañante, como se puede comprobar en el algoritmo 3.23. 58 Una buena regla nemotécnica para distinguir ambos operadores podría ser tener en cuenta que el símbolo dos puntos (:) siempre se encuentra próximo a la secuencia mientras que el símbolo + se encuentra próximo al elemento 59 En la API de Scala podremos encontrar muchas más funciones que serán de gran utilidad a la hora de manejar vectores 60 En lugar de almacenar los elementos, Stream almacena funciones objeto para calcular tanto la cabeza (head) del flujo de datos como el resto de los elementos (tail). Página 115
  • 132. scala> val miStream=Stream.range(1,10) miStream: scala.collection.immutable.Stream[Int] = Stream(1, ?) scala> miStream(3) res0: Int = 4 scala> println(miStream) Stream(1, 2, 3, 4, ?) scala> val otroStream = 1 #:: 2 #:: 3 #:: 5 #:: 7 #:: Stream.empty otroStream: scala.collection.immutable.Stream[Int] = Stream(1, ?) scala> otroStream(2) res2: Int = 3 scala> println(otroStream) Stream(1, 2, 3, ?) Algoritmo 3.23: Definición de Streams Un uso muy apropiado de Stream sería el de calcular el siguiente valor de la secuencia haciendo uso de los valores previamente calculados. Una aplicación en la que se puede ver perfectamente la utilización de Stream en este sentido sería el cálculo de la serie de Fibonacci, en el que el siguiente valor de la secuencia se calcula sumando los dos valores anteriores. En el algoritmo 3.24 se muestra una posible implementación de la serie de Fibonacci utilizando Stream y el cálculo de los diez primeros valores de la serie. scala> val serieFibonacci = { | def fib(a:Int,b:Int):Stream[Int] = a #:: fib(b,a+b) | fib (0,1) | } serieFibonacci: Stream[Int] = Stream(0, ?) scala> serieFibonacci take 10 toList res5: List[Int] = List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34) Algoritmo 3.24: Cálculo de la serie de Fibonacci utilizando Stream Los programadores que han tenido un contacto previo con la programación funcional en lenguajes como Haskell suelen utilizar List por defecto para representar listas. Hay que tener en cuenta que, como se ha comentado anteriormente, List presenta evaluación impaciente o estricta mientras que la listas en Haskell presentan evaluación perezosa. Además Stream es estricta en el elemento, una vez producido, mientras que las listas en Haskell no lo son. Cuando se quiera crear una lista con evaluación perezosa habrá que usar Stream en Scala en lugar de List[6]. Las operaciones más frecuentes sobre flujos son similares a las operaciones vistas anterior- mente sobre listas. 61 . 3.11.3.6. Conjuntos en Scala. La clase Set El concepto de conjunto es matemático. Hay dos diferencias fundamentales entre las se- cuencias (tipo Seq) y los conjuntos (tipo Set). La diferencia principal que podemos encontrar entre ambos tipos es que los conjuntos no permiten la existencia de elementos duplicados. Es decir, si en un conjunto compuesto por diferentes elementos se trata de añadir un elemento que es igual62 a otro existente en el conjunto, dicho elemento no se añadirá quedando el tamaño del conjunto igual que antes de realizar la operación de añadir. Por tanto, se elegirá un conjunto cuando se desee que la estructura de datos no presente elementos duplicados o cuando se quiera comprobar si un elemento está en la misma. 61 En la API de Scala se pueden encontrar muchas más funciones que serán de gran utilidad a la hora de manejar flujos 62 En términos del método ==. Página 116
  • 133. Otra de las diferencias fundamentales que existen entre los tipos Set y Seq es que no se puede garantizar el orden entre los elementos de los conjuntos63 . La forma más fácil de definir conjuntos será haciendo uso del método apply definido en el objeto acompañante, al que se le pasará como argumento los elementos del mismo: Ejemplo: 1 scala> val numeros = Set(1,2,3,4,5) //Definimos un nuevo conjunto 2 numeros: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4) 3 4 scala> numeros + 7 //Agregamos el 7 al conjunto definido anteriormente 5 res0: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 7, 3, 4) 6 7 scala> numeros + 4 //Agregamos el 4 al conjunto numeros 8 res1: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4) 9 10 scala> res0 + 4 //Agregamos el 4 al conjunto resultante tras agregar el 7 11 res2: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 7, 3, 4) 12 13 scala> res0 + -25 // Agregamos el -25 al conjunto resultante tras agregar el 7 14 res3: scala.collection.immutable.Set[Int] = Set(5, 1, 2, -25, 7, 3, 4) 15 16 scala> res3 - 1 //Eliminamos el 1 del conjunto resultante de agregar -25 tras agregar el 7 17 res4: scala.collection.immutable.Set[Int] = Set(5, 2, -25, 7, 3, 4) 18 19 scala> res4 + 123 //Agregamos el 123 al conjunto resultante de eliminar el 1 20 res5: scala.collection.immutable.Set[Int] = Set(5, 2, -25, 7, 3, 123, 4) Algoritmo 3.25: Definición y propiedades fundamentales de los conjuntos En el algoritmo 3.25 se puede ver como se cumplen las propiedades anteriormente definidas: Por defecto, cuando se define una colección del tipo Set, se obtiene una estructura de datos inmutable. El conjunto resultante tras añadir el número 4 a un conjunto en el que este número ya existe es el conjunto original, sin modificaciones. Tras realizar diferentes operaciones de añadir/eliminar elementos de un conjunto se com- prueba que el orden del conjunto no es previsible, ni se puede asegurar. Otra de las diferencias que se pueden apreciar con respecto a la clase List es referente al acceso a los elementos. En este tipo de datos, en lugar de obtener un valor ubicado en una posición de nuestra colección (como ocurría en el caso de los subtipos de LinearSeq), se utilizará el método apply (definido en cualquiera de los subtipos de Set[A]) para saber si un elemento está o no está en un conjunto. Recorriendo conjuntos La forma más fácil de recorrer los elementos de un conjunto será haciendo uso del bucle for. No se podrán utilizar otros bucles, como por ejemplo while, ya que no se accede a los elementos del conjunto por la posición de los mismos64 . Ejemplo: 63 Excepto si se trata de un SortedSet ya que el orden de los elementos de este tipo coincidirá con el orden natural de los elementos, aunque se puede considerar este caso como la excepción que confirma la regla. 64 Para esto, se hará uso de un iterador. Página 117
  • 134. scala> for (x <- numeros){println(x)} 5 1 2 3 4 Algoritmo 3.26: Recorriendo los elementos de un conjunto Métodos definidos para el tipo Set en Scala. Además de los métodos vistos anteriormente para crear conjuntos, en el tipo de datos Set se pueden aplicar las funciones más usuales de los tipos iterables vistas en la tabla 3.1. Otras operaciones características de los conjuntos se encuentran definidas en la tabla 3.3 Métodos del Tipo Set def def +(elem: A): Set[A] Crea un conjunto nuevo que contendrá el elemento elem además de los elementos de este conjunto, excepto si elem ya se en- cuentra en este conjunto. def -(elem: A): Set[A] Devuelve el resultado de eliminar el ele- mento elem del conjunto. def apply(elem: A): Boolean Indica si un elemento pertenece al conjun- to def & (that: Set[A]): Set[A] Devuelve un conjunto con los elementos de este conjunto y los elementos del con- junto that. def &∼ (that: Set[A]): Set[A] Devuelve la diferencia entre este conjunto y el conjunto pasado como argumento. def ++(elems: Set[A]): Set[A] Concatena los elementos de este conjunto con los elementos del conjunto elems . def diff(that: Set[A]): Set[A] Devuelve la diferencia entre este conjunto y el conjunto pasado como argumento. def intersect(that: Set[A]): Set[A] Realiza la intersección entre los elemen- tos de este conjunto y del conjunto pasado como argumento. def iterator: Iterator[A] Devolverá un iterador sobre los elementos del conjunto. def last: A Devolverá el último elemento del conjun- to. def isEmpty: Boolean Devolverá true si el conjunto no tiene nin- gún elemento. def subsetOf(that: Set[A]): Boolean Devolverá true si este conjunto es un sub- conjunto del conjunto pasado como argu- mento. Tabla 3.3: Métodos del tipo Set El tipo Set presenta algunos métodos muy interesantes que dan respuesta a las propieda- des de este tipo de estructuras de datos. En la API de Scala se pueden encontrar muchas más funciones que pueden ser de gran utilidad a la hora de manejar conjuntos. Página 118
  • 135. 3.11.3.7. Asociaciones en Scala. La clase Map Las asociaciones son una de las estructuras de datos más utilizadas en el desarrollo de pro- gramas ya que presentan una búsqueda eficiente de valores en base a sus claves. Su nombre, al igual que ocurría en el caso de los conjuntos, tiene su origen en las matemáticas donde elemen- tos de un conjunto son asociados con elementos de otro conjunto65 . El tipo de datos Map[A,B] asocia elementos del tipo paramétrico A, también llamado tipo de las claves, a otro tipo de datos B (el tipo de los valores). El tipo de las claves y el tipo de los valores pueden ser iguales, aunque en la mayoría de las ocasiones serán de tipos diferentes. Como se veía que ocurría en el caso de los tipos de datos para conjuntos, la forma más fácil de definir asociaciones será haciendo uso del método apply definido en el objeto acompañante, al que se le pasará como argumento los elementos del mismo. Los siguientes ejemplos pueden ayudar a entender la utilidad de las asociaciones: scala> val simpleMap:Map[Int,String]=Map(1->"uno",2->"dos",3->"tres") simpleMap: Map[Int,String] = Map(1 -> uno, 2 -> dos, 3 -> tres) Algoritmo 3.27: Definicion de Maps. En el algoritmo 3.27 se ha definido un Map simple. Los tipos de los parámetros son Int y String. Los elementos que se han pasado a la asociación son tuplas en las que el primer elemento es la clave y el segundo elemento es el valor. A la hora de definir los valores se ha empleado una sintaxis clara, utilizando el operador ->, aunque como se verá en el algoritmo 3.28, también se podría haber definido la asociación introduciendo las tuplas directamente (sin necesidad de utilizar el operador ->). scala> val miMapa:Map[String,String]=Map(("hola","hello"),("adios","bye")) miMapa: Map[String,String] = Map(hola -> hello, adios -> bye) Algoritmo 3.28: Definicion de Maps con tuplas. En el algoritmo 3.29 se puede apreciar como, para acceder a los elementos de una asocia- ción, se hace uso de las claves. Así, cuando se consulte por el valor del elemento cuya claves es 2, se obtiene como resultado “dos”, del tipo String, como era de esperar. scala> simpleMap(2) res0: String = dos Algoritmo 3.29: Recuperar valores en Maps Otra similitud entre los conjuntos y las asociaciones es la forma en la que pueden añadir o eliminar elementos de estas estructuras de datos. En las asociaciones se hará uso de los operado- res + o -, para añadir o eliminar elementos respectivamente, como se puede ver en el algoritmo 3.30. simpleMap + (4->"cuatro") res1: scala.collection.immutable.Map[Int,String] = Map(1 -> uno, 2 -> dos, 3 -> tres, 4 -> cuatro) scala> simpleMap - 3 res3: scala.collection.immutable.Map[Int,String] = Map(1 -> uno, 2 -> dos) Algoritmo 3.30: Agregar y eliminar elementos en un Map 65 Una función matemática hace exactamente esto, asociar elementos de un conjunto con elementos de otro conjunto. Página 119
  • 136. Otra de las similitudes que comparten los tipos datos Set y Map es el hecho de que, por defecto, cuando se define un Map o un Set se obtendrá una estructura de datos inmutables66 . Para recorrer las asociaciones se podrán utilizar funciones de orden superior o el bucle for, al igual que con el resto de las colecciones. Para las asociaciones habrá que prestar especial atención ya que se deberá tener en cuenta que los elementos de una asociación se almacenan como tuplas de pares (clave,valor). En el algoritmo 3.31 se muestra un ejemplo simple en el que se recorren los elementos de la asociación definida anteriormente. scala> for ((clave,valor)<-simpleMap) yield clave*2 res4: scala.collection.immutable.Iterable[Int] = List(2, 4, 6) Algoritmo 3.31: Recorrer los elementos de un Map Métodos definidos para el tipo Map en Scala. Además de los métodos vistos anteriormente para crear asociaciones, en el tipo de datos Map se pueden aplicar las funciones más usuales de los tipos iterables vistas en la tabla 3.1. Otras operaciones características de las asociaciones están definidas en la tabla 3.4 Se han destacado algunos métodos que no han sido vistos en colecciones anteriores o que son propios del tipo Map. Al igual que ocurría con las demás colecciones, en la API de Scala se pueden encontrar muchas más funciones que pueden ser de gran utilidad a la hora de manejar asociaciones. 3.11.3.8. Selección de una colección Uno de los aspectos más importantes a tener en cuenta a la hora de decidir que colección escoger será el rendimiento que ofrece. Como se ha visto anteriormente, el rendimiento es una característica que está muy relacionada con la implementación de la colección y con las operaciones que se vayan a realizar sobre la misma. En la tabla 3.5 se muestra el rendimiento de las operaciones más importantes que se pueden realizar sobre las secuencias. Para representar la complejidad de cada operación se han utilizado las siguientes abreviatu- ras: RC. Es una operación rápida que tomará un tiempo constante. La complejidad de estas operaciones es de O(1). Lin. Es una operación lineal y tomará un tiempo proporcional a la dimensión de la colec- ción. La complejidad de estas operaciones es de O(n). DC. Es una operación que tomará un tiempo constante, asumiendo ciertas características en la colección como el tamaño de un vector o la distribución de las claves hash. -. Operación no soportada. Log. La operación tomará un tiempo proporcional al logaritmo del tamaño de la colec- ción. La complejidad de estas operaciones será de O(log(N)). En la tabla 3.6 se muestra la eficiencia de las operaciones más comunes en las estructuras de datos Map y Set. 66 No obstante, existe una versión mutable tanto para los conjuntos como las asociaciones. Para utilizar la versión mutable se tendrá que importar la librería scala.collection.mutable y luego se tendrá que hacer referencia a estas estructuras mutables como mutable.Map o mutable.Set. Página 120
  • 137. Métodos del Tipo Map def ++(xs: Map[(A, B)]): Map[A, B] Devuelve un nuevo Map que contendrá los pares (clave,valor) de esta asociación y los de la asociación pasada como argu- mento. def get(key: A): Option[B] Devuelve el valor asociado a una clave pasada como argumento. def apply(key: A): B Nos devuelve el valor asociado a la clave pasada como argumento. En caso de no existir nos devolvería el resultado del mé- todo default. def default(key: A): B Define el valor por defecto que devolve- rá la asociación cuando la clave buscada no se encuentre. El método devolverá una excepción pero puede ser sobreescrito en las diferentes subclases. def keys: Iterable[A] Devuelve un iterador sobre todas las cla- ves. def remove(key: A): Option[B] Elimina el par (clave,valor) cuya clave coincida con el argumento pasado. def iterator: Iterator[A] Nos devolverá un iterador sobre los ele- mentos de la colección. def last: A Nos devolverá el último elemento del map. def isEmpty: Boolean Nos devolverá true si la asociación no contiene ningún elemento. Tabla 3.4: Métodos del tipo Map 3.11.3.9. Colecciones como funciones Todas las colecciones que se han visto pueden ser utilizadas como funciones ya que heredan de PartialFunction, el cual hereda del tipo estándar de función en Scala. El comportamiento que tendrán las colecciones como funciones dependerá de la implementación del método apply de la colección67 . Por ejemplo, el tipo de datos Set[T] se podrá utilizar como una función T =>Boolean, el tipo de datos Seq[T] puede ser utilizado como una función Int =>T o el tipo Map[C,V] como una función C =>V. Por tanto, se podrán utilizar las colecciones en lugares en los que se esperaría que apareciera una función. scala> val conj=Set(1,2,3) conj: scala.collection.immutable.Set[Int] = Set(1, 2, 3) scala> 1 to 2 map conj res1: scala.collection.immutable.IndexedSeq[Boolean] = Vector(true, true) 67 Distinto del método apply del objeto acompañante de la colección y que se utiliza para construir las mismas. Página 121
  • 138. Eficiencia de las operaciones sobre secuencias head tail apply actualización insertar(por la cabeza) insertar(por la cola) List RC RC Lin Lin C Lin Vector DC DC DC DC DC DC Stream RC RC Lin Lin C Lin Range RC RC RC - - - Tabla 3.5: Secuencias. Eficiencia de las operaciones. Eficiencia de las operaciones en conjuntos y maps buscar añadir eliminar minimo HashSet & HashMap DC DC DC Lin TreeSet & TreeMap Log Log Log Log Tabla 3.6: Set y Map. Eficiencia de las operaciones 3.11.4. Expresiones for como una combinación elegante de funciones de orden superior En general, no se permite el uso de estructuras de control iterativas en el ámbito de la pro- gramación funcional ya que se incumpliría uno de los principios de la programación funcional: el uso de variables inmutables. En la Subsubsección 1.5.2.3: Bucles for « página 25 » se intro- dujeron los bucles for como una estructura de control iterativa aunque ya se advirtió que este tipo de bucles en Scala presentaban unas características muy especiales que los convertirían en una herramienta muy útil en la programación funcional en Scala. Existen dos motivos por los que en Scala, los bucles for son una herramienta muy útil dentro del paradigma de la programación funcional: 1. La traducción que realiza el compilador de Scala de esta estructura de control. En realidad, el compilador de Scala expresa los bucles for que almacenan un resultado con yield en términos de las funciones de orden superior map, flatMap y filter. Los bucles for que no almacenen resultados serán traducidos por el compilador en términos de las funciones de orden superior foreach y filter. 2. Anteriormente se ha visto como combinando las funciones de orden superior map, flat- map y filter se pueden resolver problemas complejos, aunque el nivel de abstracción de las mismas pueden dar lugar a que el código resultante sea difícil de interpretar. Las ex- presiones for nos proporcionarán expresiones más claras para resolver estos problemas. 3.11.4.1. Traducción de expresiones for con un generador Un bucle for con un generador presentará la siguiente construcción: for (x <- expr1) yield expr2 que será traducida por el compilador a la siguiente expresión: expr1 map (x=>expr2) con la cual, como se puede comprobar en el algoritmo 3.32, se obtendrá el mismo resultado que con el bucle for. Página 122
  • 139. scala> val miLista = 1::2::3::4::5::Nil miLista: List[Int] = List(1, 2, 3, 4, 5) scala> for (x <- miLista) yield(x*2) reso: List[Int] = List(2, 4, 6, 8, 10) scala> miLista map (x => x*2) res1: List[Int] = List(2, 4, 6, 8, 10) Algoritmo 3.32: Ejemplo traducción bucle for con un generador 3.11.4.2. Traducción de expresiones for con un generador y un filtro Si en nuestro bucle for, además de un generador, aparece un filtro: for (x <- expr1 if expr2) yield expr3 el compilador lo traducirá a la siguiente expresión: for (x <- expr1 filter (x => expr2)) yield expr3 con lo que la traducción final quedará de la siguiente manera: expr1 filter (x => expr2) map (x => expr3) En el algoritmo 3.33 se puede comprobar como, tanto el bucle for, como la expresión a la que es traducida, obtienen los mismos resultados. scala> def esPar(x:Int):Boolean = (x % 2)==0 esPar: (x: Int)Boolean scala> for (x <- miLista; if esPar(x)) yield (x,"es par") res2: List[(Int, String)] = List((2,es par), (4,es par)) scala> miLista filter (esPar) map (x=>(x,"es Par")) res3: List[(Int, String)] = List((2,es Par), (4,es Par)) Algoritmo 3.33: Ejemplo traducción de expresiones for con un generador y un filtro 3.11.4.3. Traducción de expresiones for con dos generadores Si ahora se definiera un bucle for con dos generadores anidados: for (x <- expr1; y <- expr2) yield expr3 que sería traducido por el compilador a la siguiente expresión: expr1 flatmap (x => for (y <- expr2) yield (expr3)) en la que se puede observar que hay otra expresión for dentro de la función que se le pasa a flatmap, que también será traducida por el compilador generando finalmente la expresión: expr1 flatMap (x => expr2 map (y => expr3)) En el algoritmo 3.34 se puede comprobar que la expresión final generada por el compilador y el bucle for con dos generadores son equivalentes scala> val auxLista= ’a’::’b’::’c’::Nil auxLista: List[Char] = List(a, b, c) scala> for (x <- auxLista; y <- miLista) yield (x, y*y) res4: List[(Char, Int)] = List((a,1), (a,4), (a,9), (a,16), (a,25), (b,1), (b,4), (b,9), Página 123
  • 140. (b,16), (b,25), (c,1), (c,4), (c,9), (c,16), (c,25)) scala> auxLista flatMap (x=> miLista map (y=>(x,y*y))) res33: List[(Char, Int)] = List((a,1), (a,4), (a,9), (a,16), (a,25), (b,1), (b,4), (b,9), (b,16), (b,25), (c,1), (c,4), (c,9), (c,16), (c,25)) Algoritmo 3.34: Ejemplo de traducción de expresiones for con dos generadores 3.11.4.4. Traducción de bucles for La traducción de los bucles que for sigue el mismo patrón que el visto anteriormente en la traducción de expresiones for. La diferencia es que en lugar de utilizar las funciones de orden superior map y flatMap se utilizará foreach. De este modo, un bucle for definido de la siguiente forma: for (x <- expr1; if expr2; y <- expr3) {cuerpo del bucle} será traducido por el compilador a la siguiente expresión intermedia: expr1 filter (expr2) foreach(x => for (y <- expr3)) {cuerpo del bucle} la cual, será finalmente traducida a la siguiente expresión: expr1 filter (expr2) foreach(x => expr3 foreach (y=> {cuerpo del bucle})) En el algoritmo 3.35 se muestra como efectivamente se obtiene el mismo resultado utilizan- do bucles for y su traducción equivalente. Para ilustrar el ejemplo se han definido dos listas de enteros, miLista y tmpLista. Se desea obtener el resultado total de sumar las parejas de elemen- tos pares de ambas listas. Se puede apreciar ver como el resultado total es acumulado en una variable mutable. scala> var a = 0 a: Int = 0 scala> val tmpLista = 6::7::8::9::10::Nil tmpLista: List[Int] = List(6, 7, 8, 9, 10) scala> for (x<-miLista;if esPar(x);y<-tmpLista;if esPar(y)) {println(x + " + " + y+" = "+(x+y));a+=x+y};println("Total = "+a) 2 + 6 = 8 2 + 8 = 10 2 + 10 = 12 4 + 6 = 10 4 + 8 = 12 4 + 10 = 14 Total = 66 scala> a=0 a: Int = 0 scala> miLista filter esPar foreach(x=> tmpLista filter esPar foreach (y=> {println(x + " + " + y+" = "+(x+y));a+=x+y}));println("Total = "+a) 2 + 6 = 8 2 + 8 = 10 2 + 10 = 12 4 + 6 = 10 4 + 8 = 12 4 + 10 = 14 Total = 66 Algoritmo 3.35: Ejemplo traducción bucle for genérico Página 124
  • 141. 3.11.4.5. Definición de map, flatMap y filter con expresiones for A continuación se mostrará como es posible definir las funciones de orden superior map, flatMap y filter de los tipos de datos definidos, haciendo uso de las expresiones for: def map[A, B](xs: List[A], f: A => B): List[B] = for (x <- xs) yield f(x) def flatMap[A, B](xs: List[A], f: A => List[B]): List[B] = for (x <- xs; y <- f(x)) yield y def filter[A](xs: List[A], p: A => Boolean): List[A] = for (x <- xs if p(x)) yield x 3.11.4.6. Uso generalizado de for en estructuras de datos Como se ha visto, el compilador necesita de las funciones de orden superior map, flatMap y filter para la traducción de las expresiones for que devuelven algún valor, mientras que para los bucles for que no devuelven ningún valor, sólo necesita que estén definidas las funciones de orden superior foreach y filter. Por tanto, podremos recorrer todas las colecciones que im- plementen estas funciones (como Stream, List, Vector, ...) haciendo uso de los bucles for. Pero además también se pueden disfrutar de todas las ventajas de las expresiones for vistas anterior- mente en las estructuras de datos definidas por el usuario siempre y cuando implementen las funciones map, flatMap, filter y foreach. Si las estructuras de datos no definen todas estas fun- ciones, pero si algunas de ellas, también será posible disfrutar de algunas de estas ventajas. A continuación se muestran las diferentes posibilidades que existen, dependiendo de las funciones de orden superior definidas: Si sólo se define la función map, se podrá definir expresiones for que tengan un único generador. Si se definen las funciones map y flatMap se podrán definir expresiones for que tengan varios generadores. Si se encuentra definida la función foreach, podremos definir bucles for que no devuelvan ningún valor, independientemente del número de generadores que tengan. Si se ha definido la función filter, se podrán definir expresiones for que incluyan filtros como se ha visto anteriormente En la siguiente sección se verá un concepto muy relacionado con la programación funcional, las mónadas. Las mónadas y las funciones de orden superior map, flatMap y filter están muy relacionadas ya que se definirán en las mónadas estas funciones que, además, caracterizarán las mismas junto con el método unit. Por tanto, se pueden ver las funciones de orden superior map, flatMap y filter como una versión orientada a objetos de la definición del concepto de mónada característico de la programación funcional. Además, como se ha visto anteriormente, las expresiones for son una traducción de las funciones map, flatMap y filter que permitirán escribir de forma más clara y comprensible ciertos algoritmos y que, por tanto, también podrán utilizarse con las mónadas. Por todo esto, es posible imaginar que los bucles for juegan un papel muy importante dentro de Scala, más allá de ser simples estructuras de control o iteradores de colecciones, ya que siempre que para cualquier tipo de datos en el que se encuentren definidas las funciones map, flatMap y filter se podrán utilizar expresiones for. Página 125
  • 142. 3.11.5. Ejercicios Ejercicio 58. Implementar una función construirMap la cual devuelva un valor del tipo Map a partir de cualquier tipo de secuencia dada y de una función que permitirá crear las claves de la asociación. 1 def contruirMap[A,B](datos: Seq[A], f: A=>B): Map[B,A] Ejercicio 59. Implementar la función palabrasDistintas que indique las palabras distintas hay en una variable de tipo String pasada como argumento a la función: 1 def palabrasDistintas(str:String):Int Ejercicio 60. Implementar una clase Persona para almacenar datos de personas (nombre, ape- llidos, teléfono, dirección, email) y una clase Agenda que nos permita almacenar a nuestros contactos y acceder rápidamente a los datos de los mismos si buscamos por el nombre del contacto. En principio se supondrá que no habrá dos contactos que tengan el mismo nombre. Defina la función add que nos permita añadir un nuevo contacto de tipo Persona pasado como argumento a nuestra agenda. Dentro de la clase Agenda, definir la función contactos que nos devuelva una lista con el nombre de todos los contactos que hay almacenados en la agenda. Definir una función telefonos dentro de la clase Agenda que devuelva una lista con todos los pares (Nombre,Teléfono) correspondientes a los contactos almacenados en la agenda. Modificar la clase Agenda para poder almacenar a más de un contacto con el mismo nombre. A la hora de imprimir por pantalla el resultado de las funciones contactos y telefonos se deberá poder diferenciar a cada uno de los contactos de la agenda. De modo que si después de imprimir por pantalla el resultado de las funciones contactos y telefonos se obtenía antes: List(Anton, Lourdes) List((Anton,11111111), (Lourdes,22222222)) Ahora se deberá obtener: List(Anton Oliva Olmo, Lourdes de Marcos Soria) List((Anton Oliva Olmo,11111111), (Lourdes de Marcos Soria,22222222)) Implementar una función en la clase Agenda que nos devuelva una lista con todos los con- tactos que tengan el mismo nombre que el valor de tipo String pasado como argumento. En caso de no haber ningún contacto nos devolverá la lista vacía. Implementar una función vecinos en la clase Agenda que devuelva una lista con todos los contactos que vivan en la calle pasada como argumento. Página 126
  • 143. Implementar una función vecinos en la clase Agenda que devuelva un Map cuya clave será la calle y tendrá como valor la lista de vecinos que viven en esa calle. Mejorar la versión de la función vecinos para que no muestre como resultado las direc- ciones en las que sólo exista un contacto. Ejercicio 61. Implementar una función que identifique las palabras distintas hay en una variable de tipo String pasada como argumento a la función, y además informe de cuantas apariciones hay de cada palabra en la variable pasada como argumento. Página 127
  • 145. Capítulo 4 Programación Funcional Avanzada en Scala 4.1. Implícitos 4.1.1. Parámetros ímplicitos en funciones En la Sección 3.5: Currificación y Parcialización « página 63 », se estudió la aplicación parcial de funciones y se vio como una función podía ser invocada sin necesidad de especificar todos los argumentos, obteniendo otra función como valor que podía ser invocada especificando los parámetros restantes. Una de las aplicaciones que tendrán los parámetros implícitos será la de poder aplicar funciones sin la necesidad de especificar todos sus parámetros1 . Para definir valores, variables o parámetros implícitos en funciones se utilizará la palabra reservada implicit. Las variables o valores implícitos definidos se utilizarán como “valores por defecto”2 de los parámetros definidos como implícitos de las funciones que se invoquen dentro del namespace (espacio de nombres), donde estas variables o valores implícitas son definidas. El parámetro implícito3 de las funciones se especificará después de la definición de los parámetros no implícitos o normales de la misma. En el algoritmo 4.1 se define la función hacerLista con un parámetro implícito dentro del objeto IntOps. El parámetro implícito definido es de tipo lista de enteros List[Int]. object IntOps { def hacerLista(x:Int)(implicit xs:List[Int]):List[Int] = {x :: xs} } Algoritmo 4.1: Definición de parámetros implícitos A continuación, se verá en el REPL de Scala como la invocación de la función hacerLista, sin especificar el parámetro implícito, provoca un error: scala> val lista5=IntOps.hacerLista(5)(List()) lista5: List[Int] = List(5) scala> IntOps.hacerLista(3) <console>:9: error: could not find implicit value for parameter xs: List[Int] IntOps.hacerLista(3) 1 Se tendrán que especificar todos los parámetros que no se declaren como implícitos. 2 Si éstos no son aportados específicamente cuando se invoque la función. 3 Normalmente se definirá un parámetro implícito aunque se pueden definir un grupo de parámetros implícitos separados por comas. Página 129
  • 146. ^ En el algoritmo 4.2 se ha definido la clase ListaSingleInt. Dentro de la clase se ha definido un valor implícito de tipo lista: la lista vacía (List()). También se ha definido la función hacerLista que se encarga de hacer una llamada a la operación hacerLista del objeto IntOps, pasándole el valor de clase x como argumento. Por tanto, la función IntOps.hacerLista tomará el valor implícito xs cuando no se haya especificado ningún otro valor para el parámetro implícito xs. case class ListaSingleInt(x:Int){ implicit val xs = List() def hacerLista = IntOps.hacerLista(x) } Algoritmo 4.2: Definición de valores implícitos A continuación, se muestra como es posible crear listas de un sólo elemento sin necesidad de especificar un segundo parámetro utilizando la clase ListaSingleInt: scala> val lista3 = ListaSingleInt(3).hacerLista lista3: List[Int] = List(3) Los valores implícitos son muy usados en Scala. La mayoría aportan funcionalidad a las funciones, como por ejemplo el orden por defecto en las colecciones. Esta funcionalidad puede ser sobreescrita por las clases que utilicen estas funciones. Se deberá de tener en cuenta que el código puede llegar a ser difícil de leer y de entender si se hace un uso excesivo de los parámetros implícitos. Se puede evitar que esto ocurra limitando el uso de los mismos dentro del código, por ejemplo al aporte de funcionalidad a determinadas funciones. 4.1.2. Clases implícitas Otra característica de los implícitos en Scala es el de servir para declarar conversiones im- plícitas entre clases. Una clase implícita servirá para realizar una conversión automática de un tipo (de una clase A), a otro tipo (de otra clase B). El compilador de Scala utiliza las clases implícitas cuando una instancia invoca un método o valor desconocido. En este momento buscará dentro del espacio de nombres (namespace) en busca de una clase implícita que tome como argumento la propia instancia e implemente el método o valor invocado. Si el compilador encuentra una coincidencia incorporará una conver- sión automática a la clase implícita que haga accesibles las invocaciones de este método o valor desde el propio tipo. En el algoritmo 4.3 se define la clase implícita Listas en la que se implementa la operación hacerSingletonLista de un elemento entero. object UtilInts { implicit class Listas(x:Int) { def hacerSingletonLista = List(x) } } Algoritmo 4.3: Definición de clases implícitas Por tanto, para que el compilador sea capaz de realizar la conversión automáticamente de un elemento de tipo Int, a un elemento de tipo Listas, pudiendo así hacer accesible la operación hacerSingletonLista a todos los enteros, se tendrá que importar el objeto UtilInts para que esté disponible dentro del ámbito del REPL después de haber definido el mismo: Página 130
  • 147. scala> object UtilInts { | implicit class Listas(x:Int) { | def hacerSingletonLista = List(x) | } | } defined object UtilInts scala> import UtilInts._ import UtilInts._ scala> println(3.hacerSingletonLista) List(3) Como se ha visto, las clases implícitas facilitan la incorporación de métodos o valores a los tipos de datos pero para que Scala pueda realizar esta tarea, las clases implícitas deberán de cumplir unas reglas: Las clases implícitas se tienen que definir dentro de otra clase, objeto o rasgo. Como se ha visto, las clases implícitas que se definen dentro de objetos se pueden importar fácilmente. Las clases implícitas sólo podrán tomar un valor cuyo tipo no podrá ser el de una clase implícita.. El nombre de la clase implícita no podrá ser igual al de otro objeto, clase o rasgo definido dentro del ámbito de la clase implícita4 Todas estas reglas se cumplen en el ejemplo anterior. Se recomienda definir las clases implícitas dentro de objetos ya que: Se pueden incorporar fácilmente dentro del ámbito como se ha visto en el anterior ejem- plo, importando parte o todos los objetos miembros. Ninguna clase, objeto o rasgo podrán heredar de un objeto, algo que aportará seguridad de que no se incorporen conversiones de forma automática. 4.2. Tipos en Scala Scala es un lenguaje tipado estáticamente. El sistema de tipos es un componente muy im- portante del lenguaje que, combinando ideas de la programación funcional y la POO, intenta ser comprensible, completo y consistente. El sistema de tipos de Scala ofrece un conjunto de opti- mizaciones y de restricciones que permitirán mejorar el tiempo de ejecución de los programas y prevenir errores de programación. Informando suficientemente al compilador, éste será capaz de detectar una gran cantidad de errores. Un tipo representa un conjunto de información conocida para el compilador como puede ser “qué clase se usó para instanciar una variable” o “qué métodos están disponibles para una varia- ble” , información que se puede aportar al compilador o que éste puede inferir inspeccionando el código. 4 Por lo que no se podrá definir una clase implícita haciendo uso de case class ya que las clases case crean automáticamente un objeto acompañante, dentro del cual se define automáticamente el método de fábrica apply. Página 131
  • 148. 4.2.1. Definición de tipos. En Scala se pueden definir tipos de dos modos: Definiendo una clase, objeto o rasgo se creará automáticamente un tipo asociado a la cla- se, objeto o rasgo definido. Se podrá hacer referencia a estos tipos con el mismo nombre que la clase o rasgo, mientras que para referirnos al tipo de un objeto5 utilizaremos el miembro type del objeto. A continuación se muestra un ejemplo: object MiObjeto def miFuncion(x:MiObjeto.type) Definiendo directamente el tipo usando la palabra reservada type.. Utilizando type se po- drán crear tanto tipos concretos, como tipos abstractos6 . Con type sólo se podrán definir tipos dentro de un contexto, es decir, dentro de un objeto, clase o rasgo. Ejemplo: abstract class Suma { type MiTipo // MiTipo es un tipo abstracto def suma(x:MiTipo,y:MiTipo):MiTipo } class SumaInt extends Suma { type MiTipo = Int def suma(x:Int,y:Int):Int = x+y } def sumarInt = new SumaInt() val l1= List(1,2,3,4) val valor= l1 foldRight (0) (sumarInt.suma) 4.2.2. Parámetros de tipo. Los parámetros de tipo se definen antes de que se haya definido los parámetros de la fun- ción, clase, etc. encerrando los parámetros de tipo dentro de corchetes. Una vez se definen los parámetros de tipo, se podrán utilizar estos parámetros tanto en los argumentos como dentro de la definición de las funciones, clases,etc. La importancia de los parámetros de tipo para crear funciones polimórficas o clases genéricas fue vista en la sección dedicada al polimorfismo en Scala, en la Sección 2.5: Polimorfismo en Scala « página 45 ». Los parámetros de tipo pueden presentar restricciones que limiten los posibles valores que puedan tomar, acotando los mismos. Otra de las características de los parámetros de tipo es la varianza que define la relación entre los tipos parametrizados y sus subtipos. La varianza y la acotación de tipos fue estudiada en detalle en la Subsección 2.5.1: Acotación de tipos y varianza « página 47 ». 4.2.2.1. Nombres de los parámetros de tipo. La decisión sobre qué nombre utilizar para los parámetros de tipo es un tema que ha ge- nerado siempre un gran debate entre los programadores. De un lado hay quien apoya la idea 5 No es habitual referirnos al tipo de un objeto ya que si conocemos el objeto podemos hacer referencia al mismo sin necesidad de tener que preguntar por el tipo del mismo. 6 Los contextos en los que se definan tipos abstractos no se podrán instanciar. Página 132
  • 149. de otorgar nombres descriptivos a los parámetros de tipo, mientras que por otro lado hay quien apoya utilizar nombres cortos. Entre la comunidad de programadores de Scala se ha llegado a un acuerdo: Utilizar nombres cortos, definidos con una letra o dos letras (A,B,A1,T2,...), normalmente para definir los elementos que pueden existir en un contenedor y que no presentan ninguna relación con el contenedor. Utilizar nombre largos para definir aquellos tipos de datos que si están relacionados con el contenedor. Por ejemplo, si observamos el tipo That aparece en la definición del método scan de List para hacer referencia al tipo del contenedor en el que se almacenarán los resultados de la operación op, que serán del tipo B. def scan[B >: A, That](z: B)(op: (B, B) => B)(implicit cbf: CanBuildFrom[List[A], B, That]): That 4.2.3. Constructores de tipos. En muchas ocasiones se verá el término de constructor de tipos en referencia a las clases parametrizadas. El término constructor de tipos se utiliza para enfatizar en el hecho de que los parámetros de tipo se utilizan para crear tipos específicos. Por ejemplo, se podría decir que List sería el constructor de tipos de los tipos específicos List[Int] o List[Boolean]. 4.2.4. Tipos compuestos. Es posible definir nuevos tipos, tipos compuestos, como resultado de la combinación de otros tipos. El compilador se asegurará, en la medida de la información que disponga, que los tipos sean compatibles. Para combinar tipos utilizaremos la palabra reservada with. Por ejemplo: class MiClase trait MiTrait trait Rasgo type MiTipo = MiClase with MiTrait type MapInt = Map[Int,_] type MapIntRasgo = MapInt with Rasgo En el anterior ejemplo se puede apreciar el uso del guión bajo (_) en la expresión type MapInt = Map[Int,_], en la que actúa como comodín, haciendo referencia a un tipo existencial desconocido en el momento de la definición. En la Subsubsección 2.3.1.1: Rasgos y herencia múltiple en Scala « página 39 », dedicada a estudiar los rasgos y la herencia múltiple en Scala, se vieron la bondades del uso de tipos compuestos. 4.2.5. Tipos estructurales En Scala se pueden definir tipos estructurales haciendo uso de la palabra reservada ty- pe y encerrando entre llaves las definiciones de métodos y variables que deseemos que estén presentes en el tipo definido. Página 133
  • 150. La definición de tipos estructurales permitirá definir tipos abstractos, es decir, Scala permite especificar ciertas características que deben de cumplir los objetos: métodos que deben estar definidos, tipos, etc. Los tipos estructurales sólo tendrán en cuenta la estructura, por lo que podrán ser anónimos aunque normalmente serán tipos nominales. 1 trait ComidaAnimalMap 2 type Animal 3 type ComidaAnimal = {def comida(animal:Any):Unit} 4 5 def addAnimal(animal : Animal, as:List[Animal]):List[Animal] = animal::as 6 7 8 } Algoritmo 4.4: Ejemplo de tipos estructurales. En la línea 3 del algoritmo 4.4 se observa cómo la única condición que se impone a un objeto para satisfacer el tipo ComidaAnimal es que haya implementado el método comida. Scala no permite hacer referencia a tipos abstractos o a parámetros de tipo dentro de la definición de un tipo estructural por lo que no se puede utilizar el tipo Animal. En su lugar se ha utilizado un tipo conocido como el tipo Any. Esto hará que la clase que implemente este método tenga que hacer cast de la variable y transformarla en el tipo correcto. Otro inconveniente que puede presentar el uso de tipos estructurales es el hecho de que, para verificar que una instancia del rasgo ComidaAnimal implementa el método comida, se recomienda importar scala.language.reflectiveCalls, aunque esto añadirá un sobrecoste en de- terminadas situaciones. 4.2.6. Tipos de orden superior. Igual que se ha visto que existen las funciones de orden superior, como funciones que re- ciben otras funciones como argumentos o que devuelven una función, existen tipos de orden superior. Los tipos de orden superior son aquellos que utilizan otros tipos para construir un tipo nuevo7 . Los tipos de orden superior se utilizarán para simplificar la definición de otros tipos o para hacer que tipos complejos se ajusten a parámetros de tipo definidos. Ejemplo: type Funcion[T] = Function1[T,Unit] En el ejemplo anterior el tipo Funcion tomará un parámetro de tipo para construir una nueva función del tipo Function1[T,Unit]. Por tanto, podremos usar el tipo Funcion para simplificar la signatura de las funciones que reciben un único parámetro y no devuelven ningún resultado. Otra característica del tipo Funcion es que no será un tipo completo hasta que se asigne un valor al parámetro de tipo8 . Como se ha descrito anteriormente, el tipo Funcion se puede utilizar para simplificar la definición de funciones que toman un argumento y no devuelven ningún valor. Ejemplo: def sumaTres:Funcion[Int]={x=>println(x+3)} 7 Los tipos de orden superior pueden recibir uno o más tipos como parámetros 8 Al igual que las clases parametrizadas eran consideradas constructores de tipos, los tipos de orden superior también son considerados constructores de tipos ya que pueden ser utilizados para definir tipos específicos. Página 134
  • 151. El tipo de Funcion[Int] será traducido por el compilador al tipo (Int) → Unit. Hasta ahora se ha visto como los tipos de orden superior simplificarán la definición de tipos complejos. Además, se podrán usar para hacer que tipos complejos se ajusten a parámetros de tipo simples. Ejemplo: def ajusteTipo[M[_]](f:M[Int]) = f def masTres = ajusteTipo[Funcion](sumaTres) En el algoritmo 4.5 se puede ver como se comporta el ejemplo anterior en el REPL de Scala. scala> def sumaTres:Funcion[Int]={x=>println(x+3)} sumaDos: Funcion[Int] scala> def ajusteTipo[M[_]](f:M[Int]) = f ajusteTipo: [M[_]](f: M[Int])M[Int] scala> ajusteTipo[Funcion](sumaTres) res2: Funcion[Int] = <function1> Algoritmo 4.5: Ejemplo de tipos de orden superior. En el algoritmo 4.5 se observa como el método ajusteTipo recibe un parámetro de tipo M parametrizado por un tipo desconocido. Como se vio en la Subsección 4.2.4: Tipos compuestos « página 133 », el _ es utilizado como identificador de un tipo existencial desconocido. Si se hubiera intentado evaluar la expresión ajusteTipo[Function1](sumaTres) se habría obtenido un error ya que Function1 toma dos parámetros de tipo y el método ajusteTipo espera un tipo con un único parámetro de tipo. 4.2.7. Tipos existenciales Los tipos existenciales representan una forma de abstracción sobre tipos permitiendo indicar en el código la existencia de un tipo sin especificar de qué tipo se trata. Serán de especial utilidad en aquellas ocasiones en las que no se conoce el tipo o simple- mente no es necesario indicarlo porque no aporte información relevante en ese contexto. Formalmente los tipos existenciales se definen haciendo uso de la palabra reservada forSo- me: type forSome lista de tipos y vals abstractos Los tipos existenciales se emplearán sobre todo para acceder a algunas clases Java para las que los tipos conocidos previamente no aportan una solución como, por ejemplo, Collec- tion<?>. En general, los tipos de datos existenciales son muy importantes para mantener la compatibilidad entre los tipos en Java y los tipos en Scala en tres situaciones: Cuando en Java se usa el comodín para expresar la varianza en los tipos genéricos. Los parámetros de tipo de los genéricos son eliminados por el proceso borradura por lo que, por ejemplo, a nivel de código de la Máquina Virtual de Java, para la JVM es imposible distinguir entre estas dos listas basándose en la información de tipos conocida: List[Perros] y List[Pajaros]. Los tipos simples9 , como todos los tipos existentes en las bibliotecas de Java (antes de la versión 5 de Java), en la que todos los parámetros de tipo eran objetos del tipo Object. 9 Tipos de datos sin parámetros de tipo Página 135
  • 152. Normalmente los tipos existenciales se indicarán haciendo uso del guión bajo _, de modo que cuando Scala encuentre el guión bajo en el lugar en el que debería aparecer un tipo colocará un tipo existencial en dicho lugar10 . Como se muestra en la tabla 4.1 es posible utilizar cotas superiores y/o cotas inferiores en los tipos existenciales. Def. abreviada Def. formal Significado List[_] List[T] forSome type T Lista de cualquier tipo List[_ <: Perro] List[T] forSometype T <: Perro Lista de cualquier tipo que sea sub- tipo del tipo Perro List[_ >: Animal] List[T] forSometype T >: Ani- mal Lista de cualquier tipo que sea su- pertipo del tipo Animal Tabla 4.1: Tipos existenciales en Scala 4.3. Teoría de categorías La teoría de categorías es una teoría que trata de axiomatizar de forma abstracta diversas estructuras matemáticas como una sola, mediante el uso de objetos y morfismos. Al mismo tiempo trata de mostrar una nueva forma de ver las matemáticas sin incluir las nociones de elementos, pertenencia, entre otras.[34] La definición general de categoría contiene tres entidades: 1. Una clase que actúa como contenedor de objetos. 2. Una clase de morfismos, también llamados aplicaciones que generalizan el concepto de función f : A → B (f:A=>B en Scala). 3. Una operación binaria, llamada composición de morfismos, que tiene la propiedad de ∀f, g | f : A → B, g : B → C; ∃ g ◦ f : A → C. Además la composición de morfismos satisface dos axiomas: Existencia de un único morfismo identidad, IDx en el que el dominio y el codominio son iguales. La composición con el morfismo identidad tiene la siguiente propiedad: f ◦ IDx = IDx ◦ f Propiedad asociativa en la composición, ∀f : A → B, g : C → A, h : D → C : (f ◦ g) ◦ h = f ◦ (g ◦ h) La teoría de las categorías modela muchos aspectos de la programación aunque en la ma- yoría de las ocasiones pasa desapercibida para los programadores. Una buena forma aprovechar la aplicación de la teoría de las categorías en programación es a través de los patrones de diseño. La teoría de las categorías define conceptos abstractos de bajo nivel que pueden ser expresados directamente en un lenguaje de programación funcional como Scala, además de ofrecer librerías de soporte como Scalaz11 . A continuación se verán dos categorías muy usadas en el desarrollo software: Funtores y Mónadas. 10 Cada guión bajo será convertido a un parámetro de tipo en una sentencia forSome por lo que si se utiliza dos veces el guión bajo en el mismo tipo Scala pondrá una sentencia forSome con dos tipos 11 https://github.com/scalaz/scalaz. Página 136
  • 153. 4.3.1. El patrón funcional Funtor Los funtores representan una categoría dentro de la teoría de las categorías. Los funtores representan transformaciones de una categoría en otra categoría que también deben transformar y preservar los morfismos. En programación, los funtores se entenderán como patrones funcionales, en los que a un contenedor que referencia a un conjunto de objetos se le quiere dotar de la posibilidad de aplicar una función a cualquier objeto dentro del mismo, algo que se realizará sin alterar la estructura del propio contenedor12 . Dicho de otro modo, los funtores permitirán aplicar funciones puras (f : A → B), a los elementos de un contenedor en el que existan uno o más valores del tipo A. Es decir, los funtores representan la abstracción de la función map que se ha visto anteriormente. Además, los funtores serán clases, objetos, rasgos...en los que habitualmente sólo habrá un método (el método map) que será el encargado de aplicar la función deseada a los objetos del contenedor. Los funtores, como objetos, podrán ser pasados como parámetros y utilizados en el mismo lugar en el que pueda aparecer una función para ser aplicada a un conjunto de objetos. A continuación se muestra una posible implementación en el siguiente rasgo: trait Funtor[F[_]] { def map[A,B](fa: F[A])(f: A => B): F[B] } Se puede observar que el rasgo Funtor está parametrizado en el tipo. A continuación se muestra una implementación concreta para tres tipos concretos, Seq, Option y Set: object seqFuntor extends Funtor[Seq] { def map[A,B](seq: Seq[A])(f: A => B): Seq[B] = seq map f } object setFuntor extends Funtor[Set] { def map[A,B](set: Set[A])(f: A => B): Set[B] = set map f } object optFuntor extends Funtor[Option] { def map[A,B](opt: Option[A])(f: A => B): Option[B] = opt map f } A continuación, se verá un ejemplo en el que se hará uso de las implementaciones seqFuntor, setFuntor y optFuntor13 : scala> def multD(x:Int):Double= x*2.5 multD: (x: Int)Double scala> seqFuntor.map(List(1,2,3))(doble) res26: Seq[Int] = List(2, 4, 6) scala> seqFuntor.map(List())(doble) res27: Seq[Int] = List() scala> setFuntor.map(Set(1,2,3))(triple) res29: Set[Int] = Set(3, 6, 9) scala> optFuntor.map(Some(5))(cuadrado) res30: Option[Int] = Some(25) scala> optFuntor.map(Some(5))(multD) res31: Option[Double] = Some(12.5) 12 Normalmente se devolverá una nuevo contenedor con los resultados de aplicar la función a los elementos del contenedor original. 13 Se consideran definidas anteriormente las funciones doble, triple y cuadrado cuyo dominio y codominio es Int Página 137
  • 154. scala> optFuntor.map(None)(multD) res32: Option[Double] = None Con la abstracción Funtor es posible crear un conjunto de funciones que se puedan implementar haciendo uso de map, como por ejemplo la función distribuir: 1 def distribuir[A,B](fab: F[(A, B)]): (F[A], F[B]) = (map(fab)(_._1), map(fab)(_._2)) Algoritmo 4.6: Función distribuir usando funtores. Si se hablara en términos de teoría de categorías, otras categorías serán los objetos y los morfismos serán las funciones entre las categorías. Los funtores presentan dos propiedades fundamentales que son consecuencia directa de las propiedades y axiomas de la teoría de categorías: 1. Un funtor F preserva la identidad, esto significará que la identidad del dominio se corres- ponderá con la identidad del codominio. 2. Un funtor F preserva la composición. F(f ◦ g) = F(f) ◦ F(g) Una vez definidos los funtores, se puede observar que las clases de la biblioteca collection de Scala presentan las características propias de los funtores. A pesar de que los funtores son mucho más importantes en la teoría de categorías que las mónadas, su importancia en la programación es anecdótica si se compara con las mónadas, el siguiente patrón de programación funcional que se estudiará. 4.3.2. El patrón funcional Mónada Las mónadas representan otra categoría dentro de la teoría de categorías. Los fundamentos matemáticos que definen a las mónadas serán importantes a la hora de definir el patrón mónada. El término mónadas tiene su origen en el término griego monas, usado por los filósofos de la antigua Grecia, que quiere decir algo así como "la Divinidad por la que otras cosas son creadas". En un programación funcional pura, una mónada es una estructura que representa las computaciones en un conjunto de objetos. Si el patrón funcional funtor servía para abstraer la aplicación de una función a los elementos referenciados por un contenedor, el patrón mónada se usará para realizar un aplanado de la aplicación de determinados funtores. Más concretamen- te, el patrón mónada será el encargado de establecer como crear los contenedores para manejar la creación y la combinación de mónadas, la aplicación de funciones a miembros de ese con- tenedor, como varias funciones son aplicadas a los elementos del contenedor y como múltiples contenedores pueden ser aplanados en un contenedor único. Como se verá a continuación, el patrón mónada aparece muy frecuentemente dentro de la programación funcional. Al igual que ocurría en el caso de los funtores, las mónadas serán clases, objetos, rasgos...en los que habitualmente sólo habrá un método (el método flatMap) que será el encargado de apli- car la función deseada a los objetos del contenedor. Las mónadas se suelen utilizar en progra- mación funcional para abstraer el comportamiento en la ejecución de un programa. Algunas mónadas se utilizan para manejar la concurrencia, las excepciones o los efectos colaterales, por ejemplo. Durante la sección dedicada al estudio de las colecciones en Scala se ha podido apreciar que es muy común que aparezcan definidas las operaciones map y flatMap dentro de las estructuras Página 138
  • 155. de datos. De hecho, hay un nombre para definir a estas estructuras que, además, cumplen unas reglas algebraicas. Se llamarán Mónadas14 . Una mónada será un tipo paramétrico M[T] con dos operaciones: flatMap15 y unit que deben satisfacer algunas reglas. Por tanto, una posible definición de mónadas en Scala podría ser: trait M[T] { def flatMap[U](f: T => M[U]): M[U] def unit[T](x: T): M[T] } Algoritmo 4.7: Definición básica del trait Mónada. Dentro del rasgo M (que representa la mónada) se encuentra el método flatMap que toma un tipo U como parámetro y una función que mapea T a una mónada de U, devolviendo la misma mónada aplicada a U. Después, se encontrará el método unit que toma un elemento del tipo T y devuelve una instancia de la mónada de T. Por tanto, sabiendo que la función flatMap se encuentra definida para algunas de las estruc- turas vistas anteriormente, algunos ejemplos de mónadas serían: List es una mónada donde unit(x)=List(x) Set es una mónada donde unit(x)=Set(x) Option es una mónada donde unit(x)=Some(x) 4.3.2.1. Reglas que deben satisfacer las mónadas Pero como se ha dicho anteriormente, estas operaciones deben de satisfacer ciertas propie- dades: 1. Asociatividad: m flatMap f flatMap g == m flatMap (x => f(x) flatMap g) 2. Unit es identidad por la izquierda: unit(x) flatmap f == f(x) 3. Unit es identidad por la derecha: m flatmap unit == m Se comprueba que, efectivamente, la clase Option satisface las reglas descritas anteriormen- te. Para las demostraciones, se tendrá que recordar la definición del método flatMap dentro de la clase Option: abstract class Option[+T] { def flatMap[U](f: T => Option[U]): Option[U] = this match { case Some(x) => f(x) case None => None } } En primer lugar, se comprueba que se cumple la propiedad unit es identidad por la izquierda: Habrá que demostrar: Some(x) flatMap f == f(x) 14 Haskell, un lenguaje de programación funcional en el que se enfatiza en la pureza funcional, fue pionero en el uso de mónadas al separar la entrada y salida (IO) de lo que es puramente código. 15 Dentro de la programación funcional es posible reconocer esta operación como bind Página 139
  • 156. Some(x) flatMap f == Some(x) match { case Some(x) => f(x) case None => None } == f(x) A continuación, se verifica que se cumple la regla unit es identidad por la derecha: Se tendrá que demostrar: opt flatMap Some == opt opt flatMap Some == opt match { case Some(x) => Some(x) case None => None } == opt Finalmente se demuestra la propiedad de la asociatividad: En este caso hay que demostrar: opt flatMap f flatMap g == opt flatMap (x => f(x) flatMap g) opt flatMap f flatMap g == opt match { case Some(x) => f(x) case None => None } == opt match { case Some(x) => f(x) match { case Some(y) => g(y) case None => None } case None => None match { case Some(y) => g(y) case None => None } } == opt match { case Some(x) => f(x) match { case Some(y) => g(y) case None => None } case None => None } == opt match { case Some(x) => f(x) flatMap g case None => None } == opt flatMap (x => f(x) flatMap g) Página 140
  • 157. 4.3.2.2. Importancia de las propiedades de las mónadas en las expresiones for El hecho de que se verifiquen las anteriores reglas tendrá una importancia especial en la definición de las expresiones for. 1. La asociatividad permitirá anidar los generadores dentro de un bucle for: for (y <- for (x <- m; y <- f(x)) yield y z <- g(y)) yield z == for (x <- m; y <- f(x); z <- g(y)) yield z 2. La regla derecha de unit implica que: for (y <- m) yield y == m 3. La regla derecha de unit no tiene un análogo dentro de las expresiones for. 4.3.2.3. Map en las mónadas Hasta el momento no se ha referido el hecho de definir la función map para las mónadas ya que ésta puede ser definida en términos de fmap: m map f == m flatmap (x=> unit(f(x))) == m flatmap (f andThen unit) Se podría completar la definición del rasgo definido anteriormente para representar móna- das, con la definición de la función map y la función map2. Se aprovechará la ocasión para dar una definición más amplia para el rasgo Mónada, utilizando tipos de orden superior: trait Monada[M[_]]{ def unit[A](a:A):M[A] def flatMap[A,B] (ma:M[A])(f:A=>M[B]):M[B] def map[A,B](ma:M[A])(f:A=>B):M[B]= flatMap (ma) (x=> unit(f(x))) def map2[A,B,C](ma:M[A], mb:M[B])(f:(A,B)=>M[C]):M[C] = flatMap (ma) (x => map (mb) (y=> f(x,y))) } } De este modo, al implementar los métodos flatMap16 y unit necesarios para definir una mónada estarán disponibles también los métodos map y map2. Ahora el rasgo podría heredar de Funtor al incorporar una definición de map. Después de haber visto los funtores y las mónadas, se puede decir que “todas las mónadas son funtores pero no todos los funtores son mónadas”. 16 En esta ocasión se han utilizado dos parámetros de tipo para la definición de la función flatMap, algo que difiere de la definición vista en el algoritmo4.7 ya que en la signatura del rasgo Monada se ha utilizado el _ para indicar que Monada recibe un parámetro existencial desconocido. Página 141
  • 158. 4.3.2.4. La importancia de las mónadas Las mónadas son importantes dentro de la programación ya que son capaces de envolver la información que rodea a un valor del mismo modo que envolvería el propio valor, minimizando el acoplamiento entre ambos. Inspirándose en el uso de este patrón en Haskell, el patrón Mónada también se utiliza recu- rrentemente en Scala. De hecho, ya se han visto algunos ejemplos de clases monádicas en Scala como puedan ser los tipos List o Either17 . Ambas clases monádicas presentan características comunes: Implementan el método flatMap y la construcción haciendo uso del método de fábrica apply en lugar de unit. Pueden utilizarse en expresiones for. Permiten aplicar una secuencia de funciones y manejar fallos de diferentes formas (nor- malmente devolviendo una subclase del tipo). 4.3.2.5. La mónada Identidad A continuación se verá una de las principales características de las mónadas ejemplificada en la mónada identidad, la cual sólo se encargará de envolver los datos. Para ello, se definirá la clase parametrizada Id que recibirá un único parámetro, el valor que se desea envolver. Dentro de la clase Id se definirán los métodos map y flatMap. case class Id[A](value:A){ def map[B](f:A=>B):Id[B]=Id(f(value)) def flatMap[B](f:A=>Id[B]):Id[B]=f(value) } A continuación, se implementará el objeto MonadaID[Id]. Es importante recordad que sólo se tienen que definir los métodos unit y flatMap: object MonadaID extends Monada[Id]{ def unit[A](a:A):Id[A]=Id(a) def flatMap[A,B](ma:Id[A])(f:A=>Id[B]):Id[B]=ma flatMap f } Se aprecia que la Id es un simple envoltorio, sin añadir ningún tipo de información adicional. Si se observa la función flatMap, ésta desempeña una tarea simple en la mónada identidad, la sustitución de variables. En el algoritmo 4.8 se puede ver este comportamiento. 17 Tomando como operaciones unit(x)=List(x) en las listas o, en el caso de los conjuntos, unit(x)=Set(x). Página 142
  • 159. import scala.language.higherKinds scala> trait Monada[M[_]]{ def unit[A](a:A):M[A] def flatMap[A,B] (ma:M[A])(f:A=>M[B]):M[B] def map[A,B](ma:M[A])(f:A=>B):M[B]= flatMap (ma) (x=> unit(f(x))) } defined trait Monada scala> case class Id[A](value:A){ def map[B](f:A=>B):Id[B]=Id(f(value)) def flatMap[B](f:A=>Id[B]):Id[B]=f(value) } defined class Id scala> object MonadaID extends Monada[Id]{ def unit[A](a:A):Id[A]=Id(a) def flatMap[A,B](ma:Id[A])(f:A=>Id[B]):Id[B]=ma flatMap f } defined object MonadaID scala> for (x<- Id("Hola "); y<- Id("mundo!")) yield x+y res0: Id[String] = Id(Hola mundo!) Algoritmo 4.8: Monada Identidad Es posible observar en la expresión for del algoritmo 4.8 como: Se puede utilizar la clase monádica Id en expresiones for. Las variables x e y toman los valores “Hola ” y “mundo!”, respectivamente. Estos valores son substituidos en la expresión x+y. Ahora se puede afirmar que es posible utilizar las mónadas para envolver valores, o algo más contundente, la mónada es un tipo de lenguaje de programación que soporta la sustitución de variables[5] 4.3.2.6. Envolviendo el contexto con mónadas. La clase monádica Try Se ilustrará uno de los principales usos de las mónadas, envolver el contexto en el que se ejecuta el programa, desarrollando un juego muy simple18 . Se desarrollará un juego, JailBreak, en el que el héroe tendrá que rescatar a su amigo de la cárcel. Para ello necesitará conseguir la llave maestra que abre la celda en la que está encerrado su amigo. La misión de conseguir la llave maestra será muy peligrosa, ya que está custodiada por la guardia. Se sabe que la guardia posee 5 llaves: dos llaves amarillas, dos llaves verdes y una llave roja. La llave roja abre el teatro. Las dos llaves amarillas son indistinguibles entre si. Una llave amarilla sirve para abrir la celda y la otra llave amarilla abre la puerta de los vestuarios. Igualmente, cada una de las llaves verdes, indistinguibles entre si, abre una puerta diferente, una abrirá la celda mientras que la otra abrirá el gimnasio. Cada día se reparten las llaves de modo que el director tiene dos llaves, una verde y otra amarilla, pero no sabe que puerta abre cada una. El guardia se queda con las otras tres llaves en un llavero y éstas serán las llaves que nuestro héroe deberá conseguir. Si el guardián que vigila la llave descubre a el héroe, la partida habrá finalizado. Aunque si el héroe tiene fortuna, podrá coger el llavero con las tres llaves aprovechando que los guardas se distraen durante la mitad del tiempo que custodian la llave, aunque se desconoce el momento en el que están distraídos. Una vez conseguida la llave, podrá salvar a su amigo siempre y cuando la llave maestra que abre la celda se encuentre en el llavero. 18 Adaptación del juego publicado en [19] Página 143
  • 160. Una vez más, el héroe deberá ser afortunado ya que ni el director, ni el guardia encargado de custodiar las llaves saben qué puertas abre cada una de las llaves indistinguibles que poseen. El algoritmo 4.9 muestra la definición de algunos tipos que se utilizarán en la resolución del juego. 1 trait Llave{ 2 val maestra:Boolean 3 } 4 case class Amarilla(maestra:Boolean) extends Llave 5 case object Roja extends Llave{ 6 val maestra=false; 7 } 8 case class Verde(maestra:Boolean) extends Llave 9 10 case class Liberado(amigo:String) { 11 override def toString():String= amigo+ " eres Libre" 12 13 } 14 case class GameOverException(i:String) extends Exception(i) Algoritmo 4.9: Juego JailBreak. Definición tipos necesarios. En el algoritmo 4.10 se define el rasgo que servirá para definir los métodos que posterior- mente serán implementados por nuestra clase JailBreak. 1 2 trait Game { 3 4 def rescatar(llaves:List[Llave]):Liberado 5 def conseguirLlave():List[Llave] 6 } Algoritmo 4.10: Juego JailBreak. Trait Game. Conocida la interfaz, el algoritmo 4.11 muestra una posible resolución del juego planteado sin tener en cuenta la implementación de la clase JailBreak que heredará del rasgo Game. 1 val jailBreakGame = new JailBreak() 2 val llaves = jailBreakGame.conseguirLlave() 3 val resultado=jailBreakGame.rescatar(llaves) Algoritmo 4.11: Juego JailBreak. Solución al juego. En principio, sin más información, parece una solución bastante simple pero acertada. Co- mo se ha dicho anteriormente, no se ha tenido en cuenta la implementación de los métodos conseguirLlave y rescatar que la clase JailBreak tiene que implementar, ya que se encuentran definidos en el rasgo Game. Finalmente, en el algoritmo 4.12 se muestra la implementación de los métodos conseguir- Llave y rescatar que hace la clase JailBreak. Página 144
  • 161. 1 class JailBreak extends Game{ 2 def conseguirLlave():List[Llave]={ 3 if (descubierto()) throw new GameOverException("Game Over: Has sido atrapado") 4 List(Amarilla(Random.nextBoolean()), Roja, Verde(Random.nextBoolean())) 5 } 6 7 def rescatar(llaves:List[Llave]):Liberado={ 8 if (!llaveCorrecta(llaves)) throw new GameOverException("Game Over: Buen intento! No tienes la llave maestra") 9 Liberado("Luis") 10 } 11 12 // Metodos auxiliares 13 private def descubierto():Boolean=Random.nextBoolean() 14 private def llaveCorrecta(llaves:List[Llave])= 15 llaves.foldLeft(false)((acu,llave)=>acu||llave.maestra) 16 } 17 } Algoritmo 4.12: Juego JailBreak. Definición de la clase JailBreak. A continuación se analizarán algunos de los detalles relevantes de la definición de la clase JailBreak hecha en el algoritmo 4.12: Si se presta atención a la definición del método conseguirLlave, se observa que si el héroe es descubierto, se lanzará una excepción del tipo GameOverException, lo cual provocará un fallo en la ejecución del programa. Si no se encuentra la llaveCorrecta en el llavero que nuestro héroe ha conseguido, el mé- todo rescatar lanzará una excepción del tipo GameOverException, provocando un fallo en la ejecución del programa. Siguiendo la definición del problema, existe un 50 % de posibilidades de que el guarda descubra al héroe, así como un 50 % de posibilidades de que la llave maestra sea la ama- rilla y un 50 % de posibilidades de que la llave maestra sea la verde. Estas probabilidades se han implementando utilizando el método nextBoolean() del objeto Random definido en scala.util.Random19 , el cual generará aleatoriamente valores booleanos. El método auxiliar llaveCorrecta es utilizado para determinar si se encuentra la llave correcta en el llavero, empleando un plegado de listas. Por tanto, el algoritmo 4.11 estará bloqueado en la línea 2 mientras se ejecuta el método conseguirLlave. Si el método finaliza como se espera, el algoritmo volverá a estar bloqueado en la línea 3 mientras se ejecuta el método rescatar. Aunque cuando se ve el algoritmo 4.11 no se puede imaginar que el código puede presentar estos problemas, ya que nada hace pensar que esta resolución simple pueda fallar, se ha visto como puede bloquearse la ejecución del mismo y provocar un funcionamiento incorrecto del programa. 19 Biblioteca que habrá que importar para la compilación del programa. Página 145
  • 162. A continuación se muestra ejemplificado el funcionamiento del programa en el REPL de Scala, tras haber introducido previamente las definiciones vistas en los algoritmos 4.9, 4.10 y 4.12 que como se ha indicado puede presentar un comportamiento no deseado: scala> val jugar= new JailBreak() jugar: JailBreak = JailBreak@32a53d86 scala> val llaves = jugar.conseguirLlave() llaves: List[Llave] = List(Amarilla(true), Roja, Verde(true)) scala> val resultado = jugar.rescatar(llaves) resultado: Liberado = Luis eres Libre scala> jugar.conseguirLlave() java.lang.Exception: Game Over: Has sido atrapado at JailBreak.conseguirLlave(<console>:24) ... 33 elided scala> val masterkeys=jugar.conseguirLlave() masterkeys: List[Llave] = List(Amarilla(false), Roja, Verde(false)) scala> val resultado2=jugar.rescatar(masterkeys) java.lang.Exception: Game Over: Buen intento! No tienes la llave maestra at JailBreak.rescatar(<console>:29) ... 33 elided La mónada Try. Para solucionar este problema, se utilizará alguna de las clases monádicas existentes. En la Sección 4.4: Manejo de errores sin usar excepciones « página 149 » se verán las clases monádicas Option y Either en el ámbito de manejar posibles errores sin necesidad de lanzar excepciones. Para las situaciones en las que se tenga que lidiar con excepciones existe otra mónada, la mónada Try20 , la cual nos permitirá capturar excepciones en un envoltorio, informar de la existencia de estos efectos colaterales y elevar este contexto para su posterior tratamiento. abstract class Try[T] case class Success[T](elem: T) extends Try[T] case class Failure(t: Throwable) extends Try[Nothing] object Try { def apply[T](r: =>T): Try[T] = { try { Success(r) } catch { case t => Failure(t) } } Se puede observar que existen dos subclases de la clase abstracta Try, Success y Failure, que representan las situaciones de éxito y fallo en la ejecución de un bloque, método...Se puede ver como el método de fábrica apply del objeto acompañante de Try, recibe un parámetro que sigue la estrategia de evaluación evaluación no estricta. El motivo es que se pretende capturar el resultado de la evaluación de este parámetro y si se hubiera pasado por valor (evaluación estricta) no se podría capturar su comportamiento, el cual se captura posteriormente dentro de los bloques try/catch. A continuación, se verán las modificaciones necesarias que habrá que realizar al código de los ejemplos anteriores para hacer uso de la clase monádica Try. En el algoritmo 4.13 se muestra como quedaría la definición del rasgo Game realizada previamente en el algoritmo 4.10, 20 Estrictamente hablando, Try no sería una mónada ya que si se comprueban las reglas que han de cumplir las mónadas se puede comprobar como falla la regla derecha de unit. Página 146
  • 163. mientras que el algoritmo 4.14 muestra como quedaría la clase JailBreak, vista anteriormente en el algoritmo 4.12, adaptada al uso de Try. 1 2 trait Game { 3 4 def rescatar(llaves:List[Llave]):Try[Liberado] 5 def conseguirLlave():Try[List[Llave]] 6 } Algoritmo 4.13: Juego JailBreak. Trait Game incluyendo Try. Ahora, simplemente al ver el rasgo Game, se puede observar que los métodos rescatar y conseguirLlave presentan efectos colaterales que son tratados por la mónada Try. 1 class JailBreak extends Game{ 2 def conseguirLlave():Try[List[Llave]]={Try({ 3 if (descubierto()) throw new GameOverException("Has sido atrapado") 4 List(Amarilla(Random.nextBoolean()),Roja, 5 Verde(Random.nextBoolean())) }) 6 } 7 8 def rescatar(llaves:List[Llave]):Try[Liberado]={Try({ 9 if (!llaveCorrecta(llaves)) throw new GameOverException("Buen intento! No tienes la llave maestra") 10 Liberado("Luis")}) 11 } 12 ... 13 } Algoritmo 4.14: Juego JailBreak. Trait Game incluyendo Try. En el algoritmo 4.14 se observan modificaciones en la implementación de los métodos con- seguirLlave y rescatar. El cuerpo de ambos métodos ha sido encerrado entre llaves, formando un bloque, el cual se pasa como parámetro al método de fábrica apply del objeto acompañante de Try. El código de la solución al juego vista en el algoritmo 4.11 también se verá afectada por el uso de Try, quedando como se muestra en el algoritmo 4.15. 1 val jailBreakGame = new JailBreak() 2 val llaves:Try[List[Llave]]= jailBreakGame.conseguirLlave() 3 val resultado= llaves match { 4 case Success(lla)=>jailBreakGame.rescatar(lla) 5 case failure @ Failure(t)=>failure 6 } Algoritmo 4.15: Juego JailBreak. Solución al juego. Ahora se observa que para la definición de la variable inmutable resultado se utiliza concor- dancia de patrones, definiendo dos posibles situaciones (una por cada posible valor de Try). Try define algunas funciones de orden superior que pueden ser de gran utilidad a la hora de manejar esta mónada, entre las que se encuentran las siguientes funciones: Página 147
  • 164. def flatMap[S](f: T => Try[S]):Try[S] def map[S](f: T => S):Try[S] def flatten[U <: Try[T]]:Try[U] def filter (p: T => Boolean): Try[T] Dado que se utilizará la mónada Try, en la que se encuentra definida la función flatMap, en primer lugar se deberá de ver como está definida esta función: def flatMap[S](f: T=>Try[S]): Try[S] = this match { case Success(value) =>try { f(value) } catch { case t => Failure(t) } case failure @ Failure(t) => failure } Ahora que es conocida la definición de la función flatMap, es posible refinar un poco el código del algoritmo 4.15, obteniendo el algoritmo 4.16. 1 val jailBreakGame = new JailBreak() 2 val llaves:Try[List[Llave]] = jailBreakGame.conseguirLlave() 3 val resultado = llaves flatMap ( llavero => jailBreakGame.rescatar (llavero) ) Algoritmo 4.16: Juego JailBreak. Solución al juego utilizando flatMap. Como se aprecia en el algoritmo 4.17, se podría refinar el algoritmo 4.16 algo más, aun- que habrá muchos programadores que opten por la implementación del algoritmo 4.16, en el que la variable llaves permite una legibilidad más clara del código. Evidentemente, con ambas soluciones se obtendrá un resultado correcto. 1 val jailBreakGame = new JailBreak() 2 val resultado= jailBreakGame.conseguirLlave().flatMap (llavero => jailBreakGame.rescatar(llavero)) Algoritmo 4.17: Juego JailBreak. Solución al juego utilizando flatMap 2. Si se vuelve a introducir la definición del rasgo Game y de la clase JailBreak, se verá ahora que el juego, como era de esperar, presenta el comportamiento esperado: scala> val resultado9 = jailBreakGame.conseguirLlave().flatMap (llavero => jailBreakGame.rescatar(llavero)) resultado9: scala.util.Try[Liberado] = Failure(GameOverException: Has sido atrapado) scala> val resultado10 = jailBreakGame.conseguirLlave().flatMap (llavero => jailBreakGame.rescatar(llavero)) resultado10: scala.util.Try[Liberado] = Success(Luis eres Libre) scala> val resultado11 = jailBreakGame.conseguirLlave().flatMap (llavero => jailBreakGame.rescatar(llavero)) resultado11: scala.util.Try[Liberado] = Failure(GameOverException: Buen intento! No tienes la llave maestra) Otra de las características que se han destacado de las mónadas es la posibilidad de ser utilizadas en expresiones for. En el algoritmo 4.18 de expresa el código del algoritmo 4.17 usando expresiones for. Página 148
  • 165. 1 val jailBreakGame = new JailBreak() 2 for (llaves <- jailBreakGame.conseguirLlave(); 3 resultado <- jailBreakGame.rescatar(llaves)) yield liberado Algoritmo 4.18: Juego JailBreak. Solución al juego utilizando expresiones for. 4.4. Manejo de errores sin usar excepciones 4.4.1. Introducción Como se vio en la Sección 3.2: Sentido estricto y amplio de la programación funcional « página 54 », lanzar excepciones y parar la ejecución de un programa son efectos colaterales que no se pueden producir en las funciones puras ya que no cumplirían la transparencia referencial, sin la cual, la programación funcional no tendría sentido. Cuando se lanza una excepción no se produce un valor en sí, sino que el flujo del programa salta hasta el bloque catch que captura a la misma por lo que el valor final dependerá del con- texto en el que la excepción sea tratada. Haciendo uso de throw y catch será imposible sustituir los términos de las expresiones por sus definiciones. Para tratar el uso de situaciones anómalas en el código, así como el manejo de errores, se utilizará una técnica basada en devolver un valor especial indicando que ha ocurrido una situación especial. Para manejar situaciones excepcionales existen diferentes opciones: Lanzar excepciones. Incluir un argumento en la llamada a nuestras funciones indicando qué hacer cuando se produzcan estos casos especiales. Usar el tipo de datos Option. Usar el tipo de datos Either. Se entenderán mejor estas opciones viendo el siguiente ejemplo, en el que la función media calcula la media de una lista de enteros: 1 2 def media(xs: List[Int]): Int = 3 if (xs.isEmpty) throw new ArithmeticException("media de una lista vacia!") 4 else xs.sum / xs.length Algoritmo 4.19: Excepciones.Función media con excepciones Por los motivos descritos anteriormente, se evitará el uso de excepciones para tratar casos excepcionales. Ahora se mostrará una solución basada en la inclusión de un argumento para indicar qué hacer en caso de que se den estas situaciones especiales: 1 def media_1(xs: List[Int], siVacia: Int): Int = 2 if (xs.isEmpty) siVacia 3 else xs.sum / xs.length Algoritmo 4.20: Excepciones.Función media con argumento para caso especial Página 149
  • 166. Esta opción presenta una ventaja, ya que se ha convertido la función media en una función total. Pero también se observan dos grandes inconvenientes: Se requiere saber como manejar estos casos especiales a la hora de realizar las llamadas. Limita el valor devuelto a un valor del tipo devuelto por la función, en este caso Int. 4.4.2. Tipo de datos Option Por su simplicidad es una de las soluciones más usadas. Algo que se puede apreciar en la biblioteca estándar de Scala, por ejemplo: La búsqueda de una clave en un Map devuelve un valor de tipo Option Las funciones headOption y lastOption devuelven, respectivamente, un tipo Option que contendrá el primer y el último elemento de una estructura iterable (como por ejemplo las listas) en caso de que la lista no esté vacía. La solución basada en el tipo de datos Option servirá para representar, de forma explícita, el hecho de que en alguna situación especial el valor devuelto no esté definido. Para comprender mejor esta alternativa se muestra el tipo de datos Option: 1 sealed trait Option[+A] 2 case class Some[+A](get: A) extends Option[A] 3 case object None extends Option[Nothing] Algoritmo 4.21: Tipo de datos Option Como se puede ver, Option presenta dos casos: 1. Some. Cuando el valor devuelto esté definido. 2. None. Para esas situaciones especiales en las que el valor devuelto no está definido. Se verá como quedaría el ejemplo con el uso del tipo de datos Option: 1 def media_2(xs: List[Int], siVacia: Int): Option[Int] = 2 if (xs.isEmpty) None 3 else Some(xs.sum / xs.length) Algoritmo 4.22: Excepciones. Función media con tipo de datos Option Ahora se puede ver que la función media devuelve un valor del tipo de datos de retorno (en nuestro ejemplo Option[Int]) para cada una de las entradas posibles, por lo que la función media será una función total. Este es otro de los usos del tipo de datos Option, convertir funciones parciales en funciones totales. Un inconveniente que presenta el tipo de datos Option para el manejo de errores y casos especiales es que no aporta información sobre el error o situación especial, simplemente se devuelve None. Página 150
  • 167. 4.4.3. Tipo de datos Either Si se necesitara obtener más información sobre el error que se ha producido para su posterior tratamiento, el tipo de datos Option no se ajustará a estas nuevas necesidades, ya que sólo indica que se ha producido un error pero no aporta más información. Para las situaciones en las que se necesite obtener más información sobre el tipo de error que se ha producido se utilizará el tipo de datos Either. Para comprender mejor esta opción para el manejo de errores y casos especiales, se verá en primer lugar la definición del tipo de datos Either: 1 sealed trait Either[+E, +A] 2 case class Left[+E](value: E) extends Either[E, Nothing] 3 case class Right[+A](value: A) extends Either[Nothing, A] Algoritmo 4.23: Tipo de datos Either Either, al igual que Option, presenta dos casos pero en esta ocasión ambos casos definen un valor. Otra diferencia entre ambos es que el tipo de datos Either representa, de forma general, valores que pueden ser de dos tipos disjuntos. Cuando se utiliza el tipo de datos Either para el manejo de errores, por convención, el constructor Left se utiliza para indicar el error, fallo...que se ha producido. A continuación se muestra como quedaría el ejemplo haciendo uso del tipo de datos Either: 1 def media_3(xs: List[Int], siVacia: Int): Either[String,Int] = 2 if (xs.isEmpty) 3 Left("media de una lista vacia!") 4 else 5 Right(xs.sum / xs.length) Algoritmo 4.24: Excepciones.Función media con tipo de datos Either En ocasiones, sobre todo a la hora de depurar, se puede necesitar más información sobre el error producido. Para ello puede ser de gran ayuda incluir la excepción en el valor devuelto. Ejemplo: 1 def division(x: Double, y: Double): Either[Exception, Double] = 2 try { 3 Right(x / y) 4 } catch { 5 case e: Exception => Left(e) 6 } Algoritmo 4.25: Excepciones.Division con Either 4.5. Ejercicios Ejercicio 62. Se van a realizar búsquedas dentro de una base de datos de familias. El resultado de la búsqueda puede ser un valor y también puede ser que no tenga éxito. Implementar la mónada Maybe que capture este comportamiento. Ejercicio 63. Dada la siguiente definición de la clase y el objeto persona: Página 151
  • 168. 1 object Persona { 2 3 val personas = List("P", "MP", "MMP", "FMP", "FP", "MFP", "FFP") map { Persona(_) } 4 5 private val madres = Map( 6 Persona("P") -> Persona("MP"), 7 Persona("MP") -> Persona("MMP"), 8 Persona("FP") -> Persona("MFP")) 9 10 private val padres = Map( 11 Persona("P") -> Persona("FP"), 12 Persona("MP") -> Persona("FMP"), 13 Persona("FP") -> Persona("FFP")) 14 15 def madre(p: Persona): Maybe[Persona] = relacion(p, madres) 16 17 def padre(p: Persona): Maybe[Persona] = relacion(p, padres) 18 19 private def relacion(p: Persona, relacionMap: Map[Persona, Persona]) = relacionMap.get(p) match { 20 case Some(m) => Just(m) 21 case None => MaybeNot 22 } 23 } 24 25 case class Persona(nombre: String) { 26 def madre: Maybe[Persona] = Persona.madre(this) 27 def padre: Maybe[Persona] = Persona.padre(this) 28 } Se pide: 1. Definir la función abueloMaterno que devuelva la persona que es el abuelo materno de la persona pasada como argumento. Utilizar flatMap y concordancia de patrones. 2. Definir la función abuelaMaterna que devuelva la persona que es el abuelo materno de la persona pasada como argumento. 3. Definir la función abueloPaterno que devuelva la persona que es el abuelo materno de la persona pasada como argumento. 4. Definir la función abuelaPaterna que devuelva la persona que es el abuelo materno de la persona pasada como argumento. Ejercicio 64. Definir la función abuelos que devuelva una dupla con los dos abuelos de la persona pasada como argumento. Utilizar la función flatMap. Ejercicio 65. Definir la función abuelos que devuelva una dupla con los dos abuelos de la persona pasada como argumento. Utilizar bucles for. Página 152
  • 169. Ejercicio 66. Del mismo modo que se ha utilizado en la definición de las funciones de los ejercicios anteriores la mónada Maybe, se podría emplear con las operaciones matemáticas. Definir la función divSec que reciba dos números de tipo Double, dividendo y divisor, como parámetros y devuelva el resultado de la división en caso de que el divisor sea distinto de cero. Ejercicio 67. Definir la función sqrtSec que reciba un número de tipo Double como parámetro y devuelva el resultado de realizar la raíz cuadrada al número pasado como argumento en caso de que el argumento sea positivo. Ejercicio 68. Definir la función sqrtdivSec que devuelva la raíz cuadra del resultado de divi- dir dos argumentos de tipo Double pasados como argumentos. Utilizar las funciones definidas anteriormente. Página 153
  • 171. Capítulo 5 Tests en Scala El desarrollo guiado por pruebas de software (Test Driven Development (TDD)), es una práctica fundamental en el desarrollo del software para construir un producto de calidad. Ade- más, los tests son muy importantes en otros aspectos relacionados con las metodologías de desarrollo ágiles1 . Se sabe que el software se comportará según hayamos codificado el mismo, así que realizar tests al software se convierte en una tarea fundamental para comprobar que el comportamiento del software se corresponde con la especificación del mismo. Por todo esto, todo desarrollador debe de tener presente la importancia de las pruebas unitarias. Para comprobar el funcionamiento del software se dispondrá de dos opciones fundamental- mente: 1. Afirmaciones (Asserts). 2. Herramientas específicas de tests. 5.1. Afirmaciones Asserts Para hacer las afirmaciones que debe cumplir el código, se hará uso de uno de los dos méto- dos (assert y ensuring) definidos en el objeto singleton Predef, cuyos miembros son importados automáticamente en todos los ficheros fuente. 5.1.1. Assert Se podrán realizar llamadas al método predefinido assert en cualquier parte del código. Con el método assert se podrán crear dos tipos de expresiones que se diferenciarán en la información que aportarán en caso de no cumplirse la afirmación evaluada: assert(condición). Esta expresión lanzará una excepción del tipo AssertionError si no se cumple la condición. assert(condición,explicación). En este caso si la condición no se cumple, lanzará una ex- cepción del tipo AssertionError que contendrá la explicación dada. La explicación puede ser un string, un objeto,...ya que su tipo es Any. A continuación se muestra cómo se pueden definir afirmaciones. Para ello se definirá un método que calcule el enésimo término de la serie de Fibonacci2 . 1 Procesos de integración continua 2 La serie de Fibonacci o sucesión de Fibonacci es la sucesión infinita de números naturales cuyos dos primeros términos son 0 y 1 y el resto de términos se calculan como la suma de los dos anteriores. Página 155
  • 172. 1 def fibonacci(x:Int):Int= x match{ 2 case 0 => 0 3 case 1 => 1 4 case _ => fibonacci(x-2)+fibonacci(x-1) 5 } Algoritmo 5.1: Fibonacci sin recursión de cola En un primer intento se afirmará que el término fibonaccix−2 siempre es menor que el término fibonaccix−1, es decir, fibonaccix−2 ≤ fibonaccix−1∀x : x ≥ 2, lo cual se podría comprobar con la siguiente sentencia assert3 : 1 def fibonacci(x:Int):Int= x match{ 2 case 0 => 0 3 case 1 => 1 4 case _ => {assert(fibonacci(x-2) < fibonacci(x-1)) 5 fibonacci(x-2) + fibonacci(x-1)} 6 } Algoritmo 5.2: Fibonacci sin recursión de cola. Assert que falla Si se prueba la definición... fibonacci: (x: Int)Int scala> fibonacci(1) res0: Int = 1 scala> fibonacci(2) res1: Int = 1 scala> fibonacci(3) java.lang.AssertionError: assertion failed at scala.Predef$.assert(Predef.scala:151) at .fibonacci(<console>:10) ... 33 elided Algoritmo 5.3: Excepcion AssertError Se puede comprobar que se lanza una excepción del tipo AssertionError cuyo mensaje as- sertion failed no aportará mayor información. Se muestra una segunda aproximación en la que se construirá la afirmación con la misma condición pero se añadirá una explicación: 1 def fibonacci(x:Int):Int= x match{ 2 case 0 => 0 3 case 1 => 1 4 case _ => {assert(fibonacci(x-2) < fibonacci(x-1), "El fibonacci de "(x-2)" no es menor que el fibonacci de "(x-1)) 5 fibonacci(x-2)+fibonacci(x-1)} 6 } Algoritmo 5.4: Fibonacci sin recursión de cola. Assert que falla + explicación Si se introduce en el REPL de Scala la nueva definición, se obtendrá: 3 Se puede observar que la definición de Fibonacci es poco eficiente ya que se podría desbordar la pila de llamadas recursivas si se quisiera calcular un término muy alto de la serie. Además es exponencial en el número de sumas realizadas. Página 156
  • 173. fibonacci: (x: Int)Int scala> fibonacci(3) java.lang.AssertionError: assertion failed: El fibonacci de 1 no es menor que el fibonacci de 2 at scala.Predef$.assert(Predef.scala:165) at .fibonacci(<console>:11) ... 33 elided Algoritmo 5.5: Excepcion AssertError con información Ahora se podrá comprobar cómo la información que acompaña a la excepción que se lanza sí que es de utilidad. La afirmación es correcta excepto para los dos primeros términos. Como se sabía, se partía de una premisa que no era cierta. Por tanto, se modificará la afirmación realizada previamente, de modo que ahora si esté definida correctamente4 : 1 def fibonacci(x:Int):Int= x match{ 2 case 0 => 0 3 case 1 => 1 4 case _ => {assert(fibonacci(x-2) <= fibonacci(x-1)," El fibonacci de "+(x-2)+" no es menor que el fibonacci de "+(x-1)); 5 fibonacci(x-2)+fibonacci(x-1)} 6 } Algoritmo 5.6: Fibonacci sin recursión de cola. Assert que no falla 5.1.2. Ensuring En ocasiones sólo se querrán realizar las comprobaciones al final del método, justo antes de que devuelva un valor5 . El método ensuring podrá ser utilizado con cualquier tipo, ya que se realiza una conversión implícita. El método ensuring tomará como argumento una condición booleana en forma de predicado. Si ensuring devuelve true, entonces el método devolverá el valor, en otro caso se lanzará una excepción el tipo AssertError. Volviendo al ejemplo que calcula los términos de la serie de Fibonacci, en un primer intento se hará una afirmación que, según hemos definido nuestro método, debería fallar: 1 def fibonacci(x:Int):Int={ x match{ 2 case 0 => 0 3 case 1 => 1 4 case _ => fibonacci(x-2)+ fibonacci(x-1) 5 } 6 }ensuring(x >= 1) Algoritmo 5.7: Fibonacci sin recursión de cola. Ensuring que falla Se puede apreciar que se ha afirmado que siempre se llamará al método con un valor mayor o igual que uno, lo cual debería de fallar al calcular fibonacci2 6 . De vuelta al REPL se observa que, con esta afirmación, se lanza una excepción del tipo AssertionError: 4 Obsérvese que ahora se usa ≤. 5 La realización de comprobaciones al final del método se le llama postcondiciones técnicamente. 6 Ya que se producirá una llamada recursiva a fibonacci0 que provocará el lanzamiento de la excepción Página 157
  • 174. scala> fibonacci(1) res8: Int = 1 scala> fibonacci(2) java.lang.AssertionError: assertion failed at scala.Predef$.assert(Predef.scala:151) Algoritmo 5.8: Ensuring.Excepcion AssertError A continuación se corregirá el código, poniendo una afirmación que sí se sabe que se cumplirá siempre7 : 1 def fibonacci(x:Int):Int={ 2 x match{ 3 case 0 => 0 4 case 1 => 1 5 case _ => fibonacci(x-2)+ fibonacci(x-1) 6 } 7 }ensuring(fibonacci(x-2) <= fibonacci(x-1)) Algoritmo 5.9: Fibonacci sin recursión de cola. Ensuring que no falla Ahora que ya se ha comprendido el uso de ensuring, se verá un ejemplo más realista en el que usar este método. Para ello se recordará la definición del método inversa de una lista que aparece en el algoritmo 3.10 (página 93): 1 def inversa[A](xs:Lista[A]):Lista[A]={ 2 @annotation.tailrec 3 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match { 4 case Nil => res 5 case Cons(elem,xss)=>go(xss,elem::res) 6 } 7 go(xs,Nil) 8 9 } Algoritmo 5.10: Inversa de una lista. Recursión de cola Una afirmación que se puede hacer es que la longitud de la lista original y la longitud de la inversa de la misma han de ser iguales: 1 def inversa[A](xs:Lista[A]):Lista[A]={ 2 @annotation.tailrec 3 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match { 4 case Nil => res 5 case Cons(elem,xss)=>go(xss,elem::res) 6 } 7 go(xs,Nil) 8 9 }ensuring( xs.length == _.length) Algoritmo 5.11: Ensuring en el cálculo de la inversa de una lista 7 Para asegurar que nuestra función no falla se debería de poner como primera linea de código require(x>=0) ya que la serie de Fibonacci sólo está definida para números naturales y se trata de una precondición, no de una postcondición. Página 158
  • 175. En la expresión (xs.length) == _.length, el guión bajo hace referencia al argumento pasado al predicado, en este caso, el resultado del tipo Lista[A] del método inversa. 5.2. Herramientas específicas para tests Existen muchas opciones a la hora de escoger las herramientas que se usarán para realizar tests al código en Scala. En primer lugar, se podrá optar por utilizar las herramientas de tests propias de Java como JUnit8 o TestNG9 , o también se podrá optar por utilizar nuevas herramien- tas de tests desarrolladas para Scala como ScalaTest, specs10 o ScalaCheck11 . El uso de estas herramientas para comprobar el comportamiento de nuestro software ayudará a mejorar el diseño del mismo, desacoplando la especificación del programa de los casos de prueba y haciendo que el código sea más coherente, legible y fácil de comprender que si se intentara incluir los casos de prueba en el mismo12 , sobre todo cuando el software es complejo y tiene una dimensión considerable. A continuación se verán las opciones que ofrece el framework ScalaTest y cómo se podrán implementar los casos de pruebas en alguna de las Suites que se ofrecen. 5.2.1. ScalaTest ScalaTest es un framework de tests para Scala. ScalaTest no pertenece a la librería estándar de Scala, por lo que antes de empezar a realizar pruebas al código habrá que instalarlo en nuestro sistema13 . El framework de ScalaTest ofrece diversos estilos para realizar pruebas al software, cada uno de ellos desarrollado para cubrir las diferentes necesidades de los programadores. La elección de cada tipo sólo determinará cómo se quieren expresar los tests, no el tipo de test que se puede realizar. La forma más fácil de implementar el primer conjunto de pruebas es crear un clase que extienda de org.scalatest.Suite. El rasgo Suite incluye un método execute, el cual es sobreescrito en cada uno de los estilos que el framework ScalaTest ofrece a los programadores. En la tabla 5.1 se pueden ver los distintos estilos disponibles en ScalaTest: 8 http://junit.org 9 http://testng.org 10 http://www.scalatest.org/ 11 http://www.scalatest.org/ 12 Utilizando afirmaciones, por ejemplo. 13 Habrá que descargarse el framework de la web www.artimia.com y descomprimirlo en la misma carpeta en la que se tenga instalado Scala en el sistema. Además, se recomienda descargar el plugin ScalaTest del IDE de Scala para Eclipse. Página 159
  • 176. Suites en ScalaTest FunSuite Facilita la codificación de los nombres descriptivos de las pruebas. Simplifica el desarrollo de tests. Genera artefactos de salida útiles para la comunicación. FlatSpec Estructura similar a XUnit en la que los nombres de los tests han de ser del tipo “X should Y”,“A must B” FunSpec Ideal para desarrolladores acostumbrados a Ruby’s RSpec. Excelente opción de uso general, bien estructurada, que ha- ce uso de describe y de it para escribir pruebas. WordSpec Ideal para portar tests en specsN a Scala Buena opción para los desarrolladores que quieren mante- ner un alto grado de disciplina a la hora de especificar sus tests. FreeSpec Ofrece absoluta libertad a la hora de decidir cómo escribir tests. Spec Permite escribir tests como métodos que serán representados me- diante funciones. Se reducirán los tiempos de compilación, algo que resultará muy adecuado para proyectos con grandes cantida- des de pruebas. PropSpec Ideal para desarrolladores que escriban tests especialmente orien- tados a verificar propiedades. FeatureSpec Diseñada con la intención de facilitar el proceso de aceptación de requisitos entre los programadores y la parte interesada. Tabla 5.1: Suites del framework ScalaTest En el primer ejemplo se creará una clase que extienda de org.scalatest.Suite: Página 160
  • 177. 1 2 import org.scalatest.Suite 3 class EjemploSuite extends Suite { 4 def testAssert() { 5 val v1 = true 6 val v2 = true 7 assert(v1 == v2) 8 } 9 def testAssertResult() { 10 val v1 = 15 11 val v2 = 22 12 assertResult(37) { 13 v1 + v2 14 } 15 } 16 def testIntercept() { 17 val v1 = 9 18 val v2 = 0 19 intercept[ArithmeticException] { 20 v1 / v2 21 } 22 } 23 def testExpect() { 24 val v1 = 12 25 expect(12)(valor1) 26 } 27 28 } Algoritmo 5.12: ScalaTest. Ejemplo que extiende de org.scalatest.Suite En este ejemplo se hace uso, además de assert, de: expect, definiendo el valor esperado como primer parámetro y la validación en el segun- do. intercept, indicando entre corchetes la excepción que esperamos capturar y como segun- do parámetro el código en el que esperamos que la produzca. La función currificada assertResult. Se definirá el valor que se espera y como segundo parámetro la verificación. Una vez visto el primer ejemplo, se estudiará otra de las soluciones que el framework Scala- Test ofrece: el rasgo FunSuite, en el cual se pueden definir los casos de prueba como funciones (en lugar de métodos), motivo por el cual FunSuite tiene el prefijo “Fun”14 . El método test defi- nido en FunSuite será el que se invoque para definir los casos de prueba, pasándole como primer argumento un string con el nombre del test y como segundo argumento un bloque en el que es- tará definido el código propio de la prueba. El segundo argumento correspondiente al código de la prueba será una función, la cual será evaluada por el método test siguiendo la estrategia de paso de parámetros por necesidad o evaluación perezosa (véase la Subsección 1.4.3: Sistema de Evaluación de Scala « página 19 »). 14 En referencia a función Página 161
  • 178. A continuación se muestra un ejemplo simple del uso de FunSuite, definiendo algunos casos de prueba para el tipo abstracto de datos Lista definido en el algoritmo 3.9(página 91): 1 import org.scalatest.FunSuite 2 import Lista._ 3 4 class prueba extends FunSuite { 5 val miLista:Lista[Int]= Cons(1,Nil) 6 test("Una lista vacia deberia de tener longitud 0") { 7 assert(Nil.length == 0) 8 } 9 test("La longitud de miLista debe ser 1"){ 10 assert( miLista length === 1) 11 } 12 13 test("Invocamos Head en una lista vacia") { 14 intercept[NoSuchElementException] { 15 Nil head 16 } 17 } 18 test("Invocamos el metodo !! con un elemento que no existe"){ 19 intercept[IllegalArgumentException]{ 20 miLista !! 2 21 } 22 } 23 test("Expect 1") { 24 expect(1) (miLista length) 25 } 26 27 } Algoritmo 5.13: ScalaTest. Ejemplo de la suite FunSuite En este ejemplo se puede apreciar el uso del triple igual (===) en el test “El tamaño de MiLista deber ser 1”. La diferencia entre el uso del operador de igualdad habitual y el uso del triple igual radica en la información que obtenemos en caso de que la afirmación falle. Cuando una afirmación realizada con assert falla, sólo se sabrá que se ha lanzado una excepción del tipo AssertionError y un mensaje indicando el número de línea en el que se ha producido el fallo, pero no se sabrá qué valores son los que han producido el fallo. Se podrían conocer los valores que producen el fallo, incluyendo una aplicación en el assert que nos proporcione dicha información, como se vio en el algoritmo 5.4, pero resulta más apropiado el uso del triple igual para obtener esta información. Igualmente, el triple igual no indicará cual era el valor esperado y cual ha sido el valor obtenido, simplemente indicará los valores que no cumplen la igualdad. Para conocer esta distinción se hará uso de expect. 5.3. Ejercicios Ejercicio 69. Utilizar la suite FunSuite de ScalaTest para comprobar que la suma de números enteros es conmutativa, es decir, ∀a, b/a, b ∈ Z : a + b = b + a. Ejercicio 70. Utilizar la suite FunSuite de ScalaTest para comprobar que la suma de números Página 162
  • 179. enteros es distributiva con respecto al producto, es decir, ∀a, b/a, b ∈ Z : a∗(b+c) = a∗b+a∗c. Ejercicio 71. Utilizar la suite FunSuite de ScalaTest para comprobar que la mónada Maybe cumple con las reglas de las mónadas. Antes de comenzar con la solución del ejercicio, se recordará la implementación de la clase monádica Maybe realizada en el ejercicio 62: 1 sealed trait Maybe[+A] { 2 3 def flatMap[B](f: A => Maybe[B]): Maybe[B] 4 def map[B](f:A=>B):Maybe[B] 5 } 6 7 case class Just[+A](a: A) extends Maybe[A] { 8 override def flatMap[B](f: A => Maybe[B]) = f(a) 9 override def map[B](f:A=>B):Maybe[B]=Just(f(a)) 10 } 11 12 case object MaybeNot extends Maybe[Nothing] { 13 override def flatMap[B](f: Nothing => Maybe[B]) = MaybeNot 14 override def map[B](f:Nothing=>B):Maybe[B]=MaybeNot 15 } Ejercicio 72. Utilizar la suite FunSuite de ScalaTest para comprobar que se obtiene el mis- mo resultado al calcular los abuelos de una persona utilizando las funciones definidas en los ejercicios 64 y 65. Ejercicio 73. Utilizar la suite FunSuite de ScalaTest para comprobar que se obtiene el mismo resultado al calcular los abuelos maternos de una persona utilizando las funciones definidas en el ejercicio 63. Página 163
  • 181. Capítulo 6 Concurrencia en Scala. Modelo de actores 6.1. Programación Concurrente. Problemática 6.1.1. Introducción La programación concurrente aúna el conjunto de metodologías, lenguajes, técnicas y he- rramientas de programación necesarias para la construcción de programas reactivos, es decir, programas que interaccionan continuamente con el entorno, recibiendo estímulos del mismo y produciendo salidas en respuesta a los mismos. Durante la fase de diseño de un software que deba interaccionar con un sistema reactivo se tendrán que indicar aquellos eventos, acciones, estímulos ...que tengan lugar de forma inde- pendiente, paralela, concurrente ... “Un programa concurrente no va a ser más que el resultado de la intercalación no- determinista de las instrucciones de los procesos secuenciales que lo componen” [11] 6.1.1.1. Sistema Reactivo Vs Sistema Transformacional Sistema Reactivo Sistema Transformacional Interacciona continuamente con el en- torno, recibiendo estímulos del mismo y produciendo salidas en respuesta a los mismos. Toma unos datos de entrada y devuelve una salida. El orden de los eventos en el sistema no es predecible, viene determinado externa- mente. El orden de entrada de los datos está preestablecido. La ejecución de los sistemas reactivos no tiene por qué terminar. Su ejecución debe finalizar. Tabla 6.1: Sistema Reactivo Vs Sistema Transformacional Página 165
  • 182. 6.1.2. Speed-Up en programación concurrente La programación concurrente es utilizada para mejorar los tiempos de ejecución en máqui- nas multiprocesadoras. La ganancia en velocidad (speed-up) de un programa secuencial con respecto a su versión paralela se define como: G = TiempoSecuencial TiempoParalelo donde, teóricamente, G de- bería ser igual o muy próximo al número de procesadores usados, aunque en la realidad existen diversos inconvenientes que harán que esto no sea así: Balanceo de carga. Distribución de las tareas y recolección de los resultados. La importancia de aprovechar al máximo el uso de los procesadores se ha hecho mayor desde 2005, momento en el cual se puede observar un punto de inflexión en la tendencia de fabricación de microprocesadores ya que se apuesta por el aumento del número de núcleos de los procesadores, en lugar de aumentar la velocidad de los procesadores. El número de núcleos de los procesadores se ha aumentado fundamentalmente de dos maneras: Multiplicando el número de núcleos que se encuentran integrados en un único chip, ac- cediendo todos ellos a la memoria compartida. Creando núcleos virtualizados que comparten un único núcleo físico de ejecución, el cual puede ejecutar muchas hebras lógicas de ejecución. Hay diferentes formas de aprovechar esta nueva tendencia: Multitarea. Teniendo en ejecución varios programas de forma paralela.1 Multihebra. Ejecutando varias partes de un mismo programa de forma paralela. La diferencia entre ambos radica en que mientras las tareas que realizan los programas en multitarea no comparten información, es decir, se ejecuta cada una de forma independiente, las tareas que lleva a cabo cada hebra de un programa multihebra se ejecutan de forma colabo- rativa, es decir, necesitan sincronizar sus acciones para obtener el resultado buscado, lo cual implicará que el desarrollo de estos programas se realice de distinta forma que los programas secuenciales. 6.1.3. Problemática 6.1.3.1. Propiedades de los programas concurrentes Cuando se desarrollan programas concurrentes habrá que asegurarse de que cumplen un conjunto de propiedades, las cuales se dividen en dos grupos (Owicky y Lampart, 82): 1. Seguridad. Aseguran que no ocurrirá nada indeseable durante la ejecución del programa. 2. Vivacidad. Aseguran que algo (bueno) ocurrirá durante la ejecución del programa. Las propiedades de seguridad son equivalentes a las propiedades de corrección parcial de los programas secuenciales. Vienen a decir que, si el programa acaba, lo hace con un resultado correcto.2 1 Algo que se hace desde las primeras versiones de Linux. 2 Suelen poder asegurarse a costa de perder parte de la concurrencia. Página 166
  • 183. Exclusión mutua: nunca debe de haber más de un proceso en una sección crítica.3 Sincronización: los procesos deben de estar sincronizados en su ejecución. En un cruce de caminos, un semáforo no podrá cambiar su color a verde hasta que el otro esté en rojo. Ausencia de bloqueo Deadlocks: bloqueo permanente de varios procesos, hilos, he- bras,...que se encuentran a la espera de un recurso en un sistema concurrente que no será liberado nunca. Las propiedades de vivacidad estarían relacionadas con la corrección total de los progra- mas secuenciales: el programa acaba y lo hace con un resultado correcto. Livelock: todos los procesos están realizando una tarea inútil esperando que suceda una acción que nunca sucederá. Postergación indefinida: el sistema en su conjunto sigue progresando pero hay un pro- ceso o varios que se encuentran bloqueados porque los demás le quitan el acceso a los recursos compartidos. Las propiedades de vivacidad son mucho más difíciles de asegurar, ya que el sistema podrá detectar si los procesos se encuentran bloqueados pero es más difícil determinar si lo que están haciendo es útil o no (livelock).[11] 6.1.3.2. Bloqueos y secciones críticas La forma tradicional de afrontar estos problemas ha sido mediante el uso de Mutex/Locks o semáforos. La diferencia entre ambos es que los semáforos permiten el acceso a múltiples hebras definidas previamente que pueden tener acceso a las secciones críticas, mientras los mutex sólo permitirán/denegarán el acceso a un único hilo. Problemas del uso de bloqueos El uso de bloqueos es el origen de uno de los mayores problemas en concurrencia: Dead- Locks. Son perjudiciales para la utilización de la CPU ya que si una hebra se bloquea la CPU quedará inactiva excepto si existen otras hebras en espera de ejecución. Además tiene un gran coste despertar y volver a ejecutar una hebra bloqueada. Como consecuencia, los programas se ejecutarán más lentamente. 6.1.3.3. Concurrencia en Java A pesar de que Java ofrece a los programadores suficiente soporte para la creación de apli- caciones concurrentes, este soporte puede convertirse en el peor enemigo cuando se diseñan aplicaciones complejas y de un tamaño considerable, principalmente debido a las dificultades que entraña el modelo de concurrencia basado en procesos sincronizados interactuando me- diante memoria compartida, así como la complejidad que supone garantizar las propiedades de seguridad y vivacidad. 3 Fragmento de código donde puede modificarse un recurso compartido y, por tanto, el resto de procesos deberán ver como una acción atómica Página 167
  • 184. Java introdujo la palabra reservada synchronized para hacer referencia a las secciones críti- cas de los programas concurrentes, indicando que se debe acceder a ellos en exclusión mutua. Para garantizar el acceso en exclusión mutua a estas zonas críticas Java emplea un mecanis- mo de bloqueos que garantizan que una sola hebra puede acceder a estas zonas críticas a la vez, ofreciendo de este modo una forma fácil para compartir datos entre muchas hebras. En la versión 5 de Java se introdujo la biblioteca java.util.concurrent con la que los progra- madores tienen a su disposición una librería con un nivel mayor de abstracción de concurrencia, la cual, usada de forma correcta, debería de ayudar a obtener programas concurrentes con mu- chos menos errores que si se utilizara la primitiva synchronized, aunque al estar basada en el modelo de datos compartidos y bloqueos no resuelve los problemas principales de este modelo. Algunos de los problemas a los que los programadores que desarrollen programas concu- rrentes en Java, especialmente aquellos que vayan creciendo tanto en tamaño como en comple- jidad, deberán enfrentarse son: Se deberá de razonar, siempre que se acceda o se modifique algún dato, si otras hebras puedan estar accediendo o modificando simultáneamente el mismo dato y cuáles son los bloqueos que pueden estar activos. Cada vez que se realice una llamada a un método se deberá de tener en cuenta los bloqueos que pueda tener y esperar que no se produzcan DeadLock. El programa puede crear nuevos bloqueos durante la ejecución del mismo, por lo que estos problemas no pueden ser resueltos durante la compilación. No se puede confiar en los resultados de las pruebas de la aplicación ya que se podrían realizar miles de pruebas obteniendo resultados satisfactorios y que falle la primera vez que se ejecute en la máquina del cliente. Cuando dos o más hebras pueden acceder simultáneamente a una sección crítica e intentan modificar algún valor en la misma se dirá que se dan condiciones de carrera ya que el planificador puede cambiar de hebra en cualquier momento, por lo que no se conocerá el orden en el que las hebras accederán a los datos y, por tanto, el resultado final dependerá del planificador. La clave del modelo basado en actores que incorpora Scala es el modelo de no comparti- ción, ofreciendo un espacio seguro (el método act de cada actor) en el que se podrá razonar de manera secuencial. Expresándolo de manera diferente, los actores permiten escribir programas multihilo como un conjunto independiente de programas monohilo. La simplificación anterior se cumple siempre y cuando el único mecanismo de comunicación entre actores sea el paso de mensajes. 6.2. Modelo de actores 6.2.1. Origen del Modelo de Actores El uso de un modelo basado en actores para dar solución a los problemas de los sistemas reactivos no es una novedad. El modelo de actores fue desarrollado y publicado por primera vez por Carl Hewitt, Bishop y Steiger en 1973[12] cuando buscaban crear un modelo con el que poder formular los programas de sus investigaciones en Inteligencia Artificial. Página 168
  • 185. En 1986, Ericsson comenzó el desarrollo del lenguaje de programación Erlang, un lenguaje de programación funcional puro que basaba su modelo de concurrencia en actores en lugar de hilos por primera vez. De hecho, la popularidad alcanzada por Erlang en determinados ámbi- tos empresariales ha 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. Philipp Harler advirtió el éxito que el modelo de actores había otorgado a Earlang y en 2006 añadió la librería estándar que da soporte a este modelo en Scala. Jonas Bonér, influenciado tanto por Earlang como por el modelo de actores de Scala, creó Akka en 2009. 6.2.2. Filosofía del Modelo de Actores El modelo de actores ofrece una solución diferente al problema de la concurrencia, por lo que habrá que aprender a estructurar los programas para usar actores. Los actores representan objetos y el modelo de actores describe las interacciones entre dichos objetos inspirándose en cómo los humanos se relacionan4 . Por este motivo, para comprender el modelo de actores será de gran ayuda pensar en los actores como personas, en lugar de verlos como objetos abstractos con unos determinados métodos que podemos invocar. Formalmente, como fue definido por Hewit, Bishop y Steiger un actor[12]: Es un objeto con una identidad. Tiene un comportamiento. Sólo puede interactuar mediante el paso asíncrono de mensajes. En lugar de procesos interactuando mediante memoria compartida, el modelo de actores ofrece una solución basada en buzones y paso de mensajes (eventos) asíncronos. Los mensajes son almacenados en estos buzones y posteriormente recuperados para su procesamiento por los actores. En lugar de compartir variables en memoria, el uso de estos buzones permite aislar cada uno de los procesos. Los actores son entidades independientes con identidad propia, un estado que puede variar durante la ejecución y 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. El comportamiento de los actores se definirá en una función a la que se le pasará cada uno de los mensajes recibidos y en la que se indicarán las acciones a llevar a cabo en respuesta a cada mensaje en un momento concreto de la ejecución. Los actores procesan los mensajes en tiempo de ejecución por lo que no hay una comprobación previa de los mismos, para poder conocer si un Actor podrá procesar un determinado mensaje, durante el tiempo de compilación. Como se ha dicho anteriormente, los actores procesan los mensajes de forma asíncrona, pu- diendo atender a los mismos en un orden distinto al de llegada al buzón. De hecho, un actor eliminará del buzón el primer mensaje que pueda procesar. Si no pudiera procesar ningún men- saje, el actor quedaría suspendido hasta que el estado del buzón de entrada cambiase. Un actor sólo puede procesar un mensaje a la vez, aunque sí puede recibir distintos mensajes simultánea- mente en su buzón.[23] Se verá como los actores, además de intercambiar mensajes, pueden crear nuevos actores o incluso cambiar su propio comportamiento durante su ejecución. Los cambios en el compor- tamiento de los actores podrán ser determinados por la variación de alguna de las variables de 4 En referencia al intercambio de mensajes entre humanos Página 169
  • 186. estado que sean leídas por la lógica de la función de comportamiento o bien porque se cambie durante la ejecución del programa la propia función que define el comportamiento. En esta aproximación a la concurrencia basada en el modelo de actores se verá como, si- guiendo unas buenas prácticas en el desarrollo de programas concurrentes, se podrá asegurar que se cumplen las propiedades de seguridad de los programas concurrentes, ya que no existirán secciones críticas a las que sean necesario acceder en exclusión mutua, ni secciones sincroni- zadas por lo que los problemas derivados de las mismas (deadlocks, pérdida de actualizaciones de datos,...) no deberían de darse en este modelo. Los actores están pensados para trabajar de manera concurrente, no de modo secuencial. 6.3. Actores en Scala. Librería scala.actors 6.3.1. Definición de actores Para comenzar, se implementará un actor en Scala, para ello no habrá más que extender scala.actors.Actor5 e implementar el método act, el cual definirá cómo procesará el actor los mensajes. El siguiente fragmento de código ilustra un actor sumamente simple que no realiza nada con su buzón: 1 import scala.actors._ 2 object PrimerActor extends Actor{ 3 def act(){ 4 for(i <- 1 to 11){ 5 println("Ejecutando mi primer actor!") 6 Thread.sleep(1000) 7 } 8 } 9 } Algoritmo 6.1: Mi Primer Actor Si se desea ejecutar un actor no tenemos más que invocar a su método start(): scala> PrimerActor.start() res0: scala.actors.Actor = PrimerActor$@4c2b880a scala> Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Ejecutando Primer Actor Se puede apreciar como la salida Ejecutando Primer Actor se intercala con el prompt de Scala ya que la hebra del actor se ejecuta independientemente de la hebra del shell. Los actores siempre se ejecutarán independientemente unos de otros. Otro mecanismo diferente que permitiría instanciar un actor sería hacer uso del método actor, muy útil y disponible en scala.actors.Actor: 5 Hasta la versión 2.12 de Scala en la que la biblioteca scala.actors desaparecerá. Página 170
  • 187. scala> import scala.actors.Actor._ scala> val otroActor = actor { for (i <- 1 to 11) println("Otro actor.") Thread.sleep(1000) } Hasta el momento se ha visto como se puede crear un actor y ejecutarlo de manera indepen- diente pero, ¿cómo se puede conseguir que dos actores trabajen de manera conjunta? Tal y como se ha descrito en la sección anterior, los actores se comunican mediante el paso de mensajes. Para enviar un mensaje se hará uso del operador !. A continuación se definirá un actor que hará uso de su buzón, simplemente esperará un mensaje e imprimirá aquello que ha recibido: 1 val echoActor = actor { 2 while (true) { 3 receive { 4 case msg => println ("Mensaje recibido " + msg) 5 } 6 } 7 } Algoritmo 6.2: Procesando primer mensaje Cuando un actor envía un mensaje no se bloquea y cuando lo recibe no es interrumpido. El mensaje enviado queda a la espera en el buzón del receptor hasta que este último ejecute la instrucción receive6 . El siguiente fragmento de código ilustra el comportamiento descrito: scala> echoActor ! "Primer Mensaje" Mensaje recibido Primer Mensaje scala> echoActor ! 11 Mensaje recibido 11 Algoritmo 6.3: Mi primer mensaje Un actor sólo podrá procesar los mensajes que coincidan con alguna de las definiciones case definidas en la función parcial receive. Receive devuelve el valor de la última expresión evaluada, correspondiente al bloque de código ejecutado por la función parcial. 6.3.2. Estado de los actores El estado de los actores en Scala puede ser implementado haciendo uso de variables reasig- nables o acarreando el mismo en la pila de llamadas. A continuación se implementará un sen- cillo contador haciendo uso de un actor, cuyo estado se pueda modificar mediante el paso de mensajes: 6 receiveWithin es una variante a receive en la que se puede indicar el tiempo de espera en milisegundos, una vez superado el tiempo de espera devolverá TIMEOUT Página 171
  • 188. 1 import scala.actors._ 2 3 class Contador extends Actor { 4 private var count = 0 5 def act()={ 6 while(true) { 7 receive { 8 case "incr" => { 9 count += 1 10 println ("El valor del contador es "+count)} 11 12 case "decr"=> { 13 count -= 1 14 println("El valor del contador es " + count)} 15 } 16 } 17 } 18 } Algoritmo 6.4: Actor con estado definido con variable mutable El comportamiento de este actor está definido en la función parcial receive donde se pue- de encontrar una única sentencia case. Si el mensaje coincide con el string “incr” entonces se incrementará la variable definida. En cambio, si el mensaje coincide con el string “decr” decrementará la variable una unidad. defined class Contador scala> val counter = new Contador () counter: Contador = Contador@40d0dd48 scala> counter.start() res0: scala.actors.Actor = Contador@40d0dd48 scala> counter ! "hola" scala> counter ! "adios" scala> counter ! "incr" El valor del contador es 1 scala> counter ! "incr" El valor del contador es 2 scala> counter ! "decr" El valor del contador es 1 Algoritmo 6.5: Ejecutando Contador con estado mutable Se observa que el estado se ha definido utilizando una variable privada counter que apunta a un estado no permanente, algo que no se ajusta a la programación funcional desde un punto de vista estricto. Si se quisiera definir el contador con un estado inmutable, se podría hacer con una solución similar a la aportada en el siguiente código: Página 172
  • 189. 1 import scala.actors._ 2 3 class Contador extends Actor { 4 def act()= run(0) 5 private def run(cnt:Int):Unit = { 6 receive { 7 case "incr" => { 8 val newcount = cnt + 1 9 println ("El valor del contador es "+newcount) 10 run(newcount)} 11 12 case "decr"=> { 13 val newcount = cnt - 1 14 println("El valor del contador es " + newcount) 15 run(newcount)} 16 } 17 } 18 } Algoritmo 6.6: Actor con estado inmutable Se puede apreciar que se ha utilizado una solución similar a la empleada para evitar la reasignación de variables en bucles y optimizar el uso de la pila, la recursión de cola (ver la Subsección 3.4.2: Recursión de cola « página 62 »). A continuación se comprobará que el funcionamiento del contador se ajusta al comportamiento definido: defined class Contador scala> val counter = new Contador () counter: Contador = Contador@40d0dd48 scala> counter.start() res0: scala.actors.Actor = Contador@40d0dd48 scala> counter ! "hola" scala> counter ! "adios" scala> counter ! "incr" El valor del contador es 1 scala> counter ! "incr" El valor del contador es 2 scala> counter ! "decr" El valor del contador es 1 Algoritmo 6.7: Ejecutando Contador con estado inmutable En esta definición de Contador ya no hay estados mutables y, por tanto, el estado se man- tiene en la pila de mensajes y es pasado al método privado run cada vez que se procesa un mensaje. 6.3.3. Mejora del rendimiento con react Como se ha dicho anteriormente, cada actor tiene su propia hebra por lo que todos los métodos act de los actores tendrán su turno. Sería ideal poder implementar todos los actores con su propio método act y ejecutar cada uno en su propia hebra pero las máquinas virtuales Página 173
  • 190. típicas de Java, capaces de almacenar millones de objetos, sólo pueden manejar unos pocos cientos de hebras7 . Para intentar dar una solución a esta problemática, Scala incorpora react, una alternativa al método receive8 . En la tabla 6.2 aparecen reflejadas las principales diferencias entre receive y react. Receive Vs React Toma una función parcial Toma una función parcial Devuelve el valor de la última expresión evaluada No devuelve ningún valor Preserva la pila de llamadas de la hebra actual No preserva pila de llamadas La biblioteca no puede reutilizar la hebra La biblioteca reuti- lizará la hebra para otro actor Tabla 6.2: Receive Vs React La biblioteca scala.actors.Actor incorpora la función loop que ayudará a crear los actores basados en eventos. La función Actor.loop ejecutará un bloque de código de forma ininterrum- pida incluso si en el mismo código se realiza una llamada a react(). En el algoritmo 6.8 se puede ver un ejemplo de la función loop incluida en la biblioteca scala.actors.Actor. 1 object LoopActor extends Actor{ 2 def act(){ 3 loop { 4 react{ 5 case str : String => println(str) 6 case msg => println("Mensaje sin caso especial") 7 } 8 } 9 } 10 } Algoritmo 6.8: Método act con loop y react 7 Los cambios de contexto de una hebra a otro suelen tener asociado un coste de cientos o miles ciclos de procesador 8 En la práctica, los programas necesitarán al menos unos pocos métodos receive pero intentaremos utilizar react cuando nos sea posible para preservar las hebras. Página 174
  • 191. 6.4. Actores en Scala con Akka 6.4.1. Introducción Akka es un framework que simplifica la construcción de aplicaciones concurrentes y dis- tribuidas en la JVM. Soporta múltiples modelos de programación concurrente, aunque hace especial énfasis en la concurrencia basada en el modelo de actores. Akka está escrita en Scala y, desde la versión 2.10, ha sido incorporada a la biblioteca estándar de Scala. Akka ofrece procesamiento escalable en tiempo real de transacciones.Algunas de las carac- terísticas que han llevado a incorporar el modelo de actores de Akka en la biblioteca estándar9 son[2]: Akka está desarrollada en Scala. La concurrencia en Akka está basada en el modelo de actores Tolerancia a fallos. Excelente para diseñar sistemas con una gran tolerancia a fallos, sin paradas y con capacidad para auto-repararse. Transparencia local. Akka ha sido diseñada para trabajar en entornos distribuidos. Las interacciones entre actores serán a través del paso de mensajes asíncronos. Soporte para clusters. Persistencia. Akka ofrece la posibilidad de que los actores puedan volver a un estado ante- rior incluso después de ser reiniciados o iniciados nuevamente tras una parada volviendo a reproducir los mensajes que el actor hubiera recibido. 6.4.1.1. Diferencias entre Akka y la librería Actors de Scala. Las principales diferencias entre Akka y la biblioteca scala.actors son: En Akka existe un sólo tipo de Actor. A diferencia con la biblioteca de actores de Scala en la que se encuentran disponibles diferentes tipos de actores (Reactor, ReplyActor, DaemonActor ...), en Akka todos los actores heredarán la funcionalidad de la clase Actor. Por ejemplo, con la biblioteca de Scala se podría tener la siguiente definición: 1 class MyServ extends ReplyReactor Algoritmo 6.9: Diferencias entre Akka y la librería de Scala. Instanciación en Scala En Akka se definirá de la siguiente forma: 1 class MyServ extends Actor Algoritmo 6.10: Diferencias entre Akka y la librería de Scala. Instanciación en Scala Instanciación de actores. En Akka solo se podrá acceder a los actores haciendo uso de la interfaz ActorRef. Se podrán obtener instancias de ActorRef invocando el método actor del objeto ActorDSL o haciendo uso del método actorOf en una instancia de ActorRef- Factory. Por ejemplo, con la biblioteca scala.actors se podría haber definido: 9 Sustituyendo la implementación original del modelo de actores que será finalmente eliminada en la versión 2.12 de Scala Página 175
  • 192. 1 val myActor = new MyActor(arg1, arg2) 2 myActor.start() Algoritmo 6.11: Diferencias entre Akka y la librería de Scala. Instanciación en Scala I 1 object MyActor extends Actor { 2 // MyActor definition 3 } 4 MyActor.start() Algoritmo 6.12: Diferencias entre Akka y la librería de Scala. Instanciación en Scala II Ahora, en Akka, se definirán de la siguiente manera10 : 1 ActorDSL.actor(new MyActor(arg1, arg2) Algoritmo 6.13: Diferencias entre Akka y la librería de Scala. Instanciación en Akka I 1 class MyActor extends Actor { 2 // Definicion MyActor 3 } 4 object MyActor { 5 val ref = ActorDSL.actor(new MyActor) 6 } Algoritmo 6.14: Diferencias entre Akka y la librería de Scala. Instanciación en Akka II Se deberá de tener en cuenta que los actores, en Akka, siempre se inician cuando se ins- tancian, por lo que si se desea que no se inicien en el momento en el que son instanciados, habrá que especificarlo. Eliminación en Akka del método act. En Scala el comportamiento de los actores se define implementando el método act. En Akka hay un único manejador de mensajes, una función parcial devuelta por el método recieve que se aplicará a cada uno de los mensajes existentes en el buzón de un actor. Si se quisiera incluir algún código de iniciación, habría que incluirlo en el método preStart. Ejemplo: 1 def act() { 2 // codigo de iniciacion 3 loop { 4 react { //cuerpo } 5 } 6 } Algoritmo 6.15: Eliminación de act. Ejemplo en Scala 10 Todas las referencias al objeto MyActor ahora deberán de hacerse a MyActor.ref Página 176
  • 193. 1 override def preStart() { 2 // codigo de iniciacion 3 } 4 def receive = { 5 // body 6 } Algoritmo 6.16: Eliminación de act. Ejemplo en Akka Cambio de la signatura de métodos. Habrá que tener en cuenta que, aunque conserven la misma funcionalidad, la signatura de algunos de los métodos de Actor y ActorRef cam- bian en Akka. Como, por ejemplo, el método exit() de Scala, ahora en Akka se utilizará context.stop(self) Éstas son algunas de las diferencias entre el modelo de actores de Scala y Akka. [13] y [2] 6.4.2. Definición y estado de los actores Una vez se han visto las principales características de Akka, así como las diferencias más destacables entre el modelo de actores de Scala y el de Akka, es el momento de tener una primera toma de contacto con el rasgo Actor de Akka antes de definir el primer actor. El rasgo Actor define un método abstracto receive11 que devuelve algo del tipo Receive: 1 Type Receive = PartialFunction[Any,Unit] 2 3 trait Actor { 4 5 def receive:Receive 6 7 ... 8 9 } Algoritmo 6.17: Akka.Trait Actor A continuación, se implementará en Akka un actor con un comportamiento igual al visto en la Subsección 6.3.2: Estado de los actores « página 171 » y sabiendo que el estado de un actor en Akka, al igual que ocurría en el modelo de actores de Scala, se puede implementar tanto con variables privadas, objetos mutables, etc., así como llevando el mismo en la pila de llamadas. En primer lugar se implementará en Akka un actor similar al implementado en el algoritmo 6.4 (página 172) en el que el valor del contador se guarda en una variable privada. 11 receive es una función parcial de Any a Unit y describe la respuesta del actor a un mensaje. Página 177
  • 194. 1 import akka.actor.Actor 2 3 class Contador extends Actor { 4 private var count = 0 5 def receive = { 6 case "incr" => { 7 count += 1 8 println ("El valor del contador es "+count)} 9 case "decr" => { 10 count -= 1 11 println("El valor del contador es " + count)} 12 } 13 } Algoritmo 6.18: Akka.Actor con estado definido con variable mutable Se puede observar que las diferencias entre el algoritmo 6.18 y el algoritmo 6.4 se ajustan a las descritas anteriormente en la Subsubsección 6.4.1.1: Diferencias entre Akka y la librería Actors de Scala « página 175 ». A continuación se implementará en Akka un actor similar al implementado en el algoritmo 6.6 (página 173), es decir, se implementará un actor capaz de mantener su estado sin necesidad de utilizar variables reasignables que puedan dar lugar a comportamientos no deseados. En una primera aproximación se podría escribir: 1 import akka.actor.Actor 2 3 class Contador extends Actor { 4 def receive = run(0) 5 private def run(cnt:Int):Receive = { 6 case "incr" => { 7 val newcount = cnt + 1 8 println ("El valor del contador es "+newcount) 9 run(newcount)} 10 11 case "decr"=> { 12 val newcount = cnt - 1 13 println("El valor del contador es " + newcount) 14 run(newcount)} 15 } 16 } Algoritmo 6.19: Akka.Actor con estado inmutable Esta solución no aprovecha el potencial de Akka. Como se vio en la Subsección 6.2.2: Filosofía del Modelo de Actores « página 169 », los actores pueden cambiar la función que define su comportamiento durante la ejecución del programa, algo que se puede aprovechar para modificar el estado del actor sin necesidad de utilizar variables. El tipo Actor sólo posee el método receive, el cual, como ya se ha visto, define el compor- tamiento de un actor. El encargado de llevar a cabo la ejecución del comportamiento descrito es el ActorContext asociado a cada actor. Para comprender todo esto mejor, se muestra el rasgo ActorContext y cómo se refleja en el rasgo Actor: Página 178
  • 195. 1 trait ActorContext { 2 def become(behavior:Receive, discardOld:Boolean = true):Unit 3 def unbecome():Unit 4 } 5 6 trait Actor { 7 implicit val context: ActorContext 8 def receive:Receive 9 10 ... 11 12 } Algoritmo 6.20: Akka.Trait Actor y Trait ActorContext Cada actor tiene asociada una pila de comportamientos. En la cima de esta pila se encuentra siempre el comportamiento activo en ese momento. Los métodos become y unbecome están relacionados con las operaciones tradicionales sobre pilas. Habitualmente, utilizaremos el mé- todo become para cambiar la función que se encuentra en la cima de la pila y que define el comportamiento actual de un actor. Para acceder al contexto dentro de un actor sólo habrá que escribir context. Ahora se modificará el algoritmo para que haga uso del método become para actualizar el estado del actor. 1 import akka.actor.Actor 2 3 class Contador extends Actor { 4 def receive = run(0) 5 private def run(cnt:Int):Receive = { 6 case "incr" => { 7 val newcount = cnt + 1 8 println ("El valor del contador es "+newcount) 9 context.become(run(newcount))} 10 11 case "decr"=> { 12 val newcount = cnt - 1 13 println("El valor del contador es " + newcount) 14 context.become(run(newcount))} 15 } 16 } Algoritmo 6.21: Akka.Actor con estado inmutable (become) Puede parecer que otra vez se ha utilizado recursión de cola para que nuestro actor sea un objeto inmutable ya que se realiza una llamada a la función run dentro de si misma, pero esta llamada es asíncrona ya que context.become evaluará el nuevo comportamiento cuando se procese el siguiente mensaje. Otra las acciones que los actores pueden hacer es intercambiar mensajes. Hasta el momento se ha visto cómo se pueden mandar mensajes de un actor a un segundo actor, pero no cómo este segundo actor puede responder al actor que mandó el mensaje. Si se observa el comportamiento de la última versión de nuestro actor Contador, rápidamen- te se puede notar la ausencia de un método que permita saber cuál es el valor del contador en Página 179
  • 196. un momento dado y, al mismo tiempo, permita prescindir de las sentencias println en nuestro actor Contador que como se sabe, presenta efectos colaterales no deseables en las funciones. Cada uno de los actores posee una dirección única que es determinada por el tipo ActorRef. A continuación se muestra la clase abstracta ActorRef y el rasgo Actor para tener una idea de cómo se puede utilizar: 1 2 trait Actor { 3 implicit val self: ActorRef 4 implicit val context: ActorContext 5 def receive: Receive 6 def sender: ActorRef 7 8 ... 9 10 } 11 abstract class ActorRef { 12 def !(msg:Any)(implicit sender: ActorRef = Actor.noSender):Unit 13 ... 14 } Algoritmo 6.22: Akka.Trait Actor y Clase Abstracta ActorRef Cada actor conoce su propia dirección del tipo ActorRef, disponible de forma implícita haciendo uso de self. Cuando se envía un mensaje a otro actor, la dirección del remitente se envía de forma implícita haciendo referencia a la variable inmutable self. El actor que recibe el mensaje dispondrá del valor de la dirección del remitente invocando sender, que le devolverá una dirección del tipo ActorRef. Ahora ya se puede incorporar un nuevo comportamiento al actor Contador que devuelva el valor del mismo al actor que haga la petición: 1 import akka.actor.Actor 2 3 class Contador extends Actor { 4 5 private def run(cnt:Int):Receive = { 6 7 case "incr" => context.become(run(cnt + 1)) 8 case "decr"=> context.become(run(cnt -1)) 9 case "get"=> sender ! cnt 10 11 } 12 def receive = run(0) 13 } Algoritmo 6.23: Akka.Mensjaes bidireccionales Ahora si se recubre un mensaje con el string “get” se enviará al remitente el valor del contador. Según se describe en la Subsección 6.2.2: Filosofía del Modelo de Actores « página 169 », la última de las acciones fundamentales que pueden realizar los actores es la de crear nue- vos actores, así como parar a los mismos. ActorContext presenta métodos para realizar dichas Página 180
  • 197. operaciones. En el siguiente ejemplo se ven dichos métodos: 1 trait ActorContext { 2 def become(behavior:Receive, discardOld:Boolean = true):Unit 3 def unbecome():Unit 4 def actorOf(p:Props, name: String): ActorRef 5 def stop(a: ActorRef): Unit 6 } Algoritmo 6.24: Akka.Trait ActorContext II En primer lugar, para crear actores se utilizará el método actorOf, cuyo primer parámetro de tipo Props servirá para describir cómo crear el actor y el segundo parámetro servirá para asignarle un nombre. Los actores son siempre creados por otros actores, por lo que formarán un sistema jerárquico. Un actor puede parar otros actores haciendo uso del método stop, el cual frecuentemente irá acompañado de self12 . Mostraremos un ejemplo definiendo un nuevo actor que haga uso del actor Contador y que reciba la cuenta del contador después de varios mensajes: 1 import akka.actor.Actor 2 import akka.actor.Props 3 4 class MainContador extends Actor { 5 val counter = context.actorOf(Props[Contador],"counter") 6 7 counter ! "incr" 8 counter ! "incr" 9 counter ! "incr" 10 counter ! "decr" 11 counter ! "decr" 12 counter ! "get" 13 14 def receive = { 15 case msg:Int => 16 println("El valor del contador es:"+msg) 17 context.stop(self) 18 } 19 } Algoritmo 6.25: Akka.Crear Actores Ya se han visto las principales acciones de los actores. La biblioteca Akka es mucho más potente, ofreciendo al programador diversas herramientas que facilitarán el diseño de programas concurrentes complejos.[19] 6.5. Buenas prácticas Llegados a este punto, ya se conocen los fundamentos básicos para escribir actores. El punto fuerte de los métodos vistos hasta este momento es que ofrecen un modelo de programación concurrente basado en actores por lo que, en la medida que se puedan escribir siguiendo este 12 Lo que significará que el actor desea parar su ejecución Página 181
  • 198. estilo, el código será más sencillo de depurar y tendrá menos interbloqueos y condiciones de carrera. Los siguientes apartados describen, de manera breve, algunas directrices que permitirán adoptar un estilo de programación basado en actores. 6.5.1. Ausencia de bloqueos Un actor no debería bloquearse mientras se encuentra procesando un mensaje. El proble- ma radica en que mientras un actor se bloquea, otro actor podría realizar una petición sobre el primero. Si el actor se bloquea en la primera petición no se dará cuenta de una segunda solici- tud. En el peor de los casos, se podría producir un interbloqueo en el que varios actores están esperando a otros actores que a su vez están bloqueados. En lugar de bloquearse, el actor debería esperar la llegada de un mensaje indicando que la acción está lista para ser ejecutada. Esta nueva disposición, por norma general, implicará la participación de otros actores. 6.5.2. Comunicación exclusiva mediante mensajes La clave de los actores es el modelo de no compartición, ofreciendo un espacio seguro (el método act de cada actor) en el que se podría razonar de manera secuencial. Expresándolo de manera diferente, los actores permiten escribir programas multihilo como un conjunto indepen- diente de programas monohilo. La simplificación anterior se cumple siempre y cuando el único mecanismo de comunicación entre actores sea el paso de mensajes. 6.5.3. Mensajes inmutables Puesto que el modelo de actores provee un entorno monohilo dentro de cada método act, no habrá que preocuparse de si los objetos que se utilizan dentro de la implementación de dicho método son thread-safe. Este es el motivo por el que el modelo de actores es llamado shared- nothing, los datos están confinados en un único hilo en lugar de ser compartidos por varios. La excepción a esta regla reside en la información de los mensajes intercambiados entre ac- tores, dado que es compartida por varios de ellos. Por tanto, habrá que prestar especial atención al hecho de que los mensajes intercambiados entre actores sean thread-safe. 6.5.4. Mensajes autocontenidos Cuando se devuelve un valor al finalizar la ejecución de un método, el fragmento de código que realiza la llamada se encuentra en una posición idónea para recordar lo que se estaba ha- ciendo anteriormente a la ejecución del método, recoger el resultado y actuar en consecuencia. Sin embargo, en el modelo de actores las cosas se vuelven un poco más complicadas. Cuan- do un actor realiza una petición a otro actor, el primero de ellos no es consciente del tiempo que tardará la respuesta, instantes en los que dicho actor no debería bloquearse, sino que debería continuar ejecutando otro trabajo hasta que la respuesta a su petición le sea enviada. ¿Puede el actor recordar qué estaba haciendo en el momento en que se envió la petición inicial? Se podrían adoptar dos soluciones para intentar resolver el problema planteado en el párrafo anterior: Un mecanismo para simplificar la lógica de los actores sería incluir información redun- dante en los mensajes. Si la petición es un objeto inmutable, el coste de incluir una refe- rencia a la solicitud en el valor de retorno no sería costoso. Página 182
  • 199. Otro mecanismo adicional que permitiría incrementar la redundancia en los mensajes sería la utilización de una clase diferente para cada una de las clases de mensajes de los que se dispongan. 6.6. Ejercicios Ejercicio 74. Implementar un sistema concurrente en el que dos actores jueguen al pin-pong de forma que el primero de ellos inicie el juego con la palabra “ping” y el adversario responda con la palabra “pong”. Después de tres intercambios de mensajes el juego finalizará. Página 183
  • 201. Capítulo 7 Conclusiones Scala presenta las características del paradigma de la programación funcional y el paradigma de la POO, ofreciendo un amplio conjunto de mecanismos para la resolución de problemas, lo que permite a los desarrolladores seleccionar la técnica que mejor se adapte a las características del problema que están tratando de resolver. En primer lugar, se ha presentado Scala como lenguaje de programación, introduciendo los elementos básicos presentes en cualquier lenguaje de programación como son los tipos básicos, la definición de variables (tanto mutables como inmutables) o los diferentes tipos de operadores existentes para esos tipos básicos. Se ha explicado cómo crear nuevos operadores y cómo definir características propias de los operadores como son la prioridad y la asociatividad, las cuales determinarán el resultado final de evaluación de una expresión. Se ha utilizado Scala para presentar otros conceptos básicos de la programación como son la visibilidad y el ámbito de uso de las variables o las sentencias de control selectivas e iterativas. Se ha visto las diferentes técnicas de evaluación y cómo se pueden definir variables que respondan a cada una de esas técnicas de evaluación. Se han tratado las ventajas e inconve- nientes de cada una de las estrategias de evaluación y se ha presentado la evaluación estricta, o por valor, como la estrategia de evaluación utilizada por defecto en el paso de parámetros a métodos en Scala. También se ha indicado cómo se puede cambiar la estrategia de evaluación de los parámetros de las funciones y utilizar evaluación no estricta o por nombre, separando de esta forma los conceptos de definición y evaluación de una función, es decir desacoplando el “cómo y cuándo”. Igualmente, se ha visto cómo se puede utilizar la estrategia de evaluación perezosa en Scala, estrategia usada por defecto en otros lenguajes de programación funcional como Haskell. Por tanto, se puede apreciar que Scala es un lenguaje que puede ser utilizado para estudiar los conceptos fundamentales de los lenguajes de programación. Cuando se aprende un lenguaje de POO, las clases son uno de los primeros conceptos que son estudiados y, por tanto, algo que se tenía que definir ya que Scala es un lenguaje orientado a objetos puro, en el que todo son objetos: las funciones, los valores... Las clases son el núcleo de las aplicaciones en Scala, por lo que se ha explicado la jerar- quía de clases en Scala y se han cubierto los conceptos fundamentales presentes en un lenguaje orientado a objetos. Se ha visto el polimorfismo en el paradigma de la POO en referencia al subtipado y cómo la herencia simple y el uso de los rasgos, que permiten un mecanismo si- milar a la herencia múltiple en nuestras clases dentro de Scala, aumentan, de este modo, la reusabilidad del código. Se aborda otro concepto relacionado con el polimorfismo en la POO, la generalización del uso de las clases en referencia a las clases genéricas o parametrizadas. Se muestra como un parámetro de tipo de una clase (clase parametrizada) otorga otra ca- Página 185
  • 202. racterística a la clase, ya que, además de definir la clase un tipo propio, tiene la posibilidad de construir un nuevo tipo cada vez que sea instanciada con un tipo distinto1 . Por ejemplo, List[String] y List[Int] presentan el mismo tipo List aunque hayan sido instanciadas con tipos diferentes. Los tipos pueden hacer más restrictivas las clases y funciones, indicando cotas, o hacerlas menos restrictivas cuando se especifica la varianza de un tipo. Además, se pone de manifiesto cómo se pueden aprovechar las características de un tipo especial de clases presentes en Scala, las case class (como por ejemplo en la concordancia de patrones) o las ventajas que ofrecen este tipo de clases como la creación de un método de fábrica con el nombre de la clase automáticamente. En Scala, cada instancia de clase y cada literal corresponden a un tipo. Pero en Scala, las clases son algo más que meros contenedores de valores y métodos. Por ejemplo, los rasgos otorgan una característica especial a las clases ya que, si se tiene definida una clase y diferentes rasgos, ésta puede ser instanciada con uno o varios de los rasgos definidos, lo que hará, por ejemplo, que un mismo tipo pueda presentar diferentes comportamientos. Tras este análisis de Scala como lenguaje orientado a objetos puro se puede observar que Scala es un lenguaje en el que se ven reflejados los conceptos relacionados con la programación orientada a objetos. Seguidamente se ha definido el concepto de programación funcional, se ha explicado exac- tamente en qué consiste la programación funcional, se han argumentado motivos por los que es aconsejable el uso de la programación funcional y cómo hay que cambiar la forma de razo- nar cuando se aborda un problema desde el punto de vista del paradigma de la programación funcional y la evaluación de expresiones. Se ha utilizado el lenguaje para ver conceptos fundamentales asociados al paradigma de la programación funcional como la definición de funciones, definición de operadores funcionales, la currificación de funciones, parcialización...o cómo se pueden expresar algoritmos iterativos haciendo uso de la recursión. Profundizando un poco más, se muestra cómo trata Scala las funciones y la importancia del método de fábrica apply. También se ha estudiado el polimorfismo dentro del paradigma de la programación funcio- nal y cómo la aplicación de la genericidad en las funciones multiplica la reutilización de código. Se ha introducido el concepto de funciones de alto nivel y se muestra cómo Scala trata a las fun- ciones como tipos de primera clase. También se ha visto cómo escribir funciones polimórficas, así cómo las funciones polimórficas pueden definir restricciones sobre que tipos pueden usar las mismas. Otro aspecto importante dentro de la programación es la definición de estructuras algebrai- cas de datos y cómo pueden ser implementadas en un lenguaje funcional como Scala. También se ha puesto de manifiesto una de las características de Scala como es la potencia de usar la concordancia de patrones, sobre todo en la definición de una estructura de datos lineal como la lista enlazada o de una estructura de datos no lineal, como los árboles. Se ha explicado la técnica de la compartición estructural, utilizada para optimizar los recur- sos empleados por las estructuras de datos inmutables. Posteriormente se han introducido las colecciones definidas en Scala, una de las razones más importantes para utilizar este lenguaje ya que ofrecen soluciones muy elegantes para una gran cantidad de problemas. Se ha visto cómo se pueden manejar las colecciones: crear, filtrar, aplicar o realizar otro tipo de operaciones sobre los elementos de una colección. Se ha prestado especial atención a las colecciones inmutables de Scala, colecciones que presentan característi- cas propias de la programación funcional como la imposibilidad de modificar el tamaño o los 1 A estos tipos se los conocen con el nombre de constructores de tipos. Página 186
  • 203. elementos de las mismas. Las colecciones inmutables se importan, por defecto, automáticamen- te dentro de los espacios de nombres en Scala2 . Es muy común que los lenguajes funcionales nos ofrezcan la posibilidad de iterar o aplicar los elementos de una colección pero el hecho de un sistema de tipos estáticos de la colección de origen y de la colección obtenida como resultado ya es algo menos usual. Los lenguajes funcionales que nos ofrecen la posibilidad de definir tipos seguros en las funciones de orden superior que manipulan las colecciones permitirán desarrollar un código muy expresivo y que minimice los errores de conversión de tipos en tiempo de ejecución, lo que se traducirá en una gran productividad para los programadores. También se han explicado las virtudes y desventajas que presentan cada uno de las coleccio- nes inmutables estudiadas y la importancia de escoger una u otra en función de las operaciones que se vayan a realizar sobre las mismas. Llegados a este punto se puede entender el hecho de que Scala se presente como un lenguaje de programación adecuado para explicar los conceptos básicos de la programación funcional. A continuación se han estudiado los tipos dentro de la programación funcional avanzada aunque anteriormente se explica como se pueden crear tipos propios, definiendo clases, ras- gos...Pero en Scala los tipos son algo más que una clase. Se hace referencia a los constructores de tipos3 y a los tipos compuestos4 y se introducen los tipos estructurales como una forma de definir tipos abstractos (tipos en los que se han especificado ciertas características), los tipos de orden superior como tipos que utilizan otros tipos para construir un tipo nuevo y los tipos existenciales que nos permitirán indicar la existencia de un tipo sin especificar de qué tipo se trata. Se ha presentado otra característica del lenguaje como es el método de búsqueda de Scala y la importancia de los implícitos. Se ha mostrado que tanto los parámetros implícitos de las fun- ciones, como las clases implícitas comparten el mismo mecanismo de búsqueda, aunque tengan diferentes aplicaciones. Las clases se utilizan para hacer conversiones entre tipos, mientras que en las funciones se utiliza para evitar la declaración de todos los argumentos de las mismas. Se expone cómo Scala busca la declaración de los parámetros implícitos en el propio espacio de nombres y cómo cuando no es capaz de resolver un método, busca una posible conversión existente dentro de su espacio de nombres. También se ha hecho hincapié en la importancia de hacer un uso responsable de los implí- citos. Dentro de la programación funcional avanzada, se ha explicado un patrón muy importante como es el patrón funtor en Scala5 y como simplifica la aplicación de una función a los elemen- tos de un contenedor. Dentro de la programación funcional, se ha visto que la aplicación del patrón funtor devolverá un contenedor igual al contenedor de los elementos previos a la aplica- ción de la función. Otro de los patrones importantes en la programación funcional es el patrón mónada, el cual presenta una serie de reglas que han de cumplir las clases monádicas para poder ser utilizadas como mónadas. Se ha visto como para definir clases monádicas en Scala sólo se tendrán que definir los métodos unit y flatMap6 . Se ha comprobado como el uso de ambos patrones, funtores y mónadas, nos permiten definir combinadores que serán aplicables a una gran cantidad de tipos de datos, tipos que en principio 2 Scala también presenta un conjunto de colecciones mutables pero estas no han sido estudiadas por carecer de interés dentro de la programación funcional. 3 Clases parametrizadas 4 Resultado de la combinación de otros tipos ya conocidos 5 Como implementación de la operación map de Scala, también llamada fmap dentro de la literatura relativa a la programación funcional 6 También conocidos como identity y bind dentro de la literatura. Página 187
  • 204. podría parecer que no tienen nada en común. Las colecciones monádicas en Scala nos ofrecerán una forma segura de encadenar operacio- nes y de manejar situaciones sensibles y complejas como las condiciones de error o el manejo de excepciones. Las utilidades de las mónadas se ha ejemplifican con uno de los usos de las mismas, como envoltorio (con la mónada identidad) o la importancia de utilizar la clase monádica Try para manejar situaciones en las que aparecen excepciones. Se ha estudiado cómo la utilización de las mónadas Either y Option nos pueden ayudar a la hora de manejar situaciones especiales durante el desarrollo de las estructuras de datos sin necesidad de lanzar excepciones. Se ha visto cómo estos tipos algebraicos de datos nos ofrecen una manera simple de razonar a la hora de manejar situaciones especiales y cómo se pueden aprovechar sus características modulares y de composición. El uso de estas mónadas, junto con funciones de orden superior, ayudan a tratar con errores de una manera especial. Algo que sería imposible si se tuvieran que lanzar excepciones. De este modo, el uso de excepciones se dejaría para situaciones irrecuperables del programa. A pesar de no haber desarrollado programas complejos, los principios definidos son aplica- bles tanto en la construcción de programas complejos, como en programas más simples. Tras este recorrido por la programación funcional y después de haber estudiado los funtores y las mónadas en Scala, es notable que Scala es un lenguaje de programación que se puede utilizar para explicar los conceptos relacionados con la programación funcional avanzada. También se han presentado, de forma resumida, algunas de las diferentes posibilidades que nos ofrece Scala para realizar comprobaciones al código escrito. Se analiza cómo se pueden utilizar algunas de estas posibilidades para hacer pruebas a las estructuras de datos y así poder constatar que los resultados obtenidos son los esperados o que las estructuras cumplen con las propiedades que se han definido sobre ellas. Adicionalmente, se ha examinado brevemente la solución que propone Scala a la proble- mática de la concurrencia, el modelo de actores, una solución basada en el paso asíncrono de mensajes inmutables. Inicialmente se ha explicado las bases del modelo de actores en Scala, la biblioteca scala.actors, para después introducir los principios y explicar las diferencias de una solución mucho más completa, la biblioteca Akka. A través de la concurrencia se han visto tam- bién las posibles soluciones al manejo de estados en la programación funcional y cómo cambiar el comportamiento de una clase. Definitivamente es posible aseverar que Scala es un lenguaje en el que se pueden explicar tanto los conceptos básicos presentes en los lenguajes de programación, como los conceptos re- lativos a la POO o los conceptos relacionados con el paradigma de la programación funcional, todos ellos presentes en un mismo lenguaje de programación. Además, Scala dispone de dife- rentes soluciones para las necesidades que pueda tener el programador a la hora de desarrollar aplicaciones concurrentes o realizar pruebas al código escrito. Todas las características descritas previamente, junto que el código generado por el com- pilador se ejecuta en la AcrónimosJVM y la posibilidad de interacción que presenta con Java hacen que Scala sea un lenguaje de programación muy interesante para el programador. Por tanto, se podría afirmar que Scala es un lenguaje de programación que podría ser utili- zado como una alternativa dentro del ámbito de la educación. Página 188
  • 205. Capítulo 8 Solución a los ejercicios propuestos 8.1. Evaluación en Scala Solución del ejercicio 1 Si ejecutamos el código en el intérprete veremos que devuelve 33. Razonemos la solución: 1. En el ámbito principal del intérprete se define el valor de las variables x (10) e y (20), así como la función prueba. 2. La ejecución de prueba(3) crea un nuevo ámbito de evaluación, dentro del ámbito princi- pal, en el que se ejecuta el cuerpo de la función. 3. En el ámbito de evaluación se liga el valor de z (argumento de prueba ) con el valor 3 (parámetro con el que se realiza la llamada). 4. En este nuevo ámbito se ejecuta el cuerpo de la función: se crea la variable local x con el valor 0 y se evalúa la expresión x+y+z . El valor de x y z están definidos en el propio ámbito local (0 y 3). El valor de y se obtiene del entorno padre: 20. El resultado de la expresión es 23. 5. Se invoca a la función g, con el valor 23 como parámetro y . Para evaluar esta invocación se crea un nuevo ámbito local dentro del ámbito global en donde está definida la función. 6. Dentro de este nuevo ámbito se evalúa la expresión x+y devolviéndose 33 como resultado ya que x valdrá 10 (definida en el ámbito global). 8.2. Introducción a la Programación Funcional Solución del ejercicio 2 No. Solución del ejercicio 3 Sí. Solución del ejercicio 4 No. Página 189
  • 206. Solución del ejercicio 5 No. Solución del ejercicio 6 Sí. Solución del ejercicio 7 Int 10 Solución del ejercicio 8 fact2 • Se aprecian dos problemas en la función fact2: las llamadas a la función con núme- ros negativos provocarán un comportamiento incorrecto de la función. Las llamadas a fact2 con números muy grandes puede provocar desbordamiento de pila. • El primer problema se podría solucionar añadiendo a nuestro código la precondición n > 0. El segundo problema se podría solucionar haciendo una versión recursiva de cola. 1 def fact2(n: Int):Int = { 2 require (n>=0) 3 @annotation.tailrec 4 def go(x:Int,acu:Int):Int= if (x == 0) acu else go((x-1),(acu*x)) 5 go(n,1) 6 } Solución del ejercicio 9 Int. Error de compilación. 15. Error de compilación. Solución del ejercicio 10 6. Solución del ejercicio 11 fun(1,2)(3). Página 190
  • 207. Solución del ejercicio 12 11. Solución del ejercicio 13 Sí. Solución del ejercicio 14 20. hello hello Solución del ejercicio 15 Una posible solución sería: 1 def numeroDigitos(x:Int):Int = { 2 require(x>0) 3 x match{ 4 case x if (x<10) => 1 5 case other => 1 + numeroDigitos(other / 10) 6 } 7 } Solución del ejercicio 16 Una posible solución sería: 1 def aprueba(calificaciones: List[Int]): List[Int] = 2 calificaciones.map(x=> if (x<5) 5 else x) Solución del ejercicio 17 Una posible solución podría ser: 1 def eliminaBajos(xs: List[Int],cota: Int): List[Int] = 2 xs.filter(valor => valor >= cota) Solución del ejercicio 18 Una posible solución sería: 1 def invierteDigitos(x:Int):Int ={ 2 require(x>=0) 3 @annotation.tailrec 4 def invierte(digito:Int,acum:Int):Int={ 5 digito match{ 6 case digito if (digito<10) => acum+digito 7 case other => invierte((other / 10),((acum+digito % 10)*10)) 8 } 9 10 } 11 invierte(x,0) 12 13 } Página 191
  • 208. Solución del ejercicio 19 Una posible solución sería: 1 def esCapicua(x:Int):Boolean = x==invierteDigitos(x) Solución del ejercicio 20 Una posible solución sería: 1 def fibonacci(x:Int):Int={ 2 @annotation.tailrec 3 def go(fib2:Int,fib1:Int,y:Int):Int={ 4 if (x==y) fib1 5 else go(fib1,fib2+fib1,y+1) 6 } 7 if (x==0) 0 8 else go(0,1,1) 9 } Solución del ejercicio 21 Una posible solución sería: 1 type TotalSegundos=Int 2 type Horas =Int 3 type Minutos = Int 4 type Segundos = Int 5 6 def descomponer(seg:TotalSegundos):(Horas,Minutos,Segundos)={ 7 val horas = seg / 3600 8 val resto = seg % 3600 9 val minutos = resto / 60 10 val segundos = resto % 60 11 (horas,minutos,segundos) 12 } Solución del ejercicio 22 Una posible solución sería: 1 def mcd (x: Int)(y:Int): Int = { 2 if (y==0) x else mcd (y) (x % y) 3 } 4 def coprimos(x:Int, y:Int):Boolean ={ 5 mcd(x)(y)== 1 6 } Solución del ejercicio 23 Una posible solución sería: 1 def pascal(c: Int, r: Int): Int = { 2 require(c>=0 & r>=0 & r>=c) 3 4 if (c == 0 || c == r) 1 5 else pascal(c - 1, r - 1) + pascal(c, r - 1) 6 } Página 192
  • 209. Solución del ejercicio 24 Una posible implementación de la función balanceado si el parámetro es una cadena podría ser: 1 def balanceado(str:String):Boolean =(str count ( p => p == ’(’ )) == (str count (p => p==’)’)) Si el parámetro es del tipo List[Char]: 1 def balanceado(str:List[Char]):Boolean ={ 2 @annotation.tailrec 3 def cuenta(carac:Char,cadena:List[Char],acc:Int):Int = cadena match { 4 case Nil => acc 5 case a::xs if (a!=carac) => cuenta(carac,xs,acc) 6 case a::xs => cuenta(carac,xs,acc+1) 7 } 8 cuenta(’(’,str,0) == cuenta (’)’,str,0) 9 } 8.3. Estructuras de datos 8.3.1. TAD Lista Solución del ejercicio 25 1 def last[A](xs:Lista[A]):A=xs derecho Solución del ejercicio 26 1 def init[A](xs:Lista[A]):Lista[A]=xs elim_der Solución del ejercicio 27 1 def take[A](xs:Lista[A])(x:Int):Lista[A]= x match{ 2 case 0 => Nil 3 case _ => if (xs esVacia) Nil else (xs cabeza) :: take(xs cola)(x-1) 4 } Solución del ejercicio 28 Página 193
  • 210. 1 def splitAt[A] (xs:Lista[A]) (x:Int): (Lista[A],Lista[A]) =(take(xs)(x),drop(xs)(Nat(x))) Solución del ejercicio 29 1 def zipWith[A,B,C] (xs:Lista[A]) (ys:Lista[B]) (f:A=>B=>C):Lista[C] = { 2 @annotation.tailrec 3 def go(xs:Lista[A],ys:Lista[B],zs:Lista[C]):Lista[C]= xs match { 4 case Nil => zs 5 case otro => ys match{ 6 case Nil => zs 7 case other => go(xs.cola,ys.cola,zs ## f(xs cabeza)(ys cabeza)) 8 } 9 } 10 go(xs,ys,Nil) 11 } Solución del ejercicio 30 1 def zip[A,B](xs:Lista[A])(ys:Lista[B]):Lista[(A,B)]=zipWith (xs) (ys) (x=>y=>(x,y)) Solución del ejercicio 31 1 /** Definicion de unzip sin considerar la implementacion de lista */ 2 def unzip[A,B](xs:Lista[(A,B)]):(Lista[A],Lista[B])={ 3 def go(lista:Lista[(A,B)], res1:Lista[A], res2:Lista[B]): (Lista[A],Lista[B])= lista match{ 4 case Nil => (res1,res2) 5 case other=>go(lista.cola,res1 ## other.cabeza._1,res2 ## other.cabeza._2) 6 } 7 go(xs,Nil,Nil) 8 } 9 /** Definicion de unzip teniendo en cuenta la implementacion de lista */ 10 def unzip1[A,B](xs:Lista[(A,B)]):(Lista[A],Lista[B])={ 11 def go(lista:Lista[(A,B)], res1:Lista[A], res2:Lista[B]): (Lista[A],Lista[B])= lista match{ 12 case Nil => (res1,res2) 13 case Cons((a,b),xs)=>go(lista.cola,res1 ## a,res2 ## b) 14 } 15 go(xs,Nil,Nil) 16 } Página 194
  • 211. Solución del ejercicio 32 1 def map[A,B](xs:Lista[A])(f:A=>B):Lista[B]= xs match { 2 case Nil => Nil 3 case Cons(x,xss)=>f(x) :: map(xss)(f) 4 } Solución del ejercicio 33 1 def filter[A](xs:Lista[A])(f:A=>Boolean):Lista[A]={ 2 @annotation.tailrec 3 def go(xs:Lista[A],res:Lista[A],f:A=>Boolean):Lista[A]= xs match{ 4 case Nil => res 5 case Cons(el,xss)=>if (f(el)) go(xss,res ## el,f) else go(xss,res,f) 6 } 7 go(xs,Nil,f) 8 } Solución del ejercicio 34 1 def takeWhile[A](xs:Lista[A])(f:A=>Boolean):Lista[A]={ 2 @annotation.tailrec 3 def go[A](xs:Lista[A],res:Lista[A],f:A=>Boolean):Lista[A]= xs match{ 4 case Nil => res 5 case other => if (f(other cabeza)) go(other.cola, res ## (other cabeza),f) else res 6 } 7 go(xs,Nil,f) 8 } Solución del ejercicio 35 1 def dropWhile[A](xs:Lista[A])(f:A=>Boolean):Lista[A]= xs match{ 2 case Nil => Nil 3 case Cons(el,xss)=>if(f(el)) dropWhile(xss)(f) else xs 4 } Solución del ejercicio 36 1 def foldr[A,B](xs:Lista[A])(base:B)(f:A=>B=>B):B=xs match{ 2 case Nil => base 3 case Cons(ele,xss)=>f(ele)(foldr(xss)(base)(f)) 4 } Página 195
  • 212. Solución del ejercicio 37 1 @annotation.tailrec 2 def foldl[A,B](xs:Lista[A])(base:B)(f:B=>A=>B):B=xs match{ 3 case Nil => base 4 case Cons(ele,xss)=>foldl(xss)(f(base)(ele))(f) 5 } 8.3.2. TAD Arbol Solución del ejercicio 38 Una posible solución podría ser: 1 def listaToArbol[T < % Ordered[T]](ls: List[T]) : ArbolBB[T] = { 2 ls.reverse match { 3 case Nil => Vacio 4 case head :: xs => listaToArbol(xs.reverse).add(head) 5 } 6 } Hacer la inversa de las listas servirá para que los nodos se introduzcan en el mismo orden en el que aparecen en la lista. Solución del ejercicio 39 Una posible solución podría ser: 1 def isomorfismo[U >: T](A2: ArbolBB[U]) : Boolean = this match { 2 case Vacio => A2 == Vacio 3 case Nodo(raiz,izq,der) => A2 match { 4 case Nodo(ov,a2izq,a2der) => izq.isomorfismo(a2izq) && der.isomorfismo(a2der) 5 case _ => false 6 } 7 } Esta función irá dentro del trait ArbolBB. Solución del ejercicio 40 Una posible solución podría ser: 1 def esSimetrico : Boolean = this match { 2 case Vacio => true 3 case Nodo(raiz ,izq,der) => izq.isomorfismo(der) 4 } Esta función irá dentro del trait ArbolBB. Página 196
  • 213. 8.4. Colecciones 8.4.1. Tipo List Solución del ejercicio 41 List(1,2,3,4,5). Solución del ejercicio 42 List(4, 3, 2, 1). Solución del ejercicio 43 8. Solución del ejercicio 44 Una posible solución podría ser: 1 def miMax(xs:List[Int]) : Int = xs.foldLeft(0)( (x,y)=> if (x>y) x else y) Solución del ejercicio 45 Una posible solución sería: 1 def suma(xs:List[Int]):Int=xs.foldRight(0)(_ + _) 2 def producto(xs:List[Int]):Int=xs.foldRight(1)(_ * _) Solución del ejercicio 46 Una posible solución sería: 1 def diferencias(xs:List[Int]):List[Int] = xs match { 2 case Nil => Nil 3 case x :: Nil => Nil 4 case x :: y :: rest => (y-x) :: diferencias(y :: rest) 5 } Solución del ejercicio 47 Una posible solución sería: 1 def aplana[T] (xs:List[List[T]]):List[T] = xs.fold(Nil)(_ ++ _) No si usa Nil como caso base. Si se emplea List[T]() como caso base sí se podría utilizar foldLeft y foldRight: 1 def aplana[T] (xs:List[List[T]]):List[T] = xs.foldLeft(List[T]()) (_ ++ _) Si se usa Nil como caso base no se podrían utilizar foldRight y foldLeft, ya que son dos funciones parametrizadas. Por tanto, al definir el valor del caso base se inferirá el tipo del mismo (que a su vez debe de ser el tipo devuelto y el tipo del segundo parámetro de la operación a realizar por foldRight), lo cual daría un error ya que el tipo de los elementos de xs es List[T]. La función fold define una cota superior1 para su parámetro en relación al parámetro T del tipo List[T], por lo que el tipo del caso base (Nil) se inferirá List[T]. 1 Como se vio en la Subsección 2.5.1: Acotación de tipos y varianza « página 47 ». Página 197
  • 214. Solución del ejercicio 48 Una posible solución sería: 1 def aEntero(l:List[Int]):Int = { 2 def pegaDigitos(x:Int,y:Int)={ 3 def pega(x:Int,y:Int,cont:Int):Int={ 4 cont match{ 5 case 0 => x + y 6 case other => pega(x*10,y,cont-1) 7 } 8 } 9 pega(x,y,numeroDigitos(y)) 10 } 11 def aEnt(lis:List[Int],acum:Int):Int={ 12 lis match{ 13 14 case (x::Nil) => pegaDigitos(acum,x) 15 case (y::t)=>aEnt(t,pegaDigitos(acum,y)) 16 case other => 0 //Situacion error 17 } 18 } 19 aEnt(l,0) 20 } Solución del ejercicio 49 Una posible solución sería: 1 def aLista(x:Int):List[Int]= { 2 def aList(x:Int):List[Int]={ 3 x match{ 4 case x if x<10 => x::Nil 5 case other => other % 10 :: aList(other/10) 6 7 } 8 9 } 10 aList(invierteDigitos(x)) 11 } Solución del ejercicio 50 Una posible solución sería: 1 def miLength[A](xs:List[A]):Int= xs match { 2 case Nil => 0 3 case h::t => 1 + miLength(t) 4 } Solución del ejercicio 51 Una posible solución sería: Página 198
  • 215. 1 def penultimo(xs: List[Int]) : Int = xs match { 2 | case x :: y :: Nil => x 3 | case x :: y :: rest => penultimo(y :: rest) 4 | case _ => throw new Error("No existe penultimo") 5 | } Solución del ejercicio 52 Una posible solución sería: 1 def esPalindromo[T](list: List[T]) : Boolean = { 2 list match { 3 case Nil => true 4 case head :: Nil => true 5 case _ => 6 (list.head == list.last) && 7 esPalindromo(list.tail.reverse.tail.reverse) 8 } 9 } Solución del ejercicio 53 Una posible solución podría basarse en la definición vista del método apply en la clase List: 1 def enesimo[A](n:Int,xs:List[A]):A = xs(n) Si se tienen en cuenta las precondiciones de la función, n ≥ 0, sería posible definir la función: 1 def enesimo[A](n:Int,xs:List[A]):A = {require(n>=0); xs(n)} Solución del ejercicio 54 Una posible solución podría ser: 1 def eliminaDuplicados[A](xs:List[A]):List[A]={ 2 xs match { 3 case Nil => Nil 4 case x :: Nil => xs 5 case x :: y :: rest if (x==y) => 6 eliminaDuplicados(y::rest) 7 case x :: y :: rest => 8 x :: eliminaDuplicados(y::rest) 9 } 10 } Aunque también se podría haber optado por utilizar plegado de listas: 1 def eliminaDuplicados[A](xs:List[A]):List[A] = xs.foldLeft (List[A]()) 2 ( (acc:List[A],elem:A) => acc match { 3 case Nil=> List(elem); 4 case _ => if (acc.head != elem) elem :: acc else acc 5 }).reverse Página 199
  • 216. Solución del ejercicio 55 1 def duplica[A](xs:List[A]):List[A]= xs flatMap (x=>List(x,x)) Solución del ejercicio 56 1 def repiteN[A](n:Int,xs:List[A]):List[A] = xs flatMap (x => (1 to n) map (_ => x)) Solución del ejercicio 57 1 def agrupaDuplicados[A](xs:List[A]):List[List[A]] = { 2 xs match { 3 case Nil => List(Nil) 4 case elem :: Nil => List(List(elem)) 5 case x :: y :: rest if x != y => 6 List(List(x)) ::: agrupaDuplicados(y :: rest) 7 case x :: y :: rest => // x e y son iguales 8 agrupaDuplicados(rest) match { 9 case List(Nil) => List(List(x,y))//No habia mas elementos en xs 10 case restList :: restXs if (restList.head == x) => //Habia mas elementos 11 //iguales a x e y 12 List(List(x,y) ::: restList) ::: restXs 13 case rest => List(List(x,y)) ::: rest //Habia mas elementos en xs pero 14 //distintos a x e y 15 } 16 } 17 } 8.4.2. Otras colecciones Solución del ejercicio 58 Una posible solución al ejercicio propuesto podría ser: 1 def construirMap[A,B](datos: Seq[A], f: A=>B): Map[B,A] = { 2 @annotation.tailrec 3 def go (datos: Seq[A], f: A=>B, acc: Map[B,A]): Map[B,A] = 4 datos match { 5 case Nil => acc 6 case _ => go(datos.tail,f,acc + (f(datos.head)->datos.head)) 7 } 8 go(datos,f,Map()) 9 } Página 200
  • 217. Solución del ejercicio 59 Una posible solución al problema planteado podría utilizar con- juntos: 1 def palabrasDistintas(str:String):Set[String] = 2 str.split(" +").map(_.filter(_.isLetter).toLowerCase).filter(_ != "").toSet Solución del ejercicio 60 La agenda de teléfonos es un buen ejemplo para utilizar Maps, en el que la clave puede ser el nombre y el valor los datos del contacto. Una posible solución podría ser: 1 case class Persona(nombre:String, 2 apellidos:String, 3 tfno:String, 4 direccion:String, 5 email:String) 6 case class Agenda(agenda:Map[String,Persona]){ 7 def add(contacto:Persona):Agenda = Agenda(agenda + (contacto.nombre -> contacto)) 8 } Se define la función contactos: 1 case class Agenda(agenda:Map[String,Persona]){ 2 def add(contacto:Persona):Agenda = Agenda(agenda + (contacto.nombre -> contacto)) 3 def contactos():List[String] = agenda.keys.toList 4 } Se define la función telefonos: 1 case class Agenda(agenda:Map[String,Persona]){ 2 def add(contacto:Persona):Agenda = Agenda(agenda + (contacto.nombre -> contacto)) 3 def contactos():List[String] = agenda.keys.toList 4 def telefonos():List[(String,String)]= contactos.zip(agenda.values.map { persona => persona.tfno }) 5 } Una posible solución para que la clase Agenda pueda guardar más de un contacto con el mismo nombre y seguir utilizando el nombre como clave podría ser: Página 201
  • 218. 1 case class Agenda(agenda:Map[String,List[Persona]]){ 2 def contactos():List[Persona] = { 3 (for ((contacto,listaContacto) <- agenda; 4 persona <- listaContacto) yield persona).toList; 5 } 6 7 def add(contacto:Persona):Agenda = Agenda({ 8 if (agenda.contains(contacto.nombre)) 9 agenda + (contacto.nombre->agenda(contacto.nombre).::(contacto)) 10 else agenda + (contacto.nombre -> List(contacto))}) 11 12 def telefonos():List[(Persona,String)] = contactos.zip(agenda.values.flatMap( listaPerson => listaPerson.map(persona=> persona.tfno))) 13 } Además de los cambios realizados en la clase Agenda también se ha sobreescrito el méto- do toString de la clase Persona para que muestre el nombre y el apellido de cada contacto: 1 case class Persona(nombre:String, 2 apellidos:String, 3 tfno:String, 4 direccion:String, 5 email:String){ 6 override def toString():String = nombre +" " + apellidos 7 } Se podría sobrecargar el método contactos, con la implementación de la función que devuelva la lista vacía si no hay ningún contacto en la agenda que se corresponda con el nombre pasado como parámetro o una lista con todas los contactos almacenados en la agenda que tengan ese nombre. 1 def contactos(nombre:String):List[Persona]= agenda.getOrElse(nombre, List()) Una posible implementación de la función vecinos de una dirección dada podría ser: 1 def vecinos(place:String):List[Persona]={ 2 contactos().filter { x => x.direccion==place} 3 } Una posible implementación de la función vecinos sería: 1 def vecinos():Map[String,List[Persona]]=contactos().flatMap({ 2 x => Map(x.direccion->vecinos(x.direccion))}).toSet.toMap La función vecinos() mejorada podría ser: Página 202
  • 219. 1 def vecinos():Map[String,List[Persona]]=contactos().flatMap({ 2 x => Map(x.direccion->vecinos(x.direccion))}).toSet.toMap Solución del ejercicio 61 Una posible solución al problema planteado podría utilizar con- juntos: 1 def numeroPalabrasDistintas(str:String):Map[String,Int] = { 2 val listaPalabras = str.split(" ").map(_.filter(_.isLetter).toLowerCase).filter( _ != "" ).toList 3 listaPalabras.zip( (0 until listaPalabras.size).map(x => listaPalabras.count( _ == listaPalabras(x)))).toSet.toMap 4 } Algoritmo 8.1: Función numeroPalabrasDistintas utilizando zip y map A continuación se verá la solución aportada con más detalle: En primer lugar, se observa en la línea 2 del algoritmo 8.1 cómo se ha asignado a la variable listaPalabras el valor resultante de crear la lista de palabras existentes en str. El resultado de la evaluación de la expresión escrita en la línea 3 del algoritmo 8.1 va a ser analizado con más detalle: 1. Se ha iterado sobre cada palabra (listaPalabras(x)) utilizando el método map de la colección Rango, obteniendo un Vector con las veces que se repite cada palabra con resultado de la evaluación de la expresión: 1 (0 until listaPalabras.size).map(x=> listaPalabras.count(_ == listaPalabras(x))) 2. Posteriormente, haciendo uso del método zip de List, hemos creado un vector con las tuplas formadas por listaPalabras y el vector resultante de la anterior acción. 1 listaPalabras.zip((0 until listaPalabras.size).map(x=> listaPalabras.count(_ == listaPalabras(x)))) 3. Como las tuplas pueden encontrarse repetidas, tanto en listaPalabras como en el vector con la cuenta de la repetición de cada palabra, se eliminan las tuplas repetidas transformando el anterior resultado en un conjunto haciendo uso del método toSet de List. 4. Finalmente se transforma el anterior resultado en un Map utilizando el método to- Map de Set. Otra posible solución podría haber sido: Página 203
  • 220. 1 def numeroPalabrasDistintas(str:String):Map[String,Int] = { 2 val listaPalabras = str.split(" ").map(_.filter(_.isLetter).toLowerCase).filter(_ != "").toList 3 listaPalabras.foldLeft(Map[String,Int]())((m,w) => { 4 if(m.contains(w)) m + (w -> (m(w)+1)) 5 else m + (w -> 1) 6 }) Algoritmo 8.2: Función numeroPalabrasDistintas utilizando plegado de Listas Los literales m y w hacen referencia al Map que actúa como acumulador y a los elementos de la lista, respectivamente. Nótese que las expresiones definidas en las líneas 2 y 3 del algoritmo 8.2 podrían unirse, obteniéndose un código que puede resultar más incómodo de leer y entender: 1 def numeroPalabrasDistintas(str: String): Map[String,Int] = 2 str.split(" ").map(_.filter(_.isLetter).toLowerCase).filter(_ != "").toList.foldLeft(Map[String,Int]())((m,w) => { 3 if(m.contains(w)) m + (w -> (m(w)+1)) 4 else m + (w -> 1) 5 }) Algoritmo 8.3: Función numeroPalabrasDistintas en una expresión 8.5. Programación Funcional Avanzada Solución del ejercicio 62 La mónada Maybe es similar a la mónada Option, la cual se vio que cumplía las reglas de las mónadas en la Subsubsección 4.3.2.1: Reglas que deben satisfacer las mónadas « página 139 » y cuyo comportamiento se vio en la Subsección 4.4.2: Tipo de datos Option « página 150 ». Por tanto, en primer lugar se definirá la clase Maybe: 1 sealed trait Maybe[+A] { 2 3 def flatMap[B](f: A => Maybe[B]): Maybe[B] 4 def map[B](f:A=>B):Maybe[B] 5 } 6 7 case class Just[+A](a: A) extends Maybe[A] { 8 override def flatMap[B](f: A => Maybe[B]) = f(a) 9 override def map[B](f:A=>B):Maybe[B]=Just(f(a)) 10 } 11 12 case object MaybeNot extends Maybe[Nothing] { 13 override def flatMap[B](f: Nothing => Maybe[B]) = MaybeNot 14 override def map[B](f:Nothing=>B):Maybe[B]=MaybeNot 15 } También se podría haber aprovechado la definición del rasgo Monada realizado en la Sub- subsección 4.3.2.3: Map en las mónadas « página 141 » para crear la mónada Maybe: Página 204
  • 221. 1 object MonadaMaybe extends Monada[Maybe]{ 2 def unit[A](x:A):Maybe[A]=Just(x) 3 def flatMap[A,B] (ma:Maybe[A]) (f:A=> Maybe[B]):Maybe[B]= ma flatMap f 4 } En realidad, se puede considerar la clase Maybe como una clase monádica cuyo método unit es el método de fábrica apply del objeto acompañante que se crea automáticamente junto con las case class y el método flatMap también se encuentra definido. Solución del ejercicio 63 1. Una posible solución basada en la función flatMap: 1 def abueloMaterno(p: Persona): Maybe[Persona] = 2 p.madre flatMap { _.padre } También se podría haber definido la función abueloMaterno sin utilizar flatMap: 1 def abueloMaternoMatch(p: Persona): Maybe[Persona] = 2 p.madre match { 3 case Just(m) => m.padre 4 case MaybeNot => MaybeNot 5 } 2. Se usará en la solución la función flatMap: 1 def abuelaMaterna(p: Persona): Maybe[Persona] = 2 p.madre flatMap { _.madre } 3. Para resolver el problema se utilizará la función flatMap: 1 def abueloPaterno(p: Persona): Maybe[Persona] = 2 p.padre flatMap { _.padre } 4. Una posible solución usando flatMap podría ser: 1 def abuelaPaterna(p: Persona): Maybe[Persona] = 2 p.padre flatMap { _.madre } Solución del ejercicio 64 Una posible solución al ejercicio, utilizando flatMap, podría ser: Página 205
  • 222. 1 def abuelos(p: Person): Maybe[(Persona, Persona)] = 2 p.madre flatMap { m => 3 m.padre flatMap { fm => 4 p.padre flatMap { f => 5 f.padre flatMap { ff => 6 Just(fm, ff) 7 } 8 } 9 } 10 } Solución del ejercicio 65 Una posible solución al ejercicio, utilizando bucles for, podría ser: 1 def abuelosFor(p: Persona): Maybe[(Persona, Persona)] = 2 for( 3 m <- p.madre; 4 fm <- m.padre; 5 f <- p.padre; 6 ff <- f.padre) 7 yield (fm, ff) Solución del ejercicio 66 1 def divSec(dividendo: Double, divisor: Double):Maybe[Double] = divisor match { 2 case 0 => MaybeNot 3 case _ => Just(dividendo/divisor) 4 } Solución del ejercicio 67 La función sqrtSec podría implementarse: 1 def sqrtSec(num: Double): Maybe[Double] = num match { 2 case n if n<0 => MaybeNot 3 case _ =>Just(scala.math.sqrt(d)) 4 } Solución del ejercicio 68 Una posible solución al problema sería: 1 def sqrtDivSec(div:Double,dsor:Double):Maybe[Double] = 2 divSec(div,dsor) flatMap (x=> sqrtSec(x)) 8.6. Tests en Scala Solución del ejercicio 69 Se definirá la clase Test para realizar las comprobaciones: Página 206
  • 223. 1 import org.scalatest.FunSuite 2 3 class Test extends FunSuite{ 4 val a:Int=5 5 val b:Int=7 6 val c:Int=10 7 test("Propiedad conmutativa numeros enteros"){ 8 assert (a+b==b+a) 9 } Solución del ejercicio 70 Se agregará la siguiente función a nuestra clase Test: 1 test("Propiedad distributiva de la suma con respecto al producto de enteros"){ 2 assert(a*(b+c)==a*b+a*c) 3 } Solución del ejercicio 71 A continuación se definirá una nueva clase Test para realizar las comprobaciones: 1 import org.scalatest.FunSuite 2 3 class Test extends FunSuite { 4 test("Regla izquierda de Unit"){ 5 Persona.personas foreach { p => 6 7 // Regla izquierda Unit 8 9 assert((Just(p) flatMap { _.madre }) == p.madre) 10 } 11 } 12 val maybes = MaybeNot +: (Persona.personas map { Just(_) }) 13 // Regla derecha Unit 14 test("Regla derecha de Unit"){ 15 16 17 maybes foreach { m => 18 assert((m flatMap { Just(_) }) == m) 19 } 20 } 21 // Asociatividad 22 test("Asociatividad"){ 23 maybes foreach { m => 24 assert( 25 (m flatMap { _.madre } flatMap { _.padre }) == 26 (m flatMap { _.madre flatMap { _.padre } })) 27 } 28 } 29 } Página 207
  • 224. Solución del ejercicio 72 Se añadirá una nueva función a la suite: 1 test("Mismos abuelos con metodo abuelos y abuelosFor"){ 2 Persona.personas foreach { p => 3 assert(Persona.abuelos(p)==Persona.abuelosFor(p)) 4 } 5 } Solución del ejercicio 73 Para la solución de este ejercicio se añadirá una nueva función a la suite: 1 test("Mismo abuelo materno con metodo abueloMaterno y abueloMaternoMatch"){ 2 Persona.personas foreach { p => 3 assert(Persona.abuelos(p)==Persona.abuelosFor(p)) 4 } 5 } 8.7. Concurrencia Solución del ejercicio 74 Una posible solución podría ser: Página 208
  • 225. 1 package pinpong 2 3 import akka.actor.{Actor, ActorLogging, Props} 4 5 class PingActor extends Actor with ActorLogging { 6 import PingActor._ 7 8 var counter = 0 9 val pongActor = context.actorOf(PongActor.props, "pongActor") 10 11 def receive = { 12 case Iniciar => 13 log.info("In PingActor - starting ping-pong") 14 pongActor ! PingMessage("ping") 15 case PongActor.PongMessage(text) => 16 log.info("En PingActor - mensaje recibido: {}", text) 17 counter += 1 18 if (counter == 3) context.system.shutdown() 19 else sender() ! PingMessage("ping") 20 } 21 } 22 23 object PingActor { 24 val props = Props[PingActor] 25 case object Iniciar 26 case class PingMessage(text: String) 27 } Algoritmo 8.4: Juego Ping-Pong. Actor Ping 1 package pinpong 2 3 import akka.actor.{Actor, ActorLogging, Props} 4 5 class PongActor extends Actor with ActorLogging { 6 import PongActor._ 7 8 def receive = { 9 case PingActor.PingMessage(text) => 10 log.info("En PongActor - mensaje recibido: {}", text) 11 sender() ! PongMessage("pong") 12 } 13 } 14 15 object PongActor { 16 val props = Props[PongActor] 17 case class PongMessage(text: String) 18 } Algoritmo 8.5: Juego Ping-Pong. Actor Pong Página 209
  • 226. 1 package pinpong 2 3 import akka.actor.ActorSystem 4 5 object ApplicationMain extends App { 6 val system = ActorSystem("MyActorSystem") 7 val pingActor = system.actorOf(PingActor.props, "pingActor") 8 pingActor ! PingActor.Iniciar 9 10 system.awaitTermination() 11 } Algoritmo 8.6: Juego Ping-Pong. Main Página 210
  • 227. Lista de Tablas 1.1. Tipos de datos primitivos y tamaño en Scala . . . . . . . . . . . . . . . . . . . 6 1.2. Caracteres de escape reconocidos por Char y String . . . . . . . . . . . . . . . 9 1.3. Prioridad y asociatividad de los operadores . . . . . . . . . . . . . . . . . . . 11 1.4. Operadores aritméticos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.5. Operadores relacionales en Scala . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.6. Operadores lógicos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.7. Tabla de verdad de los operadores bit a bit &, | y ˆ . . . . . . . . . . . . . . . . 14 1.8. Operadores bit a bit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.9. Operadores de asignación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 1.10. Reglas de reducción para expresiones booleanas . . . . . . . . . . . . . . . . . 20 1.11. Tipos de bucles en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.1. Métodos del tipo Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 3.2. Métodos del tipo List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 3.3. Métodos del tipo Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 3.4. Métodos del tipo Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 3.5. Secuencias. Eficiencia de las operaciones. . . . . . . . . . . . . . . . . . . . . 122 3.6. Set y Map. Eficiencia de las operaciones . . . . . . . . . . . . . . . . . . . . . 122 4.1. Tipos existenciales en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 5.1. Suites del framework ScalaTest . . . . . . . . . . . . . . . . . . . . . . . . . . 160 6.1. Sistema Reactivo Vs Sistema Transformacional . . . . . . . . . . . . . . . . . 165 6.2. Receive Vs React . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 Página 211
  • 229. Lista de Algoritmos 1.1. Hola Mundo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.2. Función con dos parámetros. El primero es evaluado por valor y el segundo por nombre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 1.3. Sintaxis sentencia if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.4. Expresión condicional if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 1.5. Sintaxis sentencia if / else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 1.6. Expresiones condicionales. Función valor absoluto . . . . . . . . . . . . . . . 22 1.7. Sintaxis sentencia if / else if / else . . . . . . . . . . . . . . . . . . . . . . . . 23 1.8. Sentencias if anidadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 1.9. Sintaxis bucles while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.10. Ejemplo de bucle while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.11. Sintaxis bucles do... while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 1.12. Ejemplo de bucle do... while. . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 1.13. Sintaxis bucles for con rangos. . . . . . . . . . . . . . . . . . . . . . . . . . . 26 1.14. Ejemplo de bucle for con rangos. . . . . . . . . . . . . . . . . . . . . . . . . . 26 1.15. Sintaxis bucles for con colecciones . . . . . . . . . . . . . . . . . . . . . . . . 26 1.16. Ejemplo bucle for con colecciones. . . . . . . . . . . . . . . . . . . . . . . . . 27 1.17. Sintaxis bucles for con filtros. . . . . . . . . . . . . . . . . . . . . . . . . . . 27 1.18. Ejemplo bucle for con filtros . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 1.19. Sintaxis bucles for con yield. . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 1.20. Ejemplo bucle for con yield . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 1.21. Fecha actual formateada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 2.1. Mi primera clase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.2. Método suma que no compila . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 2.3. MiPrimeraClase con método suma . . . . . . . . . . . . . . . . . . . . . . . . 34 2.4. Números racionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.5. Cálculo del máximo común divisor de dos enteros . . . . . . . . . . . . . . . . 37 2.6. Clase Abstracta Animal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 2.7. Clases Perro y Pajaro de tipo Animal . . . . . . . . . . . . . . . . . . . . . . . 39 2.8. Lista de Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 2.9. Lista de Booleanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 2.10. Lista Genérica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 2.11. Lista Genérica con funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 2.12. Ejemplo de función parametrizada . . . . . . . . . . . . . . . . . . . . . . . . 47 2.13. Función sonTodosPositivos para ListaInt . . . . . . . . . . . . . . . . . . . . . 48 2.14. Función sonTodosPositivos para ListaInt con tipo acotado superiormente . . . . 48 2.15. Función sonTodosPositivos para ListaInt con tipo acotado inferiormente . . . . 48 2.16. Trait Function1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 2.17. Tipos de funciones A y B . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 Página 213
  • 230. 2.18. Trait Function1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 2.19. Lista genérica de enteros, covariante y con Nil como objeto . . . . . . . . . . . 51 3.1. Cálculo de la raíz cuadrada de un número por el método de Newton . . . . . . 56 3.2. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 3.3. Recursión Indirecta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.4. Función estricta valor absoluto . . . . . . . . . . . . . . . . . . . . . . . . . . 76 3.5. Ejemplo de función no estricta . . . . . . . . . . . . . . . . . . . . . . . . . . 76 3.6. Ejemplo de función no estricta con estrategia Call By Need . . . . . . . . . . . 77 3.7. Selectiva if como función no estricta . . . . . . . . . . . . . . . . . . . . . . . 77 3.8. Implementación final del tipo Naturales . . . . . . . . . . . . . . . . . . . . . 86 3.9. Implementacion TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 3.10. Object Lista ampliado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 3.11. Implementación básica de Arboles Binarios . . . . . . . . . . . . . . . . . . . 98 3.12. TAD Arbol Binario de Búsqueda . . . . . . . . . . . . . . . . . . . . . . . . . 99 3.13. Métodos next y hashNext en un bucle while . . . . . . . . . . . . . . . . . . . 105 3.14. Definicion de rangos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 3.15. Acceso a las cotas y valor de paso de los rangos . . . . . . . . . . . . . . . . . 108 3.16. Acceso a las cotas y valor de paso de los rangos . . . . . . . . . . . . . . . . . 108 3.17. Definición de tuplas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 3.18. Acceso a los elementos de una tupla. . . . . . . . . . . . . . . . . . . . . . . . 109 3.19. Iterando sobre los elementos de las tuplas . . . . . . . . . . . . . . . . . . . . 109 3.20. Método swap en tuplas de dos elementos . . . . . . . . . . . . . . . . . . . . . 110 3.21. Definición de Vector. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 3.22. Agregar elementos a un Vector . . . . . . . . . . . . . . . . . . . . . . . . . . 115 3.23. Definición de Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 3.24. Cálculo de la serie de Fibonacci utilizando Stream . . . . . . . . . . . . . . . . 116 3.25. Definición y propiedades fundamentales de los conjuntos . . . . . . . . . . . . 117 3.26. Recorriendo los elementos de un conjunto . . . . . . . . . . . . . . . . . . . . 118 3.27. Definicion de Maps. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 3.28. Definicion de Maps con tuplas. . . . . . . . . . . . . . . . . . . . . . . . . . . 119 3.29. Recuperar valores en Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 3.30. Agregar y eliminar elementos en un Map . . . . . . . . . . . . . . . . . . . . . 119 3.31. Recorrer los elementos de un Map . . . . . . . . . . . . . . . . . . . . . . . . 120 3.32. Ejemplo traducción bucle for con un generador . . . . . . . . . . . . . . . . . 123 3.33. Ejemplo traducción de expresiones for con un generador y un filtro . . . . . . . 123 3.34. Ejemplo de traducción de expresiones for con dos generadores . . . . . . . . . 123 3.35. Ejemplo traducción bucle for genérico . . . . . . . . . . . . . . . . . . . . . . 124 4.1. Definición de parámetros implícitos . . . . . . . . . . . . . . . . . . . . . . . 129 4.2. Definición de valores implícitos . . . . . . . . . . . . . . . . . . . . . . . . . 130 4.3. Definición de clases implícitas . . . . . . . . . . . . . . . . . . . . . . . . . . 130 4.4. Ejemplo de tipos estructurales. . . . . . . . . . . . . . . . . . . . . . . . . . . 134 4.5. Ejemplo de tipos de orden superior. . . . . . . . . . . . . . . . . . . . . . . . 135 4.6. Función distribuir usando funtores. . . . . . . . . . . . . . . . . . . . . . . . . 138 4.7. Definición básica del trait Mónada. . . . . . . . . . . . . . . . . . . . . . . . . 139 4.8. Monada Identidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 4.9. Juego JailBreak. Definición tipos necesarios. . . . . . . . . . . . . . . . . . . . 144 4.10. Juego JailBreak. Trait Game. . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 4.11. Juego JailBreak. Solución al juego. . . . . . . . . . . . . . . . . . . . . . . . . 144 Página 214
  • 231. 4.12. Juego JailBreak. Definición de la clase JailBreak. . . . . . . . . . . . . . . . . 145 4.13. Juego JailBreak. Trait Game incluyendo Try. . . . . . . . . . . . . . . . . . . . 147 4.14. Juego JailBreak. Trait Game incluyendo Try. . . . . . . . . . . . . . . . . . . . 147 4.15. Juego JailBreak. Solución al juego. . . . . . . . . . . . . . . . . . . . . . . . . 147 4.16. Juego JailBreak. Solución al juego utilizando flatMap. . . . . . . . . . . . . . . 148 4.17. Juego JailBreak. Solución al juego utilizando flatMap 2. . . . . . . . . . . . . . 148 4.18. Juego JailBreak. Solución al juego utilizando expresiones for. . . . . . . . . . . 149 4.19. Excepciones.Función media con excepciones . . . . . . . . . . . . . . . . . . 149 4.20. Excepciones.Función media con argumento para caso especial . . . . . . . . . 149 4.21. Tipo de datos Option . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 4.22. Excepciones. Función media con tipo de datos Option . . . . . . . . . . . . . . 150 4.23. Tipo de datos Either . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 4.24. Excepciones.Función media con tipo de datos Either . . . . . . . . . . . . . . 151 4.25. Excepciones.Division con Either . . . . . . . . . . . . . . . . . . . . . . . . . 151 5.1. Fibonacci sin recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . 156 5.2. Fibonacci sin recursión de cola. Assert que falla . . . . . . . . . . . . . . . . . 156 5.3. Excepcion AssertError . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 5.4. Fibonacci sin recursión de cola. Assert que falla + explicación . . . . . . . . . 156 5.5. Excepcion AssertError con información . . . . . . . . . . . . . . . . . . . . . 157 5.6. Fibonacci sin recursión de cola. Assert que no falla . . . . . . . . . . . . . . . 157 5.7. Fibonacci sin recursión de cola. Ensuring que falla . . . . . . . . . . . . . . . 157 5.8. Ensuring.Excepcion AssertError . . . . . . . . . . . . . . . . . . . . . . . . . 158 5.9. Fibonacci sin recursión de cola. Ensuring que no falla . . . . . . . . . . . . . . 158 5.10. Inversa de una lista. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . 158 5.11. Ensuring en el cálculo de la inversa de una lista . . . . . . . . . . . . . . . . . 158 5.12. ScalaTest. Ejemplo que extiende de org.scalatest.Suite . . . . . . . . . . . . . 161 5.13. ScalaTest. Ejemplo de la suite FunSuite . . . . . . . . . . . . . . . . . . . . . 162 6.1. Mi Primer Actor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 6.2. Procesando primer mensaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 6.3. Mi primer mensaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 6.4. Actor con estado definido con variable mutable . . . . . . . . . . . . . . . . . 172 6.5. Ejecutando Contador con estado mutable . . . . . . . . . . . . . . . . . . . . . 172 6.6. Actor con estado inmutable . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 6.7. Ejecutando Contador con estado inmutable . . . . . . . . . . . . . . . . . . . 173 6.8. Método act con loop y react . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 6.9. Diferencias entre Akka y la librería de Scala. Instanciación en Scala . . . . . . 175 6.10. Diferencias entre Akka y la librería de Scala. Instanciación en Scala . . . . . . 175 6.11. Diferencias entre Akka y la librería de Scala. Instanciación en Scala I . . . . . 176 6.12. Diferencias entre Akka y la librería de Scala. Instanciación en Scala II . . . . . 176 6.13. Diferencias entre Akka y la librería de Scala. Instanciación en Akka I . . . . . 176 6.14. Diferencias entre Akka y la librería de Scala. Instanciación en Akka II . . . . . 176 6.15. Eliminación de act. Ejemplo en Scala . . . . . . . . . . . . . . . . . . . . . . 176 6.16. Eliminación de act. Ejemplo en Akka . . . . . . . . . . . . . . . . . . . . . . 177 6.17. Akka.Trait Actor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 6.18. Akka.Actor con estado definido con variable mutable . . . . . . . . . . . . . . 178 6.19. Akka.Actor con estado inmutable . . . . . . . . . . . . . . . . . . . . . . . . . 178 6.20. Akka.Trait Actor y Trait ActorContext . . . . . . . . . . . . . . . . . . . . . . 179 6.21. Akka.Actor con estado inmutable (become) . . . . . . . . . . . . . . . . . . . 179 Página 215
  • 232. 6.22. Akka.Trait Actor y Clase Abstracta ActorRef . . . . . . . . . . . . . . . . . . 180 6.23. Akka.Mensjaes bidireccionales . . . . . . . . . . . . . . . . . . . . . . . . . . 180 6.24. Akka.Trait ActorContext II . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 6.25. Akka.Crear Actores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 8.1. Función numeroPalabrasDistintas utilizando zip y map . . . . . . . . . . . . . 203 8.2. Función numeroPalabrasDistintas utilizando plegado de Listas . . . . . . . . . 204 8.3. Función numeroPalabrasDistintas en una expresión . . . . . . . . . . . . . . . 204 8.4. Juego Ping-Pong. Actor Ping . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 8.5. Juego Ping-Pong. Actor Pong . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 8.6. Juego Ping-Pong. Main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 Página 216
  • 233. Bibliografía [1] Growing a Language, Burlington, Massachusetts, Octubre 1998. 2 [2] Akka Scala Documentation. 2.3.12 edition, julio 2015. 175, 177 [3] Bagwell. Learning scala. http://www.scala-lang.org/node/1305, septiem- bre 2012. [Online; accedido 15-Abril-2013]. [4] Richard Bird. Introducción a la Programación funcional con Haskell. Prentice Hall, segunda edición edition, 2000. http://goo.gl/cGTpJ.[Online; accedido 15-Abril- 2013]. [5] Paul Chiusano and Rúnar Bjarnason. Functional Programming in Scala. MEAP, 2013. 143 [6] Joshua D. Suereth. Scala in Depth. Mannaging Publications, 2012. 103, 116 [7] Desconocido. Scala (lenguaje de programación). http://es.wikipedia.org/ wiki/Scala_(lenguaje_de_programaci%C3%B3n). [Online; accedido 15- Abril-2013]. 1 [8] Desconocido. Scala to a haskell programmer. goo.gl/k0Ks4V, septiembre 2012. [On- line; accedido 15-Abril-2013]. [9] EPFL. Scala api. http://www.scala-lang.org/api. [Online; accedido 15- Abril-2015]. 110 [10] Xavier Franch Gutiérrez. Estructuras de Datos. Especificación, diseño e implementación. Ediciones UPC, tercera edition, abril 1999. 89 [11] Daniel Garrido Márquez. Apuntes programación concurrente. 165, 167 [12] Hewitt, Bishop, and Steiger. A Universal Actor Formalism for Artificial Intelligence. IJ- CAI, 1973. 168, 169 [13] Vojin Jovanovic and Philipp Haller. The scala actors migration gui- de. http://http://docs.scala-lang.org/overviews/core/ actors-migration-guide.html. [Online; accedido 25-Agosto-2013]. 177 [14] Mark C. Lewis. Introduction to the art of programming using Scala. CRC Press, Agosto 2012. 104 [15] Narciso Martí Oliet, Yolanda Ortega Mallén, and Alberto Verdejo. Estructuras de Datos y Métodos Algorítmicos. Garceta, segunda edition, 2013. 80, 89 Página 217
  • 234. [16] Jesper Nordenberg. My Take on Haskell vs Scala. http://jnordenberg. blogspot.com.es/2012/05/my-take-on-haskell-vs-scala.html, mayo 2012. [Online; accedido 15-Abril-2013]. [17] M. Odersky. Scala functional programming. Coursera. [Online; accedido 29-Agosto- 2015]. 38 [18] M. Odersky. Scala by Example. PROGRAMMING METHODS LABORATORY, EPFL, SWITZERLAND, noviembre 2010. http://es.scribd.com/doc/47368246/ Scala-By-Example-Martin-Odersky. [19] M. Odersky and Roland Kuhn. Scala. principles of reactive programming. Coursera. [Online; accedido 29-Agosto-2015]. 143, 181 [20] M. Odersky, L. Spoon, and B. Venners. Programming in Scala: A Comprehensive Step- by-Step Guide. Artima Inc., segunda edición edition, 2010. [21] Miguel Pastor. Scala: Primeros pasos. http://miguelinlas3.blogspot.com. es/2011/01/scala-primeros-pasos.html, enero 2011. [Online; accedido 15- Abril-2013]. [22] Alex Payne and Dean Wampler. Programming Scala. O’Reilly Media, septiembre 2009. [23] David Pollak. Beginning Scala. Apress, 2009. 169 [24] Nilanjan Raychaudhuri. Scala in Action. Manning, abril 2010. 53 [25] Blas C. Ruiz, Francisco Gutiérrez, Pablo Guerrero, and José E. Gallardo. Razonando con Haskell. Universidad de Málaga, segunda edition, abril 2004. 80 [26] ScalaTest. Scalatest user guide. http://es.wikipedia.org/wiki/ Arquitectura_de_von_Neumann. [Online; accedido 30-Agosto-2015]. 3 [27] ScalaTest. Scalatest user guide. http://www.scalatest.org/user_guide. [Online; accedido 30-Agosto-2015]. [28] Michel Schinz and Philipp Haller. A scala tutorial for java pro- grammers. http://docs.scala-lang.org/es/tutorials/ scala-for-java-programmers.html, mayo 2011. [Online; accedido 15- Julio-2013]. [29] Jason Swartz. Learning Scala. Practical Functional Programming for the JVM. O’Really, segunda edition, Diciembre 2014. 102 [30] Tutorialspoint. Scala tutorial. http://www.tutorialspoint.com/scala/ index.htm. [Online; accedido 10-Septiembre-2015]. 102 [31] Antonio Vallecillo Moreno and R. Guerequeta García. Técnicas de diseño de algoritmos. Universidad de Málaga, segunda edition, 2000. 78 [32] Wikipedia. Método de newton. https://es.wikipedia.org/wiki/M%C3% A9todo_de_Newton. [Online; accedido 12-Diciembre-2015]. 56 Página 218
  • 235. [33] Wikipedia. Programacion funcional. https://es.wikipedia.org/wiki/ Programaci%C3%B3n_funcional. [Online; accedido 29-Agosto-2015]. [34] Wikipedia. Teoría de las categorías. https://es.wikipedia.org/wiki/ Teoria_de_categorias. [Online; accedido 1-Octubre-2015]. 136 Página 219
  • 237. Glosario .NET .NET representa la evolución del Component Object Model (COM), la plataforma de desarrollo de Microsoft anterior a .NET y sobre la cual se basaba el desarrollo de apli- caciones Visual Basic 6 (entre otros tantos lenguajes y versiones). Es una plataforma de desarrollo y ejecución de aplicaciones. Esto quiere decir que no sólo nos brinda todas las herramientas y servicios que se necesitan para desarrollar modernas aplicaciones empre- sariales y de misión crítica, sino que también nos provee de mecanismos robustos, seguros y eficientes para asegurar que la ejecución de las mismas sea óptima. Android Sistema operativo basado en el núcleo Linux. Fue diseñado principalmente para disposi- tivos móviles con pantalla táctil, como teléfonos inteligentes o tabletas; y también para relojes inteligentes, televisores y automóviles. buffer Espacio de la memoria en un disco o en un instrumento digital reservado para el almace- namiento temporal de información, mientras que está esperando ser procesada. bytecode El bytecode es un código intermedio más abstracto que el código máquina. Habitualmente es tratado como un archivo binario que contiene un programa ejecutable similar a un módulo objeto, que es un archivo binario producido por el compilador cuyo contenido es el código objeto o código máquina. evaluación estricta Estrategia de evaluación que evalúa los argumentos antes de reemplazar la función por la definición de la misma. También conocida como Call by Value o paso por valor. evaluación no estricta Estrategia de evaluación que no evalúa los argumentos antes de reemplazar la función por la definición de la misma. También conocida como Call by Name o paso por referencia. evaluación perezosa Estrategia de evaluación que retrasa el cálculo de una expresión hasta que su valor sea necesario, y que también evita repetir la evaluación en caso de ser necesaria en posteriores ocasiones. También conocida como Call by Need o paso por necesidad. Página 221
  • 238. Haskell Lenguaje de programación funcional puro, fuertemente tipado. Su nombre se debe al lógico estadounidense Haskell Curry. literal Notación que representa un valor dentro del lenguaje de programación. llamada de cola Llamada a una subrutina que se realiza como la última acción de un procedimiento. Si esta llamada de cola se realiza a la misma subrutina que desde donde se realiza, la subrutina se llamará recursiva de cola (que es un caso especial de recursión). . programación declarativa La programación declarativa, en contraposición a la programación imperativa es un pa- radigma de programación que está basado en el desarrollo de programas especificando o “declarando” un conjunto de condiciones, proposiciones, afirmaciones, restricciones, ecuaciones o transformaciones que describen el problema y detallan su solución. programación funcional Es un tipo de paradigma de programación dentro del paradigma de programación decla- rativa que se basa en el concepto de función (que no es más que una evolución de los predicados), de corte más matemático. Los lenguajes funcionales se basan en el cálculo lambda.. programación imperativa Es un paradigma de programación que describe la programación en términos del estado del programa y sentencias que cambian dicho estado. Los programas imperativos son un conjunto de instrucciones que le indican al computador cómo realizar una tarea. programación lógica Es un tipo de paradigma de programación dentro del paradigma de programación decla- rativa que gira en torno al concepto de predicado, o relación entre elementos. La mayoría de los lenguajes de programación lógica se basan en la teoría lógica de primer orden, aunque también incorporan algunos comportamientos de orden superior como la lógica difusa (aunque ésta no siempre tiene que ser de orden superior). prueba unitaria Una prueba unitaria es una forma de comprobar el correcto funcionamiento de un módulo de código. Esto sirve para asegurar que cada uno de los módulos funcione correctamente por separado. Luego, con las pruebas de integración, se podrá asegurar el correcto fun- cionamiento del sistema o subsistema en cuestión. Scheme Lenguaje de programación funcional (no puro) interpretado. Es un dialecto de Lisp, muy expresivo y soporta varios paradigmas. Estuvo influenciado por el cálculo lambda. Página 222
  • 239. script Secuencia de instrucciones almacenadas en un fichero que se ejecutan normalmente de forma interpretada.. sistema de tipos Un sistema de tipos define cómo un lenguaje de programación clasifica los valores y las expresiones en tipos, cómo se pueden manipular estos tipos y cómo interactúan. unicode Unicode es un estándar de codificación de caracteres diseñado para facilitar el tratamiento informático, transmisión y visualización de textos de múltiples lenguajes y disciplinas técnicas, además de textos clásicos de lenguas muertas. El término Unicode proviene de los tres objetivos perseguidos: universalidad, uniformidad y unicidad. vector Colección de un número determinado de elementos de un mismo tipo de datos ubicados en una zona de almacenamiento continuo, adecuado para situaciones en las que el acceso a los datos se realice de forma aleatoria e impredecible. Página 223
  • 241. Acrónimos ABB Árbol binario de búsqueda. ACM Asociación para la Maquinaria Computacional. API Application Programming Interface. asociación maps. cierre closure. clase acompañante companion class. COM Component Object Model. CPU unidad central de procesamiento. EPFL École Polytechnique Fédérale de Lausanne. espacio de nombres namespace. FIFO First In First Out – “primero en entrar, primero en salir” –. flujo de datos stream. hilo de ejecución thread. Página 225
  • 242. IDE Entorno de Desarrollo Integrado. IMC Índice de masa corporal. JVM Máquina Virtual de Java. modificaciones apilables stackable modifications. método de fábrica factory method. objeto acompañante companion object. objeto singleton singleton objects. POO programación orientada a objetos. REPL Bucle Leer-Evaluar-Imprimir. TADs Tipo abstracto de datos. TDD Test Driven Development. Página 226