Simulando objetos interactivos con estado en Haskell

Actualmente estoy escribiendo un programa Haskell que implica simular una máquina abstracta, que tiene un estado interno, toma entrada y da salida. Sé cómo implementar esto usando la mónada estatal, lo que resulta en un código mucho más limpio y manejable.

Mi problema es que no sé cómo hacer el mismo truco cuando tengo dos (o más) objetos con estado interactuando entre sí. A continuación, doy una versión muy simplificada del problema y bosquejo lo que tengo hasta ahora.

En aras de esta pregunta, supongamos que el estado interno de una máquina consta solo de un único registro entero, de modo que su tipo de datos es

data Machine = Register Int
        deriving (Show)

(La máquina real podría tener múltiples registros, un puntero de programa, una pila de llamadas, etc., etc., pero no nos preocupemos por eso por ahora).Pregunta anterior Sé cómo implementar la máquina usando la mónada de estado, para no tener que pasar explícitamente su estado interno. En este ejemplo simplificado, la implementación se ve así, después de importarControl.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

Esto me permite escribir cosas como

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

runProgram = evalState program (Register 0)

Esto agrega 6 al registro, luego resta 4, luego devuelve el resultado. La mónada de estado realiza un seguimiento del estado interno de la máquina para que el código del "programa" no tenga que seguirlo explícitamente.

En un estilo orientado a objetos en un lenguaje imperativo, este código de "programa" podría verse como

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

En ese caso, si quiero simular dos máquinas que interactúan entre sí, podría escribir

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

que establecemachine1's estado a 0, agregando su valor enmachine2's estado y devolver el resultado.

Mi pregunta es sencilla: ¿cuál es la forma paradigmática de escribir este tipo de código imperativo en Haskell? Originalmente pensé que necesitaba encadenar dos mónadas de estado, pero después de una pista de Benjamin Hodgson en los comentarios, me di cuenta de que debería poder hacerlo con una sola mónada de estado donde el estado es una tupla que contiene ambas máquinas.

El problema es que no sé cómo implementar esto en un estilo imperativo limpio y agradable. Actualmente tengo lo siguiente, que funciona pero es poco elegante y frágil:

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)

El tipo de firmainteraction :: State (Machine, Machine) Int es una buena traducción directa de la declaración de la función Pythondef doInteraction(machine1, machine2):, pero el código es frágil porque recurrí al estado de subprocesamiento a través de las funciones usando explícitolet fijaciones Esto requiere que introduzca un nuevo nombre cada vez que quiero cambiar el estado de una de las máquinas, lo que a su vez significa que tengo que hacer un seguimiento manual de qué variable representa el estado más actualizado. Para interacciones más largas, es probable que el código sea propenso a errores y difícil de editar.

Espero que el resultado tenga algo que ver con las lentes. El problema es que no sé cómo ejecutar una acción monádica en solo una de las dos máquinas. Lentes tiene un operador<<~ cuya documentación dice "Ejecutar una acción monádica y establecer el objetivo de Lens a su resultado", pero esta acción se ejecuta en la mónada actual, donde el estado es tipo(Machine, Machine) más bien queMachine.

Entonces, en este punto, mi pregunta es, ¿cómo puedo implementar elinteraction ¿funciona arriba en un estilo más imperativo / orientado a objetos, usando mónadas de estado (o algún otro truco) para hacer un seguimiento implícito de los estados internos de las dos máquinas, sin tener que pasar los estados explícitamente?

Finalmente, me doy cuenta de que querer escribir código orientado a objetos en un lenguaje funcional puro podría ser una señal de que estoy haciendo algo mal, por lo que estoy muy abierto a que me muestren otra forma de pensar sobre el problema de simular varias cosas con estado interactuando juntos. Básicamente, solo quiero saber la "forma correcta" de abordar este tipo de problema en Haskell.

Respuestas a la pregunta(3)

Su respuesta a la pregunta