Подтвердить что ты не робот

Дизайн программы в Haskell: как сделать симуляцию без изменчивости

У меня есть вопрос о том, как лучше всего разработать программу, над которой я работаю в Haskell. Я пишу физический симулятор, что я и делал с букетом на стандартных императивных языках, и обычно основной метод выглядит примерно так:

while True:
  simulationState = stepForward(simulationState)
  render(simulationState)

И мне интересно, как сделать что-то подобное в Haskell. У меня есть функция step :: SimState -> SimState и функция display :: SimState -> IO (), которая использует HOpenGL для рисования состояния моделирования, но я не понимаю, как это сделать в "петле" рода, так как все решения, которые я могу придумать что-то вроде изменчивости. Я немного нуб, когда дело доходит до Haskell, так что вполне возможно, что мне не хватает очень очевидного дизайнерского решения. Кроме того, если есть лучший способ архитектовать мою программу в целом, я был бы рад ее услышать.

Спасибо заранее!

4b9b3361

Ответ 1

По-моему, правильный способ подумать об этой проблеме не как цикл, а как список или другая такая бесконечная потоковая структура. Я дал аналогичный ответ на аналогичный вопрос; основная идея, как C. А. Макканн написал, чтобы использовать iterate stepForward initialState, где iterate :: (a -> a) -> a -> [a] "возвращает бесконечный список повторяющихся приложений [ stepForward] до [initialState]".

Проблема с этим подходом заключается в том, что у вас есть проблемы с монадическим шагом и, в частности, с монадической функцией рендеринга. Один из подходов состоял бы в том, чтобы заранее взять желаемый фрагмент списка (возможно, с функцией вроде takeWhile, возможно с ручной рекурсией), а затем mapM_ render. Лучшим подходом было бы использование другой, по существу монодичной, потоковой структуры. Четыре из них, о которых я могу думать, следующие:

  • Пакет iteratee, который первоначально был предназначен для потоковой передачи ввода-вывода. Я думаю, здесь ваши шаги будут источником (enumerator), и ваш рендеринг будет раковиной (iteratee); вы можете использовать канал (a enumeratee) для применения функций и/или фильтрации в середине.
  • пакет перечислителя, основанный на тех же идеях; один может быть более чистым, чем другой.
  • Новый пакет pipe, который выставляет счет как "iteratees done right" - новее, но семантика - это, по крайней мере, для меня, значительно яснее, как и имена (Producer, Consumer и Pipe).
  • Пакет списка, в частности его трансформатор ListT. Этот монадный трансформатор предназначен для создания списков монадических значений с более полезной структурой, чем [m a]; например, работа с бесконечными монадическими списками становится более управляемой. Пакет также обобщает множество функций в списках в новый класс типа. Он обеспечивает функцию iterateM дважды; первый раз в невероятной общности, а второй раз специализируется на ListT. Затем вы можете использовать функции, такие как takeWhileM, чтобы выполнить фильтрацию.

Большое преимущество для повторения итерации ваших программ в некоторой структуре данных, а не просто с помощью рекурсии, заключается в том, что ваша программа может тогда делать полезные вещи с потоком управления. Разумеется, нет ничего грандиозного, но, например, он отделяет "как прекратить" решение от процесса "как генерировать". Теперь пользователь (даже если он только вы) может отдельно решить, когда остановиться: после n шагов? После того, как состояние удовлетворяет определенному предикату? Нет никаких оснований для того, чтобы привести в действие ваш генерирующий код с этими решениями, поскольку это логически является отдельной проблемой.

Ответ 2

Ну, если рисовать последовательные состояния - это все, что вы хотите сделать, это довольно просто. Сначала возьмите функцию step и начальное состояние и используйте функцию iterate. iterate step initialState - это (бесконечный) список каждого состояния моделирования. Затем вы можете сопоставить display с тем, чтобы заставить IO-действия рисовать каждое состояние, поэтому вместе у вас будет что-то вроде этого:

allStates :: [SimState]
allStates = iterate step initialState

displayedStates :: [IO ()]
displayedStates = fmap display allStates

Самый простой способ запустить его - использовать функцию intersperse, чтобы поместить действие "задержка" между каждым действием экрана, затем используйте функция sequence_, чтобы запустить все это:

main :: IO ()
main = sequence_ $ intersperse (delay 20) displayedStates

Конечно, это означает, что вам нужно принудительно прекратить действие приложения и исключить любую интерактивность, поэтому это не очень хороший способ сделать это в целом.

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

runLoop :: SimState -> IO ()
runLoop st = do display st
                isDone <- checkInput
                if isDone then return () 
                          else delay 20 >> runLoop (step st)

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

runStep :: SimState -> IO SimState
runStep st = do display st
                delay 20
                return (step st)

runLoop :: SimState -> IO ()
runLoop initialState = iterUntilM_ checkInput runStep initialState

Реализация функции iterUntilM_ оставлена ​​в качестве упражнения для читателя, хе.

Ответ 3

Ваш подход в порядке, вам просто нужно помнить, что циклы выражаются как рекурсия в Haskell:

simulation state = do
    let newState = stepForward state
    render newState
    simulation newState

(Но вам определенно нужен критерий, как закончить цикл.)