Pourquoi Scala infère le type inférieur lorsque le paramètre de type n'est pas spécifié?
Je me demande si quelqu'un pourrait expliquer la règle d'inférence dans ce cas particulier ci-dessous, et surtout c'est rationnel / implication?
case class E[A, B](a: A) // class E
E(2) // E[Int,Nothing] = E(2)
Notez que j'aurais pu écrire E[Int](2)
. Ce qui m'importe, c'est pourquoi le deuxième type de paramètre est-il supposé être Nothing
(c'est-à-dire le type Bottom) au lieu de dire Any
par exemple? Pourquoi est-ce et quelle est la raison / l'implication?
Juste pour donner un peu de contexte, cela est lié à la définition de l'un ou l'autre et de son fonctionnement pour la gauche et la droite. Les deux sont définis selon le modèle
final case class X[+A, +B](value: A) extends Either[A, B]
Où vous l'instanciez laissez dire comme Right[Int](2)
et le type déduit est Right[Nothing, Int]
et par extensionEither[Nothing, Int]
EDIT1
Il y a de la cohérence ici, mais je peux toujours comprendre le rationnel. Voici la même définition avec un paramètre contra-variant:
case class E[A, -B](a: A)// class E
E(2) // E[Int, Any] = E(2)
Par conséquent, nous avons la même chose à l'inverse quand elle est contre-variante, et cela rend cohérente la règle de tout comportement ou d'inférence. Cependant, le rationnel pour cela, je ne suis pas sûr ....
Pourquoi pas la règle opposée, c'est-à-dire déduire Any
quand Co-Variant / Invariant et Nothing
quand Contra-Variant?
MODIFIER2
À la lumière de @slouc Answer, qui a du bon sens, il ne me reste plus qu'à comprendre quoi et pourquoi le compilateur fait ce qu'il fait. L'exemple ci-dessous illustre ma confusion
val myleft = Left("Error") // Left[String,Nothing] = Left(Error)
myleft map { (e:Int) => e * 4} // Either[String,Int] = Left(Error)
- Tout d'abord, le compilateur fixe le type à quelque chose qui "fonctionne à coup sûr" pour réutiliser la conclusion de @slouc (bien que cela ait plus de sens dans le contexte d'une fonction)
Left[String,Nothing]
- Ensuite, la compilation en déduit
myleft
qu'elle est de type Either [String, Int]
une définition de carte donnée def map[B](f: A => B): Either[E, B]
, (e:Int) => e * 4
ne peut être fournie que si myleft
est réellement Left[String,Int]
ouEither[String,Int]
Donc, en d'autres termes, ma question est de savoir à quoi sert de fixer le type Nothing
s'il doit le changer plus tard.
En effet, ce qui suit ne compile pas
val aleft: Left[String, Nothing] = Left[String, Int]("Error")
type mismatch;
found : scala.util.Left[String,Int]
required: Left[String,Nothing]
val aleft: Left[String, Nothing] = Left[String, Int]("Error")
Alors, pourquoi devrais-je déduire à un type, cela me bloquerait normalement de faire quoi que ce soit d'autre sur une variable de ce type ( mais fonctionne bien sûr en terme d'inférence ), pour finalement changer ce type, afin que je puisse faire quelque chose avec une variable de ce type type inféré.
MODIFIER3
Edit2 est un peu malentendu et tout est clarifié dans la réponse et les commentaires de @slouc.
Réponses
Covariance:
Étant donné le typeF[+A]
et la relationA <: B
, alors ce qui suit est vrai:F[A] <: F[B]
Contravariance:
Étant donné le typeF[-A]
et la relationA <: B
, alors ce qui suit est vrai:F[A] >: F[B]
Si le compilateur ne peut pas déduire le type exact, il résoudra le type le plus bas possible en cas de covariance et le type le plus élevé possible en cas de contravariance.
Pourquoi?
Il s'agit d'une règle très importante en ce qui concerne la variance du sous-typage. Il peut être montré sur l'exemple du type de données suivant de Scala:
trait Function1[Input-, Output+]
De manière générale, lorsqu'un type est placé dans les paramètres de la fonction / méthode, cela signifie qu'il est dans la position dite "contravariante". S'il est utilisé dans les valeurs de retour de fonction / méthode, il se trouve dans la soi-disant «position covariante». Si c'est dans les deux, alors c'est invariant.
Maintenant, étant donné les règles du début de cet article, nous concluons que, étant donné:
trait Food
trait Fruit extends Food
trait Apple extends Fruit
def foo(someFunction: Fruit => Fruit) = ???
nous pouvons fournir
val f: Food => Apple = ???
foo(f)
La fonction f
est un substitut valide pour someFunction
parce que:
Food
est un supertype deFruit
(contravariance de l'entrée)Apple
est un sous-type deFruit
(covariance de la sortie)
Nous pouvons expliquer cela en langage naturel comme ceci:
"La méthode a
foo
besoin d'une fonction qui peut prendre aFruit
et produire aFruit
. Cela signifiefoo
qu'elle en auraFruit
et aura besoin d'une fonction à laquelle elle pourra la nourrir, et en attendra uneFruit
partie. Si elle obtient une fonctionFood => Apple
, tout va bien - elle peut toujours la nourrirFruit
( car la fonction prend n'importe quel aliment), et elle peut recevoirFruit
(les pommes sont des fruits, donc le contrat est respecté).
Pour en revenir à votre dilemme initial, j'espère que cela explique pourquoi, sans aucune information supplémentaire, le compilateur utilisera le type le plus bas possible pour les types covariants et le type le plus élevé possible pour les types contravariants. Si nous voulons fournir une fonction foo
, il y a celui que nous connaissons fonctionne sûrement: Any => Nothing
.
Variance en général .
Variance dans la documentation Scala .
Article sur la variance dans Scala (divulgation complète: je l'ai écrit).
ÉDITER:
Je pense que je sais ce qui vous trouble.
Lorsque vous instanciez un Left[String, Nothing]
, vous êtes autorisé à le faire ultérieurement map
avec une fonction Int => Whatever
, ou String => Whatever
, ou Any => Whatever
. Ceci est précisément dû à la contravariance de l'entrée de fonction expliquée précédemment. C'est pourquoi vos map
oeuvres.
"quel est l'intérêt de fixer le type à Nothing si c'est de le changer plus tard?"
Je pense qu'il est un peu difficile de comprendre le compilateur qui corrige le type inconnu Nothing
en cas de contravariance. Quand il corrige le type inconnu Any
en cas de covariance, cela semble plus naturel (cela peut être "N'importe quoi"). En raison de la dualité de la covariance et de la contravariance expliquée précédemment, le même raisonnement s'applique pour les contravariants Nothing
et les covariants Any
.
Ceci est une citation de Unification of Compile-Time and Runtime Metaprogramming in Scala par Eugene Burmako
https://infoscience.epfl.ch/record/226166 (p. 95 à 96)
Pendant l'inférence de type, le vérificateur de type collecte les contraintes sur les arguments de type manquants à partir des limites des paramètres de type, des types d'arguments de terme et même des résultats de la recherche implicite (l'inférence de type fonctionne avec la recherche implicite car Scala prend en charge un analogue des dépendances fonctionnelles). On peut voir ces contraintes comme un système d'inégalités où les arguments de type inconnu sont représentés comme des variables de type et l'ordre est imposé par la relation de sous-typage.
Après avoir collecté les contraintes, le vérificateur de type lance un processus étape par étape qui, à chaque étape, essaie d'appliquer une certaine transformation aux inégalités, créant un système d'inégalités équivalent, mais supposément plus simple. Le but de l'inférence de type est de transformer les inégalités d'origine en égalités qui représentent une solution unique du système d'origine.
La plupart du temps, l'inférence de type réussit. Dans ce cas, les arguments de type manquants sont déduits des types représentés par la solution.
Cependant, l'inférence de type échoue parfois. Par exemple, lorsqu'un paramètre de type
T
est fantôme, c'est-à-dire inutilisé dans le terme paramètres de la méthode, sa seule entrée dans le système d'inégalités seraL <: T <: U
, oùL
etU
sont respectivement ses bornes inférieure et supérieure. SiL != U
, cette inégalité n'a pas de solution unique, et cela signifie un échec de l'inférence de type.Lorsque l'inférence de type échoue, c'est-à-dire lorsqu'il est incapable de prendre d'autres étapes de transformation et que son état de fonctionnement contient encore des inégalités, le vérificateur de type rompt l'impasse. Il prend tous les arguments de type non encore inférés, c'est-à-dire ceux dont les variables sont toujours représentées par des inégalités, et les minimise de force , c'est-à-dire les assimile à leurs bornes inférieures. Cela produit un résultat où certains arguments de type sont déduits avec précision, et certains sont remplacés par des types apparemment arbitraires. Par exemple, les paramètres de type sans contrainte sont déduits
Nothing
, ce qui est une source courante de confusion pour les débutants Scala.
Vous pouvez en savoir plus sur l'inférence de type dans Scala:
Hubert Plociniczak décrypter l' inférence de type local https://infoscience.epfl.ch/record/214757
Guillaume Martres Scala 3, tapez Inference et vous! https://www.youtube.com/watch?v=lMvOykNQ4zs
Guillaume Martres Dotty et types: l'histoire jusqu'ici https://www.youtube.com/watch?v=YIQjfCKDR5A
Diapositives http://guillaume.martres.me/talks/
Aleksander Boruch-Gruszecki GADT à Dotty https://www.youtube.com/watch?v=VV9lPg3fNl8