【问题标题】:Different, interacting levels of state in haskellhaskell 中不同的、交互的状态级别
【发布时间】:2012-02-23 23:35:46
【问题描述】:

我正在模拟一个 4 位微处理器。我需要跟踪寄存器、内存和运行输出(还有一个获取执行周期计数器的奖励点)。我已经设法在没有单子的情况下做到了这一点,但是一次明确地传递这么多东西感觉很混乱。此外,函数定义混乱、冗长且难以阅读。

我试过用 monad 来做这件事,但它就是不适合。我尝试将所有单独的状态组件视为单一类型,但这给我留下了如何制作价值的问题。

State Program () -- Represents the state of the processor after a single iteration of the fetch execute cycle

是唯一有意义的类型。但在这一点上,为什么还要打扰呢?我尝试通过将字符串从复合类型中拉出并将其视为值来分解它

State Program' String

效果很好,除了我需要 RUNNING 输出。无论我做什么,我都无法同时保持字符串和状态。

现在我正在努力解决 monad 转换器。似乎我必须分离出所有不同级别的状态。但是我的脑袋快爆炸了。

StateT Registers (StateT Memory (State Output)) a = 
StateT (registers -> (StateT Memory (State Output)) (a,registers))

StateT Registers (StateT Memory (State Output)) a = 
StateT (registers -> (Memory -> (Output -> (((a,Registers),Memory),Output))))

我还没把FEcycle计数器放进去!

问题:

  1. 我在正确的轨道上吗?
  2. 看到我现在正在拔出 monad 转换器,是否可以停止将“运行输出”视为状态,而只是将其交给 IO monad?那太棒了,我可以打印它而不是坚持它。
  3. 我应该将状态分为多少层?我可以看到两个不同的层,但它们彼此密切相关(内存和寄存器都取决于内存和寄存器的状态)。我应该将它们作为一个单一的状态保持在一起还是将它们分开并堆叠起来?哪种方法会产生最易读的代码?

【问题讨论】:

标签: haskell state monads monad-transformers state-monad


【解决方案1】:

将多个状态 monad 相互叠加是一个坏主意:您必须组合一组 lifts 才能获取每个状态,仅通过堆栈中的层数来识别。呸!实际上,mtl 库通常被设计为用于堆栈中的每个“种类”的一个 monad 转换器,除了极少数例外。

相反,我建议StateT Program IO ()。状态接口是相同的,正如您所说,您只需使用liftIO 即可在IO 中输出。当然,值类型是(),但这有什么问题呢?您可以从顶级模拟器返回没有相关的值。而且,当然,您可能拥有更小的、可重用的组件作为模拟器的part,并且这些组件将具有相关的结果类型。 (确实,get 就是这样一个组件。)在顶层没有有意义的返回值并没有错。

就方便地访问该州的每个部分而言,您正在寻找的是镜头; this Stack Overflow answer 是一个很好的介绍。它们让您可以简单轻松地访问和修改您的状态的独立部分。例如,使用data-lens 实现,您可以轻松编写类似regA += 1 的内容来增加regA,或使用stack %= drop 2 来删除堆栈的前两个元素。

当然,它本质上是将您的代码转换为一组全局变量的命令式突变,但这实际上是一个优势,因为这正是您正在模拟的 CPU 所基于的范例。而且使用data-lens-template 包,您可以在一行中从记录定义中派生这些镜头。

【讨论】:

  • 这太棒了。是时候学习模板haskell了。是否仍然很容易找到支持 regA += 1 之类的功能定义?因为虽然它很好读,并且最清楚地表达了(非常必要的)意图,但它看起来不像函数式代码。
  • 您不必学习 Template Haskell 即可使用 data-lens-template;只需将{-# LANGUAGE TemplateHaskell #-} 粘贴在文件顶部,将makeLenses [''Program] 粘贴在Program 定义下方的某个位置即可。至于(+=)的定义,当然;它们只是 StateT 的核心镜头 API 的简单包装器; the source 链接自 Hackage 文档。
  • 小问题 - 分层状态单子是个坏主意 - 除非你真的想要它们。它们允许对状态进行分区,因此您可以限制某些客户端仅具有对状态的特定层的读取访问权限而不是读写访问权限,这用于“安全意识”代码。否则很好的答案。
  • @stephentetley:确实,因此是“罕见的例外”:) 我倾向于将这些东西包装在 newtypes 中,以避免您在使用 MonadState 定义时遇到的歧义。
  • 我最后一次使用这种设计模式:当我完成优化时,签名已经从 State Program [Word8] 变成了 ReaderT (STProgram s) (ST s) [Word8]
【解决方案2】:

一个简单的方法是创建一个表示寄存器和内存的数据类型:

data Register = ...
data Memory = ...
data Machine = Machine [Register] Memory

然后有一些更新寄存器/内存的功能。现在将此类型用于您的状态和您的类型的输出:

type Simulation = State Machine Output

现在每个操作都可以是以下形式:

operation previous = do machine <- get
                        (result, newMachine) <- operate on machine
                        put newMachine
                        return result

这里previous是机器之前的输出。您也可以将其合并到结果中。

所以Machine类型代表机器的状态;您正在通过它线程化先前操作的输出。

更复杂的方法是使用state threads (Control.Monad.ST)。这些允许您在函数内部使用可变引用和数组,同时在外部保证纯度。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-07-26
    • 1970-01-01
    • 1970-01-01
    • 2017-02-28
    • 1970-01-01
    • 2019-07-27
    • 1970-01-01
    • 2018-09-17
    相关资源
    最近更新 更多