[英]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
。
不要沮喪,讓我們繼續使用異構列表作為我們將解釋PT
的State
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進行排序就像玩多米諾骨牌一樣。 您可以養活一個計算這需要國家從i
到j
注入了從雲計算j
到k
,並獲得更大的計算,從i
到k
。 (這個索引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
到所述存儲器,以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
將一個索引的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的一個缺點是你必須編寫的樣板才能讓Free
和Pure
更易於使用。 下面是一些構成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.