Skip to content

Juan Mellado

Scala (y 5)

Finalizando el tour de Scala.

Self-Type

Un auto-tipo en Scala indica que un trait extiende de otro. Es similar al uso de la palabra reservada extends, pero utiliza una nomenclatura distinta y no obliga a importar el trait del que se extiende.

Resulta un tanto confuso en un principio, pero por lo que podido leer al respecto, la motivación principal es permitir modelar relaciones de tipo “requiere un” frente a las más clásicas “extiende de”. Algo que resulta de utilidad para implementar algunos patrones, en particular el de inyección de dependencias.

Para definir un auto-tipo hay que definir un identificador, normalmente this, seguido del nombre del trait del que se quiere obligar a extender, seguido del símbolo =>.

La declaración del auto-tipo obliga a extender con with de Vehicle, y por consiguiente, a definir el miembro brand. El compilador generará un error si no se implementa dicho requerimiento.

Implicit Parameters

Un método puede declarar una o más listas de parámetros con la palabra reservada implicit. Esto hace que dichos parámetros sean opcionales y que Scala trate de proporcionar un valor para los mismos si no se pasan de manera explícita.

En el código de ejemplo anterior, Scala resuelve en tiempo de ejecución el valor de y para completar la llamada a scale. En este sencillo ejemplo casan tanto el nombre como el tipo, pero podría cambiarse el nombre y aún así Scala seguiría casando el valor. Si ningún valor tuviera el mismo tipo entonces Scala trataría de resolver el valor atendiendo a unas determinadas reglas de conversión de tipo y resolución de contexto.

La resolución de los valores implícitos es un tanto compleja. La documentación oficial contiene un apartado dedicado de forma específica a explicar los detalles del proceso. Es conveniente revisarlo si se quiere comprender como implementa Scala esta característica, ya que es materia para un artículo completo.

Implict Conversions

Las conversiones implícitas en Scala son similares a los castings automáticos de algunos lenguajes de programación, pero con algunos matices propios de Scala.

Si se espera un valor de tipo T y se proporciona un valor de tipo S entonces se trata de hacer la conversión. En este sentido, Scala tiene un paquete llamado scala.Predef con un conjunto de conversores predefinidos.

En el código de ejemplo anterior se convierte un valor de tipo scala.Int en otro de tipo java.lang.Integer.

El compilador avisa con warnings cuando detecta alguna de estas conversiones implícitas, que se pueden silenciar importando el paquete scala.language.implicitConversions.

Polymorphic Methods

Scala denomina métodos polimórficos a los métodos con tipos parametrizados. Estos métodos se definen y comportan de manera similar a las clases genéricas.

En el código de ejemplo anterior se observa que el tipo parametrizado se puede indicar de forma explícita, o dejar que sea el propio Scala el que lo determine.

Type Inference

En línea con lo comentado en el apartado anterior, y lo visto a lo largo de estos artículos, Scala es capaz de inferir los tipos de determinadas expresiones sin necesidad de que se indiquen de forma explícita.

Se puede omitir el tipo en la declaración de un valor o variable inicializada en su definición.

En algunos casos se puede omitir el tipo resultante de un método o función cuando se puede inferir automáticamente del resultado.

Se puede omitir el tipo parametrizado en la instanciación de clases genéricas o métodos polimórficos cuando se puede inferir del contexto.

Scala no puede inferir los tipos de los parámetros, excepto en funciones anónimas pasadas como parámetros.

Por último, comentar que Scala no puede inferir el tipo del resultado en el caso de funciones recursivas.

Operators

Scala permite definir operadores, de igual forma que otros lenguajes de programación, tratándolos como métodos de la clase dentro de la que se definen.

De forma alternativa, los operadores pueden escribirse utilizando la notación punto como cualquier otro método.

By-name Parameters

Los parámetros por nombre son una forma que tiene Scala de permitir indicar que el valor de un parámetro debe evaluarse cada vez que se acceda a él dentro del cuerpo de un método, en vez de cuando se realice la llamada al método.

