我的问题是,当我有两个(或更多)有状态的对象相互交互时,我不知道如何使用相同的技巧。下面是问题的高度简化版本,并概述了我目前的情况。
为了解决这个问题,假设机器的内部状态仅由一个整数寄存器组成,因此它的数据类型为:
data Machine = Register Int
deriving (Show)
实际机器可能有多个寄存器、程序指针、调用堆栈等等,但现在我们不用担心这些。在之前的问题中,我知道如何使用状态单子来实现该机器,因此我不必显式地传递其内部状态。在这个简化的例子中,导入Control.Monad.State.Lazy
后,实现看起来像这样:
addToState :: Int -> State Machine ()
addToState i = do
(Register x) <- get
put $ Register (x + i)
getValue :: State Machine Int
getValue = do
(Register i) <- get
return i
这让我可以编写类似以下的内容:
program :: State Machine Int
program = do
addToState 6
addToState (-4)
getValue
runProgram = evalState program (Register 0)
这段代码将6加到寄存器中,然后减去4,并返回结果。状态单子模式跟踪机器的内部状态,以便“程序”代码不必显式地跟踪它。
在面向对象风格的命令式语言中,“程序”代码可能看起来像:
def runProgram(machine):
machine.addToState(6)
machine.addToState(-4)
return machine.getValue()
在这种情况下,如果我想模拟两台机器相互交互,我可能会写成:
def doInteraction(machine1, machine2):
a = machine1.getValue()
machine1.addToState(-a)
machine2.addToState(a)
return machine2.getValue()
这段代码将machine1
的状态设置为0,将其值添加到machine2
的状态中并返回结果。
我的问题很简单,就是如何用Haskell编写这种命令式代码的典型方式?最初我认为需要链接两个状态单子,但在Benjamin Hodgson的提示下,我意识到应该能够使用单个状态单子来完成,其中状态是包含两个机器的元组。
问题是我不知道如何以漂亮干净的命令式风格实现它。目前我有以下代码,虽然可以工作,但不够优雅且易错:
interaction :: State (Machine, Machine) Int
interaction = do
(m1, m2) <- get
let a = evalState (getValue) m1
let m1' = execState (addToState (-a)) m1
let m2' = execState (addToState a) m2
let result = evalState (getValue) m2'
put $ (m1',m2')
return result
doInteraction = runState interaction (Register 3, Register 5)
类型签名
interaction :: State (Machine, Machine) Int
是 Python 函数声明 def doInteraction(machine1, machine2):
的一个好的直接翻译,但这段代码是脆弱的,因为我使用显式的 let
绑定将状态线程化通过函数。这要求我每次想要改变其中一台机器的状态时都要引入一个新名称,这反过来又意味着我必须手动跟踪哪个变量表示最新的状态。对于更长时间的交互,这很可能使代码容易出错并且难以编辑。我希望结果与 Lenses 有关。问题是我不知道如何在两台机器中只运行单个 monadic 操作。Lenses 有一个运算符
<<~
,其文档说“运行一个 monadic 操作,并将 Lens 的目标设置为其结果”,但此操作在当前 monad 中运行,其中状态类型为 (Machine, Machine)
而不是 Machine
。所以现在我的问题是,如何以更具命令式 / 面向对象的风格实现上述的
interaction
函数,使用状态 monads(或其他技巧)隐式地跟踪两台机器的内部状态,而无需显式地传递状态?最后,我意识到,在一个纯函数语言中想要编写面向对象的代码可能是我正在做错事的迹象,因此我非常愿意展示另一种思考模拟多个有状态的东西相互作用问题的方法。基本上,我只想知道在 Haskell 中处理这种问题的“正确方式”。
def doInteraction(machine1, machine2)
被翻译为doInteraction :: State (Machine, Machine) Int
。 - Benjamin HodgsonState
来模拟抽象机器对我来说似乎非常奇怪。您确定不想使用通常的“严格”State
吗?我在“严格”一词周围加上引号,因为它的严格性仅来自对成对操作执行案例分析,而不是执行任何奇怪的操作。惰性State
才是奇怪的。 - dfeuer