在Haskell标记

时间:2018-03-27 02:04:38

标签: haskell monads state-monad do-notation

为了生成x86汇编代码,我定义了一个名为X86的自定义类型:

data X86 a = X86 { code :: String, counter :: Integer, value :: (X86 a -> a) }

此类型用于如下所示的标记。这使得编写用于生成if语句,for循环等的模板变得容易......

generateCode :: X86 ()
generateCode = do
  label1 <- allocateUniqueLabel
  label2 <- allocateUniqueLabel
  jmp label1
  label label1
  jmp label2
  label label2

说明定义如下:

jmp :: String -> X86 ()
jmp l = X86 { code = "jmp " ++ l ++ ";\n", counter = 0, value = const () }

label :: String -> X86 ()
label l = X86 { code = l ++ ":\n", counter = 0, value = const () }

完成的装配文件打印如下:

printAsm :: X86 a -> String
printAsm X86{code=code} = code

main = do
  putStrLn (printAsm generateCode)

我以下列方式实现了X86 monad。本质上,序列运算符按顺序连接汇编代码块,并确保计数器递增。

instance Monad X86 where
  x >> y = X86 { code = code x ++ code y, counter = counter x + counter y, value = value y }
  x >>= f = x >> y
    where y = f (value x x)

问题是标签没有正确递增,所以它们不是唯一的!以下是输出:

jmp Label1;
Label1:
jmp Label1;
Label1:

我希望输出对每个标签都有唯一的值:

jmp Label1;
Label1:
jmp Label2;
Label2:

要完成此示例,以下是allocatedUniqueLabel函数的实现:

allocateUniqueId :: X86 Integer
allocateUniqueId = X86 { code = "", counter = 1, value = counter }

allocateUniqueLabel :: X86 String
allocateUniqueLabel = do
  id <- allocateUniqueId
  return ("Label" ++ show id)

如何修复我的X86 monad,以便标签是唯一的?

以下是我尝试的内容:

  • 增加全球反击。 =&GT; Haskell不能安全地允许IO monad之外的全局状态。
  • 使用State monad。 =&GT;我已经查看了一些示例,但不了解如何将它们集成到我现有的X86 monad中。
  • 跟踪monad外的计数器。 =&GT;我宁愿更新计数器&#34;在幕后&#34 ;;否则,很多不使用标签的代码模板都需要手动传播计数器。

3 个答案:

答案 0 :(得分:8)

我们可以使用mtl classes将X86代码描述为有效的程序。我们想要:

  • 生成代码,这是Writer效果;
  • 维持一个计数器,这是State效果。

我们担心最后会实例化这些效果,并且在程序说明中我们会使用MonadWriterMonadState约束。

import Control.Monad.State  -- mtl
import Control.Monad.Writer

分配新标识符会使计数器递增,而不会生成任何代码。这仅使用State效果。

type Id = Integer

allocateUniqueLabel :: MonadState Id m => m String
allocateUniqueLabel = do
  i <- get
  put (i+1)  -- increment
  return ("Label" ++ show (i+1))

当然,我们有动作生成代码,不需要关心当前状态。所以他们使用Writer效果。

jmp :: MonadWriter String m => String -> m ()
jmp l = tell ("jmp " ++ l ++ ";\n")

label :: MonadWriter String m => String -> m ()
label l = tell (l ++ ":\n")

实际程序与原始程序看起来相同,但有更常见的类型。

generateCode :: (MonadState Id m, MonadWriter String m) => m ()
generateCode = do
  label1 <- allocateUniqueLabel
  label2 <- allocateUniqueLabel
  jmp label1
  label label1
  jmp label2
  label label2

当我们运行此程序时,效果会被实例化,此处使用runWriterT / runWriterrunStateT / runState(顺序并不重要,这些两种效果通勤)。

type X86 = WriterT String (State Id)

runX86 :: X86 () -> String
runX86 gen = evalState (execWriterT gen) 1 -- start counting from 1
-- evalState and execWriterT are wrappers around `runStateT` and `runWriterT`:
-- - execWriterT: discards the result (of type ()), only keeping the generated code.
-- - evalState: discards the final state, only keeping the generated code,
--   and does some unwrapping after there are no effects to handle.

答案 1 :(得分:4)