Notar que el nombre de esta característica resulta un poco confusa, y que además, aunque lo parezca, no es totalmente equivalente a una evaluación perezosa. De hecho, Scala tiene la palabra reservada lazy para evaluar perezosamente un valor.

En el código de ejemplo anterior el valor impreso es siempre el mismo, ya que se evalúa una sola vez, justo cuando se invoca al método execute. Y aunque este es el comportamiento por defecto esperado, Scala permite modificarlo utilizando los parámetros por nombre. Es una característica curiosa del lenguaje.

Los parámetros por nombre se denotan anteponiendo el símbolo => delante del tipo del parámetro.

En el nuevo código modificado el valor impreso es diferente cada vez, ya que la expresión pasada como parámetro ahora es evaluada cada vez que se accede a ella.

En buena lógica, el caso de uso habitual de esta característica es del demorar la ejecución de procesos computacionalmente costosos hasta que sea realmente necesario acceder al resultado de los mismos.

Annotations

Scala permite utilizar las anotaciones definidas en Java de la misma forma que se hacen en el propio código Java.

La documentación de Scala menciona un conjunto de anotaciones de propósito bien conocido como @deprecated, @deprecatedName, @transient, @volatile, @SerialVersionUID y @throws. Algunas más específicas como @scala.beans.BeanProperty y @scala.beans.BooleanBeanProperty. Y algunas que afectan a la forma en la que compilador genera el código como @unchecked, @uncheckedStable y @specialized.

Respecto a la construcción de anotaciones propias, la especificación las menciona en un apartado, pero no proporciona ningún ejemplo.

Default Parameter Values

El tour de Scala reintroduce en este punto un apartado acerca de los valores por defecto de los parámetros. Algo ya visto anteriormente.

Named Arguments

En la misma línea que en el apartado anterior, el tour de Scala comenta en este punto que los parámetros pueden pasarse en cualquier orden indicando su nombre.

Packages and Imports

Las clases en Scala se organizan en paquetes de igual forma que en Java, aunque no se fuerza a que la ruta del fichero coincida con el nombre del paquete.

Los paquetes se pueden definir de forma anidada utilizando bloques.

La sentencia import es similar a la de Java, pero puede utilizarse en cualquier punto, no sólo al principio de los ficheros, usa el caracter _ en vez de * para importar todos los miembros de un paquete, permite importar varias clases en una sola línea, e incluso permite renombrar un miembro al ser importado.

Scala (4)

Más cosas de Scala.

For Comprehensions

Scala implementa el concepto de generador, presente en otros lenguajes de programación, mediante la combinación de las palabras reservadas for y yield.

El uso más sencillo que se puede hacer de esta característica es el de procesar secuencias de valores y transformarlos en otros valores, de forma similar a como funciona el método map sobre una colección.

Dentro del for puede haber varias expresiones separadas por punto y coma, llamadas enumeradores, y pueden ser tanto generadores como filtros con guardas.

Otra forma de utilizar esta expresión es con la palabra reservada until, que hace que se comporte como un bucle for de una forma más tradicional.

Esta construcción es más importante de lo que a priori pueda parecer, y Scala permite aplicarla sobre cualquier tipo que implemente los métodos withFilter, map y flatMap.

Generic Classes

Una clase genérica es aquella que admite un tipo o más como parámetro. El concepto en Scala es el mismo que se encuentra presente en Java desde su versión 5, cuando se introdujeron los generics.

El caso de uso más habitual es el de la colecciones de elementos, pero es aplicable a cualquier tipo abstracto de datos, e incluso a funciones o métodos.

En el código de ejemplo, copiado directamente de la documentación de Scala, se observa que el tipo parametrizado A se indica utilizando corchetes. Puede indicarse más de uno utilizando la coma como separador.

Variances

El concepto de varianza se utiliza en Scala para describir las relaciones entre los tipos de una jerarquía de tipos, e indicar cuales de ellos son aceptables cuando se utilizan genéricos.

