Pourquoi Scala infère le type inférieur lorsque le paramètre de type n'est pas spécifié?

Nov 23 2020

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 Anypar 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 Anyquand Co-Variant / Invariant et Nothingquand 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)

  1. 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]
  2. Ensuite, la compilation en déduit myleftqu'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 * 4ne peut être fournie que si myleftest réellement Left[String,Int]ouEither[String,Int]

Donc, en d'autres termes, ma question est de savoir à quoi sert de fixer le type Nothings'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

4 slouc Nov 23 2020 at 17:33
  • Covariance:
    Étant donné le type F[+A]et la relation A <: B, alors ce qui suit est vrai:F[A] <: F[B]

  • Contravariance:
    Étant donné le type F[-A]et la relation A <: 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 fest un substitut valide pour someFunctionparce que:

  • Foodest un supertype de Fruit(contravariance de l'entrée)
  • Appleest un sous-type de Fruit(covariance de la sortie)

Nous pouvons expliquer cela en langage naturel comme ceci:

"La méthode a foobesoin d'une fonction qui peut prendre a Fruitet produire a Fruit. Cela signifie fooqu'elle en aura Fruitet aura besoin d'une fonction à laquelle elle pourra la nourrir, et en attendra une Fruitpartie. Si elle obtient une fonction Food => Apple, tout va bien - elle peut toujours la nourrir Fruit( car la fonction prend n'importe quel aliment), et elle peut recevoir Fruit(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 mapavec 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 mapoeuvres.

"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 Nothingen cas de contravariance. Quand il corrige le type inconnu Anyen 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 Nothinget les covariants Any.

1 DmytroMitin Nov 23 2020 at 20:54

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 Test 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 sera L <: T <: U, où Let Usont respectivement ses bornes inférieure et supérieure. Si L != 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