Ya sabemos que es implícitos ahora veamos un ejemplo más realista: dar formato a los precios.
Utilizar un String como idioma sirve para entender el mecanismo, pero no es una buena práctica para representar una configuración importante. Podríamos confundir el idioma con cualquier otra cadena de texto.
Vamos a crear un tipo específico.
case class Moneda(simbolo: String)
given monedaArgentina: Moneda = Moneda("$")
def mostrarPrecio(precio: Double)(using moneda: Moneda): String =
s"${moneda.simbolo}$precio"
¿Qué significa cada parte?
case class Moneda define un tipo de dato para representar una moneda.
simbolo es el símbolo que queremos mostrar.
given monedaArgentina declara una moneda disponible para el compilador.
using moneda: Moneda indica que la función necesita una moneda para trabajar.
Ahora podemos ejecutar:
mostrarPrecio(1500)
mostrarPrecio(2500)
El resultado será:
$1500.0
$2500.0
La función no necesita recibir la moneda en cada llamada. Scala la encuentra en el contexto.
Este ejemplo es deliberadamente sencillo: en una aplicación real, el formato de los precios también debería contemplar separadores decimales, miles y otras reglas regionales.
¿Qué ocurre si tenemos dos monedas?
Supongamos que nuestra aplicación necesita trabajar tanto con pesos argentinos como con euros.
Podríamos declarar:
given pesos: Moneda = Moneda("$")
given euros: Moneda = Moneda("€")
Ahora tenemos dos valores del mismo tipo.
Si llamamos a:
mostrarPrecio(1500)
el compilador no tiene una única moneda que pueda elegir. Ambas coinciden con el tipo que necesita la función.
En este caso, la resolución del parámetro contextual es ambigua y el programa no compila.
¿Cómo lo resolvemos?
Podemos indicar explícitamente cuál queremos utilizar:
mostrarPrecio(1500)(using pesos)
mostrarPrecio(1500)(using euros)
También podemos organizar el código para que cada contexto tenga disponible solamente la moneda que corresponde.
La lección es importante: given no significa que Scala elige cualquier valor. El compilador debe encontrar un valor adecuado y una resolución válida.
A veces queremos pedirle al compilador que encuentre un valor contextual, sin estar dentro de una función que tenga un parámetro using.
Para eso existe summon.
case class Moneda(simbolo: String)
given monedaArgentina: Moneda = Moneda("$")
val monedaActual: Moneda = summon[Moneda]
println(monedaActual.simbolo)
El resultado es: $
La expresión: summon[Moneda] significa, aproximadamente: «Compilador, encontrá un valor contextual de tipo Moneda».
Si no existe un valor apropiado, el programa no compila.
No hay que confundir summon con una búsqueda que ocurre en cualquier momento mientras el programa se ejecuta. La resolución de estos valores es principalmente una tarea del compilador.
Hasta ahora vimos cómo proporcionar una configuración, pero los parámetros contextuales también permiten construir abstracciones más generales.
Imaginemos que queremos imprimir distintos tipos de valores. Para algunos tipos sabemos cómo convertirlos en texto; para otros, todavía no.
Podemos definir una abstracción:
trait Mostrar[A]:
def mostrar(valor: A): String
Esta definición dice que Mostrar[A] representa una forma de mostrar valores de tipo A como texto.
Por ejemplo, podemos proporcionar una implementación para los enteros:
given mostrarEnteros: Mostrar[Int] with
def mostrar(valor: Int): String =
s"El número es $valor"
Y otra para las cadenas:
given mostrarTextos: Mostrar[String] with
def mostrar(valor: String): String =
s"El texto es '$valor'"
Ahora podemos definir una función genérica:
def imprimir[A](valor: A)(using m: Mostrar[A]): Unit =
println(m.mostrar(valor))
¿Qué sucede cuando llamamos a la función?
imprimir(42)
imprimir("Hola")
En el primer caso, Scala busca un Mostrar[Int].
En el segundo, busca un Mostrar[String].
Como hemos declarado ambas implementaciones mediante given, el compilador puede seleccionar la correspondiente al tipo del argumento.
El resultado es:
El número es 42
El texto es 'Hola'
Este ejemplo muestra una de las ventajas más importantes del mecanismo: podemos definir una función general sin conocer de antemano cómo se representa cada tipo.
Las implementaciones concretas se proporcionan por separado y el compilador las conecta.
El ejemplo anterior es una forma de implementar una type class.
Una type class permite definir una capacidad que puede tener un tipo sin necesidad de modificar la definición original de ese tipo.
Por ejemplo, podemos definir cómo mostrar un entero y cómo mostrar una cadena, aunque Int y String sean tipos que no hemos creado nosotros.
La estructura es:
1. Definimos una abstracción, como Mostrar[A].
2. Proporcionamos implementaciones para los tipos que nos interesan mediante given.
3. Escribimos funciones que solicitan esa capacidad mediante using.
4. El compilador encuentra la implementación apropiada.
Esta idea es muy utilizada en Scala para implementar operaciones genéricas, comparaciones, serialización, conversión de datos y muchas otras abstracciones.
Scala 3 introdujo una sintaxis más específica para implicitos :
- given declara instancias o valores contextuales.
- using declara parámetros contextuales.
- summon permite solicitar un valor contextual.
- Conversion permite definir conversiones entre tipos.
- extension permite agregar métodos de extensión a tipos existentes.
Los implícitos de Scala permiten que el compilador encuentre valores y comportamientos que una función necesita, sin que debamos pasarlos explícitamente en cada llamada.






