web-dev-qa-db-fra.com

Pourquoi ne pas être tapé de manière dépendante?

J'ai vu plusieurs sources se faire l'écho de l'opinion selon laquelle "Haskell devient progressivement un langage typé de manière dépendante". L'implication semble être qu'avec de plus en plus d'extensions de langage, Haskell dérive dans cette direction générale, mais n'est pas encore là.

Il y a essentiellement deux choses que j'aimerais savoir. La première est, tout simplement, qu'est-ce que "être une langue de type dépendante" signifie? (Si tout va bien sans être trop technique à ce sujet.)

La deuxième question est ... quel est l'inconvénient? Je veux dire, les gens savent que nous allons dans cette direction, donc il doit y avoir un avantage. Et pourtant, nous n'en sommes pas encore là, il doit donc y avoir des inconvénients qui empêchent les gens d'aller jusqu'au bout. J'ai l'impression que le problème est une forte augmentation de la complexité. Mais, ne comprenant pas vraiment ce qu'est la frappe dépendante, je n'en suis pas sûr.

Ce que je fais sais, c'est que chaque fois que je commence à lire sur un langage de programmation de type dépendant, le texte est totalement incompréhensible ... Vraisemblablement que c'est le problème. (?)

158
MathematicalOrchid

La saisie dépendante est vraiment juste l'unification des niveaux de valeur et de type, vous pouvez donc paramétrer les valeurs sur les types (déjà possible avec les classes de type et le polymorphisme paramétrique dans Haskell) et vous pouvez paramétrer les types sur les valeurs (pas, à proprement parler, encore possible dans Haskell , bien que DataKinds soit très proche).

Edit: Apparemment, à partir de ce moment-là, j'avais tort (voir le commentaire de @ pigworker). Je vais conserver le reste de ceci comme un enregistrement des mythes que j'ai nourris. : P


D'après ce que j'ai entendu, le passage au typage entièrement dépendant est qu'il briserait la restriction de phase entre le type et les niveaux de valeur qui permet à Haskell d'être compilé en code machine efficace avec des types effacés. Avec notre niveau actuel de technologie, un langage typé de manière dépendante doit passer par un interpréteur à un moment donné (soit immédiatement, soit après avoir été compilé en bytecode typé de manière dépendante) ou similaire).

Ce n'est pas nécessairement une restriction fondamentale, mais je ne suis personnellement au courant d'aucune recherche actuelle qui semble prometteuse à cet égard mais qui n'a pas encore été intégrée au GHC. Si quelqu'un d'autre en sait plus, je serais heureux d'être corrigé.

21
Ptharien's Flame

Haskell dactylographié de façon dépendante, maintenant?

Haskell est, dans une faible mesure, un langage typé de manière dépendante. Il existe une notion de données de niveau type, désormais plus judicieusement typées grâce à DataKinds, et il existe des moyens (GADTs) pour donner une représentation d'exécution aux données de niveau type. Par conséquent, les valeurs des éléments d'exécution s'affichent effectivement dans les types , ce qui signifie que le langage est typé de manière dépendante.

Les types de données simples sont promu au niveau kind, de sorte que les valeurs qu'ils contiennent peuvent être utilisées dans les types. D'où l'exemple archétypal

data Nat = Z | S Nat

data Vec :: Nat -> * -> * where
  VNil   :: Vec Z x
  VCons  :: x -> Vec n x -> Vec (S n) x

devient possible, et avec lui, des définitions telles que

vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil         VNil         = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)

ce qui est sympa. Notez que la longueur n est une chose purement statique dans cette fonction, garantissant que les vecteurs d'entrée et de sortie ont la même longueur, même si cette longueur ne joue aucun rôle dans l'exécution de vApply. En revanche, il est beaucoup plus délicat (c'est-à-dire impossible) d'implémenter la fonction qui fait n copies d'un x donné (qui serait pure à vApply) <*>)

vReplicate :: x -> Vec n x

car il est essentiel de savoir combien de copies effectuer au moment de l'exécution. Entrez singletons.

data Natty :: Nat -> * where
  Zy :: Natty Z
  Sy :: Natty n -> Natty (S n)

Pour tout type promotable, nous pouvons construire la famille singleton, indexée sur le type promu, habitée par des doublons d'exécution de ses valeurs. Natty n Est le type de copies d'exécution du niveau type n :: Nat. Nous pouvons maintenant écrire

vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy     x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)

Ainsi, vous avez une valeur de niveau type liée à une valeur d'exécution: l'inspection de la copie d'exécution affine la connaissance statique de la valeur de niveau type. Même si les termes et les types sont séparés, nous pouvons travailler de manière typée dépendante en utilisant la construction singleton comme une sorte de résine époxy, créant des liaisons entre les phases. C'est loin d'autoriser des expressions d'exécution arbitraires dans les types, mais ce n'est pas rien.

Qu'est-ce qui est méchant? Qu'est-ce qui manque?

Mettons un peu de pression sur cette technologie et voyons ce qui commence à vaciller. Nous pourrions avoir l'idée que les singletons devraient être gérables un peu plus implicitement

class Nattily (n :: Nat) where
  natty :: Natty n
instance Nattily Z where
  natty = Zy
instance Nattily n => Nattily (S n) where
  natty = Sy natty

nous permettant d'écrire, disons,

instance Nattily n => Applicative (Vec n) where
  pure = vReplicate natty
  (<*>) = vApply

Cela fonctionne, mais cela signifie maintenant que notre type Nat d'origine a généré trois copies: une sorte, une famille singleton et une classe singleton. Nous avons un processus plutôt maladroit pour échanger des valeurs explicites de Natty n Et des dictionnaires Nattily n. De plus, Natty n'est pas Nat: nous avons une sorte de dépendance vis-à-vis des valeurs d'exécution, mais pas au type auquel nous avons d'abord pensé. Aucun langage de saisie entièrement dépendante ne rend les types dépendants aussi compliqués!

Pendant ce temps, bien que Nat puisse être promu, Vec ne peut pas. Vous ne pouvez pas indexer par un type indexé. Plein de langages typés dépendants n'impose aucune restriction, et dans ma carrière de show-off typé dépendamment, j'ai appris à inclure des exemples d'indexation à deux couches dans mes discussions, juste pour enseigner aux gens qui ont fait de l'indexation à une couche difficile mais possible de ne pas m'attendre à ce que je me replie comme un château de cartes. Quel est le problème? Égalité. Les GADT fonctionnent en traduisant les contraintes que vous atteignez implicitement lorsque vous donnez à un constructeur un type de retour spécifique en demandes équationnelles explicites. Comme ça.

data Vec (n :: Nat) (x :: *)
  = n ~ Z => VNil
  | forall m. n ~ S m => VCons x (Vec m x)

Dans chacune de nos deux équations, les deux côtés ont le type Nat.

Essayez maintenant la même traduction pour quelque chose indexé sur des vecteurs.

data InVec :: x -> Vec n x -> * where
  Here :: InVec z (VCons z zs)
  After :: InVec z ys -> InVec z (VCons y ys)

devient

data InVec (a :: x) (as :: Vec n x)
  = forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
  | forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)

et maintenant nous formons des contraintes équationnelles entre as :: Vec n x et VCons z zs :: Vec (S m) x où les deux côtés ont des types syntaxiquement distincts (mais prouvablement égaux). Le cœur GHC n'est actuellement pas équipé pour un tel concept!

Que manque-t-il d'autre? Eh bien, la plupart de Haskell est manquante au niveau du type. Le langage des termes que vous pouvez promouvoir n'a vraiment que des variables et des constructeurs non GADT. Une fois que vous les avez, la machine type family Vous permet d'écrire des programmes au niveau du type: certains d'entre eux pourraient être tout à fait comme des fonctions que vous envisageriez d'écrire au niveau du terme (par exemple, équiper Nat avec plus , vous pouvez donc donner un bon type à ajouter pour Vec), mais ce n'est qu'une coïncidence!

Une autre chose qui manque, dans la pratique, est une bibliothèque qui utilise nos nouvelles capacités pour indexer les types par valeurs. Que deviennent Functor et Monad dans ce nouveau monde courageux? J'y pense, mais il reste encore beaucoup à faire.

Exécution de programmes de niveau type

