web-dev-qa-db-fra.com

Où Scala cherche-t-il des implicites?

Une question implicite aux nouveaux arrivants sur Scala semble être la suivante: où le compilateur recherche-t-il les implicites? Je veux dire implicite parce que la question ne semble jamais être complètement formée, comme s’il n’y avait pas de mots pour le dire. :-) Par exemple, d'où proviennent les valeurs pour integral 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 la suivante: comment le compilateur choisit-il l’implicite à utiliser, dans certaines situations d’ambiguïté apparente (mais qui compile quand même)?

Par exemple, scala.Predef Définit deux conversions de String: une en WrappedString et une autre en StringOps. Cependant, les deux classes partagent beaucoup de méthodes, alors pourquoi Scala ne se plaint-il pas de l'ambiguïté quand, par exemple, en appelant map?

Remarque: cette question a été inspirée par cette autre question , dans l'espoir d'exposer 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.

387
Daniel C. Sobral

Types d'implications

Implique in Scala fait référence à une valeur pouvant être transmise "automatiquement", pour ainsi dire, ou à une conversion d'un type à un autre effectuée automatiquement.

Conversion implicite

Parlant très brièvement de ce dernier type, si on appelle une méthode m sur un objet o d’une classe C, et que cette classe ne supporte pas la méthode m, alors Scala cherchera une conversion implicite de C en quelque chose que fait prend en charge m. Un exemple simple serait la méthode map sur String:

"abc".map(_.toInt)

String ne supporte pas la méthode map, mais StringOps, et il existe une conversion implicite de String en StringOps disponible (voir implicit def augmentString Sur Predef).

Paramètres implicites

L'autre type d'implicite est le paramètre implicite . Ceux-ci sont passés aux appels de méthodes comme n'importe quel autre paramètre, mais le compilateur essaie de les remplir automatiquement. S'il ne peut pas, il va se plaindre. Un peut transmettre explicitement ces paramètres, ce qui correspond à l'utilisation de breakOut, par exemple (voir la question à propos de breakOut, un jour que vous ressentez. pour un défi).

Dans ce cas, il faut déclarer la nécessité d'une implicite, telle que la déclaration de méthode foo:

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

Afficher les limites

Il existe une situation où une 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 getIndex peut recevoir n'importe quel objet, à condition qu'une conversion implicite soit disponible dans sa classe en Seq[T]. A cause de cela, je peux passer un String à getIndex, et cela fonctionnera.

En coulisse, le compilateur change seq.IndexOf(value) en conv(seq).indexOf(value).

C'est tellement utile qu'il y a du sucre syntaxique pour les écrire. En utilisant ce sucre syntaxique, getIndex peut être défini comme suit:

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 un borne inférieure (T >: Null).

Limites de contexte

Un autre modèle courant dans les paramètres implicites est le modèle de classe de types . Ce modèle permet de fournir des interfaces communes aux classes qui ne les ont pas déclarées. Il peut à la fois servir de modèle de pont - permettant de séparer les problèmes entre eux - et de modèle d’adaptateur.

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

Ceci est 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 existe également un sucre syntaxique, 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 devez simplement les transmettre à d'autres méthodes qui les utilisent. Par exemple, la méthode sorted sur Seq nécessite un _ implicite Ordering. Pour créer une méthode reverseSort, on pourrait écrire:

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

Parce que Ordering[T] A été implicitement passé à reverseSort, il peut alors le transmettre implicitement à sorted.

D'où viennent les implicites?

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

Cette recherche obéit à certaines règles qui définissent quelles sont les implications qui sont visibles et celles qui ne le sont pas. Le tableau suivant indiquant où le compilateur va rechercher les implicites provient d’un excellent présentation concernant les implicites de Josh Suereth, que je recommande vivement à tous ceux qui souhaitent améliorer leur Scala connaissances, complétées depuis par des commentaires et des mises à jour.

Les implicites 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 à l'aide des règles de résolution de la surcharge statique (voir = Scala Spécification §6.26.3). Des informations plus détaillées peuvent être trouvées dans une question que je vous renvoie à la fin de cette réponse.

  1. Premier coup d'oeil dans le champ actuel
    • Implications définies dans la portée actuelle
    • Importations explicites
    • importations génériques
    • Même portée dans d'autres fichiers
  2. Maintenant, regardons 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 externes pour les types imbriqués
    • Autres dimensions

Donnons quelques exemples pour eux:

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 la priorité ne soit pas différente. Si vous avez des exemples qui démontrent une distinction de priorité, veuillez faire un commentaire. Sinon, ne comptez pas sur celui-ci.

Cela ressemble au premier exemple, mais en supposant que la définition implicite se trouve dans un fichier différent de son utilisation. Voir aussi comment objets de package peut être utilisé pour impliquer.

Objets compagnon d'un type

Il y a deux compagnons d'objets notables ici. Tout d'abord, l'objet compagnon du type "source" est examiné. Par exemple, à l'intérieur de l'objet Option, il y a une conversion implicite en Iterable. Vous pouvez donc appeler les méthodes Iterable sur Option ou 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.flatMap S'attend à un TraversableOnce, ce que Option n'est pas. Le compilateur regarde ensuite à l'intérieur de l'objet compagnon de Option et trouve la conversion en Iterable, qui est un TraversableOnce, corrigeant ainsi cette expression.

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

List(1, 2, 3).sorted

La méthode sorted prend un _ implicite Ordering. Dans ce cas, il cherche à l'intérieur de l'objet Ordering, associé à la classe Ordering, et y trouve un Ordering[Int] Implicite.

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é les noms implicite Numeric[Int] Et Numeric[Long] Dans votre question, comme ils se trouvent dans Numeric, Integral.

Portée implicite du type d'argument

Si vous avez une méthode avec un type d'argument A, alors la portée implicite de type A sera également considérée. Par "portée implicite", je veux dire que toutes ces règles seront appliquées de manière récursive - par exemple, l'objet compagnon de A fera l'objet d'une recherche implicite, conformément à la règle ci-dessus.

Notez que cela ne signifie pas que la portée implicite de A sera 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. Prenons Ordering, par exemple: il contient certaines implications dans son objet compagnon, mais vous ne pouvez pas y ajouter de contenu. Alors, comment pouvez-vous créer un Ordering pour votre propre classe qui se trouve automatiquement?

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, considérez ce qui se passe lorsque vous appelez

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

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

C’est aussi le cas de diverses méthodes de collecte qui attendent CanBuildFrom: les implications se trouvent dans les objets compagnons des paramètres de type de CanBuildFrom.

Remarque : Ordering est défini comme trait Ordering[T], Où T est 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ée ci-dessus est Ordering[A], Où A est un type réel, pas paramètre de type: il s'agit d'un argument de type à 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'ai pas réellement vu d'exemples de cela. Je serais reconnaissant si quelqu'un pouvait 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 à peu près sûr que c'était une blague, mais cette réponse pourrait ne pas être à jour. Donc, ne prenez pas cette question comme l'arbitre final de ce qui se passe, et si vous remarquez qu'elle est périmée, veuillez m'en informer afin que je puisse résoudre le problème.

[~ # ~] éditer [~ # ~]

Questions connexes d'intérêt:

545
Daniel C. Sobral

Je voulais connaître la priorité de la résolution du paramètre implicite, et pas seulement où elle le cherchait. J'ai donc écrit un article de blog revisiter les implications sans taxe à l'importation (et priorité implicite des paramètres à nouvea = = après quelques commentaires).

Voici la liste:

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

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

23
Eugene Yokota