Предположим, вы создаете довольно большую симуляцию в Haskell. Существует множество различных типов объектов, атрибуты которых обновляются по мере продвижения имитации. Скажем, ради примера, что ваши сущности называются обезьянами, слонами, медведями и т.д.
Каков ваш предпочтительный метод для сохранения состояний этих объектов?
Первый и самый очевидный подход, о котором я думал, был следующим:
mainLoop :: [Monkey] -> [Elephant] -> [Bear] -> String
mainLoop monkeys elephants bears =
let monkeys' = updateMonkeys monkeys
elephants' = updateElephants elephants
bears' = updateBears bears
in
if shouldExit monkeys elephants bears then "Done" else
mainLoop monkeys' elephants' bears'
Это уже уродливо, если каждый тип объекта явно указан в сигнатуре функции mainLoop
. Вы можете себе представить, как это будет ужасно, если бы у вас было, скажем, 20 типов сущностей. (20 не является необоснованным для сложных симуляций.) Поэтому я считаю, что это неприемлемый подход. Но его спасительная благодать заключается в том, что функции типа updateMonkeys
очень ясны в том, что они делают: они берут список обезьян и возвращают новый.
Итак, следующая мысль заключалась бы в том, чтобы перевернуть все в одну большую структуру данных, которая содержит все состояние, очищая таким образом подпись mainLoop
:
mainLoop :: GameState -> String
mainLoop gs0 =
let gs1 = updateMonkeys gs0
gs2 = updateElephants gs1
gs3 = updateBears gs2
in
if shouldExit gs0 then "Done" else
mainLoop gs3
Некоторые предположили бы, что мы завершаем GameState
вверх в State Monad и вызываем updateMonkeys
и т.д. в do
. Это здорово. Некоторые предпочли бы предложить нам очистить его функциональным составом. Также прекрасно, я думаю. (Кстати, я новичок в Haskell, так что, возможно, я ошибаюсь в некоторых из них.)
Но тогда проблема в том, что функции типа updateMonkeys
не дают вам полезной информации из их сигнатуры типа. Вы не можете быть уверены, что они делают. Конечно, updateMonkeys
- описательное имя, но это небольшое утешение. Когда я передаю объект god и скажу "пожалуйста, обновите мое глобальное состояние", я чувствую, что мы вернулись в императивный мир. Это похоже на глобальные переменные с помощью другого имени: у вас есть функция, которая делает что-то в глобальном состоянии, вы называете это, и вы надеетесь на лучшее. (Я полагаю, что вы все еще избегаете проблем с concurrency, которые будут присутствовать с глобальными переменными в императивной программе. Но meh, concurrency не является почти единственной ошибкой глобальных переменных.)
Еще одна проблема заключается в следующем: предположим, что объекты должны взаимодействовать. Например, у нас есть такая функция:
stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
(elongateEvilGrin elephant, decrementHealth monkey)
Скажите, что это вызвано в updateElephants
, потому что там, где мы проверяем, есть ли какой-либо из слонов в топании диапазона любых обезьян. Как вы элегантно распространяете изменения как обезьян, так и слонов в этом сценарии? В нашем втором примере updateElephants
принимает и возвращает объект god, поэтому он может влиять на оба изменения. Но это только путает воды дальше и усиливает мою мысль: с божественным объектом вы фактически просто изменяете глобальные переменные. И если вы не используете объект god, я не уверен, как вы распространяете эти типы изменений.
Что делать? Конечно, многие программы должны управлять сложным состоянием, поэтому я предполагаю, что есть некоторые известные подходы к этой проблеме.
Просто для сравнения, вот как я могу решить проблему в мире ООП. Там будут объекты Monkey
, Elephant
и т.д. У меня, вероятно, были бы методы класса для поиска в наборе всех живых животных. Может быть, вы можете искать по местоположению, по ID, что угодно. Благодаря структурам данных, лежащим в основе функций поиска, они оставались выделенными в куче. (Я принимаю GC или подсчет ссылок.) Их переменные-члены будут мутировать все время. Любой метод любого класса мог бы мутировать любое живое животное любого другого класса. Например. a Elephant
может иметь метод stomp
, который уменьшал бы здоровье переданного объекта Monkey
, и не было бы необходимости передавать этот
Аналогично, в Erlang или другом актер-ориентированном дизайне вы могли бы решить эти проблемы довольно элегантно: каждый актер поддерживает свою собственную петлю и, следовательно, свое собственное состояние, поэтому вам никогда не нужен объект-бог. Передача сообщений позволяет одной активности объекта инициировать изменения в других объектах, не передавая кучу вещей полностью обратно в стек вызовов. Тем не менее, я слышал, что он сказал, что актеры в Haskell недовольны.