Je peux voir dans la documentation de l'API pour Predef qu'il s'agit de sous-classes d'un type de fonction générique (From) => To, mais c'est tout ce qu'il dit. Euh, quoi? Peut-être qu'il y a de la documentation quelque part, mais les moteurs de recherche ne gèrent pas très bien les "noms" comme "<: <", donc je n'ai pas pu le trouver.
Question de suivi: quand dois-je utiliser ces symboles/classes géniaux, et pourquoi?
Celles-ci sont appelées contraintes de type généralisé . Ils vous permettent, depuis une classe ou un trait paramétré par type, de contraindre davantage l'un de ses paramètres de type. Voici un exemple:
case class Foo[A](a:A) { // 'A' can be substituted with any type
// getStringLength can only be used if this is a Foo[String]
def getStringLength(implicit evidence: A =:= String) = a.length
}
L'argument implicite evidence
est fourni par le compilateur, ssi A
est String
. Vous pouvez le considérer comme une preuve que A
est String
- l'argument lui-même n'est pas important, sachant seulement qu'il existe. [edit: eh bien, techniquement, c'est réellement important car cela représente une conversion implicite de A
en String
, ce qui vous permet d'appeler a.length
et ne pas crier après le compilateur]
Maintenant, je peux l'utiliser comme ceci:
scala> Foo("blah").getStringLength
res6: Int = 4
Mais si j'ai essayé de l'utiliser avec un Foo
contenant autre chose qu'un String
:
scala> Foo(123).getStringLength
<console>:9: error: could not find implicit value for parameter evidence: =:=[Int,String]
Vous pouvez lire cette erreur comme "n'a pas pu trouver la preuve que Int == String" ... c'est comme ça! getStringLength
impose d'autres restrictions sur le type de A
que ce que Foo
requiert en général; à savoir, vous ne pouvez appeler getStringLength
que sur un Foo[String]
. Cette contrainte est appliquée au moment de la compilation, ce qui est cool!
<:<
et <%<
fonctionne de la même façon, mais avec de légères variations:
A =:= B
signifie que A doit être exactement BA <:< B
signifie que A doit être un sous-type de B (analogue à la contrainte de type simple<:
)A <%< B
signifie que A doit être visible comme B, éventuellement via une conversion implicite (analogue à la contrainte de type simple <%
)Cet extrait par @retronym est une bonne explication de la façon dont ce genre de choses était accompli et comment les contraintes de type généralisées le facilitent maintenant.
[~ # ~] addendum [~ # ~]
Pour répondre à votre question complémentaire, il est vrai que l'exemple que j'ai donné est assez artificiel et n'est évidemment pas utile. Mais imaginez l'utiliser pour définir quelque chose comme un List.sumInts
méthode, qui additionne une liste d'entiers. Vous ne voulez pas autoriser cette méthode à être invoquée sur un ancien List
, juste un List[Int]
. Cependant, le constructeur de type List
ne peut pas être ainsi contraint; vous voulez toujours pouvoir avoir des listes de chaînes, foos, barres et autres joyeusetés. Ainsi, en plaçant une contrainte de type généralisé sur sumInts
, vous pouvez vous assurer que juste cette méthode a une contrainte supplémentaire qui ne peut être utilisée que sur un List[Int]
. Essentiellement, vous écrivez du code spécial pour certains types de listes.
Pas une réponse complète (d'autres ont déjà répondu à cela), je voulais juste noter ce qui suit, ce qui peut peut-être aider à mieux comprendre la syntaxe: La façon dont vous utilisez normalement ces "opérateurs", comme par exemple dans l'exemple de pelotom:
def getStringLength(implicit evidence: A =:= String)
utilise l'alternative de Scala syntaxe d'infixe pour les opérateurs de type .
Alors, A =:= String
est le même que =:=[A, String]
(et =:=
est juste une classe ou un trait avec un nom d'apparence fantaisiste). Notez que cette syntaxe fonctionne également avec les classes "normales", par exemple vous pouvez écrire:
val a: Tuple2[Int, String] = (1, "one")
comme ça:
val a: Int Tuple2 String = (1, "one")
Elle est similaire aux deux syntaxes des appels de méthode, la "normale" avec .
et ()
et la syntaxe de l'opérateur.
Lisez les autres réponses pour comprendre quelles sont ces constructions. Voici quand vous devez les utiliser. Vous les utilisez lorsque vous devez contraindre une méthode pour des types spécifiques uniquement.
Voici un exemple. Supposons que vous souhaitiez définir une paire homogène, comme ceci:
class Pair[T](val first: T, val second: T)
Maintenant, vous voulez ajouter une méthode smaller
, comme ceci:
def smaller = if (first < second) first else second
Cela ne fonctionne que si T
est commandé. Vous pouvez restreindre la classe entière:
class Pair[T <: Ordered[T]](val first: T, val second: T)
Mais cela semble dommage - il pourrait y avoir des utilisations pour la classe lorsque T
n'est pas commandé. Avec une contrainte de type, vous pouvez toujours définir la méthode smaller
:
def smaller(implicit ev: T <:< Ordered[T]) = if (first < second) first else second
Il est correct d'instancier, disons, un Pair[File]
, tant que vous n'appelez pas smaller
dessus.
Dans le cas de Option
, les implémenteurs voulaient une méthode orNull
, même si cela n'a pas de sens pour Option[Int]
. En utilisant une contrainte de type, tout va bien. Vous pouvez utiliser orNull
sur un Option[String]
, Et vous pouvez former un Option[Int]
Et l'utiliser, tant que vous n'appelez pas orNull
dessus. Si vous essayez Some(42).orNull
, vous obtenez le charmant message
error: Cannot prove that Null <:< Int
Cela dépend de l'endroit où ils sont utilisés. Le plus souvent, lorsqu'ils sont utilisés lors de la déclaration de types de paramètres implicites, ce sont des classes. Ils peuvent aussi être des objets dans de rares cas. Enfin, ils peuvent être des opérateurs sur des objets Manifest
. Ils sont définis à l'intérieur de scala.Predef
dans les deux premiers cas, mais pas particulièrement bien documenté.
Ils sont destinés à fournir un moyen de tester la relation entre les classes, tout comme <:
et <%
do, dans les situations où ce dernier ne peut pas être utilisé.
Quant à la question "quand dois-je les utiliser?", La réponse est que vous ne devriez pas, à moins que vous sachiez que vous devriez. :-) [[# # ~] modifier [~ # ~]: Ok, ok, voici quelques exemples de la bibliothèque. Sur Either
, vous avez:
/**
* Joins an <code>Either</code> through <code>Right</code>.
*/
def joinRight[A1 >: A, B1 >: B, C](implicit ev: B1 <:< Either[A1, C]): Either[A1, C] = this match {
case Left(a) => Left(a)
case Right(b) => b
}
/**
* Joins an <code>Either</code> through <code>Left</code>.
*/
def joinLeft[A1 >: A, B1 >: B, C](implicit ev: A1 <:< Either[C, B1]): Either[C, B1] = this match {
case Left(a) => a
case Right(b) => Right(b)
}
Sur Option
, vous avez:
def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse null
Vous trouverez d'autres exemples sur les collections.
Dans Scala 2.13 ils ont été déplacés de Predef
: Déplacer <: <, =: =, DummyImplicits hors de Predef # 735
Une fonctionnalité de contraintes de type qui pourraient ne pas avoir été explicitées dans d'autres réponses est qu'elles peuvent être utilisées pour
... contraindre tout type abstrait
T
qui est de portée dans la liste d'arguments d'une méthode ( pas seulement les paramètres de type de la méthode )
Voici un exemple illustrant "pas seulement les paramètres de type de la méthode". Disons que nous avons
case class Foo[A, B](f: A => B) {
def bar[C <: A](x: C)(implicit e: B <:< String): B = f(x)
}
Foo[Int, String](x => x.toString).bar(1) // OK.
Foo[Int, Double](x => x.toDouble).bar(1) // error: Cannot prove that Double <:< String.
Notez comment nous sommes capables de contraindre le paramètre de type B
malgré le fait qu'il n'apparaît pas dans la clause de paramètre de type de bar
[C <: A]
. Si nous essayions plutôt de contraindre B
dans la clause de paramètre de type de bar
comme ceci
def bar[B <: String]
nous serions en train de masquer le paramètre de type B
de la portée englobante de Foo[A, B]
. Un exemple réel de cela de la bibliothèque serait toMap
:
trait IterableOnceOps[+A, +CC[_], +C] extends Any {
...
def toMap[K, V](implicit ev: A <:< (K, V)): immutable.Map[K, V] =
...
}