Table of Contents
Este artículo no va a ser una mirada en profundidad a los implícitos en Scala, sino una presentación sobre qué son. Va dirigido tanto a aquellos que estén empezando en Scala y necesiten un first-look a los implícitos como a los curiosos que pese a no usar Scala quieren sabe de qué trata esto de los «implícitos».
Va a tratar de una serie de artículos, siendo este el primero (y sirviendo de índice para los siguientes) y continuando con artículos más detallados y profundos sobre los implícitos, su funcionamiento y su uso en Scala.
Así que sin más dilación, vamos allá:
Qué significa implícito y explícito?
Estas dos palabras se usan muy a menudo cuando hablamos de implícitos en Scala (vaya, no me lo esperaba) y como casi todo en el mundo de la programación, son conceptos robados tomados prestados de otros aspectos de nuestras vidas.
Definición y ejemplo cotidiano. La definición de diccionario de ambas es la siguiente:
- implícito, ta Del lat. implicĭtus.
1.adj. Incluido en otra cosa sin que esta lo exprese.
- explícito, ta Del lat. explicĭtus.
1.adj. Que expresa clara y determinadamente una cosa.
Pero vamos a un ejemplo cotidiano. Cuando hablamos entre nosotros, no mencionamos explícitamente todo aquello de lo que hablamos, si no que hay muchas cosas que se entienden por contexto.
Si, por ejemplo, vamos a salir en moto y te pido que me pases el casco, me vas a dar mi casco, sin embargo yo no he dicho explícitamente que sea ese casco. Lo has entendido por contexto, que cuando te pidiese el casco me estuviese refiriendo al mío venía implícito.
De hecho es raro hablar nombrando de forma explicita todo aquello a lo que nos referimos(fuera de un contexto puramente técnico en el que se den instrucciones precisas).
Implícitos en Scala
Y en el código?
Vale, y qué tiene que ver esta chapa que me estás dando con la programación?
Pues resulta que Scala tiene un concepto que se acerca más de lo que nos creemos a este concepto del lenguaje que acabamos de explicar. Qué pasaría si no tuviésemos que pasar explícitamente los parámetros a una función? Si la función los entendiese por contexto? O si no tuviésemos que llamar a una función explícitamente si no que el compilador entendiese que por el contexto en el que estamos, queremos usarla? De esto trata, todo está en el contexto, y hay diferentes formas en las que Scala ha implementado el concepto de implícitos.
Qué tipos de implícitos hay en Scala?
implicit parameters.
Los métodos en Scala pueden recibir una última lista de parámetros, con el prefijo implicit.
def sendText(body: String)(implicit from: String): String = s"$body, from: $from"
Esta lista de parámetros puede ser llamada con normalidad si se quiere:
sendText("hola mundo")("Apiumhub")
//res3: String = hola mundo, from: Apiumhub
Pero su característica principal es que puedes definir un valor/funcion/definición implícita en tu código y si está en el mismo contexto… el compilador la utilizará!
implicit val sender: String = "Alice"
sendText("hola mundo")
//res4: String = hola mundo, from: Alice
implicit def sender: String = "Bob"
sendText("I love you")
//res0: String = I love you, from: Bob
Sin embargo si el compilador no encuentra ningún valor implícito con el tipo indicado… fallará:
sendText("hola")
//:13: error: could not find implicit value for parameter from: String
// sendText("hola")
// ^
Esto permite desde eliminar duplicación de código en las llamadas a distintos métodos que requieran el mismo parámetro a inyectar colaboradores a componentes (Dependency Injection).
Implicit conversions (implicit functions)
Qué ocurre cuando un método requiere un tipo A y tu quieres pasarle un valor del tipo B? Qué ocurre cuando un método retorna un tipo C y tu quieres un tipo D? La respuestas a ambas preguntas es la misma: O le pides a alguien que cambie la firma y la implementación… o bien te picas una función A=>B para arreglarte el problema. El «problema» es que tendrás que aplicar esta función en todos los sitios donde lo requieras, lo que implica cierta duplicación de código, Es más, tenemos que modificar nuestro código para añadir esta transformación. Y si estamos usando genéricos? No podríamos saber qué conversor utilizar a priori… quizás usando pattern matching…
object Playground {
def createNumber: Int = scala.util.Random.nextInt
val myNumber: String = createNumber
}
Este es uno de esos casos, el compilador se queja de createNumber pues devuelve Int y no String. Vamos a crear una conversión implícita para que haga la transformación automáticamente:
import scala.language.implicitConversions
object Playground {
implicit def Int2String(number: Int): String = number.toString()
def createNumber: Int = scala.util.Random.nextInt
val myNumber: String = createNumber
val text: String = 123
}
La potencia que tiene esta herramienta tiene pocos límites y tiene algunos usos tan prácticos como definir la transformación de un DTO a dominio (o a la inversa) en una conversión implícita y olvídate de tener que aplicar ningún tipo de transformación nunca, ya que se hará automáticamente.
Implicit classes
Los parámetros y las transformaciones implícitas son los más conocidos, pero hay más tipos, como las clases implícitas.
object Playground {
implicit class MyString(x: String) {
def mirror: String = x.reverse
def addThings: String = x+"abcdefg"
}
}
Qué creéis que hace este trozo de código? La respuesta es fácil (y muchos diréis, aaaah vaalee) extension methods
import Playground.MyString
"123456".mirror
//res1: String = 654321
"123456".addThings
//res2: String = 123456abcdefg
Como todos los implícitos tiene sus limitaciones, pero también mucha utilidad: Cómo añadiríais comportamiento adicional a una clase, que podría o no ser vuestra? Composición o herencia, no? Y si es final y no puedes extenderla? Solo te queda composición, y eso puede significar hacer un wrapper, etc.
final class Author(private val name: String) {
def sayname: String = name
}
object Playground {
implicit class MyAuthor(x: Author) {
def shoutname: String = x.sayname+"!!!"
}
}
import Playground.MyAuthor
val author:Author = new Author("Jack")
author.shoutname
//res7: String = Jack!!!
De esta forma podemos trabajar con Author de forma natural.
Implicit objects
Esta es la forma de implícito más usada y a la vez la menos usada por si misma. Permite crear y usar type classes y estas se usan extendidamente tanto en la stdlib como en librerías, sin embargo su uso fuera de type classes es prácticamente inexistente. Un implicit object que el compilador puede entregar cuando se solicite un parámetro implícito del mismo tipo que dicho object. Es como un implicit parameter para objetos, para que nos entendamos.
object Playground {
trait MyTrait {
val number: Int
def transform(number:Int): String
}
implicit object MyObject extends MyTrait {
val number: Int = 1
def transform(number:Int): String = number.toString
}
def doSomething(implicit theTrait: MyTrait) = theTrait.transform(theTrait.number)
}
import Playground._
doSomething
//res1: String = 1
Pensaréis, pues no le veo mucha utilidad, así por si mismo. Puede ser, o puede ser que no le echéis suficiente imaginación. Más adelante veréis que se puede llegar a hacer con implicit objects.
Qué problemas tienen?
Oscuridad de código
Los implícitos son una herramienta muy potente y al descubrirlos puede llevarnos a abusar de ellos. Y eso no es bueno, créeme, lo he vivido. Un código en el que se abusa de los implícitos es de las cosas más difíciles de entender, seguir y debugar que te puedes encontrar. Todo es magia (magia negra en muchos casos), las cosas ocurren y a simple vista no tienes control de por qué. Es posible que te dejes un implícito por declarar y que todo compile, pues alguien haya declarado un valor implícito en ese scope y los tipos coincidan, y créeme, debugar eso es criminal.
ambiguous implicit definition
Los implícitos se buscan basándose en el tipo requerido, eso significa que no podemos tener dos implícitos con el mismo tipo compartiendo scope, el compilador no sabría qué hacer!
object Playground {
implicit val anumber: Int = 1
def plus1(implicit number: Int): Int = number+1
}
import Playground._
implicit val myNumber: Int = 0
plus1
//:1: error: ambiguous implicit values:
// both value anumber in object Playground of type => Int
// and value myNumber of type => Int
// match expected type Int
// plus1
// ^
Es decir, aunque los quieras usar para cosas distintas, si tienen el mismo tipo, no puedes tener dos implícitos compartiendo scope. Lo que nos lleva a una conclusión: ojo con la Primitive Obsesion. Si tipamos de forma específica, con tipos concretos y no con primitivos, podemos evitar este tipo de problemas.
Final Words
En resumen, eliminamos mucho, mucho boilerplate con los implícitos, escribimos mucho menos código y resolvemos todo en compilación, sin embargo no es oro todo lo que reluce y esto conlleva tener mucha magia en el código. Código en ocasiones imposible de entender (Tanto que los autores de Kotlin han tomado la decisión específica de no implementarlos en el lenguaje) .
Podríamos decir: «hay que usarlos con moderación», pero para nosotros, la cuestión no es tanto moderación o no, sino los criterios y los patrones de cómo y cuándo usarlos, ya que esta es una decisión arquitectural.
Al final de esta serie, mencionaremos algunos ejemplos de criterios.
Lecturas recomendadas sobre implícitos
Si queréis lectura ligera sobre implícitos, podéis leer el siguiente capítulo en el que hablaremos sobre patrones con implícitos, cómo los busca el compilador y ejemplos encontrados en la stdlib y en librerías como scalaz, o podéis leer estas recomendaciones:
- Scala in action: capítulo 8.3
- Scala by example: capítulo 15
- Solo para valientes: https://www.artima.com/pins1ed/implicit-conversions-and-parameters.html
Suscríbete a nuestro newsletter para estar al día de desarrollo backend e implícitos en Scala en concreto!
Si este artículo sobre implícitos en Scala te gustó, te puede interesar:
Simular respuestas del servidor con Nodejs
Principio de responsabilidad única
Arquitectura de microservicios
F-bound en Scala: traits genéricos con higher-kinded types
Scala Generics I : Clases genéricas y Type bounds
Author
-
Software developer with over 8 years experience working with different code languages, able to work as a FullStack Developer, with the best skills in the backend side. Passionate about new technologies, and the best software development practices involved in a DevOps culture. Enjoying mentoring teams and junior developers and I feel very comfortable with the stakeholders management-wise.
Ver todas las entradas