¿Dónde busca Scala implicits?

398

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

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 aquellos que deciden aprender la respuesta a la primera pregunta es ¿cómo elige el compilador cuál implícito usar, en ciertas situaciones de aparente ambigüedad (pero que compila de todos modos)?

Por ejemplo, scala.Predefdefine dos conversiones de String: una a WrappedStringy otra a StringOps. Sin embargo, ambas clases comparten muchos métodos, entonces, ¿por qué Scala no se queja de la ambigüedad cuando, por ejemplo, llama 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.

Daniel C. Sobral
fuente

Respuestas:

554

Tipos de implicidades

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 el último tipo, si uno llama a un método men un objeto ode una clase C, y esa clase no admite el método m, entonces Scala buscará una conversión implícita de Calgo que sí lo admite m. Un ejemplo simple sería el método mapen String:

"abc".map(_.toInt)

Stringno soporta el método map, pero StringOpslo hace, y hay una conversión implícita de Stringque StringOpsdispone (véase implicit def augmentStringel Predef).

Parámetros implícitos

El otro tipo de implícito es el parámetro implícito . Estos se pasan a las llamadas a métodos como cualquier otro parámetro, pero el compilador intenta completarlos 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, en un día en que se siente preparado para un desafío).

En este caso, uno tiene que declarar la necesidad de un implícito, como la foodeclaración del método:

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

Ver límites

Hay una situación en la que 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 getIndexpuede recibir cualquier objeto, siempre que haya una conversión implícita disponible de su clase a Seq[T]. Por eso, puedo pasar un Stringa getIndex, y funcionará.

Detrás de escena, el compilador 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, getIndexse puede definir 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 a clases que no las declararon. Puede servir como un patrón de puente, obteniendo una separación de las preocupaciones, y como un patrón adaptador.

La Integralclase que mencionó 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 ello, llamado un contexto vinculado , 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 sorteden Seqnecesita una implícita Ordering. Para crear un método reverseSort, se 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 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 ciertas reglas que definen qué implicaciones son visibles y cuáles no. La siguiente tabla que muestra dónde buscará implicidades el compilador se tomó de una excelente presentación sobre las implicidades de Josh Suereth, que recomiendo de todo corazón a cualquiera que quiera mejorar sus conocimientos de Scala. Se ha complementado desde entonces con comentarios y actualizaciones.

Las implicaciones disponibles bajo el número 1 a continuación tienen prioridad sobre las que están bajo el 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 (ver Scala Especificación §6.26.3). Se puede encontrar información más detallada en una pregunta que enlace al final de esta respuesta.

  1. Primer vistazo en el alcance actual
    • Implicits definidos en el alcance actual
    • Importaciones explícitas
    • importaciones de comodines
    • Mismo alcance en otros archivos
  2. Ahora mire los tipos asociados en
    • Objetos complementarios de un tipo
    • Alcance implícito del tipo de argumento (2.9.1)
    • Alcance implícito de los argumentos de tipo (2.8.0)
    • Objetos externos para tipos anidados
    • Otras dimensiones

Demos algunos ejemplos para ellos:

Implicitos 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

Editar : 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íes 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 generar implicidades.

Objetos complementarios de un tipo

Hay dos compañeros de objeto de nota aquí. Primero, se analiza el objeto compañero del tipo "fuente". Por ejemplo, dentro del objeto Optionhay una conversión implícita a Iterable, por lo que uno puede invocar Iterablemétodos Optiono pasar Optiona algo que espera un Iterable. Por ejemplo:

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

El compilador traduce esa expresión a

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

Sin embargo, List.flatMapespera un TraversableOnce, que Optionno lo es. El compilador luego mira dentro Optiondel objeto compañero y encuentra la conversión a Iterable, que es a TraversableOnce, haciendo correcta esta expresión.

En segundo lugar, el objeto complementario del tipo esperado:

List(1, 2, 3).sorted