你可能想要使用这个monad堆栈:

type X86 a = StateT Integer (Writer String) a

由于你有一个州和一个作家,你也可以考虑使用RWS(读者 - 作家 - 州一体):

type X86 a = RWS () String Integer a

让我们选择第一个有趣的。我首先定义一个辅助函数来递增计数器(monads cannot lawfully increment a counter "automatically"):

instr :: X86 a -> X86 a
instr i = do
    x <- i
    modify (+1)
    return x

然后您可以将jmp定义为:

jmp :: String -> X86 ()
jmp l = instr $ do
    lift (tell ("jmp " ++ l ++ ";\n"))
       -- 'tell' is one of Writer's operations, and then we 'lift'
       -- it into StateT

do是多余的,但我怀疑会有instr $ do启动指令定义的模式

为此推出我自己的monad - 这样做很有启发性,但我认为使用标准库可以获得更多里程。

答案 2 :(得分:3)

正如你现在可能从其他答案中看到的那样,你的问题就出现了问题 方法是即使你使用柜台,你仍然是 在本地生成标签。特别是

label1 <- allocateUniqueLabel
label label1

相当于

X86 { code = "Label1:\n", counter = 1, value = const () }    

我们需要先组装整个代码,然后生成标签 之后(在某种意义上)使用标签生成实际代码。 这就是其他答案通过存储计数器而提出的建议 在State(或RWS)monad。

还有一个问题我们可以解决:你希望能够同时跳过这两个问题 前进和后退。这很可能是你分开的原因 allocateUniqueLabellabel函数。但这允许设置相同 标签两次。

实际上可以使用do表示法使用“向后”绑定 MonadFix, 它定义了这个monadic操作:

mfix :: (a -> m a) -> m a

由于StateRWS都有MonadFix个实例,我们确实可以编写代码 像这样:

{-# LANGUAGE GeneralizedNewtypeDeriving, RecursiveDo #-}
module X86
    ( X86()
    , runX86
    , label
    , jmp
    ) where

import Control.Monad.RWS

-- In production code it'll be much faster if we replace String with
-- ByteString.
newtype X86 a = X86 (RWS () String Int a)
    deriving (Functor, Applicative, Monad, MonadFix)

runX86 :: X86 a -> String
runX86 (X86 k) = snd (execRWS k () 1)

newtype Label = Label { getLabel :: String }

label :: X86 Label
label = X86 $ do
    counter <- get
    let l = "Label" ++ show counter
    tell (l ++ ":\n")
    modify (+1)
    return (Label l)

jmp :: Label -> X86 ()
jmp (Label l) = X86 . tell $ "jmp " ++ l ++ ";\n"

并像这样使用它:

example :: X86 ()
example = do
    rec l1 <- label
        jmp l2
        l2 <- label
    jmp l1

有几点需要注意:

  • 我们需要使用RecursiveDo扩展名来启用rec关键字。
  • 关键字rec分隔一系列相互递归的定义。在我们的例子中 它也可以稍后开始一行(rec jmp l2)。然后GHC将其翻译成 在内部使用mfix。 (使用已弃用的mdo关键字而不是rec 会使代码更自然。)
  • 我们将内部包裹在X86 newtype中。首先,隐藏它总是好的 内部实现,它允许以后轻松重构。第二,mfix 要求传递给它的函数a -> m a不严格 论点。效果不能取决于参数,否则mfix 发散。这是条件满足我们的功能,但如果 内部暴露,有人可以定义这样一个人为的功能:

    -- | Reset the counter to the specified label.
    evilReset :: Label -> X86 ()
    evilReset = X86 . put . read . drop 5 . getLabel
    

    它不仅会破坏标签的唯一性,还会导致以下代码 挂起:

    diverge :: X86 ()
    diverge = do
        rec evilReset l2
            l2 <- label
        return ()
    

另一个非常相似的替代方案是使用 Rand monad并生成标签 Random 的例子 UUID。 像WriterT String Rand a这样的东西,也有MonadFix个实例。

(从纯粹的学术角度来看,有可能构建一个箭头而不是一个monad,它可以实现 ArrowLoop, 但不允许依赖于值的状态修改,例如evilReset。但X86的封装实现了相同的目标,保持了更友好的do语法。)