Scala soporta invarianza, covarianza y contravarianza. El símbolo + delante del tipo indica covarianza, el simbolo - contravarianza, y la ausencia de símbolo indica invarianza.

La invarianza quiere decir que los únicos elementos aceptados son aquellos que son exactamente del tipo A. Este es el comportamiento por defecto en Scala.

La covarianza quiere decir que los únicos elementos aceptados son aquellos que son de tipo A o cualquier subtipo de A. Por ejemplo, las listas en Scala son covariantes.

La contravarianza quiere decir que los únicos elementos aceptados son aquellos que son de tipo A o cualquier supertipo de A. Esta forma de varianza es la que resulta habitualmente menos intuitiva. Lo que pretende es forzar que se cumpla la regla de que un tipo T es un subtipo de A si se puede utilizar un valor de tipo T cuando se requiere un valor de tipo A, que es lo que se conoce como “Principio de Sustitución de Liskov”.

En este último código de ejemplo, la clase Writer fuerza a que una instancia del tipo parametrizado T sólo pueda reemplazarse por otra del mismo tipo T o algún supertipo de la misma. De esta forma, una instancia de Writer<String> no puede utilizarse como sustituta de otra instancia de tipo Writer<AnyRef>, ya que una instancia que sólo soporta cadenas de texto no es intercambiable con otra que soporta referencias a cualquier tipo de objeto.

Upper Type Bounds

La expresión T <: A en Scala quiere decir que T es un subtipo de A.

Sirve para limitar los tipos de valores admisibles en un genérico. Es equivalente a utilizar <? extends A>  en Java.

Lower Type Bounds

La expresión T >: A en Scala quiere decir que T es un supertipo de A.

Sirve para limitar los tipos de valores admisibles en un genérico. Es equivalente a utilizar <? super A>  en Java.

En el código del ejemplo, adaptado de la documentación oficial de Scala, se observa como se utiliza U >: B en los métodos para forzar que los elementos contenidos en los nodos sean de un determinado tipo B, o alguno de sus supertipos, y el valor retornado sea de dicho tipo B.

Inner Classes

Scala permite definir clases embebidas dentro de otras clases. Java también lo permite, pero mientras que en Java la clase interna pertenece a la clase contenedora, en Scala la clase interna pertenece a las instancias de la clase contenedora.

En el código de ejemplo se produce un error de compilación porque la clase Item es diferente para  stream1 y stream2. Cada variable tiene su propia definición de Item.

Si el comportamiento por defecto de Scala no es el deseado, se pueden cambiar los métodos para que trabajen con una referencia a la clase interna de la clase contenedora, en vez de a la clase interna de cada instancia. Para ello hay que indicar la ruta completa a la clase interna mediante Stream#Item.

Abstract Types

Scala permite definir clases con tipos abstractos. Es una característica similar a la definición de clases genéricas, pero con alguna diferencia de orden semántico. La idea es que se puedan definir tipos abstractos en una clase de igual forma en la que, por ejemplo, se declaran métodos abstractos.

Es posible incluso definir una clase abstracta en base a otra y redefinir el tipo abstracto.

Compound Types

Como ya se vio anteriormente, Scala soporta mixins, que de forma simplificada se puede decir que es la unión de varias interfaces. Lo interesante de esta característica es que su uso no está restringido a la definición de las clases, sino que se puede aplicar en otras construcciones, como por ejemplo en los tipos de los parámetros de las funciones.

Utilizando la palabra reservada with se pueden concatenar dos o más interfaces y crear un tipo compuesto. Es similar a la unión de tipos que soportan algunos lenguajes de programación.

Scala (3)

Más construcciones propias de Scala.

Tuples

Una tupla representa un conjunto de valores, ya sean del mismo o distinto tipo.

Es particularmente útil cuando se debe retornar más de un valor desde una función.

Para acceder a un valor de una tupla se utiliza el ordinal de su posición precedido por un guión bajo.

Aunque también se puede desestructurar la tupla para acceder por nombre.

