模拟Haskell中的交互状态对象

Nat*_*iel 14 monads haskell state-monad lenses haskell-lens

我正在编写一个Haskell程序,它涉及模拟一个抽象机器,它具有内部状态,接受输入并提供输出.我知道如何使用状态monad实现这一点,从而产生更清晰,更易于管理的代码.

我的问题是,当我有两个(或更多)有状态对象相互交互时,我不知道如何使用相同的技巧.下面我给出了一个高度简化的问题版本,并勾勒出我到目前为止的内容.

为了这个问题,让我们假设一个机器的内部状态只包含一个整数寄存器,因此它的数据类型是

data Machine = Register Int
        deriving (Show)
Run Code Online (Sandbox Code Playgroud)

(实际的机器可能有多个寄存器,程序指针,调用堆栈等等,但现在不要担心.)在上一个问题之后我知道如何使用状态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
Run Code Online (Sandbox Code Playgroud)

这让我可以写出类似的东西

program :: State Machine Int
program = do
        addToState 6
        addToState (-4)
        getValue

runProgram = evalState program (Register 0)
Run Code Online (Sandbox Code Playgroud)

这会将6添加到寄存器,然后减去4,然后返回结果.状态monad跟踪机器的内部状态,以便"程序"代码不必明确跟踪它.

在命令式语言的面向对象样式中,这个"程序"代码可能看起来像

def runProgram(machine):
    machine.addToState(6)
    machine.addToState(-4)
    return machine.getValue()
Run Code Online (Sandbox Code Playgroud)

在这种情况下,如果我想模拟两台彼此交互的机器,我可能会写

def doInteraction(machine1, machine2):
    a = machine1.getValue()
    machine1.addToState(-a)
    machine2.addToState(a)
    return machine2.getValue()
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

类型签名interaction :: State (Machine, Machine) Int是Python函数声明的一个很好的直接转换def doInteraction(machine1, machine2):,但代码很脆弱,因为我使用显式let绑定通过函数使用线程状态.这需要我每次想要更改其中一台机器的状态时引入一个新名称,这反过来意味着我必须手动跟踪哪个变量代表最新状态.对于较长的交互,这可能会使代码容易出错且难以编辑.

我希望结果与镜头有关.问题是我不知道如何只在两台机器中的一台机器上运行monadic动作.镜头有一个运算符,<<~其文档说"运行monadic动作,并将Lens的目标设置为其结果",但此动作在当前monad中运行,其中状态是类型(Machine, Machine)而不是Machine.

所以在这一点上我的问题是,如何interaction使用状态monads(或其他一些技巧)以更加命令式/面向对象的方式实现上述函数,以隐式跟踪两台机器的内部状态,而不必明确地传递状态?

最后,我意识到想要在纯函数式语言中编写面向对象的代码可能表明我做错了什么,所以我很容易被另一种方式来思考模拟多个有状态事物的问题.彼此.基本上我只是想知道在Haskell中处理这类问题的"正确方法".

Ale*_*lec 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)
Run Code Online (Sandbox Code Playgroud)

另外,为了测试这段代码,我们可以在GHCi上检查它是否符合我们的要求:

ghci> runState doInteraction (System (Machine 3) (Machine 4))
(7,System {_machine1 = Machine {_register = 0}, _machine2 = Machine {_register = 7}})
Run Code Online (Sandbox Code Playgroud)

好处:

  • 通过使用记录lens,如果我决定添加额外的字段,将不会进行重构.例如,假设我想要第三台机器,那么我所做的就是改变System:

    data System = System
      { _machine1, _machine2, _machine3 :: Machine } deriving (Show)
    
    Run Code Online (Sandbox Code Playgroud)

    但没有在我现有的代码将改变别人-刚才我将能够使用machine3像我使用machine1machine2.

  • 通过使用lens,我可以更容易地扩展到嵌套结构.请注意,我完全避免了非常简单addToStategetValue功能.由于a 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
    
    Run Code Online (Sandbox Code Playgroud)

    请注意,这相当于在Python中具有如下函数:

    def doInteraction2(machine1,machine2):
      a = machine1.registers[2]
      machine1.registers[2] -= a
      machine2.registers[1] += a
      b = machine2.registers[1]
      return b
    
    Run Code Online (Sandbox Code Playgroud)

    你可以再次在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)]}})
    
    Run Code Online (Sandbox Code Playgroud)

编辑

OP已经指定他想要一种嵌入a State Machine a的方法State System a.lens和往常一样,如果你去深入挖掘就有这样的功能.zoom(和它的兄弟magnify)为"缩放"输出/输入提供设施State/ Reader(它才有意义缩小的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`
Run Code Online (Sandbox Code Playgroud)

但请注意,如果我们这样做,我们必须提交一个特定的状态monad变换器(而不是通用MonadState),因为并非所有存储状态的方式都必须以这种方式"可缩放".也就是说,RWST是由另一个州支持的monad变压器zoom.


Jon*_*rdy 5

一种选择是将状态转换为对Machine值进行操作的纯函数:

getValue :: Machine -> Int
getValue (Register x) = x

addToState :: Int -> Machine -> Machine
addToState i (Register x) = Register (x + i)
Run Code Online (Sandbox Code Playgroud)

然后你可以State根据需要将它们提升,State在多台机器上编写动作,如下所示:

doInteraction :: State (Machine, Machine) Int
doInteraction = do
  a <- gets $ getValue . fst
  modify $ first $ addToState (-a)
  modify $ second $ addToState a
  gets $ getValue . snd
Run Code Online (Sandbox Code Playgroud)

其中first(resp.second)是一个函数Control.Arrow,在这里使用的类型:

(a -> b) -> (a, c) -> (b, c)
Run Code Online (Sandbox Code Playgroud)

也就是说,它修改了元组的第一个元素.

然后按预期runState doInteraction (Register 3, Register 5)生产(8, (Register 0, Register 8)).

(总的来说,我认为你可以通过镜头对这些"放大"的子像素进行"放大",但我并不是很熟悉,不能提供一个例子.)