可以纯粹执行`ST`之类的monad(没有'ST`库)吗?

时间:2015-11-28 19:04:12

标签: haskell state monads ghc purely-functional

这篇文章是有文化的Haskell。只需输入一个类似" pad.lhs"的文件。并且ghci将能够运行它。

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

好的,所以我能够想出如何用纯代码表示ST monad。首先,我们从我们的引用类型开始。它的具体价值并不重要。最重要的是PT s a不应与任何其他类型forall s同构。 (特别是,它应该与()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.

s的种类是*->*,但现在这并不重要。对于我们所关心的一切,它可能是polykind

> 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

非常直接。 AndThen允许我们将其用作Monad。您可能想知道return是如何实现的。这是它的monad实例(它仅尊重与runPF相关的monad定律,稍后定义):

> 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

现在我们可以将fib定义为测试用例。

> 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

并进行类型检查。欢呼!现在,我可以将其转换为ST(我们现在看到为什么s必须是* -> *

> 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)

现在我们可以定义一个函数来运行PT而根本不引用ST

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

runPF $ fib 7提供13,这是正确的。

我的问题是我们可以使用runPF来定义ST而无需参考吗?

是否有一种纯粹的方式来定义runPFPTRef的定义完全不重要;它无论如何只是占位符类型。它可以被重新定义为任何使它工作的东西。

如果您无法纯粹定义runPF,请提供不能证明的证据。

性能不是问题(如果是,我不会让每个return都有自己的参考号。)

我认为存在类型可能有用。

注意:如果我们假设a是动态的或其他东西,那就太微不足道了。我正在寻找适用于所有a的答案。

注意:事实上,答案甚至不一定与PT有很大关系。它只需要与ST一样强大而不使用魔法。 (从(forall s. PT s)转换是对答案是否有效的测试。)

3 个答案:

答案 0 :(得分:14)

tl; dr:如果不调整PT的定义,就不可能。这是核心问题:您将在某种存储介质的上下文中运行有状态计算,但是存储介质必须知道如何存储任意类型。如果没有将某种证据打包到MkRef构造函数中,这是不可能的 - 或者像其他人所建议的那样存储包裹的Typeable字典,或者证明该值属于已知的一个有限的类型集。

首次尝试时,让我们尝试使用列表作为存储介质,并使用整数来引用列表中的元素。

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)
-- ...

在环境中存储新项目时,我们务必将其添加到列表的末尾,以便我们之前发出的Ref保持指向正确的元素。

这不对。我可以引用任何类型a,但interp的类型表示存储介质是b s的同类列表。当GHC拒绝这种类型的签名时,GHC让我们感到愤怒,抱怨它不能将bMkRef内的内容匹配。

不要沮丧,让我们继续使用异构列表作为State monad的环境,我们将在其中解释PT

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

这是我个人最喜欢的Haskell数据类型之一。它是一个可扩展元组,由其中的事物类型列表索引。元组是异构链表,其中包含有关其中内容类型的类型级信息。 (它通常在Kiselyov's paper之后被称为HList,但我更喜欢Tuple。)当您在元组的前面添加内容时,将其类型添加到列表的前面类型。在一种诗意的情绪中,I once put it this way:&#34;元组及其类型一起生长,就像藤蔓爬上竹子一样。&#34;

Tuple s的例子:

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

对元组内部值的引用是什么样的?我们必须向GHC证明我们从元组中获取的东西的类型确实是我们期望的类型。

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

Elem的定义在结构上与自然数相似(Elem值,如There (There Here)看起来类似于自然数,如S (S Z)),但有额外的类型 - 在此case,证明类型a位于类型级别列表as中。我之所以提到这一点是因为它提示:Nat制作好的列表索引,同样Elem对索引元组很有用。在这方面,它可以替代我们的引用类型中的Int

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

我们需要一些函数来处理元组和索引。

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)

让我们尝试在PT环境中为Tuple编写解释器。

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
-- ...

无能为力,苦苦挣扎。问题是当我们获得新引用时,环境中Tuple的类型会发生变化。正如我之前提到的,向元组添加一些东西会将其类型添加到元组的类型中,类型State (Tuple as) a所暗示的事实。 GHC并没有被这种尝试过的诡计所迷惑:Could not deduce (as ~ (as :++: '[a1]))

据我所知,这是轮子脱落的地方。你真正想做的是在整个PT计算中保持元组的大小不变。这将要求您通过您可以获取引用的类型列表来索引PT本身,证明您每次执行此操作时都允许(通过提供Elem值)。然后环境看起来像一个列表元组,引用将包括Elem(选择正确的列表)和Int(以查找列表中的特定项目)。

该计划当然违反了规则(您需要更改PT的定义),但它也存在工程问题。当我致电MkRef时,我有责任给Elem一个{I}引用的值,这非常繁琐。 (也就是说,您通常可以通过使用hacky类型的证明搜索来说服GHC找到Elem值。)

另一件事:组成PT变得困难。计算的所有部分都必须由相同的类型列表编制索引。您可以尝试引入组合器或类,这些组合或类允许您扩展PT的环境,但是当您这样做时,您还必须更新所有引用。使用monad会非常困难。

一个可能更清晰的实现将允许PT中的类型列表随着您在数据类型中的变化而变化:每次遇到MkRef时类型都会变长一个。由于计算类型随着进度的变化而变化,因此您无法使用常规monad - 您必须使用IxMonad 。如果您想知道该程序的外观,请参阅我的other answer

最终,关键点在于元组的类型由PT请求的值决定。环境给定请求决定存储在其中的内容。 interp无法选择元组中的内容,它必须来自PT上的索引。任何欺骗该要求的企图都会崩溃和焚烧。现在,在一个真正依赖类型的系统中,我们可以检查我们给出的PT值并找出as应该是什么。唉,Haskell不是一个依赖类型的系统。

答案 1 :(得分:11)

一个简单的解决方案是打包State monad并显示与ST相同的API。在这种情况下,不需要存储运行时类型信息,因为它可以从STRef - s的类型确定,并且通常的ST s量化技巧可以让我们防止用户弄乱存储的容器引用。

我们将ref-s保持在IntMap并在每次分配新ref时递增计数器。阅读和写作只是修改了IntMap,其中一些unsafeCoerce撒在了顶上。

{-# 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

现在我们可以做到:

> runST foo
(100, True)

但是以下因常见ST类型错误而失败:

> runST (newSTRef True)

当然,上面的方案从不垃圾收集引用,而是释放每个runST调用的所有内容。我认为一个更复杂的系统可以实现多个不同的区域,每个区域都由一个类型参数标记,并以更细粒度的方式分配/释放资源。

此外,unsafeCoerce的使用意味着直接使用内部内容与直接使用GHC.ST内部和State#一样危险,因此我们应确保提供安全的API ,并且还彻底测试我们的内部(或者我们可能在Haskell中获得段错误,这是一个伟大的罪恶)。

答案 2 :(得分:9)

自从我发布了earlier answer后,您已经表示您不介意更改PT的定义。我很高兴地报告:放宽该限制会将您的问题的答案从更改为!我已经认为你需要按照存储介质中的一组类型索引你的monad,所以这里有一些工作代码显示了如何做到这一点。 (我最初将此作为我之前答案的编辑,但它太长了,所以我们在这里。)

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

import Prelude

我们需要一个比Prelude中更聪明的Monad类:indexed monad-like things描述通过有向图的路径。由于显而易见的原因,我也将定义索引函子。

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

索引monad使用类型系统来跟踪有状态计算的进度。 m i j a是一个monadic计算,需要输入状态i,将状态更改为j,并生成类型a的值。使用>>>=对带索引的monad进行排序就像玩多米诺骨牌一样。您可以将从ij的状态计算的计算提供给从jk的计算,并从i获得更大的计算到k。 (Kleisli Arrows of Outrageous Fortuneand elsewhere)中描述了这个索引monad的更丰富版本,但这个版本足以满足我们的目的。)

MonadIx的一种可能性是File monad,它跟踪文件句柄的状态,确保您不会忘记释放资源。 fOpen :: File Closed Open ()以已关闭的文件开头并打开它,fRead :: File Open Open String返回已打开文件的内容,fClose :: File Open Closed ()将文件从打开关闭。 run操作采用类型File Closed Closed a的计算,这可确保您的文件句柄始终被清理。

但是我离题了:这里我们不关心文件句柄,而是关注一组键入的&#34;内存位置&#34 ;;虚拟机内存库中的东西类型是我们用于monad索引的东西。我喜欢得到我的&#34;程序/翻译&#34; monads for free因为它表达了一个事实,即结果存在于计算的叶子中,并促进可组合性和代码重用,所以这就是当我们将它插入{PT时会生成FreeIx的仿函数{1}}下面:

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由引用类型ref :: [*] -> * -> *参数化 - 允许引用知道系统中的哪些类型 - 并通过存储在解释器中的类型列表进行索引&& #34;存储器&#34 ;.有趣的案例是MkRef_:制作新引用会将a类型的值添加到内存中,将as带到a ': as;延续期望在扩展环境中ref。其他操作不会更改系统中的类型列表。

当我按顺序创建引用(x <- mkRef 1; y <- mkRef 2)时,它们会有不同的类型:第一个是ref (a ': as) a,第二个是ref (b ': a ': as) b。为了使类型排成一行,我需要一种在比它创建的环境更大的环境中使用引用的方法。通常,这个操作取决于引用的类型,所以我将它放在一个类中

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

这个类的一种可能的概括将结束expand的重复应用模式,其类型为inflate :: ref as a -> ref (bs :++: as) a

这是另一个可重复使用的基础架构,我之前提到的索引的免费monad FreeIx通过提供类型对齐的连接操作Free将索引的仿函数转换为索引的monad,该操作将仿函数参数中的递归结连接起来,并执行无操作操作{{1 }}

Pure

免费monad的一个缺点是你必须编写以使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) Free更容易使用的样板。以下是一些构成monad API基础的单一操作Pure,以及在我们解包PT值时隐藏Free构造函数的pattern synonyms

PT

我们需要能够编写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) 次计算的所有内容。这是您的PT示例。我正在使用fib并在本地重新定义monad运算符(对于它们的索引等价物),因此我可以在我的索引monad上使用RebindableSyntax表示法。

do

这个版本的-- 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 看起来就像你想在原始问题中写的那个。唯一的区别(除fib之外的本地绑定等)是对>>=的调用。每次创建新引用时,都必须expand所有旧引用,这有点单调乏味。

最后,我们可以完成我们要完成的工作并构建一个expand - 机器,该机器使用PT作为存储介质,Tuple作为参考类型。

Elem

要在比您为其构建的元组更大的元组中使用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 ,您只需要让它在列表中向下看。

Elem

请注意,instance Expand Elem where expand = There 的此部署与de Bruijn索引相似:更近期绑定的变量具有更小的索引。

Elem

当解释器遇到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 请求时,它会通过在前面添加MkRef来增加其内存的大小。类型检查器会提醒您,x之前的所有ref必须正确MkRef,因此现有的引用在元组更改大小时不会出现问题。我们支付了一个没有不安全演员的翻译,但我们得到了引用完整性。

从一开始就运行要求expand计算期望从一个空的内存库开始,但我们允许它以任何状态结束。

PT

它有点检查,但有效吗?

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