Haskell, comme la plupart des langages de programmation typés de manière dépendante, a deux sémantique opérationnelle. Il y a la façon dont le système d'exécution exécute les programmes (expressions fermées uniquement, après l'effacement de type, hautement optimisé) et puis il y a la façon dont le vérificateur de typage exécute les programmes (vos familles de types, votre "type class Prolog", avec des expressions ouvertes). Pour Haskell, vous ne mélangez pas normalement les deux, car les programmes en cours d'exécution sont dans des langues différentes. Les langages typés de manière dépendante ont des modèles d'exécution statiques et d'exécution distincts pour la langue des programmes même, mais ne vous inquiétez pas, le modèle d'exécution vous permet toujours d'effacer les types et, en fait, de prouver effacement: c'est ce que vous donne le mécanisme - extraction de Coq; c'est du moins ce que fait le compilateur d'Edwin Brady (bien qu'Edwin efface les valeurs inutilement dupliquées, ainsi que les types et les preuves). La distinction de phase n'est peut-être plus une distinction de catégorie syntaxique, mais elle est bel et bien vivante.

Les langages typés de manière dépendante, étant totaux, permettent au vérificateur de type d'exécuter des programmes sans craindre quoi que ce soit de pire qu'une longue attente. Alors que Haskell devient de plus en plus typé, nous devons nous demander quel devrait être son modèle d'exécution statique? Une approche pourrait consister à restreindre l'exécution statique aux fonctions totales, ce qui nous donnerait la même liberté d'exécution, mais pourrait nous obliger à faire des distinctions (au moins pour le code au niveau du type) entre les données et les codées, afin que nous puissions dire s'il faut imposer la résiliation ou la productivité. Mais ce n'est pas la seule approche. Nous sommes libres de choisir un modèle d'exécution beaucoup plus faible qui hésite à exécuter des programmes, au prix de faire sortir moins d'équations par le simple calcul. Et en fait, c'est ce que fait GHC. Les règles de typage pour le noyau GHC ne font aucune mention des programmes en cours d'exécution, mais uniquement pour vérifier les preuves des équations. Lors de la traduction vers le noyau, le solveur de contraintes de GHC essaie d'exécuter vos programmes au niveau du type, générant une petite trace argentée de preuves qu'une expression donnée est égale à sa forme normale. Cette méthode de génération de preuves est un peu imprévisible et inévitablement incomplète: elle lutte contre la récursion effrayante, par exemple, et c'est probablement sage. Une chose dont nous n'avons pas à nous soucier est l'exécution de IO calculs dans le vérificateur de typage: rappelez-vous que le vérificateur de typage n'a pas à donner à launchMissiles la même signification que le système d'exécution Est-ce que!

Culture Hindley-Milner

Le système de type Hindley-Milner réalise la coïncidence vraiment impressionnante de quatre distinctions distinctes, avec l'effet secondaire culturel malheureux que beaucoup de gens ne peuvent pas voir la distinction entre les distinctions et supposer que la coïncidence est inévitable! De quoi je parle?

  • termes vs types
  • choses écrites explicitement vs choses écrites implicitement
  • présence lors de l'exécution vs effacement avant l'exécution
  • abstraction non dépendante vs quantification dépendante

Nous sommes habitués à écrire des termes et à laisser des types à déduire ... puis à effacer. Nous sommes habitués à quantifier les variables de type avec l'abstraction et l'application de type correspondantes qui se produisent silencieusement et statiquement.

Vous n'avez pas à vous éloigner trop de Vanilla Hindley-Milner avant que ces distinctions ne sortent de l'alignement, et c'est pas une mauvaise chose. Pour commencer, nous pouvons avoir des types plus intéressants si nous sommes prêts à les écrire à quelques endroits. Pendant ce temps, nous n'avons pas à écrire de dictionnaires de classe de type lorsque nous utilisons des fonctions surchargées, mais ces dictionnaires sont certainement présents (ou intégrés) au moment de l'exécution. Dans les langages typés de manière dépendante, nous nous attendons à effacer plus que des types au moment de l'exécution, mais (comme avec les classes de type), certaines valeurs implicitement déduites ne seront pas effacées. Par exemple, l'argument numérique de vReplicate est souvent déductible du type du vecteur souhaité, mais nous devons toujours le connaître au moment de l'exécution.

Quels choix de conception de langue devrions-nous revoir parce que ces coïncidences ne tiennent plus? Par exemple, est-il vrai que Haskell ne fournit aucun moyen d'instancier explicitement un quantificateur forall x. t? Si le vérificateur de typage ne peut pas deviner x en unifiant t, nous n'avons aucun autre moyen de dire ce que x doit être.

Plus largement, nous ne pouvons pas traiter l '"inférence de type" comme un concept monolithique dont nous avons tout ou rien. Pour commencer, nous devons séparer l'aspect "généralisation" (règle "laisser" de Milner), qui repose fortement sur la restriction des types existants pour garantir qu'une machine stupide puisse en deviner un, de l'aspect "spécialisation" (var "Milner" "règle) qui est aussi efficace que votre solveur de contraintes. Nous pouvons nous attendre à ce que les types de niveau supérieur deviennent plus difficiles à déduire, mais les informations de type interne resteront assez faciles à propager.

Prochaines étapes pour Haskell

Nous voyons le type et les niveaux de nature devenir très similaires (et ils partagent déjà une représentation interne dans GHC). Nous pourrions aussi bien les fusionner. Ce serait amusant de prendre * :: * Si nous le pouvons: nous avons perdu logique la solidité il y a longtemps, lorsque nous avons autorisé le bas, mais type la solidité est généralement une exigence plus faible. Nous devons vérifier. Si nous devons avoir des niveaux de type, de type, etc. différents, nous pouvons au moins nous assurer que tout au niveau du type et au-dessus peut toujours être promu. Il serait formidable de réutiliser le polymorphisme que nous avons déjà pour les types, plutôt que de réinventer le polymorphisme au niveau du type.

Nous devons simplifier et généraliser le système de contraintes actuel en autorisant hétérogène équations a ~ b Où les types de a et b ne sont pas syntaxiquement identiques (mais peut être prouvé égal). C'est une vieille technique (dans ma thèse du siècle dernier) qui rend la dépendance beaucoup plus facile à gérer. Nous pourrions exprimer des contraintes sur les expressions dans les GADT, et ainsi assouplir les restrictions sur ce qui peut être promu.

Nous devons éliminer le besoin de la construction singleton en introduisant un type de fonction dépendant, pi x :: s -> t. Une fonction avec un tel type pourrait être appliquée explicitement à toute expression de type s qui vit dans l'intersection du langage de type et du terme (donc, variables, constructeurs, avec plus à venir plus tard). Le lambda et l'application correspondants ne seraient pas effacés au moment de l'exécution, nous pourrions donc écrire

vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z     x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)

sans remplacer Nat par Natty. Le domaine de pi peut être de n'importe quel type promotable, donc si des GADT peuvent être promus, nous pouvons écrire des séquences quantifiantes dépendantes (ou "télescopes" comme les appelait de Briuijn)

pi n :: Nat -> pi xs :: Vec n x -> ...

quelle que soit la longueur dont nous avons besoin.

Le but de ces étapes est de éliminer la complexité en travaillant directement avec des outils plus généraux, au lieu de se contenter d'outils faibles et d'encodages maladroits. Le buy-in partiel actuel rend les avantages des types dépendants de Haskell plus chers qu'ils ne devraient l'être.

Trop dur?

Les types dépendants rendent beaucoup de gens nerveux. Ils me rendent nerveux, mais j'aime être nerveux, ou du moins j'ai du mal à ne pas être nerveux de toute façon. Mais cela n'aide pas qu'il y ait un tel brouillard d'ignorance autour du sujet. Cela est dû en partie au fait que nous avons tous encore beaucoup à apprendre. Mais les partisans d'approches moins radicales sont connus pour attiser la peur des types dépendants sans toujours s'assurer que les faits sont entièrement avec eux. Je ne nommerai pas de noms. Ces "vérifications typographiques indécidables", "Turing incomplètes", "aucune distinction de phase", "aucun effacement de type", "preuves partout", etc., les mythes persistent, même s'ils sont des ordures.

Ce n'est certainement pas le cas que les programmes typés de manière dépendante doivent toujours être prouvés corrects. On peut améliorer l'hygiène de base de ses programmes, en imposant des invariants supplémentaires dans les types sans aller jusqu'à une spécification complète. De petits pas dans cette direction aboutissent assez souvent à des garanties beaucoup plus solides avec peu ou pas d'obligations de preuve supplémentaires. Ce n'est pas vrai que les programmes typés dépendants sont inévitablement complet de preuves, en effet je prends habituellement la présence de toutes les preuves dans mon code comme indice pour remettre en question mes définitions =.

Car, comme pour toute augmentation de l'articulation, nous devenons libres de dire de nouvelles choses grossières aussi bien que justes. Par exemple, il existe de nombreuses façons minables de définir des arbres de recherche binaires, mais cela ne signifie pas qu'il n'y en a pas ne bonne façon . Il est important de ne pas présumer que les mauvaises expériences ne peuvent pas être améliorées, même si cela ébranle l'ego pour l'admettre. La conception de définitions dépendantes est une nouvelle compétence qui demande de l'apprentissage, et être programmeur Haskell ne fait pas automatiquement de vous un expert! Et même si certains programmes sont répugnants, pourquoi refuseriez-vous à d'autres la liberté d'être équitable?

Pourquoi s'embêter avec Haskell?

J'apprécie vraiment les types dépendants, mais la plupart de mes projets de piratage sont toujours à Haskell. Pourquoi? Haskell a des classes de types. Haskell possède des bibliothèques utiles. Haskell a un traitement réalisable (bien que loin d'être idéal) de la programmation avec effets. Haskell possède un compilateur de puissance industrielle. Les langages typés de manière dépendante sont à un stade beaucoup plus précoce de la croissance de la communauté et de l'infrastructure, mais nous y arriverons, avec un réel changement de génération dans ce qui est possible, par exemple, par le biais de métaprogrammation et de génériques de types de données. Mais il suffit de regarder ce que les gens font à la suite des étapes de Haskell vers les types dépendants pour voir qu'il y a beaucoup d'avantages à faire avancer la génération actuelle de langues.

