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 ...
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
import Prelude hiding (foldr, zipWith)
import qualified Prelude
import Data.Type.Equality
import Data.Foldable
import Data.Traversable
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 écrit S (S (S Z))
.
data Nat = Z | S Nat
Avec l' extension DataKinds , cette data
déclaration introduit un type appelé Nat
et deux constructeurs de type appelés S
et Z
- en d'autres termes, nous avons des nombres naturels au niveau du type . Notez que les types S
et Z
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 un Z
ou un S
type) pour représenter sa longueur.
data Vec :: Nat -> * -> * where
VNil :: Vec Z a
VCons :: a -> Vec n a -> Vec (S n) a
deriving instance (Show a) => Show (Vec n a)
deriving instance Functor (Vec n)
deriving instance Foldable (Vec n)
deriving instance Traversable (Vec n)
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 de Z
(ero), soit c'est une VCons
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 type n
. 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:
ghci> :t (VCons 'a' (VCons 'b' VNil))
(VCons 'a' (VCons 'b' VNil)) :: Vec ('S ('S 'Z)) Char -- (S (S Z)) means 2
ghci> :t (VCons 13 (VCons 11 (VCons 3 VNil)))
(VCons 13 (VCons 11 (VCons 3 VNil))) :: Num a => Vec ('S ('S ('S 'Z))) a -- (S (S (S Z))) means 3
Le produit scalaire procède comme pour une liste:
-- note that the two Vec arguments are declared to have the same length
vap :: Vec n (a -> b) -> Vec n a -> Vec n b
vap VNil VNil = VNil
vap (VCons f fs) (VCons x xs) = VCons (f x) (vap fs xs)
zipWith :: (a -> b -> c) -> Vec n a -> Vec n b -> Vec n c
zipWith f xs ys = fmap f xs `vap` ys
dot :: Num a => Vec n a -> Vec n a -> a
dot xs ys = foldr (+) 0 $ zipWith (*) xs ys
vap
, qui applique «zippement» un vecteur de fonctions à un vecteur d 'arguments, est son Vec
applicatif <*>
; Je ne l'ai pas mis dans un Applicative
exemple parce que ça devient salissant . Notez également que j'utilise le à foldr
partir de l'instance générée par le compilateur de Foldable
.
Essayons-le:
ghci> let v1 = VCons 2 (VCons 1 VNil)
ghci> let v2 = VCons 4 (VCons 5 VNil)
ghci> v1 `dot` v2
13
ghci> let v3 = VCons 8 (VCons 6 (VCons 1 VNil))
ghci> v1 `dot` v3
<interactive>:20:10:
Couldn't match type ‘'S 'Z’ with ‘'Z’
Expected type: Vec ('S ('S 'Z)) a
Actual type: Vec ('S ('S ('S 'Z))) a
In the second argument of ‘dot’, namely ‘v3’
In the expression: v1 `dot` v3
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:
-- This won't compile because the type checker can't deduce the length of the returned vector
-- VNil +++ ys = ys
-- (VCons x xs) +++ ys = VCons x (concat xs ys)
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 :
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
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 deux Nat
s.) Maintenant, nous pouvons faire +++
compiler:
infixr 5 +++
(+++) :: Vec n a -> Vec m a -> Vec (n :+: m) a
VNil +++ ys = ys
(VCons x xs) +++ ys = VCons x (concat xs ys)
Voici comment vous l'utilisez:
ghci> VCons 1 (VCons 2 VNil) +++ VCons 3 (VCons 4 VNil)
VCons 1 (VCons 2 (VCons 3 (VCons 4 VNil)))
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:
-- this won't work because there aren't any values of type `S` and `Z`
-- split :: (n :: Nat) -> Vec (n :+: m) a -> (Vec n a, Vec m a)
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 cela DataKinds
ne nous donne que des constructeurs de type promus. Pour le dire autrement, les constructeurs de types S
et Z
n'apparaissent pas au niveau de la valeur. Nous devrons nous contenter des valeurs singleton pour une représentation d'exécution d'un certain Nat
. *
data Natty (n :: Nat) where
Zy :: Natty Z -- pronounced 'zed-y'
Sy :: Natty n -> Natty (S n) -- pronounced 'ess-y'
deriving instance Show (Natty n)
Pour un type donné n
(avec kind Nat
), il existe précisément un terme de type Natty n
. Nous pouvons utiliser la valeur singleton comme témoin d'exécution pour n
: l'apprentissage d'un Natty
nous apprend son n
et vice versa.
split :: Natty n ->
Vec (n :+: m) a -> -- the input Vec has to be at least as long as the input Natty
(Vec n a, Vec m a)
split Zy xs = (Nil, xs)
split (Sy n) (Cons x xs) = let (ys, zs) = split n xs
in (Cons x ys, zs)
Prenons-le pour un tour:
ghci> split (Sy (Sy Zy)) (VCons 1 (VCons 2 (VCons 3 VNil)))
(VCons 1 (VCons 2 VNil), VCons 3 VNil)
ghci> split (Sy (Sy Zy)) (VCons 3 VNil)
<interactive>:116:21:
Couldn't match type ‘'S ('Z :+: m)’ with ‘'Z’
Expected type: Vec ('S ('S 'Z) :+: m) a
Actual type: Vec ('S 'Z) a
Relevant bindings include
it :: (Vec ('S ('S 'Z)) a, Vec m a) (bound at <interactive>:116:1)
In the second argument of ‘split’, namely ‘(VCons 3 VNil)’
In the expression: split (Sy (Sy Zy)) (VCons 3 VNil)
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 comme Natty
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.
data AVec a = forall n. AVec (Natty n) (Vec n a)
deriving instance (Show a) => Show (AVec a)
fromList :: [a] -> AVec a
fromList = Prelude.foldr cons nil
where cons x (AVec n xs) = AVec (Sy n) (VCons x xs)
nil = AVec Zy VNil
AVec
est un type existentiel : la variable type n
n'apparaît pas dans le type de retour du AVec
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 - le Natty 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.
ghci> fromList [1,2,3]
AVec (Sy (Sy (Sy Zy))) (VCons 1 (VCons 2 (VCons 3 Nil)))
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:
filter :: (a -> Bool) -> Vec n a -> AVec a
filter f = foldr (\x (AVec n xs) -> if f x
then AVec (Sy n) (VCons x xs)
else AVec n xs) (AVec Zy VNil)
A dot
deux AVec
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:
data (a :: k) :~: (b :: k) where
Refl :: a :~: a -- short for 'reflexivity'
Lorsque vous faites correspondre des motifs Refl
, GHC le sait a ~ b
. Il existe également quelques fonctions pour vous aider à travailler avec ce type: nous utiliserons gcastWith
pour convertir entre des types équivalents et TestEquality
pour déterminer si deux Natty
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 sur S
):
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
La correspondance des motifs sur Refl
le côté gauche permet au GHC de le savoir n ~ m
. Avec cette connaissance, c'est trivial S n ~ S m
, alors GHC nous permet de retourner un nouveau Refl
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 simplement Nothing
.)
instance TestEquality Natty where
-- testEquality :: Natty n -> Natty m -> Maybe (n :~: m)
testEquality Zy Zy = Just Refl
testEquality (Sy n) (Sy m) = fmap congSuc (testEquality n m) -- check whether the predecessors are equal, then make use of congruence
testEquality Zy _ = Nothing
testEquality _ Zy = Nothing
Maintenant, nous pouvons assembler les pièces en dot
une paire de AVec
s de longueur inconnue.
dot' :: Num a => AVec a -> AVec a -> Maybe a
dot' (AVec n u) (AVec m v) = fmap (\proof -> gcastWith proof (dot u v)) (testEquality n m)
Tout d'abord, le modèle correspond au AVec
constructeur pour extraire une représentation d'exécution des longueurs des vecteurs. Utilisez maintenant testEquality
pour déterminer si ces longueurs sont égales. S'ils le sont, nous aurons Just Refl
; gcastWith
utilisera cette preuve d'égalité pour s'assurer qu'elle dot u v
est bien typée en libérant son n ~ m
hypothèse implicite .
ghci> let v1 = fromList [1,2,3]
ghci> let v2 = fromList [4,5,6]
ghci> let v3 = fromList [7,8]
ghci> dot' v1 v2
Just 32
ghci> dot' v1 v3
Nothing -- they weren't the same length
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'appeler dot
, vous devez avoir testé si les listes d'entrée ont la même longueur en utilisant testEquality
. Je suis enclin à obtenir des if
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 de dot'
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 Haskell Either 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".