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!
Author: Haldean Brown, 2012-03-03

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 (an enumeratee) 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, i Pipe).
  • 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ą do ListT. Ty może wtedy korzystać z funkcji takich jak takeWhileM 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.

 20
Author: Antal Spector-Zabusky,
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.

 20
Author: C. A. McCann,
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ę.)

 11
Author: Ingo,
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