220
pigworker

John, c'est une autre idée fausse commune sur les types dépendants: ils ne fonctionnent pas lorsque les données ne sont disponibles qu'au moment de l'exécution. Voici comment vous pouvez faire l'exemple getLine:

data Some :: (k -> *) -> * where
  Like :: p x -> Some p

fromInt :: Int -> Some Natty
fromInt 0 = Like Zy
fromInt n = case fromInt (n - 1) of
  Like n -> Like (Sy n)

withZeroes :: (forall n. Vec n Int -> IO a) -> IO a
withZeroes k = do
  Like n <- fmap (fromInt . read) getLine
  k (vReplicate n 0)

*Main> withZeroes print
5
VCons 0 (VCons 0 (VCons 0 (VCons 0 (VCons 0 VNil))))

Edit: Hm, c'était censé être un commentaire à la réponse du cochon. J'échoue clairement à SO.

20
ulfnorell

pigworker explique très bien pourquoi nous devrions nous diriger vers des types dépendants: (a) ils sont impressionnants; (b) ils simplifieraient en fait beaucoup de ce que fait déjà Haskell.

Quant au "pourquoi pas?" question, il y a quelques points je pense. Le premier point est que si la notion de base derrière les types dépendants est facile (permettre aux types de dépendre de valeurs), les ramifications de cette notion de base sont à la fois subtiles et profondes. Par exemple, la distinction entre les valeurs et les types est toujours bien vivante; mais discuter de la différence entre eux devient loin plus nuancé que dans yer Hindley - Milner ou System F. Dans une certaine mesure, cela est dû au fait que les types sont fondamentalement difficiles (par exemple, la logique du premier ordre est indécidable). Mais je pense que le plus gros problème est vraiment que nous manquons d'un bon vocabulaire pour capturer et expliquer ce qui se passe. À mesure que de plus en plus de personnes apprennent les types dépendants, nous développerons un meilleur vocabulaire et les choses deviendront ainsi plus faciles à comprendre, même si les problèmes sous-jacents sont toujours difficiles.

