В настоящее время я пишу программу Haskell, которая включает в себя моделирование абстрактной машины, которая имеет внутреннее состояние, принимает входные данные и выводит результат. Я знаю, как реализовать это, используя государственную монаду, которая приводит к значительно более чистым и управляемым кодам.
Моя проблема заключается в том, что я не знаю, как вытащить тот же трюк, когда у меня есть два (или более) объекта с состоянием, взаимодействующих друг с другом. Ниже я даю очень упрощенную версию проблемы и набросаю то, что у меня есть до сих пор.
Для этого вопроса предположим, что внутреннее состояние машины состоит только из одного целочисленного регистра, поэтому его тип данных
data Machine = Register Int
deriving (Show)
(Фактическая машина может иметь несколько регистров, указатель на программу, стек вызовов и т.д. и т.д., но пока не беспокойтесь об этом.) После предыдущего вопроса Я знаю, как реализовать машину, используя государственную монаду, так что мне не нужно явно передавать ее внутреннее состояние. В этом упрощенном примере реализация выглядит так, после импорта 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, а затем возвращает результат. Государственная монада отслеживает внутреннее состояние машины, так что "программный" код не должен явно отслеживать его.
В объектно-ориентированном стиле на императивном языке этот "программный" код может выглядеть как
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? Первоначально я думал, что мне нужно связать две государственные монады, но после намека Бенджамина Ходжсона в комментариях я понял, что должен сделать это с помощью одной государственной монады, где государство является кортежем, содержащим обе машины.
Проблема в том, что я не знаю, как реализовать это в хорошем чистом императивном стиле. В настоящее время у меня есть следующее, которое работает, но является неэлегантным и хрупким:
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
. Это требует, чтобы я вводил новое имя каждый раз, когда я хочу изменить состояние одной из машин, что, в свою очередь, означает, что я должен вручную отслеживать, какая переменная представляет собой самое современное состояние. Для более длительных взаимодействий это, вероятно, сделает код уязвимым и трудно отредактированным.
Я ожидаю, что результат будет иметь какое-то отношение к объективам. Проблема в том, что я не знаю, как выполнять монадическое действие только на одной из двух машин. В объективах есть оператор <<~
, в документации которого говорится: "Запустите монодическое действие и установите цель объектива на результат", но это действие запускается в текущей монаде, где состояние типа (Machine, Machine)
, а не Machine
.
Итак, на данный момент мой вопрос: как я могу реализовать функцию interaction
выше в более императивном/объектно-ориентированном стиле, используя государственные монады (или какой-либо другой трюк), чтобы неявно отслеживать внутренние состояния две машины, без необходимости явно передавать состояния?
Наконец, я понимаю, что желание писать объектно-ориентированный код на чистом функциональном языке может быть признаком того, что я делаю что-то неправильно, поэтому я очень открыт, чтобы показать другой способ подумать о проблеме имитации множественных состояния, взаимодействующие друг с другом. В основном я просто хочу знать "правильный путь", чтобы подойти к этой проблеме в Haskell.