Tutorial de Scala para Programadores Java
Tutorial de Scala para Programadores Java
Tutorial de Scala para Programadores Java
Introducción
Este documento provee una rápida introducción al lenguaje Scala como también a su compilador. Está
pensado para personas que ya poseen cierta experiencia en programación y quieren una vista rápida de lo que
pueden hacer con Scala. Se asume como un conocimiento básico de programación orientada a objetos,
especialmente en Java.
Un primer ejemplo
Como primer ejemplo, usaremos el programa Hola mundo estándar. No es muy fascinante, pero de esta
manera resulta fácil demostrar el uso de herramientas de Scala sin saber demasiado acerca del lenguaje.
Veamos como luce:
1. object HolaMundo {
2. def main(args: Array[String]) {
3. println("¡Hola, mundo!")
4. }
5. }
La estructura de este programa debería ser familiar para programadores Java: consiste de un método
llamado main que toma los argumentos de la línea de comando (un array de objetos String) como parámetro;
el cuerpo de este método consiste en una sola llamada al método predefinido println con el saludo amistoso
como argumento. El método main no retorna un valor (se puede entender como un procedimiento). Por lo
tanto, no es necesario que se declare un tipo retorno.
Lo que es menos familiar a los programadores Java es la declaración de object que contiene al
método main. Esa declaración introduce lo que es comúnmente conocido como objeto singleton, que es una
clase con una sola instancia. Por lo tanto, dicha construcción declara tanto una clase
llamada HolaMundo como una instancia de esa clase también llamada HolaMundo. Esta instancia es creada
bajo demanda, es decir, la primera vez que es utilizada.
El lector astuto notará que el método main no es declarado como static. Esto es así porque los miembros
estáticos (métodos o campos) no existen en Scala. En vez de definir miembros estáticos, el programador de
Scala declara estos miembros en un objeto singleton.
Compilando el ejemplo
Para compilar el ejemplo utilizaremos scalac, el compilador de Scala. scalac funciona como la mayoría de
los compiladores. Toma un archivo fuente como argumento, algunas opciones y produce uno o varios
archivos objeto. Los archivos objeto que produce son archivos class de Java estándar.
Si guardamos el programa anterior en un archivo llamado HolaMundo.scala, podemos compilarlo
ejecutando el siguiente comando (el símbolo mayor > representa el prompt del shell y no debe ser escrita):
1. > scalac HolaMundo.scala
Esto generará algunos archivos class en el directorio actual. Uno de ellos se llamará HolaMundo.class y
contiene una clase que puede ser directamente ejecutada utilizando el comando scala, como mostramos en la
siguiente sección.
Ejecutando el ejemplo
Una vez compilado, un programa Scala puede ser ejecutado utilizando el comando scala. Su uso es muy
similar al comando java utilizado para ejecutar programas Java, y acepta las mismas opciones. El ejemplo de
arriba puede ser ejecutado utilizando el siguiente comando, que produce la salida esperada:
1. > scala -classpath . HolaMundo
2.
3. ¡Hola, mundo!
1. df.format(ahora)
Esto parece tener como un detalle sintáctico menor, pero tiene importantes consecuencias, una de ellas la
exploraremos en la próxima sección.
Para concluir esta sección sobre la interacción con Java, es importante notar que es también posible heredar
de clases Java e implementar interfaces Java directamente en Scala.
Todo es un objeto
Scala es un lenguaje puramente orientado a objetos en el sentido de que todo es un objeto, incluyendo
números o funciones. Difiere de Java en este aspecto, ya que Java distingue tipos primitivos
(como boolean e int) de tipos referenciales, y no nos permite manipular las funciones como valores.
1. 1 + 2 * 3 / x
Consiste exclusivamente de llamadas a métodos, porque es equivalente a la siguiente expresión, como vimos
en la sección anterior:
1. (1).+(((2).*(3))./(x))
Esto también indica que +, *, etc. son identificadores válidos en Scala.
Los paréntesis alrededor de los números en la segunda versión son necesarios porque el analizador léxico de
Scala usa la regla de “mayor coincidencia”. Por lo tanto partiría la siguiente expresión:
1. 1.+(2)
En estas partes: 1., +, y 2. La razón que esta regla es elegida es porque 1. es una coincidencia válida y es
mayor que 1, haciendo a este un Double en vez de un Int. Al escribir la expresión así:
1. (1).+(2)
previene que el 1 sea tomado como un Double.
Funciones anónimas
El programa anterior es fácil de entender, pero puede ser refinado aún más. Primero que nada, es interesante
notar que la función tiempoVuela está definida solamente para ser pasada posteriormente a la
función unaVezPorSegundo. Tener que nombrar esa función, que es utilizada solamente una vez parece un
poco innecesario y sería bueno poder construirla justo cuando sea pasada a unaVezPorSegundo. Esto es
posible en Scala utilizando funciones anónimas, que son exactamente eso: funciones sin nombre. La versión
revisada de nuestro temporizador utilizando una función anónima luce así:
1. object TemporizadorAnonimo {
2. def unaVezPorSegundo(callback: () => Unit) {
3. while (true) { callback(); Thread sleep 1000 }
4. }
5. def main(args: Array[String]) {
6. unaVezPorSegundo(
7. () => println("El tiempo vuela como una flecha...")
8. )
9. }
10. }
La presencia de una función anónima en este ejemplo es revelada por la flecha a la derecha =>que separa los
argumentos de la función del cuerpo de esta. En este ejemplo, la lista de argumentos está vacía, como se ve
por el par de paréntesis vacíos a la izquierda de la flecha. El cuerpo de la función es el mismo que
en tiempoVuela del programa anterior.
Clases
Como hemos visto anteriormente, Scala es un lenguaje orientado a objetos, y como tal tiene el concepto de
Clase (en realidad existen lenguajes orientados a objetos que no cuentan con el concepto de clases, pero Scala
no es uno de ellos). Las clases en Scala son declaradas utilizando una sintaxis que es cercana a la de Java. Una
diferencia importante es que las clases en Scala pueden tener parámetros. Ilustramos esto en el siguiente
ejemplo, la definición de un número complejo:
Herencia y sobreescritura
Todas las clases en Scala heredan de una superclase. Cuando ninguna superclase es especificada, como es el
caso de Complejo se utiliza implícitamente scala.AnyRef.
Es posible sobreescribir métodos heredados de una superclase en Scala. Aunque es necesario explicitar
específicamente que un método sobreescribe otro utilizando el modificador override, de manera de evitar
sobreescrituras accidentales. Como ejemplo, nuestra clase Complejo puede ser aumentada con la redefinición
del método toString heredado de Object.
1. class Complejo(real: Double, imaginaria: Double) {
2. def re = real
3. def im = imaginaria
4. override def toString() =
5. "" + re + (if (im < 0) "" else "+") + im + "i"
6. }
Ahora examinaremos cómo estos árboles son representados y manipulados en Scala mediante un pequeño
programa que oficie de calculadora. El objetivo de este programa es manipular expresiones aritméticas
simples compuestas de sumas de enteros y variables. Dos ejemplos de estas expresiones pueden
ser: 1+2 y (x+x)+(7+y).
Primero tenemos que decidir una representación para tales expresiones. La más natural es un árbol, donde los
nodos son las operaciones (la adición en este caso) y las hojas son valores (constantes o variables).
En Java, un árbol así sería representado utilizando una superclase abstracta para los árboles, y una subclase
concreta por nodo u hoja. En un lenguaje de programación funcional uno utilizaría un tipo de dato algebraico
para el mismo propósito. Scala provee el concepto de clases case que está en el medio de los dos conceptos
anteriores. Aquí mostramos como pueden ser usadas para definir el tipo de los árboles en nuestro ejemplo:
1. abstract class Arbol
2. case class Sum(l: Arbol, r: Arbol) extends Arbol
3. case class Var(n: String) extends Arbol
4. case class Const(v: Int) extends Arbol
El hecho de que las clases Sum, Var y Const sean declaradas como clases case significa que dififieren de las
clases normales en varios aspectos:
no es obligatorio utilizar la palabra clave new para crear instancias de estas clases (es decir, se puede
escribir Const(5) en lugar de new Const(5)),
se crea automáticamente un “getter” (un método para obtener el valor) para los parámetros utilizados
en el constructor (por ejemplo es posible obtener el valor de v de una instancia cde la clase Const de
la siguiente manera: c.v),
se proveen definiciones por defecto de los métodos equals y hashCode, que trabajan sobre la
estructura de las instancias y no sobre su identidad,
se crea una definición por defecto del método toString que imprime el valor de una forma “tipo
código) (ej: la expresión del árbol x+1 se imprimiría Sum(Var(x),Const(1))),
las instancias de estas clases pueden ser descompuestas mediante reconocimiento de
patrones (pattern matching) como veremos más abajo.
Ahora que hemos definido el tipo de datos para representar nuestra expresión aritmética podemos empezar
definiendo operaciones para manipularlas. Empezaremos con una función para evaluar una expresión en
un entorno. El objetivo del entorno es darle valores a las variables. Por ejemplo, la expresión x+1 evaluada en
un entorno que asocia el valor 5 a la variable x, escrito { x -> 5 }, da como resultado 6.
Por lo tanto, tenemos que encontrar una manera de representar entornos. ¡Podríamos por supuesto utilizar
alguna estructura de datos asociativa como una tabla hash, pero podemos directamente utilizar funciones! Un
entorno realmente no es nada más que una función la cual asocia valores a variables. El entorno { x ->
5 } mostrado anteriormente puede ser fácilmente escrito de la siguiente manera en Scala:
1. { case "x" => 5 }
Esta notación define una función la cual, dado un string "x" como argumento retorna el entero 5, y falla con
una excepción si no fuera así.
Antes de escribir la función evaluadora, démosle un nombre al tipo de los entornos. Podríamos por supuesto
simplemente utilizar String => Int para los entornos, pero simplifica el programa introducir un nombre
para este tipo, y hace que los futuros cambios sean más fáciles. Esto lo realizamos de la siguiente manera:
1. type Entorno = String => Int
De ahora en más, el tipo Entorno puede ser usado como un alias del tipo de funciones definidas
de String a Int.
Ahora podemos dar la definición de la función evaluadora. Conceptualmente, es muy sencillo: el valor de una
suma de dos expresiones es simplemente la suma de los valores de estas expresiones; el valor de una variable
es obtenido directamente del entorno; y el valor de una constante es la constante en sí misma. Expresar esto
en Scala no resulta para nada difícil:
Para explorar un poco más esto de pattern matching definamos otra operación aritmética: derivación
simbólica. El lector recordará las siguientes reglas sobre esta operación:
1. la derivada de una suma es la suma de las derivadas,
2. la derivada de una variable v es uno (1) si v es la variable relativa a la cual la derivada toma lugar, y
cero (0) de otra manera,
3. la derivada de una constante es cero (0).
Estas reglas pueden ser traducidas casi literalmente en código Sclaa, para obtener la siguiente definición.
1. Expresión: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y)))
2. Evaluación con x=5, y=7: 24
3. Derivada con respecto a x:
4. Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0)))
5. Derivada con respecto a y:
6. Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))
Al examinar la salida vemos que el resultado de la derivada debería ser simplificado antes de ser presentado al
usuario. Definir una función de simplificación básica utilizando reconocimiento de patrones es un problema
interesante (y, por no decir complejo, que necesita una solución astuta), lo dejamos para un ejercicio para el
lector.
Traits
Nota: La palabra Trait(/treɪt/, pronunciado Treit) puede ser traducida literalmente como “Rasgo”. De todas
maneras, decido utilizar la notación original por ser un concepto muy arraigado a Scala
Aparte de poder heredar código de una super clase, una clase en Scala puede también importar código de uno
o varios traits.
Tal vez la forma más fácil para un programador Java de entender qué son los traits es verlos como interfaces
que también pueden contener código. En Scala, cuando una clase hereda de un trait, implementa la interface
de ese trait, y hereda todo el código contenido en el trait.
Para ver la utilidad de los traits, veamos un ejemplo clásico: objetos ordenados. Generalmente es útil tener la
posibilidad de comparar objetos de una clase dada entre ellos, por ejemplo, para ordenarlos. En Java, los
objetos que son comparables implementan la interfaz Comparable. En Scala, podemos hacer algo un poco
mejor que en Java al definir un trait equivalente Comparable que invocará a Ord.
Cuando comparamos objetos podemos utilizar seis predicados distintos: menor, menor o igual, igual, distinto,
mayor o igual y mayor. De todas maneras, definir todos estos es fastidioso, especialmente que cuatro de estos
pueden ser expresados en base a los otros dos. Esto es, dados los predicados “igual” y “menor” (por ejemplo),
uno puede expresar los otros. En Scala, todas estas observaciones pueden ser fácilmente capturadas mediante
la siguiente declaración de un Trait:
1. trait Ord {
2. def < (that: Any): Boolean
3. def <=(that: Any): Boolean = (this < that) || (this == that)
4. def > (that: Any): Boolean = !(this <= that)
5. def >=(that: Any): Boolean = !(this < that)
6. }
Esta definición crea un nuevo tipo llamado Ord el cual juega el mismo rol que la interfaz Comparable, como
también provee implementaciones de tres predicados en términos de un cuarto, abstracto. Los predicados para
igualidad y su inverso (distinto, no igual) no aparecen aquí ya que por defecto están presenten en todos los
objetos.
El tipo Any el cual es usado arriba es el supertipo de todos los otros tipos en Scala. Puede ser visto como una
versión más general del tipo Object en Java, ya que Any también es supertipo de Int, Float, etc. cosa que
no se cumple en Java (int por ejemplo es un tipo primitivo).
Para hacer a un objeto de la clase comparable es suficiente definir los predicados que comprueban la igualdad
y la inferioridad y mezclar la clase Ord de arriba. Como un ejemplo, definamos una clase Fecha que
representa fechas en el calendario gregoriano.
1. class Fecha(d: Int, m: Int, a: Int) extends Ord {
2. def anno = a
3. def mes = m
4. def dia = d
5. override def toString(): String = anno + "-" + mes + "-" + dia
La parte importante aquí es la declaración extends Ord la cual sigue al nombre de la clase y los parámetros.
Declara que la clase Fecha hereda del trait Ord.
Después redefinimos el método equals, heredado de Object, para comparar correctamente fechas mediante
sus campos individuales. La implementación por defecto de equals no es utilizable, porque como en Java,
compara los objetos físicamente. Por lo tanto, llegamos a esto:
1. override def equals(that: Any): Boolean =
2. that.isInstanceOf[Fecha] && {
3. val o = that.asInstanceOf[Fecha]
4. o.dia== dia && o.mes == mes && o.anno== anno
5. }
Este método utiliza el método predefinido isInstanceOf (“es instancia de”) y asInstanceOf(“como
instancia de”). El primero isInstanceOf se corresponde con el operador java instanceOf y
retorna true si y solo si el objeto en el cual es aplicado es una instancia del tipo dado. El
segundo, asInstanceOf, corresponde al operador de casteo en Java: si el objeto es una instancia de un tipo
dado, esta es vista como tal, de otra manera se lanza una excepción ClassCastException.
Finalmente, el último método para definir es el predicado que comprueba la inferioridad. Este hace uso de
otro método predefinido, error que lanza una excepción con el mensaje de error provisto.
1. def <(that: Any): Boolean = {
2. if (!that.isInstanceOf[Fecha])
3. error("no se puede comparar" + that + " y una fecha")
4.
5. val o = that.asInstanceOf[Fecha]
6. (anno < o.anno) ||
7. (anno== o.anno && (mes < o.mes ||
8. (mes == o.mes && dia < o.dia)))
9. }
Esto completa la definición de la clase Fecha. Las instancias de esta clase pueden ser vistas tanto como
fechas o como objetos comparables. Además, todas ellas definen los seis predicados de comparación
mencionados arriba: equals y < porque aparecen directamente en la definición de la clase Fecha y los otros
porque son heredados del trait Ord.
Los traits son útiles en muchas otras más situaciones que las aquí mostrada, pero discutir sus aplicaciones está
fuera del alcance de este documento.
Tipos Genéricos
Nota: El diseñador de los tipos genéricos en Java fue nada más ni nada menos que Martin Odersky, el
diseñador de Scala.
La última característica de Scala que exploraremos en este tutorial es la de los tipos genéricos. Los
programadores de Java deben estar bien al tanto de los problemas que genera la falta de genéricos en su
lenguaje, lo cual es solucionado en Java 1.5.
Los tipos genéricos proveen al programador la habilidad de escribir código parametrizado por tipos. Por
ejemplo, escribir una librería para listas enlazadas se enfrenta al problema de decidir qué tipo darles a los
elementos de la lista. Ya que esta lista está pensada para ser usada en diferentes contextos, no es posible
decidir que el tipo de elementos sea, digamos, Int. Esto sería completamente arbitrario y muy restrictivo.
Los programadores Java cuentan como último recurso con Object, que es el supertipo de todos los objetos.
Esta solución de todas maneras está lejos de ser ideal, ya que no funciona con tipos primitivos
(int, long, float, etc.) e implica que el programador tenga que realizar muchos casteos de tipos en su
programa.
Scala hace posible definir clases genéricas (y métodos) para resolver este problema. Examinemos esto con un
ejemplo del contenedor más simple posible: una referencia, que puede estar tanto vacía como apuntar a un
objeto de algún tipo.
1. class Referencia[T] {
2. private var contenido: T = _
3. def set(valor: T) { contenido = valor }
4. def get: T = contenido
5. }
La clase Referencia es parametrizada por un tipo llamado T, que es el tipo de sus elementos. Este tipo es
usado en el cuerpo de la clase como el tipo de la variable contenido, el argumento del método set y el tipo
de retorno del método get.
El ejemplo anterior introduce a las variables en Scala, que no deberían requerir mayor explicación. Es
interesante notar que el valor inicial dado a la variable contenido es _, que representa un valor por defecto.
Este valor por defecto es 0 para tipos numéricos, false para tipos Boolean,() para el
tipo Unit y null para el resto de los objetos.
Para utilizar esta clase Referencia, uno necesita especificar qué tipo utilizar por el parámetro T, es decir, el
tipo del elemento contenido por la referencia. Por ejemplo, para crear y utilizar una referencia que contenga
un entero, podríamos escribir lo siguiente:
1. object ReferenciaEntero {
2. def main(args: Array[String]) {
3. val ref = new Referencia[Int]
4. ref.set(13)
5. println("La referencia tiene la mitad de " + (ref.get * 2))
6. }
7. }
Como puede verse en el ejemplo, no es necesario castear el valor retornado por el método get antes de
usarlo como un entero. Tampoco es posible almacenar otra cosa que no sea un entero en esa referencia en
particular, ya que fue declarada como contenedora de un entero.
Conclusión
Scala es un lenguaje tremendamente poderoso que ha sabido heredar las mejores cosas de cada uno de los
lenguajes más exitosos que se han conocido. Java no es la excepción, y comparte muchas cosas con este. La
diferencia que vemos es que, para cada uno de los conceptos de Java, Scala los aumenta, refina y mejora.
Poder aprender todas las características de Scala nos equipa con más y mejores herramientas a la hora de
escribir nuestros programas. Si bien la programación funcional no ha sido una característica de Java, el
programador experimentado puede notar la falta de soporte de este paradigma en múltiples ocasiones. El solo
pensar en el código necesario para proveer a un JButton con el código que debe ejecutar al ser presionado
nos muestra lo necesario que sería contar con herramientas funcionales. Recomendamos entonces tratar de ir
incorporando estas características, por más que sea difícil para el programador Java al estar tan acostumbrado
al paradigma imperativo de este lenguaje.
Este documento dio una rápida introducción al lenguaje Scala y presento algunos ejemplos básicos. El lector
interesado puede seguir, por ejemplo, leyendo el Tutorial de Scala que figura en el sitio de documentación,
o Scala by Example (en inglés). También puede consultar la especificación del lenguaje cuando lo desee.
Introduction
Scala es un lenguaje de programación moderno multi-paradigma diseñado para expresar patrones de
programación comunes de una forma concisa, elegante, y de tipado seguro. Integra fácilmente características
de lenguajes orientados a objetos y funcionales.
Scala es funcional
Scala es también un lenguaje funcional en el sentido que toda función es un valor. Scala provee una sintaxis
ligera para definir funciones anónimas. Soporta funciones de primer orden, permite que las funciones
sean anidadas, y soporta currying. Las clases caso de Scala y las construcciones incorporadas al lenguaje
para reconocimiento de patrones modelan tipos algebráicos usados en muchos lenguajes de programación
funcionales.
Además, la noción de reconocimiento de patrones de Scala se puede extender naturalmente al procesamiento
de datos XML con la ayuda de patrones de expresiones regulares. En este contexto, seq
comprehensions resultan útiles para formular consultas. Estas características hacen a Scala ideal para
desarrollar aplicaciones como Web Services.
Scala es extensible
En la práctica el desarrollo de aplicaciones específicas para un dominio generalmente requiere de “Lenguajes
de dominio específico” (DSL). Scala provee una única combinación de mecanismos del lenguaje que
simplifican la creación de construcciones propias del lenguaje en forma de librerías: * cualquier método
puede ser usado como un operador de infijo o postfijo * las closures son construidas automáticamente
dependiendo del tipo esperado (tipos objetivo).
El uso conjunto de ambas características facilita la definición de nuevas sentencias sin tener que extender la
sintaxis y sin usar facciones de meta-programación como tipo macros.
Scala está diseñado para interoperar bien con el popular Entorno de ejecución de Java 2 (JRE). En particular,
la interacción con el lenguaje orientado a objetos Java es muy sencillo. Scala tiene el mismo esquema de
compilación (compilación separada, carga de clases dinámica) que java y permite acceder a los miles de
librerías de gran calidad existentes.
Tipos Abstractos
En Scala, las cases son parametrizadas con valores (los parámetros de construcción) y con tipos (si las clases
son genéricas). Por razones de consistencia, no es posible tener solo valores como miembros de objetos; tanto
los tipos como los valores son miembros de objetos. Además, ambos tipos de miembros pueden ser concretos
y abstractos. A continuación un ejemplo el cual define de forma conjunta una asignación de valor tardía y un
tipo abstracto como miembros del trait Buffer.
1. trait Buffer {
2. type T
3. val element: T
4. }
Los tipos abstractos son tipos los cuales su identidad no es precisamente conocida. En el ejemplo anterior, lo
único que sabemos es que cada objeto de la clase Buffer tiene un miembro de tipo T, pero la definición de la
clase Buffer no revela qué tipo concreto se corresponde con el tipo T. Tal como las definiciones de valores,
es posible sobrescribir las definiciones de tipos en subclases. Esto permite revelar más información acerca de
un tipo abstracto al acotar el tipo ligado (el cual describe las posibles instancias concretas del tipo abstracto).
En el siguiente programa derivamos la clase SeqBuffer la cual nos permite almacenar solamente sequencias
en el buffer al estipular que el tipo T tiene que ser un subtipo de Seq[U] para un nuevo tipo abstracto U:
1. abstract class SeqBuffer extends Buffer {
2. type U
3. type T <: Seq[U]
4. def length = element.length
5. }
Traits o clases con miembros de tipos abstractos son generalmente usados en combinación con instancias de
clases anónimas. Para ilustrar este concepto veremos un programa el cual trata con un buffer de sequencia que
se remite a una lista de enteros.
1. abstract class IntSeqBuffer extends SeqBuffer {
2. type U = Int
3. }
4.
5. object AbstractTypeTest1 extends App {
6. def newIntSeqBuf(elem1: Int, elem2: Int): IntSeqBuffer =
7. new IntSeqBuffer {
8. type T = List[U]
9. val element = List(elem1, elem2)
10. }
11. val buf = newIntSeqBuf(7, 8)
12. println("length = " + buf.length)
13. println("content = " + buf.element)
14. }
El tipo retornado por el método newIntSeqBuf está ligado a la especialización del trait Buffer en el cual el
tipo U es ahora equivalente a Int. Existe un tipo alias similar en la instancia de la clase anónima dentro del
cuerpo del método newIntSeqBuf. En ese lugar se crea una nueva instancia de IntSeqBuffer en la cual el
tipo T está ligado a List[Int].
Es necesario notar que generalmente es posible transformar un tipo abstracto en un tipo paramétrico de una
clase y viceversa. A continuación, se muestra una versión del código anterior el cual solo usa tipos
paramétricos.
Anotaciones
Las anotaciones sirven para asociar meta-información con definiciones.
Una anotación simple tiene la forma @C o @C(a1, .., an). Aquí, C es un constructor de la clase C, que
debe extender de la clase scala.Annotation. Todos los argumentos de construcción dados a1, ..,
an deben ser expresiones constantes (es decir, expresiones de números literales, strings, clases,
enumeraciones de Java y arrays de una dimensión de estos valores).
Una anotación se aplica a la primer definición o declaración que la sigue. Más de una anotación puede
preceder una definición o declaración. El orden en que es dado estas anotaciones no importa.
Scala Java
scala.cloneable java.lang.Cloneable
scala.deprecated java.lang.Deprecated
scala.remote java.rmi.Remote
scala.serializable java.io.Serializable
1. package examples
2. import java.io._
3. class Reader(fname: String) {
4. private val in = new BufferedReader(new FileReader(fname))
5. @throws(classOf[IOException])
6. def read() = in.read()
7. }
El siguiente programa de Java imprime en consola los contenidos del archivo cuyo nombre es pasado como
primer argumento al método main.
1. package test;
2. import examples.Reader; // Scala class !!
3. public class AnnotaTest {
4. public static void main(String[] args) {
5. try {
6. Reader in = new Reader(args[0]);
7. int c;
8. while ((c = in.read()) != -1) {
9. System.out.print((char) c);
10. }
11. } catch (java.io.IOException e) {
12. System.out.println(e.getMessage());
13. }
14. }
15. }
Si comentamos la anotación throws en la clase Reader se produce el siguiente error cuando se intenta
compilar el programa principal de Java:
1. Main.java:11: exception java.io.IOException is never thrown in body of
2. corresponding try statement
3. } catch (java.io.IOException e) {
4. ^
5. 1 error
Anotaciones en Java
Nota: Asegurate de usar la opción -target:jvm-1.5 con anotaciones de Java.
Java 1.5 introdujo metadata definida por el usuario en la forma de anotaciones. Una característica
fundamental de las anotaciones es que se basan en pares nombre-valor específicos para inicializar sus
elementos. Por ejemplo, si necesitamos una anotación para rastrear el código de alguna clase debemos
definirlo así:
1. @interface Source {
2. public String URL();
3. public String mail();
4. }
Y después utilizarlo de la siguiente manera
1. @Source(URL = "http://coders.com/",
2. mail = "support@coders.com")
3. public class MyClass extends HisClass ...
Una anotación en Scala se asemeja a una invocación a un constructor. Para instanciar una anotación de Java
es necesario usar los argumentos nombrados:
1. @Source(URL = "http://coders.com/",
2. mail = "support@coders.com")
3. class MyScalaClass ...
Esta sintaxis es bastante tediosa si la anotación contiene solo un elemento (sin un valor por defecto) por lo
tanto, por convención, si el nombre es especificado como value puede ser utilizado en Java usando una
sintaxis similar a la de los constructores:
1. @interface SourceURL {
2. public String value();
3. public String mail() default "";
4. }
Y podemos aplicarlo así:
1. @SourceURL("http://coders.com/")
2. public class MyClass extends HisClass ...
En este caso, Scala provee la misma posibilidad:
1. @SourceURL("http://coders.com/")
2. class MyScalaClass ...
El elemento mail fue especificado con un valor por defecto (mediante la cláusula default) por lo tanto no
necesitamos proveer explicitamente un valor para este. De todas maneras, si necesitamos pasarle un valor no
podemos mezclar los dos estilos en Java:
1. @SourceURL(value = "http://coders.com/",
2. mail = "support@coders.com")
3. public class MyClass extends HisClass ...
Scala provee más flexibilidad en este caso:
1. @SourceURL("http://coders.com/",
2. mail = "support@coders.com")
3. class MyScalaClass ...
Clases
En Scala, las clases son plantillas estáticas que pueden ser instanciadas por muchos objetos en tiempo de
ejecución. Aquí se presenta una clase la cual define la clase Point:
1. class Point(xc: Int, yc: Int) {
2. var x: Int = xc
3. var y: Int = yc
4. def move(dx: Int, dy: Int) {
5. x = x + dx
6. y = y + dy
7. }
8. override def toString(): String = "(" + x + ", " + y + ")";
9. }
Esta clase define dos variables x e y, y dos métodos: move y toString. El método move recibe dos
argumentos de tipo entero, pero no retorna ningún valor (implícitamente se retorna el tipo Unit, el cual se
corresponde a void en lenguajes tipo Java). toString, por otro lado, no recibe ningún parámetro, pero
retorna un valor tipo String. Ya que toString sobreescribe el método toString predefinido en una
superclase, tiene que ser anotado con override.
Las clases en Scala son parametrizadas con argumentos constructores (inicializadores). En el código anterior
se definen dos argumentos contructores, xc y yc; ambos son visibles en toda la clase. En nuestro ejemplo son
utilizados para inicializar las variables x e y.
Para instanciar una clase es necesario usar la primitiva new, como se muestra en el siguiente ejemplo:
1. object Classes {
2. def main(args: Array[String]) {
3. val pt = new Point(1, 2)
4. println(pt)
5. pt.move(10, 10)
6. println(pt)
7. }
8. }
El programa define una aplicación ejecutable a través del método main del objeto singleton Classes. El
método main crea un nuevo Point y lo almacena en pt. Note que valores definidos con la
signatura val son distintos de los definidos con var (véase la clase Point arriba) ya que los primeros
(val) no permiten reasignaciones; es decir, que el valor es una constante.
Aquí se muestra la salida del programa:
1. (1, 2)
2. (11, 12)
Clases Case
Scala da soporte a la noción de clases caso (en inglés case classes, desde ahora clases Case). Las clases Case
son clases regulares las cuales exportan sus parámetros constructores y a su vez proveen una descomposición
recursiva de sí mismas a través de reconocimiento de patrones.
A continuación, se muestra un ejemplo para una jerarquía de clases la cual consiste de una super clase
abstracta llamada Term y tres clases concretas: Var, Fun y App.
1. abstract class Term
2. case class Var(name: String) extends Term
3. case class Fun(arg: String, body: Term) extends Term
4. case class App(f: Term, v: Term) extends Term
Esta jerarquía de clases puede ser usada para representar términos de cálculo lambda no tipado. Para facilitar
la construcción de instancias de clases Case, Scala no requiere que se utilice la primitiva new. Simplemente es
posible utilizar el nombre de la clase como una llamada a una función.
Aquí un ejemplo:
1. val x = Var("x")
2. println(x.name)
Para cada una de las clases Case el compilador de Scala genera el método equals el cual implementa la
igualdad estructural y un método toString. Por ejemplo:
1. val x1 = Var("x")
2. val x2 = Var("x")
3. val y1 = Var("y")
4. println("" + x1 + " == " + x2 + " => " + (x1 == x2))
5. println("" + x1 + " == " + y1 + " => " + (x1 == y1))
imprime
Tipos Compuestos
Algunas veces es necesario expresar que el tipo de un objeto es un subtipo de varios otros tipos. En Scala esto
puede ser expresado con la ayuda de tipos compuestos, los cuales pueden entenderse como la intersección de
otros tipos.
Suponga que tenemos dos traits Cloneable y Resetable:
1. trait Cloneable extends java.lang.Cloneable {
2. override def clone(): Cloneable = {
3. super.clone().asInstanceOf[Cloneable]
4. }
5. }
6. trait Resetable {
7. def reset: Unit
8. }
Ahora suponga que queremos escribir una función cloneAndReset la cual recibe un objeto, lo clona y
resetea el objeto original:
1. def cloneAndReset(obj: ?): Cloneable = {
2. val cloned = obj.clone()
3. obj.reset
4. cloned
5. }
La pregunta que surge es cuál es el tipo del parámetro obj. Si este fuera Cloneable entonces el objeto puede
ser clonado mediante el método clone, pero no puede usarse el método reset; Si
fuera Resetable podríamos resetearlo mediante el método reset, pero no sería posible clonarlo. Para evitar
casteos (refundiciones, en inglés casting) de tipos en situaciones como la descrita, podemos especificar que
el tipo del objeto obj sea tanto Clonable como Resetable. En tal caso estaríamos creando un tipo
compuesto; de la siguiente manera:
1. def cloneAndReset(obj: Cloneable with Resetable): Cloneable = {
2. //...
3. }
Los tipos compuestos pueden crearse a partir de varios tipos de objeto y pueden tener un refinamiento el cual
puede ser usado para acotar la signatura los miembros del objeto existente.
La forma general es: A with B with C ... { refinamiento }
1. (13, 19)
2. (14, 18)
3. (15, 17)
4. (16, 16)
Existe también una forma especial de comprensión de secuencias la cual retorna Unit. En este caso las
variables que son creadas por la lista de generadores y filtros son usados para realizar tareas con efectos
colaterales (modificaciones de algún tipo). El programador tiene que omitir la palabra reservada yield para
usar una comprensión de este tipo.
1. object ComprehensionTest3 extends App {
2. for (i <- Iterator.range(0, 20);
3. j <- Iterator.range(i, 20) if i + j == 32)
4. println("(" + i + ", " + j + ")")
5. }
Objetos Extractores
En Scala pueden ser definidos patrones independientemente de las clases Caso (en inglés case classes, desde
ahora clases Case). Para este fin exite un método llamado unapply que proveera el ya dicho extractor. Por
ejemplo, en el código siguiente se define el objeto extractor Twice
1. object Twice {
2. def apply(x: Int): Int = x * 2
3. def unapply(z: Int): Option[Int] = if (z%2 == 0) Some(z/2) else None
4. }
5.
6. object TwiceTest extends App {
7. val x = Twice(21)
8. x match { case Twice(n) => Console.println(n) } // imprime 21
9. }
Hay dos convenciones sintácticas que entran en juego aquí:
Clases genéricas
Tal como en Java 5 (JDK 1.5), Scala provee soporte nativo para clases parametrizados con tipos. Eso es
llamado clases genéricas y son especialmente importantes para el desarrollo de clases tipo colección.
A continuación, se muestra un ejemplo:
1. class Stack[T] {
2. var elems: List[T] = Nil
3. def push(x: T) { elems = x :: elems }
4. def top: T = elems.head
5. def pop() { elems = elems.tail }
6. }
La clase Stack modela una pila mutable que contiene elementos de un tipo arbitrario T (se dice, “una pila de
elementos T). Los parámetros de tipos nos aseguran que solo elementos legales (o sea, del tipo T) sean
insertados en la pila (apilados). De forma similar, con los parámetros de tipo podemos expresar que el
método top solo devolverá elementos de un tipo dado (en este caso T).
Aquí se muestra un ejemplo del uso de dicha pila:
1. 97
2. 1
Nota: los subtipos de tipos genéricos son *invariante*. Esto significa que si tenemos una pila de caracteres
del tipo Stack[Char], esta no puede ser usada como una pila de enteros tipo Stack[Int]. Esto no sería
razonable ya que nos permitiría introducir elementos enteros en la pila de caracteres. Para
concluir, Stack[T] es solamente un subtipo de Stack[S] si y solo si S = T. Ya que esto puede llegar a
ser bastante restrictivo, Scala ofrece un mecanismo de anotación de parámetros de tipo para controlar el
comportamiento de subtipos de tipos genéricos.
Parámetros implícitos
Un método con parámetros implícitos puede ser aplicado a argumentos tal como un método normal. En este
caso la etiqueta implicit no tiene efecto. De todas maneras, si a un método le faltan argumentos para sus
parámetros implícitos, tales argumentos serán automáticamente provistos.
Los argumentos reales que son elegibles para ser pasados a un parámetro implícito están contenidos en dos
categorías: * Primera, son elegibles todos los identificadores x que puedan ser accedidos en el momento de la
llamada al método sin ningún prefijo y que denotan una definición implícita o un parámetro implícito. *
Segunda, además son elegibles todos los miembros de modulos companion (ver [objetos companion]
(singleton-objects.html)) del tipo de parámetro implicito que tienen la etiqueta implicit.
En el siguiente ejemplo definimos un método sum el cual computa la suma de una lista de elementos usando
las operaciones add y unit de Monoid. Note que los valores implícitos no pueden ser de nivel superior (top-
level), deben ser miembros de una plantilla.
abstract class SemiGroup[A] {
def add(x: A, y: A): A
}
abstract class Monoid[A] extends SemiGroup[A] {
def unit: A
}
object ImplicitTest extends App {
implicit object StringMonoid extends Monoid[String] {
def add(x: String, y: String): String = x concat y
def unit: String = ""
}
implicit object IntMonoid extends Monoid[Int] {
def add(x: Int, y: Int): Int = x + y
def unit: Int = 0
}
def sum[A](xs: List[A])(implicit m: Monoid[A]): A =
if (xs.isEmpty) m.unit
else m.add(xs.head, sum(xs.tail))
println(sum(List(1, 2, 3)))
println(sum(List("a", "b", "c")))
}
Esta es la salida del programa:
6
abc
Clases Internas
En Scala es posible que las clases tengan como miembro otras clases. A diferencia de lenguajes
similares a Java donde ese tipo de clases internas son miembros de las clases que las envuelven,
en Scala esas clases internas están ligadas al objeto externo. Para ilustrar esta diferencia, vamos a
mostrar rápidamente una implementación del tipo grafo:
1. class Graph {
2. class Node {
3. var connectedNodes: List[Node] = Nil
4. def connectTo(node: Node) {
5. if (connectedNodes.find(node.equals).isEmpty) {
6. connectedNodes = node :: connectedNodes
7. }
8. }
9. }
10. var nodes: List[Node] = Nil
11. def newNode: Node = {
12. val res = new Node
13. nodes = res :: nodes
14. res
15. }
16. }
En nuestro programa, los grafos son representados mediante una lista de nodos. Estos nodos son
objetos de la clase interna Node. Cada nodo tiene una lista de vecinos que se almacena en la
listaconnectedNodes. Ahora podemos crear un grafo con algunos nodos y conectarlos
incrementalmente:
1. object GraphTest extends App {
2. val g = new Graph
3. val n1 = g.newNode
4. val n2 = g.newNode
5. val n3 = g.newNode
6. n1.connectTo(n2)
7. n3.connectTo(n1)
8. }
Ahora vamos a completar el ejemplo con información relacionada al tipado para definir
explicitamente de qué tipo son las entidades anteriormente definidas:
Singleton Objects
Métodos y valores que no están asociados con instancias individuales de una clase se denominanobjetos
singleton y se denotan con la palabra reservada object en vez de class.
1. package test
2.
3. object Blah {
4. def sum(l: List[Int]): Int = l.sum
5. }
Este método sum está disponible de manera global, y puede ser referenciado, o importado,
comotest.Blah.sum.
Los objetos singleton son una especie de mezcla entre la definición de una clase de utilización única, la cual
no pueden ser instanciada directamente, y un miembro val. De hecho, de la misma menera que los val, los
objetos singleton pueden ser definidos como miembros de un trait o de una clase, aunque esto no es muy
frecuente.
Un objeto singleton puede extender clases y traits. De hecho, una clase Case sin parámetros de tipo generará
por defecto un objeto singleton del mismo nombre, con una Función* trait implementada.
Acompañantes
La mayoría de los objetos singleton no están solos, sino que en realidad están asociados con clases del mismo
nombre. El “objeto singleton del mismo nombre” de una case Case, mencionada anteriormente es un ejemplo
de esto. Cuando esto sucede, el objeto singleton es llamado el objeto acompañante de la clase, y la clase
es a su vez llamada la clase acompañante del objeto.
Scaladoc proporciona un soporte especial para ir y venir entre una clase y su acompañante: Si el gran círculo
conteniendo la “C” u la “O” tiene su borde inferior doblado hacia adentro, es posible hacer click en el círculo
para ir a su acompañante.
Una clase y su objeto acompañante, si existe, deben estar definidos en el mismo archivo fuente. Como por
ejemplo:
Funciones Anidadas
En scala es posible anidar definiciones de funciones. El siguiente objeto provee una función filter para
extraer valores de una lista de enteros que están por debajo de un valor determinado:
object FilterTest extends App {
def filter(xs: List[Int], threshold: Int) = {
def process(ys: List[Int]): List[Int] =
if (ys.isEmpty) ys
else if (ys.head < threshold) ys.head :: process(ys.tail)
else process(ys.tail)
process(xs)
}
println(filter(List(1, 9, 2, 8, 3, 7, 4), 5))
}
Nota: la función anidada process utiliza la variable threshold definida en el ámbito externo como un
parámetro de filter.
La salida del programa es:
List(1,2,3,4)
1. () => { System.getProperty("user.dir") }
Existe también una forma simple para escribir los tipos de las funciones. A continuación, se muestran los
tipos de las trés funciones escritas anteriormente:
1. Function1[Int, Int]
2. Function2[Int, Int, String]
3. Function0[String]
Currying
Nota de traducción: Currying es una técnica de programación funcional nombrada en
honor al matemático y lógico Haskell Curry. Es por eso que no se intentará hacer ninguna
traducción sobre el término Currying. Entiendase este como una acción, técnica base de
PF. Como una nota al paso, el lenguaje de programación Haskell debe su nombre a este
eximio matemático.
Los métodos pueden definir múltiples listas de parámetros. Cuando un método es invocado con un número
menor de listas de parámetros, en su lugar se devolverá una función que toma las listas faltantes como sus
argumentos.
1. List(2,4,6,8)
2. List(3,6)
Operadores
el cual reciba un solo parámetro puede ser usado como un operador de infijo (infix). Aquí se muestra la
definición de la clase MyBool, la cual define tres métodos and, or, y negate.
class MyBool(x: Boolean) {
def and(that: MyBool): MyBool = if (x) that else this
def or(that: MyBool): MyBool = if (x) this else that
def negate: MyBool = new MyBool(!x)
}
Ahora es posible utilizar and y or como operadores de infijo:
def not(x: MyBool) = x negate; // punto y coma necesario aquí
def xor(x: MyBool, y: MyBool) = (x or y) and not(x and y)
Como muestra la primera línea del código anterior, es también posible utilizar métodos nularios (que no
reciban parámetros) como operadores de postfijo. La segunda línea define la función xorutilizando los
métodos andy or como también la función not. En este ejemplo el uso de losoperadores de postfijo ayuda a
crear una definición del método xor más fácil de leer.
Para demostrar esto se muestra el código correspondiente a las funciones anteriores pero escritas en una
notación orientada a objetos más tradicional:
def not(x: MyBool) = x.negate; // punto y coma necesario aquí
def xor(x: MyBool, y: MyBool) = x.or(y).and(x.and(y).negate)
1. [7]
En este ejemplo, el método decorator.layout es coaccionado automáticamente a un valor del tipo Int =>
String como es requerido por el método apply. Por favor note que el método decorator.layout es
un método polimórfico (esto es, se abstrae de algunos de sus tipos) y el compilador de Scala primero tiene que
instanciar correctamente el tipo del método.
Reconocimiento de patrones
Nota de traducción: Es difícil encontrar en nuestro idioma una palabra que se relacione directamente con el
significado de match en inglés. Podemos entender a match como “coincidir” o “concordar” con algo. En
algunos lugares se utiliza la palabra machear, aunque esta no existe en nuestro idioma con el sentido que se
le da en este texto, sino que se utiliza como traducción dematch.
Scala tiene incorporado un mecanismo general de reconocimiento de patrones. Este permite identificar
cualquier tipo de datos una política primero-encontrado. Aquí se muestra un pequeño ejemplo el cual muestra
cómo coincidir un valor entero:
Métodos polimórficos
Los métodos en Scala pueden ser parametrizados tanto con valores como con tipos. Como a nivel de clase,
parámetros de valores son encerrados en un par de paréntesis, mientras que los parámetros de tipo son
declarados dentro de un par de corchetes.
retirados de Scala
Desde que descubrimos un problema en la precisión, esta característica está temporariamente retirada del
lenguaje. Si existiese una petición de parte de la comunidad de usuarios, podríamos llegar a reactivarla de una
forma mejorada.
De acuerdo a nuestra opinión los patrones basados en expresiones regulares no resultaron útiles para el
procesamiento de XML. En la vida real, las aplicaciones que procesan XML, XPath parece una opción mucho
mejor. Cuando descubrimos que nuestra traducción de los patrones para expresiones regulares tenía algunos
errores para patrones raros y poco usados, aunque difícil de excluir, decidimos que sería tiempo de simplificar
el lenguaje.
Traits
Nota de traducción: La palabra trait en inglés puede traducirse literalmente como rasgo o
característica. Preferimos la designación original trait por ser una característica muy natural de Scala.
De forma similar a las interfaces de Java, los traits son usados para definir tipos de objetos al especificar el
comportamiento mediante los métodos provistos. A diferencia de Java, Scala permite a los traits ser
parcialmente implementados, esto es, es posible definir implementaciones por defecto para algunos métodos.
En contraste con las clases, los traits no pueden tener parámetros de constructor. A continuación, se muestra
un ejemplo:
1. trait Similarity {
2. def isSimilar(x: Any): Boolean
3. def isNotSimilar(x: Any): Boolean = !isSimilar(x)
4. }
Este trait consiste de dos métodos isSimilar y isNotSimilar. Mientras isSimilar no provee una
implementación concreta del método (es abstracto en la terminología Java), el método isNotSimilar define
una implementación concreta. Consecuentemente, las clases que integren este trait solo tienen que proveer
una implementación concreta para isSimilar. El comportamiento de isNotSimilar es directamente
heredado del trait. Los traits típicamente son integrados a una clase (u otros traits) mediante una Composición
de clases mixin:
1. class Point(xc: Int, yc: Int) extends Similarity {
2. var x: Int = xc
3. var y: Int = yc
4. def isSimilar(obj: Any) =
5. obj.isInstanceOf[Point] &&
6. obj.asInstanceOf[Point].x == x
7. }
8. object TraitsTest extends Application {
9. val p1 = new Point(2, 3)
10. val p2 = new Point(2, 4)
11. val p3 = new Point(3, 3)
12. println(p1.isNotSimilar(p2))
13. println(p1.isNotSimilar(p3))
14. println(p1.isNotSimilar(2))
15. }
Esta es la salida del programa:
1. false
2. true
3. true
1. object InferenceTest2 {
2. def fac(n: Int) = if (n == 0) 1 else n * fac(n - 1)
3. }
Tampoco es obligatorio especificar el tipo de los parámetros cuando se trate de métodos polimórficos o sean
instanciadas clases genéricas. El compilador de Scala inferirá esos tipos de parámetros faltantes mediante el
contexto y de los tipos de los parámetros reales del método/constructor.
Aquí se muestra un ejemplo que ilustra esto:
1. object InferenceTest4 {
2. var obj = null
3. obj = new Object()
4. }
Este programa no compila porque el tipo inferido para la variable obj es Null. Ya que el único valor de ese
tipo es null, es imposible hacer que esta variable refiera a otro valor.
Tipos Unificados
A diferencia de Java, todos los valores en Scala son objetos (incluyendo valores numéricos y funciones).
Dado que Scala está basado en clases, todos los valores son instancias de una clase. El diagrama siguiente
ilustra esta jerarquía de clases:
1. This is a string
2. 732
3. c
4. true
5. <function>
Varianzas
Scala soporta anotaciones de varianza para parámetros de tipo para clases genéricas. A diferencia de Java 5
(JDK 1.5), las anotaciones de varianza pueden ser agregadas cuando una abstracción de clase es definidia,
mientras que en Java 5, las anotaciones de varianza son dadas por los clientes cuando una albstracción de
clase es usada.
En el artículo sobre clases genéricas dimos un ejemplo de una pila mutable. Explicamos que el tipo definido
por la clase Stack[T] es objeto de subtipos invariantes con respecto al parámetro de tipo. Esto puede
restringir el reuso de la abstracción (la clase). Ahora derivaremos una implementación funcional (es decir,
inmutable) para pilas que no tienen esta restricción. Nótese que este es un ejemplo avanzado que combina el
uso de métodos polimórficos, límites de tipado inferiores, y anotaciones de parámetros de tipo covariante de
una forma no trivial. Además hacemos uso declases internas para encadenar los elementos de la pila sin
enlaces explícitos.
class Stack[+A] {
def push[B >: A](elem: B): Stack[B] = new Stack[B] {
override def top: B = elem
override def pop: Stack[B] = Stack.this
override def toString() = elem.toString() + " " +
Stack.this.toString()
}
def top: A = sys.error("no element on stack")
def pop: Stack[A] = sys.error("no element on stack")
override def toString() = ""
}
Vistas
Parámetros implícitos y métodos también pueden definir conversiones implícitas llamadas vistas. Una vista de
tipo S a T es definida por un valor implícito que tiene una función del tipo S => T, o por un método implícito
convertible a un valor de tal tipo.
Las vistas son aplicadas en dos situaciones: * Si una expresión e es de tipo S, y S no se ajusta al tipo esperado
de la expresión T. * En una selección e.m con e de tipo T, si el selector m no es un miembro de T.
En el primer caso, una vista v es buscada la cual sea aplicable a e y cuyo tipo resultado se ajusta a T. En el
segundo caso, una vista v es buscada para la cual sea aplicable a e y cuyor resultado contenga un miembro
llamado m.
La siguiente operación entre las dos listas xs y ys de tipo List[Int] es legal:
1. xs <= ys
asumiendo que los métodos implícitos list2ordered e int2ordered definidos abajo estén en el alcance
de la operación:
1. implicit def list2ordered[A](x: List[A])
2. (implicit elem2ordered: a => Ordered[A]): Ordered[List[A]] =
3. new Ordered[List[A]] { /* .. */ }
4.
5. implicit def int2ordered(x: Int): Ordered[Int] =
6. new Ordered[Int] { /* .. */ }
La función list2ordered puede ser también expresada con el uso de un límite de vista por un parámetro de
tipo:
1. implicit def list2ordered[A <% Ordered[A]](x: List[A]): Ordered[List[A]] =
...
El compilador de Scala genera código equivalente a la definición de list2ordered vista anteriormente.
El objeto scala.Predef importado implicitamente declara varios tipos predefinidos (ej. Pair) y métodos
(ej. assert) pero también varias vistas. El siguiente ejemplo muestra una idea de la vista
predefinida charWrapper:
1. final class RichChar(c: Char) {
2. def isDigit: Boolean = Character.isDigit(c)
3. // isLetter, isWhitespace, etc.
4. }
5. object RichCharTest {
6. implicit def charWrapper(c: char) = new RichChar(c)
7. def main(args: Array[String]) {
8. println('0'.isDigit)
9. }
10. }
Representación en ejecución
Los datos en XML son representados como árboles etiquetados. A partir de Scala 1.2 (versiones previas
debían usar la opción -Xmarkup), es posible crear convenientemente tales nodos etiquetados utilizando
sintaxis XML.
1. <html>
2. <head>
3. <title>Hello XHTML world</title>
4. </head>
5. <body>
6. <h1>Hello world</h1>
7. <p><a href="http://scala-lang.org/">Scala</a> talks XHTML</p>
8. </body>
9. </html>
Este documento puede ser creado por el siguiente programa en Scala:
Data Binding
En muchos casos se tiene un DTD para los documentos XML que se quieren procesar. En este caso se quieren
crear clases especiales para esto, y algo de código para parsear y guardar el XML. Scala tiene una ingeniosa
herramienta que transforma tus DTDs en una colección de definiciones de clases en Scala que hacen todo el
trabajo.