Skip to content

scala

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

trait Vehicle {
  def brand: String
}

trait Car {
  this: Vehicle =>

  def drive(): Unit = println("driving")
}

class Batmovil() extends Car with Vehicle {
  def brand = "batmovil"
}

val car = new Batmovil()
car.drive()

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.

def scale(x: Int)(implicit y: Int) = x * y

implicit val y: Int = 5

scale(5) // 25

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.

import scala.language.implicitConversions

implicit def int2Integer(x: Int)= java.lang.Integer.valueOf(x)

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.

def repeat[A](x: A, length: Int): List[A] =
  if (length < 1) Nil else x :: repeat(x, length - 1)

println(repeat[Int](3, 4)) // 3, 3, 3, 3
println(repeat("Hi", 5)) // Hi, Hi, Hi, Hi, Hi

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.

val entero = 5

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

def square (x: Int) = x * x

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.

val lista = List(1, 2, 3)

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

List(1, 2, 3).map(x => x * x)

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

def factorial(n: Int) = if (n == 0) 1 else n * factorial(n - 1) // Error

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.

case class Vector(val x: Int, val y: Int) {
  def +(other: Vector) = Vector(this.x + other.x, this.y + other.y)
}

val vec1 = Vector(1, 2)
val vec2 = Vector(3, 4)
val vec3 = vec1 + vec2

println(vec3) // 4, 6

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

vec1.+(vec2)

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.

def execute(time: Long) {
  println(time)
  Thread.sleep(1000L)
  println(time)
}

execute(System.nanoTime())

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.

def execute(time: => Long) {
  println(time)
  Thread.sleep(1000L)
  println(time)
}

execute(System.nanoTime())

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.

@deprecated
def old(): Unit = ()

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.

case class Point(val x: Int = 0, val y: Int = 0)

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.

case class Point(val x: Int, val y: Int)

val p1 = Point(x = 1, y = 2)
val p2 = Point(y = 4, x = 3)

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.

package com.example.services

Los paquetes se pueden definir de forma anidada utilizando bloques.

package com.example {
  package services {
  }
  package controllers {
  }
}

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.

import com.example.services._
import com.example.services.UserService
import com.example.services.{UserService, RoleService}
import com.example.services.{UserService => Users}

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.

def squares(values: List[Int]): List[Int] =
  for (value <- values) yield value * value

val values = List(1, 2, 3)

println(squares(values)) // 1, 4, 9

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

def odds(values: List[Int]): List[Int] =
  for (
    value <- values if (value & 1) == 1
  ) yield value

val values = List(1, 2, 3, 4, 5, 6)

println(odds(values)) // 1, 3, 5

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.

case class Point(val x: Int, val y: Int)

def grid(n: Int): Seq[Point] =
  for (
    x <- 0 until n;
    y <- 0 until n
  ) yield Point(x, y)

println(grid(2)) // Point(0,0), Point(0,1), Point(1,0), Point(1,1)

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.

class Stack[A] {
  private var elements: List[A] = Nil
  def push(x: A) { elements = x :: elements }
  def peek: A = elements.head
  def pop(): A = {
    val currentTop = peek
    elements = elements.tail
    currentTop
  }
}

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.

val stack = new Stack[Int]

stack.push(1)
stack.push(2)

println(stack.pop) // 2
println(stack.pop) // 1

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.

class Invariant[A]

class Covariant[+A]

class Contravariant[-A]

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.

trait Operator[A] {
  def identity(value: A): A = value
}

object IntOperator extends Operator[Int]

println(IntOperator.identity(123))
println(IntOperator.identity("abc")) // Error de compilación, String no es Int

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.

abstract class Vehicle

case class Car() extends Vehicle

case class Motorcycle() extends Vehicle

val vehicles: List<Vehicle> = List(Car(), Motorcycle())

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

trait Writer[-T] {
  def write(value: T)
}

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.

trait Node[+B] {
  def prepend[U >: B](elem: U): Node[U]
}

case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {
  def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem, this)
  def head: B = h
  def tail: Node[B] = t
}

case class NilNode[+B]() extends Node[B] {
  def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem, this)
}

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.

class Stream {
  class Item()

