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

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

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

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

Заранее спасибо!

Ответы [ 3 ]

20 голосов
/ 03 марта 2012

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

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_ оставлена ​​читателю как упражнение, хе.

20 голосов
/ 03 марта 2012

На мой взгляд, правильный способ думать об этой проблеме - это не цикл, а список или другая такая бесконечная потоковая структура.Я дал аналогичный ответ на аналогичный вопрос ;основная идея состоит в том, как CA Макканн написал , использовать iterate stepForward initialState, где iterate :: (a -> a) -> a -> [a] «возвращает бесконечный список повторных применений [stepForward] к [* 1012»*] ”.

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

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

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

11 голосов
/ 03 марта 2012

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...