Le absurd
function dans Data.Void
a la signature suivante, où Void
est le type logiquement inhabité exporté par ce package:
-- | Since 'Void' values logically don't exist, this witnesses the logical
-- reasoning tool of \"ex falso quodlibet\".
absurd :: Void -> a
Je connais suffisamment de logique pour obtenir la remarque de la documentation selon laquelle cela correspond, par la correspondance propositions-as-types, à la formule valide ⊥ → a
.
Ce qui me rend perplexe et curieux est la suivante: dans quels types de problèmes de programmation pratiques cette fonction est-elle utile? Je pense que c'est peut-être utile dans certains cas en tant que moyen sûr de manipuler de manière exhaustive les cas "ne peut pas arriver", mais je ne connais pas suffisamment les utilisations pratiques de Curry-Howard pour dire si cette idée est valable. bonne voie du tout.
EDIT: Des exemples de préférence en Haskell, mais si quelqu'un veut utiliser un langage typé de manière dépendante, je ne vais pas me plaindre ...
La vie est un peu dure, puisque Haskell n'est pas strict. Le cas d'utilisation général consiste à gérer des chemins impossibles. Par exemple
simple :: Either Void a -> a
simple (Left x) = absurd x
simple (Right y) = y
Cela s'avère être quelque peu utile. Considérons un type simple pour Pipes
data Pipe a b r
= Pure r
| Await (a -> Pipe a b r)
| Yield !b (Pipe a b r)
il s'agit d'une version simplifiée et stricte du type de tuyaux standard de la bibliothèque Pipes
de Gabriel Gonzales. Maintenant, nous pouvons encoder un tuyau qui ne cède jamais (c’est-à-dire un consommateur) en tant que
type Consumer a r = Pipe a Void r
cela ne cède jamais vraiment. Cela implique que la règle de pliage appropriée pour un Consumer
est
foldConsumer :: (r -> s) -> ((a -> s) -> s) -> Consumer a r -> s
foldConsumer onPure onAwait p
= case p of
Pure x -> onPure x
Await f -> onAwait $ \x -> foldConsumer onPure onAwait (f x)
Yield x _ -> absurd x
ou bien, vous pouvez ignorer le cas de rendement lorsque vous traitez avec des consommateurs. Voici la version générale de ce modèle de conception: utilisez les types de données polymorphes et Void
pour vous débarrasser des possibilités lorsque vous en avez besoin.
L’utilisation la plus classique de Void
se trouve probablement dans CPS.
type Continuation a = a -> Void
c'est-à-dire qu'une Continuation
est une fonction qui ne revient jamais. Continuation
est la version type de "not". De là, nous obtenons une monade de CPS (correspondant à la logique classique)
newtype CPS a = Continuation (Continuation a)
puisque Haskell est pur, nous ne pouvons rien obtenir de ce type.
Considérons cette représentation des termes lambda paramétrés par leurs variables libres. (Voir les articles de Bellegarde et Hook 1994, Bird et Paterson 1999, Altenkirch et Reus 1999.)
data Tm a = Var a
| Tm a :$ Tm a
| Lam (Tm (Maybe a))
Vous pouvez certainement en faire un Functor
, capturant la notion de changement de nom, et un Monad
capturant la notion de substitution.
instance Functor Tm where
fmap rho (Var a) = Var (rho a)
fmap rho (f :$ s) = fmap rho f :$ fmap rho s
fmap rho (Lam t) = Lam (fmap (fmap rho) t)
instance Monad Tm where
return = Var
Var a >>= sig = sig a
(f :$ s) >>= sig = (f >>= sig) :$ (s >>= sig)
Lam t >>= sig = Lam (t >>= maybe (Var Nothing) (fmap Just . sig))
Considérons maintenant les termes closed: ce sont les habitants de Tm Void
. Vous devriez pouvoir intégrer les termes fermés à des termes avec des variables libres arbitraires. Comment?
fmap absurd :: Tm Void -> Tm a
Le problème, bien sûr, est que cette fonction traverse le terme sans rien faire. Mais c'est une touche plus honnête que unsafeCoerce
. Et c’est pourquoi vacuous
a été ajouté à Data.Void
...
Ou écrivez à un évaluateur. Voici les valeurs avec des variables libres dans b
.
data Val b
= b :$$ [Val b] -- a stuck application
| forall a. LV (a -> Val b) (Tm (Maybe a)) -- we have an incomplete environment
Je viens de représenter les lambdas comme des fermetures. L'évaluateur est paramétré par un environnement mappant des variables libres dans a
à des valeurs supérieures à b
.
eval :: (a -> Val b) -> Tm a -> Val b
eval g (Var a) = g a
eval g (f :$ s) = eval g f $$ eval g s where
(b :$$ vs) $$ v = b :$$ (vs ++ [v]) -- stuck application gets longer
LV g t $$ v = eval (maybe v g) t -- an applied lambda gets unstuck
eval g (Lam t) = LV g t
Tu l'as deviné. Pour évaluer un terme fermé sur n'importe quelle cible
eval absurd :: Tm Void -> Val b
Plus généralement, Void
est rarement utilisé seul, mais il est pratique lorsque vous souhaitez instancier un paramètre de type de manière à indiquer une sorte d'impossibilité (par exemple, ici, en utilisant une variable libre dans un terme fermé). Ces types paramétrés s'accompagnent souvent de fonctions d'ordre supérieur, qui soulèvent des opérations sur les paramètres pour des opérations sur le type entier (par exemple, ici, fmap
, >>=
, eval
). Donc, vous passez absurd
comme opération polyvalente sur Void
.
Pour un autre exemple, imaginez que vous utilisiez Either e v
pour capturer des calculs qui, espérons-le, vous donneront un v
, mais qui pourraient générer une exception de type e
. Vous pouvez utiliser cette approche pour documenter le risque de mauvais comportement de manière uniforme. Pour des calculs parfaitement sains dans ce paramètre, prenez e
pour être Void
, puis utilisez
either absurd id :: Either Void v -> v
courir en toute sécurité ou
either absurd Right :: Either Void v -> Either e v
intégrer des composants sûrs dans un monde dangereux.
Oh, et un dernier bravo, gérer un "ne peut pas arriver". Il apparaît dans la construction de fermeture à glissière générique, partout où le curseur ne peut pas être.
class Differentiable f where
type D f :: * -> * -- an f with a hole
plug :: (D f x, x) -> f x -- plugging a child in the hole
newtype K a x = K a -- no children, just a label
newtype I x = I x -- one child
data (f :+: g) x = L (f x) -- choice
| R (g x)
data (f :*: g) x = f x :&: g x -- pairing
instance Differentiable (K a) where
type D (K a) = K Void -- no children, so no way to make a hole
plug (K v, x) = absurd v -- can't reinvent the label, so deny the hole!
J'ai décidé de ne pas supprimer le reste, même si ce n'est pas tout à fait pertinent.
instance Differentiable I where
type D I = K ()
plug (K (), x) = I x
instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
type D (f :+: g) = D f :+: D g
plug (L df, x) = L (plug (df, x))
plug (R dg, x) = R (plug (dg, x))
instance (Differentiable f, Differentiable g) => Differentiable (f :*: g) where
type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
plug (L (df :&: g), x) = plug (df, x) :&: g
plug (R (f :&: dg), x) = f :&: plug (dg, x)
En fait, c'est peut-être pertinent. Si vous vous sentez aventureux, cet article inachevé montre comment utiliser Void
pour compresser la représentation des termes avec des variables libres
data Term f x = Var x | Con (f (Term f x)) -- the Free monad, yet again
dans toute syntaxe générée librement à partir d'un foncteur Differentiable
et Traversable
f
. Nous utilisons Term f Void
pour représenter des régions sans variables libres et [D f (Term f Void)]
pour représenter tubes tunnel à travers des régions sans variables libres soit vers une variable libre isolée, soit vers une jonction dans les chemins d'accès à deux variables libres ou plus. Doit finir cet article parfois.
Pour un type sans valeur (ou du moins, aucune valeur ne mérite d'être mentionnée en société polie), Void
est remarquablement utile. Et absurd
est la façon dont vous l'utilisez.
Je pense que c'est peut-être utile dans certains cas en tant que moyen sûr de traiter de manière exhaustive les cas "ne peut pas arriver"
C'est justement ça.
Vous pourriez dire que absurd
n'est pas plus utile que const (error "Impossible")
. Cependant, il est de type restreint, de sorte que sa seule entrée peut être du type Void
, un type de données intentionnellement laissé inhabité. Cela signifie qu'il n'y a pas de valeur réelle que vous pouvez transmettre à absurd
. Si vous vous retrouvez dans une branche de code où le vérificateur de types pense que vous avez accès à quelque chose de type Void
, alors, eh bien, vous êtes dans une situation absurd. Vous utilisez donc simplement absurd
pour indiquer en gros que cette branche de code ne doit jamais être atteinte.
"Ex falso quodlibet" signifie littéralement "de [une] [proposition] fausse, tout ce qui suit". Ainsi, lorsque vous constatez que vous détenez une donnée dont le type est Void
, vous savez que vous avez de fausses preuves entre vos mains. Vous pouvez donc remplir tout trou de votre choix (via absurd
), car une fausse proposition entraîne tout ce qui suit.
J'ai écrit un article sur les idées derrière Conduit qui présente un exemple d'utilisation de absurd
.
En général, vous pouvez l'utiliser pour éviter les correspondances de motifs apparemment partielles. Par exemple, extraire une approximation des déclarations de type de données à partir de this answer :
data RuleSet a = Known !a | Unknown String
data GoRuleChoices = Japanese | Chinese
type LinesOfActionChoices = Void
type GoRuleSet = RuleSet GoRuleChoices
type LinesOfActionRuleSet = RuleSet LinesOfActionChoices
Ensuite, vous pouvez utiliser absurd
comme ceci, par exemple:
handleLOARules :: (String -> a) -> LinesOfActionsRuleSet -> a
handleLOARules f r = case r of
Known a -> absurd a
Unknown s -> f s
Il existe différentes manières de représenter le type de données vide . L'un est un type de données algébrique vide. Vous pouvez également en faire un alias pour α.α ou
type Void' = forall a . a
en Haskell - c’est ainsi que nous pouvons le coder dans System F (voir le chapitre 11 de Essais et types ). Ces deux descriptions sont bien sûr isomorphes et l’isomorphisme est observé par \x -> x :: (forall a.a) -> Void
et par absurd :: Void -> a
.
Dans certains cas, nous préférons la variante explicite, généralement si le type de données vide apparaît dans un argument d'une fonction ou dans un type de données plus complexe, tel que dans Data.Conduit :
type Sink i m r = Pipe i i Void () m r
Dans certains cas, nous préférons la variante polymorphe, généralement le type de données vide est impliqué dans le type de retour d'une fonction.
absurd
survient lors de la conversion entre ces deux représentations.
Par exemple, callcc :: ((a -> m b) -> m a) -> m a
utilise (implicitement) forall b
. Il pourrait aussi bien s'agir de type ((a -> m Void) -> m a) -> m a
, car un appel à la contination ne retourne pas réellement, il transfère le contrôle sur un autre point. Si nous voulions travailler avec des continuations, nous pourrions définir
type Continuation r a = a -> Cont r Void
(Nous pourrions utiliser type Continuation' r a = forall b . a -> Cont r b
mais cela nécessiterait des types de rang 2.) Ensuite, vacuousM
convertit ce Cont r Void
en Cont r b
.
(Notez également que vous pouvez utiliser haskellers.com pour rechercher une utilisation (dépendances inverses) d'un certain paquet, comme pour voir qui et comment utilise le paquet void.)
Dans les langages à caractères dépendants comme Idris, c'est probablement plus utile que dans Haskell. En règle générale, dans une fonction totale, lorsque vous faites correspondre une valeur au modèle avec une valeur impossible à insérer dans la fonction, vous devez ensuite construire une valeur de type inhabité et utiliser absurd
pour finaliser la définition de cas.
Par exemple, cette fonction supprime un élément d'une liste avec la contrainte de coût au niveau du type qui y est présente:
shrink : (xs : Vect (S n) a) -> Elem x xs -> Vect n a
shrink (x :: ys) Here = ys
shrink (y :: []) (There p) = absurd p
shrink (y :: (x :: xs)) (There p) = y :: shrink (x :: xs) p
Là où le second cas dit qu'il y a un certain élément dans une liste vide, ce qui est, bien absurde. En général, cependant, le compilateur ne le sait pas et nous devons souvent être explicites. Ensuite, le compilateur peut vérifier que la définition de la fonction n’est pas partielle et nous obtenons des garanties de compilation plus solides.
Du point de vue de Curry-Howard, où sont les propositions, alors absurd
est en quelque sorte le CQFD dans une preuve par contradiction.