  def produce(): Item = new Item
  def consume(item: Item): Unit = ()
}

val stream1 = new Stream
val stream2 = new Stream

val item = stream1.produce()
stream2.consume(item) // Error de compilación stream1.Item no es stream2.Item

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.

class Stream {
  class Item()

  def produce(): Stream#Item = new Item
  def consume(item: Stream#Item): Unit = ()
}

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.
abstract class Sequence {
  type U
}

class IntSequence extends Sequence {
  type U = Int
  
  private var current: U = 0
  
  def next(): U = {
    current += 1
    current
  }
}

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.
def maximize(widget: Moveable with Resizable): Unit = {
  widget.move(viewport.origin);
  widget.resize(viewport.size);
}

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.

val currency = ("Euro" , "€", 166.386)

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.

println(currency._1)
println(currency._2)
println(currency._3)

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

val (name, symbol, exchange) = currency

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

val currency = ("Euro", "€", 166.386): Tuple3[String, String, Double]

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.

trait Artist

trait Writer extends Artist {
  def write(): Unit = println("Call me Ishmael...")
}

trait Musician extends Artist {
  def sing(): Unit = println("Imagine there's no heaven...")
}

class Genius extends Artist with Musician with Writer

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

class Widget extends Window with Scrollable with Resizable

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.

val numbers = Seq(1, 2, 3)

val double = (x: Int) => x * 2

val result = numbers.map(double) // 2, 4, 6

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

val result = numbers.map(x => x * 2) // 2, 4, 6

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

val result = numbers.map(_ * 2) // 2, 4, 6

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.

class Converter[A, B](_converter: A => B) {
  def convert(value: A): B = _converter(value)

