Programación Funcional en Scala

242

Transcript of Programación Funcional en Scala

Page 1: Programación Funcional en Scala
Page 2: Programación Funcional en Scala
Page 3: Programación Funcional en Scala

UNIVERSIDAD DE MÁLAGA

ESCUELA TÉCNICA SUPERIOR DE INGENIERÍAINFORMÁTICA

INGENIERO EN INFORMÁTICA

PROGRAMACIÓN FUNCIONAL EN SCALA(FUNCTIONAL PROGRAMMING IN SCALA)

Realizado porRUBÉN PÉREZ LUJANO

Dirigido porJOSÉ ENRIQUE GALLARDO RUIZ

DepartamentoLENGUAJES Y CIENCIAS DE LA COMPUTACIÓN

MÁLAGA, SEPTIEMBRE 2016

Page 4: Programación Funcional en Scala
Page 5: Programación Funcional en Scala

Dedicado ala memoria de mi padre

Page 6: Programación Funcional en Scala
Page 7: Programación Funcional en Scala

Agradecimientos

Después de recorrer un largo camino ha llegado el momento de parar, coger aire, mirar haciaatrás un instante y dar las gracias a todas esas personas que en algún momento han formadoparte del mismo, que han compartido los mejores momentos, que me han ayudado a mirar haciadelante a pesar de las adversidades y junto a las que me gustaría continuar recorriendo el caminode 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 CarlosRuiz 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 lamano y ayudar a levantarme en los días más oscuros y por darme ánimos para continuar cuandomás duro se hacía el camino. Gracias por hacerme el hombre más feliz del mundo. Gracias porapostar 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 haenseñ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 ciegamenteen 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 quesiempre podré contar con ellos.

Gracias a mis amigos Nacho y Jesús, con los que he compartido algunos de los mejoresmomentos 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 necesitabay 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 yCoí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 miscompañeros de universidad y a mis compañeros de piso.

A todos, eternamente agradecido.

Page 8: Programación Funcional en Scala
Page 9: Programación Funcional en Scala

Introducción

La elección de un lenguaje para introducir la programación a los alumnos de las actualesingenierí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 ysencilla 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, yque debe conocer un futuro informático: el funcional y el orientado a objetos. Sin embargo esdifícil encontrar un lenguaje que integre ambos paradigmas de forma sencilla si se parte de labase de que tal lenguaje será el primer contacto de un estudiante con la programación. Ademásde esto, se quiere una buena elección desde el punto de vista del programador profesional; esdecir, tal lenguaje debe facilitar de forma “natural” el aprendizaje de los principales lenguajescon 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 paraexpresar 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 Javaexistentes; por ello el uso de Scala como un primer lenguaje será un puente importante con elmundo de la programación profesional.

El trabajo en Scala surge a partir de un esfuerzo de investigación para desarrollar un mejorsoporte de los lenguajes de programación para la composición de software. Hay dos hipótesisque 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 mismosconceptos pueden describir tanto partes pequeñas como grandes. Por tanto, los autores se hanconcentrado en los mecanismos para la abstracción, composición y descomposición en vez deañadir un conjunto grande de primitivas que pueden ser útiles para los componentes a algúnnivel de escala, pero no a otro nivel. Segundo, se postula, que el soporte escalable para loscomponentes puede ser previsto por un lenguaje de programación que unifica y generaliza laprogramación orientada a objetos y la funcional. Para los lenguajes con tipos estáticos, de losque 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íadonde 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 deprogramación multiparadigma como Scala en la resolución de los diferentes problemas que sepuedan plantear.

En el Capítulo 1: Scala « página 1 » se realiza una breve introducción a Scala, se presentael lenguaje y se analizan conceptos básicos de los lenguajes de programación como los tipos dedatos básicos, los operadores, las estructuras de control o la evaluación en Scala.

Página I

Page 10: Programación Funcional en Scala

II

En el Capítulo 2: Programación Orientada a Objetos en Scala « página 31 » se realiza unanálisis de Scala como un lenguaje orientado a objetos puro, se presenta la jerarquía de clasesy 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óna 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 dela programación funcional. Para finalizar el capítulo se hace un repaso por las colecciones queScala 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 introductorioa la programación. Después de haber estudiado previamente los aspectos fundamentales de laprogramación funcional, en el Capítulo 4: Programación Funcional Avanzada en Scala « página129 » se analizan conceptos más complejos de este paradigma, como la programación monádicaa 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 lassoluciones 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 bibliotecaAkka, 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 Scalacomo 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 puedenencontrar las soluciones de los ejercicios propuestos a lo largo de la guía.

Page 11: Programación Funcional en Scala

Índice general

1. Scala 11.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

1.1.1. Scala. Un lenguaje escalable . . . . . . . . . . . . . . . . . . . . . . . 21.1.2. Paradigmas de la programación . . . . . . . . . . . . . . . . . . . . . 2

1.1.2.1. Scala. Un lenguaje multiparadigma . . . . . . . . . . . . . . 31.1.3. Preparación del sistema . . . . . . . . . . . . . . . . . . . . . . . . . . 3

1.1.3.1. Descargar Scala . . . . . . . . . . . . . . . . . . . . . . . . 31.1.3.2. Herramientas de Scala . . . . . . . . . . . . . . . . . . . . . 3

El compilador de Scala: scalac . . . . . . . . . . . . . . . . . . 3El intérprete de código: scala . . . . . . . . . . . . . . . . . . . 4Scala como lenguaje compilado . . . . . . . . . . . . . . . . . 4Scala como lenguaje interpretado desde un script . . . . . . . . 5Scala como lenguaje interpretado desde un intérprete . . . . . . 5

1.2. Conceptos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61.2.1. Elementos de un lenguaje de programación . . . . . . . . . . . . . . . 61.2.2. Elementos básicos en Scala . . . . . . . . . . . . . . . . . . . . . . . 6

1.2.2.1. Tipos de datos básicos en Scala . . . . . . . . . . . . . . . . 61.2.2.2. Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

Operadores de igualdad. . . . . . . . . . . . . . . . . . . . . . 151.2.2.3. Nombrar expresiones . . . . . . . . . . . . . . . . . . . . . 151.2.2.4. Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

1.2.3. Uso del carácter punto y coma (;) en Scala . . . . . . . . . . . . . . . . 171.3. Bloques en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

1.3.1. Visibilidad y bloques en Scala . . . . . . . . . . . . . . . . . . . . . . 181.4. Evaluación en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

1.4.1. Evaluación de expresiones . . . . . . . . . . . . . . . . . . . . . . . . 181.4.2. Evaluación de funciones . . . . . . . . . . . . . . . . . . . . . . . . . 191.4.3. Sistema de Evaluación de Scala . . . . . . . . . . . . . . . . . . . . . 19

1.4.3.1. Valores de las definiciones . . . . . . . . . . . . . . . . . . . 191.4.3.2. Evaluación de Booleanos . . . . . . . . . . . . . . . . . . . 19

1.4.4. Ámbito y visibilidad de las variables . . . . . . . . . . . . . . . . . . . 201.5. Estructuras de control en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . 21

1.5.1. Estructuras condicionales . . . . . . . . . . . . . . . . . . . . . . . . . 211.5.1.1. La sentencia if . . . . . . . . . . . . . . . . . . . . . . . . . 21

La sentencia if / else . . . . . . . . . . . . . . . . . . . . . . . 22La sentencia if. . . else if . . . else . . . . . . . . . . . . . . . . . 22Estructuras condicionales anidadas . . . . . . . . . . . . . . . . 23

1.5.2. Estructuras iterativas . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

Página III

Page 12: Programación Funcional en Scala

IV ÍNDICE GENERAL

1.5.2.1. Bucles while . . . . . . . . . . . . . . . . . . . . . . . . . . 241.5.2.2. Bucles do. . . while . . . . . . . . . . . . . . . . . . . . . . . 241.5.2.3. Bucles for . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

Bucles for con rangos . . . . . . . . . . . . . . . . . . . . . . . 25Bucles for con colecciones . . . . . . . . . . . . . . . . . . . . 26Bucles for con filtros . . . . . . . . . . . . . . . . . . . . . . . 27Bucles for con yield. . . . . . . . . . . . . . . . . . . . . . . . 28

1.6. Interacción con Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281.6.1. Ejecución sobre la JVM . . . . . . . . . . . . . . . . . . . . . . . . . 29

1.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

2. Programación Orientada a Objetos en Scala 312.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 . . . 312.1.2. Scala como lenguaje orientado a objetos . . . . . . . . . . . . . . . . . 31

2.2. Paquetes, clases, objetos y namespaces . . . . . . . . . . . . . . . . . . . . . . 322.2.1. Objetos Singleton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322.2.2. Módulos, objetos, paquetes y namespaces . . . . . . . . . . . . . . . . 322.2.3. Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332.2.4. Objetos funcionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

2.2.4.1. Constructores . . . . . . . . . . . . . . . . . . . . . . . . . 352.2.4.2. Sobrescritura de métodos . . . . . . . . . . . . . . . . . . . 352.2.4.3. Precondiciones . . . . . . . . . . . . . . . . . . . . . . . . . 352.2.4.4. Atributos y Métodos . . . . . . . . . . . . . . . . . . . . . . 362.2.4.5. Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

2.3. Jerarquía de clases en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372.3.1. Herencia en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

2.3.1.1. Rasgos y herencia múltiple en Scala . . . . . . . . . . . . . 392.3.1.2. Funcionamiento de los rasgos . . . . . . . . . . . . . . . . . 392.3.1.3. Rasgos como modificaciones apiladas . . . . . . . . . . . . 402.3.1.4. ¿Cuándo usar rasgos? . . . . . . . . . . . . . . . . . . . . . 42

2.4. Patrones y clases case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422.4.1. Clases case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422.4.2. Patrones: estructuras y tipos . . . . . . . . . . . . . . . . . . . . . . . 43

2.4.2.1. Patrones comodín . . . . . . . . . . . . . . . . . . . . . . . 432.4.2.2. Patrones constantes . . . . . . . . . . . . . . . . . . . . . . 432.4.2.3. Patrones variables . . . . . . . . . . . . . . . . . . . . . . . 442.4.2.4. Patrones constructores . . . . . . . . . . . . . . . . . . . . . 442.4.2.5. Patrones de secuencia . . . . . . . . . . . . . . . . . . . . . 442.4.2.6. Patrones tipados . . . . . . . . . . . . . . . . . . . . . . . . 45

2.5. Polimorfismo en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452.5.1. Acotación de tipos y varianza . . . . . . . . . . . . . . . . . . . . . . 47

2.5.1.1. Acotación de tipos . . . . . . . . . . . . . . . . . . . . . . . 472.5.1.2. Varianza . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

Page 13: Programación Funcional en Scala

ÍNDICE GENERAL V

3. Programación Funcional en Scala 533.1. Introducción a la programación funcional . . . . . . . . . . . . . . . . . . . . 53

3.1.1. Características de los Lenguajes de Programación Funcionales . . . . . 533.1.2. Scala como lenguaje funcional . . . . . . . . . . . . . . . . . . . . . . 543.1.3. ¿Por qué la programación funcional? . . . . . . . . . . . . . . . . . . 54

3.2. Sentido estricto y amplio de la programación funcional . . . . . . . . . . . . . 543.2.1. ¿Qué son las funciones puras? . . . . . . . . . . . . . . . . . . . . . . 55

3.3. Funciones y cierres en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . 553.3.1. Definición de funciones . . . . . . . . . . . . . . . . . . . . . . . . . 553.3.2. Funciones anidadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 563.3.3. Diferencias entre métodos y funciones . . . . . . . . . . . . . . . . . . 573.3.4. Funciones de primera clase . . . . . . . . . . . . . . . . . . . . . . . . 583.3.5. Funciones anónimas y funciones valor . . . . . . . . . . . . . . . . . . 583.3.6. Cierres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

3.4. Recursión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 593.4.1. Importancia de la pila del sistema en recursión. . . . . . . . . . . . . . 60

3.4.1.1. La pila de Java . . . . . . . . . . . . . . . . . . . . . . . . . 613.4.1.2. Contexto de pila . . . . . . . . . . . . . . . . . . . . . . . . 61

3.4.2. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 623.5. Currificación y Parcialización . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

3.5.1. Currificacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633.5.2. Parcialización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64

3.6. Orden Superior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 653.6.1. Funciones de orden superior . . . . . . . . . . . . . . . . . . . . . . . 65

3.7. Funciones polimórficas. Genericidad . . . . . . . . . . . . . . . . . . . . . . . 663.8. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68

3.8.1. Ejercicio Resuelto . . . . . . . . . . . . . . . . . . . . . . . . . . . . 683.8.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71

3.9. Programación funcional estricta y perezosa . . . . . . . . . . . . . . . . . . . 763.9.1. Funciones estrictas y no estrictas . . . . . . . . . . . . . . . . . . . . . 76

3.10. Estructuras de Datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 783.10.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

3.10.1.1. ¿Qué es una teoría?. Definición de Estructuras de Datos . . . 783.10.1.2. La abstracción en la programación . . . . . . . . . . . . . . 783.10.1.3. Datos, Tipos de Datos, Estructuras de Datos y Tipos Abstrac-

tos de Datos . . . . . . . . . . . . . . . . . . . . . . . . . . 793.10.2. Definición de Estructuras de Datos en Lenguajes Funcionales . . . . . 80

3.10.2.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . 803.10.2.2. Definición . . . . . . . . . . . . . . . . . . . . . . . . . . . 813.10.2.3. Los Naturales . . . . . . . . . . . . . . . . . . . . . . . . . 82

Ejercicios resueltos. . . . . . . . . . . . . . . . . . . . . . . . 863.10.3. Estructuras de datos lineales. Listas . . . . . . . . . . . . . . . . . . . 89

3.10.3.1. TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . 893.10.3.2. Ejercicios sobre el TAD Lista . . . . . . . . . . . . . . . . . 94

3.10.4. Estructuras de datos no lineales . . . . . . . . . . . . . . . . . . . . . 953.10.4.1. Árboles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 953.10.4.2. Arboles Binarios . . . . . . . . . . . . . . . . . . . . . . . . 973.10.4.3. Arboles Binarios de Búsqueda . . . . . . . . . . . . . . . . 99

Page 14: Programación Funcional en Scala

VI ÍNDICE GENERAL

3.10.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1013.11. Colecciones en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102

3.11.1. El paquete scala.collection . . . . . . . . . . . . . . . . . . . . . . . . 1033.11.2. Iteradores en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

3.11.2.1. Métodos definidos para el tipo Iterator en Scala. . . . . . . . 1053.11.3. Colecciones inmutables . . . . . . . . . . . . . . . . . . . . . . . . . 107

3.11.3.1. Definición de rangos en Scala. La clase Range. . . . . . . . . 107Métodos definidos para el tipo Range en Scala. . . . . . . . . . 108

3.11.3.2. Definición de tuplas en Scala. La clase Tuple . . . . . . . . . 1083.11.3.3. Listas en Scala. La clase List . . . . . . . . . . . . . . . . . 110

Métodos definidos para el tipo List en Scala. . . . . . . . . . . 112Ejercicios sobre listas . . . . . . . . . . . . . . . . . . . . . . . 112

3.11.3.4. Vectores en Scala. La clase Vector . . . . . . . . . . . . . . 1143.11.3.5. Flujos en Scala. La clase Stream . . . . . . . . . . . . . . . 1153.11.3.6. Conjuntos en Scala. La clase Set . . . . . . . . . . . . . . . 116

Recorriendo conjuntos . . . . . . . . . . . . . . . . . . . . . . 117Métodos definidos para el tipo Set en Scala. . . . . . . . . . . . 118

3.11.3.7. Asociaciones en Scala. La clase Map . . . . . . . . . . . . . 119Métodos definidos para el tipo Map en Scala. . . . . . . . . . . 120

3.11.3.8. Selección de una colección . . . . . . . . . . . . . . . . . . 1203.11.3.9. Colecciones como funciones . . . . . . . . . . . . . . . . . 121

3.11.4. Expresiones for como una combinación elegante de funciones de ordensuperior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1223.11.4.1. Traducción de expresiones for con un generador . . . . . . . 1223.11.4.2. Traducción de expresiones for con un generador y un filtro . 1233.11.4.3. Traducción de expresiones for con dos generadores . . . . . 1233.11.4.4. Traducción de bucles for . . . . . . . . . . . . . . . . . . . 1243.11.4.5. Definición de map, flatMap y filter con expresiones for . . . 1253.11.4.6. Uso generalizado de for en estructuras de datos . . . . . . . 125

3.11.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126

4. Programación Funcional Avanzada en Scala 1294.1. Implícitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

4.1.1. Parámetros ímplicitos en funciones . . . . . . . . . . . . . . . . . . . 1294.1.2. Clases implícitas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

4.2. Tipos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1314.2.1. Definición de tipos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1324.2.2. Parámetros de tipo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132

4.2.2.1. Nombres de los parámetros de tipo. . . . . . . . . . . . . . . 1324.2.3. Constructores de tipos. . . . . . . . . . . . . . . . . . . . . . . . . . . 1334.2.4. Tipos compuestos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1334.2.5. Tipos estructurales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1334.2.6. Tipos de orden superior. . . . . . . . . . . . . . . . . . . . . . . . . . 1344.2.7. Tipos existenciales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

4.3. Teoría de categorías . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1364.3.1. El patrón funcional Funtor . . . . . . . . . . . . . . . . . . . . . . . . 1374.3.2. El patrón funcional Mónada . . . . . . . . . . . . . . . . . . . . . . . 138

4.3.2.1. Reglas que deben satisfacer las mónadas . . . . . . . . . . . 139

Page 15: Programación Funcional en Scala

Í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 . . . . . . . . . . . . . . . . . . . . . . 1414.3.2.4. La importancia de las mónadas . . . . . . . . . . . . . . . . 1424.3.2.5. La mónada Identidad . . . . . . . . . . . . . . . . . . . . . 1424.3.2.6. Envolviendo el contexto con mónadas. La clase monádica Try 143

La mónada Try. . . . . . . . . . . . . . . . . . . . . . . . . . . 1464.4. Manejo de errores sin usar excepciones . . . . . . . . . . . . . . . . . . . . . 149

4.4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1494.4.2. Tipo de datos Option . . . . . . . . . . . . . . . . . . . . . . . . . . . 1504.4.3. Tipo de datos Either . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

4.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

5. Tests en Scala 1555.1. Afirmaciones Asserts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

5.1.1. Assert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1555.1.2. Ensuring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157

5.2. Herramientas específicas para tests . . . . . . . . . . . . . . . . . . . . . . . . 1595.2.1. ScalaTest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

5.3. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162

6. Concurrencia en Scala. Modelo de actores 1656.1. Programación Concurrente. Problemática . . . . . . . . . . . . . . . . . . . . 165

6.1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1656.1.1.1. Sistema Reactivo Vs Sistema Transformacional . . . . . . . 165

6.1.2. Speed-Up en programación concurrente . . . . . . . . . . . . . . . . . 1666.1.3. Problemática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166

6.1.3.1. Propiedades de los programas concurrentes . . . . . . . . . . 1666.1.3.2. Bloqueos y secciones críticas . . . . . . . . . . . . . . . . . 167

Problemas del uso de bloqueos . . . . . . . . . . . . . . . . . . 1676.1.3.3. Concurrencia en Java . . . . . . . . . . . . . . . . . . . . . 167

6.2. Modelo de actores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1686.2.1. Origen del Modelo de Actores . . . . . . . . . . . . . . . . . . . . . . 1686.2.2. Filosofía del Modelo de Actores . . . . . . . . . . . . . . . . . . . . . 169

6.3. Actores en Scala. Librería scala.actors . . . . . . . . . . . . . . . . . . . . . . 1706.3.1. Definición de actores . . . . . . . . . . . . . . . . . . . . . . . . . . . 1706.3.2. Estado de los actores . . . . . . . . . . . . . . . . . . . . . . . . . . . 1716.3.3. Mejora del rendimiento con react . . . . . . . . . . . . . . . . . . . . 173

6.4. Actores en Scala con Akka . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1756.4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175

6.4.1.1. Diferencias entre Akka y la librería Actors de Scala. . . . . . 1756.4.2. Definición y estado de los actores . . . . . . . . . . . . . . . . . . . . 177

6.5. Buenas prácticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1816.5.1. Ausencia de bloqueos . . . . . . . . . . . . . . . . . . . . . . . . . . 1826.5.2. Comunicación exclusiva mediante mensajes . . . . . . . . . . . . . . . 1826.5.3. Mensajes inmutables . . . . . . . . . . . . . . . . . . . . . . . . . . . 1826.5.4. Mensajes autocontenidos . . . . . . . . . . . . . . . . . . . . . . . . . 182

6.6. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183

Page 16: Programación Funcional en Scala

VIII ÍNDICE GENERAL

7. Conclusiones 185

8. Solución a los ejercicios propuestos 1898.1. Evaluación en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1898.2. Introducción a la Programación Funcional . . . . . . . . . . . . . . . . . . . . 1898.3. Estructuras de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193

8.3.1. TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1938.3.2. TAD Arbol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196

8.4. Colecciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1978.4.1. Tipo List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1978.4.2. Otras colecciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200

8.5. Programación Funcional Avanzada . . . . . . . . . . . . . . . . . . . . . . . . 2048.6. Tests en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2068.7. Concurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208

Lista de tablas 210

Listados de algoritmos 211

Referencias 219

Glosario 221

Acrónimos 225

Page 17: Programación Funcional en Scala

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 expresarpatrones comunes de programación de forma concisa, elegante y con tipos estáticos. Integraelegantemente características de lenguajes funcionales y orientados a objetos, lo cual hace quela 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 ÉcolePolytechnique Fédérale de Lausanne (EPFL).

Scala hizo su aparición pública sobre la plataforma Máquina Virtual de Java (JVM) en enerode 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 lenguajesde programación, ha adquirido una notable popularidad y las ventajas que ofrece han hecho yaque 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 decomputació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 loslenguajes de programación que se utilizan para desarrollar aplicaciones para los dispositivosAndroid.

“ Si le das a una persona un pescado, comerá un día. Si enseñas a pescar a unapersona, comerá toda su vida. Si das herramientas a una persona, puede construirse

1Martin Odersky es profesor de la EPFL en Lausanne, Suiza y ha trabajado en los lenguajes de programacióndurante la mayor parte de su carrera. Estudió programación estructurada y orientada a objetos como estudiantede doctorado de Niklaus Wirth. Después, mientras trabajaba en IBM y en la Universidad de Yale se enamoró dela programación funcional. Cuando apareció Java, comenzó a añadir construcciones de programación funcionalespara la nueva plataforma, lo cual dio lugar a Pizza y GJ y eventualmente a Java 5 con los genéricos. Durante esetiempo 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ónfuncional y programación orientada a objetos en el lenguaje Scala. Scala pasó rápidamente del laboratorio deinvestigación a convertirse en una herramienta de código abierto y en un lenguaje industrial. Odersky ahora es elencargado de supervisar el desarrollo de Scala como jefe del grupo de programación de la EPFL y como presidentede la empresa Typesafe.

Página 1

Page 18: Programación Funcional en Scala

una caña de pescar, ¡y muchas otras herramientas!. Incluso podrá construir unamá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 parahacer más herramientas del mismo tipo.

El diseño de un lenguaje de programación debe ser un patrón, un patrón decrecimiento, un patrón para desarrollar el patrón para la definición de patrones quelos 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 quepermita el crecimiento y la escalabilidad en función de las exigencias del desarrollador. Scalapuede ser utilizado como lenguaje de scripting, así como también se puede adoptar en el procesode 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ónfuncional han contribuido a que el lenguaje sea más escalable y haga de la escalabilidad una desus 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 Turingpor 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 cuellode 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 mantenidoatados al pensamiento de “una palabra a la vez” en lugar de fomentarnos el pensaren unidades conceptuales mayores. Entonces la programación es básicamente laplanificación del enorme tráfico de palabras que cruzan el cuello de botella de VonNeumann, y gran parte de ese tráfico no concierne a los propios datos, sino a dóndeencontrar é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” queotros lenguajes anteriores (como por ejemplo Fortran), pero internamente, esto sigue siendo

2El paradigma de la POO se considera ortogonal a los paradigmas de programación funcional, programaciónimperativa y programación lógica.

Página 2

Page 19: Programación Funcional en Scala

lo que hacen durante gran parte del tiempo los computadores, incluso los supercomputadoresaltamente paralelos3.[26]

Del cuello de botella intelectual criticado por Backus subyace la necesidad de disponer deotras 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 sedeberí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 instanciase conoce en tiempo de compilación, así como la disponibilidad de cualquier método de unainstancia 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 conjuntode mecanismos de resolución de problemas de modo que los desarrolladores puedan seleccionarla 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 dela instalación.

El compilador de Scala: scalac

El compilador scalac transforma un programa de Scala en archivos .class que podrán serutilizados por la JVM. El nombre del archivo no tiene que corresponder con el nombre de laclase.

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 usar3Un 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.4En 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

Page 20: Programación Funcional en Scala

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 unfichero 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á ylo 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 (unvector, array en inglés, de objetos del tipo String) como parámetro. El cuerpo de este métodopresenta una sola llamada al método predefinido println con el saludo como argumento. Elmé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. Estadeclaració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 unaclase llamada HolaMundo, como una instancia de esa clase también llamada HolaMundo. Estainstancia 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í porquelos miembros estáticos (métodos o campos) no existen en Scala. En vez de definir miembrosestáticos, en Scala se declararán estos miembros en un objeto singleton.

Compilando el ejemploPara 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 sonarchivos class para la JVM.

Página 4

Page 21: Programación Funcional en Scala

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 comandoscala, como mostramos en la siguiente sección.

Ejecutando el ejemploUna 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 mismasopciones. El ejemplo de arriba puede ser ejecutado utilizando el siguiente comando que, comose puede comprobar, produce la salida esperada:$ scala HolaMundoHola, mundo!

Scala como lenguaje interpretado desde un script

Como ya se ha introducido anteriormente, es posible ejecutar archivos fuente haciendo usodel 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 obtenidoes el esperado:$>scala hola.scalaHola mundo, desde un script!

Desde la línea de comandos se le pueden pasar argumentos a los scripts mediante el vectorde argumentos args. En Scala, el acceso a los elementos de un vector se realiza especificandoel í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 argumentosdesde 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 seaccede 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 igualque 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ónfuncional, por lo que se recomienda familiarizarse con el mismo.

5Notación que es homogénea para las demás estructuras, incluso las estructuras definidas por el usuario.

Página 5

Page 22: Programación Funcional en Scala

1.2. Conceptos básicos

1.2.1. Elementos de un lenguaje de programaciónUn 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 nospermitirá hacer referencia a la expresión.

1.2.2. Elementos básicos en Scala1.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 queocupa 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 lostipos de datos son clases. En la tabla 1.1 se muestran los tipos básicos en Scala.

