32 votes

Une monade de type `ST` peut-elle être exécutée purement (sans la bibliothèque `ST`) ?

Ce billet est du Haskell littéral. Il suffit de mettre dans un fichier comme "pad.lhs" et ghci sera en mesure de l'exécuter.

> {-# LANGUAGE GADTs, Rank2Types #-}
> import Control.Monad
> import Control.Monad.ST
> import Data.STRef

Ok, donc j'ai pu comprendre comment représenter le ST en code pur. Tout d'abord, nous commençons par notre type de référence. Sa valeur spécifique n'est pas vraiment importante. La chose la plus importante est que PT s a ne doit pas être isomorphe à tout autre type forall s . (En particulier, il devrait être isomorphe à ni . () ni Void .)

> newtype PTRef s a = Ref {unref :: s a} -- This is defined liked this to make `toST'` work. It may be given a different definition.

Le genre pour s est *->* mais ce n'est pas vraiment important pour le moment. Cela pourrait être polythène pour ce qui nous concerne.

> data PT s a where
>     MkRef   :: a -> PT s (PTRef s a)
>     GetRef  :: PTRef s a -> PT s a
>     PutRef  :: a -> PTRef s a -> PT s ()
>     AndThen :: PT s a -> (a -> PT s b) -> PT s b

C'est assez simple. AndThen nous permet de l'utiliser comme un Monad . Vous vous demandez peut-être comment return est mis en œuvre. Voici son instance de monade (elle ne respecte les lois de la monade que par rapport à runPF à définir ultérieurement) :

> instance Monad (PT s) where
>     (>>=)    = AndThen
>     return a = AndThen (MkRef a) GetRef --Sorry. I like minimalism.
> instance Functor (PT s) where
>     fmap = liftM
> instance Applicative (PT s) where
>     pure  = return
>     (<*>) = ap

Nous pouvons maintenant définir fib comme un cas de test.

> fib :: Int -> PT s Integer
> fib n = do
>     rold <- MkRef 0
>     rnew <- MkRef 1
>     replicateM_ n $ do
>         old <- GetRef rold
>         new <- GetRef rnew
>         PutRef      new  rold
>         PutRef (old+new) rnew
>     GetRef rold

Et ça fait des contrôles de type. Hourra ! Maintenant, j'ai pu convertir ceci en ST (nous voyons maintenant pourquoi s devait être * -> * )

> toST :: PT (STRef s) a -> ST s a
> toST (MkRef  a        ) = fmap Ref $ newSTRef a
> toST (GetRef   (Ref r)) = readSTRef r
> toST (PutRef a (Ref r)) = writeSTRef r a
> toST (pa `AndThen` apb) = (toST pa) >>= (toST . apb)

Maintenant nous pouvons définir une fonction pour exécuter PT sans référence ST du tout :

> runPF :: (forall s. PT s a) -> a
> runPF p = runST $ toST p

runPF $ fib 7 donne 13 ce qui est correct.


Ma question est de savoir si on peut définir runPF sans référence en utilisant ST du tout ?

Existe-t-il un moyen pur de définir runPF ? PTRef La définition d'un type n'a aucune importance ; ce n'est qu'un type de remplacement de toute façon. Il peut être redéfini comme bon lui semble.

Si vous ne peut pas définir runPF purement, donnez une preuve qu'il ne peut pas.

La performance n'est pas un problème (si c'était le cas, je n'aurais pas fait tous les travaux de construction). return ont leur propre référence).

Je pense que les types existentiels peuvent être utiles.

Note : C'est trivial si nous supposons que est a est dynamisable ou autre. Je cherche une réponse qui fonctionne avec tous les systèmes de gestion de l'information. a .

Note : En fait, une réponse n'a même pas nécessairement beaucoup à voir avec PT . Il faut juste qu'il soit aussi puissant que ST sans utiliser la magie. (Conversion de (forall s. PT s) est une sorte de test pour savoir si une réponse est valide ou non).

14voto

Benjamin Hodgson Points 3510

Tl;dr : Ce n'est pas possible sans ajustements à la définition de PT . Voici le problème principal : vous exécuterez votre calcul à état dans le contexte d'une sorte de support de stockage, mais ledit support de stockage doit savoir comment stocker les éléments suivants arbitraire types. Ce n'est pas possible sans emballer une sorte de preuve dans les MkRef un constructeur - soit une enveloppe existentielle Typeable comme d'autres l'ont suggéré, ou une preuve que la valeur appartient à l'un des types d'un ensemble fini connu.

Pour une première tentative, essayons d'utiliser une liste comme support de stockage et des entiers pour faire référence aux éléments de la liste.

newtype Ix a = MkIx Int  -- the index of an element in a list

interp :: PT Ix a -> State [b] a
interp (MkRef x) = modify (++ [x]) >> gets (Ref . MkIx . length)
-- ...

Lorsque nous stockons un nouvel élément dans l'environnement, nous veillons à l'ajouter à la fin de la liste, de sorte que Ref que nous avons donnés précédemment continuent de pointer vers le bon élément.

Ce n'est pas bien. Je peux faire une référence à tout type a mais le type de interp dit que le support de stockage est une liste homogène de b s. GHC nous donne raison quand il rejette cette signature de type, se plaignant qu'il ne peut pas correspondre à b avec le type de la chose à l'intérieur MkRef .


Ne vous laissez pas décourager, essayons d'utiliser une hétérogène comme environnement pour le State dans laquelle nous allons interpréter PT .

infixr 4 :>
data Tuple as where
    E :: Tuple '[]
    (:>) :: a -> Tuple as -> Tuple (a ': as)

C'est l'un de mes types de données Haskell préférés. C'est un tuple extensible indexé par une liste des types des objets qu'il contient. Les tuples sont des listes liées hétérogènes contenant des informations sur les types des objets qu'elles contiennent. (On l'appelle souvent HList suivant L'article de Kiselyov mais je préfère Tuple .) Lorsque vous ajoutez quelque chose au début d'un tuple, vous ajoutez son type au début de la liste des types. Dans une ambiance poétique, Je l'ai dit une fois de cette façon : "Le tuple et son type grandissent ensemble, comme une vigne rampant sur un bambou."

Exemples de Tuple s :

ghci> :t 'x' :> E
'x' :> E :: Tuple '[Char]
ghci> :t "hello" :> True :> E
"hello" :> True :> E :: Tuple '[[Char], Bool]

A quoi ressemblent les références aux valeurs à l'intérieur des tuples ? Nous devons prouver à GHC que le type de la chose que nous obtenons du tuple est bien le type que nous attendons.

data Elem as a where  -- order of indices arranged for convenient partial application
    Here :: Elem (a ': as) a
    There :: Elem as a -> Elem (b ': as) a

La définition de Elem est structurellement celui des nombres naturels ( Elem des valeurs comme There (There Here) ressemblent à des nombres naturels comme S (S Z) ) mais avec des types supplémentaires - dans ce cas, prouver que le type a est dans la liste de niveau de type as . Je le mentionne parce que c'est suggestif : Nat font de bons indices de liste, et de même Elem est utile pour indexer un tuple. À cet égard, elle sera utile pour remplacer la fonction Int dans notre type de référence.

(!) :: Tuple as -> Elem as a -> a
(x :> xs) ! Here = x
(x :> xs) ! (There ix) = xs ! ix

Nous avons besoin de quelques fonctions pour travailler avec les tuples et les indices.

type family as :++: bs where
    '[] :++: bs = bs
    (a ': as) :++: bs = a ': (as :++: bs)

appendT :: a -> Tuple as -> (Tuple (as :++: '[a]), Elem (as :++: '[a]) a)
appendT x E = (x :> E, Here)
appendT x (y :> ys) = let (t, ix) = appendT x ys
                      in (y :> t, There ix)

Essayons d'écrire un interpréteur pour une PT dans un Tuple l'environnement.

interp :: PT (Elem as) a -> State (Tuple as) a
interp (MkRef x) = do
    t <- get
    let (newT, el) = appendT x t
    put newT
    return el
-- ...

Je ne peux pas, mon pote. Le problème est que le type de Tuple dans l'environnement change lorsque nous obtenons une nouvelle référence. Comme je l'ai mentionné précédemment, ajouter quelque chose à un tuple ajoute son type à celui du tuple, un fait démenti par le type State (Tuple as) a . GHC n'est pas dupe de cette tentative de subterfuge : Could not deduce (as ~ (as :++: '[a1])) .


C'est là que les choses se gâtent, pour autant que je sache. Ce que l'on veut vraiment faire, c'est garder la taille du tuple constante tout au long de l'exécution d'un PT calcul. Pour cela, il faut indexer PT par la liste des types auxquels vous pouvez obtenir des références, en prouvant à chaque fois que vous le faites que vous êtes autorisé à le faire (en donnant un nom de type Elem valeur). L'environnement ressemblerait alors à un tuple de listes, et une référence serait constituée d'un Elem (pour sélectionner la bonne liste) et un Int (pour trouver l'élément particulier dans la liste).

Ce plan enfreint les règles, bien sûr (vous devez changer la définition de PT ), mais il présente également des problèmes d'ingénierie. Lorsque j'appelle MkRef c'est à moi de donner une réponse. Elem pour la valeur à laquelle je fais référence, ce qui est assez fastidieux. (Cela dit, vous pouvez généralement faire Elem les valeurs implicites avec une classe de type "hacky").

Autre chose : la composition PT devient difficile. Toutes les parties de votre calcul doivent être indexées par les même liste de types. Vous pourriez essayer d'introduire des combinateurs ou des classes qui vous permettent de faire croître l'environnement d'une PT mais vous devrez également mettre à jour toutes les références lorsque vous le ferez. L'utilisation de la monade serait assez difficile.

Une implémentation éventuellement plus propre permettrait à la liste des types d'un groupe d'utilisateurs d'avoir accès à l'information. PT varie au fur et à mesure que l'on se promène dans le type de données : chaque fois que l'on rencontre une MkRef le type s'allonge. Étant donné que le type du calcul change au fur et à mesure qu'il progresse, vous ne pouvez pas utiliser une monade ordinaire - vous devez avoir recours à IxMonad . Si vous voulez savoir à quoi ressemble ce programme, voyez mon autre réponse .

En fin de compte, le point délicat est que le type du tuple est déterminé par la valeur de l'attribut PT demande. L'environnement est ce qu'une requête donnée décide d'y stocker. interp ne peut pas choisir le contenu du tuple, il doit provenir d'un index de l'application PT . Toute tentative de contourner cette exigence est vouée à l'échec. Maintenant, dans un vrai système typé de manière dépendante, nous pourrions examiner la PT valeur qui nous a été donnée et de déterminer ce que as devrait être. Hélas, Haskell n'est pas un système typé de manière dépendante.

11voto

András Kovács Points 5385

Une solution simple consiste à envelopper un State et présentent la même API que ST . Dans ce cas, il n'est pas nécessaire de stocker les informations relatives au type d'exécution, puisqu'elles peuvent être déterminées à partir du type de l'élément STRef -et les habituels ST s L'astuce de la quantification permet d'éviter que les utilisateurs n'altèrent le conteneur qui stocke les références.

Nous gardons ref-s dans un IntMap et incrémenter un compteur à chaque fois que l'on alloue une nouvelle référence. La lecture et l'écriture modifient simplement le IntMap avec quelques unsafeCoerce saupoudré sur le dessus.

{-# LANGUAGE DeriveFunctor, GeneralizedNewtypeDeriving, RankNTypes, RoleAnnotations #-}

module PureST (ST, STRef, newSTRef, readSTRef, modifySTRef, runST) where

import Data.IntMap (IntMap, (!))
import qualified Data.IntMap as M

import Control.Monad
import Control.Applicative
import Control.Monad.Trans.State
import GHC.Prim (Any)
import Unsafe.Coerce (unsafeCoerce)

type role ST nominal representational
type role STRef nominal representational
newtype ST s a = ST (State (IntMap Any, Int) a) deriving (Functor, Applicative, Monad)
newtype STRef s a = STRef Int deriving Show

newSTRef :: a -> ST s (STRef s a)
newSTRef a = ST $ do
  (m, i) <- get
  put (M.insert i (unsafeCoerce a) m, i + 1)
  pure (STRef i)

readSTRef :: STRef s a -> ST s a
readSTRef (STRef i) = ST $ do
  (m, _) <- get
  pure (unsafeCoerce (m ! i))

writeSTRef :: STRef s a -> a -> ST s ()
writeSTRef (STRef i) a = ST $ 
  modify $ \(m, i') -> (M.insert i (unsafeCoerce a) m, i')

modifySTRef :: STRef s a -> (a -> a) -> ST s ()
modifySTRef (STRef i) f = ST $
  modify $ \(m, i') -> (M.adjust (unsafeCoerce f) i m, i')                      

runST :: (forall s. ST s a) -> a
runST (ST s) = evalState s (M.empty, 0)

foo :: Num a => ST s (a, Bool)
foo = do
  a <- newSTRef 0 
  modifySTRef a (+100)
  b <- newSTRef False
  modifySTRef b not
  (,) <$> readSTRef a <*> readSTRef b

Maintenant on peut le faire :

> runST foo
(100, True)

Mais ce qui suit échoue avec l'habituel ST erreur de type :

> runST (newSTRef True)

Bien entendu, le schéma ci-dessus ne récupère jamais les références, mais libère tout ce qui se trouve dans chaque fichier runST appel. Je pense qu'un système plus complexe pourrait mettre en œuvre plusieurs régions distinctes, chacune étiquetée par un paramètre de type, et allouer/libérer des ressources d'une manière plus fine.

De même, l'utilisation de unsafeCoerce signifie ici que l'utilisation directe d'internes est tout aussi dangereuse que l'utilisation de GHC.ST internes et State# Nous devons donc nous assurer de présenter une API sûre et de tester minutieusement nos internes (sinon, nous risquons d'obtenir des erreurs de segmentation en Haskell, ce qui est un grand péché).

9voto

Benjamin Hodgson Points 3510

Depuis que j'ai posté mon réponse précédente vous avez indiqué que cela ne vous dérangeait pas de changer votre définition de PT . Je suis heureux de vous annoncer qu'en assouplissant cette restriction, la réponse à votre question passe de pas de à oui ! J'ai déjà fait valoir que vous devez indexer votre monade par l'ensemble des types dans votre support de stockage, alors voici un code fonctionnel montrant comment le faire. (A l'origine, j'avais prévu de modifier ma réponse précédente, mais elle est devenue trop longue, alors nous y voilà).

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE TypeOperators #-}

import Prelude

Nous allons avoir besoin d'une plus intelligente Monad classe que celle du prélude : celle des des choses indexées de type monade décrivant les chemins à travers un graphe dirigé. Pour des raisons qui devraient devenir évidentes, je vais également définir des foncteurs indexés.

class FunctorIx f where
    imap :: (a -> b) -> f i j a -> f i j b

class FunctorIx m => MonadIx m where
    ireturn :: a -> m i i a
    (>>>=) :: m i j a -> (a -> m j k b) -> m i k b

(>>>) :: MonadIx m => m i j a -> m j k b -> m i k b
ma >>> mb = ma >>>= \_ -> mb

replicateM_ :: MonadIx m => Int -> m i i a -> m i i ()
replicateM_ 0 _ = ireturn ()
replicateM_ n m = m >>> replicateM_ (n - 1) m

Une monade indexée utilise le système de types pour suivre la progression d'un calcul avec état. m i j a est un calcul monadique qui nécessite un état d'entrée de i change l'état en j et produit une valeur de type a . Séquençage des monades indexées avec >>>= c'est comme jouer aux dominos. Vous pouvez alimenter un calcul qui prend l'état de i à j dans un calcul qui va de j à k et obtenir un calcul plus important de i à k . (Il existe une version plus riche de cette monade indexée décrite dans le document Flèches Kleisli de la fortune outrageante ( et ailleurs ) mais celui-ci est bien suffisant pour nos besoins).

Une possibilité avec MonadIx est un File qui suit l'état d'un handle de fichier, vous assurant ainsi de ne pas oublier de libérer des ressources. fOpen :: File Closed Open () part d'un fichier fermé et l'ouvre, fRead :: File Open Open String renvoie le contenu d'un fichier ouvert, et fClose :: File Open Closed () fait passer un fichier d'ouvert à fermé. Le site run L'opération prend un calcul de type File Closed Closed a ce qui garantit que vos poignées de fichiers sont toujours nettoyées.

Mais je m'égare : ici, nous ne sommes pas concernés par un identifiant de fichier mais par un ensemble d'"emplacements mémoire" typés ; les types des éléments de la banque mémoire de la machine virtuelle sont ceux que nous utiliserons pour les indices de la monade. J'aime que mes monades "programme/interprète" soient de type gratuitement parce qu'il exprime le fait que les résultats se trouvent à la fin d'un calcul, et favorise la composabilité et la réutilisation du code, donc voici le foncteur qui produira PT quand on le branche sur FreeIx ci-dessous :

data PTF ref as bs r where
    MkRef_ :: a -> (ref (a ': as) a -> r) -> PTF ref as (a ': as) r
    GetRef_ :: ref as a -> (a -> r) -> PTF ref as as r
    PutRef_ :: a -> ref as a -> r -> PTF ref as as r

instance FunctorIx (PTF ref) where
    imap f (MkRef_ x next) = MkRef_ x (f . next)
    imap f (GetRef_ ref next) = GetRef_ ref (f . next)
    imap f (PutRef_ x ref next) = PutRef_ x ref (f next)

PTF est paramétrée par le type de référence ref :: [*] -> * -> * - Les références sont autorisées à connaître les types présents dans le système - et indexés par la liste des types stockés dans la "mémoire" de l'interpréteur. Le cas intéressant est MkRef_ : faire une nouvelle référence ajoute une valeur de type a à la mémoire, en prenant as à a ': as ; la suite attend un ref dans l'environnement étendu. Les autres opérations ne modifient pas la liste des types dans le système.

Lorsque je crée des références de manière séquentielle ( x <- mkRef 1; y <- mkRef 2 ), ils seront de différents types : le premier sera une ref (a ': as) a et le second sera un ref (b ': a ': as) b . Pour que les types s'alignent, j'ai besoin d'un moyen d'utiliser une référence dans un environnement plus grand que celui dans lequel elle a été créée. En général, cette opération dépend du type de référence, donc je vais la mettre dans une classe.

class Expand ref where
    expand :: ref as a -> ref (b ': as) a

Une généralisation possible de cette classe engloberait le modèle d'applications répétées de expand avec un type comme inflate :: ref as a -> ref (bs :++: as) a .

Voici un autre élément réutilisable de l'infrastructure, le monade libre indexée Je l'ai déjà mentionné. FreeIx transforme un foncteur indexé en une monade indexée en fournissant une opération de jonction alignée sur le type. Free qui noue le nœud récursif dans le paramètre du foncteur, ainsi qu'une opération sans effet. Pure .

data FreeIx f i j a where
    Pure :: a -> FreeIx f i i a
    Free :: f i j (FreeIx f j k a) -> FreeIx f i k a

lift :: FunctorIx f => f i j a -> FreeIx f i j a
lift f = Free (imap Pure f)

instance FunctorIx f => MonadIx (FreeIx f) where
    ireturn = Pure
    Pure x >>>= f = f x
    Free love {- , man -} >>>= f = Free $ imap (>>>= f) love

instance FunctorIx f => FunctorIx (FreeIx f) where
    imap f x = x >>>= (ireturn . f)

L'un des inconvénients des monades libres réside dans le langage standard que vous devez écrire pour faire de la monade un outil de gestion de la qualité. Free et Pure plus facile à travailler. Voici quelques armes à simple action PT qui constituent la base de l'API de la monade, et quelques synonymes de modèle pour cacher le Free lorsque nous déballons PT valeurs.

type PT ref = FreeIx (PTF ref)

mkRef :: a -> PT ref as (a ': as) (ref (a ': as) a)
mkRef x = lift $ MkRef_ x id

getRef :: ref as a -> PT ref as as a
getRef ref = lift $ GetRef_ ref id

putRef :: a -> ref as a -> PT ref as as ()
putRef x ref = lift $ PutRef_ x ref ()

pattern MkRef x next = Free (MkRef_ x next)
pattern GetRef ref next = Free (GetRef_ ref next)
pattern PutRef x ref next = Free (PutRef_ x ref next)

C'est tout ce dont nous avons besoin pour être en mesure d'écrire PT les calculs. Voici votre fib exemple. J'utilise RebindableSyntax et en redéfinissant localement les opérateurs de la monade (en leurs équivalents indexés) de façon à ce que je puisse utiliser do sur ma monade indexée.

-- fib adds two Ints to an arbitrary environment
fib :: Expand ref => Int -> PT ref as (Int ': Int ': as) Int
fib n = do
    rold' <- mkRef 0
    rnew <- mkRef 1
    let rold = expand rold'
    replicateM_ n $ do
        old <- getRef rold
        new <- getRef rnew
        putRef new rold
        putRef (old+new) rnew
    getRef rold
        where (>>=) = (>>>=)
              (>>) = (>>>)
              return :: MonadIx m => a -> m i i a
              return = ireturn
              fail :: MonadIx m => String -> m i j a
              fail = error

Cette version de fib ressemble exactement à celui que vous vouliez écrire dans la question originale. La seule différence (à part les liaisons locales de >>= et ainsi de suite) est l'appel à expand . Chaque fois que vous créez une nouvelle référence, vous devez expand tous les anciens, ce qui est un peu fastidieux.

Enfin, nous pouvons terminer le travail que nous avons entrepris et construire un PT -machine qui utilise un Tuple comme support de stockage et Elem comme type de référence.

infixr 5 :>
data Tuple as where
    E :: Tuple '[]
    (:>) :: a -> Tuple as -> Tuple (a ': as)

data Elem as a where
    Here :: Elem (a ': as) a
    There :: Elem as a -> Elem (b ': as) a

(!) :: Tuple as -> Elem as a -> a
(x :> xs) ! Here = x
(x :> xs) ! There ix = xs ! ix

updateT :: Elem as a -> a -> Tuple as -> Tuple as
updateT Here x (y :> ys) = x :> ys
updateT (There ix) x (y :> ys) = y :> updateT ix x ys

Pour utiliser un Elem dans un tuple plus grand que celui pour lequel vous l'avez construit, vous devez juste le faire regarder plus bas dans la liste.

instance Expand Elem where
    expand = There

Notez que ce déploiement de Elem est un peu comme un indice de Bruijn : les variables les plus récemment liées ont des indices plus petits.

interp :: PT Elem as bs a -> Tuple as -> a
interp (MkRef x next) tup = let newTup = x :> tup
                            in interp (next $ Here) newTup
interp (GetRef ix next) tup = let x = tup ! ix
                              in interp (next x) tup
interp (PutRef x ix next) tup = let newTup = updateT ix x tup
                                in interp next newTup
interp (Pure x) tup = x

Lorsque l'interprète rencontre un MkRef demande, il augmente la taille de sa mémoire en ajoutant x à l'avant. Le vérificateur de caractères vous rappellera que toute ref d'avant le MkRef doivent être correctement expand ed, de sorte que les références existantes ne soient pas déréglées lorsque le tuple change de taille. Nous avons payé pour un interprète sans casts non sécurisés, mais nous avons obtenu l'intégrité référentielle pour démarrer.

Pour fonctionner à partir d'un départ arrêté, il faut que le PT Le calcul s'attend à commencer avec une banque de mémoire vide, mais nous lui permettons de se terminer dans n'importe quel état.

run :: (forall ref. Expand ref => PT ref '[] bs a) -> a
run x = interp x E

Il vérifie les caractères, mais est-ce que ça marche ?

ghci> run (fib 5)
5
ghci> run (fib 3)
2

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X