  def converter(): A => B = _converter
}

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.

def foldLeft[B](z: B)(op: (B, A) => B): B

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

val numbers = List(1, 2, 3)
val result = numbers.foldLeft(0)((lh, rh) => lh + rh)
print(result) // 6

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.

val result = numbers.foldLeft(0)(_ + _)

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.

val fold = numbers.foldLeft(0)_

val plus = fold(_ + _)
val minus = fold(_ - _)

println(plus) // 6
println(minus) // -6

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.

def test(value: Int): String = value match {
  case 1 => "one"
  case 2 => "two"
  case _ => "other"
}

println(test(1)) // one
println(test(2)) // two
println(test(3)) // other

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.

abstract class Connector

case class Mail() extends Connector {
  def sendMessage(): Unit = {}
}

case class Chat() extends Connector {
  def sendNotification(): Unit = {}
}

def send(connector: Connector) : Unit = connector match {
  case m: Mail => m.sendMessage()
  case c: Chat => c.sendNotification()
}

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

abstract class Shape

case class Square(val length: Int) extends Shape

case class Circle(val radius: Double) extends Shape

def area(shape: Shape): Double = shape match {
  case Square(length) => length * length
  case Circle(radius) => Math.PI * (radius * radius)
}

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

abstract class Meal

case class Pizza(ingredients: List[String]) extends Meal

case class Burger(ingredients: List[String]) extends Meal

def veganTest(value: Meal): String = value match {
  case Pizza(ingredients) if (ingredients.contains("meat"))
    => "This pizza contains meat"
  case Burger(ingredients) if (ingredients.contains("meat"))
    => "This burger contains meat"
...

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.

import scala.util.matching.Regex

val digits: Regex = "[0-9]+".r

def isInteger(value: String): Boolean =
  digits.findFirstMatchIn(value) match {
    case Some(_) => true
    case None => false
  }

println(isInteger("123")) // true
println(isInteger("abc")) // false

Singleton Objects

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

object Sequence {
  private var current = 0

  def next(): Int = {
    current += 1
    current
  }
}

Sequence.next() // 1
Sequence.next() // 2

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.

case class Position(val x: Int, val y: Int, val z: Int)

object Position {
  def center: Position = Position(0, 0, 0)

  def from(other: Position): Position = other.copy()
}

val p1 = Position.center
val p2 = Position.from(p1)

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.

object Customer {

  def apply(name: String, surname: String): String = name + " " + surname;

  def unapply(fullName: String): Option[(String, String)] = {
    val parts = fullName.split(' ')
    if (parts.length == 2) Some (parts(0), parts(1))
    else None
  }
}

val customer = Customer("John", "Smith")
println(customer) // John Smith 

val Customer(name, surname) = customer
println(s"$surname, $name") // Smith, John

Scala (2)

Después de la instalación del entorno de trabajo es hora de dar los primeros pasitos con Scala. Lo más básico es conocer las estructuras más sencillas y aprender como definir valores, variables, funciones, métodos, clases, … sin entrar en profundidad en todas sus capacidades.

Values

Un valor, según la definición formal de Scala, es un nombre que se le asigna al resultado de una expresión.

val x = 1 + 1

Una vez asignado un valor no se puede cambiar, de forma similar a un variable de tipo final en Java.

x = 2 // error

Scala infiere automáticamente el tipo de los valores, aunque también permite indicarlos de forma explícita.

val x: Int = 1 + 1

Variables

Las variables son como los valores, pero se pueden reasignar. Es decir, son como las clásicas variables presentes en la mayoría de lenguajes de programación.

var x = 1

x = 2 // correcto

De igual forma que con los valores, Scala infiere el tipo en función de la asignación, pero se puede indicar de forma explícita.

var x: Int = 1

Blocks

Un bloque es una secuencia de expresiones, de forma que el resultado del bloque es el resultado de la última expresión.

println({
  val x = 1
  x + 1
}) // 2

En Scala existe la palabra reservada return, pero no suele utilizarse.

Functions

Las funciones son expresiones que admiten cero o más parámetros.

Pueden ser anónimas, como las expresiones lambda presentes en algunos lenguajes, o estar asociadas a un nombre.

(x: Int) => x + 1

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

println(incr(1)) // 2

Si el cuerpo de una función necesita más de una línea entonces se utiliza un bloque.

Methods

Los métodos son similares a las funciones, pero declaran un tipo de retorno.

def incr(x: Int): Int = x + 1

println(incr(1)) // 2

La lista de parámetros es opcional, e incluso se pueden indicar varias listas de parámetros.

def answer: Int = 42;

def add(x: Int)(y: Int): Int = x + y;

Classes (1)

La forma de definir una clase en Scala es similar a la forma en que se hace en otros lenguajes de programación. Una característica interesante es que junto con el propio nombre de la clase se definen los parámetros del constructor.

class Greeter(prefix: String, suffix: String) {

  def greet(name: String): Unit =
    println(prefix + name + suffix)
}

En el ejemplo, copiado directamente de la documentación de Scala, se observa que se define un método que retorna el tipo Unit. Este tipo se utiliza de forma similar a como se utiliza void en Java, para indicar que no se quiere retornar ningún resultado. Pero en la práctica es una instancia singleton de tipo Unit, ya que Scala requiere que todas las expresiones evalúen a un valor.

Las clases se instancian utilizando la palabra reservada new.

val greeter = new Greeter("¡Hola, ", "!")

greeter.greet("mundo") // ¡Hola, mundo!

Case Classes

Las clases de tipo case permiten definir objetos inmutables.

case class Point(x: Int, y: Int)

Se instancian sin utilizar la palabra reservada new.

val point = Point(1, 2)

Y se caracterizan porque se comparan por valor.

if (Point(1, 2) == Point(1, 2)) { // true

Lo que quiere decir que dos objetos inmutables creados con los mismos valores son equivalentes, representan dos áreas de memoria inicializadas con los mismos valores, y por tanto intercambiables.

Las clases de este tipo implementan el método copy que permite crear copias de objetos ya existentes variando uno o más de sus valores miembro.

val point1 = Point(1, 2)
val point2 = point1.copy(y = 3)

Este método es importante desde el punto de vista de la programación funcional, que favorece el uso de objetos inmutables que carecen de estado interno, y recomienda que se creen nuevos objetos clonando los originales cuando se tenga que modificar alguno de sus valores.

Objects

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

object Sequence {
  private var current = 0

  def next(): Int = {
    current += 1
    current
  }
}

Se acceden a ellos por su nombre, de forma similar a como se accede a los métodos estáticos de una clase en muchos lenguajes de programación:

println(Sequence.next());
println(Sequence.next());

Traits

Un trait en Scala es una definición de tipo, lo que incluye tanto atributos como métodos.

trait Predicate {
  def test: Boolean
}

Una forma simple de entender esta construcción es verla como una interface de Java 8 o superior. Un trait puede heredar de otros, puede tener implementaciones de métodos por defecto y puede hacer un override de los métodos de los padres.

Types

Scala define el tipo Any como padre de todos los tipos, y que por su semejanza con Java, define los métodos equals, hasCode y toString.

Los tipos AnyVal y AnyRef son hijos directos de Any. El primero representa tipos de valores no nulables, y el segundo tipos de referencias.

Los tipos de valores no nulables disponibles son Double, Float, Long, Int, Short, Byte, Char, Boolean y Unit.

El tipo de referencia AnyRef es equivalente a java.lang.Object en Java, y es el padre de todos los tipos propios definidos por los usuarios.

El tipo Null es un subtipo de todos los tipos de referencias, tiene un único valor representado por la palabra reservada null, y normalmente no se utiliza dentro de los programas escritos en Scala, existe más por compatibilidad con Java.

El tipo Nothing es un subtipo de todos los tipos y se utiliza para indicar una terminación o proceso anormal, como por ejemplo el lanzamiento de una excepción o un bucle infinito.

Classes (2)

De vuelta a las clases, comentar que una clase puede contener valores, variables, métodos, tipos, objects, traits, u otras clases. La clase más sencilla que se puede definir es aquella que no contiene ningún miembro.

class Point

val point = new Point

Todas las clases tienen un constructor sin parámetros por defecto, aunque como ya se mencionó anteriormente, se pueden definir parámetros en la propia declaración de la clase.

class Point(var x: Int, var y: Int) {

  def move(dx: Int, dy: Int): Unit = {
    x = x + dx
    y = y + dy
  }

  override def toString: String = s"($x, $y)"
}

Los parámetros pueden tener valores por defecto y hacer referencia a ellos por su nombre.

class Point(var x: Int = 0, var y: Int = 0) {
...

val p1 = new Point
val p2 = new Point(1, 2)
val p3 = new Point(y = 3)

Para hacer miembros privados se puede utilizar la palabra reservada private, siendo la norma utilizar un guión bajo como prefijo del miembro privado. Para implementar un getter la norma es utilizar un método con el mismo nombre del miembro privado pero sin guión bajo y sin lista de parámetros. Y para implementar un setter la norma es utilizar el nombre del miembro privado pero con el sufijo _=.

class Point {
  private var _x = 0
  private var _y = 0

  def x = _x
  def x_= (value: Int): Unit = _x = value

  def y = _y
  def y_= (value: Int): Unit = _y = value
...

Para hacer privados los parámetros del constructor se debe omitir la palabra reservada var o val en su declaración.

class Point(x: Int, y: Int) {
...

Traits (2)

El trait más sencillo que puede definirse es el que no contiene ningún miembro, pero resultan de mayor utilidad cuando tienen tipos genéricos y métodos abstractos.

trait Iterator[A] {
  def hasNext: Boolean
  def next(): A
}

En el ejemplo, se define la clásica interface para un iterador de tipo genérico A.

Una clase implementa un trait utilizando la palabra reservada extends.

class IntIterator(to: Int) extends Iterator[Int] {

  private var current = 0

  override def hasNext: Boolean = current < to

  override def next(): Int = {
    if (hasNext) {
      val t = current
      current += 1
      t
    } else 0
  }
}

Llegado este punto queda claro que Scala hereda muchas características de Java, pero también que añade muchas propias, en particular para reducir la cantidad de código a escribir y proporcionar algunas estructuras básicas dentro del ámbito de la programación funcional.

Queda mucho por descubrir todavía.

Scala (1)

Scala es un lenguaje de programación que se ejecuta sobre la máquina virtual de Java y aspira a sacar partido de lo mejor de los dos paradigmas actuales dominantes, es decir, de la programación orientada a objetos y de la programación funcional.

Por necesidades del guión estoy ahora revisando sus características, y como de costumbre he decidido publicar mis notas personales para futuras referencias. La idea es hacer una instalación desde cero, evitando utilizar wizards al principio, y realizando una configuración de forma manual. El plan es ejecutar primero un ejemplo sencillo desde línea de comandos, a continuación instalar y configurar un IDE, y por último realizar un «tour» por el lenguaje siguiendo el guión propuesto en la propia página oficial de Scala.

JVM

Scala necesita una máquina virtual de Java para compilar y ejecutarse. La versión actual del JDK es la 11.0.1, que puede descargarse en formato zip y descomprimirse en cualquier directorio <JDK>.

Basta con añadir el directorio <JDK>/bin en el PATH del sistema para poder ejecutar Java desde línea de comandos:

set PATH=<JDK>/bin

java -version

java version "11.0.1" 2018-10-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.1+13-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.1+13-LTS, mixed mode, sharing)

No obstante, antes de continuar, es recomendable crear el fichero de clases compartidas precompiladas de Java. Este fichero aumenta ligeramente el rendimiento, particularmente el arranque de la máquina virtual. Para crearlo hay que ejecutar la siguiente instrucción desde línea de comandos:

java -Xshare:dump

Si la ejecución termina de forma correcta se creará un fichero classes.jsa en el directorio <JDK>/bin/server.

Scala

La versión actual de Scala es la 2.12.7, que puede descargarse en formato zip y descomprimirse en cualquier directorio <SCALA>.

Basta con añadir el directorio <SCALA>/bin en el PATH del sistema para poder ejecutar Scala como un REPL desde línea de comandos:

set PATH=<JDK>/bin;<SCALA>/bin

scala -version

Scala code runner version 2.12.7 -- Copyright 2002-2018, LAMP/EPFL and Lightbend, Inc.

Para compilar un programa se debe utilizar scalac, que prácticamente es igual que javac, el compilador de Java.

¡Hola, mundo!

Como primer ejemplo de compilación se puede utilizar el clásico «¡Hola, mundo!», que sin entrar en detalles, se puede crear en un fichero HolaMundo.scala con el siguiente código:

object HolaMundo extends App {
  println("¡Hola, mundo!")
}

La compilación se realiza ejecutando la siguiente instrucción desde línea de comandos:

scalac HolaMundo.scala

Como resultado de la compilación se crearán tres ficheros: HolaMundo.class,  HolaMundo$.class y HolaMundo$delayedInit$body.class en el propio directorio donde se lanza la compilación, y que pueden ejecutarse con la siguiente instrucción desde línea de comandos:

scala HolaMundo

O de forma más abreviada, compilando y ejecutando con una sola línea:

scala HolaMundo.scala

En buena lógica, un desarrollo moderno implica el uso de un IDE. No obstante, conocer como funcionan las cosas a más bajo nivel puede ayudar a futuro a resolver problemas, o aspirar a usos más avanzados del lenguaje, su entorno y herramientas.

IntelliJ IDEA

La versión gratuita actual del popular IDE es la 2018.2.5, que puede descargarse en formato zip y descomprimirse en cualquier directorio <INTELLIJ>.

Basta con añadir el directorio <INTELLIJ>/bin en el PATH del sistema para poder ejecutar IntelliJ (64 bits) desde línea de comandos:

idea64

Tras aceptar la licencia, y decidir si se quiere enviar o no informes de uso, se puede crear un nuevo proyecto.

La instalación por defecto no tiene instalado el plugin de Scala. Se puede instalar desde la propia ventana de de creación de proyecto a través de «Configure > Plugins > Install JetBrains plugin… > Scala > Install». Después de reiniciar el IDE aparecerá la opción para crear un proyecto de tipo Scala con tres plantillas, siendo la basada en «sbt» la recomendada por defecto, siendo sbt una herramienta de construcción de proyectos en Scala que funciona de forma similar a como lo hacen otras más populares como Maven o Gradle.

A partir de aquí la creación de un nuevo proyecto puede realizarse seleccionando la versión de Scala y sbt que se quiere utilizar. El IDE automáticamente descargará las versiones seleccionadas y creará el proyecto en la ruta indicada con la estructura clásica de directorios de las aplicaciones Java basadas en Maven, pero cambiando el directorio java por scala. Y con esto ya se puede empezar el tour básico de Scala.