Projektowanie programów w Haskell: jak zrobić symulację bez mutability
Mam pytanie o najlepszy sposób zaprojektowania programu, nad którym pracuję w Haskell. Piszę symulator fizyki, który robiłem kilka razy w standardowych językach imperatywnych i zazwyczaj główna metoda wygląda tak:
while True:
simulationState = stepForward(simulationState)
render(simulationState)
I zastanawiam się, jak zrobić coś podobnego w Haskell. Mam funkcję step :: SimState -> SimState
i funkcję display :: SimState -> IO ()
, która używa HOpenGL do rysowania stanu symulacji, ale jestem w błędzie, jak to zrobić w" pętli " rodzaju, jak wszystkie rozwiązania, które mogę wymyślić, obejmują jakiś rodzaj zmienności. Jestem trochę noobem, jeśli chodzi o Haskell, więc jest całkowicie możliwe, że brakuje mi bardzo oczywistej decyzji projektowej. Poza tym, jeśli jest lepszy sposób na zaprojektowanie mojego programu jako całości, z przyjemnością to usłyszę.
Z góry dzięki! 3 answers
Moim zdaniem właściwym sposobem myślenia o tym problemie nie jest pętla, ale lista lub inna tego typu nieskończona struktura strumieniowa. Dałem podobną odpowiedź do podobne pytanie; podstawową ideą jest, jak napisał C. A. McCann, użycie iterate stepForward initialState
, gdzie iterate :: (a -> a) -> a -> [a]
"zwraca nieskończoną listę powtarzających się aplikacji z [stepForward
] do [initialState
]".
Problem z tym podejściem polega na tym, że masz problemy z radzeniem sobie z monadycznym krokiem, a w w szczególności funkcja renderowania monadycznego. Jednym z rozwiązań byłoby wcześniejsze pobranie pożądanego fragmentu listy (ewentualnie z funkcją taką jak takeWhile
, ewentualnie z rekurencją ręczną), a następnie mapM_ render
. Lepszym podejściem byłoby użycie innej, wewnętrznie monadycznej struktury strumieniowej. Cztery, które mogę wymyślić to:
-
pakiet iteratee , który został pierwotnie zaprojektowany do przesyłania strumieniowego IO. Myślę, że tutaj twoje kroki byłyby źródłem (
enumerator
) i Twój rendering będzie sink (iteratee
); możesz następnie użyć rury (anenumeratee
) do zastosowania funkcji i / lub filtrowania w środku. - pakiet enumerator , oparty na tych samych pomysłach; jeden może być czystszy od drugiego.
-
nowszy pakiet pipes , który nazywa się "iteratees done right" -jest nowszy, ale semantyka jest, przynajmniej dla mnie, znacznie wyraźniejsza, podobnie jak nazwy (
Producer
,Consumer
, iPipe
). -
Lista pakiet , w szczególności jego
ListT
transformator monad. Ten transformator monad został zaprojektowany, aby umożliwić tworzenie list wartości monadycznych o bardziej użytecznej strukturze niż[m a]
; na przykład praca z nieskończonymi listami monadycznymi staje się łatwiejsza do zarządzania. Pakiet uogólnia również wiele funkcji na listach do nowej klasy typu. Posiada funkcjęiterateM
dwukrotnie; pierwszy raz w ogólności, a drugi raz wyspecjalizowaną doListT
. Ty może wtedy korzystać z funkcji takich jaktakeWhileM
do filtrowania.
Dużą zaletą reifying programu iteracji w jakiejś strukturze danych, a nie po prostu za pomocą rekurencji, jest to, że program może wtedy zrobić przydatne rzeczy z przepływu sterowania. Oczywiście nic zbyt wspaniałego, ale na przykład oddziela to decyzję "jak zakończyć" od procesu "jak wygenerować". Teraz użytkownik (nawet jeśli to tylko ty) może samodzielnie zdecydować, kiedy przestać: po N kroki? Po tym, jak państwo spełni pewne orzeczenie? Nie ma powodu, aby bagno kod generujący z tych decyzji, ponieważ jest to logicznie oddzielny problem.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:00:38
Cóż, jeśli rysowanie kolejnych stanów jest wszystkim, które chcesz zrobić, to całkiem proste. Najpierw weź swoją funkcję step
i stan początkowy i użyj funkcji iterate
. iterate step initialState
jest wtedy (nieskończoną) listą każdego stanu symulacji. Następnie możesz odwzorować display
, aby uzyskać działania IO, aby narysować każdy stan, więc razem mielibyście coś takiego:
allStates :: [SimState]
allStates = iterate step initialState
displayedStates :: [IO ()]
displayedStates = fmap display allStates
Najprostszym sposobem jej uruchomienia byłoby użycie Funkcji intersperse
, aby umieścić akcję "delay" pomiędzy każda akcja wyświetlania, następnie użyj funkcji sequence_
, aby uruchomić całość:
main :: IO ()
main = sequence_ $ intersperse (delay 20) displayedStates
Oczywiście oznacza to, że musisz siłą zakończyć aplikację i wyklucza jakąkolwiek interakcję, więc nie jest to dobry sposób, aby to zrobić w ogóle.
Bardziej rozsądnym podejściem byłoby przeplatanie rzeczy takich jak" sprawdzanie, czy aplikacja powinna wyjść " na każdym kroku. Można to zrobić za pomocą jawnej rekurencji:
runLoop :: SimState -> IO ()
runLoop st = do display st
isDone <- checkInput
if isDone then return ()
else delay 20 >> runLoop (step st)
Moim preferowanym podejściem jest pisanie nie-rekurencyjne kroki, a następnie użyć bardziej abstrakcyjnego kombinatora pętli. Niestety nie ma zbyt dobrego wsparcia dla robienia tego w ten sposób w standardowych bibliotekach, ale wyglądałoby to mniej więcej tak: {]}
runStep :: SimState -> IO SimState
runStep st = do display st
delay 20
return (step st)
runLoop :: SimState -> IO ()
runLoop initialState = iterUntilM_ checkInput runStep initialState
Implementacja funkcji iterUntilM_
jest pozostawiona jako ćwiczenie dla czytelnika, heh.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-03 19:21:10
Twoje podejście jest ok, musisz tylko pamiętać, że pętle są wyrażane jako rekurencja w Haskell:
simulation state = do
let newState = stepForward state
render newState
simulation newState
(ale zdecydowanie potrzebujesz kryterium, jak zakończyć pętlę.)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-03 19:04:15