Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
209 views
in Technique[技术] by (71.8m points)

monads - Simulating interacting stateful objects in Haskell

I'm currently writing a Haskell program that involves simulating an abstract machine, which has internal state, takes input and gives output. I know how to implement this using the state monad, which results in much cleaner and more manageable code.

My problem is that I don't know how to pull the same trick when I have two (or more) stateful objects interacting with one another. Below I give a highly simplified version of the problem and sketch out what I have so far.

For the sake of this question, let's assume a machine's internal state consists only of a single integer register, so that its data type is

data Machine = Register Int
        deriving (Show)

(The actual machine might have multiple registers, a program pointer, a call stack etc. etc., but let's not worry about that for now.) After a previous question I know how to implement the machine using the state monad, so that I don't have to explicitly pass its internal state around. In this simplified example the implementation looks like this, after importing 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

This allows me to write things like

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

runProgram = evalState program (Register 0)

This adds 6 to the register, then subtracts 4, then returns the result. The state monad keeps track of the machine's internal state so that the "program" code doesn't have to explicitly track it.

In object oriented style in an imperative language, this "program" code might look like

def runProgram(machine):
    machine.addToState(6)
    machine.addToState(-4)
    return machine.getValue()

In that case, if I want to simulate two machines interacting with each other I might write

def doInteraction(machine1, machine2):
    a = machine1.getValue()
    machine1.addToState(-a)
    machine2.addToState(a)
    return machine2.getValue()

which sets machine1's state to 0, adding its value onto machine2's state and returning the result.

My question is simply, what is the paradigmatic way to write this kind of imperative code in Haskell? Originally I thought I needed to chain two state monads, but after a hint by Benjamin Hodgson in the comments I realised I should be able to do it with a single state monad where the state is a tuple containing both machines.

The problem is that I don't know how to implement this in a nice clean imperative style. Currently I have the following, which works but is inelegant and fragile:

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)

The type signature interaction :: State (Machine, Machine) Int is a nice direct translation of the Python function declaration def doInteraction(machine1, machine2):, but the code is fragile because I resorted to threading state through the functions using explicit let bindings. This requires me to introduce a new name every time I want to change the state of one of the machines, which in turn means I have to manually keep track of which variable represents the most up-to-date state. For longer interactions this is likely to make the code error-prone and hard to edit.

I expect that the result will have something to do with lenses. The problem is that I don't know how to run a monadic action on only one of the two machines. Lenses has an operator <<~ whose documentation says "Run a monadic action, and set the target of Lens to its result", but this action gets run in the current monad, where the state is type (Machine, Machine) rather than Machine.

So at this point my question is, how can I implement the interaction function above in a more imperative / object-oriented style, using state monads (or some other trick) to implicitly keep track of the internal states of the two machines, without having to pass the states around explicitly?

Finally, I realise that wanting to write object oriented code in a pure functional language might be a sign that I'm doing something wrong, so I'm very open to being shown another way to think about the problem of simulating multiple stateful things interacting with each other. Basically I just want to know the "right way" to approach this sort of problem in Haskell.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I think good practice would dictate that you should actually make a System data type to wrap your two machines, and then you might as well use 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)

Also, just to test this code, we can check at GHCi that it does what we want:

ghci> runState doInteraction (System (Machine 3) (Machine 4))
(7,System {_machine1 = Machine {_register = 0}, _machine2 = Machine {_register = 7}})

Advantages:

  • By using records and lens, there will be no refactoring if I decide to add extra fields. For example, say I want a third machine, then all I do is change System:

    data System = System
      { _machine1, _machine2, _machine3 :: Machine } deriving (Show)
    

    But nothing else in my existing code will change - just now I will be able to use machine3 like I use machine1 and machine2.

  • By using lens, I can scale more easily to nested structures. Note that I just avoided the very simple addToState and getValue functions completely. Since a Lens is actually just a function, machine1.register is just regular function composition. For example, lets say I want a machine to now have an array of registers, then getting or setting particular registers is still simple. We just modify Machine and 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
    

    Note that this is equivalent to having a function like the following in Python:

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

    You can again test this out on 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)]}})
    

EDIT

The OP has specified that he would like to have a way of embedding a State Machine a into a State System a. lens, as always, has such a function if you go digging deep enough. zoom (and its sibling magnify) provide facilities for "zooming" out/in of State/Reader (it only makes sense to zoom out of State and magnify into Reader).

Then, if we want to implement doInteraction while keeping as black boxes getValue and addToState, we get

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`

Notice however that if we do this we really must commit to a particular state monad transformer (as opposed to the generic MonadState), since not all ways of storing state are going to be necessarily "zoomable" in this way. That said, RWST is another state monad transformer supported by zoom.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...