假设我们想要构建一个代表典型操作的类型,比如说,一个无锁算法:
newtype IntPtr = IntPtr { ptr :: Int } deriving (Eq, Ord, Show)
data Op r where
OpRead :: IntPtr -> Op Int
OpWrite :: IntPtr -> Int -> Op ()
OpCAS :: IntPtr -> Int -> Int -> Op Bool
理想情况下,我们希望使用方便的do
符号来表示此模型中的一些算法,例如(假设相应read = OpRead
和cas = OpCAS
出于审美原因)以下几乎是Wikipedia example的字面翻译:
import Prelude hiding (read)
import Control.Monad.Loops
add :: IntPtr -> Int -> Op Int
add p a = snd <$> do
iterateUntil fst $ do
value <- read p
success <- cas p value (value + a)
pure (success, value + a)
我们怎么能实现这一目标?让我们向Op
添加几个构造函数来表示纯注入值和monadic绑定:
OpPure :: a -> Op a
OpBind :: Op a -> (a -> Op b) -> Op b
因此,让我们尝试编写一个Functor
实例。 OpPure
和OpBind
很容易,例如:
instance Functor Op where
fmap f (OpPure x) = OpPure (f x)
但是指定GADT类型的构造函数开始闻起来很糟糕:
fmap f (OpRead ptr) = do
val <- OpRead ptr
pure $ f val
在这里,我们假设我们稍后会编写Monad
实例,以避免丑陋的嵌套OpBind
。
这是处理此类型的正确方法,还是我的设计非常错误,这是它的标志?
答案 0 :(得分:10)
这种使用do
的符号 - 用于构建语法树的符号将在以后解释,由 free monad 建模。 (我实际上将演示所谓的更自由或运营 monad,因为它更接近你目前所拥有的。)
原始Op
数据类型 - 不包含OpPure
和OpBind
- 表示一组原子类型指令(即read
,write
和{{1} })。在命令式语言中,程序基本上是一个指令列表,所以让我们设计一个表示cas
列表的数据类型。
一个想法可能是使用实际列表,即Op
。很明显,不会这样做,因为它会限制程序中的每个指令具有相同的返回类型,这不会成为非常有用的编程语言。
关键的见解是,在解释的命令式语言的任何合理的操作语义中,控制流不会经过指令直到解释器计算出该指令的返回值。也就是说,程序的 n 指令通常取决于指令0到 n -1的结果。我们可以使用延续传递样式对此进行建模。
type Program r = [Op r]
data Program a where
Return :: a -> Program a
Step :: Op r -> (r -> Program a) -> Program a
是一种指令列表:它是一个返回单个值的空程序,或者是一条指令,后跟一条指令列表。 Program
构造函数中的函数意味着运行Step
的解释器必须先提供Program
值,然后才能恢复解释程序的其余部分。因此,类型确保了顺序性。
要构建原子程序r
,read
和write
,您需要将它们放在单个列表中。这涉及将相关指令放在cas
构造函数中,并传递无操作继续。
Step
lift :: Op a -> Program a
lift i = Step i Return
read ptr = lift (OpRead ptr)
write ptr val = lift (OpWrite ptr val)
cas ptr cmp val = lift (OpCas ptr cmp val)
与您调整的Program
的不同之处在于,每个Op
只有一条指令。 Step
的左参数可能是OpBind
s的整个树。这将允许您区分不同的相关Op
s,打破monad关联性法则。
您可以将>>=
设为monad。
Program
instance Monad Program where
return = Return
Return x >>= f = f x
Step i k >>= f = Step i ((>>= f) . k)
基本上执行列表连接 - 它走到列表的末尾(通过在>>=
延续下编写递归调用)并在新尾部移植。这是有道理的 - 它对应于插件&#34;运行该程序,然后运行该程序&#34; Step
的语义。
注意>>=
Program
个实例并不依赖Monad
,一个明显的概括是参数化指令的类型并使{{1}进入任何旧指令集的列表。
Op
所以Program
是免费的monad,无论data Program i a where
Return :: a -> Program i a
Step :: i r -> (r -> Program i a) -> Program a
instance Monad (Program i) where
-- implementation is exactly the same
是什么。此版本的Program i
是一种用于建模命令式语言的通用工具。