Dans un langage purement fonctionnel, la seule chose que vous pouvez faire avec une valeur est de lui appliquer une fonction.
En d'autres termes, si vous voulez faire quelque chose d'intéressant avec une valeur de type a
vous avez besoin d'une fonction (par exemple) de type f :: a -> b
puis l'appliquer. Si quelqu'un vous remet (flip apply) a
avec le type (a -> b) -> b
, est-ce un remplacement approprié pour a
?
Et comment appelleriez-vous quelque chose de type (a -> b) -> b
? Vu qu'il semble être un remplaçant pour un a
, je serais tenté de l'appeler un proxy, ou quelque chose de http://www.thesaurus.com/browse/proxy .
la réponse de luqui est excellente mais je vais offrir une autre explication de forall b. (a -> b) -> b === a
pour deux raisons: Premièrement, parce que je pense que la généralisation à Codensity est un peu trop enthousiaste. Et deuxièmement, parce que c'est l'occasion de lier un tas de choses intéressantes ensemble. À partir de!
Imaginez que quelqu'un lance une pièce puis la mette dans une boîte magique. Vous ne pouvez pas voir à l'intérieur de la boîte mais si vous choisissez un type b
et passez à la boîte une fonction avec le type Bool -> b
, La boîte crachera un b
. Que pouvons-nous apprendre sur cette boîte sans regarder à l'intérieur? Pouvons-nous savoir quel est l'état de la pièce? Pouvons-nous apprendre quel mécanisme la boîte utilise pour produire le b
? Il s'avère que nous pouvons faire les deux.
Nous pouvons définir la boîte comme une fonction rang 2 de type Box Bool
Où
type Box a = forall b. (a -> b) -> b
(Ici, le type de rang 2 signifie que le fabricant de boîtes choisit a
et l'utilisateur de boîtes choisit b
.)
Nous mettons le a
dans la boîte puis nous fermons la boîte, créant ... a fermeture.
-- Put the a in the box.
box :: a -> Box a
box a f = f a
Par exemple, box True
. Une application partielle n'est qu'un moyen intelligent de créer des fermetures!
Maintenant, est-ce que les pièces de monnaie ou les queues? Puisque moi, l'utilisateur de la boîte, suis autorisé à choisir b
, je peux choisir Bool
et passer une fonction Bool -> Bool
. Si je choisis id :: Bool -> Bool
, La question est: la boîte crachera-t-elle la valeur qu'elle contient? La réponse est que la boîte crachera la valeur qu'elle contient ou qu'elle crachera un non-sens (une valeur inférieure comme undefined
). En d'autres termes, si vous obtenez une réponse, cette réponse doit être correcte.
-- Get the a out of the box.
unbox :: Box a -> a
unbox f = f id
Parce que nous ne pouvons pas générer de valeurs arbitraires dans Haskell, la seule chose sensée que la boîte puisse faire est d'appliquer la fonction donnée à la valeur qu'elle cache. Ceci est une conséquence du polymorphisme paramétrique, également connu sous le nom parametricity.
Maintenant, pour montrer que Box a
Est isomorphe à a
, nous devons prouver deux choses à propos de la boxe et du déballage. Nous devons prouver que vous sortez ce que vous mettez et que vous pouvez mettre ce que vous sortez.
unbox . box = id
box . unbox = id
Je vais faire le premier et laisser le second comme exercice au lecteur.
unbox . box
= {- definition of (.) -}
\b -> unbox (box b)
= {- definition of unbox and (f a) b = f a b -}
\b -> box b id
= {- definition of box -}
\b -> id b
= {- definition of id -}
\b -> b
= {- definition of id, backwards -}
id
(Si ces preuves semblent plutôt triviales, c'est parce que toutes les fonctions polymorphes (totales) dans Haskell sont des transformations naturelles et ce que nous prouvons ici est la naturalité. Paramétrie nous fournit une fois de plus des théorèmes pour des prix bas, bas!)
En aparté et un autre exercice pour le lecteur, pourquoi ne puis-je pas réellement définir rebox
avec (.)
?
rebox = box . unbox
Pourquoi dois-je incorporer la définition de (.)
Moi-même comme une sorte de caverne?
rebox :: Box a -> Box a
rebox f = box (unbox f)
(Astuce: quels sont les types de box
, unbox
et (.)
?)
Maintenant, comment généraliser Box
? luqui utilise Codensity : les deux b
s sont généralisés par un constructeur de type arbitraire que nous appellerons f
. Il s'agit de la Codensity transform of f a
.
type CodenseBox f a = forall b. (a -> f b) -> f b
Si nous corrigeons f ~ Identity
, Nous récupérons Box
. Cependant, il existe une autre option: nous ne pouvons frapper que le type de retour avec f
:
type YonedaBox f a = forall b. (a -> b) -> f b
(J'ai en quelque sorte donné le jeu ici sous ce nom mais nous y reviendrons.) Nous pouvons également corriger f ~ Identity
Ici pour récupérer Box
, mais nous laissons l'utilisateur de la boîte passer dans une fonction normale plutôt qu'une flèche de Kleisli. Pour comprendre ce que nous généralisons, regardons à nouveau la définition de box
:
box a f = f a
Eh bien, c'est juste flip ($)
, n'est-ce pas? Et il s'avère que nos deux autres boîtes sont construites en généralisant ($)
: CodenseBox
est une liaison monadique inversée partiellement appliquée et YonedaBox
est une flip fmap
. (Cela explique également pourquoi Codensity f
Est un Monad
et Yoneda f
Est un Functor
pour tout choix de f
: La seule façon d'en créer un est de fermer respectivement une liaison ou une fmap.) De plus, ces deux concepts de la théorie des catégories ésotériques sont vraiment des généralisations d'un concept qui est familier à de nombreux programmeurs qui travaillent: la transformation CPS!
En d'autres termes, YonedaBox
est l'incorporation Yoneda et les lois box
/unbox
correctement abstraites pour YonedaBox
sont la preuve du lemme Yoneda!
TL; DR:
forall b. (a -> b) -> b === a
est une instance du lemme de Yoneda.
Cette question est une fenêtre sur un certain nombre de concepts plus profonds.
Tout d'abord, notez qu'il y a une ambiguïté dans cette question. Est-ce que nous voulons dire le type forall b. (a -> b) -> b
, de sorte que nous pouvons instancier b
avec le type que nous aimons, ou voulons-nous dire (a -> b) -> b
pour certains b
spécifiques que nous ne pouvons pas choisir.
Nous pouvons formaliser cette distinction dans Haskell ainsi:
newtype Cont b a = Cont ((a -> b) -> b)
newtype Cod a = Cod (forall b. (a -> b) -> b)
Ici, nous voyons du vocabulaire. Le premier type est la monade Cont
, le second est Codensity
Identity
, bien que ma familiarité avec le dernier terme ne soit pas assez forte pour dire ce que vous devriez appeler cela en anglais.
Cont b a
ne peut pas être équivalent à a
à moins que a -> b
peut contenir au moins autant d'informations que a
(voir le commentaire de Dan Robertson ci-dessous). Ainsi, par exemple, notez que vous ne pouvez jamais rien retirer de Cont
Void
a
.
Cod a
est équivalent à a
. Pour voir cela, il suffit de voir l'isomorphisme:
toCod :: a -> Cod a
fromCod :: Cod a -> a
dont les implémentations je vais laisser comme un exercice. Si vous voulez vraiment le faire, vous pouvez essayer de prouver que cette paire est vraiment un isomorphisme. fromCod . toCod = id
c'est facile, mais toCod . fromCod = id
nécessite le théorème libre pour Cod
.
Les autres réponses ont fait un excellent travail décrivant la relation entre les types forall b . (a -> b) -> b
et a
mais je voudrais souligner une mise en garde car cela conduit à des questions ouvertes intéressantes que j'ai été travaille sur.
Techniquement, forall b . (a -> b) -> b
et a
sont pas isomorphes dans une langue comme Haskell qui (1) vous permet d'écrire une expression qui ne se termine pas et ( 2) est appelé par valeur (strict) ou contient seq
. Mon point ici est de ne pas être tatillon ou montrer que la paramétricité est affaiblie dans Haskell (comme cela est bien connu) mais qu'il peut y avoir des façons intéressantes de renforcer et en quelque sorte récupérer des isomorphismes comme celui-ci.
Il existe certains termes de type forall b . (a -> b) -> b
qui ne peuvent pas être exprimés sous la forme d'un a
. Pour voir pourquoi, commençons par regarder la preuve que Rein a laissée comme exercice, box . unbox = id
. Il s'avère que cette preuve est en fait plus intéressante que celle de sa réponse, car elle repose sur la paramétricité d'une manière cruciale.
box . unbox
= {- definition of (.) -}
\m -> box (unbox m)
= {- definition of box -}
\m f -> f (unbox m)
= {- definition of unbox -}
\m f -> f (m id)
= {- free theorem: f (m id) = m f -}
\m f -> m f
= {- eta: (\f -> m f) = m -}
\m -> m
= {- definition of id, backwards -}
id
L'étape intéressante, où la paramétricité entre en jeu, est l'application du théorème libref (m id) = m f
. Cette propriété est une conséquence de forall b . (a -> b) -> b
, le type de m
. Si nous considérons m
comme une boîte avec une valeur sous-jacente de type a
à l'intérieur, alors la seule chose que m
peut faire avec son argument est de l'appliquer à cette valeur sous-jacente et retourner le résultat. Sur le côté gauche, cela signifie que f (m id)
extrait la valeur sous-jacente de la boîte et la transmet à f
. À droite, cela signifie que m
applique f
directement à la valeur sous-jacente.
Malheureusement, ce raisonnement ne tient pas tout à fait lorsque nous avons des termes comme m
et f
ci-dessous.
m :: (Bool -> b) -> b
m k = seq (k true) (k false)
f :: Bool -> Int
f x = if x then ⊥ else 2`
Rappelons que nous voulions montrer f (m id) = m f
f (m id)
= {- definition f -}
if (m id) then ⊥ else 2
= {- definition of m -}
if (seq (id true) (id false)) then ⊥ else 2
= {- definition of id -}
if (seq true (id false)) then ⊥ else 2
= {- definition of seq -}
if (id false) then ⊥ else 2
= {- definition of id -}
if false then ⊥ else 2
= {- definition of if -}
2
m f
= {- definition of m -}
seq (f true) (f false)
= {- definition of f -}
seq (if true then ⊥ else 2) (f false)
= {- definition of if -}
seq ⊥ (f false)
= {- definition of seq -}
⊥
De toute évidence, 2
N'est pas égal à ⊥
, Nous avons donc perdu notre théorème libre et l'isomorphisme entre a
et (a -> b) -> b
Avec lui. Mais que s'est-il passé exactement? Essentiellement, m
n'est pas seulement une boîte bien comportée car il applique son argument à deux valeurs sous-jacentes différentes (et utilise seq
pour garantir que ces deux applications sont réellement évaluées), ce que nous pouvons observer en passant dans une suite qui se termine sur l'une de ces valeurs sous-jacentes, mais pas sur l'autre. En d'autres termes, m id = false
N'est pas vraiment une représentation fidèle de m
en tant que Bool
car il "oublie" le fait que m
appelle son entrée avec les deux true
et false
.
Le problème est le résultat de l'interaction entre trois choses:
forall b . (a -> b) -> b
peuvent appliquer leur entrée plusieurs fois.Il n'y a pas beaucoup d'espoir de contourner les points 1 ou 2. types linéaires peut cependant nous donner l'occasion de lutter contre le troisième problème. Une fonction linéaire de type a ⊸ b
Est une fonction du type a
au type b
qui doit utiliser son entrée exactement une fois. Si nous exigeons que m
ait le type forall b . (a -> b) ⊸ b
, cela exclut notre contre-exemple du théorème libre et devrait nous montrer un isomorphisme entre a
et forall b . (a -> b) ⊸ b
même en présence de non-terminaison et seq
.
C'est vraiment cool! Il montre que la linéarité a la capacité de "sauver" des propriétés intéressantes en apprivoisant des effets qui peuvent rendre difficile le vrai raisonnement équationnel.
Un gros problème demeure cependant. Nous n'avons pas encore de techniques pour prouver le théorème libre dont nous avons besoin pour le type forall b . (a -> b) ⊸ b
. Il s'avère que les relations logiques actuelles (les outils que nous utilisons normalement pour faire de telles preuves) n'ont pas été conçues pour prendre en compte la linéarité de la manière qui est nécessaire. Ce problème a des implications pour établir l'exactitude des compilateurs qui effectuent des traductions CPS.