Tipo de dato Tamaño Rango EjemploByte 8 bits con signo [−128, 127] 38Short 16 bits con signo [−32768, 32767] 23Int 32 bits con signo [−231, 231 − 1] 45Long 64 bits con signo [−263, 263 − 1] 3434115Float 32 bits con signo [−3,4028 ∗ 1038, 3,4028 ∗ 1038 1.38Double 64 bits con signo [−1,7977 ∗ 10308, 1,7977 ∗ 10308] 54.37Boolean true o false trueChar 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 juntocon 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 severá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 enterostambién podrán ser de tipo Long añadiendo el sufijo L o l al final del literal. A continuación semuestran algunos ejemplos:

Página 6

Page 23: Programación Funcional en Scala

scala> 5res0: Int = 5

scala> 777Lres3: Long = 777

scala> 0xFFAFAFA5res2: 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 yaquellos 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 Ay 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 of. En otro caso serán de tipo Double. Estos literales sí podrán contener uno o varios ceros comoprimeros dígitos.

scala> 0.0res0: Double = 0.0

scala> 01.2res1: Double = 1.2

scala> 01.2Fres2: Float = 1.2

scala> 00045.34res3: Double = 45.34

Literales lógicos

Los literales lógicos o booleanos son aquellos que pertenecen a la clase Boolean y que sólopueden 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 precedidasdel prefijo ’. La clase case Symbol está definida de la siguiente forma:

1 package scala2 final case class Symbol private (name: String) {3 override def toString: String = "’" + name4 }

Ejemplo:

6En versiones anteriores de Scala, cuando un entero comenzaba por cero era porque el número estaba en base8 y, por tanto, sólo podía estar seguido de los dígitos comprendidos entre 0 y 7.

Página 7

Page 24: Programación Funcional en Scala

scala> ’miSimbolores4: Symbol = ’miSimbolo

Literales de tipo carácter

Un literal de tipo carácter consiste en un carácter encerrado entre comillas simples querepresentará un carácter representable o un carácter de escape7. El tipo de datos de los literalesde tipo carácter es Char. El estándar de codificación de caracteres utilizado para representar loscaracteres es Unicode. También podremos indicar el código unicode del carácter que queramosrepresentar 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 entrecomillas dobles cuyo tipo de datos será String. Los caracteres que conformen la cadena decaracteres 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, barrainversa,. . . 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 losliterales de caracteres como en los literales de cadenas de caracteres.

7También conocidos como secuencia de escape

Página 8

Page 25: Programación Funcional en Scala

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ódigounicode entre 0 y 255, indicando el código en base octal. Esto se indicaba precediendo al códigooctal de \. Por ejemplo \150 representaría el carácter h. Esta representación está depreciada enla 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. Porejemplo: 1, 2.5, 3.74E10f, true, false, ’a’. . .

Un operador 8 junto con dos operandos (operadores binarios) o un operando (operadoresunarios) 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 valoresnuméricos de tipo entero. Con los tipos de datos Float y Double se representarán valores detipo real.

El tipo de datos devuelto por defecto al evaluar una expresión que represente un númeroentero es Int mientras que Double será el tipo de datos por defecto devuelto al evaluar unaexpresión que represente un número real.scala> 1.2res0: Double = 1.2

scala> 5res1: Int = 5

8Los operadores se verán en la Subsubsección 1.2.2.2: Operadores « página 10 »

Página 9

Page 26: Programación Funcional en Scala

Expresiones booleanas

Una expresión booleana puede estar compuesta por un literal, por uno de los operadoresló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ónbooleana 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. EnScala los operadores son en realidad métodos y, por tanto, se reducen a la llamada a un métodode un objeto de una de clases de los tipos de datos básicos. Es decir, 1 + 2 realmente invocaal método + del objeto 1 con el parámetro 2: (1).+(2). Es más, en la clase Int hay diferentesdefiniciones del método + (método sobrecargado) que difieren las unas de las otras en el tipodel parámetro con el que se invocan y que nos permiten realizar la suma de objetos de diferentestipos enteros. Por ejemplo, existe otro método + en la clase Int que recibe como parámetro unobjeto 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 invocarse ubica entre sus dos operandos. Un ejemplo de operador infijo podría ser el operadoraritmético + de los tipos numéricos.

Operadores prefijos. Son operadores unarios en los que el nombre del método se sitúadelante 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étodose 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 lenguajesde programación como Haskell, por lo que cualquier método puede ser un operador. Lo queconvertirá un método en un operador será la forma de usar el mismo. Por ejemplo, si se escribe1.+(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 datosse define un método unary_!, una instancia de este tipo de datos podrá invocar ! en notaciónprefija. Scala transforma las llamadas a operadores prefijos en llamadas al método unary_. Porejemplo, 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 esnecesario el uso de paréntesis11

9Los operadores postfijos tienen que ser habilitados -visibles- importando scala.language.postfixOps o indican-do al compilador la opción -language:postfixOps

10Excepto los operadores prefijos en los que los identificadores que pueden ser usados en este tipo de operadoresson +, -, ! y ~.

11Excepto si el método presenta efectos colaterales, como println(), en cuyo caso sí habrá que poner los parén-tesis.

Página 10

Page 27: Programación Funcional en Scala

Los operadores infijos son aquellos métodos que reciben un argumento. Los operadoresinfijos se ubican entre sus dos operandos (como el operador +, en el que el primer operandoserá 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 nosindicará 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 resultadode la anterior expresión es el mismo que si se evalúa la expresión 100 - (40 * 2). Si se quisieracambiar el orden de evaluación anterior se debería escribir la expresión: (100 - 40) * 2, cuyoresultado 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 operadoresse determinará según el primer carácter, tal como se muestra en la tabla 1.3, donde encontramosla 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ácterempleando según esta misma tabla.

Tipo Operador AsociatividadPostfijos (), [],. . . IzquierdaUnarios ! ˆ DerechaMultiplicativos * / % IzquierdaAditivos + - IzquierdaBinario : IzquierdaBinario = ! IzquierdaDesplazamiento >> >>> << IzquierdaRelación <><= >= IzquierdaIgualdad == != IzquierdaBit a bit AND & IzquierdaBit a bit XOR ˆ IzquierdaBit a bit OR | IzquierdaAND lógico && IzquierdaOR lógico || IzquierdaTodas las letras IzquierdaAsignación = += -= *= /= %= >>= <<= &= ˆ= |= DerechaComa , 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ómose agrupan los operadores y, por tanto, como se evaluará la expresión. La asociatividad de unoperador en Scala se determina por el último carácter del operador. Si el último carácter de

Página 11

Page 28: Programación Funcional en Scala

un operador binario es :, el operador tendrá asociatividad derecha, lo que quiere decir quelos métodos que terminen en : serán invocados por el operando situado en el lado derecho deloperador y se les pasará como parámetro el operando izquierdo. Es decir, si se tiene la expresiónx /: y, será equivalente a y./:(x). En cualquier otro caso, el operador presentará asociatividadizquierda.

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á eloperando 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. Silos métodos terminan en ’:’, se agruparán de derecha a izquierda, mientras que en cualquier otrocaso se agruparán de izquierda a derecha. Si se tienen las expresiones a * b * c y x /: y /: z, seevaluarán como (a * b) * c y x /: (y /: z), respectivamente. A pesar de conocer las reglas quedeterminan la prioridad de los operadores y la asociatividad de los mismos, es aconsejable usarparé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, esdecir, con objetos de tipo Short, Int, Double o Long. En Scala se pueden distinguir dos tipos deoperadores aritméticos: unarios y binarios. En la tabla 1.4 se muestran los diferentes operadoresaritméticos presentes en Scala.

Descripción Operador TipoSigno positivo + unarioSigno negativo - unarioSuma + binarioResta - binarioDivisión / binarioProducto * binarioResto % binario

Tabla 1.4: Operadores aritméticos en Scala

Todos los operadores admiten expresiones enteras y reales. En el caso de los operadoresbinarios, si los dos operandos son enteros o reales el resultado de la operación será un valorentero o real respectivamente. Si uno de sus operandos es entero y el otro real entonces elresultado devuelto será de tipo real.

Los operadores aritméticos también servirán para combinar expresiones de tipo carácter. Eneste caso, Scala tomará el valor decimal del código unicode que represente el carácter de cadauno de los operadores.scala> 1.5 * 3res2: Double = 4.5

scala> 5 * 3res3: Int = 15

Página 12

Page 29: Programación Funcional en Scala

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 operadoresrelacionales que se muestran en la tabla 1.5. Los operadores relacionales son binarios.

Descripción OperadorMenor <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 * 12res4: Boolean = true

scala> 3.0f < 7res5: Boolean = true

scala> "cadena" == "cadena"res6: Boolean = true

En cambio, si lo que pretende es comparar dos expresiones booleanas se deberá tener encuenta 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 cuentaque se basan en el valor decimal del código unicode de cada carácter12. En el caso de expresionesdel tipo de datos String, cuando los caracteres situados en la primera posición de las cadenas quese estén comparando sean iguales (mismo código unicode) se comparará el segundo carácter deambas 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 seamenor se termine, en cuyo caso la cadena de menor longitud será menor que la cadena de mayorlongitud. A continuación se muestran unos ejemplos en el intérprete de Scala que pueden ayudara 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

12En Scala, una expresión de tipo String y otra de tipo Char no se pueden comparar.

Página 13

Page 30: Programación Funcional en Scala

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 tiposreferenciados (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ógicasson 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 TipoAND lógico && binarioOR lógico || binarioNOT 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 unode los bits de los operadores. Las tablas de verdad para los operadores &, | y ˆ son las que semuestran en la tabla 1.7

p q p& q p ˆ q p | q0 0 0 0 00 1 0 1 11 0 1 1 01 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

Page 31: Programación Funcional en Scala

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 = 5scale: Intscala> 7 * scaleres4: Int = 35scala> def pi = 3.141592653589793pi: Doublescala> def radius = 10radius: Intscala> 2 * pi * radiusres5: 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/0r: Int

scala> rjava.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 quese evalúa por primera vez r.

Es lo contrario que la forma especial define de Scheme, que se utiliza para asociar nombrescon expresiones, y en primer lugar se evalúa la expresión. En realidad, nombrar una expresiónsólo aportará azúcar sintáctico, permitiendo crear funciones y evitando escribir la expresiónlambda 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 ovar.

1.2.2.4. Variables

Las variables están encapsuladas en objetos, es decir, no pueden existir por si mismas. Lasvariables son referencias a instancias de clases.

Página 15

Page 32: Programación Funcional en Scala

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, unafunció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, mientrasque se utiliza var para indicar que sí puede reasignarse. La variable val es similar a una variablefinal en Java: una vez inicializada, no se podrá reasignar. Una variable var, por el contrario, sepuede 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 compiladorde Scala tiene inferencia de tipos. Por ejemplo, las siguientes definiciones son equivalentes:scala> var x : Int = 1scala> 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 inicializadola variable msg con una cadena, Scala asocia el tipo de msg al tipo de datos String. Si se intentamodificar 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 valmsg = "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 recomiendaencarecidamente usar val cuando la variable no vaya a mutar.

Página 16

Page 33: Programación Funcional en Scala

1.2.3. Uso del carácter punto y coma (;) en ScalaEn 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 seaobligatorio. 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 escribirexpresiones 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 comouna ú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 queno 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ónno 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, salvoque 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 nopueden contener múltiples sentencias

Página 17

Page 34: Programación Funcional en Scala

1.3. Bloques en ScalaUn 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 quedefinirá 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 encualquier lugar en el que pueda aparecer una expresión. Ejemplo:

1 { val x = f(3)2 x * x3 }

1.3.1. Visibilidad y bloques en ScalaScala 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 definicionesrealizadas fuera del bloque que tengan el mismo nombre.

Las definiciones realizadas en bloques externos estarán visibles dentro de un bloque siemprey cuando no sean ocultadas por otras definiciones con el mismo nombre en el bloque.

Ejemplo:

1 val x = 102 def f(y: Int)=y+13 val result = {4 val x = f(3)5 x * x6 } + 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 expresionesPara 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 lamisma 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) ∗ radius6,28318 ∗ radius

Página 18

Page 35: Programación Funcional en Scala

6,28318 ∗ 1062,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 ideagira en torno a que lo que hace una evaluación es reducir una expresión a su valor y puede seraplicado a todas las expresiones (siempre y cuando no tengan efectos laterales). El modelo desustitución está formalizado en el λ-cálculo. Esta estrategia de evaluación es conocida comoevaluación estricta o Call by Value.

Existe otra alternativa: no evaluar los argumentos antes de reemplazar la función por ladefinició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 sedesea que algún argumento de una función use evaluación no estricta. Para esto último, sepondrá => 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 pornombre

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

Page 36: Programación Funcional en Scala

!true → false!false → true

true && e → efalse && e→ false

true || e → truefalse || 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 derechosea evaluado. En estos casos, se dirá que estas expresiones usan una evaluación en cortocir-cuito.

1.4.4. Ámbito y visibilidad de las variablesEl 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 lafunció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 quese invoca.

Los argumentos de la función son variables no mutables locales de este nuevo ámbito quequedan 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 = 53 x+y+z4 }5 def g(z: Int): Int = {6 val x = 107 z+x8 }9 f(g(3),g(5))

Se definen dos funciones f y g, dentro de cada una de las cuales hay declaradas distintasvariables locales. Las funciones f y g devuelven una suma de los parámetros con la variablelocal. En la última línea de código se realiza una invocación a f con los resultados devueltos pordos 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 quese evalúa la función. Las invocaciones devuelven 13 y 15, respectivamente.

Página 20

Page 37: Programación Funcional en Scala

2. Después se realiza la invocación a f con esos valores 13 y 15. Esta invocación vuelve acrear 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 funcionesanónimas durante la ejecución de otras funciones, lo cual permitirá la definición de cierres.

1.5. Estructuras de control en ScalaLas 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 decontrol de flujo se emplean en los programas para ejecutar sentencias condicionalmente, repetirun conjunto de sentencias o, en general, cambiar el flujo secuencial de ejecución. Las estructurasde 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 utilizanen Scala están separadas por el carácter punto y coma (véase el uso del carácter punto y comaen Scala en la Subsección 1.2.3: Uso del carácter punto y coma (;) en Scala « página 17 »). Eluso 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 quese ejecutan haciendo uso de estructuras condicionales e iterativas.

1.5.1. Estructuras condicionalesSe 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. Encaso 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 la3 // expresion booleana se evalua a true4 }

Algoritmo 1.3: Sintaxis sentencia if

Ejemplo:

13Cuando se escribe un programa, se introduce la secuencia de sentencias dentro de un archivo. Sin sentenciasde control del flujo, el intérprete ejecuta las sentencias conforme aparecen en el programa de principio a fin.

Página 21

Page 38: Programación Funcional en Scala

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 laexpresión booleana del if sea falsa. Scala tiene la expresión condicional if-else para expresar laelección entre dos alternativas (parecida a if-else de Java pero usada para expresiones, no parainstrucciones).

La sintaxis de una sentencia if / else es:

1 if (expresion_booleana) {2 // Sentencias que se ejecutaran si la3 // expresion booleana se evalua a true4 } else {5 // Sentencias que se ejecutaran si la6 // expresion booleana se evalua a false7 }

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 ayudapara 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 algunospuntos importantes:

Una sentencia if podrá tener cero o una declaración else, la cual estará declarada siempreal 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 otrasexpresiones 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 muestrala sintaxis de una sentencia if / else if / else.

Página 22

Page 39: Programación Funcional en Scala

1 if (expresion_booleana1) {2 // Sentencias que se ejecutaran si la3 // expresion booleana 1 se evalua a true4 } else if (expresion_boolena2){5 // Sentencias que se ejecutaran si la6 // expresion booleana 2 se evalua a true7 } else if (expresion_boolena3){8 // Sentencias que se ejecutaran si la9 // expresion booleana 3 se evalua a true

10 }11 else {12 // Sentencias que se ejecutaran si ninguna13 // de las anteriores expresiones booleanas14 // se evaluan a true15 }

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 deejecutar algunas sentencias dispuestas en un programa, en función del resultado de la evaluaciónde una expresión booleana. Pero el flujo del programa, en cualquier caso, siempre avanza haciaadelante y nunca se vuelve a ejecutar una sentencia ejecutada anteriormente.

Las sentencias iterativas permitirán iterar un bloque de sentencias, es decir, ejecutar unbloque de sentencias mientras la condición especificada sea cierta. A este tipo de sentencias seles 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 darrespuesta a las necesidades de iteración de los programadores.

Página 23

Page 40: Programación Funcional en Scala

Bucle Descripciónwhile 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 bucledo. . . 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 irencerrada 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 realizaantes de que se ejecute el cuerpo del bucle, por lo que si la condición se evalúa a falsa, el cuerpodel bucle no se ejecutaría ninguna vez.

La sintaxis de un bucle while es:

1 while (condicion) {2 // Sentencias que se ejecutaran mientras3 // la condicion sea cierta4 }

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 formala condición de control del bucle.

Ejemplo:

1 def printUntil(x: Int) = {2 //variable local3 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 hastaque la condición de control del bucle sea falsa. Por tanto, al igual que en el bucle while, el cuerpodel bucle se ejecuta mientras la expresión lógica sea cierta. Los bucles do. . . while también sedenominan post-prueba ya que, a diferencia de los bucles while, los bucles do. . . while evalúan

Página 24

Page 41: Programación Funcional en Scala

la condición después de ejecutar el cuerpo del bucle, motivo por el cual este tipo de buclesgarantizan 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 mientras3 // la condicion sea cierta4 } 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 queel cuerpo del bucle se ejecutará al menos una vez. Si la condición se evalúa a cierta, el flujode control saltará hasta la declaración do y el cuerpo del bucle se ejecutará nuevamente. Esteproceso se repetirá hasta que la condición se evalúe a falsa.

Ejemplo:

1 def printUntil2(x: Int) = {2 //variable local3 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 siguientesvariantes 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 especificidadde 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 cualse 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 definidoen 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

Page 42: Programación Funcional en Scala

1 for (a <- Range) {2 // Sentencias que se ejecutaran tantas veces3 // como valores contenga el tipo de datos4 // Range dado5 }

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 degenerar 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 local3 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 elcarácter punto y coma (;). En este caso el bucle iterará sobre todas las posibles combinacionesde 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: 4Valor x: 1 Valor y: 5Valor x: 1 Valor y: 6Valor x: 2 Valor y: 4Valor x: 2 Valor y: 5Valor x: 2 Valor y: 6Valor x: 3 Valor y: 4Valor x: 3 Valor y: 5Valor 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 veces3 // como valores contenga el tipo de datos4 // Collection dado5 }

Algoritmo 1.15: Sintaxis bucles for con colecciones

Se puede iterar sobre los elementos de las distintas estructuras de datos definidas en lalibrería Scala.collection de Scala como listas, conjuntos,etc., así como sobre los tipos de datos

Página 26

Page 43: Programación Funcional en Scala

creados por el usuario14

La variable de control de bucle (a) se inicializará con el primer valor de la colección eirá tomando el valor de los distintos elementos que componen la colección en las sucesivasiteraciones 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ónde números. Se ha creado esta colección usando el tipo de datos List de Scala. Las coleccionesen Scala se estudiarán en mayor profundidad en la Sección 3.11: Colecciones en Scala « página102 ».

Bucles for con filtros

Los bucles for en Scala permiten emplear filtros para descartar la iteración sobre algunoselementos de la colección que se desea iterar (rangos, listas, conjuntos,. . . ) que no cumplan conalguna 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 veces4 // como valores del tipo de datos5 // Collection|Range dado satisfagan los filtros6 }

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/rangoque satisfaga las condiciones: condicion1 y condicion2, e irá tomando el valor de los distintoselementos que componen la colección y que también satisfagan las condiciones impuestas comofiltros.

1 def forListaconFiltros = {2 val numList = List(0,1,2,3,4,5,6,7,8,9,10);3 for( a <- numList4 if a <= 5;5 if a != 3 ){6 println( "Valor de a: " + a );7 }8 }

Algoritmo 1.18: Ejemplo bucle for con filtros

14Estos 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

Page 44: Programación Funcional en Scala

En el ejemplo del algoritmo 1.18 se recorren los elementos de la lista numList que seanmenores 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 oque éstos sean el valor devuelto por una función. Para hacer esto se añadirá la palabra reservadayield 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 veces4 // como valores del tipo de datos5 // Collection dado satisfagan los filtros6 }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 queestamos iterando con los valores recordados. En los bucles for con yield se podrá también haceruso 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 <- numList4 if a <= 5;5 if a != 3 )yield a * 26 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 eldoble de los números de la lista numList que satisfagan los filtros del bucle. El segundo buclefor iterará sobre la lista resultante del primer bucle, imprimiendo los valores por pantalla.

1.6. Interacción con JavaUna 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 lainteractuación con código Java.

Como se verá a lo largo de los próximos capítulos, Scala es un lenguaje de programaciónque ha sabido integrar algunas de las principales características presentes en los lenguajes deprogramación más populares. Java no es la excepción, y comparte muchas cosas con éste. Ladiferencia que se puede ver es que para cada uno de los conceptos de Java, Scala los aumenta,

15Como 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

Page 45: Programación Funcional en Scala

refina y mejora. Poder aprender todas las características de Scala nos equipa con más y mejoresherramientas a la hora de escribir nuestros programas.

También es posible heredar de clases Java e implementar interfaces Java directamente enScala.

A continuación se presenta un ejemplo que demuestra esto. Se quiere obtener y formatearla 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 estasclases equivalentes en las librerías de Scala, se pueden simplemente importar las clases de loscorrespondientes 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 Date6 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 mismopaquete al encerrarlas entre llaves, como se muestra en la primer linea. Otra diferencia es quese 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 (porejemplo se puede nombrar a un método).

Por tanto, la declaración import en la segunda línea, importa todos los miembros de la claseDateFormat. Esto hace que el método estático getDateInstance y el campo estático LONG seandirectamente 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étodoestático getDateInstance que ha sido importado previamente. Finalmente, se imprime la fechaactual formateada de acuerdo a la instancia de DateFormat que fue “localizada”. Esta últimalínea muestra un ejemplo de un método que toma un solo argumento y que ha sido utilizadocomo operador infijo (véase la Subsubsección 1.2.2.2: Operadores « página 10 »). Es decir, laexpresió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 JVMUna 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

Page 46: Programación Funcional en Scala

su ecosistema (herramientas, librerías, IDE,. . . ), por lo que no será necesario desprenderse detodas las inversiones hechas en el pasado.

El compilador de Scala genera bytecode, siendo indistinguible a este nivel el código escritoen Java y el escrito en Scala. Adicionalmente, puesto que se ejecuta sobre la JVM, se beneficiadel rendimiento y estabilidad de dicha plataforma. Y siendo un lenguaje de tipado estático, losprogramas 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 ciertascaracterísticas, como la recursividad de cola, no están implementadas en la JVM y hay quesimularlas.

1.7. EjerciciosEjercicio 1. Supongamos el siguiente código en Scala:

1 val x = 102 val y = 203 def g(y: Int): Int = {4 x+y5 }6 def prueba(z: Int): Int = {7 val x = 08 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ónx+y ? ¿Qué valores tienen esas variables en el momento de la evaluación?

Página 30

Page 47: Programación Funcional en Scala

Capítulo 2

Programación Orientada a Objetos enScala

2.1. Introducción a la programación orientada a objetos enScala

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 paradigmaampliamente 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 sinos 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 modelopuro de orientación a objetos muy similar al presentado por Smalltalk (lenguaje creado por AlanKay sobre el año 1980)1.

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

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

Página 31

Page 48: Programación Funcional en Scala

Composición modular de los elementos mezclados (mixin). Mecanismo que permitela 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 clasesabstractas. Por una parte se pueden definir múltiples “contratos”(del mismo modo que losinterfaces). 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 ambosestilos 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 lugarel concepto de objeto singleton. La definición de objetos de este tipo es muy similar a la delas clases, salvo que se utiliza la palabra reservada object en lugar de class. Cuando un objetosingleton 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 (claseacompañ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íandefinir 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 palabrareservada new), mientras que las clases sí lo permiten. Cada uno de los objetos singleton esimplementado mediante una instancia de una clase synthetic referenciada desde una variableestática, por lo que presentan la misma semántica de inicialización que los estáticos de Java. Unobjeto 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étodonombreMétodo habrá sido definido dentro del objeto nombreObjeto.

En realidad, en Scala todos los valores serán objetos. El principal objetivo de un objeto esel 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 reservadasval o object2

Para hacer referencia a los miembros de un objeto en Scala se utilizará la notación típica dela POO (se indicará el namespace del objeto seguido de un puto y del nombre del miembro).Ejemplo: MyModule.abs(42)

2Los objetos también pueden contener otros tipos de objetos, que no se mencionarán en este capítulo.

Página 32

Page 49: Programación Funcional en Scala

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 argumentopueden ser usados con una sintaxis de infijo3. De este modo, se puede comprobar como lallamada 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: importMyModule._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 tantocon 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. ClasesDel 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 ladefinición de la clase MiPrimeraClase.

1 class MiPrimeraClase{2 val a = 13 }

Algoritmo 2.1: Mi primera clase

Si se quiere instanciar un objeto de la clase anterior habrá que hacer uso de la palabrareservada 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 tipoes 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 conesto 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 esla afirmación de que su conjunto de atributos (variables de instancia) permanece constante alo largo de todo el ciclo de vida del mismo. El primer paso para evitar que agentes externostengan acceso a los campos de una clase es declarar los mismos como private. Puesto quelos campos privados sólo podrán ser accedidos desde métodos que se encuentran definidos enla misma clase, todo el código que podría modificar el estado del mismo estará localizado endicha clase.5

3En estos casos, en Scala podremos omitir el uso del punto y los paréntesis. Para más información, véase laSubsubsección 1.2.2.2: Operadores « página 10 »

4Véase el algoritmo 1.21 (página 29) donde se podrán ver otros ejemplos de las declaraciones de importaciónen Scala

5Por defecto, si no se especifica en el momento de la definición, los atributos y/o métodos de una clase tienenacceso público. Es decir, public es el cualificador por defecto en Scala.

Página 33

Page 50: Programación Funcional en Scala

El siguiente paso será incorporar funcionalidad a la clase que se ha creado. Para ello sepueden definir métodos mediante el uso de la palabra reservada def :

1 class MiPrimeraClase{2 var a = 13 def suma(b:Byte):Unit={4 a += b5 }6 }

Una característica importante de los métodos en Scala es que todos los parámetros soninmutables, es decir, vals. Por tanto, si se intenta modificar el valor de un parámetro en elcuerpo 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 el3 // parametro b es de tipo val4 a += b5 }

Algoritmo 2.2: Método suma que no compila

Los métodos presentan otra característica común entre los lenguajes orientados a objetoscomo Java y es la asignación dinámica de métodos (dynamic method dispatch), lo que significaque el código resultante de la llamada a un método depende del tipo del objeto que contiene elmé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 eluso explícito de la palabra return. Scala retornará el valor de la última expresión que apareceen 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 acabo la definición de este tipo de métodos consiste en eliminar el tipo de retorno y el símboloigual, y englobar el cuerpo de la función entre llaves, tal y como se indica a continuación:

1 class MiPrimeraClase {2 private var sum = 03 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 aspectosfuncionales y los de orientación a objetos confluyen en el lenguaje.

Página 34

Page 51: Programación Funcional en Scala

Números Racionales

Los números racionales son aquellos que pueden ser expresados como un cociente nd. Algu-

nas de sus características principales son:

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

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

División de números racionales. Se intercambian el numerador y denominador 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 racionalessean inmutables, se necesitará que los clientes de esta clase proporcionen toda la informaciónen 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 declase. 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 cuerpode 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íaque 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 siguientemodo:

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íaescribir 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 unaclase inmutable y toda la información debe estar disponible en el momento que se invoca alconstructor. Este último deberá asegurarse de que el denominador indicado no toma el valorcero (0).

Página 35

Page 52: Programación Funcional en Scala

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 puedenestablecerse sobre los valores pasados a métodos o constructores y que deben ser satisfechaspor 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 +"/"+ d4 }

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úmeroracional como parámetro y retorne como resultado la suma de ambos operandos. Puesto que seestá construyendo una clase inmutable, el nuevo método deberá devolver la suma en un nuevonúmero racional:

1 class Rational(n: Int, d: Int) {2 require(d != 0)3 override def toString = n +"/"+ d4 // no compila: no podemos hacer that.d o that.n5 // deben definirse como atributos6 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, dadoque se producirá un error de compilación. Aunque los parámetros de clase n y d están el ámbitodel 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 declarard 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 atributos4 val denom: Int = d5 override def toString = numer +"/"+ denom6 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 inmutabilidaddel diseño. En este caso, el operador de adición add devuelve un nuevo objeto racional querepresenta la suma de ambos números, en lugar de realizar la suma sobre el objeto que realizala llamada.

Página 36

Page 53: Programación Funcional en Scala

A continuación se incorporará a la clase Rational un método privado que determinará elmá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 enScala haciendo uso de private. En este caso, el método gcd se utilizará como método auxiliarpara 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 sepodrían introducir sería la inclusión de los operadores comúnmente utilizados para la suma y elproducto:

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

2.3. Jerarquía de clases en ScalaUna 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 basadoen 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 Scalase encuentra el tipo Any, que es el tipo base de todos los tipos en Scala. El tipo Any tiene dossubtipos:

El tipo AnyVal, para definir las clases que representan un valor (value classes), similar alde los tipos primitivos definidos en lenguajes como Java (Int,Double,. . . ). Las subclasesdel tipo AnyVal se encuentran predefinidas.

El tipo AnyRef. El resto de clases harán referencia a un tipo. Las clases que se definansiempre harán referencia, aunque sea de forma indirecta, a un tipo por defecto, el tipoAnyRef, ya que todas las clases que se definan extenderán de forma implícita del traitscala.ScalaObject. ScalaObject es el alias de java.lang.Object.

6También podríamos escribir a.+(b) aunque en este caso el código resultante sería mucho menos legible. Paramás información, véase la Subsubsección 1.2.2.2: Operadores « página 10 »

7Incluyendo valores numéricos, funciones. . .

Página 37

Page 54: Programación Funcional en Scala

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 eltipo Nothing, subtipo de cualquier otro tipo. No hay valores para el tipo Nothing. Este tipo dedatos se utilizará para indicar una terminación anormal o para representar el elemento vacío ennuestros 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 untipo 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 herenciade clases. Con este mecanismo se puede extender una clase, agregando nuevos métodos, sinnecesidad de recompilar el código existente.

La herencia en Scala presenta las mismas limitaciones que en otros lenguajes de POO, comoJava. Las clases sólo podrán heredar su comportamiento de, a lo sumo, otra clase. Es decir, unaclase sólo podrá extender de otra clase.

Si se quisiera crear una clase para representar animales que tuviera una operación paraconocer el número de patas del animal, una posible implementación podría ser:

1 abstract class Animal {2 def patas():Int3 }

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:

8Haciendo uso de la palabra reservada extends

Página 38

Page 55: Programación Funcional en Scala

1 class Perro extends Animal{2 def patas() = 43 }4 class Pajaro extends Animal{5 def patas() = 26 }

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 clasespodrá 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 heredarlos 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, perotambié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 encapsuladefiniciones 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 deherencia, en el que únicamente se puede tener un padre, una clase puede llevar a cabo un procesode 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 lapalabra 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 palabrasreservadas extends o with.

1 class MiPrimerMixin extends MiPrimerTrait{2 override def toString = "Este es mi primer mixin en Scala"3 }

Página 39

Page 56: Programación Funcional en Scala

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 rasgose utilizan del mismo modo que se utilizan los métodos heredados de una clase. De maneraadicional, 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 unpadre de manera explicita mediante el uso de extends, se tendrá que utilizar la palabra reservadawith. Si se quisieran incluir en el proceso de mezcla múltiples rasgos, únicamente se tendríanque incluir más cláusulas with.

En este punto sería posible pensar que los rasgos son como interfaces Java con métodosconcretos, pero realmente pueden hacer muchas más cosas. Por ejemplo, los rasgos puedendefinir atributos y mantener un estado. Realmente en un rasgo se puede hacer lo mismo que enuna 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 cualserá la implementación del método invocada. Sin embargo, el mismo código escrito en unrasgo provoca un desconocimiento de la implementación del método que será invocadoen tiempo de ejecución. Dicha implementación será determinada cada una de las vecesque un 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 modificacionesapilables 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 denú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 elmismo orden en el que fueron introducidos.

Dada una clase que implementa el comportamiento descrito en el párrafo anterior, se podríadefinir 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ñadidoa 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 elproceso de mezcla. Los rasgos también son apilables: se podría escoger cualquier subconjuntode los tres anteriores e incorporarlos a una clase, de manera que conseguiríamos una nueva clasecon la funcionalidad deseada. El siguiente fragmento de código representa una implementaciónreducida del comportamiento de una cola FIFO:

Página 40

Page 57: Programación Funcional en Scala

1 import scala.collection.mutable.ArrayBuffer2 abstract class ColaEnteros {3 def get(): Int4 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 deun 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 serintegrado (mezclado) con una clase que presenta una implementación concreta del método encuestión.

A continuación se muestra un ejemplo de uso del rasgo anterior:scala> class MiCola extends ColaEnterosBasica with Duplicadodefined class MiColascala> val cola = new MiColacola: 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 lugarlos 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 modoque 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 = 1scala> cola.get()res16: Int = 2

Página 41

Page 58: Programación Funcional en Scala

El orden en el que se mezclan los rasgos es importante . De manera resumida, cuando seinvoca a un método de una clase que presenta modificaciones apiladas, el método del rasgodefinido más a la derecha es el primero en ser invocado. Si dicho método invoca a super, ésteinvocará 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úmerosmenores que cero no serán incorporados a la cola. El método put del trait Incremento sumará elvalor 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á utilizaruna clase abstracta.

Si la eficiencia es importante, se debería escoger el uso de las clases. La mayoría de losentornos de ejecución Java hacen una llamada a un método virtual de una clase muchomás rápido que la invocación de un método de un interfaz. Los rasgos son compilados ainterfaces 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á cambiaren el futuro y, generalmente, se mantienen más opciones abiertas.

2.4. Patrones y clases caseLas 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ódigorepetitivo. 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 depatrones.

2.4.1. Clases case

El uso del modificador case provoca que el compilador de Scala incorpore una serie defacilidades a la clase indicada.

En primer lugar incorpora un factory method (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 lugarde new Foo(“x”). Una de las principales ventajas de este tipo de métodos es la ausencia deoperadores new cuando los anidamos:

1 val op = BinaryOperation(‘‘+’’, Number(1), v)

Página 42

Page 59: Programación Funcional en Scala

Otra funcionalidad sintáctica incorporada por el compilador es que todos los argumentos enla lista de parámetros incorporan de manera implícita el prefijo val, por lo que éstos últimosserán atributos de clase. Por último, pero no por ello menos importante, el compilador añadeimplementaciones “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 definicionesde nuestras clases. La principal ventaja de este tipo de clases es que soportan la concordanciade 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 palabrareservada case. Cada una de estas alternativas incorpora un patrón y una o más expresiones queserán evaluadas en caso de que se produzca la concordancia del patrón. Se utiliza el símbolode flecha (=>) para separar el patrón de las expresiones. Como se ha podido comprobar, lasintaxis de los patrones es sumamente sencilla por lo que, a continuación, se profundizará enlos 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 unaalternativa 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 }

9Son más grandes porque se generan métodos adicionales y se incorporan atributos implícitos para cada uno delos parámetros del constructor

Página 43

Page 60: Programación Funcional en Scala

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, porlo que posteriormente se podrá hacer uso de dicha variable para actuar sobre el objeto como sepuede apreciar en el siguiente ejemplo:

1 expr match {2 case 0 => "valor cero"3 case somethingElse => "no es cero: valor "+ somethingElse4 }

2.4.2.4. Patrones constructores

Son en este tipo de construcciones donde los patrones se convierten en una herramientamuy poderosa. Básicamente están formados por un nombre y un número indefinido de patrones.Asumiendo que el nombre designa una clase de tipo case, este tipo de patrones comprobaránprimero si el objeto pertenece a dicha clase para, posteriormente, comprobar si los parámetrosdel constructor concuerdan con el conjunto de patrones extra indicados.

La definición anterior puede no resultar demasiado explicativa, por lo que a continuación seincluye un pequeño ejemplo en el que se 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 tercerargumento 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óncomodí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 modoque 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 valorsea 0:

1 expr match {2 case List(0, _, _) => println("Concordancia de patrones!")

Página 44

Page 61: Programación Funcional en Scala

3 case _ =>4 }

2.4.2.6. Patrones tipados

Se puede utilizar este tipo de construcciones como reemplazo de las comprobaciones yconversiones de tipos:

1 def tamanoGenerico(x: Any) = x match {2 case s: String => s.length3 case m: Map[_, _] => m.size4 case _ => -15 }

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 tipoString concordará con dicho patrón. La variable de patrón s hará referencia a dicha cadena.

2.5. Polimorfismo en ScalaCuando 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 laPOO, se corresponde con el concepto de herencia visto en la Subsección 2.3.1: Herencia enScala « página 38 ».

La segunda técnica, la genericidad, se corresponde con la abstracción de los tipos en clasesy 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 laabstracció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 puedehablar 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 lasclases implementando una versión muy simple de lista inmutable enlazada para los númerosenteros, 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

Page 62: Programación Funcional en Scala

1 trait ListaInt2 class Cons (val cabeza:Int, val cola:ListaInt) extends ListaInt3 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 (queserá la cabeza de la lista) y un elemento xs (la cola de la lista, que tendrá que ser de tipoListaInt).

Ahora también se necesitará definir una lista inmutable enlazada para valores booleanos quesiga las mimas especificaciones que la anterior lista:

1 trait ListaBool2 class Cons (val cabeza:Bool, val cola:ListaBool) extends ListaBool3 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 entreambos 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 crearuna 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 dealguna funcionalidad extra a las listas, habría que buscar y modificar una gran cantidad deimplementaciones de listas11. Otra consecuencia relacionada con la extensión del código será elhecho de que aumentaría notablemente la posibilidad de que éste contenga errores. Además, nose 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 aambas, sería posible tener una única clase que valdría tanto para los tipos Int, Boolean, comopara cualquier otro tipo de datos definido.

Se podrán generalizar las clases definidas anteriormente parametrizando el tipo. Para ellojunto al nombre de la clase o rasgo, se indicará el nombre que se utilizará para hacer referenciaa los tipos de datos parametrizados en la clase o rasgo, en una lista separada por comas yencerrada 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 quepresente el comportamiento descrito anteriormente para la listas de enteros y booleanos.

1 trait ListaGen[T] //Indicamos que el tipo del trait estaparametrizado.

2 //T sera el nombre del tipo generico.

10Se ha usado val delante de los parámetros de clase para convertir a los mismos en atributos de clase11Concretamente una por cada tipo de datos que nuestra lista pueda contener.

Página 46

Page 63: Programación Funcional en Scala

3 class Cons[T] (val cabeza:T, val cola:ListaGen[T]) extendsListaGen[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:Boolean3 def head:T4 def tail:ListaGen[T]5 }6 class Cons[T](val head:T, val tail:ListaGen[T]) extends ListaGen[T]{7 def isEmpty = false8 }9 class Nil[T] extends ListaGen[T]{

10 def isEmpty = true11 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. Laparametrizació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 ejemplosimple, 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 varianzaLa 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 ListaInt2 case class Cons (val cabeza:Int, val cola:ListaInt) extends

ListaInt3 case object Nil extends ListaInt

tal que:

Página 47

Page 64: Programación Funcional en Scala

Tenga un parámetro del tipo ListaInt.

Devuelva como valor una lista del tipo ListaInt si todos los elementos de la lista sonpositivos.

Lance una excepción en otro caso.

En una primera solución, se podría definir la función sonTodosPositivos como se muestra enel algoritmo 2.13.

1 def sonTodosPositivos(xs:ListaInt):ListaInt = xs match{2 case Nil => Nil3 case Cons(h,ys) if h>=0 => Cons(h,sonTodosPositivos(ys))4 case _ => throw new Error5 }

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 devueltoserá una instancia de la clase Cons. Por tanto, la definición de la función sonTodosPositivospodrí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 deListaInt. 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 unejemplo 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ámetroT que sólo podrá variar con los diferentes supertipos de ListaInt. Por tanto T podrá ser de tipoListaInt, AnyRef o Any, según está definida la jerarquía de clases en Scala (véase la secciónSecció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 unacota superior como una cota inferior sería:

1 def sonTodosPositivos[S <: ListaInt,T >: ListaInt](xs:S):T = xsmatch{

2 case Nil => Nil3 case Cons(h,ys) if h>=0 => Cons(h,sonTodosPositivos(ys))

Página 48

Page 65: Programación Funcional en Scala

4 case _ => throw new Error5 }

Algoritmo 2.16: Trait Function1

Es posible combinar cota superior y cota inferior restringiendo el tipo a un intervalo, por loque se podría encontrar una definición de tipo similar a “[S >: Cons <: ListaInt]” pudiendo serS, 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 datosdefinidos en los algoritmos 2.6 y 2.7 vistos en la Subsección 2.3.1: Herencia en Scala « página38 ».

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 mantenganesta 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 sedefinan. La covarianza será apropiada para los tipos de datos inmutables12 y no será adecuadapara 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 presentanla siguiente relación A <: B, se plantea la duda sobre la varianza que presentará C, lo cual noes una decisión binaria (covarianza o no) sino que se pueden encontrar tres posibles relacionesentre C[A] y C[B]:

C[A] <: C[B], siendo C[A] un subtipo de C[B], por lo que diremos que C presenta unarelació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], loque 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 deinvarianza.

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.12Si los métodos definidos cumplen ciertas condiciones

Página 49

Page 66: Programación Funcional en Scala

Se ha dicho que la covarianza será adecuada para los tipos inmutables cuyos miembroscumplan unas ciertas propiedades.

Antes de empezar a enumerar estas propiedades se estudiará la relación que presentan lostipos de funciones definidos a continuación:

1 type A = Animal => Perro2 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 desustitució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 nuestrasfunciones o métodos. Si se tuvieran cuatro tipos A1, A2, B1 y B2 que presentan la siguienterelación: A2 <: A1 y B1 <: B2 y las funciones A1 =>B1 y A2 =>B2, aplicando nuevamenteel 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 larelación entre los argumentos es de contravarianza y la relación entre los tipos resultantes es decovarianza.

Por tanto, las características que deben de presentar los miembros de las clases que presentenparamé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):U3 }

Algoritmo 2.18: Trait Function1

Por tanto, se deberá de tener en cuenta que los parámetros tipados covariantes sólo puedenaparecer como resultados de los métodos. Los parámetros tipados contravariantes sólo puedenaparecer como argumentos de los métodos, mientras que los parámetros tipados invariantespodrá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 cadavez que se quiera representar la lista vacía, convirtiendo Nil en un objeto cuya superclase esListaGen:

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 unaconferencia magistral en 1987 titulada La Abstracción de Datos y Jerarquía y que dice que: si S es un subtipo deT, entonces los objetos de tipo T en un programa de computadora pueden ser sustituidos por objetos de tipo S (esdecir, los objetos de tipo S pueden sustituir objetos de tipo T), sin alterar ninguna de las propiedades deseables deese programa

Página 50

Page 67: Programación Funcional en Scala

1 trait ListaGen[+T]{2 def isEmpty:Boolean3 def head:T4 def tail:ListaGen[T]5 }6 class Cons[T](val cabeza:T, val cola:ListaGen[T]) extends

ListaGen[T]{7 def isEmpty = false8 }9 object Nil extends ListaGen[Nothing]{

10 def isEmpty = true11 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 enel objeto Nil ya que el parámetro T no está accesible para el objeto Nil y esto provocaría unerror. Por último, se ha especificado que el parámetro de tipo presente en el rasgo ListaGen escovariante para poder realizar definiciones del tipo:

1 val x:ListaGen[String] = Nil

Página 51

Page 68: Programación Funcional en Scala

Página 52

Page 69: Programación Funcional en Scala

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 lacomputación como la evaluación de funciones matemáticas evitando los estadosy 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 sermodificados. Se adopta una visión más matemática en la que los programas están compuestospor numerosas funciones que esperan una determinada entrada y producen una determinadasalida y, en muchas ocasiones, otras funciones.

Otra de las principales características de la programación funcional es la ausencia de efectoscolaterales, 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 conviertaen 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 ofreceralgunas 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

Page 70: Programación Funcional en Scala

3.1.2. Scala como lenguaje funcionalEs 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 avariables 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 delpropósito del programa. Por tanto, el significado del programa vendrá determinado por el propiosignificado que le demos a estos componentes y por las reglas que determinen la composiciónde estos componentes.

3.2. Sentido estricto y amplio de la programación funcionalEn 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 unlenguaje de programación funcional. (Lisp, OCaml, Haskell, Scala...)

Por tanto, se puede afirmar que la programación funcional se basa en una premisa simplepero 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 programaspero no en qué programas se pueden escribir. A lo largo de este capítulo se verá como muchosprogramas que se creían imposibles escribir sin evitar efectos colaterales tienen una versiónfuncional 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 éstosno sean observables. Por ejemplo, se mutarán datos declarados localmente en el cuerpo de unafunció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 programascon funciones puras incrementará la modularidad de los mismos y, como consecuencia de lamodularidad, 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 lenguajede 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.1Un ejemplo de lenguaje funcional puro sería Haskell2Sin mónadas I/O o UnsafePerformIO3La 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

Page 71: Programación Funcional en Scala

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 funcionestambié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 sustituirdicha 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 programasllamado modelo de sustitución4.

Cuando las expresiones son referencialmente transparentes, se puede imaginar el procesode computación como la resolución de una ecuación algebraica.

3.3. Funciones y cierres en ScalaHasta 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 queposibiliten dividir dicho programa en piezas más pequeñas y manejables, las cuales permitiránuna mejor comprensión del mismo. Scala ofrece varios mecanismos para definir funciones queno están presentes en Java. Además de los métodos, que no son más que funciones miembro deun objeto, se puede hacer uso de funciones anidadas, funciones anónimas y funciones valor.

3.3.1. Definición de funcionesMediante 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 lasfunciones recursivas, que siempre hay que indicarlo. Las llaves que delimitan el cuerpo de lafunció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.

4Para 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

Page 72: Programación Funcional en Scala

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 bmax: (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 muchasocasiones es deseable que esas pequeñas funciones no estén visibles para poder ser usadasfuera del ámbito en el que se han creado, por lo que se tendrán escribir dentro del bloque deotra función.

En el algoritmo 3.1 se muestra un ejemplo de la definición de pequeñas funciones dentrodel bloque de otra función. En concreto, se define la función sqrt que devolverá el cálculode la raíz cuadrada del número pasado como argumento utilizando el método de Newton deaproximaciones 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 cuadrada3 if (suficienteBuena(valorEstimado)) valorEstimado4 else sqrtIter(mejorAproximacion(valorEstimado))5

6 def mejorAproximacion(valorEstimado: Double) = //Funcion paramejorar la aproximacion

7 (valorEstimado + x / valorEstimado) / 28

9 def suficienteBuena(valorEstimado: Double) = //Condicion determinacion del algoritmo

10 abs(cuadrado(valorEstimado) - x) < 0.00111 sqrtIter(1.0)12 }

Algoritmo 3.1: Cálculo de la raíz cuadrada de un número por el método de Newton

5En términos generales, cada vez que se tenga una estimación valorEstimado del valor de la raíz cuadrada de unnúmero x (pasado como argumento a la función), se podrá hacer una pequeña manipulación para obtener una mejoraproximación (una más cercana a la verdadera raíz cuadrada) realizando la media aritmética del valorEstimadoy x

valorEstimado ,es decir, valorEstimado+ xvalorEstimado

2 . Al repetir este proceso, se obtendrán cada vez mejoresestimaciones de la raíz cuadrada. El algoritmo deberá detenerse cuando la estimación sea lo “suficientementebuena”, algo que deberá ser un criterio bien definido.

6Este criterio de detención no es muy bueno ya que no es muy preciso para números pequeños y cuando sele pasa como argumento un número grande podría no terminar(ya las operaciones aritméticas son casi siemprerealizadas con una precisión limitada en los ordenadores). El criterio de detención se podría mejorar indicandocomo condición de terminación la invariabilidad de los n primeros decimales.

Página 56

Page 73: Programación Funcional en Scala

3.3.3. Diferencias entre métodos y funcionesEn 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. Ladiferencia sutil está en que un método es siempre un miembro de una clase (junto con camposy 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 eltípico estilo funcional.scala> def m(x: Int) = 2*xm: (x: Int)Int

scala> val f = (x: Int) => 2*xf: (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 anteriorejemplo, el intérprete de Scala crea un objeto invisible que rodea todo lo definido desde la líneade 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 functionm.toString ^

scala> f.toStringres1: 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 funcionesson objetos con este método especial. Así que en el fondo, las llamadas a funciones en Scalaacaban siendo “por debajo” ejecuciones de algún método apply definido sobre algún objeto. Lacreació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 definidascon val). Reiterando, esto es una asignación de un objeto tipo FunctionN a dicha variable. Perotambién es posible asignar a una variable un método de una clase. Como por ejemplo:scala> def m(x: Int) = 2*xm: (x: Int)Int

scala> val f = m _f: (Int) => Int = <function1>

scala> f.toStringres4: 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 elobjeto asignado a f. También se aprecia en este ejemplo la sintaxis utilizada para convertir m enuna función parcialmente aplicada:scala> val f = m _

Si se hubiera escrito simplemente:

Página 57

Page 74: Programación Funcional en Scala

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, lasfunciones parcialmente aplicadas son funciones a las que no se les ha proporcionado todossus argumentos (en el ejemplo que se ha visto, no se proporciona ninguno), y que por tanto notienen 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 invocarlassino 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 unafunción valor. Por lo tanto, la principal diferencia entre las funciones anónimas y las funcionesvalor es que las primeras existen en el código fuente mientras que las segundas existen comoobjetos en tiempo de ejecución.

A continuación se define un pequeño ejemplo de una función anónima que suma el valor 1al número indicado:

1 val f = (x:Int) => x + 1

Las funciones valor son objetos propiamente dichos, por lo que se pueden almacenar envariables o invocarlas mediante la notación de paréntesis habitual.

Las funciones anónimas se usan frecuentemente en programación funcional para pasarlascomo 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étodollamado apply. 9. Cuando se define una función anónima cómo (x : Int) => x + 1, lo querealmente se crea es un objeto:

1 val f = new Function1[Int,Int] {2 def apply (a:Int):Int = a + 13 }

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 seinvoca a la función f con un valor, f(5), lo que realmente se hace es una llamada al métodoapply de f :

1 f.apply(5)

7Una vez se ha hecho la conversión de método en función8El nombre lambda viene del lambda cálculo,9En Scala los objetos que tienen un método apply pueden ser llamados como si fueran métodos

Página 58

Page 75: Programación Funcional en Scala

La biblioteca estándar de Scala provee de distintos rasgos FunctionN para representar lasfunciones como objetos de N argumentos.

3.3.6. CierresLas 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 funcionesanó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ónno 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 intentautilizar esta función en un contexto en el que no esté accesible una variable otro se obtendría unerror 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 sonconocidas como cierres. El nombre se deriva del acto de “cerrar” la función anónima mediantela captura en el ámbito de la función de los valores de sus variables libres. Una función valorque no presenta variables libres, creada en tiempo de ejecución a partir de su función anónimano 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 dentrode 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 lavariable 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 cumpleen sentido contrario: si un cierre modifica alguno de los valores capturados, estos últimos sonvisibles fuera del ámbito del mismo.

3.4. RecursiónUna 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 nose utilizarán los bucles iterativos, ya que suelen utilizar vars y, principalmente, no determinanun valor12.

Aunque la recursión es mucho más que simular bucles ya que es una técnica esencial encomputació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 queun proceso es recursivo si se puede definir en términos de si mismo, y a dicha definición sele 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áspequeña del problema original.

10Sin utilizar bucles While, Do-While. . .11Cuando una función se llama así misma12La última expresión evaluada del bucle será la condición de salida del mismo

Página 59

Page 76: Programación Funcional en Scala

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 aR, 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 larelación de recurrencia, habrá que identificar el caso base, el cual permitirá conocer el valorde 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 deque 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 basey 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 casoEsta 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 unasentencia 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 necesitenrealizar un gran número de llamadas recursivas hasta alcanzar el caso base, la función podríano ser capaz de alcanzar la condición de terminación, devolviendo un error como consecuenciadel desbordamiento de pila: StackOverflowError. Por este motivo será determinante evaluar laprofundidad 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 quese produzca un error por desbordamiento de pila dependerá del tamaño de la pila de Java,definido por defecto en 1024kb13.

13En 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 dela pila se podrá cambiar desde la línea de comandos utilizando la opción -Xss

Página 60

Page 77: Programación Funcional en Scala

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 queel hilo de ejecución14. En la pila de Java se guardará el estado del hilo de ejecución asociadoy la JVM sólo podrá realizar dos operaciones sobre dicha pila: almacenar y sacar contextos depila.

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 contextoactual. 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. Cuandola 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, delas necesidades de cada método. Para cada método de una clase se determinará el tamaño delcontexto de pila necesario y se incluirá en el fichero de clase durante la compilación. Así, eltamaño de cada contexto de pila dependerá de las variables locales, los parámetros del métodoy 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 alos 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óndesde 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ónde 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ónserá necesaria más memoria. En la próxima sección se verá como la recursión de cola resuelveel 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 elfactorial de un entero pasado como argumento:

14Por tanto, en las aplicaciones en las que haya varios hilo de ejecucións, existirán varias pilas privadas asociadasa dichos hilo de ejecucións, en concreto, tantas pilas como hilo de ejecucións.

15Los datos almacenados en la pila de un hilo de ejecución son privados para dicho hilo de ejecución.16La pila de operandos será un espacio de memoria que será usado como área de trabajo dentro del contexto de

pila.17Contiene 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.18Se 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.19El contexto actual estará situado en la cima de la pila privada al hilo de ejecución.

Página 61

Page 78: Programación Funcional en Scala

1 def fact(n: Int):Int = {2 var valor:Int=03 if (n == 0) {valor=1}4 else {valor=n * fact(n - 1)}5 valor6 }

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 llamadasfunciones recursivas de cola (tail recursive). El compilador de Scala detecta esta situación y laoptimiza, reemplazando la última llamada con un salto al comienzo de la función tras actualizarlos 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 dadohasta otro natural dado:

1 def sumaDesdeHastaRec(x:Int,y:Int):Int={2 @annotation.tailrec3 def go(x:Int,y:Int,acc:Int):Int=4 if (x>y) acc5 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 acontinuación es recursiva de cola. Si se hace esta indicación y la función no fuera recursiva decola daría un error.

Cuando se emplea recursión de cola, el compilador de Scala traducirá el código al mismo

Página 62

Page 79: Programación Funcional en Scala

grupo de bytecodes que si se hubiera utilizado un bucle while20. El objetivo de utilizar estaoptimización para la recursión es el de evitar el error provocado por un desbordamiento de lapila – 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, uncontexto 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 ofrecidopor la máquina virtual (JVM) dificulta de manera notable la implementación de otros tipos derecursión de cola. Scala únicamente optimiza llamadas recursivas a una función dentro de lamisma (recursión directa).

Si la recursión es indirecta, como la que se muestra en el siguiente fragmento de código, nose puede llevar a cabo ningún tipo de optimización:

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 definidasy 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óximoa extensiones del lenguaje.

El primer paso consiste en comprender una de las técnicas más comunes de los lenguajesfuncionales: la currificación (currying21).

Haskell B. Curry propuso trabajar sólo con funciones de un argumento. Aunque esto pareceuna limitación, el truco consiste en que las funciones de más de un argumento son de ordensuperior: 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 querecibe 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 decó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 llamadastradicionales de manera consecutiva. La primera invocación recibe el parámetro x y devuelveuna función valor para la segunda función. Esta segunda función recibe el parámetro y. Elsiguiente código muestra una función primera que lleva a cabo lo que haría la primera de lasinvocaciones de la función sumaCurrificada anterior:

20Siempre y cuando la llamada recursiva sea la última instrucción (esté en la posición de cola o tail position)21Se 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

Page 80: Programación Funcional en Scala

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 querecibe 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 unafunció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. Ambasdevolverán como resultado la suma de los dos parámetros enteros que se les pase. La únicadiferencia entre ellas es que sumaC es una función currificada:

1 def suma(x:Int,y:Int):Int = x + y2 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íadefinir 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 elsegundo 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 sobreel segundo argumento de la función sumaC (y) mientras que la función suma5Cb se ha definidoparcializando 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

Page 81: Programación Funcional en Scala

En este caso sí se puede observar una diferencia clara, atendiendo al tipo de datos devueltopor ambas funciones. El tipo devuelto por sumaP es: (Int, Int) => Int y el tipo de sumaCPes: 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 losprimeros 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 operadoresson en realidad métodos y, por tanto, se reducen a la llamada a un método de un objeto de unade clases de los tipos de datos. Además, en la Subsección 3.3.3: Diferencias entre métodos yfunciones « página 57 », se vieron las principales diferencias entre métodos y funciones y cómoen la mayoría de los casos se podrían tratar métodos y funciones indistintamente. Por tanto, sepodrá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 dosdiferencias notables en la definición de ambas funciones.

Aunque ambas funciones han sido definidas parcializando el operador * definido en el tipode datos Int, la función por3 ha sido definida parcializando sobre el segundo operando yla función por4 sobre el primer operando.

En la función por3 se ha especificado el tipo del operando parcializado mientras que enla 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 cualquierotro 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 funcionescomo parámetros o que devuelven una función, y que permiten hacer mucho más conciso ygeneral 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 ycó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, lascuales calcularán la suma y el producto del intervalo comprendido entre los números enterospasados como parámetros, respectivamente.

Página 65

Page 82: Programación Funcional en Scala

1 def sumaDesdeHasta(x:Int,y:Int):Int = if (x>y) 02 else x + sumaDesdeHasta(x+1,y)3 def productoDesdeHasta(x:Int,y:Int):Int = if (x>y) 14 else x * productoDesdeHasta(x+1,y)

En el ejemplo anterior se puede observar la similitud entre ambas funciones. Lo único quelas 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) e2 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 argumentof. En el ejemplo anterior se le ha pasado dicho argumento haciendo uso de la parcialización deoperadores (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 lasuma 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 crearclases genéricas parametrizando las mismas, ahora se estudiará como utilizar la genericidadpara 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 dadosun 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 unpar de elementos de tipo Float, Double,. . . habría que definir otra versión de la función swapIntpara este tipo concreto:

1 def swapFloat(a:Float,b:Float):(Float,Float)=(b,a)

Página 66

Page 83: Programación Funcional en Scala

Una vez más se puede apreciar la similitud de las funciones swapInt y swapFloat. En estassituaciones se desearía poder abstraer el tipo de la función ya que no influye en el cálculo de lamisma. Para poder abstraer el tipo de una función no se podrá recurrir a las funciones de ordensuperior ya que éstas abstraen operaciones no tipos.

Para abstraer el tipo de una función se usará polimorfismo22.El polimorfismo servirá paraintroducir variables de tipo23. Se dirá que una función cuyo tipo sea polimórfico será unafunció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ánvariables de una sola letra [A,B,C]

Para definir una función polimórfica en Scala se introducirá una lista de las variables detipo, 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 quelas variables pasadas como argumentos a una función pueden ser utilizadas en el cuerpo de lamisma).

Haciendo uso del polimorfismo, se definirá una función swapGen que, pasados cualesquierados 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 referenciasde tipo de la signatura de la función SwapGen hace referencia al tipo A por lo que se estaríaforzando 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 conuno de los propósitos de la misma, el resultado no es el esperado ya que se esperaba una tupladel tipo (Double,String) en lugar de una tupla del tipo (Any,Any). La función será más genéricay 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 tuplacon los tipos esperados y con los parámetros invertidos.

scala>swap("hola",2.3)res4:(Double,String) =(2.3,"hola")

22Usaremos 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 comopolimorfismo paramétrico y en otros paradigmas de programación se denomina genericidad

23Las variables de tipo también suelen llamarse parámetros de tipo

Página 67

Page 84: Programación Funcional en Scala

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*x2 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 comoargumentos, 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 ofrecela programación funcional. Se podrían desacoplar las condiciones del If en funciones del tipoDouble => Boolean y así ganar tanto en legibilidad del código como en reusabilidad delmismo. A continuación se verá una nueva aproximación a la solución final:

24Ideado por el estadístico belga Adolphe Quetelet, por lo que también se conoce como índice de Quetelet.25http://www.ncbi.nlm.nih.gov/mesh?term=body%20mass%20index

Página 68

Page 85: Programación Funcional en Scala

1 def bajoPeso(imcValue:Double):Boolean = 0<imcValue && imcValue < 18.52 def pesoNormal(imcValue:Double):Boolean = 18.5 <= imcValue &&

imcValue < 24.93 def sobrePeso(imcValue:Double):Boolean = 24.9 <= imcValue &&

imcValue < 304 def obesidad(imcValue:Double):Boolean = 30 <= imcValue && imcValue <

1005

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 suICM queda mucho más clara. Además se podrán utilizar las “funciones auxiliares” definidas enun futuro. Si se presta atención a dichas funciones auxiliares, se puede advertir que si se abstraeel límite inferior y el límite superior del cada rango, junto con el valor que se está comparandose 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 lavariable 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 devuelvauna tupla con su IMC y su clasificación.

Página 69

Page 86: Programación Funcional en Scala

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 personascon 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 ala obesidad y no el exceso de peso, los estudios que examinan los riegos para la salud asociadosa la obesidad en los que se mide realmente la adiposidad son menos usuales de lo deseado. Elporcentaje de grasa corporal puede medirse usando diferentes técnicas (análisis de impedanciabioeléctrica...).

Cuando no es posible determinar el porcentaje de grasa corporal(BF %) se suele recurrir alIMC para medir la adiposidad. Cómo se ha visto con anterioridad, el cálculo del IMC corporales fácil (peso/altura2) aunque no refleja precisamente la grasa corporal ya que los cambiosen 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 quetení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=>hombre2 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, elsexo, el peso y la altura de un individuo y nos devuelva su clasificación basado en la prediccióndel porcentaje de grasa corporal calculado con la función CUN-BAE.

Clasificación basada en BF%26Má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.27Autores 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

Page 87: Programación Funcional en Scala

normal => < 20% en hombres y < 30% en mujeressobrepeso => 20%-25% en hombres y 30%-35% mujeresobesos => > 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 individuodevuelva 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. EjerciciosResponder a las siguientes cuestiones:

Ejercicio 2. Considérese el siguiente fragmento de código:

1 var a = 12 a = a + 1

En sentido estrictamente matemático, ¿se podría pensar en a como una variable, teniendo encuenta la sentencia a = a + 1?

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?

No

Página 71

Page 88: Programación Funcional en Scala

Ejercicio 4. Considérese la función getTime la cual devuelve la hora actual. ¿Es getTime unafunción pura?

No

Ejercicio 5. Considérese la función random la cual devuelve un número aleatorio. ¿Es randomuna función pura?

No

Ejercicio 6. Si se define una función f tal que:

1 def f() = {2 103 204 }

¿Es f una función pura?

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 = 13 var t = n4 while (t > 0) {5 f = f * t6 t = t - 17 }8 f9 }

10

11 def fact2(n: Int):Int =12 if (n == 0) 1 else n * fact2(n - 1)

Página 72

Page 89: Programación Funcional en Scala

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

Page 90: Programación Funcional en Scala

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 = 13 def add(x: Int) = x + y4

5 add6 }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*x2 def cubo(x: Int) = x*x*x3

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

No

Ejercicio 14. Dadas las siguientes definiciones de:

Página 74

Page 91: Programación Funcional en Scala

1 def hello() = {2 println("hello")3 104 }5

6 def fun(x: => Int) = {7 x + x8 }

¿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 enteropasado como argumento.

Ejercicio 16. Definir la función aprueba que tome como parámetro una lista de enteros con lascalificaciones 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 enterosxs y un valor entero cota y devuelva una lista de enteros eliminando los elementos menores quela cota.

Ejercicio 18. Definir una función que devuelva un entero con los dígitos del parámetro enteroen 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 escapicúa(true) o no(false).

Ejercicio 20. Definir una función para calcular los número de Fibonacci que sea recursiva decola.

Ejercicio 21. Definir la función descomponer que convierta un número entero, pasado porpará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úmerospasados como argumentos y la función coprimos que nos diga si dos números pasados comoargumentos son coprimos.

Ejercicio 23. A la siguiente representación de números se le conoce como el “Triángulo dePascal”:

11 1

1 2 11 3 3 1

1 4 6 4 11 5 10 10 5 1. . .

Página 75

Page 92: Programación Funcional en Scala

Los números de los extremos de cada fila son 1 y el resto de elementos se calculan sumando losdos 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 lacolumna, 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 tipoList[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 estrictasSe 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énse 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 funcionesestrictas es que siempre evalúan sus argumentos antes de comenzar con la evaluación del cuerpode la función. Dependiendo del lenguaje de programación, la definición de funciones estrictaspuede ser una constante en programación funcional28. Es más, muchos lenguajes funcionales noproveen una forma de definir funciones que no sean estrictas. La gran mayoría de las funcionesdefinidas hasta el momento son estrictas, como por ejemplo la función valor absoluto definidaen 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é laexpresió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 siguenla estrategia evaluación no estricta para la evaluación de sus argumentos. Por tanto, como sevio previamente en la Sección 1.4: Evaluación en Scala « página 18 », una función puede serestricta 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 cuerpode 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

28No será una constante en lenguajes de programación funcional como Haskell, Lean o Miranda.

Página 76

Page 93: Programación Funcional en Scala

Uno de los inconvenientes que presenta la estrategia de evaluación evaluación no estrictay que, por tanto, presentan las funciones no estrictas, es que los argumentos se han de evaluarcada vez que se hace referencia a ellos en el cuerpo de la función. Este comportamiento seha ejemplificado invocando la función hacerPar definida en el algoritmo 3.5 con el bloqueprintln("hola");1+41;:scala> hacerPar { println("hola"); 1 + 41;}holaholares4: (Int, Int) = (42,42)

En el ejemplo anterior se puede observar como el efecto colateral de imprimir por pantallael 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 deevaluació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 porprimera 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 sepuede 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ásreevaluaciones.

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 elmismo bloque que se invocó previamente la función hacerPar del algoritmo 3.5:scala> hacerPar2{ println("hola"); 1 + 41;}holares6: (Int, Int) = (42,42)

En realidad, aunque se han utilizado las estrategias de evaluación no estricta y evaluaciónperezosa, las funciones hacerPar y hacerPar2 serían estrictas29, ya que siempre evalúan todossus argumentos. Se puede ver un ejemplo de función no estricta, implementando la selectiva ifde 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 produceel error.

El uso de las diferentes estrategias de evaluación ofrece al programador una gran capacidada 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.

29Ambas funciones tiene como valor devuelto una tupla y las tuplas en Scala son estrictas.

Página 77

Page 94: Programación Funcional en Scala

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 fundamentalmenteconsisten 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íahabrá 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 conceptosmás potentes en el proceso de resolución de problemas. Se entiende por abstracción la capacidadde 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 posiblemanejar, 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 lasacciones que debe realizar un programa mediante la técnica “Divide y Vencerás”30 (resolviendocada 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.

30El 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 nosólo forme parte del vocabulario informático, sino que también se utiliza en muchos otros ámbitos. En el ámbitode la informática “Divide y Vencerás” es una técnica de diseño de algoritmos que consiste en resolver un problemaa partir de la solución de subproblemas del mismo tipo, pero de menor tamaño. Si los subproblemas son todavíarelativamente grandes se aplicará de nuevo esta técnica hasta alcanzar subproblemas lo suficientemente pequeñoscomo para ser solucionados directamente[31].

Página 78

Page 95: Programación Funcional en Scala

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 abstractode datos. Todos ellos constituyen diferentes niveles en el proceso de abstracción referido a losdatos.

Los datos son las propiedades o atributos (cualidades o cantidades) sobre hechos u objetosdel 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 unadeterminada 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 devalores.

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 delos tipos de datos primitivos.

Los tipos de datos constituyen un primer nivel de abstracción, ya que no se tiene en cuentacómo se representa la información, ni cómo se manipula. Sólo se hará uso de las operacionesproporcionadas para cada tipo de datos.

Se pueden definir las estructuras de datos como agrupaciones de datos que guardan algunarelació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 elementosestá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

31También llamados tipos de datos elementales.32Los tipos de datos pueden ser primitivos y no elementales, como por ejemplo los conjuntos.33La 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íaen una lista simple. En cambio, si en una estructura no lineal en la que cada elemento se relaciona con otros doselementos, como por ejemplo ocurre en un Árbol binario de búsqueda (ABB), eliminamos una de estas relaciones,destruiríamos dicha estructura.

Página 79

Page 96: Programación Funcional en Scala

• No lineales. Estructuras de datos que presentan una organización complementaria alas estructuras de datos lineales en la que cada elemento puede estar relacionado conmú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 lainformació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 noimportará las operaciones definidas sobre los elementos de la estructura sino las operacionesque 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 unasemá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 ode otros tipos de datos compuestos ya creados. Además, habrá que implementar el conjunto deoperaciones 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 delas operaciones definidas en su interfaz, por lo que, cuando un programador haga uso del tipono conocerá su representación interna. Es más, se podría cambiar la representación del tipo dedatos sin que el funcionamiento del programa cliente se viera afectado.

Para indicar el funcionamiento de las operaciones sobre un tipo de datos se utilizará laespecificación algebraica con constructores, basada en describir el comportamiento medianteecuaciones36[15].

3.10.2. Definición de Estructuras de Datos en Lenguajes Funcionales3.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:

34El 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ómose representa internamente la misma.

35Obviamente, cuando se implementen las operaciones sobre el TADs, sí se tendrá acceso a la representacióndel mismo.

36Basado en el modelo de sustitución – ver la Sección 1.4: Evaluación en Scala « página 18 » –

Página 80

Page 97: Programación Funcional en Scala

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

¿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 delos conceptos propios de la programación funcional vistos anteriormente, como son: funcionespuras, 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á lapalabra clave trait. Un trait es una interfaz abstracta que puede incluir la implementación dealgunos métodos37. Si se añade la palabra clave sealed delante de trait se estará indicando quetodas las implementaciones del rasgo que se va a definir deberán ser declaradas en el mismoarchivo. 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 estructurade datos. La declaración de un constructor de datos proporcionará una función para construircada 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 puedenserlo. Para definir una estructura de datos polimórfica se deberá incluir una lista de variables detipo, encerrada entre corchetes, en la declaración de la estructura de datos. Posteriormente, sepodrá hacer uso de estas variables de tipo en la definición de los constructores de datos y en losmé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 elementode 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 copiarla lista xs. Se podrá reutilizar. Esta propiedad de los tipos de datos inmutables se denominacompartición estructural o compartición de datos (data sharing o sharing).

37Vé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

Page 98: Programación Funcional en Scala

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 datosinmutables sin tener la preocupación de que posteriormente el código modifique sus datos y portanto, 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 laresta o para la división (3− 8 /∈ N, 4

5/∈ N).

A continuación se muestra la especificación algebraica correspondiente a la especificaciónde los números naturales:especificación NATURALESusa BOOLEANOStipos natoperaciones

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 −→ boolmax : nat nat −→ natmin : nat nat −→ nat_/_ : nat nat −→ nat_%_ : nat nat −→ nates_par : nat −→ booles_impar : nat −→ bool

variablesn,m: nat

ecucacionesn+ cero = nn+ suc(m) = suc(n+m)

38Algo que se convierte en un problema cuando se desarrollan programas grandes, en los que los datos tienenque ser pasados a través de diversos componentes, los cuales se ven obligados a hacer “copias de seguridad” de losmismos.

Página 82

Page 99: Programación Funcional en Scala

cero ∗m = 0suc(n) ∗m = (n ∗m) +m

cero− cero = cerocero−m = errorsuc(n)− cero = suc(n)suc(n)− suc(m) = n−m

ceroˆcero = errornˆcero = suc(cero)nˆsuc(m) = n ∗ (nˆm)

cero === cero = ciertocero === suc(m) = falsosuc(n) === cero = falsosuc(n) === suc(m) = n == m

n! == m =!(n === m)

cero ≤ m = ciertosuc(n) ≤ cero = falsosuc(n) ≤ suc(m) = n ≤ m

n < m = n ≤ m&&n 6= m

n ≥ m = m ≤ nn > m = m < n

max(cero,m) = mmax(suc(n), cero) = suc(n)max(sun(n), suc(m) = suc(max(n,m))

min(cero,m) = ceromin(suc(n), cero) = ceromin(suc(n), suc(m)) = suc(min(n,m))

n/cero = errorn/m = cero⇐ n < mn/m = suc((n−m)/m)⇐ m 6= cero &&m ≤ n

n%cero = errorn%m = n⇐ n < mn%m = (n−m)%m⇐ m 6= cero &&m ≤ n

es_par(cero) = ciertoes_par(suc(n)) = es_impar(n)

es_impar(cero) = falso

Página 83

Page 100: Programación Funcional en Scala

es_impar(suc(n)) = es_par(n)

Se definirán los números naturales. Tendrán dos constructores: Cero y Suc, además de lasoperaciones 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 => this6 case Suc(m) => Suc(this+m)7 }8

9 def * (m:Nat):Nat = this match{10 case Cero => Cero11 case Suc(n) => (n * m) + m12 }13

14 def - (m:Nat):Nat = this match{15 case Cero if m==Cero => Cero16 case Cero => throw new Exception("Error en la resta")17 case Suc(n) => m match {18 case Cero => this19 case Suc(t) => n - t20 }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 => true31 case Suc(n) => m match {32 case Suc(t) => n==t33 case Cero => false34 }35 case _ => false36 }37

38 def !== (m:Nat):Boolean = !(this===m)39

40 def <= (y:Nat):Boolean = this match{41 case Cero => true42 case Suc(n)=> y match{43 case Cero => false44 case Suc(m) => n <= m45 }

Página 84

Page 101: Programación Funcional en Scala

46 }47

48 def < (y:Nat):Boolean= (this <= y) && (this !== y)49

50 def >= (y:Nat):Boolean = y <= this51

52 def > (y:Nat):Boolean = y < this53

54 def / (y:Nat):Nat = y match{55 case Cero => throw new Exception("Error division por cero")56 case Suc(m) if this < y => Cero57 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 => this63 case Suc(m) => (this - y) % y64 }65

66

67 }68 case object Cero extends Nat69 case class Suc(elem:Nat) extends Nat70

71 object Nat{72

73 def max (x:Nat,y:Nat):Nat = x match{74 case Cero => y75 case Suc(n) => y match{76 case Cero => x77 case Suc(m) => Suc(max(n,m))78 }79 }80

81 def min(x:Nat,y:Nat):Nat = x match{82 case Cero => Cero83 case Suc(n) => y match{84 case Cero => Cero85 case Suc(m) => Suc(min(n,m))86 }87 }88

89 def es_Par(x:Nat):Boolean = x match{90 case Cero => true91 case Suc(n) => es_Impar(n)92 }93

94 def es_Impar(x:Nat):Boolean = x match{95 case Cero => false

Página 85

Page 102: Programación Funcional en Scala

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 naturalanterior al pasado como argumento a la función. Para facilitar la resolución del ejerciciose 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 lafunció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 soncoprimos.

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 unnúmero del tipo Int

El algoritmo 3.8 muestra la implementación final de los números naturales con una posiblesolución a los ejercicios anteriores.

1 trait Nat {2

3 def + (y:Nat):Nat = y match {4 case Cero => this5 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) + m11 }12

13 def - (m:Nat):Nat = this match{14 case Cero if m==Cero => Cero15 case Cero => throw new Exception("Error en la resta")16 case Suc(n) => m match {17 case Cero => this18 case Suc(t) => n - t19 }

Página 86

Page 103: Programación Funcional en Scala

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 => true30 case Suc(n) => m match {31 case Suc(t) => n==t32 case Cero => false33 }34 case _ => false35 }36

37 def !== (m:Nat):Boolean = !(this===m)38

39 def <= (y:Nat):Boolean = this match{40 case Cero => true41 case Suc(n)=> y match{42 case Cero => false43 case Suc(m) => n <= m44 }45 }46

47 def < (y:Nat):Boolean= (this <= y) && (this !== y)48

49 def >= (y:Nat):Boolean = y <= this50

51 def > (y:Nat):Boolean = y < this52

53 def / (y:Nat):Nat = y match{54 case Cero => throw new Exception("Error division por cero")55 case Suc(m) if this < y => Cero56 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 => this62 case Suc(m) => (this - y) % y63 }64

65 //******** Ejercicios resueltos *********66 val toInt:Int= this match{67 case Cero => 068 case Suc(x)=> 1 + x.toInt

Página 87

Page 104: Programación Funcional en Scala

69 }70 }71

72 case object Cero extends Nat73 case class Suc(elem:Nat) extends Nat74

75 object Nat{76

77 def max (x:Nat,y:Nat):Nat = x match{78 case Cero => y79 case Suc(n) => y match{80 case Cero => x81 case Suc(m) => Suc(max(n,m))82 }83 }84

85 def min(x:Nat,y:Nat):Nat = x match{86 case Cero => Cero87 case Suc(n) => y match{88 case Cero => Cero89 case Suc(m) => Suc(min(n,m))90 }91 }92

93 def es_Par(x:Nat):Boolean = x match{94 case Cero => true95 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 => Cero105 case _ => x - Suc(Cero)106 }107 def suc(x:Nat)=Suc(x)108 def esDivisible(x:Nat)(y:Nat):Boolean= (x % y) == Cero109 private def compruebaDesdeHasta (x:Nat)

(y:Nat)(f:Nat=>Nat=>Boolean):Boolean=110 if (x==y) true111 else !f (y)(x) && compruebaDesdeHasta(suc(x))(y)(f)112

113 def es_Primo(x:Nat):Boolean= x match{114 case Cero => false115 case Suc(Cero)=> false116 case otro=>compruebaDesdeHasta(Suc(Suc(Cero)))(x)(esDivisible)117 }118

Página 88

Page 105: Programación Funcional en Scala

119 @annotation.tailrec120 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 => Cero128 case x => Suc(apply(x-1))129 }130 }

Algoritmo 3.8: Implementación final del tipo Naturales

3.10.3. Estructuras de datos lineales. Listas3.10.3.1. TAD Lista

Las listas son estructuras inmutables homogéneas, es decir, que contienen datos del mismotipo. Además, son las estructuras inmutables de datos lineales más flexibles, puesto que su únicacaracterí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 deun 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 seespecificará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.39Los elementos de una lista pueden estar repetidos40Otras 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

Page 106: Programación Funcional en Scala

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,ENTEROStipos listaoperaciones

Nil :−→ lista{constructora}Cons(_, _) : elemento lista −→ lista{constructora}esV acia : lista −→ boollongitud : lista −→ int_##_ : lista elemento −→ lista_ ++_ : lista lista −→ listaizquierdo : lista −→ elementoelim− izq : lista −→ listaderecho : lista −→ listaelim− der : lista −→ listadrop : lista nat −→ lista

variables

n,m: elementox,y,z: listaa: nat

ecuaciones

izquierdo(Nil) = errorizquierdo(Cons(e, x)) = e

elim− izq(Nil) = errorelim− izq(Cons(e, x)) = x

derecho(Nil) = errorderecho(Cons(e,Nil)) = ederecho(Cons(e, x)) = derecho(x)

elim− der(Nil) = errorelim− der(Cons(e,Nil)) = Nil

41Se corresponde con la operación cabeza42Se corresponde con la operación cola

Página 90

Page 107: Programación Funcional en Scala

elim− der(Cons(e, x)) = elim− der(x)

esV acia(Nil) = ciertoesV acia(Cons(e, x)) = falso

Nil ++y = yCons(e, x) + +y = Cons(e, x++y)

longitudNil = 0longitud(Cons(e, x)) = 1 + longitud(x)

Nil ++x = xCons(e, xs) + +y = Cons(e, xs++y)

x##e = x++Cons(e,Nil)

drop(x, 0) = xdrop(Nil, a) = Nildrop(Cons(e, x), a) = drop(x, a− 1)

Una posible implementación en Scala de la especificación del tipo abstracto de datos Listadefinido anteriormente sería:

1

2 sealed trait Lista[+A]{3 def cabeza: A4 def cola: Lista[A]5

6 /** Version recursiva para calcular la longitud de una lista */7 def longitud:Int={8 @annotation.tailrec9 def length[A](xs:Lista[A],count:Int):Int= xs match{

10 case Nil => count11 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 _ => cabeza20 }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 _ => cola26

27 }

Página 91

Page 108: Programación Funcional en Scala

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)=>elem32 case Cons(e,xs)=>xs derecho33 }34 /** Elimina el elemento situado mas a la derecha de una lista */35 def elim_der:Lista[A]= this match{36 case Nil => Nil37 case Cons(elem,Nil)=>Nil38 case Cons(elem,xs)=>Cons(elem,xs elim_der)39 }40 def esVacia:Boolean = this match{41 case Nil => true42 case Cons(e,x) => false43 }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 lalista */

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 => ys53 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+"::"+cola62 }63

64 object Lista {65 def drop[A](ys:Lista[A])(n:Nat):Lista[A]= (ys,n) match {66 case (Nil,_) => Nil67 case (xs,Cero)=> xs68 case (Cons(e,xs),n)=> drop (xs) (n-Nat(1))69 }70 def apply[A](as: A*): Lista[A] ={71 if (as.isEmpty) Nil72 else as.head :: apply(as.tail: _*)73 }74 }

Página 92

Page 109: Programación Funcional en Scala

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 elementono está en la lista.

posicion, devolverá un valor entero con la posición de la primera aparición de un elementodado 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 seencuentra en la lista.

repeticiones, devolverá un valor entero con el número de repeticiones de un elementodado 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 seestá 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,_) => Nil5 case (xs,Cero)=> xs6 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 => -113 case Cons(e,_) if e== elem => pos14 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.tailrec23 def go(ac:Int, xs:Lista[A]):Int = xs match {

43Habitualmente se declarará un objeto acompañante que acompañará al tipo de datos que se esté definiendo ya sus constructores. Un objeto acompañante es un objeto que tiene el mismo nombre que la clase acompañantedonde se definirán funciones para crear o trabajar con el tipo de datos. Los objeto acompañante tienen un soporteespecial dentro de Scala.

Página 93

Page 110: Programación Funcional en Scala

24 case Nil => ac25 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.tailrec34 def go(ac:Lista[A],xs:Lista[A]):Lista[A]= xs match{35 case Nil => ac36 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.tailrec44 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match {45 case Nil => res46 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) Nil56 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 listaoriginal 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 detake y drop.

Ejercicio 29. Definir la función zipWith que aplicará cierta función a los elementos de dos listastomándolos de dos en dos. La longitud de la lista resultado coincidirá con la de menor longitud.

Página 94

Page 111: Programación Funcional en Scala

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 formadapor dos listas. La primera de ella será el resultado de tomar el primer elemento de cada par deelementos 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 elementode la misma una función.

Ejercicio 33. Definir la función filter que permitirá seleccionar los elementos de una lista quecumplan un cierta propiedad.

Ejercicio 34. Definir la función takeWhile que tomará el mayor segmento inicial de una listaque cumpla una cierta propiedad.

Ejercicio 35. Definir la función dropWhile la cual eliminará de una lista el mayor segmentoinicial de elementos que verifiquen un propiedad.

Ejercicio 36. Definir la función foldr, función recursiva que recorre los elementos de una listade 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 lafunció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, perorealizando el plegado de la lista de izquierda a derecha.

3.10.4. Estructuras de datos no lineales3.10.4.1. Árboles

Los árboles son estructuras no lineales, como los conjuntos o los grafos, en los que lasecuencialidad que caracteriza a las estructuras lineales no existe, aunque en los árboles existeuna estructura jerárquica, de manera que un elemento tiene un solo predecesor, pero variossucesores.

Un árbol impone una estructura jerárquica sobre una colección de objetos, con un únicopunto 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 unaestructura A = (N,≺), constituida por un conjunto, N, cuyos elementos se denominan nodos, yuna relación de orden parcial transitiva, ≺, definida sobre N, y caracterizada por la existenciade:

Un elemento mínimo (anterior a todos los demás) único, la raíz.

∃! r ∈ N�(∀n ∈ N, n 6= 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 6= r −→ (∃! m ∈ N, ((m ≺ n)⋂

(∀ s ∈ N, s 6= m� s ≺ n −→ s ≺ m)))

Página 95

Page 112: Programación Funcional en Scala

La terminología utilizada con los árboles, en relación con los nodos que los forma, es lasiguiente:

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 laraí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 árbolcompleto.

Existen otros conceptos relacionados con el tamaño del árbol que definen las característicasdel mismo:

Orden: es el número potencial de hijos que puede tener cada elemento del árbol. De estemodo, se dice que un árbol en el que cada nodo puede apuntar a otros dos es de ordendos, 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. Comocada nodo de un árbol puede considerarse a su vez como la raíz de un árbol, también sepuede hablar de altura de ramas.

Esta estructura se puede considerar una estructura recursiva teniendo en cuenta que cadanodo 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 dedatos, 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 sepueden 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 (laraí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 mismotipo (subárboles de la raíz). Se puede apreciar que aquí no existe el árbol vacío, sino lasecuencia 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 delmismo tipo (subárboles de la raíz). Este último caso suele escribirse explícitamente de laforma (r, A1, . . . , AN ).

Página 96

Page 113: Programación Funcional en Scala

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 árbolesa partir de un elemento y dos árboles.especificación ARBOL_BINARIO[ELEM]usa BOOLEANOS,ENTEROStipos arbolBoperaciones

V acio :−→ arbolB{constructora}Nodo(_, _, _) : elemento arbolB arbolB −→ arbolB{constructora}hijoIzqdo : arbolB −→ arbolBhijoDcho : arbolB −→ arbolBraiz : arbolB −→ elementoesV acio : arbolB −→ boolnumeroNodos : arbolB −→ intigualForma : arbolB arbolB −→ boolaltura : arbolB ←→ int

variables

n,m: elementox,y,z: arbolB

ecuaciones

hijoIzqdo(V acio) = errorhijoIzqdo(Nodo(e, i, d)) = i

hijoDcho(V acio) = errorhijoDcho(Nodo(e, i, d)) = d

raiz(V acio) = errorraiz(e, i, d) = eesV acio(V acio)) = ciertoesV acio(Nodo(e, i, d)) = falsonumeroNodos(V acio) = 0numeroNodos(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) = 0altura(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 unpapel 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

Page 114: Programación Funcional en Scala

1 trait ArbolB[+T] {2 def hijoIzqdo:ArbolB[T] = this match {3 case Vacio => Vacio4 case Nodo(_,i,_)=>i5 }6 def hijoDcho:ArbolB[T] = this match {7 case Vacio => Vacio8 case Nodo(_,_,d)=>d9 }

10 def raiz:T= this match{11 case Vacio => throw new Exception("Solicitando raiz de arbol

vacio!!!")12 case Nodo(e,_,_)=>e13 }14 def esVacio:Boolean= this match{15 case Vacio => true16 case _ => false17 }18 def numeroNodos:Int = this match{19 case Vacio => 020 case Nodo(e,i,d)=> 1 + (i numeroNodos) + (d numeroNodos)21 }22 def altura:Int = this match{23 case Vacio => 024 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 esVacio38 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

Page 115: Programación Funcional en Scala

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 laraíz del subárbol izquierdo es menor que el valor de la clave del nodo y que el valor de la claveraí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 seha 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 hallamado Arbol:

1 trait ArbolBB[+T] {2 def esVacio:Boolean3 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):Boolean6 def raiz:ArbolBB[T]=this7 def subIzq:ArbolBB[T]8 def subDer:ArbolBB[T]9 def maximo:T

10 def minimo:T11 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=false17 def subIzq = left

Página 99

Page 116: Programación Funcional en Scala

18 def subDer = right19 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 mayores32 case Nodo(s,_,Vacio)if direct==false=>s // Si buscamos el

mayor de los elementos menores33 case Nodo(_,_,right1) if direct==false

=>valorSuc(right1,direct) // Si buscamos el mayor de loselementos 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 eliminar41 case Nodo(r,Vacio,Vacio)=> Vacio42 case Nodo(r,i,Vacio)=>i43 case Nodo(r,Vacio,d)=>d44 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 los46 //elementos mayores47 }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 _ => true56 }57 }58 def maximo:T= this match{59 case Nodo(r,_,Vacio)=>r60 case Nodo(r,_,d)=>d maximo

Página 100

Page 117: Programación Funcional en Scala

61 }62 def minimo:T= this match{63 case Nodo(r,Vacio,_)=>r64 case Nodo(r,i,_)=>i minimo65

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 x69 // y si existe,

comenzamos busquedadel 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 _ => padre77 }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]=Vacio85 def search[U >: Nothing <% Ordered[U]](x:U):Boolean =false86 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]=Vacio91 def esVacio=true92

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. EjerciciosEjercicio 38. Definir una función listaToArbol que devolverá un árbol binario a partir de unalista xs, de tipo List, pasada como argumento.

Página 101

Page 118: Programación Funcional en Scala

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 ambostienen el mismo número de nodos y además los nodos presentan la misma estructura. Definiruna función isomorfimos que devuelva un valor true si A1 es isomorfismo del árbol A2 pasadocomo parámetro.

Ejercicio 40. Se dice que un árbol binario es simétrico siempre que al trazar un línea verticalque 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 estructurasde datos que Scala ofrece. Las colecciones44 se pueden utilizar haciendo uso de la libreríacollection45, en la cual se encuentran los diferentes tipos de colecciones parametrizadas queprovee el lenguaje para resolver la mayoría de los problemas que se puedan presentar. Lascolecciones son estructuras de datos en las que se pueden agrupar uno o más datos de un tipode 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 formasecuencial, es decir, estructuras lineales que pueden tener un número arbitrario de elementoscomo Listas, Tuplas, Vectores, Conjuntos, . . . o cuyo número de elementos puede estar limitadocomo, por ejemplo, se verá en el tipo de datos Option. Desde Scala se tiene acceso completo a labiblioteca 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 yque, como se verá, ayudarán a manejar y operar sobre los datos de una colección, definiendoexpresiones cortas y expresivas.

Las colecciones pueden presentar una evaluación anticipada o una evaluación estricta operezosa 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, comopor ejemplo el tipo de datos Range, no se construyen hasta que son referenciados por primeravez.

Otra de las características importantes que definirán a una colección estará basada en lamutabilidad de la misma, es decir, si la colección será mutable o inmutable. Esta característicadefinirá si los elementos de una colección podrán cambiar una vez son asignados (coleccionesmutables) o si los elementos no pueden cambiar (la referencia que apunta al elemento de lacolección no podrá cambiar nunca). Se deberá tener en cuenta que, aunque la colección seainmutable, 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 evaluadossecuencialmente 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 enparalelo.

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

44El término “colecciones” se popularizó a raíz de la librería collections de Java.45Es 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

Page 119: Programación Funcional en Scala

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 rendimientoutilizando evaluación perezosa.[6]

3.11.1. El paquete scala.collectionSi 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 eltipo de datos Traversable[A], que agrupa a todos los tipos de datos que pueden ser usadospara representar cualquier cosa y que tienen la propiedad de poder ser recorridos. Los subtiposde Traversable deberán definir el método foreach, un método que se utilizará para recorrer laestructura de datos. El método foreach46 es un iterador interno que recibirá una función queoperará sobre los elementos del tipo A como parámetro y que se aplicará a cada uno de loselementos 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ánde 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 elementosde la estructura de datos. Esta clase mejora sustancialmente el rendimiento de su superclaseTraversable 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étodosque 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 avanzasobre los elementos de la colección y devolverá el siguiente valor de la colección o lanzará unaexcepció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

46La librería incluye algunas excepciones predefinidas para detener de forma anticipada la iteración por loselementos de la estructura. De este modo se podrá evitar que, para ciertas operaciones, se produzca una pérdidainnecesaria de ciclos de computación.

Página 103

Page 120: Programación Funcional en Scala

datos BitSet está optimizado para trabajar con conjuntos de enteros, almacenando un bitpor cada entero que forme parte del conjunto, de modo que si el valor se encuentra en elconjunto se activa el bit, en otro caso no se activará. El tipo de datos SortedSet[A] es unaversión especializada de Set que se podrá emplear cuando el tipo de datos de los valoresdel conjunto A presente un orden natural.

Seq[A]. Define los métodos length y apply, así como representa las colecciones que tienenuna estructura secuencial. Los dos subtipos de Seq[A] son: IndexedSeq y LinearSeq. Laprincipal diferencia entre ambos reside en cómo se accede a cada uno de los elementos deestas 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 ineficientespara accesos aleatorios a sus elementos ya que tienen que recorrer todos los elementosanteriores, 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 serdescompuesta en el primer elemento de la colección (la cabeza) y el resto (la cola). Lasestructuras de datos conocidas, que mejor se ajustan a este tipo de datos, son las pilasya que se puede acceder rápidamente al último elemento agregado pero se tendrían quedesapilar todos los elementos de la misma para llegar al primer elemento añadido. El tipoIndexedSeq será ideal para todos aquellos algoritmos de propósito general que no tenganque 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 datosque 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íaimplementar utilizando una lista enlazada con un requerimiento de espacio mínimo. Uti-lizando éstas soluciones obtendríamos tablas de direccionamiento directo, la cuales sólopodrá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 lastablas hash. Una tabla hash es una estructura de datos que intenta encontrar un balanceentre estos dos extremos.Cuando el número de claves que se almacenan efectivamentees pequeño comparado con el número total de claves posibles, una tabla hash ofrece unaalternativa eficiente a una tabla de búsqueda directa, utilizando solamente un vector detamaño proporcional a la cantidad de claves almacenadas. En lugar de utilizar la clavecomo 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úmerode claves posibles (existirán necesariamente claves que tengan el mismo número ) por loque se podrán producir colisiones que habrá que tratar49. .

A continuación se verán algunas de las colecciones inmutables más importantes dentro delecosistema de la librería scala.collection[14].

47También reciben el nombre de diccionarios.48Una asociación es un par (clave/valor). Un diccionario estará formado por un conjunto de asociaciones49Una función de hash adecuada podrá minimizar el número de colisiones pero no evitarlas completamente.

Página 104

Page 121: Programación Funcional en Scala

3.11.2. Iteradores en ScalaA 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 serealizan 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 nuevoelemento. El método hasNext del iterador se utilizará para conocer si existen elementos en lacolección que aún no se han recorrido.

Una forma habitual de utilizar estas funciones es en un bucle while, como se muestra en elejemplo:

scala> val it = Iterator("Ejemplo","metodos","next","hasNext","Iterator")it: Iterator[String] = non-empty iterator

scala> while (it.hasNext){| println(it.next())| }

EjemplometodosnexthasNextIterator

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ánque definir el método iterator que devuelva un Iterator[A]. En la tabla 3.1 se reflejan algunasde las operaciones más usuales dentro del tipo Iterator, que además estarán presentes en el tipoIterable[A] y que, por tanto, podremos emplear con las colecciones.

Métodos disponibles en el Tipo Iteratordef 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 iteradoresdef ++[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 loselementos 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.

50Es la clase base de todas las colecciones que definen el método iterator.

Página 105

Page 122: Programación Funcional en Scala

def dropWhile(p: (A) =>Boolean): Itera-tor[A]

Saltará todos los elementos del iteradorque verifiquen la condición p hasta queencuentre el primer elemento en el itera-dor que no verifique dicha condición. Elvalor devuelto será un iterador con todoslos elementos restantes.

def duplicate: (Iterator[A], Iterator[A]) Devolverá una dupla con dos iteradoresque iteraran sobre los mismos elementosque 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 deliterador 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 yconcatenando 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 elementosdel 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 valoresson el resultado de aplicar la función f aeste iterador.

def max:A51 Encuentra el elemento de mayor valor enel iterador. El iterador se encontrará alfinal del mismo después de invocar estemétodo.

def min:A52 Encuentra el elemento de menor valor enel iterador. El iterador se encontrará alfinal del mismo después de invocar estemétodo.

def nonEmpty: Boolean Devolverá el mismo valor que el métodohasNext.

def product: A53 Devolverá el producto de los elementosdel iterador.

def size: Int Devolverá el número de elementos del ite-rador.

51Signatura completa: def max[B >: A](implicit cmp: Ordering[B]): A52Signatura completa: def min[B >: A](implicit cmp: Ordering[B]): A53Signatura completa: def product[B >: A](implicit num: Numeric[B]): B

Página 106

Page 123: Programación Funcional en Scala

def sum: A54 Devolverá la suma de los elementos deliterador.

def take(n: Int): Iterator[A] Devolverá un iterador con los n primeroselementos 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 iteradorthat. El número de elementos del iteradordevuelto será igual al menor número deelementos de este iterador y del iteradorthat.

Tabla 3.1: Métodos del tipo Iterator

3.11.3. Colecciones inmutables3.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 desecuencias 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, noconsumirá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 lacota superior dada. En el algoritmo 3.14 se puede ver un ejemplo de la utilización de esteoperador.

by. Se empleará para cambiar el paso del rango. En el algoritmo 3.14 se puede ver unejemplo de la utilización de este operador.

Por tanto, para definir un rango habrá que indicar la cota inferior, la cota superior y el pasodel rango.

1 scala> val simpleRange = 1 to 102 simpleRange: scala.collection.immutable.Range.Inclusive = Range(1,

2, 3, 4, 5, 6, 7, 8, 9, 10)3

4 scala> val rangeSimple = 1 until 105 rangeSimple: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5,

6, 7, 8, 9)6

7 scala> val stepRange= 1 to 10 by 28 stepRange: scala.collection.immutable.Range = Range(1, 3, 5, 7, 9)

Algoritmo 3.14: Definicion de rangos

54Signatura completa: def sum[B >: A](implicit num: Numeric[B]): B

Página 107

Page 124: Programación Funcional en Scala

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 semuestra en el algoritmo 3.15.

1 scala> stepRange.step2 res8: Int = 23

4 scala> rangeSimple.start5 res9: Int = 16

7 scala> rangeSimple.end8 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 datosserá la de generar los elementos de otras estructuras de datos, aunque algunas estructuras tienendefinido el método range que realizará la misma función. Se podrá invocar el método range condos 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 delrango definido.

1

2 scala> val miLista=(1 to 10).toList3 miLista: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)4

5 scala> val miVector=(1 to 10).toVector6 miVector: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)7

8 scala> List.range(1,10) // Definimos una lista9 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 disponibleslos 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 coleccionesde dos o más elementos heterogéneos. Tuple será de gran utilidad en multitud de ocasiones enlas que se quieran agrupar varios elementos heterogéneos, es decir, elementos de distintos tiposde datos en una estructura55. Las tuplas permitirán agrupar hasta un máximo de 22 elementos,

55Algo que no se podrá hacer con las colecciones que sean subtipos de Iterable como List, Vector. . .

Página 108

Page 125: Programación Funcional en Scala

por lo que habrá tuplas del tipo Tuple2, Tuple3, . . . , Tuple22. Tuple no hereda de Iterable al noagrupar 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 entreparéntesis los elementos que formen a la misma y separarlos por comas. El tipo de una tupladependerá 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 adecuadode métodos para acceder a los elementos de cada tupla. Así, para acceder al primer elemento deuna tupla se usará el método _1, para acceder al segundo elemento utilizaremos el método _2y 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 descartaraquellos elementos de la tupla que no se deseen. Se puede ver un ejemplo del uso de estosmétodos y de concordancia de patrones con tuplas en el algoritmo 3.18.scala> miTupla._1res0: Int = 1

scala> miTupla._2res1: Double = 2.0

scala> miTupla._3res2: Float = 3.4755

scala> miTupla._4res3: String = hola

scala> miTupla._5res4: Boolean = true

scala> val (a,b,c,d,e) = miTuplaa: Int = 1b: Double = 2.0c: Float = 3.4755d: String = holae: Boolean = true

scala> val (x,_,y,_,z) = miTuplax: Int = 1y: Float = 3.4755z: 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 algoritmo3.19. Incluso es posible transformar la tupla en una colección.scala> miTupla.productIterator.foreach(i=>println("Valor: "+ i))Valor: 1Valor: 2.0Valor: 3.4755Valor: holaValor: true

scala> miTupla.productIterator.toListres8: 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 quepermitirá intercambiar la posición de los elementos de la tupla como se puede apreciar en elalgoritmo 3.20

Página 109

Page 126: Programación Funcional en Scala

scala> val tupla = ("hola","hello")tupla: (String, String) = (hola,hello)

scala> tupla.swapres9: (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 porlo que era de esperar que la librería estándar de Scala incluyera una implementación de lasmismas. Scala implementa en la clase List la estructura de datos de listas enlazadas inmutablesdonde se representan colecciones ordenadas y homogéneas de elementos. Esta estructura dedatos 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 estructurade datos en cabeza y cola, ya que esta operación de descomposición presentará una complejidaddel orden O(1). Para otros usos, el rendimiento se verá más afectado por lo que es preferibleelegir 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 yuna 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 concorrección[9], siempre y cuando se añadan elementos al principio de la lista o se descompongala lista en cabeza y cola. Cuando se necesite actualizar un elemento que se encuentre en la partecentral de la lista será necesario generar toda la lista de elementos previos al elemento que sedesea modificar.

Página 110

Page 127: Programación Funcional en Scala

Otra de las características del tipo de datos List, es que es covariante, lo cual significa quepara cada par de tipos S y T, si S es un subtipo de T, List[S] será subtipo de List[T]. Como se vioen la Sección 2.3: Jerarquía de clases en Scala « página 37 », Nothing se encuentra en la partemás baja de la jerarquía de clases de Scala y es un subtipo de cualquier otro tipo en Scala. Comolas listas son covariantes, List[Nothing] será subtipo de cualquier List[T] (independientementede 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 puedenescribir 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 crearlistas 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 deList[Nothing]. Ejemplo:

scala> val lista = Nillista: 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::Nillista3: 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 cabezade la lista corresponderá al primer elemento de la misma y la cola estará formada por el restode elementos de la lista.

List es una colección que presenta evaluación impaciente, es decir, cuando se construye unalista se calcula tanto la cabeza como la cola de la misma. Dentro de la librería collection de

56Haciendo uso del método apply del objeto acompañante de List

Página 111

Page 128: Programación Funcional en Scala

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 claseStream « 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 sepueden aplicar las funciones más usuales de los tipos iterables vistas en la tabla 3.1. Otrasoperaciones características de las listas se encuentran definidas en la tabla 3.2

Métodos del Tipo Listdef 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 utilidada la hora de manejar listas. Se recomienda ver la API de Scala, en scala-lang.org donde sepueden 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

Page 129: Programación Funcional en Scala

1 def fun(xs: List[Int], ys: List[Int]):List[Int] = (xs, ys) match {2 case (Nil, ys) => ys3 case (xs, Nil) => xs4 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.sorted4

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

Ejercicio 45. Definir las funciones suma y producto tales que, que dada una lista de enteroscomo parámetro devuelvan, respectivamente, la suma y el producto de todos sus elementos.scala> suma(List(1,2,3,4,5))res0: Int = 15scala> producto(List(1,2,3,4,5))res2: Int = 120

Página 113

Page 130: Programación Funcional en Scala

Ejercicio 46. Definir en Scala la función diferencias que devuelva la diferencia que hay entrecada 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 devuelvauna 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 devuelvauna 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 pasadacomo 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 unpalíndromo.

Ejercicio 53. Definir una función que devuelva el valor del elemento enésimo, pasado porpará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 unalista.

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 porpará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 quemejor se ajusta a la mayoría de algoritmos de propósito general. Las operaciones de acceso aun 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 datosVector 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 diversasventajas:

Los tiempos de búsqueda y de actualización de un elemento son excelentes, así como lostiempos de las operaciones para añadir un elemento al principio o al final de la estructurade datos.

Decente coherencia en caché, aumentando los aciertos57 en la misma ya que los elementos57Un acierto en caché se produce cuando el dato que requiere el procesador se encuentra en caché.

Página 114

Page 131: Programación Funcional en Scala

que se encuentren próximos en nuestra colección también deberán de estar próximos enmemoria.

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 temora que efectos colaterales puedan dar lugar a resultados no deseados, convierten a Vector en lasecuencia 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 secreaban 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 detipo Vector presentan los operadores +: y :+ para añadir un elemento al principio del vector oal final del mismo respectivamente58.

scala> simpleVector :+ 5 // Agregamos un elemento al final del vectorres5: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3, 4, 5)

scala> 0 +: simpleVector // Agregams un elemento al principio del vectorres6: 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 elementoscuando se desee que presenten evaluación perezosa. Haciendo uso de los flujos de datos sepodrán representar colecciones con un número infinito de elementos, sin que se produzca undesbordamiento de memoria60. Los flujos de datos guardan el valor de los elementos que ya hansido calculados, lo cual es una ventaja a la hora de acceder a estos elementos de forma eficienteya que no se han de recalcular estos elementos pero, a su vez, puede representar un problemade 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 queutilizarán el método #:: (en lugar del método :: que se usaba en la colección List). También sepodrá definir un flujo de datos haciendo uso del método apply de su objeto acompañante, comose puede comprobar en el algoritmo 3.23.

58Una buena regla nemotécnica para distinguir ambos operadores podría ser tener en cuenta que el símbolo dospuntos (:) siempre se encuentra próximo a la secuencia mientras que el símbolo + se encuentra próximo al elemento

59En la API de Scala podremos encontrar muchas más funciones que serán de gran utilidad a la hora de manejarvectores

60En 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

Page 132: Programación Funcional en Scala

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.emptyotroStream: 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 secuenciahaciendo uso de los valores previamente calculados. Una aplicación en la que se puede verperfectamente 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. Enel algoritmo 3.24 se muestra una posible implementación de la serie de Fibonacci utilizandoStream 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 toListres5: 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 enlenguajes como Haskell suelen utilizar List por defecto para representar listas. Hay que tener encuenta que, como se ha comentado anteriormente, List presenta evaluación impaciente o estrictamientras que la listas en Haskell presentan evaluación perezosa. Además Stream es estricta enel elemento, una vez producido, mientras que las listas en Haskell no lo son. Cuando se quieracrear 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 encontrarentre ambos tipos es que los conjuntos no permiten la existencia de elementos duplicados. Esdecir, si en un conjunto compuesto por diferentes elementos se trata de añadir un elemento quees igual62 a otro existente en el conjunto, dicho elemento no se añadirá quedando el tamaño delconjunto igual que antes de realizar la operación de añadir. Por tanto, se elegirá un conjuntocuando se desee que la estructura de datos no presente elementos duplicados o cuando se quieracomprobar si un elemento está en la misma.

61En la API de Scala se pueden encontrar muchas más funciones que serán de gran utilidad a la hora de manejarflujos

62En términos del método ==.

Página 116

Page 133: Programación Funcional en Scala

Otra de las diferencias fundamentales que existen entre los tipos Set y Seq es que no sepuede 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 elobjeto 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 conjunto2 numeros: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)34 scala> numeros + 7 //Agregamos el 7 al conjunto definido anteriormente5 res0: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 7, 3, 4)67 scala> numeros + 4 //Agregamos el 4 al conjunto numeros8 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 711 res2: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 7, 3, 4)1213 scala> res0 + -25 // Agregamos el -25 al conjunto resultante tras agregar el 714 res3: scala.collection.immutable.Set[Int] = Set(5, 1, 2, -25, 7, 3, 4)1516 scala> res3 - 1 //Eliminamos el 1 del conjunto resultante de agregar -25 tras agregar el 717 res4: scala.collection.immutable.Set[Int] = Set(5, 2, -25, 7, 3, 4)1819 scala> res4 + 123 //Agregamos el 123 al conjunto resultante de eliminar el 120 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 dedatos inmutable.

El conjunto resultante tras añadir el número 4 a un conjunto en el que este número yaexiste 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 alacceso a los elementos. En este tipo de datos, en lugar de obtener un valor ubicado en unaposició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 elementoestá 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 elementosdel conjunto por la posición de los mismos64.

Ejemplo:

63Excepto si se trata de un SortedSet ya que el orden de los elementos de este tipo coincidirá con el orden naturalde los elementos, aunque se puede considerar este caso como la excepción que confirma la regla.

64Para esto, se hará uso de un iterador.

Página 117

Page 134: Programación Funcional en Scala

scala> for (x <- numeros){println(x)}51234

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 Setse pueden aplicar las funciones más usuales de los tipos iterables vistas en la tabla 3.1. Otrasoperaciones características de los conjuntos se encuentran definidas en la tabla 3.3

Métodos del Tipo Setdef def +(elem: A): Set[A] Crea un conjunto nuevo que contendrá el

elemento elem además de los elementosde 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 elementosde este conjunto y los elementos del con-junto that.

def &∼ (that: Set[A]): Set[A] Devuelve la diferencia entre este conjuntoy el conjunto pasado como argumento.

def ++(elems: Set[A]): Set[A] Concatena los elementos de este conjuntocon los elementos del conjunto elems .

def diff(that: Set[A]): Set[A] Devuelve la diferencia entre este conjuntoy 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 pasadocomo argumento.

def iterator: Iterator[A] Devolverá un iterador sobre los elementosdel 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ásfunciones que pueden ser de gran utilidad a la hora de manejar conjuntos.

Página 118

Page 135: Programación Funcional en Scala

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, aligual 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 datosB (el tipo de los valores). El tipo de las claves y el tipo de los valores pueden ser iguales, aunqueen 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ácilde 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 puedenayudar 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 yString. Los elementos que se han pasado a la asociación son tuplas en las que el primer elementoes la clave y el segundo elemento es el valor. A la hora de definir los valores se ha empleadouna sintaxis clara, utilizando el operador ->, aunque como se verá en el algoritmo 3.28, tambiénse podría haber definido la asociación introduciendo las tuplas directamente (sin necesidad deutilizar 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 es2, 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 oeliminar 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 algoritmo3.30.

simpleMap + (4->"cuatro")res1: scala.collection.immutable.Map[Int,String] = Map(1 -> uno, 2 -> dos, 3 -> tres,4 -> cuatro)

scala> simpleMap - 3res3: scala.collection.immutable.Map[Int,String] = Map(1 -> uno, 2 -> dos)

Algoritmo 3.30: Agregar y eliminar elementos en un Map

65Una función matemática hace exactamente esto, asociar elementos de un conjunto con elementos de otroconjunto.

Página 119

Page 136: Programación Funcional en Scala

Otra de las similitudes que comparten los tipos datos Set y Map es el hecho de que, pordefecto, 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 especialatención ya que se deberá tener en cuenta que los elementos de una asociación se almacenancomo tuplas de pares (clave,valor). En el algoritmo 3.31 se muestra un ejemplo simple en el quese recorren los elementos de la asociación definida anteriormente.scala> for ((clave,valor)<-simpleMap) yield clave*2res4: 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 datosMap 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 queson propios del tipo Map. Al igual que ocurría con las demás colecciones, en la API de Scala sepueden encontrar muchas más funciones que pueden ser de gran utilidad a la hora de manejarasociaciones.

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ónescoger será el rendimiento que ofrece. Como se ha visto anteriormente, el rendimiento esuna característica que está muy relacionada con la implementación de la colección y con lasoperaciones que se vayan a realizar sobre la misma. En la tabla 3.5 se muestra el rendimientode 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 estasoperaciones 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ísticasen 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 estructurasde datos Map y Set.

66No obstante, existe una versión mutable tanto para los conjuntos como las asociaciones. Para utilizar la versiónmutable se tendrá que importar la librería scala.collection.mutable y luego se tendrá que hacer referencia a estasestructuras mutables como mutable.Map o mutable.Set.

Página 120

Page 137: Programación Funcional en Scala

Métodos del Tipo Mapdef ++(xs: Map[(A, B)]): Map[A, B] Devuelve un nuevo Map que contendrá

los pares (clave,valor) de esta asociacióny los de la asociación pasada como argu-mento.

def get(key: A): Option[B] Devuelve el valor asociado a una clavepasada como argumento.

def apply(key: A): B Nos devuelve el valor asociado a la clavepasada como argumento. En caso de noexistir 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 buscadano se encuentre. El método devolverá unaexcepción pero puede ser sobreescrito enlas 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 clavecoincida 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 delmap.

def isEmpty: Boolean Nos devolverá true si la asociación nocontiene 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 heredande PartialFunction, el cual hereda del tipo estándar de función en Scala. El comportamientoque tendrán las colecciones como funciones dependerá de la implementación del método applyde la colección67.

Por ejemplo, el tipo de datos Set[T] se podrá utilizar como una función T =>Boolean, eltipo de datos Seq[T] puede ser utilizado como una función Int =>T o el tipo Map[C,V] comouna función C =>V.

Por tanto, se podrán utilizar las colecciones en lugares en los que se esperaría que aparecierauna función.

scala> val conj=Set(1,2,3)conj: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> 1 to 2 map conjres1: scala.collection.immutable.IndexedSeq[Boolean] = Vector(true, true)

67Distinto del método apply del objeto acompañante de la colección y que se utiliza para construir las mismas.

Página 121

Page 138: Programación Funcional en Scala

Eficiencia de las operaciones sobre secuenciashead tail apply actualización insertar(por la cabeza) insertar(por la cola)

List RC RC Lin Lin C LinVector DC DC DC DC DC DCStream RC RC Lin Lin C LinRange RC RC RC - - -

Tabla 3.5: Secuencias. Eficiencia de las operaciones.

Eficiencia de las operaciones en conjuntos y mapsbuscar añadir eliminar minimo

HashSet & HashMap DC DC DC LinTreeSet & 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 deorden 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 estetipo de bucles en Scala presentaban unas características muy especiales que los convertirían enuna 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 dentrodel 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 entérminos de las funciones de orden superior map, flatMap y filter. Los bucles for que noalmacenen resultados serán traducidos por el compilador en términos de las funciones deorden 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 delas 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 quecon el bucle for.

Página 122

Page 139: Programación Funcional en Scala

scala> val miLista = 1::2::3::4::5::NilmiLista: 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 laque es traducida, obtienen los mismos resultados.scala> def esPar(x:Int):Boolean = (x % 2)==0esPar: (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 aflatmap, 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 compiladory el bucle for con dos generadores son equivalentesscala> val auxLista= ’a’::’b’::’c’::NilauxLista: 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

Page 140: Programación Funcional en Scala

(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 latraducción de expresiones for. La diferencia es que en lugar de utilizar las funciones de ordensuperior 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 delbucle}

la cual, será finalmente traducida a la siguiente expresión:

expr1 filter (expr2) foreach(x => expr3 foreach (y=> {cuerpo delbucle}))

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 deenteros, 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 unavariable mutable.scala> var a = 0a: Int = 0

scala> val tmpLista = 6::7::8::9::10::NiltmpLista: 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 = 82 + 8 = 102 + 10 = 124 + 6 = 104 + 8 = 124 + 10 = 14Total = 66

scala> a=0a: 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 = 82 + 8 = 102 + 10 = 124 + 6 = 104 + 8 = 124 + 10 = 14Total = 66

Algoritmo 3.35: Ejemplo traducción bucle for genérico

Página 124

Page 141: Programación Funcional en Scala

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 ydef 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, flatMapy filter para la traducción de las expresiones for que devuelven algún valor, mientras que paralos bucles for que no devuelven ningún valor, sólo necesita que estén definidas las funcionesde 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. Peroademá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 lasfunciones 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. Acontinuación se muestran las diferentes posibilidades que existen, dependiendo de las funcionesde orden superior definidas:

Si sólo se define la función map, se podrá definir expresiones for que tengan un únicogenerador.

Si se definen las funciones map y flatMap se podrán definir expresiones for que tenganvarios generadores.

Si se encuentra definida la función foreach, podremos definir bucles for que no devuelvanningú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 filtroscomo 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 muyrelacionadas ya que se definirán en las mónadas estas funciones que, además, caracterizarán lasmismas 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ónadacaracterístico de la programación funcional.

Además, como se ha visto anteriormente, las expresiones for son una traducción de lasfunciones map, flatMap y filter que permitirán escribir de forma más clara y comprensibleciertos 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 dentrode Scala, más allá de ser simples estructuras de control o iteradores de colecciones, ya quesiempre 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

Page 142: Programación Funcional en Scala

3.11.5. EjerciciosEjercicio 58. Implementar una función construirMap la cual devuelva un valor del tipo Map apartir de cualquier tipo de secuencia dada y de una función que permitirá crear las claves de laasociació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 hayen 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 nuestroscontactos y acceder rápidamente a los datos de los mismos si buscamos por el nombre delcontacto. 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 pasadocomo argumento a nuestra agenda.

Dentro de la clase Agenda, definir la función contactos que nos devuelva una lista con elnombre 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 todoslos 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 mismonombre. A la hora de imprimir por pantalla el resultado de las funciones contactos ytelefonos se deberá poder diferenciar a cada uno de los contactos de la agenda. De modoque si después de imprimir por pantalla el resultado de las funciones contactos y telefonosse 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 loscontactos que vivan en la calle pasada como argumento.

Página 126

Page 143: Programación Funcional en Scala

Implementar una función vecinos en la clase Agenda que devuelva un Map cuya claveserá 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 variablede tipo String pasada como argumento a la función, y además informe de cuantas aparicioneshay de cada palabra en la variable pasada como argumento.

Página 127

Page 144: Programación Funcional en Scala

Página 128

Page 145: Programación Funcional en Scala

Capítulo 4

Programación Funcional Avanzada enScala

4.1. Implícitos

4.1.1. Parámetros ímplicitos en funcionesEn 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 especificartodos los argumentos, obteniendo otra función como valor que podía ser invocada especificandolos parámetros restantes.

Una de las aplicaciones que tendrán los parámetros implícitos será la de poder aplicarfunciones sin la necesidad de especificar todos sus parámetros1.

Para definir valores, variables o parámetros implícitos en funciones se utilizará la palabrareservada implicit. Las variables o valores implícitos definidos se utilizarán como “valores pordefecto”2 de los parámetros definidos como implícitos de las funciones que se invoquen dentrodel namespace (espacio de nombres), donde estas variables o valores implícitas son definidas. Elparámetro implícito3 de las funciones se especificará después de la definición de los parámetrosno 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 delobjeto 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)

1Se tendrán que especificar todos los parámetros que no se declaren como implícitos.2Si éstos no son aportados específicamente cuando se invoque la función.3Normalmente se definirá un parámetro implícito aunque se pueden definir un grupo de parámetros implícitos

separados por comas.

Página 129

Page 146: Programación Funcional en Scala

^

En el algoritmo 4.2 se ha definido la clase ListaSingleInt. Dentro de la clase se ha definido unvalor implícito de tipo lista: la lista vacía (List()). También se ha definido la función hacerListaque se encarga de hacer una llamada a la operación hacerLista del objeto IntOps, pasándoleel valor de clase x como argumento. Por tanto, la función IntOps.hacerLista tomará el valorimplí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 necesidadde especificar un segundo parámetro utilizando la clase ListaSingleInt:scala> val lista3 = ListaSingleInt(3).hacerListalista3: List[Int] = List(3)

Los valores implícitos son muy usados en Scala. La mayoría aportan funcionalidad a lasfunciones, como por ejemplo el orden por defecto en las colecciones. Esta funcionalidad puedeser 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 sise hace un uso excesivo de los parámetros implícitos. Se puede evitar que esto ocurra limitandoel uso de los mismos dentro del código, por ejemplo al aporte de funcionalidad a determinadasfunciones.

4.1.2. Clases implícitasOtra 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 untipo (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étodoo valor desconocido. En este momento buscará dentro del espacio de nombres (namespace) enbusca de una clase implícita que tome como argumento la propia instancia e implemente elmé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 valordesde el propio tipo.

En el algoritmo 4.3 se define la clase implícita Listas en la que se implementa la operaciónhacerSingletonLista 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 deun elemento de tipo Int, a un elemento de tipo Listas, pudiendo así hacer accesible la operaciónhacerSingletonLista 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

Page 147: Programación Funcional en Scala

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 lostipos de datos pero para que Scala pueda realizar esta tarea, las clases implícitas deberán decumplir unas reglas:

Las clases implícitas se tienen que definir dentro de otra clase, objeto o rasgo. Como se havisto, 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 claseimplícita..

El nombre de la clase implícita no podrá ser igual al de otro objeto, clase o rasgo definidodentro 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á seguridadde 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 sercomprensible, 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 programasy prevenir errores de programación.

Informando suficientemente al compilador, éste será capaz de detectar una gran cantidad deerrores.

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 inspeccionandoel código.

4Por lo que no se podrá definir una clase implícita haciendo uso de case class ya que las clases case creanautomáticamente un objeto acompañante, dentro del cual se define automáticamente el método de fábrica apply.

Página 131

Page 148: Programación Funcional en Scala

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 nombreque la clase o rasgo, mientras que para referirnos al tipo de un objeto5 utilizaremos elmiembro type del objeto. A continuación se muestra un ejemplo:

object MiObjetodef 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 definirtipos dentro de un contexto, es decir, dentro de un objeto, clase o rasgo. Ejemplo:

abstract class Suma {type MiTipo // MiTipo es un tipo abstractodef suma(x:MiTipo,y:MiTipo):MiTipo

}class SumaInt extends Suma {

type MiTipo = Intdef 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 losparámetros de tipo, se podrán utilizar estos parámetros tanto en los argumentos como dentro dela definición de las funciones, clases,etc. La importancia de los parámetros de tipo para crearfunciones polimórficas o clases genéricas fue vista en la sección dedicada al polimorfismo enScala, 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 quepuedan tomar, acotando los mismos. Otra de las características de los parámetros de tipo es lavarianza que define la relación entre los tipos parametrizados y sus subtipos. La varianza y laacotació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

5No es habitual referirnos al tipo de un objeto ya que si conocemos el objeto podemos hacer referencia almismo sin necesidad de tener que preguntar por el tipo del mismo.

6Los contextos en los que se definan tipos abstractos no se podrán instanciar.

Página 132

Page 149: Programación Funcional en Scala

de otorgar nombres descriptivos a los parámetros de tipo, mientras que por otro lado hay quienapoya 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,. . . ), normalmentepara definir los elementos que pueden existir en un contenedor y que no presentan ningunarelación con el contenedor.

Utilizar nombre largos para definir aquellos tipos de datos que si están relacionados con elcontenedor. Por ejemplo, si observamos el tipo That aparece en la definición del métodoscan de List para hacer referencia al tipo del contenedor en el que se almacenarán losresultados 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 lospará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íficosList[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 lostipos sean compatibles. Para combinar tipos utilizaremos la palabra reservada with. Por ejemplo:

class MiClasetrait MiTraittrait Rasgo

type MiTipo = MiClase with MiTraittype 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 typeMapInt = Map[Int,_], en la que actúa como comodín, haciendo referencia a un tipo existencialdesconocido 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 », dedicadaa estudiar los rasgos y la herencia múltiple en Scala, se vieron la bondades del uso de tiposcompuestos.

4.2.5. Tipos estructuralesEn 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énpresentes en el tipo definido.

Página 133

Page 150: Programación Funcional en Scala

La definición de tipos estructurales permitirá definir tipos abstractos, es decir, Scala permiteespecificar ciertas características que deben de cumplir los objetos: métodos que deben estardefinidos, tipos, etc. Los tipos estructurales sólo tendrán en cuenta la estructura, por lo quepodrán ser anónimos aunque normalmente serán tipos nominales.

1 trait ComidaAnimalMap2 type Animal3 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 objetopara satisfacer el tipo ComidaAnimal es que haya implementado el método comida. Scala nopermite hacer referencia a tipos abstractos o a parámetros de tipo dentro de la definición de untipo estructural por lo que no se puede utilizar el tipo Animal. En su lugar se ha utilizado untipo conocido como el tipo Any. Esto hará que la clase que implemente este método tenga quehacer 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, serecomienda 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 ordensuperior. Los tipos de orden superior son aquellos que utilizan otros tipos para construir un tiponuevo7.

Los tipos de orden superior se utilizarán para simplificar la definición de otros tipos o parahacer 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 nuevafunción del tipo Function1[T,Unit]. Por tanto, podremos usar el tipo Funcion para simplificarla 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 valoral parámetro de tipo8.

Como se ha descrito anteriormente, el tipo Funcion se puede utilizar para simplificar ladefinición de funciones que toman un argumento y no devuelven ningún valor. Ejemplo:

def sumaTres:Funcion[Int]={x=>println(x+3)}

7Los tipos de orden superior pueden recibir uno o más tipos como parámetros8Al 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

Page 151: Programación Funcional en Scala

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 detipo simples. Ejemplo:

def ajusteTipo[M[_]](f:M[Int]) = fdef 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]) = fajusteTipo: [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 Mparametrizado 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 sehubiera intentado evaluar la expresión ajusteTipo[Function1](sumaTres) se habría obtenido unerror ya que Function1 toma dos parámetros de tipo y el método ajusteTipo espera un tipo conun único parámetro de tipo.

4.2.7. Tipos existencialesLos 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 paralas 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 lacompatibilidad 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 porlo que, por ejemplo, a nivel de código de la Máquina Virtual de Java, para la JVM esimposible 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 laversión 5 de Java), en la que todos los parámetros de tipo eran objetos del tipo Object.

9Tipos de datos sin parámetros de tipo

Página 135

Page 152: Programación Funcional en Scala

Normalmente los tipos existenciales se indicarán haciendo uso del guión bajo _, de modoque 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 enlos tipos existenciales.

Def. abreviada Def. formal SignificadoList[_] List[T] forSome type T Lista de cualquier tipoList[_ <: Perro] List[T] forSometype T <: Perro Lista de cualquier tipo que sea sub-

tipo del tipo PerroList[_ >: Animal] List[T] forSometype T >: Ani-

malLista de cualquier tipo que sea su-pertipo del tipo Animal

Tabla 4.1: Tipos existenciales en Scala

4.3. Teoría de categoríasLa 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 mismotiempo trata de mostrar una nueva forma de ver las matemáticas sin incluir las nociones deelementos, 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 defunció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 demorfismos satisface dos axiomas:

Existencia de un único morfismo identidad, IDx en el que el dominio y el codominioson iguales. La composición con el morfismo identidad tiene la siguiente propiedad:f ◦ IDx = IDx ◦ fPropiedad 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ónes a través de los patrones de diseño. La teoría de las categorías define conceptos abstractos debajo nivel que pueden ser expresados directamente en un lenguaje de programación funcionalcomo 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 yMónadas.

10Cada guión bajo será convertido a un parámetro de tipo en una sentencia forSome por lo que si se utiliza dosveces el guión bajo en el mismo tipo Scala pondrá una sentencia forSome con dos tipos

11https://github.com/scalaz/scalaz.

Página 136

Page 153: Programación Funcional en Scala

4.3.1. El patrón funcional FuntorLos 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 transformary preservar los morfismos.

En programación, los funtores se entenderán como patrones funcionales, en los que a uncontenedor que referencia a un conjunto de objetos se le quiere dotar de la posibilidad de aplicaruna función a cualquier objeto dentro del mismo, algo que se realizará sin alterar la estructuradel 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á unmétodo (el método map) que será el encargado de aplicar la función deseada a los objetos delcontenedor. Los funtores, como objetos, podrán ser pasados como parámetros y utilizados en elmismo 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 semuestra 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.5multD: (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)

12Normalmente se devolverá una nuevo contenedor con los resultados de aplicar la función a los elementos delcontenedor original.

13Se consideran definidas anteriormente las funciones doble, triple y cuadrado cuyo dominio y codominio esInt

Página 137

Page 154: Programación Funcional en Scala

scala> optFuntor.map(None)(multD)res32: Option[Double] = None

Con la abstracción Funtor es posible crear un conjunto de funciones que se puedan implementarhaciendo 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 losmorfismos serán las funciones entre las categorías.

Los funtores presentan dos propiedades fundamentales que son consecuencia directa de laspropiedades 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 deScala 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 lasmónadas, su importancia en la programación es anecdótica si se compara con las mónadas, elsiguiente 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 fundamentosmatemá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 laantigua 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 lascomputaciones en un conjunto de objetos. Si el patrón funcional funtor servía para abstraerla aplicación de una función a los elementos referenciados por un contenedor, el patrón mónadase 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 manejarla 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últiplescontenedores pueden ser aplanados en un contenedor único. Como se verá a continuación, elpatró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. . . enlos 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. Algunasmónadas se utilizan para manejar la concurrencia, las excepciones o los efectos colaterales, porejemplo.

Durante la sección dedicada al estudio de las colecciones en Scala se ha podido apreciar quees muy común que aparezcan definidas las operaciones map y flatMap dentro de las estructuras

Página 138

Page 155: Programación Funcional en Scala

de datos. De hecho, hay un nombre para definir a estas estructuras que, además, cumplen unasreglas algebraicas. Se llamarán Mónadas14.

Una mónada será un tipo paramétrico M[T] con dos operaciones: flatMap15 y unit que debensatisfacer 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 untipo U como parámetro y una función que mapea T a una mónada de U, devolviendo la mismamónada aplicada a U. Después, se encontrará el método unit que toma un elemento del tipo T ydevuelve 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 dela 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)

14Haskell, un lenguaje de programación funcional en el que se enfatiza en la pureza funcional, fue pionero en eluso de mónadas al separar la entrada y salida (IO) de lo que es puramente código.

15Dentro de la programación funcional es posible reconocer esta operación como bind

Página 139

Page 156: Programación Funcional en Scala

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 gcase None => None

}== opt flatMap (x => f(x) flatMap g)

Página 140

Page 157: Programación Funcional en Scala

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 ladefinició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 yz <- 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 yaque é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 daruna 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 unamónada estarán disponibles también los métodos map y map2. Ahora el rasgo podría heredarde 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ónadasson funtores pero no todos los funtores son mónadas”.

16En esta ocasión se han utilizado dos parámetros de tipo para la definición de la función flatMap, algo quedifiere de la definición vista en el algoritmo4.7 ya que en la signatura del rasgo Monada se ha utilizado el _ paraindicar que Monada recibe un parámetro existencial desconocido.

Página 141

Page 158: Programación Funcional en Scala

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 lainformación que rodea a un valor del mismo modo que envolvería el propio valor, minimizandoel 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 Scalacomo puedan ser los tipos List o Either17. Ambas clases monádicas presentan característicascomunes:

Implementan el método flatMap y la construcción haciendo uso del método de fábricaapply 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 ejemplificadaen la mónada identidad, la cual sólo se encargará de envolver los datos. Para ello, se definirá laclase parametrizada Id que recibirá un único parámetro, el valor que se desea envolver. Dentrode 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ólose 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.

17Tomando como operaciones unit(x)=List(x) en las listas o, en el caso de los conjuntos, unit(x)=Set(x).

Página 142

Page 159: Programación Funcional en Scala

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 valoresson substituidos en la expresión x+y.

Ahora se puede afirmar que es posible utilizar las mónadas para envolver valores, o algomás contundente, la mónada es un tipo de lenguaje de programación que soporta la sustituciónde 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 seejecuta 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 lacárcel. Para ello necesitará conseguir la llave maestra que abre la celda en la que está encerradosu amigo. La misión de conseguir la llave maestra será muy peligrosa, ya que está custodiadapor la guardia. Se sabe que la guardia posee 5 llaves: dos llaves amarillas, dos llaves verdes y unallave roja. La llave roja abre el teatro. Las dos llaves amarillas son indistinguibles entre si. Unallave 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 demodo que el director tiene dos llaves, una verde y otra amarilla, pero no sabe que puerta abrecada una. El guardia se queda con las otras tres llaves en un llavero y éstas serán las llaves quenuestro héroe deberá conseguir. Si el guardián que vigila la llave descubre a el héroe, la partidahabrá finalizado. Aunque si el héroe tiene fortuna, podrá coger el llavero con las tres llavesaprovechando 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.

18Adaptación del juego publicado en [19]

Página 143

Page 160: Programación Funcional en Scala

Una vez más, el héroe deberá ser afortunado ya que ni el director, ni el guardia encargado decustodiar 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 deljuego.

1 trait Llave{2 val maestra:Boolean3 }4 case class Amarilla(maestra:Boolean) extends Llave5 case object Roja extends Llave{6 val maestra=false;7 }8 case class Verde(maestra:Boolean) extends Llave9

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]):Liberado5 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 planteadosin 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étodosconseguirLlave y rescatar que la clase JailBreak tiene que implementar, ya que se encuentrandefinidos 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

Page 161: Programación Funcional en Scala

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 tienesla llave maestra")

9 Liberado("Luis")10 }11

12 // Metodos auxiliares13 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 claseJailBreak 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éroees 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 falloen la ejecución del programa.

Siguiendo la definición del problema, existe un 50 % de posibilidades de que el guardadescubra 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 probabilidadesse han implementando utilizando el método nextBoolean() del objeto Random definidoen scala.util.Random19, el cual generará aleatoriamente valores booleanos.

El método auxiliar llaveCorrecta es utilizado para determinar si se encuentra la llavecorrecta 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étodoconseguirLlave. Si el método finaliza como se espera, el algoritmo volverá a estar bloqueado enla 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 presentarestos problemas, ya que nada hace pensar que esta resolución simple pueda fallar, se ha vistocomo puede bloquearse la ejecución del mismo y provocar un funcionamiento incorrecto delprograma.

19Biblioteca que habrá que importar para la compilación del programa.

Página 145

Page 162: Programación Funcional en Scala

A continuación se muestra ejemplificado el funcionamiento del programa en el REPL deScala, tras haber introducido previamente las definiciones vistas en los algoritmos 4.9, 4.10 y4.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. Enla Sección 4.4: Manejo de errores sin usar excepciones « página 149 » se verán las clasesmonádicas Option y Either en el ámbito de manejar posibles errores sin necesidad de lanzarexcepciones. Para las situaciones en las que se tenga que lidiar con excepciones existe otramónada, la mónada Try20, la cual nos permitirá capturar excepciones en un envoltorio, informarde 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, querepresentan las situaciones de éxito y fallo en la ejecución de un bloque, método. . . Se puedever como el método de fábrica apply del objeto acompañante de Try, recibe un parámetro quesigue la estrategia de evaluación evaluación no estricta. El motivo es que se pretende capturarel resultado de la evaluación de este parámetro y si se hubiera pasado por valor (evaluaciónestricta) no se podría capturar su comportamiento, el cual se captura posteriormente dentro delos bloques try/catch.

A continuación, se verán las modificaciones necesarias que habrá que realizar al códigode los ejemplos anteriores para hacer uso de la clase monádica Try. En el algoritmo 4.13 semuestra como quedaría la definición del rasgo Game realizada previamente en el algoritmo 4.10,

20Estrictamente hablando, Try no sería una mónada ya que si se comprueban las reglas que han de cumplir lasmónadas se puede comprobar como falla la regla derecha de unit.

Página 146

Page 163: Programación Funcional en Scala

mientras que el algoritmo 4.14 muestra como quedaría la clase JailBreak, vista anteriormenteen 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 yconseguirLlave 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 llavemaestra")

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, formandoun bloque, el cual se pasa como parámetro al método de fábrica apply del objeto acompañantede Try.

El código de la solución al juego vista en el algoritmo 4.11 también se verá afectada por eluso 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)=>failure6 }

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 demanejar esta mónada, entre las que se encuentran las siguientes funciones:

Página 147

Page 164: Programación Funcional en Scala

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, enprimer 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 elcó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 elque la variable llaves permite una legibilidad más clara del código. Evidentemente, con ambassoluciones 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á ahoraque 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 serutilizadas en expresiones for. En el algoritmo 4.18 de expresa el código del algoritmo 4.17usando expresiones for.

Página 148

Page 165: Programación Funcional en Scala

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ónComo 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 queno 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 programasalta 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 sustituirlos 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 unasituación especial.

Para manejar situaciones excepcionales existen diferentes opciones:

Lanzar excepciones.

Incluir un argumento en la llamada a nuestras funciones indicando qué hacer cuando seproduzcan 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 mediacalcula 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 casosexcepcionales. Ahora se mostrará una solución basada en la inclusión de un argumento paraindicar 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) siVacia3 else xs.sum / xs.length

Algoritmo 4.20: Excepciones.Función media con argumento para caso especial

Página 149

Page 166: Programación Funcional en Scala

Esta opción presenta una ventaja, ya que se ha convertido la función media en una funcióntotal. 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 labiblioteca 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 quecontendrá el primer y el último elemento de una estructura iterable (como por ejemplo laslistas) 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, elhecho de que en alguna situación especial el valor devuelto no esté definido. Para comprendermejor 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) None3 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 (ennuestro ejemplo Option[Int]) para cada una de las entradas posibles, por lo que la función mediaserá una función total. Este es otro de los usos del tipo de datos Option, convertir funcionesparciales en funciones totales.

Un inconveniente que presenta el tipo de datos Option para el manejo de errores y casosespeciales es que no aporta información sobre el error o situación especial, simplemente sedevuelve None.

Página 150

Page 167: Programación Funcional en Scala

4.4.3. Tipo de datos Either

Si se necesitara obtener más información sobre el error que se ha producido para su posteriortratamiento, el tipo de datos Option no se ajustará a estas nuevas necesidades, ya que sólo indicaque 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 errorque se ha producido se utilizará el tipo de datos Either. Para comprender mejor esta opción parael manejo de errores y casos especiales, se verá en primer lugar la definición del tipo de datosEither:

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 unvalor. 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 elmanejo de errores, por convención, el constructor Left se utiliza para indicar el error, fallo. . . quese 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 else5 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 sobreel 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. EjerciciosEjercicio 62. Se van a realizar búsquedas dentro de una base de datos de familias. El resultadode la búsqueda puede ser un valor y también puede ser que no tenga éxito. Implementar lamónada Maybe que capture este comportamiento.

Ejercicio 63. Dada la siguiente definición de la clase y el objeto persona:

Página 151

Page 168: Programación Funcional en Scala

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 => MaybeNot22 }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 lapersona 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 lapersona pasada como argumento.

3. Definir la función abueloPaterno que devuelva la persona que es el abuelo materno de lapersona pasada como argumento.

4. Definir la función abuelaPaterna que devuelva la persona que es el abuelo materno de lapersona pasada como argumento.

Ejercicio 64. Definir la función abuelos que devuelva una dupla con los dos abuelos de lapersona pasada como argumento. Utilizar la función flatMap.

Ejercicio 65. Definir la función abuelos que devuelva una dupla con los dos abuelos de lapersona pasada como argumento. Utilizar bucles for.

Página 152

Page 169: Programación Funcional en Scala

Ejercicio 66. Del mismo modo que se ha utilizado en la definición de las funciones de losejercicios 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, comopará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ámetroy devuelva el resultado de realizar la raíz cuadrada al número pasado como argumento en casode 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 definidasanteriormente.

Página 153

Page 170: Programación Funcional en Scala

Página 154

Page 171: Programación Funcional en Scala

Capítulo 5

Tests en Scala

El desarrollo guiado por pruebas de software (Test Driven Development (TDD)), es unaprá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 dedesarrollo á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 elcomportamiento 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 AssertsPara 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 importadosautomáticamente en todos los ficheros fuente.

5.1.1. AssertSe 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ónque 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 secumple 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 puedeser 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á unmétodo que calcule el enésimo término de la serie de Fibonacci2.

1Procesos de integración continua2La 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

Page 172: Programación Funcional en Scala

1 def fibonacci(x:Int):Int= x match{2 case 0 => 03 case 1 => 14 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 eltérmino fibonaccix−1, es decir, fibonaccix−2 ≤ fibonaccix−1∀x : x ≥ 2, lo cual se podríacomprobar con la siguiente sentencia assert3:

1 def fibonacci(x:Int):Int= x match{2 case 0 => 03 case 1 => 14 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 mismacondición pero se añadirá una explicación:

1 def fibonacci(x:Int):Int= x match{2 case 0 => 03 case 1 => 14 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á:3Se 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úmerode sumas realizadas.

Página 156

Page 173: Programación Funcional en Scala

fibonacci: (x: Int)Int

scala> fibonacci(3)java.lang.AssertionError: assertion failed: El fibonacci de 1 no es menor que el fibonacci

de 2at 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 lanzasí que es de utilidad. La afirmación es correcta excepto para los dos primeros términos. Como sesabía, se partía de una premisa que no era cierta. Por tanto, se modificará la afirmación realizadapreviamente, de modo que ahora si esté definida correctamente4:

1 def fibonacci(x:Int):Int= x match{2 case 0 => 03 case 1 => 14 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 antesde que devuelva un valor5. El método ensuring podrá ser utilizado con cualquier tipo, ya quese realiza una conversión implícita. El método ensuring tomará como argumento una condiciónbooleana en forma de predicado. Si ensuring devuelve true, entonces el método devolverá elvalor, en otro caso se lanzará una excepción el tipo AssertError. Volviendo al ejemplo quecalcula 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 => 03 case 1 => 14 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 mayoro igual que uno, lo cual debería de fallar al calcular fibonacci26. De vuelta al REPL se observaque, con esta afirmación, se lanza una excepción del tipo AssertionError:

4Obsérvese que ahora se usa ≤.5La realización de comprobaciones al final del método se le llama postcondiciones técnicamente.6Ya que se producirá una llamada recursiva a fibonacci0 que provocará el lanzamiento de la excepción

Página 157

Page 174: Programación Funcional en Scala

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 => 04 case 1 => 15 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 elque usar este método. Para ello se recordará la definición del método inversa de una lista queaparece en el algoritmo 3.10 (página 93):

1 def inversa[A](xs:Lista[A]):Lista[A]={2 @annotation.tailrec3 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match {4 case Nil => res5 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 lainversa de la misma han de ser iguales:

1 def inversa[A](xs:Lista[A]):Lista[A]={2 @annotation.tailrec3 def go(xs:Lista[A],res:Lista[A]):Lista[A]= xs match {4 case Nil => res5 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

7Para 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 unapostcondición.

Página 158

Page 175: Programación Funcional en Scala

En la expresión (xs.length) == _.length, el guión bajo hace referencia al argumento pasadoal 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 realizartests al código en Scala. En primer lugar, se podrá optar por utilizar las herramientas de testspropias 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 deprueba y haciendo que el código sea más coherente, legible y fácil de comprender que si seintentara incluir los casos de prueba en el mismo12, sobre todo cuando el software es complejoy tiene una dimensión considerable.

A continuación se verán las opciones que ofrece el framework ScalaTest y cómo se podránimplementar 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ándarde Scala, por lo que antes de empezar a realizar pruebas al código habrá que instalarlo en nuestrosistema13.

El framework de ScalaTest ofrece diversos estilos para realizar pruebas al software, cada unode ellos desarrollado para cubrir las diferentes necesidades de los programadores. La elecciónde cada tipo sólo determinará cómo se quieren expresar los tests, no el tipo de test que se puederealizar.

La forma más fácil de implementar el primer conjunto de pruebas es crear un clase queextienda de org.scalatest.Suite. El rasgo Suite incluye un método execute, el cual es sobreescritoen cada uno de los estilos que el framework ScalaTest ofrece a los programadores. En la tabla5.1 se pueden ver los distintos estilos disponibles en ScalaTest:

8http://junit.org9http://testng.org

10http://www.scalatest.org/11http://www.scalatest.org/12Utilizando afirmaciones, por ejemplo.13Habrá 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 deScala para Eclipse.

Página 159

Page 176: Programación Funcional en Scala

Suites en ScalaTestFunSuite

Facilita la codificación de los nombres descriptivos de laspruebas.

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

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, algoque 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 derequisitos 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

Page 177: Programación Funcional en Scala

1

2 import org.scalatest.Suite3 class EjemploSuite extends Suite {4 def testAssert() {5 val v1 = true6 val v2 = true7 assert(v1 == v2)8 }9 def testAssertResult() {

10 val v1 = 1511 val v2 = 2212 assertResult(37) {13 v1 + v214 }15 }16 def testIntercept() {17 val v1 = 918 val v2 = 019 intercept[ArithmeticException] {20 v1 / v221 }22 }23 def testExpect() {24 val v1 = 1225 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 segundopará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 primerargumento 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 dela prueba será una función, la cual será evaluada por el método test siguiendo la estrategia depaso de parámetros por necesidad o evaluación perezosa (véase la Subsección 1.4.3: Sistema deEvaluación de Scala « página 19 »).

14En referencia a función

Página 161

Page 178: Programación Funcional en Scala

A continuación se muestra un ejemplo simple del uso de FunSuite, definiendo algunos casosde prueba para el tipo abstracto de datos Lista definido en el algoritmo 3.9(página 91):

1 import org.scalatest.FunSuite2 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 head16 }17 }18 test("Invocamos el metodo !! con un elemento que no existe"){19 intercept[IllegalArgumentException]{20 miLista !! 221 }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 deMiLista deber ser 1”. La diferencia entre el uso del operador de igualdad habitual y el uso deltriple igual radica en la información que obtenemos en caso de que la afirmación falle. Cuandouna afirmación realizada con assert falla, sólo se sabrá que se ha lanzado una excepción deltipo AssertionError y un mensaje indicando el número de línea en el que se ha producido elfallo, pero no se sabrá qué valores son los que han producido el fallo. Se podrían conocer losvalores que producen el fallo, incluyendo una aplicación en el assert que nos proporcione dichainformación, como se vio en el algoritmo 5.4, pero resulta más apropiado el uso del triple igualpara obtener esta información. Igualmente, el triple igual no indicará cual era el valor esperadoy 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. EjerciciosEjercicio 69. Utilizar la suite FunSuite de ScalaTest para comprobar que la suma de númerosenteros 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

Page 179: Programación Funcional en Scala

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 Maybecumple con las reglas de las mónadas. Antes de comenzar con la solución del ejercicio, serecordará 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]) = MaybeNot14 override def map[B](f:Nothing=>B):Maybe[B]=MaybeNot15 }

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 losejercicios 64 y 65.

Ejercicio 73. Utilizar la suite FunSuite de ScalaTest para comprobar que se obtiene el mismoresultado al calcular los abuelos maternos de una persona utilizando las funciones definidas enel ejercicio 63.

Página 163

Page 180: Programación Funcional en Scala

Página 164

Page 181: Programación Funcional en Scala

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 yproduciendo salidas en respuesta a los mismos.

Durante la fase de diseño de un software que deba interaccionar con un sistema reactivo setendrá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 yproduciendo salidas en respuesta a losmismos.

Toma unos datos de entrada y devuelveuna salida.

El orden de los eventos en el sistema noes predecible, viene determinado externa-mente.

El orden de entrada de los datos estápreestablecido.

La ejecución de los sistemas reactivos notiene por qué terminar.

Su ejecución debe finalizar.

Tabla 6.1: Sistema Reactivo Vs Sistema Transformacional

Página 165

Page 182: Programación Funcional en Scala

6.1.2. Speed-Up en programación concurrenteLa 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 conrespecto a su versión paralela se define como: G = T iempoSecuencial

T iempoParalelodonde, teóricamente, G de-

bería ser igual o muy próximo al número de procesadores usados, aunque en la realidad existendiversos 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 mayordesde 2005, momento en el cual se puede observar un punto de inflexión en la tendencia defabricación de microprocesadores ya que se apuesta por el aumento del número de núcleos delos procesadores, en lugar de aumentar la velocidad de los procesadores. El número de núcleosde 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 cualpuede 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 enmultitarea no comparten información, es decir, se ejecuta cada una de forma independiente, lastareas 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 cualimplicará que el desarrollo de estos programas se realice de distinta forma que los programassecuenciales.

6.1.3. Problemática6.1.3.1. Propiedades de los programas concurrentes

Cuando se desarrollan programas concurrentes habrá que asegurarse de que cumplen unconjunto 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 delos programas secuenciales. Vienen a decir que, si el programa acaba, lo hace con un resultadocorrecto.2

1Algo que se hace desde las primeras versiones de Linux.2Suelen poder asegurarse a costa de perder parte de la concurrencia.

Página 166

Page 183: Programación Funcional en Scala

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 crucede 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 noserá 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 unaacció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 losrecursos 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ánhaciendo 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/Lockso semáforos. La diferencia entre ambos es que los semáforos permiten el acceso a múltipleshebras definidas previamente que pueden tener acceso a las secciones críticas, mientras losmutex 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 CPUquedará inactiva excepto si existen otras hebras en espera de ejecución. Además tiene ungran coste despertar y volver a ejecutar una hebra bloqueada. Como consecuencia, losprogramas 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ñanaplicaciones complejas y de un tamaño considerable, principalmente debido a las dificultadesque entraña el modelo de concurrencia basado en procesos sincronizados interactuando me-diante memoria compartida, así como la complejidad que supone garantizar las propiedades deseguridad y vivacidad.

3Fragmento de código donde puede modificarse un recurso compartido y, por tanto, el resto de procesos deberánver como una acción atómica

Página 167

Page 184: Programación Funcional en Scala

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 elmodelo 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 hebraspuedan estar accediendo o modificando simultáneamente el mismo dato y cuáles son losbloqueos que pueden estar activos.

Cada vez que se realice una llamada a un método se deberá de tener en cuenta los bloqueosque pueda tener y esperar que no se produzcan DeadLock.

El programa puede crear nuevos bloqueos durante la ejecución del mismo, por lo queestos 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íanrealizar miles de pruebas obteniendo resultados satisfactorios y que falle la primera vezque 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 intentanmodificar algún valor en la misma se dirá que se dan condiciones de carrera ya que elplanificador puede cambiar de hebra en cualquier momento, por lo que no se conocerá elorden 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 demanera secuencial. Expresándolo de manera diferente, los actores permiten escribir programasmultihilo como un conjunto independiente de programas monohilo. La simplificación anteriorse cumple siempre y cuando el único mecanismo de comunicación entre actores sea el paso demensajes.

6.2. Modelo de actores

6.2.1. Origen del Modelo de ActoresEl 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 vezpor Carl Hewitt, Bishop y Steiger en 1973[12] cuando buscaban crear un modelo con el quepoder formular los programas de sus investigaciones en Inteligencia Artificial.

Página 168

Page 185: Programación Funcional en Scala

En 1986, Ericsson comenzó el desarrollo del lenguaje de programación Erlang, un lenguajede programación funcional puro que basaba su modelo de concurrencia en actores en lugar dehilos 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 maneranotable 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 2006añadió la librería estándar que da soporte a este modelo en Scala. Jonas Bonér, influenciadotanto por Earlang como por el modelo de actores de Scala, creó Akka en 2009.

6.2.2. Filosofía del Modelo de ActoresEl 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 representanobjetos y el modelo de actores describe las interacciones entre dichos objetos inspirándose encó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 abstractoscon 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 actoresofrece una solución basada en buzones y paso de mensajes (eventos) asíncronos. Los mensajesson almacenados en estos buzones y posteriormente recuperados para su procesamiento por losactores. En lugar de compartir variables en memoria, el uso de estos buzones permite aislar cadauno de los procesos.

Los actores son entidades independientes con identidad propia, un estado que puede variardurante la ejecución y que no comparten ningún tipo de memoria para llevar a cabo el proceso decomunicación. De hecho, los actores únicamente se pueden comunicar a través de los buzonesdescritos en el párrafo anterior.

El comportamiento de los actores se definirá en una función a la que se le pasará cada uno delos mensajes recibidos y en la que se indicarán las acciones a llevar a cabo en respuesta a cadamensaje en un momento concreto de la ejecución. Los actores procesan los mensajes en tiempode ejecución por lo que no hay una comprobación previa de los mismos, para poder conocer siun 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 actoreliminará 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 actorsó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 actoreso 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

4En referencia al intercambio de mensajes entre humanos

Página 169

Page 186: Programación Funcional en Scala

estado que sean leídas por la lógica de la función de comportamiento o bien porque se cambiedurante 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á asegurarque se cumplen las propiedades de seguridad de los programas concurrentes, ya que no existiránsecciones 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 actualizacionesde datos,. . . ) no deberían de darse en este modelo. Los actores están pensados para trabajar demanera 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 extenderscala.actors.Actor5 e implementar el método act, el cual definirá cómo procesará el actor losmensajes. El siguiente fragmento de código ilustra un actor sumamente simple que no realizanada 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 ActorEjecutando Primer ActorEjecutando Primer ActorEjecutando Primer ActorEjecutando Primer ActorEjecutando Primer ActorEjecutando Primer ActorEjecutando Primer ActorEjecutando Primer ActorEjecutando Primer ActorEjecutando Primer Actor

Se puede apreciar como la salida Ejecutando Primer Actor se intercala con el prompt deScala ya que la hebra del actor se ejecuta independientemente de la hebra del shell. Los actoressiempre se ejecutarán independientemente unos de otros.

Otro mecanismo diferente que permitiría instanciar un actor sería hacer uso del métodoactor, muy útil y disponible en scala.actors.Actor:

5Hasta la versión 2.12 de Scala en la que la biblioteca scala.actors desaparecerá.

Página 170

Page 187: Programación Funcional en Scala

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 comose 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á unmensaje 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 lainstrucción receive6. El siguiente fragmento de código ilustra el comportamiento descrito:

scala> echoActor ! "Primer Mensaje"Mensaje recibido Primer Mensajescala> echoActor ! 11Mensaje recibido 11

Algoritmo 6.3: Mi primer mensaje

Un actor sólo podrá procesar los mensajes que coincidan con alguna de las definicionescase definidas en la función parcial receive. Receive devuelve el valor de la última expresiónevaluada, 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 demensajes:

6receiveWithin es una variante a receive en la que se puede indicar el tiempo de espera en milisegundos, unavez superado el tiempo de espera devolverá TIMEOUT

Página 171

Page 188: Programación Funcional en Scala

1 import scala.actors._2

3 class Contador extends Actor {4 private var count = 05 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 -= 114 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” entoncesse 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 apuntaa un estado no permanente, algo que no se ajusta a la programación funcional desde un puntode vista estricto. Si se quisiera definir el contador con un estado inmutable, se podría hacer conuna solución similar a la aportada en el siguiente código:

Página 172

Page 189: Programación Funcional en Scala

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 + 19 println ("El valor del contador es "+newcount)

10 run(newcount)}11

12 case "decr"=> {13 val newcount = cnt - 114 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 lareasignación de variables en bucles y optimizar el uso de la pila, la recursión de cola (ver laSubsección 3.4.2: Recursión de cola « página 62 »). A continuación se comprobará que elfuncionamiento 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 unmensaje.

6.3.3. Mejora del rendimiento con reactComo 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 actorescon su propio método act y ejecutar cada uno en su propia hebra pero las máquinas virtuales

Página 173

Page 190: Programación Funcional en Scala

típicas de Java, capaces de almacenar millones de objetos, sólo pueden manejar unos pocoscientos de hebras7.

Para intentar dar una solución a esta problemática, Scala incorpora react, una alternativa almétodo receive8.

En la tabla 6.2 aparecen reflejadas las principales diferencias entre receive y react.

Receive Vs ReactToma una funciónparcial

Toma una funciónparcial

Devuelve el valor dela última expresiónevaluada

No devuelve ningúnvalor

Preserva la pila dellamadas de la hebraactual

No preserva pila dellamadas

La biblioteca nopuede reutilizar lahebra

La biblioteca reuti-lizará la hebra paraotro actor

Tabla 6.2: Receive Vs React

La biblioteca scala.actors.Actor incorpora la función loop que ayudará a crear los actoresbasados 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 bibliotecascala.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

7Los cambios de contexto de una hebra a otro suelen tener asociado un coste de cientos o miles ciclos deprocesador

8En la práctica, los programas necesitarán al menos unos pocos métodos receive pero intentaremos utilizarreact cuando nos sea posible para preservar las hebras.

Página 174

Page 191: Programación Funcional en Scala

6.4. Actores en Scala con Akka

6.4.1. IntroducciónAkka 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 haceespecial énfasis en la concurrencia basada en el modelo de actores. Akka está escrita en Scalay, 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, sinparadas y con capacidad para auto-repararse.

Transparencia local. Akka ha sido diseñada para trabajar en entornos distribuidos. Lasinteracciones 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 volviendoa 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 deScala 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 dela interfaz ActorRef. Se podrán obtener instancias de ActorRef invocando el método actordel 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:

9Sustituyendo la implementación original del modelo de actores que será finalmente eliminada en la versión2.12 de Scala

Página 175

Page 192: Programación Funcional en Scala

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 definition3 }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 MyActor3 }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 sedefine implementando el método act. En Akka hay un único manejador de mensajes, unafunción parcial devuelta por el método recieve que se aplicará a cada uno de los mensajesexistentes en el buzón de un actor. Si se quisiera incluir algún código de iniciación, habríaque incluirlo en el método preStart. Ejemplo:

1 def act() {2 // codigo de iniciacion3 loop {4 react { //cuerpo }5 }6 }

Algoritmo 6.15: Eliminación de act. Ejemplo en Scala

10Todas las referencias al objeto MyActor ahora deberán de hacerse a MyActor.ref

Página 176

Page 193: Programación Funcional en Scala

1 override def preStart() {2 // codigo de iniciacion3 }4 def receive = {5 // body6 }

Algoritmo 6.16: Eliminación de act. Ejemplo en Akka

Cambio de la signatura de métodos. Habrá que tener en cuenta que, aunque conservenla 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ásdestacables entre el modelo de actores de Scala y el de Akka, es el momento de tener unaprimera 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:Receive6

7 ...8

9 }

Algoritmo 6.17: Akka.Trait Actor

A continuación, se implementará en Akka un actor con un comportamiento igual al visto enla Subsección 6.3.2: Estado de los actores « página 171 » y sabiendo que el estado de un actoren Akka, al igual que ocurría en el modelo de actores de Scala, se puede implementar tanto convariables 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 algoritmo6.4 (página 172) en el que el valor del contador se guarda en una variable privada.

11receive es una función parcial de Any a Unit y describe la respuesta del actor a un mensaje.

Página 177

Page 194: Programación Funcional en Scala

1 import akka.actor.Actor2

3 class Contador extends Actor {4 private var count = 05 def receive = {6 case "incr" => {7 count += 18 println ("El valor del contador es "+count)}9 case "decr" => {

10 count -= 111 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 ajustana las descritas anteriormente en la Subsubsección 6.4.1.1: Diferencias entre Akka y la libreríaActors de Scala « página 175 ».

A continuación se implementará en Akka un actor similar al implementado en el algoritmo6.6 (página 173), es decir, se implementará un actor capaz de mantener su estado sin necesidadde utilizar variables reasignables que puedan dar lugar a comportamientos no deseados. En unaprimera aproximación se podría escribir:

1 import akka.actor.Actor2

3 class Contador extends Actor {4 def receive = run(0)5 private def run(cnt:Int):Receive = {6 case "incr" => {7 val newcount = cnt + 18 println ("El valor del contador es "+newcount)9 run(newcount)}

10

11 case "decr"=> {12 val newcount = cnt - 113 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 quedefine su comportamiento durante la ejecución del programa, algo que se puede aprovecharpara 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 descritoes el ActorContext asociado a cada actor. Para comprender todo esto mejor, se muestra el rasgoActorContext y cómo se refleja en el rasgo Actor:

Página 178

Page 195: Programación Funcional en Scala

1 trait ActorContext {2 def become(behavior:Receive, discardOld:Boolean = true):Unit3 def unbecome():Unit4 }5

6 trait Actor {7 implicit val context: ActorContext8 def receive:Receive9

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 encuentrasiempre el comportamiento activo en ese momento. Los métodos become y unbecome estánrelacionados 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 elcomportamiento 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.Actor2

3 class Contador extends Actor {4 def receive = run(0)5 private def run(cnt:Int):Receive = {6 case "incr" => {7 val newcount = cnt + 18 println ("El valor del contador es "+newcount)9 context.become(run(newcount))}

10

11 case "decr"=> {12 val newcount = cnt - 113 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 seaun objeto inmutable ya que se realiza una llamada a la función run dentro de si misma, peroesta llamada es asíncrona ya que context.become evaluará el nuevo comportamiento cuando seprocese el siguiente mensaje.

Otra las acciones que los actores pueden hacer es intercambiar mensajes. Hasta el momentose ha visto cómo se pueden mandar mensajes de un actor a un segundo actor, pero no cómo estesegundo 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

Page 196: Programación Funcional en Scala

un momento dado y, al mismo tiempo, permita prescindir de las sentencias println en nuestroactor 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 decómo se puede utilizar:

1

2 trait Actor {3 implicit val self: ActorRef4 implicit val context: ActorContext5 def receive: Receive6 def sender: ActorRef7

8 ...9

10 }11 abstract class ActorRef {12 def !(msg:Any)(implicit sender: ActorRef =

Actor.noSender):Unit13 ...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ícitahaciendo uso de self. Cuando se envía un mensaje a otro actor, la dirección del remitente seenvía de forma implícita haciendo referencia a la variable inmutable self. El actor que recibe elmensaje 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 elvalor del mismo al actor que haga la petición:

1 import akka.actor.Actor2

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

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

Page 197: Programación Funcional en Scala

operaciones. En el siguiente ejemplo se ven dichos métodos:1 trait ActorContext {2 def become(behavior:Receive, discardOld:Boolean = true):Unit3 def unbecome():Unit4 def actorOf(p:Props, name: String): ActorRef5 def stop(a: ActorRef): Unit6 }

Algoritmo 6.24: Akka.Trait ActorContext II

En primer lugar, para crear actores se utilizará el método actorOf, cuyo primer parámetrode tipo Props servirá para describir cómo crear el actor y el segundo parámetro servirá paraasignarle un nombre. Los actores son siempre creados por otros actores, por lo que formarán unsistema jerárquico.

Un actor puede parar otros actores haciendo uso del método stop, el cual frecuentemente iráacompañado de self 12.

Mostraremos un ejemplo definiendo un nuevo actor que haga uso del actor Contador y quereciba la cuenta del contador después de varios mensajes:

1 import akka.actor.Actor2 import akka.actor.Props3

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áspotente, ofreciendo al programador diversas herramientas que facilitarán el diseño de programasconcurrentes complejos.[19]

6.5. Buenas prácticasLlegados 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ónconcurrente basado en actores por lo que, en la medida que se puedan escribir siguiendo este

12Lo que significará que el actor desea parar su ejecución

Página 181

Page 198: Programación Funcional en Scala

estilo, el código será más sencillo de depurar y tendrá menos interbloqueos y condiciones decarrera. Los siguientes apartados describen, de manera breve, algunas directrices que permitiránadoptar un estilo de programación basado en actores.

6.5.1. Ausencia de bloqueosUn 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 elprimero. 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ánesperando 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 quela acción está lista para ser ejecutada. Esta nueva disposición, por norma general, implicará laparticipación de otros actores.

6.5.2. Comunicación exclusiva mediante mensajesLa 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 demanera 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 únicomecanismo de comunicación entre actores sea el paso de mensajes.

6.5.3. Mensajes inmutablesPuesto 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 dichométodo son thread-safe. Este es el motivo por el que el modelo de actores es llamado shared-nothing, los datos están confinados en un único hilo en lugar de ser compartidos por varios.

La excepción a esta regla reside en la información de los mensajes intercambiados entre ac-tores, dado que es compartida por varios de ellos. Por tanto, habrá que prestar especial atenciónal hecho de que los mensajes intercambiados entre actores sean thread-safe.

6.5.4. Mensajes autocontenidosCuando 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 quetardará la respuesta, instantes en los que dicho actor no debería bloquearse, sino que deberíacontinuar ejecutando otro trabajo hasta que la respuesta a su petición le sea enviada. ¿Puede elactor 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árrafoanterior:

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

Page 199: Programación Funcional en Scala

Otro mecanismo adicional que permitiría incrementar la redundancia en los mensajessería la utilización de una clase diferente para cada una de las clases de mensajes de losque se dispongan.

6.6. EjerciciosEjercicio 74. Implementar un sistema concurrente en el que dos actores jueguen al pin-pongde forma que el primero de ellos inicie el juego con la palabra “ping” y el adversario respondacon la palabra “pong”. Después de tres intercambios de mensajes el juego finalizará.

Página 183

Page 200: Programación Funcional en Scala

Página 184

Page 201: Programación Funcional en Scala

Capítulo 7

Conclusiones

Scala presenta las características del paradigma de la programación funcional y el paradigmade la POO, ofreciendo un amplio conjunto de mecanismos para la resolución de problemas, loque permite a los desarrolladores seleccionar la técnica que mejor se adapte a las característicasdel problema que están tratando de resolver.

En primer lugar, se ha presentado Scala como lenguaje de programación, introduciendo loselementos 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 operadoresexistentes para esos tipos básicos. Se ha explicado cómo crear nuevos operadores y cómo definircaracterísticas propias de los operadores como son la prioridad y la asociatividad, las cualesdeterminará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 lavisibilidad 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 querespondan 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 amétodos en Scala. También se ha indicado cómo se puede cambiar la estrategia de evaluaciónde los parámetros de las funciones y utilizar evaluación no estricta o por nombre, separando deesta 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ónperezosa en Scala, estrategia usada por defecto en otros lenguajes de programación funcionalcomo Haskell.

Por tanto, se puede apreciar que Scala es un lenguaje que puede ser utilizado para estudiarlos conceptos fundamentales de los lenguajes de programación.

Cuando se aprende un lenguaje de POO, las clases son uno de los primeros conceptos queson estudiados y, por tanto, algo que se tenía que definir ya que Scala es un lenguaje orientadoa 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 lenguajeorientado a objetos. Se ha visto el polimorfismo en el paradigma de la POO en referencia alsubtipado 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, lareusabilidad 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

Page 202: Programación Funcional en Scala

racterística a la clase, ya que, además de definir la clase un tipo propio, tiene la posibilidadde 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 tiposdiferentes.

Los tipos pueden hacer más restrictivas las clases y funciones, indicando cotas, o hacerlasmenos 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 tipoespecial de clases presentes en Scala, las case class (como por ejemplo en la concordancia depatrones) o las ventajas que ofrecen este tipo de clases como la creación de un método de fábricacon el nombre de la clase automáticamente.

En Scala, cada instancia de clase y cada literal corresponden a un tipo. Pero en Scala, lasclases son algo más que meros contenedores de valores y métodos. Por ejemplo, los rasgosotorgan una característica especial a las clases ya que, si se tiene definida una clase y diferentesrasgos, ésta puede ser instanciada con uno o varios de los rasgos definidos, lo que hará, porejemplo, que un mismo tipo pueda presentar diferentes comportamientos.

Tras este análisis de Scala como lenguaje orientado a objetos puro se puede observar queScala es un lenguaje en el que se ven reflejados los conceptos relacionados con la programaciónorientada 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 quees 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ónfuncional y la evaluación de expresiones.

Se ha utilizado el lenguaje para ver conceptos fundamentales asociados al paradigma de laprogramació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 iterativoshaciendo uso de la recursión. Profundizando un poco más, se muestra cómo trata Scala lasfunciones 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 usarlas 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énse ha puesto de manifiesto una de las características de Scala como es la potencia de usar laconcordancia de patrones, sobre todo en la definición de una estructura de datos lineal como lalista 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 razonesmás importantes para utilizar este lenguaje ya que ofrecen soluciones muy elegantes para unagran 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 prestadoespecial 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

1A estos tipos se los conocen con el nombre de constructores de tipos.

Página 186

Page 203: Programación Funcional en Scala

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 aplicarlos elementos de una colección pero el hecho de un sistema de tipos estáticos de la colección deorigen 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 lasfunciones de orden superior que manipulan las colecciones permitirán desarrollar un códigomuy expresivo y que minimice los errores de conversión de tipos en tiempo de ejecución, lo quese 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 operacionesque se vayan a realizar sobre las mismas.

Llegados a este punto se puede entender el hecho de que Scala se presente como un lenguajede 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 avanzadaaunque 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 constructoresde tipos3 y a los tipos compuestos4 y se introducen los tipos estructurales como una forma dedefinir tipos abstractos (tipos en los que se han especificado ciertas características), los tiposde orden superior como tipos que utilizan otros tipos para construir un tipo nuevo y los tiposexistenciales que nos permitirán indicar la existencia de un tipo sin especificar de qué tipo setrata.

Se ha presentado otra característica del lenguaje como es el método de búsqueda de Scala yla 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 tengandiferentes aplicaciones. Las clases se utilizan para hacer conversiones entre tipos, mientras queen 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 espaciode nombres y cómo cuando no es capaz de resolver un método, busca una posible conversiónexistente 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 importantecomo 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 delpatró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ónmónada, el cual presenta una serie de reglas que han de cumplir las clases monádicas para poderser utilizadas como mónadas. Se ha visto como para definir clases monádicas en Scala sólo setendrán que definir los métodos unit y flatMap6.

Se ha comprobado como el uso de ambos patrones, funtores y mónadas, nos permiten definircombinadores que serán aplicables a una gran cantidad de tipos de datos, tipos que en principio

2Scala también presenta un conjunto de colecciones mutables pero estas no han sido estudiadas por carecer deinterés dentro de la programación funcional.

3Clases parametrizadas4Resultado de la combinación de otros tipos ya conocidos5Como implementación de la operación map de Scala, también llamada fmap dentro de la literatura relativa a

la programación funcional6También conocidos como identity y bind dentro de la literatura.

Página 187

Page 204: Programación Funcional en Scala

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

Las utilidades de las mónadas se ha ejemplifican con uno de los usos de las mismas, comoenvoltorio (con la mónada identidad) o la importancia de utilizar la clase monádica Try paramanejar situaciones en las que aparecen excepciones.

Se ha estudiado cómo la utilización de las mónadas Either y Option nos pueden ayudar ala hora de manejar situaciones especiales durante el desarrollo de las estructuras de datos sinnecesidad de lanzar excepciones. Se ha visto cómo estos tipos algebraicos de datos nos ofrecenuna manera simple de razonar a la hora de manejar situaciones especiales y cómo se puedenaprovechar sus características modulares y de composición. El uso de estas mónadas, junto confunciones de orden superior, ayudan a tratar con errores de una manera especial. Algo que seríaimposible si se tuvieran que lanzar excepciones. De este modo, el uso de excepciones se dejaríapara 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 funtoresy las mónadas en Scala, es notable que Scala es un lenguaje de programación que se puedeutilizar para explicar los conceptos relacionados con la programación funcional avanzada.

También se han presentado, de forma resumida, algunas de las diferentes posibilidades quenos ofrece Scala para realizar comprobaciones al código escrito. Se analiza cómo se puedenutilizar algunas de estas posibilidades para hacer pruebas a las estructuras de datos y así poderconstatar que los resultados obtenidos son los esperados o que las estructuras cumplen con laspropiedades 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 demensajes inmutables. Inicialmente se ha explicado las bases del modelo de actores en Scala, labiblioteca scala.actors, para después introducir los principios y explicar las diferencias de unasolució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 cambiarel comportamiento de una clase.

Definitivamente es posible aseverar que Scala es un lenguaje en el que se pueden explicartanto 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 desarrollaraplicaciones 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 Javahacen 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

Page 205: Programación Funcional en Scala

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 conel 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 laexpresión es 23.

5. Se invoca a la función g, con el valor 23 como parámetro y . Para evaluar esta invocaciónse 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 resultadoya 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

Page 206: Programación Funcional en Scala

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 llamadasa 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ónn > 0. El segundo problema se podría solucionar haciendo una versión recursiva decola.

1 def fact2(n: Int):Int = {2 require (n>=0)3 @annotation.tailrec4 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

Page 207: Programación Funcional en Scala

Solución del ejercicio 12 11.

Solución del ejercicio 13 Sí.

Solución del ejercicio 14

20.

hellohello

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) => 15 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.tailrec4 def invierte(digito:Int,acum:Int):Int={5 digito match{6 case digito if (digito<10) => acum+digito7 case other => invierte((other / 10),((acum+digito % 10)*10))8 }9

10 }11 invierte(x,0)12

13 }

Página 191

Page 208: Programación Funcional en Scala

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.tailrec3 def go(fib2:Int,fib1:Int,y:Int):Int={4 if (x==y) fib15 else go(fib1,fib2+fib1,y+1)6 }7 if (x==0) 08 else go(0,1,1)9 }

Solución del ejercicio 21 Una posible solución sería:

1 type TotalSegundos=Int2 type Horas =Int3 type Minutos = Int4 type Segundos = Int5

6 def descomponer(seg:TotalSegundos):(Horas,Minutos,Segundos)={7 val horas = seg / 36008 val resto = seg % 36009 val minutos = resto / 60

10 val segundos = resto % 6011 (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)== 16 }

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) 15 else pascal(c - 1, r - 1) + pascal(c, r - 1)6 }

Página 192

Page 209: Programación Funcional en Scala

Solución del ejercicio 24

Una posible implementación de la función balanceado si el parámetro es una cadenapodrí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.tailrec3 def cuenta(carac:Char,cadena:List[Char],acc:Int):Int = cadena

match {4 case Nil => acc5 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 => Nil3 case _ => if (xs esVacia) Nil else (xs cabeza) :: take(xs

cola)(x-1)4 }

Solución del ejercicio 28

Página 193

Page 210: Programación Funcional en Scala

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.tailrec3 def go(xs:Lista[A],ys:Lista[B],zs:Lista[C]):Lista[C]= xs

match {4 case Nil => zs5 case otro => ys match{6 case Nil => zs7 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 311 /** 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

Page 211: Programación Funcional en Scala

Solución del ejercicio 32

1 def map[A,B](xs:Lista[A])(f:A=>B):Lista[B]= xs match {2 case Nil => Nil3 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.tailrec3 def go(xs:Lista[A],res:Lista[A],f:A=>Boolean):Lista[A]= xs

match{4 case Nil => res5 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.tailrec3 def go[A](xs:Lista[A],res:Lista[A],f:A=>Boolean):Lista[A]=

xs match{4 case Nil => res5 case other => if (f(other cabeza)) go(other.cola, res ##

(other cabeza),f) else res6 }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 => Nil3 case Cons(el,xss)=>if(f(el)) dropWhile(xss)(f) else xs4 }

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 => base3 case Cons(ele,xss)=>f(ele)(foldr(xss)(base)(f))4 }

Página 195

Page 212: Programación Funcional en Scala

Solución del ejercicio 37

1 @annotation.tailrec2 def foldl[A,B](xs:Lista[A])(base:B)(f:B=>A=>B):B=xs match{3 case Nil => base4 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 => Vacio4 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 elque 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 == Vacio3 case Nodo(raiz,izq,der) => A2 match {4 case Nodo(ov,a2izq,a2der) => izq.isomorfismo(a2izq) &&

der.isomorfismo(a2der)5 case _ => false6 }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 => true3 case Nodo(raiz ,izq,der) => izq.isomorfismo(der)4 }

Esta función irá dentro del trait ArbolBB.

Página 196

Page 213: Programación Funcional en Scala

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) xelse 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 => Nil3 case x :: Nil => Nil4 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 utilizarfoldLeft 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 dosfunciones parametrizadas. Por tanto, al definir el valor del caso base se inferirá el tipo delmismo (que a su vez debe de ser el tipo devuelto y el tipo del segundo parámetro de laoperación a realizar por foldRight), lo cual daría un error ya que el tipo de los elementosde xs es List[T]. La función fold define una cota superior1 para su parámetro en relaciónal parámetro T del tipo List[T], por lo que el tipo del caso base (Nil) se inferirá List[T].

1Como se vio en la Subsección 2.5.1: Acotación de tipos y varianza « página 47 ».

Página 197

Page 214: Programación Funcional en Scala

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 + y6 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 error17 }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::Nil5 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 => 03 case h::t => 1 + miLength(t)4 }

Solución del ejercicio 51 Una posible solución sería:

Página 198

Page 215: Programación Funcional en Scala

1 def penultimo(xs: List[Int]) : Int = xs match {2 | case x :: y :: Nil => x3 | 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 => true4 case head :: Nil => true5 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 delmé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 => Nil4 case x :: Nil => xs5 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 acc5 }).reverse

Página 199

Page 216: Programación Funcional en Scala

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 => (1to 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 iguales8 agrupaDuplicados(rest) match {9 case List(Nil) => List(List(x,y))//No habia mas elementos

en xs10 case restList :: restXs if (restList.head == x) => //Habia

mas elementos11 //iguales a x e y12 List(List(x,y) ::: restList) ::: restXs13 case rest => List(List(x,y)) ::: rest //Habia mas elementos

en xs pero14 //distintos a x e y15 }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.tailrec3 def go (datos: Seq[A], f: A=>B, acc: Map[B,A]): Map[B,A] =4 datos match {5 case Nil => acc6 case _ => go(datos.tail,f,acc + (f(datos.head)->datos.head))7 }8 go(datos,f,Map())9 }

Página 200

Page 217: Programación Funcional en Scala

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, enel 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.toList4 }

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.toList4 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 elmismo nombre y seguir utilizando el nombre como clave podría ser:

Página 201

Page 218: Programación Funcional en Scala

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

Se podría sobrecargar el método contactos, con la implementación de la función quedevuelva la lista vacía si no hay ningún contacto en la agenda que se corresponda conel nombre pasado como parámetro o una lista con todas los contactos almacenados en laagenda 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

Page 219: Programación Funcional en Scala

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 lavariable 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 aser analizado con más detalle:

1. Se ha iterado sobre cada palabra (listaPalabras(x)) utilizando el método map de lacolección Rango, obteniendo un Vector con las veces que se repite cada palabra conresultado 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 conlas tuplas formadas por listaPalabras y el vector resultante de la anterior acción.

1 listaPalabras.zip((0 untillistaPalabras.size).map(x=> listaPalabras.count(_== listaPalabras(x))))

3. Como las tuplas pueden encontrarse repetidas, tanto en listaPalabras como en elvector con la cuenta de la repetición de cada palabra, se eliminan las tuplas repetidastransformando el anterior resultado en un conjunto haciendo uso del método toSetde 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

Page 220: Programación Funcional en Scala

1 def numeroPalabrasDistintas(str:String):Map[String,Int] = {2 val listaPalabras = str.split("

").map(_.filter(_.isLetter).toLowerCase).filter(_ != "").toList3 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 elementosde 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 vioque cumplía las reglas de las mónadas en la Subsubsección 4.3.2.1: Reglas que deben satisfacerlas mónadas « página 139 » y cuyo comportamiento se vio en la Subsección 4.4.2: Tipo de datosOption « 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]) = MaybeNot14 override def map[B](f:Nothing=>B):Maybe[B]=MaybeNot15 }

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

Page 221: Programación Funcional en Scala

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

En realidad, se puede considerar la clase Maybe como una clase monádica cuyo método unites el método de fábrica apply del objeto acompañante que se crea automáticamente junto conlas 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.padre4 case MaybeNot => MaybeNot5 }

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

Page 222: Programación Funcional en Scala

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 => MaybeNot3 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 => MaybeNot3 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

Page 223: Programación Funcional en Scala

1 import org.scalatest.FunSuite2

3 class Test extends FunSuite{4 val a:Int=55 val b:Int=76 val c:Int=107 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 deenteros"){

2 assert(a*(b+c)==a*b+a*c)3 }

Solución del ejercicio 71A continuación se definirá una nueva clase Test para realizar las comprobaciones:

1 import org.scalatest.FunSuite2

3 class Test extends FunSuite {4 test("Regla izquierda de Unit"){5 Persona.personas foreach { p =>6

7 // Regla izquierda Unit8

9 assert((Just(p) flatMap { _.madre }) == p.madre)10 }11 }12 val maybes = MaybeNot +: (Persona.personas map { Just(_) })13 // Regla derecha Unit14 test("Regla derecha de Unit"){15

16

17 maybes foreach { m =>18 assert((m flatMap { Just(_) }) == m)19 }20 }21 // Asociatividad22 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

Page 224: Programación Funcional en Scala

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

1 test("Mismo abuelo materno con metodo abueloMaterno yabueloMaternoMatch"){

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

Page 225: Programación Funcional en Scala

1 package pinpong2

3 import akka.actor.{Actor, ActorLogging, Props}4

5 class PingActor extends Actor with ActorLogging {6 import PingActor._7

8 var counter = 09 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 += 118 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 Iniciar26 case class PingMessage(text: String)27 }

Algoritmo 8.4: Juego Ping-Pong. Actor Ping

1 package pinpong2

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

Page 226: Programación Funcional en Scala

1 package pinpong2

3 import akka.actor.ActorSystem4

5 object ApplicationMain extends App {6 val system = ActorSystem("MyActorSystem")7 val pingActor = system.actorOf(PingActor.props, "pingActor")8 pingActor ! PingActor.Iniciar9

10 system.awaitTermination()11 }

Algoritmo 8.6: Juego Ping-Pong. Main

Página 210

Page 227: Programación Funcional en Scala

Lista de Tablas

1.1. Tipos de datos primitivos y tamaño en Scala . . . . . . . . . . . . . . . . . . . 61.2. Caracteres de escape reconocidos por Char y String . . . . . . . . . . . . . . . 91.3. Prioridad y asociatividad de los operadores . . . . . . . . . . . . . . . . . . . 111.4. Operadores aritméticos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . 121.5. Operadores relacionales en Scala . . . . . . . . . . . . . . . . . . . . . . . . . 131.6. Operadores lógicos en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . 141.7. Tabla de verdad de los operadores bit a bit &, | y ˆ . . . . . . . . . . . . . . . . 141.8. Operadores bit a bit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141.9. Operadores de asignación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151.10. Reglas de reducción para expresiones booleanas . . . . . . . . . . . . . . . . . 201.11. Tipos de bucles en Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

3.1. Métodos del tipo Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1073.2. Métodos del tipo List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1123.3. Métodos del tipo Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1183.4. Métodos del tipo Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1213.5. Secuencias. Eficiencia de las operaciones. . . . . . . . . . . . . . . . . . . . . 1223.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 . . . . . . . . . . . . . . . . . 1656.2. Receive Vs React . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174

Página 211

Page 228: Programación Funcional en Scala

Página 212

Page 229: Programación Funcional en Scala

Lista de Algoritmos

1.1. Hola Mundo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41.2. Función con dos parámetros. El primero es evaluado por valor y el segundo por

nombre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191.3. Sintaxis sentencia if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211.4. Expresión condicional if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221.5. Sintaxis sentencia if / else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221.6. Expresiones condicionales. Función valor absoluto . . . . . . . . . . . . . . . 221.7. Sintaxis sentencia if / else if / else . . . . . . . . . . . . . . . . . . . . . . . . 231.8. Sentencias if anidadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231.9. Sintaxis bucles while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241.10. Ejemplo de bucle while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241.11. Sintaxis bucles do... while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251.12. Ejemplo de bucle do... while. . . . . . . . . . . . . . . . . . . . . . . . . . . . 251.13. Sintaxis bucles for con rangos. . . . . . . . . . . . . . . . . . . . . . . . . . . 261.14. Ejemplo de bucle for con rangos. . . . . . . . . . . . . . . . . . . . . . . . . . 261.15. Sintaxis bucles for con colecciones . . . . . . . . . . . . . . . . . . . . . . . . 261.16. Ejemplo bucle for con colecciones. . . . . . . . . . . . . . . . . . . . . . . . . 271.17. Sintaxis bucles for con filtros. . . . . . . . . . . . . . . . . . . . . . . . . . . 271.18. Ejemplo bucle for con filtros . . . . . . . . . . . . . . . . . . . . . . . . . . . 271.19. Sintaxis bucles for con yield. . . . . . . . . . . . . . . . . . . . . . . . . . . . 281.20. Ejemplo bucle for con yield . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281.21. Fecha actual formateada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292.1. Mi primera clase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332.2. Método suma que no compila . . . . . . . . . . . . . . . . . . . . . . . . . . . 342.3. MiPrimeraClase con método suma . . . . . . . . . . . . . . . . . . . . . . . . 342.4. Números racionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362.5. Cálculo del máximo común divisor de dos enteros . . . . . . . . . . . . . . . . 372.6. Clase Abstracta Animal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382.7. Clases Perro y Pajaro de tipo Animal . . . . . . . . . . . . . . . . . . . . . . . 392.8. Lista de Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462.9. Lista de Booleanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462.10. Lista Genérica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462.11. Lista Genérica con funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 472.12. Ejemplo de función parametrizada . . . . . . . . . . . . . . . . . . . . . . . . 472.13. Función sonTodosPositivos para ListaInt . . . . . . . . . . . . . . . . . . . . . 482.14. Función sonTodosPositivos para ListaInt con tipo acotado superiormente . . . . 482.15. Función sonTodosPositivos para ListaInt con tipo acotado inferiormente . . . . 482.16. Trait Function1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482.17. Tipos de funciones A y B . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

Página 213

Page 230: Programación Funcional en Scala

2.18. Trait Function1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502.19. Lista genérica de enteros, covariante y con Nil como objeto . . . . . . . . . . . 513.1. Cálculo de la raíz cuadrada de un número por el método de Newton . . . . . . 563.2. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 623.3. Recursión Indirecta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633.4. Función estricta valor absoluto . . . . . . . . . . . . . . . . . . . . . . . . . . 763.5. Ejemplo de función no estricta . . . . . . . . . . . . . . . . . . . . . . . . . . 763.6. Ejemplo de función no estricta con estrategia Call By Need . . . . . . . . . . . 773.7. Selectiva if como función no estricta . . . . . . . . . . . . . . . . . . . . . . . 773.8. Implementación final del tipo Naturales . . . . . . . . . . . . . . . . . . . . . 863.9. Implementacion TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . 913.10. Object Lista ampliado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 933.11. Implementación básica de Arboles Binarios . . . . . . . . . . . . . . . . . . . 983.12. TAD Arbol Binario de Búsqueda . . . . . . . . . . . . . . . . . . . . . . . . . 993.13. Métodos next y hashNext en un bucle while . . . . . . . . . . . . . . . . . . . 1053.14. Definicion de rangos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1073.15. Acceso a las cotas y valor de paso de los rangos . . . . . . . . . . . . . . . . . 1083.16. Acceso a las cotas y valor de paso de los rangos . . . . . . . . . . . . . . . . . 1083.17. Definición de tuplas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1093.18. Acceso a los elementos de una tupla. . . . . . . . . . . . . . . . . . . . . . . . 1093.19. Iterando sobre los elementos de las tuplas . . . . . . . . . . . . . . . . . . . . 1093.20. Método swap en tuplas de dos elementos . . . . . . . . . . . . . . . . . . . . . 1103.21. Definición de Vector. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1153.22. Agregar elementos a un Vector . . . . . . . . . . . . . . . . . . . . . . . . . . 1153.23. Definición de Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1163.24. Cálculo de la serie de Fibonacci utilizando Stream . . . . . . . . . . . . . . . . 1163.25. Definición y propiedades fundamentales de los conjuntos . . . . . . . . . . . . 1173.26. Recorriendo los elementos de un conjunto . . . . . . . . . . . . . . . . . . . . 1183.27. Definicion de Maps. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1193.28. Definicion de Maps con tuplas. . . . . . . . . . . . . . . . . . . . . . . . . . . 1193.29. Recuperar valores en Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1193.30. Agregar y eliminar elementos en un Map . . . . . . . . . . . . . . . . . . . . . 1193.31. Recorrer los elementos de un Map . . . . . . . . . . . . . . . . . . . . . . . . 1203.32. Ejemplo traducción bucle for con un generador . . . . . . . . . . . . . . . . . 1233.33. Ejemplo traducción de expresiones for con un generador y un filtro . . . . . . . 1233.34. Ejemplo de traducción de expresiones for con dos generadores . . . . . . . . . 1233.35. Ejemplo traducción bucle for genérico . . . . . . . . . . . . . . . . . . . . . . 1244.1. Definición de parámetros implícitos . . . . . . . . . . . . . . . . . . . . . . . 1294.2. Definición de valores implícitos . . . . . . . . . . . . . . . . . . . . . . . . . 1304.3. Definición de clases implícitas . . . . . . . . . . . . . . . . . . . . . . . . . . 1304.4. Ejemplo de tipos estructurales. . . . . . . . . . . . . . . . . . . . . . . . . . . 1344.5. Ejemplo de tipos de orden superior. . . . . . . . . . . . . . . . . . . . . . . . 1354.6. Función distribuir usando funtores. . . . . . . . . . . . . . . . . . . . . . . . . 1384.7. Definición básica del trait Mónada. . . . . . . . . . . . . . . . . . . . . . . . . 1394.8. Monada Identidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1434.9. Juego JailBreak. Definición tipos necesarios. . . . . . . . . . . . . . . . . . . . 1444.10. Juego JailBreak. Trait Game. . . . . . . . . . . . . . . . . . . . . . . . . . . . 1444.11. Juego JailBreak. Solución al juego. . . . . . . . . . . . . . . . . . . . . . . . . 144

Página 214

Page 231: Programación Funcional en Scala

4.12. Juego JailBreak. Definición de la clase JailBreak. . . . . . . . . . . . . . . . . 1454.13. Juego JailBreak. Trait Game incluyendo Try. . . . . . . . . . . . . . . . . . . . 1474.14. Juego JailBreak. Trait Game incluyendo Try. . . . . . . . . . . . . . . . . . . . 1474.15. Juego JailBreak. Solución al juego. . . . . . . . . . . . . . . . . . . . . . . . . 1474.16. Juego JailBreak. Solución al juego utilizando flatMap. . . . . . . . . . . . . . . 1484.17. Juego JailBreak. Solución al juego utilizando flatMap 2. . . . . . . . . . . . . . 1484.18. Juego JailBreak. Solución al juego utilizando expresiones for. . . . . . . . . . . 1494.19. Excepciones.Función media con excepciones . . . . . . . . . . . . . . . . . . 1494.20. Excepciones.Función media con argumento para caso especial . . . . . . . . . 1494.21. Tipo de datos Option . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1504.22. Excepciones. Función media con tipo de datos Option . . . . . . . . . . . . . . 1504.23. Tipo de datos Either . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1514.24. Excepciones.Función media con tipo de datos Either . . . . . . . . . . . . . . 1514.25. Excepciones.Division con Either . . . . . . . . . . . . . . . . . . . . . . . . . 1515.1. Fibonacci sin recursión de cola . . . . . . . . . . . . . . . . . . . . . . . . . . 1565.2. Fibonacci sin recursión de cola. Assert que falla . . . . . . . . . . . . . . . . . 1565.3. Excepcion AssertError . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1565.4. Fibonacci sin recursión de cola. Assert que falla + explicación . . . . . . . . . 1565.5. Excepcion AssertError con información . . . . . . . . . . . . . . . . . . . . . 1575.6. Fibonacci sin recursión de cola. Assert que no falla . . . . . . . . . . . . . . . 1575.7. Fibonacci sin recursión de cola. Ensuring que falla . . . . . . . . . . . . . . . 1575.8. Ensuring.Excepcion AssertError . . . . . . . . . . . . . . . . . . . . . . . . . 1585.9. Fibonacci sin recursión de cola. Ensuring que no falla . . . . . . . . . . . . . . 1585.10. Inversa de una lista. Recursión de cola . . . . . . . . . . . . . . . . . . . . . . 1585.11. Ensuring en el cálculo de la inversa de una lista . . . . . . . . . . . . . . . . . 1585.12. ScalaTest. Ejemplo que extiende de org.scalatest.Suite . . . . . . . . . . . . . 1615.13. ScalaTest. Ejemplo de la suite FunSuite . . . . . . . . . . . . . . . . . . . . . 1626.1. Mi Primer Actor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1706.2. Procesando primer mensaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1716.3. Mi primer mensaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1716.4. Actor con estado definido con variable mutable . . . . . . . . . . . . . . . . . 1726.5. Ejecutando Contador con estado mutable . . . . . . . . . . . . . . . . . . . . . 1726.6. Actor con estado inmutable . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1736.7. Ejecutando Contador con estado inmutable . . . . . . . . . . . . . . . . . . . 1736.8. Método act con loop y react . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1746.9. Diferencias entre Akka y la librería de Scala. Instanciación en Scala . . . . . . 1756.10. Diferencias entre Akka y la librería de Scala. Instanciación en Scala . . . . . . 1756.11. Diferencias entre Akka y la librería de Scala. Instanciación en Scala I . . . . . 1766.12. Diferencias entre Akka y la librería de Scala. Instanciación en Scala II . . . . . 1766.13. Diferencias entre Akka y la librería de Scala. Instanciación en Akka I . . . . . 1766.14. Diferencias entre Akka y la librería de Scala. Instanciación en Akka II . . . . . 1766.15. Eliminación de act. Ejemplo en Scala . . . . . . . . . . . . . . . . . . . . . . 1766.16. Eliminación de act. Ejemplo en Akka . . . . . . . . . . . . . . . . . . . . . . 1776.17. Akka.Trait Actor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1776.18. Akka.Actor con estado definido con variable mutable . . . . . . . . . . . . . . 1786.19. Akka.Actor con estado inmutable . . . . . . . . . . . . . . . . . . . . . . . . . 1786.20. Akka.Trait Actor y Trait ActorContext . . . . . . . . . . . . . . . . . . . . . . 1796.21. Akka.Actor con estado inmutable (become) . . . . . . . . . . . . . . . . . . . 179

Página 215

Page 232: Programación Funcional en Scala

6.22. Akka.Trait Actor y Clase Abstracta ActorRef . . . . . . . . . . . . . . . . . . 1806.23. Akka.Mensjaes bidireccionales . . . . . . . . . . . . . . . . . . . . . . . . . . 1806.24. Akka.Trait ActorContext II . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1816.25. Akka.Crear Actores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1818.1. Función numeroPalabrasDistintas utilizando zip y map . . . . . . . . . . . . . 2038.2. Función numeroPalabrasDistintas utilizando plegado de Listas . . . . . . . . . 2048.3. Función numeroPalabrasDistintas en una expresión . . . . . . . . . . . . . . . 2048.4. Juego Ping-Pong. Actor Ping . . . . . . . . . . . . . . . . . . . . . . . . . . . 2098.5. Juego Ping-Pong. Actor Pong . . . . . . . . . . . . . . . . . . . . . . . . . . . 2098.6. Juego Ping-Pong. Main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210

Página 216

Page 233: Programación Funcional en Scala

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, Agosto2012. 104

[15] Narciso Martí Oliet, Yolanda Ortega Mallén, and Alberto Verdejo. Estructuras de Datosy Métodos Algorítmicos. Garceta, segunda edition, 2013. 80, 89

Página 217

Page 234: Programación Funcional en Scala

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

Page 235: Programación Funcional en Scala

[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

Page 236: Programación Funcional en Scala

Página 220

Page 237: Programación Funcional en Scala

Glosario

.NET

.NET representa la evolución del Component Object Model (COM), la plataforma dedesarrollo 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 dedesarrollo y ejecución de aplicaciones. Esto quiere decir que no sólo nos brinda todas lasherramientas 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, segurosy 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 pararelojes 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. Habitualmentees tratado como un archivo binario que contiene un programa ejecutable similar a unmódulo objeto, que es un archivo binario producido por el compilador cuyo contenido esel 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 ladefinició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 porla 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 seanecesario, y que también evita repetir la evaluación en caso de ser necesaria en posterioresocasiones. También conocida como Call by Need o paso por necesidad.

Página 221

Page 238: Programación Funcional en Scala

Haskell

Lenguaje de programación funcional puro, fuertemente tipado. Su nombre se debe alló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 estallamada de cola se realiza a la misma subrutina que desde donde se realiza, la subrutinase 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 especificandoo “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 lospredicados), de corte más matemático. Los lenguajes funcionales se basan en el cálculolambda..

programación imperativa

Es un paradigma de programación que describe la programación en términos del estadodel programa y sentencias que cambian dicho estado. Los programas imperativos son unconjunto 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íade 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ógicadifusa (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ódulode código. Esto sirve para asegurar que cada uno de los módulos funcione correctamentepor 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, muyexpresivo y soporta varios paradigmas. Estuvo influenciado por el cálculo lambda.

Página 222

Page 239: Programación Funcional en Scala

scriptSecuencia de instrucciones almacenadas en un fichero que se ejecutan normalmente deforma interpretada..

sistema de tiposUn sistema de tipos define cómo un lenguaje de programación clasifica los valores y lasexpresiones en tipos, cómo se pueden manipular estos tipos y cómo interactúan.

unicodeUnicode es un estándar de codificación de caracteres diseñado para facilitar el tratamientoinformático, transmisión y visualización de textos de múltiples lenguajes y disciplinastécnicas, además de textos clásicos de lenguas muertas. El término Unicode proviene delos tres objetivos perseguidos: universalidad, uniformidad y unicidad.

vectorColección de un número determinado de elementos de un mismo tipo de datos ubicadosen una zona de almacenamiento continuo, adecuado para situaciones en las que el accesoa los datos se realice de forma aleatoria e impredecible.

Página 223

Page 240: Programación Funcional en Scala

Página 224

Page 241: Programación Funcional en Scala

Acrónimos

ABBÁrbol binario de búsqueda.

ACMAsociación para la Maquinaria Computacional.

APIApplication Programming Interface.

asociaciónmaps.

cierreclosure.

clase acompañantecompanion class.

COMComponent Object Model.

CPUunidad central de procesamiento.

EPFLÉcole Polytechnique Fédérale de Lausanne.

espacio de nombresnamespace.

FIFOFirst In First Out – “primero en entrar, primero en salir” –.

flujo de datosstream.

hilo de ejecuciónthread.

Página 225

Page 242: Programación Funcional en Scala

IDEEntorno de Desarrollo Integrado.

IMCÍndice de masa corporal.

JVMMáquina Virtual de Java.

modificaciones apilablesstackable modifications.

método de fábricafactory method.

objeto acompañantecompanion object.

objeto singletonsingleton objects.

POOprogramación orientada a objetos.

REPLBucle Leer-Evaluar-Imprimir.

TADsTipo abstracto de datos.

TDDTest Driven Development.

Página 226