将域建模为GADT类型并为其提供do-sugar

时间:2018-05-29 21:45:09

标签: haskell gadt

假设我们想要构建一个代表典型操作的类型,比如说,一个无锁算法:

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 = OpReadcas = 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实例。 OpPureOpBind很容易,例如:

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

这是处理此类型的正确方法,还是我的设计非常错误,这是它的标志?

1 个答案:

答案 0 :(得分:10)

这种使用do的符号 - 用于构建语法树的符号将在以后解释,由 free monad 建模。 (我实际上将演示所谓的更自由运营 monad,因为它更接近你目前所拥有的。)

原始Op数据类型 - 不包含OpPureOpBind - 表示一组原子类型指令(即readwrite和{{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值,然后才能恢复解释程序的其余部分。因此,类型确保了顺序性。

要构建原子程序rreadwrite,您需要将它们放在单个列表中。这涉及将相关指令放在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是一种用于建模命令式语言的通用工具。