Scala: Parte 2
Los primeros pasos en Scala
Como había comentado en mi post anterior sobre el lenguaje Scala, mi pretensión es contribuir "paso a paso" con algunas de las cosas importantes de este lenguaje, discutiendo conceptos relevantes y algo de código para ilustrar esos conceptos.
Creo que es necesario aprender a caminar con pasos firmes para después correr, y por esa razón me voy despacio explicando cada cosa en su momento.
Casi todos los libros y tutoriales de Scala aconsejan comenzar con el intérprete de Scala y "oyendo" a los que saben voy a hacer lo mismo, aunque también me gustaría "saltar del REPL a programas completos. Esa forma me ha ayudado a entender el lenguaje, aunque debo confesar que aun me falta mucho por aprender y que bueno que sea así pues evito el aburrimiento.
El REPL (Read Eval Print Loop) es un shell interactivo que compila código de Scala y retorna el resultado (o el tipo) de manera inmediata. Espero que todos los que leen esto ya tengan en su máquina a Scala y puedan interactuar con el intérprete a medida que leen esta notas.
Definamos algunas variables en el REPL:
El intérprete me responde:
EL REPL es muy verboso y me indica que definí una variable y el tipo usado. "Hola Java Mexico" es un literal String y notamos que Scala utiliza la clase
para los tipos cadena de caracteres.
Esa línea simple ilustra algunos conceptos simples pero importantes del lenguaje entre ellos los siguientes:
La sintaxis de Scala es más regular que la sintaxis de Java en el sentido que todas las definiciones comienzan con una palabra reservada. Las variables se definen con
y
y las funciones se definen con
. El uso de
también lo encontraremos prefijando a los parámetros de funciones cuando deseamos utilizar "call by name". Eso lo veremos en otro momento.
Scala es un lenguaje de tipado estático con inferencia de tipos. El código
no especifica el tipo de mensaje, porque el lenguaje puede inferirlo del tipo de resultado de la expresión de la parte derecha de la asignación. Podríamos por supuesto ser muy explícitos y escribir:
y el REPL me devuelve
Observen que ahora el REPL especifica el tipo con su nombre parcial: no usa el nombre totalmente calificado de la clase
.
El REPL acepta una nueva redefinición de la variable porque comienza un nuevo ámbito (scope). En un programa normal esa nueva línea resultaría en un error de compilación.
Escribimos ahora:
y el REPL nos dice:
Scala tiene dos tipos de variables, vals y vars. Una variable definida con val asocia un identificador con un valor inmutable. En ocasiones se dice que la variable definida así es inmutable. Si se desea pensar en términos de la programación funcional, es mejor decir que es el valor quien es inmutable y por tanto el identificador que usamos para referirnos a él no puede ser cambiado. No es un pecado decir que un val es similar a una variable final en Java.
Una variable definida con var es una variable en realidad y el valor que identifica puede cambiar durante la ejecución del programa. El código siguiente es válido:
Podemos aplicar métodos de la clase
sobre un literal cadena, de la misma forma que hacemos en Java:
O haciendo uso de la sintaxis de "operadores" infijos que Scala brinda:
Conversiones implícitas
¿Qué tal el código siguiente?
El método
no está definido para la clase
, por lo que algo más está interviniendo aquí, para que lo anterior no produzca un error de compilación.
Ese algo más son las conversiones implícitas en Scala: una de las soluciones que este lenguaje ofrece para "extender" la funcionalidad de una clase que no nos pertenece y que no tenemos la opción de extenderla por las vías "normales" de la herencia y la composición.
Como todos conocen, hay una diferencia fundamental entre nuestro código (clases y otras cosas que diseñamos) y el código que usamos de bibliotecas y otros diseñadores. Con nuestro código podemos hacer lo que queramos, mientras con el código de terceros no nos queda otra que usarlo tal cual.
En ocasiones necesitamos extender ese código de terceros. Varios lenguajes brindan varias técnicas para eso. Ruby ofrece el mecanismo de módulos, C# ofrece los métodos de extensión estáticos, etc.
Scala por su parte introduce la técnica de parámetros y conversiones implícitas, con las cuales se le brinda información adicional al compilador para poder usar esas conversiones ante la presencia de errores de compilación debidos fundamentalmente a compatibilidad entre tipos.
¿Qué hace el compilador cuando encuentra la línea:
Pues lo primero que hace es percatarse de que hay un error de compilación. El método
no es un método de la clase
. Inmediatamente que esto sucede, el compilador trata de buscar alguna función de conversión implícita que solucione este problema y que por supuesto esté en ámbito (scope). Esa función tendrá que lograr "convertir" el literal String, en un objeto de alguna clase que tenga el método
y que produzca un nuevo objeto de tipo
con el resultado esperado.
En el ámbito de todo programa en Scala se dispone de un objeto (¿un objeto o una clase?) denominado objeto Predef:
que dispone del método de conversión implícita siguiente:
El compilador se encarga también de "investigar" la clase
, y comprueba que esa clase ofrece el método
que invierte una "cadena". El compilador aplica "transparentemente" la conversión de
, invoca el método
sobre el objeto de tipo
y nos da como resultado la cadena invertida de tipo
.
El código del objeto incluye otras muchas definiciones utilitarias que tienen como objetivo escribir códigos más compactos. Por ejemplo escribimos:
¿Cómo está definida println?
Está definida en el objeto Predef así como otras funciones de impresión y lectura comunes como:
Definamos algunas funciones
La definición de una función en Scala comienza con la palabra reservada def, seguida por el nombre de la función, la lista o listas de parámetros (¿más de una lista?), el tipo de resultado, el signo igual y el bloque que define la función. Un ejemplo viene a continuación:
Usamos esa función:
Scala no puede inferir los tipos de sus parámetros, por lo que es obligada la anotación de tipos de los parámetros.
Las funciones en Scala no requieren en la mayoría de los casos la sentencia
. El resultado devuelto es el valor de la última expresión.
Scala utiliza el signo igual para enfatizar el concepto de función: el cuerpo de la función calcula algo y devuelve un resultado. Cuando se omite el signo igual nos referimos a un procedimiento destinado mayormente a producir efectos laterales o a realizar operaciones de I/O.
Scala puede inferir el tipo de resultado de una función: en este caso puede no escribirse el tipo de resultado de la función
. Puede inferirse del código.
Por ejemplo, ¿cuál es el tipo de retorno de la función siguiente?:
Si el argumento es verdadero, el tipo de retorno es
(se infiere del literal entero 5). Si el argumento es falso el tipo de retorno es
(se infiere del literal cadena "5").
El tipo de retorno entonces debe ser un tipo que sea base de los tipos valores y de los tipos referencias: el tipo Any.
Los dos ejemplos anteriores tienen cosas interesantes con respecto a la distinción entre expresiones y sentencias (statements). Noten que para el caso de la sentencia (statement)while tuvimos la necesidad de crear una variable temporal mutable para devolver el resultado, mientras que la estructura de control
es una expresión y se evalúa a un valor, sin la necesidad de hacer uso de variables mutables temporales.
La programación funcional tiene entre sus objetivos evitar los efectos laterales, la modificación del estado y hacer uso "exclusivamente" de expresiones que regresan un valor en el momento de ejecutarse. Que
es una expresión lo podemos demostrar en el REPL:
¿Por qué el siguiente código no compila?
Hoy es domingo y toca. Nos vemos en la próxima
- bferro's blog
- Inicie sesión o regístrese para enviar comentarios
Algo olvidé
Algo olvidé comentar. Lo dejo para la próxima
Según lo entiendo de la
Según lo entiendo de la lectura, no compila porque no va a haber forma de poder sumar un enero y Any, que es el tipo de retorno de la expresión:
y el anterior sí funcionó porque
¿Correcto?
La conversión explicita es un mecanismo mucho muy poderoso. Mientras otros lenguajes de programación permiten agregar un método a un tipo de dato, Scala convierte un tipo de dato en otro.
Esto sumado a los operadores infijos que señala bferro podria darnos con que construir una literal falsa de fecha:
Ejemplo:
Ese ejemplo lo tomé de aquí:
Lo anterior también se pudo haber escrito con este código un poquito menos mágico:
Probar
Siempre me había preguntado como estaría definido ese método "println" y ahora lo sé :)
Buenos los ejemplos
Buenos los ejemplos de OscarRyz.
Efectivamente el tipo de la expresión
es Any, la clase raíz de Scala y de la cual heredan las clases AnyVal para los tipos valores y AnyRef para los tipos referencias. Scala infiere ese tipo para la expresión atendiendo que la parte then es un Int que hereda de AnyVal y la parte else es un
que hereda de AnyRef (en Scala para la JVM, AnyRef equivale a
).
, que en el caso del ejemplo que compila se aplica al resultado de la expresión
para concatenerlo con el operando
del método (operador)
.
En la clase Any está definido el método
Podemos indicarle al compilador que el resultado es un Int con un "casting" usando el método asInstanceOf[T] definido en la clase Any.
El tipo de resultado ahora es Int (si el casting es incorrecto sucederá un error de runtime).
y entonces:
Al usar el método
de la clase Any, hay que recordar siempre la semántica de borrado de los tipos genéricos. De la documentación de Scala:
produce un error de run time de ClassCastException, pero:
no produce error.
Predef
Que interesante lo de Predef, no sabía de su existencia.
Ahora que lo mencionas (y como lo mencionas) sí causa cierta confusión conceptual el hecho de que un singleton sea nombrado 'object' siendo una clase todavía...
Muy buena continuación!!
Muy muy buena entrada
Muy muy buena entrada @bferro. De verdad que cada comentario y entrada de personas cómo usted dejan algo para los que estamos en pañales en esto de la programación en general ;), da gusto ver cómo hay gente que no pretende tener el conocimiento acumulado e inútil, en lugar de eso lo ponen en práctica y lo comparten.
De nuevo, muy buena entrada.
Un ejemplo de conversión implícita "esclarecedor"
Oderky en su libro "Programming in Scala", comienza el capítulo de conversiones implícitas con un ejemplo muy adecuado para los que conocen Java y que seguramente alguna vez han trabajado con algún componente de Swing al que se le registra un manejador de eventos (listener). Reproduzco aquí ese ejemplo para contribuir al entendimiento de las conversiones implícitas.
y que deseamos procesar esos eventos con un manejador de eventos de tipo
. Escribimos en Scala ( a lo Java) el código siguiente:
, implica que el método de callback tiene que a fuerzas llamarse
y que el argumento pasado al método
tiene que ser de tipo
.
. Si Java manipulara a las funciones como valores de primera clase, seguramente habría usado esa técnica.
espera como argumento un objeto de tipo
.
y resuelto el problema.
Todos los componentes de Swing son despachadores de eventos. Las aplicaciones con Swing son dirigidas por eventos como sabemos.
Tomemos el caso de un botón que genera eventos de tipo
Como bien comenta Odersky, esta porción de código tiene una buena porción de código "boilerplate" que puede ser inferido del contexto.
El mero hecho de que el manejador de eventos que suscribimos en el botón es un manejador de tipo
Es facíl notar que lo que se requiere pasar es una función ( en este caso un procedimiento) que tome como argumento un valor de tipo
En Scala las funciones son valores de primera clase y podríamos usarla, pero tenemos el problema de que el método
¿Qué podemos hacer entonces?
Pues hacer uso de las conversiones implícitas para convertir un tipo función en un tipo
Y ahora, escribimos la función y la pasamos al método
Estamos usando un placeholder para indicar el argumento recibido. En alguna de las partes de esta serie explicaré bien el uso de esas "cosas"
No se trata solamente de poder escribir un código más sencillo; se trata de expresar la semántica con más claridad
BaySick
Encontré otro ejemplo de uso de implicits para hacer un internal DSL de BASIC en Scala, llamado BaySick
Ejemplo