最近这个所以问题 https://stackoverflow.com/questions/33975270/can-a-st-like-monad-be-executed-purely-without-the-st-library促使我在 Haskell 中编写一个不安全且纯粹的 ST monad 模拟,您可以在下面看到其稍作修改的版本:
{-# LANGUAGE DeriveFunctor, GeneralizedNewtypeDeriving, RankNTypes #-}
import Control.Monad.Trans.State
import GHC.Prim (Any)
import Unsafe.Coerce (unsafeCoerce)
import Data.List
newtype ST s a = ST (State ([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
(env, i) <- get
put (unsafeCoerce a : env, i + 1)
pure (STRef i)
update :: [a] -> (a -> a) -> Int -> [a]
update as f i = case splitAt i as of
(as, b:bs) -> as ++ f b : bs
_ -> as
readSTRef :: STRef s a -> ST s a
readSTRef (STRef i) = ST $ do
(m, i') <- get
pure (unsafeCoerce (m !! (i' - i - 1)))
modifySTRef :: STRef s a -> (a -> a) -> ST s ()
modifySTRef (STRef i) f = ST $
modify $ \(env, i') -> (update env (unsafeCoerce f) (i' - i - 1), i')
runST :: (forall s. ST s a) -> a
runST (ST s) = evalState s ([], 0)
如果我们能够提供常用的 ST monad API 而无需unsafeCoerce
。具体来说,我想正式说明通常的 GHC ST monad 和上述模拟起作用的原因。在我看来,它们之所以有效,是因为:
- Any
STRef s a
与权利s
tag 一定是在当前 ST 计算中创建,因为runST
确保不同的状态不会混淆。
- 前一点以及 ST 计算只能扩展引用环境的事实意味着任何
STRef s a
与权利s
标签指的是一个有效的a
- 环境中的类型位置(可能在运行时削弱引用之后)。
上述几点实现了显着的无证明义务的编程体验。我能想到没有什么能真正接近安全和纯粹的 Haskell;我们可以用索引状态单子和异构列表进行相当差的模仿,但这并没有表达上述任何一点,因此需要在每个使用站点进行证明STRef
-s.
我不知道如何在 Agda 中正确地形式化这一点。对于初学者来说,“分配在this计算”已经够棘手的了。我考虑过表示STRef
-s 作为特定分配包含在特定分配中的证明ST
计算,但这似乎会导致类型索引的无限递归。