¿Dónde busca Scala implicits?




implicit-conversion (2)

Una pregunta implícita para los recién llegados a Scala parece ser: ¿dónde busca el compilador implicaciones? Quiero decir implícito porque la pregunta nunca parece estar completamente formada, como si no hubiera palabras para ello. :-) Por ejemplo, ¿de dónde vienen los valores para la integral continuación?

scala> import scala.math._
import scala.math._

scala> def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
foo: [T](t: T)(implicit integral: scala.math.Integral[T])Unit

scala> foo(0)
[email protected]

scala> foo(0L)
[email protected]

Otra pregunta que sigue a quienes deciden aprender la respuesta a la primera pregunta es ¿cómo elige el compilador qué implícito usar, en ciertas situaciones de ambigüedad aparente (pero que se compilan de todos modos)?

Por ejemplo, scala.Predef define dos conversiones de String : una a WrappedString y otra a StringOps . Sin embargo, ambas clases comparten muchos métodos, así que ¿por qué Scala no se queja de la ambigüedad cuando, por ejemplo, llama al map ?

Nota: esta pregunta se inspiró en esta otra pregunta , con la esperanza de plantear el problema de una manera más general. El ejemplo se copió desde allí, porque se menciona en la respuesta.


Tipos de implícitos

Los implícitos en Scala se refieren a un valor que se puede pasar "automáticamente", por así decirlo, o una conversión de un tipo a otro que se realiza automáticamente.

Conversión implícita

Hablando brevemente sobre el último tipo, si uno llama a un método m en un objeto o de una clase C , y esa clase no admite el método m , entonces Scala buscará una conversión implícita de C a algo que admita m . Un ejemplo simple sería el map del método en String :

"abc".map(_.toInt)

String no admite el map métodos, pero StringOps sí, y hay una conversión implícita de String a StringOps disponible (consulte la implicit def augmentString en implicit def augmentString ).

Parámetros implícitos

El otro tipo de implícito es el parámetro implícito. Estos se pasan a las llamadas de método como cualquier otro parámetro, pero el compilador intenta completarlas automáticamente. Si no puede, se quejará. Uno puede pasar estos parámetros explícitamente, que es cómo se usa breakOut , por ejemplo (vea la pregunta sobre breakOut , el día en que se siente con ganas de enfrentar un desafío).

En este caso, uno tiene que declarar la necesidad de una implícita, como la declaración del método foo :

def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}

Ver límites

Hay una situación donde un implícito es tanto una conversión implícita como un parámetro implícito. Por ejemplo:

def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)

getIndex("abc", 'a')

El método getIndex puede recibir cualquier objeto, siempre que haya una conversión implícita disponible de su clase a Seq[T] . Debido a eso, puedo pasar una String a getIndex , y funcionará.

Detrás de las escenas, el compilador cambia seq.IndexOf(value) a conv(seq).indexOf(value) .

Esto es tan útil que hay azúcar sintáctica para escribirlos. Usando este azúcar sintáctico, getIndex puede definirse así:

def getIndex[T, CC <% Seq[T]](seq: CC, value: T) = seq.indexOf(value)

Este azúcar sintáctico se describe como un límite de vista , similar a un límite superior ( CC <: Seq[Int] ) o un límite inferior ( T >: Null ).

Límites de contexto

Otro patrón común en los parámetros implícitos es el patrón de clase de tipo . Este patrón permite la provisión de interfaces comunes para las clases que no las declararon. Puede servir tanto como patrón de puente (obteniendo una separación de preocupaciones) como patrón de adaptador.

La clase Integral que mencionaste es un ejemplo clásico de patrón de clase de tipo. Otro ejemplo en la biblioteca estándar de Scala es Ordering . Hay una biblioteca que hace un uso intensivo de este patrón, llamado Scalaz.

Este es un ejemplo de su uso:

