簡體   English   中英

可以純粹執行`ST`之類的monad(沒有'ST`庫)嗎?

[英]Can a `ST`-like monad be executed purely (without the `ST` library)?

這篇文章是有文化的Haskell。 只需輸入像“pad.lhs”這樣的文件, ghci就能運行它。

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

好的,所以我能夠想出如何用純代碼表示ST monad。 首先,我們從我們的引用類型開始。 它的具體價值並不重要。 最重要的是PT sa不應該與任何其他類型的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的類型是*->* ,但現在這並不重要。 對於我們所關心的一切,它可能是多面手的

> 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情況下定義runPF嗎?

有沒有一種純粹的方法來定義runPF PTRef的定義完全不重要; 無論如何它只是一個占位符類型。 它可以重新定義為任何使它工作的東西。

如果你不能純粹定義runPF ,請提供一個不能的證明。

性能不是一個問題(如果是,我不會讓每個return都有自己的參考)。

我認為存在類型可能有用。

注:這是平凡的,如果我們假設是a為dynamicable什么的。 我在尋找與所有問題的解答a

注意:事實上,答案甚至不一定與PT有很大關系。 它只需要像ST一樣強大而不使用魔法。 (轉換自(forall s. PT s)是對答案是否有效的測試。)

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有我們一鼓作氣,以權利時,它會拒絕這種類型的簽名,抱怨它不能匹配b與里面的東西類型MkRef


不要沮喪,讓我們繼續使用異構列表作為我們將解釋PTState monad的環境。

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

這是我個人最喜歡的Haskell數據類型之一。 它是一個可擴展的元組,由其中的事物類型列表索引。 元組是異構鏈表,其中包含有關其中內容類型的類型級信息。 (它通常在Kiselyov的論文之后被稱為HList但我更喜歡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值,如There (There Here)看起來類似於自然數,如S (SZ) )但是有額外的類型 - 在這種情況下,證明類型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)

讓我們嘗試在Tuple環境中為PT編寫解釋器。

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我正在參考的值,這非常繁瑣。 (也就是說,你通常可以說服GHC通過使用hacky類型的證明搜索來找到Elem值。)

另一件事:組成PT變得困難。 計算的所有部分都必須由相同的類型列表編制索引。 您可以嘗試引入允許您擴展PT環境的組合器或類,但是當您這樣做時,您還必須更新所有引用。 使用monad會非常困難。

一個可能更清晰的實現將允許PT中的類型列表隨着您在數據類型中的變化而變化:每次遇到MkRef類型都會變長一個。 因為計算的類型隨着它的進展而改變,所以你不能使用常規的monad - 你必須求助於IxMonad 如果您想知道該程序的外觀,請參閱我的其他答案

最終,關鍵點在於元組的類型由PT請求的值確定。 環境給定請求決定存儲在其中的環境。 interp無法選擇元組中的內容,它必須來自PT上的索引。 任何欺騙該要求的企圖都會崩潰和焚燒。 現在,在一個真正的依賴性類型的系統,我們可以檢查PT我們給定值,並找出哪些as應該的。 唉,Haskell不是一個依賴類型的系統。

一個簡單的解決方案是包裝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,並徹底測試我們的內部結構(或者我們可能在哈斯克爾得到段錯誤,這是一個偉大的罪惡)。

由於我發布了我之前的回答 ,您已經表明您不介意更改PT的定義。 我很高興地報告:放寬這個限制會改變你的問題的答案從 我已經爭辯說你需要通過存儲介質中的一組類型索引你的monad,所以這里有一些工作代碼顯示如何做到這一點。 (我最初將此作為我之前答案的編輯,但它太長了,所以我們在這里。)

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

import Prelude

我們需要一個比Prelude更聰明的Monad類: 索引monad類似的東西,描述通過有向圖的路徑。 由於顯而易見的原因,我還將定義索引函子。

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使用類型系統來跟蹤有狀態計算的進度。 mija是一個mija計算,它需要i的輸入狀態,將狀態更改為j ,並產生類型a的值。 使用>>>=索引的monad進行排序就像玩多米諾骨牌一樣。 您可以養活一個計算這需要國家從ij注入了從雲計算jk ,並獲得更大的計算,從ik (這個索引monad的更豐富版本在Kleisli Arrows of Outrageous Fortune以及其他地方 )中有所描述,但這個版本足以滿足我們的目的。)

MonadIx一種可能性是File monad,它跟蹤文件句柄的狀態,確保您不會忘記釋放資源。 fOpen :: File Closed Open ()以一個已關閉的文件開頭並打開它, fRead :: File Open Open String返回打開文件的內容, fClose :: File Open Closed ()從open打開一個文件。 run操作采用File Closed Closed a類型的計算,這可確保始終清理文件句柄。

但我離題了:這里我們不關心文件句柄,而是關注一組鍵入的“內存位置”; 虛擬機內存庫中的東西類型是我們將用於monad索引的內容。 我喜歡免費獲得我的“程序/解釋器”monad 因為它表示結果存在於計算的葉子中,並促進可組合性和代碼重用,所以這里是將我們將其插入FreeIx時生成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 ),它們將具有不同的類型:第一個將是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將一個索引的FreeIx函數轉換為索引的monad, Free操作將FreeIx函數參數中的遞歸結與無操作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 ,以及一些模式同義詞,用於在我們解壓縮PT值時隱藏Free構造函數。

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運算符(到它們的索引等價物),所以我可以在索引monad上使用do notation。

-- 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 -machine,它使用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

請注意, 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來增加其內存的大小。 類型檢查器將提醒您,必須正確expand MkRef之前的任何ref ,因此當元組更改大小時,現有引用不會出現問題。 我們支付了一個沒有不安全演員的翻譯,但是我們得到了引用的參照完整性。

從常備開始運行需要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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM