我正在编写一个Haskell程序,它涉及模拟一个抽象机器,它具有内部状态,接受输入并提供输出。我知道如何使用状态monad实现它,这样可以产生更清晰,更易于管理的代码。
我的问题是,当我有两个(或更多)有状态对象相互交互时,我不知道如何使用相同的技巧。下面我给出了一个高度简化的问题版本,并勾勒出我到目前为止的内容。
为了这个问题,我们假设机器的内部状态只包含一个整数寄存器,因此其数据类型为
data Machine = Register Int
deriving (Show)
(实际的机器可能有多个寄存器,程序指针,调用堆栈等等,但现在不要担心。)在previous question之后我知道如何使用状态monad,这样我就不必明确传递其内部状态了。在此简化示例中,导入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,然后返回结果。状态monad跟踪机器的内部状态,因此“程序”代码不必明确跟踪它。
在命令式语言的面向对象样式中,这个“程序”代码可能看起来像
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中编写这种命令式代码的范式方法是什么?最初我以为我需要链接两个状态monad,但是在评论中由Benjamin Hodgson提示后,我意识到我应该能够使用单个状态monad来处理状态,其中状态是包含两个机器的元组。
问题在于我不知道如何以一种漂亮干净的命令式风格来实现它。目前我有以下内容,但有效但不够优雅和脆弱:
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
绑定通过函数处理线程状态。这需要我每次想要更改其中一台机器的状态时引入一个新名称,这反过来意味着我必须手动跟踪哪个变量代表最新状态。对于较长的交互,这可能会使代码容易出错且难以编辑。
我希望结果与镜头有关。问题是我不知道如何只在两台机器中的一台机器上运行monadic动作。镜头有一个运算符<<~
,其文档说“运行monadic动作,并将Lens的目标设置为其结果”,但此动作在当前monad中运行,其中状态为(Machine, Machine)
类型比Machine
。
所以在这一点上我的问题是,如何使用状态monads(或其他一些技巧)隐式跟踪内部状态,以更强制/面向对象的方式实现上面的interaction
函数这两台机器,没有明确地传递状态?
最后,我意识到想要用纯函数式语言编写面向对象的代码可能表明我做错了什么,所以我很容易被另外一种方式来思考模拟多个问题有状态的东西互相交流。基本上我只是想知道在Haskell中处理这类问题的“正确方法”。
答案 0 :(得分:15)
我认为良好的做法会要求您实际上应该使用System
数据类型来包装两台机器,然后您也可以使用lens
。
{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
import Control.Lens
import Control.Monad.State.Lazy
-- With these records, it will be very easy to add extra machines or registers
-- without having to refactor any of the code that follows
data Machine = Machine { _register :: Int } deriving (Show)
data System = System { _machine1, _machine2 :: Machine } deriving (Show)
-- This is some TemplateHaskell magic that makes special `register`, `machine1`,
-- and `machine2` functions.
makeLenses ''Machine
makeLenses ''System
doInteraction :: MonadState System m => m Int
doInteraction = do
a <- use (machine1.register)
machine1.register -= a
machine2.register += a
use (machine2.register)
另外,为了测试这段代码,我们可以在GHCi上检查它是否符合我们的要求:
ghci> runState doInteraction (System (Machine 3) (Machine 4))
(7,System {_machine1 = Machine {_register = 0}, _machine2 = Machine {_register = 7}})
通过使用记录和lens
,如果我决定添加额外字段,则不会进行重构。例如,假设我想要一台第三台机器,那么我所做的就是更改System
:
data System = System
{ _machine1, _machine2, _machine3 :: Machine } deriving (Show)
但我现有代码中没有任何其他内容会发生变化 - 刚才我可以使用machine3
,就像我使用machine1
和machine2
一样。
通过使用lens
,我可以更轻松地扩展到嵌套结构。请注意,我完全避免使用非常简单的addToState
和getValue
函数。由于Lens
实际上只是一个函数,machine1.register
只是常规的函数组合。例如,假设我希望机器现在有一个数组寄存器,那么获取或设置特定寄存器仍然很简单。我们只修改Machine
和doInteraction
:
import Data.Array.Unboxed (UArray)
data Machine = Machine { _registers :: UArray Int Int } deriving (Show)
-- code snipped
doInteraction2 :: MonadState System m => m Int
doInteraction2 = do
Just a <- preuse (machine1.registers.ix 2) -- get 3rd reg on machine1
machine1.registers.ix 2 -= a -- modify 3rd reg on machine1
machine2.registers.ix 1 += a -- modify 2nd reg on machine2
Just b <- preuse (machine2.registers.ix 1) -- get 2nd reg on machine2
return b
请注意,这相当于在Python中使用如下函数:
def doInteraction2(machine1,machine2):
a = machine1.registers[2]
machine1.registers[2] -= a
machine2.registers[1] += a
b = machine2.registers[1]
return b
你可以再次在GHCi上测试一下:
ghci> import Data.Array.IArray (listArray)
ghci> let regs1 = listArray (0,3) [0,0,6,0]
ghci> let regs2 = listArray (0,3) [0,7,3,0]
ghci> runState doInteraction (System (Machine regs1) (Machine regs2))
(13,System {_machine1 = Machine {_registers = array (0,3) [(0,0),(1,0),(2,0),(3,0)]}, _machine2 = Machine {_registers = array (0,3) [(0,0),(1,13),(2,3),(3,0)]}})
OP指出他希望有一种方法可以将State Machine a
嵌入到State System a
中。如果你去深入挖掘,lens
一如既往地具有这样的功能。 zoom
(及其兄弟magnify
)为&#34;缩放&#34;提供了设施。 State
/ Reader
的out / in(只有缩小State
并放大为Reader
才有意义。)
然后,如果我们想要在保持黑框doInteraction
和getValue
的同时实施addToState
,我们就会得到
getValue :: State Machine Int
addToState :: Int -> State Machine ()
doInteraction3 :: State System Int
doInteraction3 = do
a <- zoom machine1 getValue -- call `getValue` with state `machine1`
zoom machine1 (addToState (-a)) -- call `addToState (-a)` with state `machine1`
zoom machine2 (addToState a) -- call `addToState a` with state `machine2`
zoom machine2 getValue -- call `getValue` with state `machine2`
请注意,如果我们这样做,我们必须提交一个特定的状态monad变换器(而不是通用的MonadState
),因为不是所有存储状态的方式都必然是&#34;可缩放的& #34;通过这种方式。也就是说,RWST
是由zoom
支持的另一个状态monad变换器。
答案 1 :(得分:5)
一种选择是将状态转换为在ValueError: XPath error: Invalid type in div[@class="my_class"]/text() | concat('@', div[@class="my_class"]/a/text())
值上运行的纯函数:
Machine
然后您可以根据需要将它们提升到getValue :: Machine -> Int
getValue (Register x) = x
addToState :: Int -> Machine -> Machine
addToState i (Register x) = Register (x + i)
,在多台计算机上编写State
操作,如下所示:
State
其中doInteraction :: State (Machine, Machine) Int
doInteraction = do
a <- gets $ getValue . fst
modify $ first $ addToState (-a)
modify $ second $ addToState a
gets $ getValue . snd
(resp。first
)是来自second
的函数,此处使用的类型为:
Control.Arrow
也就是说,它会修改元组的第一个元素。
然后(a -> b) -> (a, c) -> (b, c)
按预期生成runState doInteraction (Register 3, Register 5)
。
(总的来说,我认为你可以通过镜头对子像素进行“放大”,但我不太熟悉,不能提供一个例子。)
答案 2 :(得分:4)
你也可以使用Gabriel Gonzales的Pipes库来说明你所说的情况。该库的教程是现有的Haskell文档中最好的部分之一。
下面举例说明一个简单的例子(未经测试)。
fetch
然后您可以使用&gt; - &gt;进行组合运营商。一个例子是运行
-- machine 1 adds its input to current state
machine1 :: (MonadIO m) => Pipe i o m ()
machine1 = flip evalStateT 0 $ forever $ do
-- gets pipe input
a <- lift await
-- get current local state
s <- get
-- <whatever>
let r = a + s
-- update state
put r
-- fire down pipeline
yield r
-- machine 2 multiplies its input by current state
machine2 :: (MonadIO m) => Pipe i o m ()
machine2 = flip evalStateT 0 $ forever $ do
-- gets pipe input
a <- lift await
-- get current local state
s <- get
-- <whatever>
let r = a * s
-- update state
put r
-- fire down pipeline
yield r
请注意,虽然可以使用双向管道,但这可以让您在两台机器之间进行通信。使用其他一些管道生态系统,您还可以使用异步管道来模拟机器的非确定性或并行操作。
我相信使用管道库可以实现同样的目标,但我对它没有多少经验。