El método sortedtiene un implícito Ordering. En este caso, mira dentro del objeto Ordering, compañero de la clase Ordering, y encuentra un implícito Ordering[Int]allí.

Tenga en cuenta que también se examinan los objetos complementarios de las superclases. 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ó lo implícito Numeric[Int]y Numeric[Long]en su pregunta, por cierto, ya que se encuentran dentro Numeric, no Integral.

Alcance implícito del tipo de argumento

Si tiene un método con un tipo de argumento A, Atambién se considerará el alcance implícito del tipo . Por "alcance implícito" quiero decir que todas estas reglas se aplicarán de forma recursiva; por ejemplo, Ase buscará implicidades en el objeto complementario de acuerdo con la regla anterior.

Tenga en cuenta que esto no significa Aque se buscará en el alcance implícito de las 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 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 implicidades en su objeto complementario, pero no puede agregarle cosas. Entonces, ¿cómo puede hacer una Orderingpara 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 sucede cuando llamas

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

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

Así es también como varios métodos de recolección esperan CanBuildFromtrabajo: las implicaciones se encuentran dentro de los objetos complementarios a los parámetros de tipo CanBuildFrom.

Nota : Orderingse define como trait Ordering[T], donde Tes un parámetro de tipo. Anteriormente, dije que Scala miró dentro de los parámetros de tipo, lo que no tiene mucho sentido. El implícito buscado anteriormente es Ordering[A], donde Aes un tipo real, no un parámetro de tipo: es un argumento de tipo para Ordering. Consulte la sección 7.2 de la especificación Scala.

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 fue una broma, pero es posible que esta respuesta no esté actualizada. Por lo tanto, no tome esta pregunta como el árbitro final de lo que está sucediendo, y si nota que se ha desactualizado, infórmeme para que pueda solucionarlo.

EDITAR

Preguntas relacionadas de interés:

Daniel C. Sobral
fuente
6060
Es hora de que comiences a usar tus respuestas en un libro, por ahora solo es cuestión de armarlo todo.
pedrofurla
3
@pedrofurla Se me ha considerado escribir un libro en portugués. Si alguien puede encontrarme un contacto con un editor técnico ...
Daniel C. Sobral
2
También se buscan los objetos de paquete de los compañeros de las partes del tipo. lampsvn.epfl.ch/trac/scala/ticket/4427
retrónimo el
1
En este caso, es parte del alcance implícito. El sitio de la llamada no necesita estar dentro de ese paquete. Eso fue sorprendente para mí.
retronym
2
Sí, entonces stackoverflow.com/questions/8623055 cubre eso específicamente, pero noté que escribió "La siguiente lista está destinada a ser presentada en orden de precedencia ... por favor informe". Básicamente, las listas internas deben estar desordenadas, ya que todas tienen el mismo peso (al menos en 2.10).
Eugene Yokota
23

Quería averiguar la precedencia de la resolución implícita de parámetros, no solo dónde busca, así que escribí una publicación de blog revisando implicidades sin impuestos de importación (y la precedencia implícita de parámetros nuevamente después de algunos comentarios).

Aquí está la lista:

  • 1) implicits visibles para el alcance de invocación actual a través de declaración local, importaciones, alcance externo, herencia, objeto de paquete al que se puede acceder sin prefijo.
  • 2) alcance implícito , que contiene todo tipo de objetos complementarios y objetos de paquete que guardan alguna relación con el tipo de implícito que buscamos (es decir, objeto de paquete del tipo, objeto complementario del tipo en sí, de su constructor de tipos, 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.

Eugene Yokota
fuente
3
Esto podría mejorarse si escribiera algún código simplemente definiendo paquetes, objetos, rasgos y clases, y usando sus letras cuando se refiera al alcance. No es necesario poner ninguna declaración de método, solo nombres y quién extiende a quién y en qué ámbito.
Daniel C. Sobral