Il existe des types dépendants du chemin et je pense qu'il est possible d'exprimer presque toutes les fonctionnalités de langages comme Epigram ou Agda dans Scala, mais je me demande pourquoi Scala ne prend pas en charge cela de manière plus explicite, comme il le fait très bien dans autres zones (par exemple, DSL)? Tout ce qui me manque comme "ce n’est pas nécessaire"?
La commodité syntaxique mise à part, la combinaison de types singleton, de types dépendants du chemin et de valeurs implicites signifie que Scala supporte étonnamment bien le typage dépendant, comme j'ai essayé de le démontrer dans informe .
Le support intrinsèque de Scala pour les types dépendants est via les types dépendants du chemin . Celles-ci permettent à un type de dépendre d'un chemin de sélecteur via un graphe d'objet (c'est-à-dire, de valeur) comme ceci,
scala> class Foo { class Bar }
defined class Foo
scala> val foo1 = new Foo
foo1: Foo = Foo@24bc0658
scala> val foo2 = new Foo
foo2: Foo = Foo@6f7f757
scala> implicitly[foo1.Bar =:= foo1.Bar] // OK: equal types
res0: =:=[foo1.Bar,foo1.Bar] = <function1>
scala> implicitly[foo1.Bar =:= foo2.Bar] // Not OK: unequal types
<console>:11: error: Cannot prove that foo1.Bar =:= foo2.Bar.
implicitly[foo1.Bar =:= foo2.Bar]
À mon avis, ce qui précède devrait suffire à répondre à la question "Est-ce que Scala est une langue typée de manière dépendante?" dans le positif: il est clair que nous avons ici des types qui se distinguent par les valeurs qui sont leurs préfixes.
Cependant, il est souvent objecté que Scala n'est pas un langage de type "totalement" dépendant du fait qu'il n'a pas la somme dépendante et les types de produits comme on en trouve dans Agda, Coq ou Idris en tant qu'intrinsèques. Je pense que cela reflète dans une certaine mesure une fixation sur la forme plutôt que sur les fondamentaux. Néanmoins, je vais essayer de montrer que Scala est beaucoup plus proche de ces autres langues que ce qui est généralement reconnu.
Malgré la terminologie, les types de somme dépendants (également appelés types Sigma) sont simplement une paire de valeurs où le type de la seconde valeur dépend de la première valeur. Ceci est directement représentable à Scala,
scala> trait Sigma {
| val foo: Foo
| val bar: foo.Bar
| }
defined trait Sigma
scala> val sigma = new Sigma {
| val foo = foo1
| val bar = new foo.Bar
| }
sigma: Java.lang.Object with Sigma{val bar: this.foo.Bar} = $anon$1@e3fabd8
et en fait, il s’agit là d’une partie cruciale du codage des types de méthodes dépendantes nécessaire pour sortir de la 'Bakery of Doom' dans Scala antérieure à 2.10 (ou plus tôt via l'option de compilateur expérimentale -Ydependent-method Scala).
Les types de produits dépendants (aussi appelés types Pi) sont essentiellement des fonctions allant des valeurs aux types. Ils sont essentiels à la représentation des vecteurs de taille statique et des autres enfants afficheurs pour les langages de programmation typés de manière dépendante. Nous pouvons coder des types Pi dans Scala en utilisant une combinaison de types dépendants du chemin, de types singleton et de paramètres implicites. Nous définissons d’abord un trait qui va représenter une fonction d’une valeur de type T à un type U,
scala> trait Pi[T] { type U }
defined trait Pi
Nous pouvons ensuite définir une méthode polymorphe qui utilise ce type,
scala> def depList[T](t: T)(implicit pi: Pi[T]): List[pi.U] = Nil
depList: [T](t: T)(implicit pi: Pi[T])List[pi.U]
(notez l'utilisation du type dépendant du chemin pi.U
dans le type de résultat List[pi.U]
). Étant donné une valeur de type T, cette fonction renverra une liste (n vide) de valeurs du type correspondant à cette valeur T particulière.
Définissons maintenant des valeurs appropriées et des témoins implicites des relations fonctionnelles que nous souhaitons avoir,
scala> object Foo
defined module Foo
scala> object Bar
defined module Bar
scala> implicit val fooInt = new Pi[Foo.type] { type U = Int }
fooInt: Java.lang.Object with Pi[Foo.type]{type U = Int} = $anon$1@60681a11
scala> implicit val barString = new Pi[Bar.type] { type U = String }
barString: Java.lang.Object with Pi[Bar.type]{type U = String} = $anon$1@187602ae
Et maintenant, voici notre fonction utilisant le type Pi en action,
scala> depList(Foo)
res2: List[fooInt.U] = List()
scala> depList(Bar)
res3: List[barString.U] = List()
scala> implicitly[res2.type <:< List[Int]]
res4: <:<[res2.type,List[Int]] = <function1>
scala> implicitly[res2.type <:< List[String]]
<console>:19: error: Cannot prove that res2.type <:< List[String].
implicitly[res2.type <:< List[String]]
^
scala> implicitly[res3.type <:< List[String]]
res6: <:<[res3.type,List[String]] = <function1>
scala> implicitly[res3.type <:< List[Int]]
<console>:19: error: Cannot prove that res3.type <:< List[Int].
implicitly[res3.type <:< List[Int]]
(Notez que nous utilisons ici le témoin de sous-type <:<
de Scala plutôt que =:=
car res2.type
et res3.type
sont des types singleton et sont donc plus précis que les types que nous vérifions sur le RHS).
En pratique, cependant, dans Scala, nous ne commencerions pas par coder les types Sigma et Pi, puis par le même principe que pour Agda ou Idris. Au lieu de cela, nous utiliserions des types dépendants du chemin, des types singleton et implicites directement. Vous pouvez trouver de nombreux exemples de la manière dont cela se déroule sans forme: types dimensionnés , enregistrements extensibles , HListes complètes , éliminez votre passe-partout , Zippers génériques etc. etc.
La seule objection restante que je puisse voir est que, dans l'encodage ci-dessus des types Pi, nous demandons que les types singleton des valeurs dépendantes soient exprimables. Malheureusement, dans Scala, cela n’est possible que pour les valeurs de types de référence et non pour les valeurs de types non-de référence (par exemple Int.). C'est une honte, mais pas une difficulté intrinsèque: le vérificateur de types de Scala représente les types de singleton de valeurs non référencées en interne, et il y a eu un couple de expérimente en les rendant directement exprimables. En pratique, nous pouvons contourner le problème avec un codage au niveau du type assez standard des nombres naturels .
Dans tous les cas, je ne pense pas que cette légère restriction de domaine puisse être utilisée comme une objection au statut de Scala en tant que langue à typage dépendante. Si tel est le cas, on pourrait en dire autant de Dependent ML (qui autorise uniquement les dépendances sur des valeurs de nombres naturels), ce qui constituerait une conclusion étrange.
Je suppose que c’est parce que (comme je le sais par expérience, après avoir utilisé des types dépendants dans l’assistant de preuve Coq, qui les supporte pleinement mais qui n’est pas encore très pratique), les types dépendants sont une fonctionnalité très avancée du langage de programmation bien faire les choses - et peut provoquer une explosion exponentielle de la complexité dans la pratique. Ils sont encore un sujet de recherche en informatique.
Je pense que les types dépendant du chemin de Scala ne peuvent représenter que des types, mais pas des types. Ce:
trait Pi[T] { type U }
n'est pas exactement un type. Par définition, le type Π, ou produit dépendant, est une fonction dont le type de résultat dépend de la valeur de l’argument, représentant le quantificateur universel, c’est-à-dire ∀x: A, B (x). Dans le cas ci-dessus, cependant, cela dépend uniquement du type T, mais pas d'une valeur de ce type. Le trait Pi lui-même est un type Σ, un quantificateur existentiel, c'est-à-dire x: A, B (x). L'auto-référence de l'objet dans ce cas agit comme une variable quantifiée. Cependant, lorsqu'il est passé en tant que paramètre implicite, il se réduit à une fonction de type ordinaire, car il est résolu en termes de type. Le codage pour le produit dépendant dans Scala peut ressembler à ceci:
trait Sigma[T] {
val x: T
type U //can depend on x
}
// (t: T) => (∃ mapping(x, U), x == t) => (u: U); sadly, refinement won't compile
def pi[T](t: T)(implicit mapping: Sigma[T] { val x = t }): mapping.U
La pièce manquante ici est une capacité à contraindre statiquement le champ x à la valeur attendue t, formant ainsi une équation représentant la propriété de toutes les valeurs du type T., en association avec nos Σ-types, utilisés pour exprimer l'existence d'objet avec une propriété donnée, la logique est formée, dans laquelle notre équation est un théorème à prouver.
Sur une note de côté, dans le cas réel, le théorème peut être hautement non trivial, jusqu’à ce qu’il ne puisse pas être automatiquement dérivé du code ou résolu sans effort significatif. On peut même formuler l'hypothèse de Riemann de cette façon, seulement pour trouver la signature impossible à mettre en œuvre sans la prouver réellement, en faisant une boucle pour toujours ou en lançant une exception.
La question portait sur l’utilisation plus directe de la fonctionnalité typée en fonction de la dépendance et, à mon avis, Il serait avantageux de disposer d’une méthode de typage dépendant plus directe que celle proposée par Scala.
Les réponses actuelles tentent de faire valoir la question au niveau théorique type. Je veux donner une tournure plus pragmatique à ce sujet… .. Cela peut expliquer pourquoi les gens sont divisés sur le niveau de prise en charge des types dépendants dans le langage Scala. Nous pouvons avoir des définitions quelque peu différentes en tête. (pour ne pas dire on a raison et on a tort).
Ce n'est pas une tentative de répondre à la question de savoir s'il serait facile de transformer Scala en quelque chose comme Idris (j'imagine très difficile) ou d'écrire une bibliothèque Offrant un support plus direct pour des fonctionnalités similaires à Idris (comme singletons
essais être à Haskell).
Au lieu de cela, je veux souligner la différence pragmatique entre Scala et une langue comme Idris.
Que sont les bits de code pour les expressions de niveau valeur et type? Idris utilise le même code, Scala utilise un code très différent.
Scala (comme Haskell) peut être capable de coder beaucoup de calculs de niveau de type. Ceci est montré par des bibliothèques comme shapeless
. Ces bibliothèques le font en utilisant des astuces vraiment impressionnantes et astucieuses. Cependant, leur code de niveau de type est (actuellement) très différent des expressions de niveau de valeur .__ (je trouve que cet écart est un peu plus proche en Haskell). Idris permet d'utiliser une expression de niveau valeur au niveau du type AS IS.
L’avantage évident est la réutilisation du code (il n’est pas nécessaire de coder les expressions de type Séparément du niveau de valeur si vous en avez besoin aux deux endroits). Il devrait être bien plus facile d’écrire le code de niveau de valeur. Il devrait être plus facile de ne pas avoir à faire face à des bidouilles comme des singletons (sans parler des coûts de performance). Vous n'avez pas besoin d'apprendre deux choses, vous apprenez une chose .. Sur un plan pragmatique, nous finissons par avoir besoin de moins de concepts. Tapez des synonymes, tapez des familles, des fonctions, ... que diriez-vous de fonctions? À mon avis, ces avantages unificateurs vont beaucoup plus loin et sont plus que la commodité syntaxique.
Considérons le code vérifié. Voir:
https://github.com/idris-lang/Idris-dev/blob/v1.3.0/libs/contrib/Interfaces/Verified.idr
Le vérificateur de types vérifie les preuves des lois monadiques/fonctionnel/applicatif et les preuves Concernent la mise en oeuvre réelle de monad/functor/applicative et non de certains équivalents de type codé La grande question est: que prouvons-nous?
La même chose peut être faite en utilisant des astuces d’encodage intelligentes (voir ce qui suit pour la version Haskell, je n’en ai pas vu pour Scala)
https://blog.jle.im/entry/verified-instances-in-haskell.html
https://github.com/rpeszek/IdrisTddNotes/wiki/Play_FunctorLaws
sauf que les types sont si compliqués qu'il est difficile de voir les lois, les expressions de niveau valeur sont converties (automatiquement mais quand même) en choses de type et vous devez également faire confiance à cette conversion. Il y a une marge d'erreur dans tout cela, ce qui défie un peu l'objectif du compilateur d'agir en tant que Assistant de preuve.
(EDITED 2018.8.10) En parlant d'assistance à la preuve, voici une autre grande différence entre Idris et Scala. Il n'y a rien dans Scala (ou Haskell) qui puisse empêcher d'écrire des preuves divergentes:
case class Void(underlying: Nothing) extends AnyVal //should be uninhabited
def impossible() : Void = impossible()
tandis que Idris a le mot clé total
empêchant la compilation d’un code comme celui-ci.
Une bibliothèque Scala qui tente d'unifier le code de niveau valeur et type (comme Haskell singletons
) constituerait un test intéressant pour la prise en charge par Scala des types dépendants. Une telle bibliothèque peut-elle être faite beaucoup mieux en Scala en raison des types dépendants du chemin?
Je suis trop nouveau à Scala pour répondre moi-même à cette question.