Имитация взаимодействующих объектов с сохранением состояния в Haskell

В настоящее время я пишу программу на 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состояние и возвращение результата.

Мой вопрос просто: каков парадигматический способ написания такого рода императивного кода на Хаскеле? Первоначально я думал, что мне нужно соединить две монады состояний, но после намека Бенджамина Ходжсона в комментариях я понял, что смогу сделать это с одной монадой состояний, где состоянием является кортеж, содержащий обе машины.

Проблема в том, что я не знаю, как реализовать это в хорошем чистом императивном стиле. В настоящее время у меня есть следующее, которое работает, но не элегантно и хрупко:

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 привязок. Это требует, чтобы я вводил новое имя каждый раз, когда я хочу изменить состояние одной из машин, что, в свою очередь, означает, что я должен вручную отслеживать, какая переменная представляет самое современное состояние. Для более длительных взаимодействий это может сделать код подверженным ошибкам и трудным для редактирования.

Я ожидаю, что результат будет как-то связан с линзами. Проблема в том, что я не знаю, как запустить монадическое действие только на одной из двух машин. Линзы имеет оператор <<~ чья документация гласит: "Запустить монадическое действие и установить цель Lens для его результата", но это действие запускается в текущей монаде, где состояние имеет тип (Machine, Machine) скорее, чем Machine,

Итак, на данный момент мой вопрос, как я могу реализовать interaction работать выше в более императивном / объектно-ориентированном стиле, используя монады состояний (или какой-то другой трюк), чтобы неявно отслеживать внутренние состояния двух машин, без необходимости явно передавать состояния вокруг?

Наконец, я понимаю, что желание писать объектно-ориентированный код на чистом функциональном языке может быть признаком того, что я делаю что-то не так, поэтому я очень открыт для того, чтобы мне показали другой способ решения проблемы симуляции взаимодействия множества состояний друг с другом. По сути, я просто хочу знать "правильный путь" решения такой проблемы в Хаскеле.

3 ответа

Я думаю, что хорошая практика будет диктовать, что вы должны сделать 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 как я использую machine1 а также machine2,

  • Используя lens Я могу легче масштабировать до вложенных структур. Обратите внимание, что я просто избежал очень простого addToState а также getValue функционирует полностью. Так как Lens на самом деле просто функция, machine1.register это просто регулярная композиция функций. Например, допустим, я хочу, чтобы у машины теперь был массив регистров, тогда получение или установка определенных регистров все еще просты. Мы просто модифицируем Machine а также 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
    

    Обратите внимание, что это эквивалентно наличию функции, подобной следующей в 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)]}})
    

РЕДАКТИРОВАТЬ

ОП указал, что он хотел бы иметь способ встраивания 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`

Однако обратите внимание, что если мы делаем это, мы действительно должны фиксировать определенный монадный преобразователь состояния (в отличие от общего MonadState), поскольку не все способы сохранения состояния будут обязательно "масштабируемыми" таким образом. Это сказало, RWST еще один государственный монадный трансформатор, поддерживаемый zoom,

Одним из вариантов является преобразование ваших состояний в чистые функции, работающие на 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

куда first (Соотв. second) является функцией от Control.Arrowиспользуется здесь с типом:

(a -> b) -> (a, c) -> (b, c)

То есть он изменяет первый элемент кортежа.

затем runState doInteraction (Register 3, Register 5) производит (8, (Register 0, Register 8)) как и ожидалось.

(В общем, я думаю, что вы могли бы сделать такое "увеличение" подзначений с помощью линз, но я не совсем знаком, чтобы привести пример.)

Вы также можете использовать библиотеку Габриэля Гонсалеса Pipes для случая, который вы иллюстрировали. Учебное пособие для библиотеки является одним из лучших кусков документации Haskell из существующих.

Ниже показан простой пример (не проверен).

-- 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

Затем вы можете объединить с помощью оператора>->. Примером может быть запуск

run :: IO ()
run :: runEffect $ P.stdinLn >-> machine1 >-> machine2 >-> P.stdoutLn

Обратите внимание, что это возможно, хотя немного сложнее иметь двунаправленные каналы, что обеспечивает связь между обеими машинами. Используя некоторые другие экосистемы каналов, вы также можете использовать асинхронные каналы для моделирования недетерминированной или параллельной работы машин.

Я верю, что то же самое может быть достигнуто с помощью библиотеки каналов, но у меня нет большого опыта с этим.

Другие вопросы по тегам