Inferencia implícita en Scala

Muchos desarrolladores principiantes y no tan de Scala toman los implícitos como una característica moderadamente útil. El uso generalmente se limita a pasar ExecutionContext



 a Future



. Otros evitan lo implícito y ven la oportunidad como dañina.





Un código como este asusta a mucha gente:





implicit def function(implicit argument: A): B
      
      



Pero creo que este mecanismo es una ventaja importante del lenguaje, veamos por qué.





Brevemente sobre implicitos

En general, implicits es un mecanismo para la finalización automática del código durante la compilación:





  • para argumentos implícitos, el valor se sustituye automáticamente





  • para las conversiones implícitas, el valor se envuelve automáticamente en una llamada al método





No voy a profundizar en este tema, a quienes les interesa ver este video del creador del lenguaje . En pocas palabras, este mecanismo se usa en varios casos diferentes (y en Scala 3 se dividirá en varias funciones separadas):





  1. Pasando contexto implícito (como ExecutionContext



    )





  2. Conversiones implícitas (obsoletas)





  3. Métodos de extensión (azúcar sintáctico para agregar métodos a tipos existentes)





  4. Clases de tipos y resolución implícita





Clases de tipos e inferencia implícita

Por sí mismas, las clases de tipos no aportan ninguna novedad o revolución: son simplemente la implementación de métodos "para" el tipo, y no "en" el tipo en sí, es como la diferencia entre Comparable



 & Ordering



( Comparator



 en Java):





Comparable



  :





class Person(age: Int) extends Comparable[Person] {
  override def compareTo(o: Person): Int = age compareTo o.age
}
def max[T <: Comparable[T]](xs: Iterable[T]): T = xs.reduce[T] {
  case (a, b) if (a compareTo b) < 0 => b
  case (a, _) => a
}
      
      



Ordering



  , :





case class Person(age: Int)

implicit object ByAgeOrdering extends Ordering[Person] {
  override def compare(o1: Person, o2: Person): Int = o1.age compareTo o2.age
}
def max[T: Ordering](xs: Iterable[T]): T = xs.reduce[T] {
  case (a, b) if Ordering[T].lt(a, b) => b
  case (a, _) => a
}
// is syntactic sugar for
def max[T](xs: Iterable[T])(implicit evidence: Ordering[T]): T = ...
      
      



.





. :





implicit val value: A = ???
implicit def definition: B = ???
implicit def conversion(argument: C): D = ???
implicit def function(implicit argument: E): F = ???
      
      



? conversion



, , : value



, definition



 & function



  . : val



 , def



  . – .





– , , .





– , – , :





implicit def pairOrder[A: Ordering, B: Ordering]: Ordering[(A, B)] = {
  case ((a1, b1), (a2, b2)) if Ordering[A].equiv(a1, a2) => Ordering[B].compare(b1, b2)
  case ((a1,  _), (a2,  _)) => Ordering[A].compare(a1, a2)
}
// again, just syntactic sugar for:
implicit def pairOrder[A, B](implicit a: Ordering[A], b: Ordering[B]): Ordering[(A, B)] = ...
      
      



, :





val values = Seq(
  (Person(30), ("A", "A")),
  (Person(30), ("A", "B")),
  (Person(20), ("A", "C"))
)
max(values) // => (Person(30),(A,B))
      
      



Seq[(Person, (String, String))]



  Ordering



  :





max(values)(
  pairOrder(
    ByAgeOrdering, 
    pairOrder(Ordering.String, Ordering.String)
  )
)
      
      



Entonces, la inferencia implícita le permite describir reglas de inferencia generales e instruir al compilador para que combine estas reglas y obtenga una implementación específica de la clase de tipo. Agregar su propio tipo o sus propias reglas no necesita describir todo desde el principio: el compilador combinará todo para obtener el objeto deseado.





Y lo más importante, si el compilador falla, recibirá un error de compilación, no un error de tiempo de ejecución, y podrá solucionar el problema de inmediato. Aunque, por supuesto, hay una mosca en la pomada en la pomada (si el compilador falla, no sabe qué eslabón de la cadena faltaba), no siempre es fácil depurar esto.





Ojalá lo implícito sea ahora un poco más explícito.








All Articles