Durante la
entrada anterior analizábamos los conceptos básicos de los
implicits en
Scala desde un punto de vista "teórico", dejando a un lado los ejemplos de código con el objetivo de no convertir el post en algo completamente inmanejable e incomprensible. A lo largo de la entrada que nos ocupa analizaremos diversos ejemplos de aplicación, describiendo con la mayor exactitud posible el mecanismo de funcionamiento de los
implicits aquí descritos.
Como punto de partida realizaremos un sencillo ejemplo en el que veremos como realizar conversiones automáticas a un tipo determinado (el compilador las realizará por nosotros). Supongamos que disponemos de la siguiente definición de clase
class User (private val username:String)
y que deseamos obtener instancias de la clase anterior sin la necesidad de realizar llamadas del tipo
val user:User = new User("migue")
El primer paso que debemos dar será llevar a cabo la definición del implicit correspondiente. En este caso la conversión deberá realizarse desde String hacia User. Para ello definimos algo como lo que sigue:
object UserImplicits {
implicit def stringToUsername(user: String) = new User(user)
}
Nuestra siguiente tarea será hacer que el implicit anterior esté disponible en el ámbito donde lo queramos utilizar (una buena práctica suele ser realizar la definición de todos nuestros implicits en un singleton object llamado Preamble e importar dicho objeto al comienzo de nuestras clases):
import UserImplicits.stringToUsername
val user:User = "migue"
¿De verdad el código anterior compila? ¿Que está pasando? El proceso es "muy sencillo", el compilador detecta que hay un error de tipos puesto que no puede asignar una cadena a una variable de tipo User. Antes de declarar que existe un error ocasionado por una asignación de tipos incorrecta, el compilador busca si existe una conversión implicita disponible que le permita resolver el error encontrado. En este caso, el compilador buscará si existe un implicit que nos permita convertir una cadena de texto en un objeto de la clase User. Si revisamos la definición de nuestro primer implicit veremos que disponemos de una función que cumple los requisitos que el compilador está intentando satisfacer, por lo que este último, de manera interna, realizará algo similar a
val user:User = stringToUsername("migue")
El segundo ejemplo de uso de los
implicits que vamos a tratar son las conversiones de objetos sobre los que se realiza la llamada de un método. Nuestro objetivo será añadir nuevos métodos a una clase externa que podría estar definida en una librería third-party sobre la que no tenemos ningún tipo de control.
Imaginemos la siguiente definición de clase en una librería externa (por sencillez esta clase estará definida en el propio archivo de código fuente junto al resto de ejemplos)
class ExternalClass {
def f1() = {
"f1"
}
def f2() = {
"f2"
}
}
en la que nos gustaría incluir el método def
f3() = {"f3"}. Para llevar a cabo nuestra tarea construiremos una clase auxiliar en la que definiremos el método que deseamos incluir:
class MethodHelper {
def f3(): String = {
"f3"
}
}
e incorporamos un nuevo
implicit
implicit def newMethodsToExternalClass(externalClass:ExternalClass) : MethodHelper = {new MethodHelper }
A continuación podremos escribir un código similar al que sigue:
import UserImplictis.newMethodsToExternalClass
println("Method f3() is added via an implicit conversion: " + externalClass.f3())
¿Qué está ocurriendo en esta ocasión? Durante el proceso de compilación,
Scala detectará que existe un error puesto que el método
f3() no forma parte de la definición de la clase
ExternalClass. Antes de emitir un error, el compilador buscará una conversión implícita en la que se convierta el objecto receptor de la llamada del método f3() en un objeto que realmente posea dicho método. En nuestro ejemplo
newMethodsToExternalClass es el elemento que nos permite que el código anterior compile correctamente. ¿Que está ocurriendo internamente? El compilador estará emitiendo un código similar al siguiente:
newMethodsToExternallClass(externalClass).f3()
Como último ejemplo para esta entrada, revisaremos cómo podemos hacer uso de parámetros implícitos en las llamadas a funciones. Incorporemos el siguiente método a la definición de la clase
User
def encodePassword(password:String)(implicit encoder: Encoder) : String = {
encoder.encode(password)
}
Fijémonos que hemos marcado el segundo argumento de la función como
implicit por lo que el compilador intentará completar las llamadas a la función añadiendo los parámetros necesarios. Definamos un encoder de ejemplo (que no realiza ningún tipo de operación útil) y utilizemos
implicit object IdentityEncoder extends Encoder {
override def encode(password:String): String = password
}
import UserImplictis.IdentityEncoder
user2.encodePassword("foo")
Como podemos observar en el fragmento del código anterior en ningún momento estamos indicando el segundo parámetro de la función encodePassword sino que es el propio compilador el que está intentado incluir en la lista de parámetros alguno de los implictis disponibles de manera que la llamada de la función sea correcta.
Ha sido una entrada un poco larga pero he considerado que todos los ejemplos encajarían mucho mejor en una misma entrada de manera que pudieramos realizar un comparativa de los diferentes puntos de utilización de este poderoso mecanismo.
Importante: los snippets de código que aparecen a lo largo de esta entrada no están listos para ejecutarse en el REPL por lo que os aconsejo que, si quereis jugar con el código fuente, utilicéis aquel que está disponible
aquí (realizaré el merge de esta rama sobre master tan pronto como me sea posible).
Hasta pronto!
Migue