Où Scala cherche-t-il des implicites?

398

Une question implicite aux nouveaux arrivants à Scala semble être: où le compilateur recherche-t-il les implicits? Je veux dire implicite parce que la question ne semble jamais se former complètement, comme s'il n'y avait pas de mots pour cela. :-) Par exemple, d'où integralviennent les valeurs ci-dessous?

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

Une autre question qui fait suite à ceux qui décident d'apprendre la réponse à la première question est de savoir comment le compilateur choisit celui implicite à utiliser, dans certaines situations d'ambiguïté apparente (mais qui compile quand même)?

Par exemple, scala.Predefdéfinit deux conversions de String: l'une vers WrappedStringet l'autre vers StringOps. Cependant, les deux classes partagent beaucoup de méthodes, alors pourquoi Scala ne se plaint-elle pas d'ambiguïté quand, par exemple, appeler map?

Remarque: cette question a été inspirée par cette autre question , dans l'espoir de poser le problème de manière plus générale. L'exemple a été copié à partir de là, car il est mentionné dans la réponse.

Daniel C. Sobral
la source

Réponses:

554

Types d'implications

Implicits dans Scala fait référence à une valeur qui peut être transmise "automatiquement", pour ainsi dire, ou à une conversion d'un type à un autre qui est effectuée automatiquement.

Conversion implicite

En parlant brièvement de ce dernier type, si l' on appelle une méthode msur un objet od'une classe C, et que classe ne prend pas en charge la méthode m, alors Scala cherchera une conversion implicite de Cquelque chose qui fait support m. Un exemple simple serait la méthode mapsur String:

"abc".map(_.toInt)

Stringne prend pas en charge la méthode map, mais le StringOpsfait, et il y a une conversion implicite de Stringto StringOpsavailable (voir implicit def augmentStringsur Predef).

Paramètres implicites

L'autre type d'implicite est le paramètre implicite . Celles-ci sont passées aux appels de méthode comme tout autre paramètre, mais le compilateur essaie de les remplir automatiquement. Sinon, il se plaindra. On peut passer ces paramètres explicitement, c'est ainsi que l'on utilise breakOut, par exemple (voir question à propos breakOut, un jour où vous vous sentez prêt à relever un défi).

Dans ce cas, il faut déclarer la nécessité d'un implicite, comme la foodéclaration de méthode:

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

Afficher les limites

Il y a une situation où un implicite est à la fois une conversion implicite et un paramètre implicite. Par exemple:

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

getIndex("abc", 'a')

La méthode getIndexpeut recevoir n'importe quel objet, tant qu'une conversion implicite est disponible de sa classe vers Seq[T]. Pour cette raison, je peux passer un Stringà getIndex, et cela fonctionnera.

Dans les coulisses, le compilateur se transforme seq.IndexOf(value)en conv(seq).indexOf(value).

C'est tellement utile qu'il y a du sucre syntaxique pour les écrire. L'utilisation de ce sucre syntaxique getIndexpeut être définie comme ceci:

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

Ce sucre syntaxique est décrit comme une vue liée , semblable à une borne supérieure ( CC <: Seq[Int]) ou une borne inférieure ( T >: Null).

Limites du contexte

Un autre modèle courant dans les paramètres implicites est le modèle de classe de type . Ce modèle permet la fourniture d'interfaces communes aux classes qui ne les ont pas déclarées. Il peut à la fois servir de modèle de pont - séparant les préoccupations - et de modèle d'adaptateur.

La Integralclasse que vous avez mentionnée est un exemple classique de modèle de classe de type. Un autre exemple sur la bibliothèque standard de Scala est Ordering. Il existe une bibliothèque qui fait un usage intensif de ce modèle, appelée Scalaz.

Voici un exemple de son utilisation:

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

Il y a aussi du sucre syntaxique pour cela, appelé un contexte lié , qui est rendu moins utile par la nécessité de se référer à l'implicite. Une conversion directe de cette méthode ressemble à ceci:

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)(_ + _)
}

Les limites de contexte sont plus utiles lorsque vous avez juste besoin de les passer à d'autres méthodes qui les utilisent. Par exemple, la méthode sortedon a Seqbesoin d'un implicite Ordering. Pour créer une méthode reverseSort, on pourrait écrire:

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

