模拟Haskell中的交互状态对象

时间:2016-10-02 03:50:35

标签: haskell monads lens state-monad lenses

我正在编写一个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中处理这类问题的“正确方法”。

3 个答案:

答案 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,就像我使用machine1machine2一样。

  • 通过使用lens,我可以更轻松地扩展到嵌套结构。请注意,我完全避免使用非常简单的addToStategetValue函数。由于Lens实际上只是一个函数,machine1.register只是常规的函数组合。例如,假设我希望机器现在有一个数组寄存器,那么获取或设置特定寄存器仍然很简单。我们只修改MachinedoInteraction

    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才有意义。)

然后,如果我们想要在保持黑框doInteractiongetValue的同时实施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

请注意,虽然可以使用双向管道,但这可以让您在两台机器之间进行通信。使用其他一些管道生态系统,您还可以使用异步管道来模拟机器的非确定性或并行操作。

我相信使用管道库可以实现同样的目标,但我对它没有多少经验。