¿Dónde busca Scala las implicidades?

Una pregunta implícita para los recién llegados a Scala parece ser: ¿dónde busca el comstackdor las implicaciones? Quiero decir implícito porque la pregunta nunca parece estar completamente formada, como si no hubiera palabras para eso. 🙂 Por ejemplo, ¿de dónde vienen los valores para 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) scala.math.Numeric$IntIsIntegral$@3dbea611 scala> foo(0L) scala.math.Numeric$LongIsIntegral$@48c610af 

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

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

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

    Tipos de Implicits

    Implicits en Scala se refiere 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 muy brevemente sobre este ú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 no sea compatible con m . Un ejemplo simple sería el map métodos en String :

     "abc".map(_.toInt) 

    String no es compatible con el map métodos, pero StringOps sí y existe una conversión implícita de String a StringOps disponible (consulte implicit def augmentString on implicit def augmentString ).

    Parámetros implícitos

    El otro tipo de implícito es el parámetro implícito. Estos se pasan a llamadas de método como cualquier otro parámetro, pero el comstackdor intenta llenarlos automáticamente. Si no puede, se quejará. Uno puede pasar estos parámetros de forma explícita, que es cómo uno usa breakOut , por ejemplo (ver pregunta sobre breakOut , en un día en que te sientes breakOut para un desafío).

    En este caso, se debe declarar la necesidad de una statement de método implícita, como foo :

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

    Ver límites

    Hay una situación en la que una implícita es una conversión implícita y 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] . Por eso, puedo pasar un String a getIndex , y funcionará.

    Detrás de escena, el comstackdor cambia seq.IndexOf(value) a conv(seq).indexOf(value) .

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

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

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

    Context Bounds

    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 como un patrón de puente (obteniendo la separación de las preocupaciones) y como un 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áctico para él, llamado 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 tiene que 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 Ordering[T] se pasó implícitamente a reverseSort , puede pasarlo implícitamente a sorted .

    ¿De dónde vienen los Implicits?

    Cuando el comstackdor 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é implicidades son visibles y cuáles no. La siguiente tabla que muestra dónde el comstackdor buscará implicits fue tomada de una excelente presentación sobre implicits por Josh Suereth, que recomiendo sinceramente a cualquiera que desee mejorar sus conocimientos de Scala. Se ha complementado desde entonces con comentarios y actualizaciones.

    Las implícitas disponibles bajo el número 1 a continuación tienen precedencia sobre las bajo el número 2. Además de eso, si hay varios argumentos elegibles que coinciden con el tipo de parámetro implícito, se elegirá el más específico usando las reglas de resolución de sobrecarga estática (ver Scala Especificación §6.26.3). Se puede encontrar información más detallada en una pregunta a la que me dirijo al final de esta respuesta.

    1. Primer vistazo en el scope actual
      • Implicits definidos en el scope actual
      • Importaciones explícitas
      • importación de comodines
      • Mismo scope en otros archivos
    2. Ahora mira los tipos asociados en
      • Objetos acompañantes de un tipo
      • Alcance implícito del tipo de un argumento (2.9.1)
      • Alcance implícito de los argumentos de tipo (2.8.0)
      • Objetos externos para tipos nesteds
      • Otras dimensiones

    Vamos a dar algunos ejemplos para ellos:

    Implicits definidos en el scope 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 scope en otros archivos

    Editar : parece que esto no tiene una precedencia diferente. Si tiene algún ejemplo que demuestre una distinción de precedencia, por favor 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 de su uso. Vea también cómo se pueden usar los objetos del paquete para traer implícitos.

    Objetos complementarios de un tipo

    Hay dos compañeros de objeto de la nota aquí. Primero, se busca el objeto compañero 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 Iterable en la Option , o pasar la Option a algo esperando un Iterable . Por ejemplo:

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

    Esa expresión es traducida por el comstackdor a

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

    Sin embargo, List.flatMap espera un TraversableOnce , cuya Option no es. A continuación, el comstackdor busca en el objeto complementario de Option y encuentra la conversión en Iterable , que es un TraversableOnce , haciendo 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 orden implícito. En este caso, mira dentro del objeto Ordering , compañero de la clase Ordering , y encuentra allí un Ordering[Int] implícito Ordering[Int] .

    Tenga en cuenta que también se investigan los objetos complementarios de las superclases. Por ejemplo:

     class A(val n: Int) object A { implicit def str(a: A) = "A: %d" format an } 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] y el Numeric[Long] implícitos en tu pregunta, por cierto, ya que se encuentran dentro de Numeric , no Integral .

    Alcance Implícito del Tipo de un Argumento

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

    Tenga en cuenta que esto no significa que se buscará en el scope implícito de A conversiones de ese parámetro, sino de toda la expresión. 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.

    Alcance implícito de 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 se 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(xn, yn) } } 

    Entonces, considere lo que sucede cuando llama

     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 hay tal cosa dentro de Ordering , y no hay un tipo de "fuente" en el que mirar. Obviamente, lo está encontrando dentro de A , que es un argumento tipo de Ordering .

    Esta es también la forma en que varios métodos de recolección esperan que CanBuildFrom funcione: las implícitas se encuentran dentro de objetos complementarios a los parámetros de tipo de CanBuildFrom .

    Nota : Ordering se define como trait Ordering[T] , donde T es un parámetro de tipo. Anteriormente, dije que Scala miraba dentro de los parámetros de tipo, lo cual no tiene mucho sentido. La búsqueda implícita anterior es Ordering[A] , donde A es un tipo real, no un parámetro de tipo: es un argumento de tipo para Ordering . Ver la sección 7.2 de la especificación de Scala.

    Esto está disponible desde Scala 2.8.0.

    Objetos externos para tipos nesteds

    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 bm } val a = new A(5) val b = new aB(3) val s: String = b // s == "B: 3" 

    Otras dimensiones

    Estoy bastante seguro de que esto fue una broma, pero esta respuesta podría no estar actualizada. Por lo tanto, no tome esta pregunta como el árbitro final de lo que está sucediendo, y si se da cuenta de que se ha desactualizado, infórmeme para que pueda solucionarlo.

    EDITAR

    Preguntas relacionadas de interés:

    • Contexto y ver límites
    • Encadenamiento de implicidades
    • Scala: precedencia de resolución implícita de parámetros

    Quería averiguar la precedencia de la resolución de parámetro implícita, no solo donde busca, así que escribí una publicación de blog revisitando implícitos sin impuesto a la importación (y precedencia de parámetro implícita nuevamente después de algunos comentarios).

    Aquí está la lista:

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

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