Si se necesita indicar el tipo de forma explícita entonces se deben utilizar los tipos Tuple2, Tuple3, … y así sucesivamente hasta Tuple22.

Mixins

Los mixins son estructuras que permiten extender una clase con distintos comportamientos sin necesidad de utilizar herencia múltiple.

Una clase sólo puede extender de una clase padre, pero de tantos mixins como se quiera. La única condición es que la clase padre y los mixins extiendan a su vez de la misma clase.

El uso de mixins favorece la composición frente a la herencia y mejora la intencionalidad del código.

Higher-order Functions

Una función de orden superior es aquella que admite una función como parámetro o retorna una función como resultado.

Un ejemplo de método que admite una función como parámetro es map. Este método aplica una función dada a todos los elementos de una secuencia.

La función pasada como parámetro puede ser una lambda anónima embebida.

E incluso es posible utilizar una notación abreviada de Scala que utiliza el símbolo _ para identificar los parámetros.

De forma general, los métodos que admiten funciones o retornan funciones se definen utilizando la “signatura” de dichas funciones. Es decir, el tipo de los parámetros que admiten y el tipo de su resultado.

Currying

Scala permite que un método tenga múltiple lista de parámetros.

Un ejemplo de este tipo de métodos es foldLeft, que aplica un operador binario op a una secuencia de valores, de izquierda a derecha, empezando por un valor inicial z.

Este método permite por ejemplo sumar todos los números de una secuencia dada.

Y gracias a que el método tiene varias listas de parámetros, se puede utilizar la notación abreviada de Scala utilizando el símbolo _ para representar los parámetros.

Esto puede resultar un poco confuso al principio, pero tiene la ventaja de que permite definir un método a partir de otro prefijando una o más de sus listas de parámetros.

Esta forma de escribir los métodos se asimila con la técnica conocida como currying. Esta técnica transforma métodos que admiten múltiples argumentos en una secuencia de métodos que admiten un único argumento cada una de ellos. Por ejemplo, se puede tomar un método que toma dos valores de tipo X e Y y retorna un valor de tipo Z, y transformarlo en un método que toma un valor de tipo X y retorna una función que convierte un valor de tipo Y en un valor de tipo Z.

Pattern Matching

La técnica de pattern matching permite comprobar si un valor cumple un determinado patrón. Scala ofrece una implementación de esta técnica a través de la palabra reservada match que utiliza una sintaxis similar a la sentencia switch de Java.

El guión bajo se utiliza para casar cualquier valor, es equivalente a default en Java.

Además de valores constantes, Scala permite utilizar tipos como patrones.

Una forma particularmente útil de match permite desestructurar los valores de clases de tipo case.

Los patrones se pueden ampliar utilizando una expresión de guarda para añadir comprobaciones adicionales.

Regular Expressions

Las expresiones regulares en Scala siguen el patrón habitual de otros lenguajes de programación. Están implementadas por la clase Regex y se construyen a partir de cadenas de texto que se convierten en expresiones regulares mediante el método r.

Singleton Objects

Un object en Scala es a la vez la definición de una clase y un objeto singleton de dicha clase.

Este tipo de objetos ya han aparecido anteriormente, pero es necesario reintroducirlos para hablar de los companion objects. Un companion object es un objeto que tiene el mismo nombre que una clase. Extiende la clase homónima añadiendo miembros que son visibles por todas las instancias de dicha clase. Es similar a decir que permite añadir métodos o propiedades estáticas a una clase desde otra clase.

Uno de los usos preferidos de esta técnica es la de crear factorías de objetos.

La única limitación de esta técnica es que la clase y el companion object tienen que estar definidos en el mismo fichero.

Extractor Objects

Sobre un object en Scala se puede definir un método de nombre apply que actúa como factoría, admite parámetros y crea un objeto. De igual forma, se puede definir un método de nombre unapply que realiza el proceso inverso, admite un objeto y retorna los parámetros que se utilizaron para construirlo.