Le deuxième point a à voir avec le fait que Haskell est en croissance vers les types dépendants. Parce que nous faisons des progrès incrémentiels vers cet objectif, mais sans vraiment y arriver, nous sommes coincés avec un langage qui a des correctifs incrémentiels en plus des correctifs incrémentiels. Le même genre de chose s'est produit dans d'autres langues à mesure que de nouvelles idées sont devenues populaires. Java n'utilisait pas le polymorphisme (paramétrique); et quand ils l'ont finalement ajouté, c'était évidemment une amélioration incrémentielle avec quelques fuites d'abstraction et une puissance paralysée. Il s'avère que mélanger le sous-typage et le polymorphisme est intrinsèquement difficile; mais ce n'est pas la raison pour laquelle Java Les génériques fonctionnent comme ils le font. Ils fonctionnent comme ils le font en raison de la contrainte d'être une amélioration incrémentielle des anciennes versions de Java. Idem, pour plus loin dans le jour où OOP a été inventé et les gens ont commencé à écrire C "objectif" (à ne pas confondre avec objectif C), etc. Rappelez-vous, C++ a commencé sous le couvert d'être un strict surensemble de C. L'ajout de nouveaux paradigmes nécessite toujours de redéfinir le langage, ou de se retrouver avec un gâchis compliqué. Mon point dans tout cela est que, l'ajout de vrais types dépendants à Haskell va nécessiter une certaine quantité d'éviscération et de la langue --- si nous voulons bien le faire. Mais il est vraiment difficile de s'engager envers ce parent d d'une refonte, alors que les progrès progressifs que nous avons réalisés semblent moins chers à court terme. Vraiment, il n'y a pas beaucoup de gens qui piratent GHC, mais il y a une bonne quantité de code hérité à garder en vie. Cela fait partie de la raison pour laquelle il existe de nombreuses langues dérivées comme DDC, Cayenne, Idris, etc.

19
wren romano