def sum[T](list: List[T])(implicit integral: Integral[T]): T = {
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

También hay azúcar sintáctica para eso, llamada un límite de contexto , que se hace menos útil por la necesidad de referirse a lo implícito. Una conversión directa de ese método se ve así:

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Los límites de contexto son más útiles cuando solo necesita pasarlos a otros métodos que los usan. Por ejemplo, el método sorted en Seq necesita un Ordering implícito. Para crear un método reverseSort , uno podría escribir:

def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse

Debido a que el Ordering[T] se pasó implícitamente a reverseSort , puede pasarlo implícitamente a sorted .

¿De dónde vienen los implícitos?

Cuando el compilador ve la necesidad de un implícito, ya sea porque está llamando a un método que no existe en la clase del objeto o porque está llamando a un método que requiere un parámetro implícito, buscará un implícito que se ajuste a la necesidad .

Esta búsqueda obedece a ciertas reglas que definen qué implicaciones son visibles y cuáles no. La siguiente tabla que muestra dónde buscará el compilador las implicaciones fue tomada de una excelente presentation sobre las implicaciones de Josh Suereth, que recomiendo sinceramente a cualquiera que desee mejorar su conocimiento de Scala. Se ha complementado desde entonces con comentarios y actualizaciones.

Los implícitos disponibles bajo el número 1 a continuación tienen prioridad sobre los del número 2. Aparte de eso, si hay varios argumentos elegibles que coinciden con el tipo de parámetro implícito, se elegirá uno más específico utilizando las reglas de resolución de sobrecarga estática (consulte Scala Especificación §6.26.3). Se puede encontrar información más detallada en una pregunta a la que me vinculo al final de esta respuesta.

  1. Primer vistazo en el ámbito actual.
    • Implícitos definidos en el alcance actual.
    • Importaciones explícitas
    • importaciones comodín
    • Mismo alcance en otros archivos
  2. Ahora mira los tipos asociados en
    • Objetos complementarios de un tipo
    • Alcance implícito del tipo de un argumento (2.9.1)
    • Ámbito implícito de los argumentos de tipo (2.8.0)
    • Objetos exteriores para tipos anidados.
    • Otras dimensiones

Vamos a dar algunos ejemplos para ellos:

Implícitos definidos en el alcance actual

implicit val n: Int = 5
def add(x: Int)(implicit y: Int) = x + y
add(5) // takes n from the current scope

Importaciones explícitas

import scala.collection.JavaConversions.mapAsScalaMap
def env = System.getenv() // Java map
val term = env("TERM")    // implicit conversion from Java Map to Scala Map

Importaciones de comodines

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Mismo alcance en otros archivos

Edit : Parece que esto no tiene una precedencia diferente. Si tiene algún ejemplo que demuestre una distinción de precedencia, haga un comentario. De lo contrario, no confíe en este.

Esto es como el primer ejemplo, pero suponiendo que la definición implícita está en un archivo diferente a su uso. Vea también cómo se pueden usar los objetos de paquete para traer implicaciones.

Objetos complementarios de un tipo

Hay dos compañeros objeto de la nota aquí. En primer lugar, se examina el objeto complementario del tipo "fuente". Por ejemplo, dentro de la Option objeto hay una conversión implícita a Iterable , por lo que uno puede llamar a los métodos de Iterable en la Option , o pasar la Option a algo que espera un Iterable . Por ejemplo:

for {
    x <- List(1, 2, 3)
    y <- Some('x')
} yield (x, y)

Esa expresión es traducida por el compilador a

List(1, 2, 3).flatMap(x => Some('x').map(y => (x, y)))

Sin embargo, List.flatMap espera un TraversableOnce , que no es la Option . Luego, el compilador busca en el compañero de objetos de Option y encuentra la conversión a Iterable , que es una TraversableOnce , lo que hace que esta expresión sea correcta.

Segundo, el objeto compañero del tipo esperado:

List(1, 2, 3).sorted

El método sorted toma un Ordering implícito. En este caso, mira dentro del objeto Ordering , que acompaña a la clase Ordering , y encuentra allí un Ordering[Int] implícito Ordering[Int] .

Tenga en cuenta que también se examinan los objetos complementarios de las súper clases. Por ejemplo:

class A(val n: Int)
object A { 
    implicit def str(a: A) = "A: %d" format a.n
}
class B(val x: Int, y: Int) extends A(y)
val b = new B(5, 2)
val s: String = b  // s == "A: 2"

Así es como Scala encontró el Numeric[Int] implícito Numeric[Int] y el Numeric[Long] en su pregunta, por cierto, tal como se encuentran dentro del Numeric , no del Integral .

Ámbito implícito del tipo de un argumento

Si tiene un método con un tipo de argumento A , también se considerará el alcance implícito del tipo A Por "alcance implícito" quiero decir que todas estas reglas se aplicarán recursivamente; por ejemplo, se buscarán implícitos en el objeto complementario de A , según la regla anterior.

Tenga en cuenta que esto no significa que se buscará en el alcance implícito de A conversiones de ese parámetro, sino de la expresión completa. Por ejemplo:

class A(val n: Int) {
  def +(other: A) = new A(n + other.n)
}
object A {
  implicit def fromInt(n: Int) = new A(n)
}

// This becomes possible:
1 + new A(1)
// because it is converted into this:
A.fromInt(1) + new A(1)

Esto está disponible desde Scala 2.9.1.

Ámbito implícito de los argumentos de tipo

Esto es necesario para que el patrón de clase de tipo realmente funcione. Considere Ordering , por ejemplo: viene con algunas implicaciones en su objeto complementario, pero no puede agregarle cosas. Entonces, ¿cómo puede hacer un Ordering para su propia clase que se encuentra automáticamente?

Comencemos con la implementación:

class A(val n: Int)
object A {
    implicit val ord = new Ordering[A] {
        def compare(x: A, y: A) = implicitly[Ordering[Int]].compare(x.n, y.n)
    }
}

Entonces, considera lo que pasa cuando llamas

List(new A(5), new A(2)).sorted

Como vimos, el método sorted espera un Ordering[A] (en realidad, espera un Ordering[B] , donde B >: A ). No existe tal cosa dentro del Ordering , y no hay un tipo de "fuente" en el que buscar. Obviamente, lo está encontrando dentro de A , que es un argumento de tipo de Ordering .

También es así como funcionan varios métodos de recopilación que esperan que CanBuildFrom funcione: las CanBuildFrom se encuentran dentro de los objetos complementarios a los parámetros de tipo de CanBuildFrom .

Nota : el Ordering se define como trait Ordering[T] , donde T es un parámetro de tipo. Anteriormente, dije que Scala buscaba parámetros de tipo interno, lo cual no tiene mucho sentido. El aspecto implícito que se mencionó anteriormente es Ordering[A] , donde A es un tipo real, no parámetro de tipo: es un argumento de tipo para Ordering . Consulte la sección 7.2 de la especificación Scala.

Esto está disponible desde Scala 2.8.0.

Objetos externos para tipos anidados

En realidad no he visto ejemplos de esto. Estaría agradecido si alguien pudiera compartir uno. El principio es simple:

class A(val n: Int) {
  class B(val m: Int) { require(m < n) }
}
object A {
  implicit def bToString(b: A#B) = "B: %d" format b.m
}
val a = new A(5)
val b = new a.B(3)
val s: String = b  // s == "B: 3"

Otras dimensiones

Estoy bastante seguro de que esto era una broma, pero esta respuesta podría no estar actualizada. Entonces, no tome esta pregunta como el árbitro final de lo que está sucediendo, y si se da cuenta de que se ha quedado obsoleto, avíseme para que pueda solucionarlo.

EDITAR

Preguntas de interés relacionadas:


Quería averiguar la prioridad de la resolución de parámetros implícita, no solo dónde se busca, así que escribí una entrada de blog revisando los implícitos sin impuesto de importación (y la prioridad de parámetros implícita nuevamente después de algunos comentarios).

Aquí está la lista:

  • 1) implicaciones visibles para el alcance de invocación actual a través de declaración local, importaciones, alcance externo, herencia, objeto de paquete que son accesibles sin prefijo.
  • 2) ámbito implícito , que contiene todo tipo de objetos complementarios y objetos de paquete que guardan alguna relación con el tipo implícito que buscamos (es decir, paquete objeto del tipo, objeto complementario del tipo mismo, de su constructor de tipo, si lo hay, de sus parámetros si los hay, y también de su supertipo y supertraits).

Si en cualquier etapa encontramos más de una regla de sobrecarga estática implícita, se utiliza para resolverla.





implicits