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

0xd*_*00d 4 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
Run Code Online (Sandbox Code Playgroud)

理想情况下,我们希望使用方便do注释在此模型中表示一些算法,例如(假设相应read = OpReadcas = OpCAS出于美学原因)维基百科示例的以下几乎字面翻译:

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)
Run Code Online (Sandbox Code Playgroud)

我们怎么能实现这一目标?让我们添加几个构造函数来Op表示纯注入值和monadic绑定:

  OpPure :: a -> Op a
  OpBind :: Op a -> (a -> Op b) -> Op b
Run Code Online (Sandbox Code Playgroud)

所以让我们尝试编写一个Functor实例.OpPure并且OpBind很容易,例如:

instance Functor Op where
  fmap f (OpPure x) = OpPure (f x)
Run Code Online (Sandbox Code Playgroud)

但是指定GADT类型的构造函数开始闻起来很糟糕:

  fmap f (OpRead ptr) = do
    val <- OpRead ptr
    pure $ f val
Run Code Online (Sandbox Code Playgroud)

在这里,我们假设我们稍后会编写Monad实例以避免丑陋的嵌套OpBind.

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

Ben*_*son 10

这种使用do注释来构建一个稍后将被解释的语法树的样式由免费的monad建模.(我实际上将演示所谓的更自由操作的 monad,因为它更接近你到目前为止所拥有的.)

您的原始Op数据类型-不OpPureOpBind-代表一组原子类型的指令(即read,writecas).在命令式语言中,程序基本上是一个指令列表,所以让我们设计一个表示Ops 列表的数据类型.

一个想法可能是使用实际列表,即type Program r = [Op r].显然,这不会做,因为它限制程序中的每个指令具有相同的返回类型,这不会成为非常有用的编程语言.

关键的见解是,在解释的命令式语言的任何合理的操作语义中,在解释器计算出该指令的返回值之前,控制流不会超过指令.也就是说,程序的第n条指令通常取决于指令0到n -1 的结果.我们可以使用延续传递样式来建模.

data Program a where
    Return :: a -> Program a
    Step :: Op r -> (r -> Program a) -> Program a
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

Program不同于你的调整Op,每个Step只有一条指令.OpBind左派的论证可能是一棵完整的树Op.这将允许你区分不同的相关>>=s,打破monad关联性法则.

你可以制作Program一个单子.

instance Monad Program where
    return = Return
    Return x >>= f = f x
    Step i k >>= f = Step i ((>>= f) . k)
Run Code Online (Sandbox Code Playgroud)

>>=基本上执行列表连接 - 它走到列表的末尾(通过在Step延续下编写递归调用)并在新尾部移植.这是有道理的 - 它对应于输入"运行此程序,然后运行该程序"的语义>>=.


注意到ProgramMonad情况下不依赖于Op,一个明显的概括是参数化指令类型,并Program为任何旧指令集的列表.

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
Run Code Online (Sandbox Code Playgroud)

所以Program i是一个免费的单子,不管是什么i是.此版本Program是一种用于建模命令式语言的通用工具.