Trabajar con type class en Scala significa trabajar con valores implícitos y parámetros implícitos. Hay algunas reglas que necesitamos saber para hacer esto de manera efectiva.
Implícitos de empaquetado : En una peculiaridad curiosa del lenguaje, cualquier definición marcada como implícita en Scala debe colocarse dentro de un objeto o trait en lugar de en el nivel superior. Colocar instancias en un objeto complementario a la clase de tipo tiene un significado especial en Scala porque juega con algo llamado alcance implícito.
Ámbito implícito : El compilador busca instancias de clase de tipo candidato por tipo. Los lugares donde el compilador busca instancias candidatas se conocen como ámbito implícito. El alcance implícito se aplica en el sitio de la llamada; ese es el punto donde llamamos a un método con un parámetro implícito. El alcance implícito que consta aproximadamente de:
• definiciones locales o heredadas;
• definiciones importadas;
• definiciones en el objeto complementario de la clase de tipo o el tipo de parámetro (en este caso, JsonWriter o String).
Las definiciones solo se incluyen en el ámbito implícito si están etiquetadas con la palabra clave implícita. Además, si el compilador ve múltiples definiciones candidatas, falla con un error de valores implícitos ambiguos:
implicit val writer1: JsonWriter[String] = JsonWriterInstances.stringWriter
implicit val writer2: JsonWriter[String] = JsonWriterInstances.stringWriter
Json.toJson("A string")
// error: ambiguous implicit values:
// both value writer1 in object App0 of type => repl.Session.App0. JsonWriter[String]
// and value writer2 in object App0 of type => repl.Session.App0. JsonWriter[String]
// match expected type repl.Session.App0.JsonWriter[String]
// Json.toJson("A string")
// ^^^^^^^^^^^^^^^^^^^^^^^
Las reglas precisas de la resolución implícita son más complejas que esto, pero la complejidad es en gran medida irrelevante para el uso diario. Para nuestros propósitos, podemos empaquetar instancias de clases de tipo en aproximadamente cuatro formas:
1. colocándolos en un objeto;
2. colocándolos en un trait;
3. colocándolos en el objeto compañero de la clase de tipos;
4. colocándolos en el objeto complementario del tipo de parámetro.
Con la opción 1 traemos las instancias al alcance importándolas. Con la opción 2 los traemos al alcance con la herencia. Con las opciones 3 y 4, las instancias siempre están en un alcance implícito, independientemente de dónde intentemos usarlas.
Es convencional colocar instancias de clase de tipo en un objeto complementario (opción 3 y 4 anteriores) si solo hay una implementación sensata, o al menos una implementación ampliamente aceptada como predeterminada. Esto hace que las instancias de clase de tipo sean más fáciles de usar, ya que no se requiere importarlas para incluirlas en el ámbito implícito.