Parce qu'il a Ordering[T]été implicitement transmis à reverseSort, il peut ensuite le transmettre implicitement à sorted.

D'où viennent les Implicits?

Lorsque le compilateur voit le besoin d'un implicite, soit parce que vous appelez une méthode qui n'existe pas sur la classe de l'objet, soit parce que vous appelez une méthode qui nécessite un paramètre implicite, il recherchera un implicite qui répondra au besoin .

Cette recherche obéit à certaines règles qui définissent les implications visibles et celles qui ne le sont pas. Le tableau suivant montrant où le compilateur recherchera les implicits est tiré d'une excellente présentation sur les implicits par Josh Suereth, que je recommande vivement à tous ceux qui souhaitent améliorer leurs connaissances Scala. Il a depuis été complété par des commentaires et des mises à jour.

Les implicits disponibles sous le numéro 1 ci-dessous ont priorité sur ceux sous le numéro 2. En dehors de cela, s'il existe plusieurs arguments éligibles qui correspondent au type du paramètre implicite, un argument plus spécifique sera choisi en utilisant les règles de résolution de surcharge statique (voir Scala Spécification §6.26.3). Des informations plus détaillées se trouvent dans une question à laquelle je renvoie à la fin de cette réponse.

  1. Premier aperçu dans la portée actuelle
    • Implicits définis dans le périmètre actuel
    • Importations explicites
    • importations de caractères génériques
    • Même portée dans d'autres fichiers
  2. Regardez maintenant les types associés dans
    • Objets compagnons d'un type
    • Portée implicite du type d'un argument (2.9.1)
    • Portée implicite des arguments de type (2.8.0)
    • Objets extérieurs pour les types imbriqués
    • Autres dimensions

Donnons-leur quelques exemples:

Implications définies dans la portée actuelle

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

Importations explicites

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

Importations génériques

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)(_ + _)
}

Même portée dans d'autres fichiers

Edit : Il semble que cela n'a pas une priorité différente. Si vous avez un exemple qui démontre une distinction de priorité, veuillez faire un commentaire. Sinon, ne comptez pas sur celui-ci.

C'est comme le premier exemple, mais en supposant que la définition implicite se trouve dans un fichier différent de son utilisation. Voir aussi comment les objets de package peuvent être utilisés pour introduire des implicits.

Objets compagnons d'un type

Il existe deux compagnons d'objet à noter ici. Tout d'abord, l'objet compagnon de type "source" est examiné. Par exemple, à l'intérieur de l'objet, Optionil y a une conversion implicite en Iterable, donc on peut appeler des Iterableméthodes Optionou passer Optionà quelque chose qui attend un Iterable. Par exemple:

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

Cette expression est traduite par le compilateur en

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

Cependant, List.flatMapattend un TraversableOnce, qui Optionne l'est pas. Le compilateur regarde ensuite à Optionl' intérieur du compagnon d'objet et trouve la conversion en Iterable, qui est un TraversableOnce, ce qui rend cette expression correcte.

Deuxièmement, l'objet compagnon du type attendu:

List(1, 2, 3).sorted

La méthode sortedprend un implicite Ordering. Dans ce cas, il regarde à l'intérieur de l'objet Ordering, compagnon de la classe Ordering, et y trouve un implicite Ordering[Int].

Notez que les objets compagnons des super classes sont également examinés. Par exemple:

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"

C'est ainsi que Scala a trouvé l'implicite Numeric[Int]et Numeric[Long]dans votre question, d'ailleurs, comme ils se trouvent à l'intérieur Numeric, non Integral.

Portée implicite du type d'un argument

Si vous avez une méthode avec un type d'argument A, la portée implicite de type Asera également prise en compte. Par "portée implicite", j'entends que toutes ces règles seront appliquées de manière récursive - par exemple, l'objet compagnon de Asera recherché pour les implicites, conformément à la règle ci-dessus.

Notez que cela ne signifie pas que la portée implicite de Asera recherchée pour les conversions de ce paramètre, mais de l'expression entière. Par exemple:

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)

Ceci est disponible depuis Scala 2.9.1.

Portée implicite des arguments de type

