【问题标题】:What's the proper way to emulate stateful closure in Haskell在 Haskell 中模拟有状态关闭的正确方法是什么
【发布时间】:2017-03-01 12:15:54
【问题描述】:

上下文:我需要编写一个几乎无状态的编译器,将 VM 字节码转换为机器码。大多数 VM 命令都可以使用如下纯函数进行无状态转换:

compilePop = ["mov ax, @sp", "dec ax", "mov @sp, ax"]

compile :: VM_COMMAND -> [String]
compile STACK_POP = compilePop 

-- compile whole program
compileAll :: [VM_COMMAND] -> [String]
compileAll = flatMap compile

但有些命令需要插入标签,每次调用都应该不同。

我了解如何对整个编译器使用“全局”状态对象:

compileGt n = [label ++ ":", "cmp ax,bx", "jgt " ++ label]
                where label = "cmp" ++ show n

compile :: Int -> COMPILER_STATE -> VM_COMMAND -> (COMPILER_STATE, [String])
-- here state currently contains only single integer, but it will grow larger
compile lcnt STACK_POP = (lcnt, compilePop)
compile lcnt CMP_GT    = (lcnt + 1, compileGt lcnt)

compileAll commands = snd $ foldr compile commands 0
                      -- incorrect, but you get the idea

但我认为这很糟糕,因为每个专门的编译函数只需要一小部分状态,甚至根本不需要。例如,在非纯函数式 JavaScript 中,我会在闭包中实现具有本地状态的专门编译函数。

// compile/gt.js
var i = 0;
export default const compileGt = () => {
  const label = "cmp" + i++;
  return [label ++ ":", "cmp ax,bx", "jgt " ++ label];
};
// index.js
import compileGt from './compile/gt';

function compile (cmd) {
  switch (cmd) {
  case CMP_GT: return compileGt();
  // ...
  }
}

export default const compileAll = (cmds) => cmds.flatMap(compile);

所以问题是我如何在 Haskell 中做同样的事情,或者解释为什么它真的是个坏主意。应该是这样的吗?

type compileFn = State -> VM_COMMAND -> [String]
(compileFn, State) -> VM_COMMAND -> ([String], (compileFn, State))

【问题讨论】:

  • 使用状态单子。
  • @BenjaminHodgson 这很明显,但我不明白它如何回答我关于全球与本地状态的问题。我在问题的最后提出的界面是否正确?特定编译器的状态不同,如何组合?

标签: haskell closures state-monad


【解决方案1】:

如果你有...

data Big = Big { little :: Little, stuff :: Whatever }

...你可以定义你的...

littleProcessor :: State Little [String]

...然后使用类似这样的函数...

innerState :: Monad m 
    => (s -> i) -> (i -> s -> s) -> StateT i m a -> StateT s m a
innerState getI setI (StateT m) = StateT $ \s -> do
    (a, i) <- m (getI s)
    return (a, setI i s)

...将其提升到更大的状态:

bigProcessor :: State Big [String]
bigProcessor = innerState little (\l b -> b {little = l}) littleProcessor

(添加辅助定义以适应口味。)

在innerState 中使用 getter/setter 对使得它看起来应该可以用镜头来表达。实际上,lens 中的zoom 基本上是innerState,具有最小化的样板:

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data Big = Big { _little :: Little, _stuff :: Whatever }
makeLenses ''Big -- little is now a lens.
bigProcessor :: State Big [String]
bigProcessor = zoom little littleProcessor

【讨论】:

  • 您的回答非常深刻,但我想澄清一下它是如何回答我的具体问题的。我是否正确理解您,您建议编译器具有“全局”状态,并通过关注此全局状态的镜头为专门的编译器函数创建局部状态?
  • 是的,就是这样。关键是State Little 计算无法访问Big 状态的其余部分,即使zoom 允许您将其用作State Big 计算。
  • 这里我们实现了隔离,但是如果我想添加新的VM命令和新的编译器,我需要重新编译Big source。将作用域与(State Scope, Scope -&gt; In -&gt; (Scope, Out)) 之类的函数保持在一起是否可能而且不太疯狂?
  • @kirilloid [1/2] (1) 请注意,例如State Scope Int 实际上并不存储Scope:它是Int 的计算,它使用(并转换)将在使用点提供的Scope 状态。换句话说,Scope -&gt; In -&gt; (Scope, Out) 等价于In -&gt; State Scope Out。 (2) “如果我想添加新的 VM 命令和新的编译器,我需要重新编译 Big source”——不一定。如果您的新命令可以根据已在 Big 中定义的子状态定义,则无需更改 Big。
  • @kirilloid [2/2] (3) 你可能会发现this question 读起来很有趣,但我强烈怀疑你实际上并不需要那里讨论的诡计。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-15
  • 1970-01-01
  • 1970-01-01
  • 2017-05-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多