Supposons que je veuille écrire une bibliothèque qui traite des vecteurs et des matrices. Est-il possible de faire cuire les dimensions dans les types, de sorte que les opérations de dimensions incompatibles génèrent une erreur au moment de la compilation?
Par exemple, je voudrais que la signature du produit scalaire soit quelque chose comme
dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a
où le d
type contient une seule valeur entière (représentant la dimension de ces vecteurs).
Je suppose que cela pourrait être fait en définissant (à la main) un type distinct pour chaque entier et en les regroupant dans une classe de type appelée VecDim
. Existe-t-il un mécanisme pour «générer» de tels types?
Ou peut-être un moyen meilleur / plus simple de réaliser la même chose?
haskell
type-systems
type-safety
mitchus
la source
la source
tensor
bibliothèque y parvienne avec élégance en utilisant unedata
définition récursive : noaxiom.org/tensor-documentation#ordinalsRéponses:
Pour développer la réponse de @ KarlBielefeldt, voici un exemple complet de la façon d'implémenter des vecteurs - des listes avec un nombre d'éléments statiquement connus - dans Haskell. Tenez votre chapeau ...
Comme vous pouvez le voir dans la longue liste de
LANGUAGE
directives, cela ne fonctionnera qu'avec une version récente de GHC.Nous avons besoin d'un moyen de représenter les longueurs dans le système de types. Par définition, un nombre naturel est soit zéro (
Z
), soit le successeur d'un autre nombre naturel (S n
). Ainsi, par exemple, le numéro 3 serait écritS (S (S Z))
.Avec l' extension DataKinds , cette
data
déclaration introduit un type appeléNat
et deux constructeurs de type appelésS
etZ
- en d'autres termes, nous avons des nombres naturels au niveau du type . Notez que les typesS
etZ
n'ont pas de valeurs de membre - seuls les types de kind*
sont habités par des valeurs.Nous présentons maintenant un GADT représentant des vecteurs de longueur connue. Notez la signature type:
Vec
nécessite un type de typeNat
(c'est-à-dire unZ
ou unS
type) pour représenter sa longueur.La définition des vecteurs est similaire à celle des listes chaînées, avec quelques informations supplémentaires au niveau du type sur sa longueur. Un vecteur est soit
VNil
, auquel cas il a une longueur deZ
(ero), soit c'est uneVCons
cellule qui ajoute un élément à un autre vecteur, auquel cas sa longueur est un de plus que l'autre vecteur (S n
). Notez qu'il n'y a pas d'argument constructeur de typen
. Il est juste utilisé au moment de la compilation pour suivre les longueurs et sera effacé avant que le compilateur ne génère le code machine.Nous avons défini un type de vecteur qui porte la connaissance statique de sa longueur. Examinons le type de quelques
Vec
s pour avoir une idée de leur fonctionnement:Le produit scalaire procède comme pour une liste:
vap
, qui applique «zippement» un vecteur de fonctions à un vecteur d 'arguments, est sonVec
applicatif<*>
; Je ne l'ai pas mis dans unApplicative
exemple parce que ça devient salissant . Notez également que j'utilise le àfoldr
partir de l'instance générée par le compilateur deFoldable
.Essayons-le:
Génial! Vous obtenez une erreur de compilation lorsque vous essayez de
dot
vecteurs dont les longueurs ne correspondent pas.Voici une tentative de fonction pour concaténer des vecteurs ensemble:
La longueur du vecteur de sortie serait la somme des longueurs des deux vecteurs d'entrée. Nous devons apprendre au vérificateur de type comment additionner
Nat
s ensemble. Pour cela, nous utilisons une fonction de niveau type :Cette
type family
déclaration introduit une fonction sur les types appelés:+:
- en d'autres termes, c'est une recette pour le vérificateur de type pour calculer la somme de deux nombres naturels. Il est défini de manière récursive - chaque fois que l'opérande gauche est supérieur àZ
ero, nous en ajoutons un à la sortie et le réduisons de un dans l'appel récursif. (C'est un bon exercice pour écrire une fonction de type qui multiplie deuxNat
s.) Maintenant, nous pouvons faire+++
compiler:Voici comment vous l'utilisez:
Jusqu'ici tout simple. Qu'en est-il lorsque nous voulons faire le contraire de la concaténation et diviser un vecteur en deux? Les longueurs des vecteurs de sortie dépendent de la valeur d'exécution des arguments. Nous aimerions écrire quelque chose comme ceci:
mais malheureusement Haskell ne nous laisse pas faire ça. Permettre à la valeur de l'
n
argument d'apparaître dans le type de retour (c'est ce qu'on appelle communément une fonction dépendante ou type pi ) nécessiterait des types dépendants "à spectre complet", alors que celaDataKinds
ne nous donne que des constructeurs de type promus. Pour le dire autrement, les constructeurs de typesS
etZ
n'apparaissent pas au niveau de la valeur. Nous devrons nous contenter des valeurs singleton pour une représentation d'exécution d'un certainNat
. *Pour un type donné
n
(avec kindNat
), il existe précisément un terme de typeNatty n
. Nous pouvons utiliser la valeur singleton comme témoin d'exécution pourn
: l'apprentissage d'unNatty
nous apprend sonn
et vice versa.Prenons-le pour un tour:
Dans le premier exemple, nous avons réussi à diviser un vecteur à trois éléments à la position 2; puis nous avons eu une erreur de type lorsque nous avons essayé de diviser un vecteur à une position après la fin. Les singletons sont la technique standard pour faire dépendre un type d'une valeur en Haskell.
* La
singletons
bibliothèque contient des assistants Template Haskell pour générer des valeurs singleton commeNatty
pour vous.Dernier exemple. Qu'en est-il lorsque vous ne connaissez pas statiquement la dimensionnalité de votre vecteur? Par exemple, que se passe-t-il si nous essayons de créer un vecteur à partir de données d'exécution sous la forme d'une liste? Vous avez besoin que le type du vecteur dépende de la longueur de la liste d'entrée. En d'autres termes, nous ne pouvons pas utiliser
foldr VCons VNil
pour construire un vecteur car le type du vecteur de sortie change à chaque itération du pli. Nous devons garder la longueur du vecteur secrète du compilateur.AVec
est un type existentiel : la variable typen
n'apparaît pas dans le type de retour duAVec
constructeur de données. Nous l'utilisons pour simuler une paire dépendante :fromList
ne peut pas vous dire statiquement la longueur du vecteur, mais il peut renvoyer quelque chose sur lequel vous pouvez faire correspondre le motif pour apprendre la longueur du vecteur - leNatty n
dans le premier élément du tuple . Comme Conor McBride le dit dans une réponse connexe : «Vous regardez une chose et, ce faisant, vous en apprenez une autre».Il s'agit d'une technique courante pour les types quantifiés existentiellement. Parce que vous ne pouvez rien faire avec des données dont vous ne connaissez pas le type - essayez d'écrire une fonction de
data Something = forall a. Sth a
- les existentielles sont souvent fournies avec des preuves GADT qui vous permettent de récupérer le type d'origine en effectuant des tests de correspondance de modèles. D'autres modèles courants d'existentiels incluent l'empaquetage de fonctions pour traiter votre type (data AWayToGetTo b = forall a. HeresHow a (a -> b)
), ce qui est une bonne façon de faire des modules de première classe, ou la création d'un dictionnaire de classes de type (data AnOrd = forall a. Ord a => AnOrd a
) qui peut aider à émuler le polymorphisme de sous-type.Les paires dépendantes sont utiles lorsque les propriétés statiques des données dépendent d'informations dynamiques non disponibles au moment de la compilation. Voici
filter
pour les vecteurs:A
dot
deuxAVec
s, il faut prouver au GHC que leurs longueurs sont égales.Data.Type.Equality
définit un GADT qui ne peut être construit que lorsque ses arguments de type sont identiques:Lorsque vous faites correspondre des motifs
Refl
, GHC le saita ~ b
. Il existe également quelques fonctions pour vous aider à travailler avec ce type: nous utiliseronsgcastWith
pour convertir entre des types équivalents etTestEquality
pour déterminer si deuxNatty
s sont égaux.Pour tester l'égalité de deux
Natty
s, nous allons devoir utiliser le fait que si deux nombres sont égaux, alors leurs successeurs sont également égaux (:~:
est congruent surS
):La correspondance des motifs sur
Refl
le côté gauche permet au GHC de le savoirn ~ m
. Avec cette connaissance, c'est trivialS n ~ S m
, alors GHC nous permet de retourner un nouveauRefl
tout de suite.Maintenant, nous pouvons écrire une instance de
TestEquality
par récursivité simple. Si les deux nombres sont nuls, ils sont égaux. Si les deux nombres ont des prédécesseurs, ils sont égaux si les prédécesseurs sont égaux. (S'ils ne sont pas égaux, revenez simplementNothing
.)Maintenant, nous pouvons assembler les pièces en
dot
une paire deAVec
s de longueur inconnue.Tout d'abord, le modèle correspond au
AVec
constructeur pour extraire une représentation d'exécution des longueurs des vecteurs. Utilisez maintenanttestEquality
pour déterminer si ces longueurs sont égales. S'ils le sont, nous auronsJust Refl
;gcastWith
utilisera cette preuve d'égalité pour s'assurer qu'elledot u v
est bien typée en libérant sonn ~ m
hypothèse implicite .Notez que, comme un vecteur sans connaissance statique de sa longueur est essentiellement une liste, nous avons effectivement ré-implémenté la version de liste de
dot :: Num a => [a] -> [a] -> Maybe a
. La différence est que cette version est implémentée en termes de vecteurs »dot
. Voici le point: avant que le vérificateur de type ne vous permette d'appelerdot
, vous devez avoir testé si les listes d'entrée ont la même longueur en utilisanttestEquality
. Je suis enclin à obtenir desif
déclarations dans le mauvais sens, mais pas dans un cadre typé de manière dépendante!Vous ne pouvez pas éviter d'utiliser des wrappers existentiels sur les bords de votre système, lorsque vous traitez avec des données d'exécution, mais vous pouvez utiliser des types dépendants partout dans votre système et conserver les wrappers existentiels sur les bords, lorsque vous effectuez une validation d'entrée.
Étant donné que ce
Nothing
n'est pas très informatif, vous pouvez affiner davantage le type dedot'
pour renvoyer une preuve que les longueurs ne sont pas égales (sous la forme de preuves que leur différence n'est pas 0) dans le cas d'échec. Ceci est assez similaire à la technique standard utilisée par HaskellEither String a
pour renvoyer éventuellement un message d'erreur, bien qu'un terme de preuve soit beaucoup plus utile en termes de calcul qu'une chaîne!Ainsi se termine ce tour de passe-passe de certaines des techniques qui sont courantes dans la programmation Haskell typée de façon dépendante. La programmation avec des types comme celui-ci dans Haskell est vraiment cool, mais vraiment maladroite en même temps. Décomposer toutes vos données dépendantes en de nombreuses représentations qui signifient la même chose -
Nat
le type,Nat
le type,Natty n
le singleton - est vraiment assez lourd, malgré l'existence de générateurs de code pour aider avec le passe-partout. Il existe également actuellement des limitations sur ce qui peut être promu au niveau du type. C'est pourtant alléchant! L'esprit s'embarrasse des possibilités - dans la littérature, il existe des exemples dans Haskell d'printf
interfaces de base de données fortement typées , de moteurs de mise en page de l'interface utilisateur ...Si vous souhaitez en savoir plus, il existe de plus en plus de documentation sur Haskell typé de manière dépendante, à la fois publié et sur des sites comme Stack Overflow. Un bon point de départ est le Hasochism papier - le papier passe par ce même exemple (entre autres), discuter des parties douloureuses en détail. Le document Singletons démontre la technique des valeurs singleton (telles que
Natty
). Pour plus d'informations sur la saisie dépendante en général, le didacticiel Agda est un bon point de départ; aussi, Idris est un langage en développement qui est (à peu près) conçu pour être "Haskell avec des types dépendants".la source
C'est ce qu'on appelle la saisie dépendante . Une fois que vous connaissez le nom, vous pouvez trouver plus d'informations à ce sujet que vous ne pourriez jamais souhaiter. Il y a aussi un langage intéressant de type haskell appelé Idris qui les utilise nativement. Son auteur a fait quelques très bonnes présentations sur le sujet que vous pouvez trouver sur YouTube.
la source
newtype Vec2 a = V2 (a,a)
,newtype Vec3 a = V3 (a,a,a)
et ainsi de suite, mais ce n'est pas ce que la question pose.)Pi (x : A). B
qui est une fonction deA
àB x
oùx
est l'argument de la fonction. Ici, le type de retour de la fonction dépend de l' expression fournie en argument. Cependant, tout cela peut être effacé, c'est seulement le temps de compilation