Cela est nécessaire pour que le modèle de classe de type fonctionne vraiment. Considérez Ordering, par exemple: il est livré avec des implicites dans son objet compagnon, mais vous ne pouvez pas y ajouter des éléments. Alors, comment pouvez-vous faire un Orderingpour votre propre classe qui est automatiquement trouvé?

Commençons par l'implémentation:

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)
    }
}

Alors, réfléchissez à ce qui se passe lorsque vous appelez

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

Comme nous l'avons vu, la méthode sortedattend un Ordering[A](en fait, elle attend un Ordering[B], où B >: A). Il n'y a rien de tel à l'intérieur Ordering, et il n'y a pas de type "source" sur lequel regarder. Évidemment, il le trouve à l'intérieur A, qui est un argument de type de Ordering.

C'est aussi ainsi que les différentes méthodes de collecte qui attendent de CanBuildFromfonctionner: les implicites se trouvent à l'intérieur des objets compagnons aux paramètres de type de CanBuildFrom.

Remarque : Orderingest défini comme trait Ordering[T], où Test un paramètre de type. Auparavant, je disais que Scala regardait à l'intérieur des paramètres de type, ce qui n'a pas beaucoup de sens. L'implicite recherché ci-dessus est Ordering[A], où Aest un type réel, pas un paramètre de type: c'est un argument de type pour Ordering. Voir la section 7.2 de la spécification Scala.

Ceci est disponible depuis Scala 2.8.0.

Objets externes pour les types imbriqués

Je n'en ai pas vu d'exemples. Je serais reconnaissant si quelqu'un pouvait en partager un. Le principe est 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"

Autres dimensions

Je suis presque sûr que c'était une blague, mais cette réponse n'est peut-être pas à jour. Ne considérez donc pas cette question comme l'arbitre final de ce qui se passe, et si vous remarquez qu'elle est obsolète, veuillez m'en informer afin que je puisse y remédier.

ÉDITER

Questions d'intérêt connexes:

Daniel C. Sobral
la source
60
Il est temps pour vous de commencer à utiliser vos réponses dans un livre, il ne s'agit maintenant que de tout rassembler.
pedrofurla
3
@pedrofurla J'ai été envisagé d'écrire un livre en portugais. Si quelqu'un peut me trouver un contact avec un éditeur technique ...
Daniel C. Sobral
2
Les objets de package des compagnons des parties du type sont également recherchés. lightsvn.epfl.ch/trac/scala/ticket/4427
retronym
1
Dans ce cas, cela fait partie de la portée implicite. Le site d'appel n'a pas besoin d'être dans ce package. Cela m'a surpris.
retronym
2
Oui, donc stackoverflow.com/questions/8623055 couvre cela spécifiquement, mais j'ai remarqué que vous avez écrit "La liste suivante est destinée à être présentée dans l'ordre de priorité ... veuillez signaler." Fondamentalement, les listes internes doivent être désordonnées car elles ont toutes le même poids (au moins en 2.10).
Eugene Yokota
23

Je voulais connaître la priorité de la résolution implicite des paramètres, pas seulement où elle se trouve, j'ai donc écrit un article de blog revisitant les implicits sans taxe à l'importation (et la priorité des paramètres implicites à nouveau après quelques commentaires).

Voici la liste:

  • 1) implique la portée de l'invocation actuelle via la déclaration locale, les importations, la portée externe, l'héritage, l'objet de package qui sont accessibles sans préfixe.
  • 2) portée implicite , qui contient toutes sortes d'objets compagnons et objet package qui ont une certaine relation avec le type implicite que nous recherchons (c'est-à-dire objet package du type, objet compagnon du type lui-même, de son constructeur de type le cas échéant, de le cas échéant, ses paramètres, ainsi que son supertype et ses supertraits).

Si à l'une ou l'autre étape, nous trouvons plus d'une règle de surcharge statique implicite pour la résoudre.

Eugene Yokota
la source
3
Cela pourrait être amélioré si vous écriviez du code définissant simplement des packages, des objets, des traits et des classes, et en utilisant leurs lettres lorsque vous vous référez à la portée. Pas besoin de mettre de déclaration de méthode - juste des noms et qui étend qui, et dans quelle portée.
Daniel C.Sobral