32

这篇文章是识字的Haskell。只需放入“pad.lhs”之类的文件ghci即可运行它。

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

好的,所以我能够弄清楚如何ST用纯代码表示 monad。首先,我们从引用类型开始。它的具体值并不重要。最重要的是它PT s a不应该与任何其他类型同构forall s。(特别是,它应该与 none()或同构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*->*,但现在这并不重要。对于我们所关心的一切,它可能是多品种的。

> 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 实例(它只遵守关于 的 monad 法则,runPF稍后将定义):

> 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)是一种测试答案是否有效。)

4

3 回答 3

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表示存储介质是bs 的同构列表。当 GHC 拒绝这种类型签名,抱怨它不能b与里面的东西的类型匹配时,它让我们大吃一惊MkRef


不要气馁,让我们尝试使用异构列表作为State我们将在其中解释的 monad的环境PT

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

这是我个人最喜欢的 Haskell 数据类型之一。它是一个可扩展的元组,由其中的事物类型列表索引。元组是异构链表,其中包含有关其中事物类型的类型级信息。(它通常HListKiselyov 的论文之后被称为,但我更喜欢Tuple.)当你在一个元组的前面添加一些东西时,你将它的类型添加到类型列表的前面。怀着诗意的心情,我曾经这样说:“元组和它的类型一起成长,就像藤蔓爬上竹子一样。”

示例Tuple

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值 likeThere (There Here)看起来类似于自然数 like S (S Z))但有额外的类型 - 在这种情况下,证明类型a在类型级列表中as。我提到这一点是因为它具有启发性:Nats 可以制作好的列表索引,同样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让我们尝试为环境中的 a编写解释器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引用的值提供一个值,这非常乏味。(也就是说,您通常可以说服 GHCElem通过使用 hacky 类型类的证明搜索来找到值。)

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

一个可能更简洁的实现将允许 a 中的类型列表PT随着您遍历数据类型而变化:每次遇到 a 时MkRef,类型都会变长。因为计算的类型随着它的进行而改变,所以你不能使用常规的 monad - 你必须求助于IxMonad . 如果您想知道该程序的外观,请参阅我的其他答案

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

于 2015-11-28T23:54:00.157 回答
11

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

我们将 ref-s 保存在 an 中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一样危险,所以我们应该确保提供一个安全的 API,并彻底测试我们的内部(否则我们可能会在 Haskell 中遇到段错误,一个大罪)。GHC.STState#

于 2015-11-28T22:51:07.843 回答
9

自从我发布了我之前的答案以来,您已经表示您不介意更改PT. 我很高兴地报告:放宽限制会将您问题的答案从“”变为“”!我已经说过你需要通过存储介质中的类型集来索引你的 monad,所以这里有一些工作代码展示了如何做到这一点。(我最初将此作为对我之前答案的编辑,但它太长了,所以我们在这里。)

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

import Prelude

我们将需要一个Monad比 Prelude 中的更智能的类:通过有向图描述路径的索引单子类事物。出于显而易见的原因,我还将定义索引函子。

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是一个单子计算,它需要输入状态i,将状态更改为j,并生成类型 的值a。对索引单子进行排序>>>=就像玩多米诺骨牌一样。您可以将一个将状态 from的计算输入到一个从 to 的计算中,并从ito获得更大的计算。(在Kleisli Arrows of Outrageous Fortune和其他地方)中描述了这个索引 monad 的更丰富的版本,但这对于我们的目的来说已经足够了。)jjkik

一种可能性MonadIxFile跟踪文件句柄状态的 monad,确保您不会忘记释放资源。fOpen :: File Closed Open ()从一个关闭的文件开始并打开它,fRead :: File Open Open String返回打开的文件的内容,并将fClose :: File Open Closed ()文件从打开状态变为关闭状态。该run操作需要计算 type File Closed Closed a,以确保您的文件句柄始终得到清理。

但我离题了:这里我们关心的不是文件句柄,而是一组键入的“内存位置”;虚拟机内存库中事物的类型是我们将用于 monad 索引的内容。我喜欢免费获得我的“程序/解释器”monad,因为它表达了结果存在于计算的叶子中的事实,并促进了可组合性和代码重用,所以这是PT当我们将其插入FreeIx下面时将产生的函子:

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 :: [*] -> * -> *- 允许引用知道系统中有哪些类型 - 并由存储在解释器“内存”中的类型列表索引。有趣的情况是MkRef_:创建一个新的引用会a在内存中添加一个类型的值,取asa ': as; 延续预期ref在扩展环境中。其他操作不会更改系统中的类型列表。

当我按顺序创建引用 ( x <- mkRef 1; y <- mkRef 2) 时,它们将具有不同的类型:第一个是 a ref (a ': as) a,第二个是 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,该操作将递归结与函子的参数联系起来,以及一个无操作操作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)

免费 monad 的一个缺点是您必须编写样板文件以使其FreePure易于使用。下面是一些构成 monad API 基础的单动作PT,以及一些模式同义词,用于在我们解包值时隐藏Free构造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例子。我正在使用RebindableSyntax并在本地重新定义 monad 运算符(与其索引等效项),因此我可以do在索引 monad 上使用符号。

-- 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使用 aTuple作为存储介质和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

请注意,这种部署Elem很像 de Bruijn 索引:最近绑定的变量具有较小的索引。

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任何s ,因此当元组更改大小时,现有引用不会失控。我们为没有不安全类型转换的解释器付费,但我们获得了引用完整性。refMkRefexpand

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

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

它进行类型检查,但它有效吗?

ghci> run (fib 5)
5
ghci> run (fib 3)
2
于 2015-12-01T11:32:09.397 回答