5 Stimmen

Wie strukturiert man Haskell-Code für Ein- und Ausgabe?

Ich versuche, Haskell zu lernen, also entschied ich mich, ein einfaches Programm zu schreiben, um die Umlaufbahnen der Planeten um die Sonne zu simulieren, aber ich stieß auf ein Problem beim Ausdrucken der Koordinaten aus der Simulation, die Top-Level-Funktion in meinem Code ist folgende:

runSim :: [Body] -> Integer -> Double -> [Body] 
runSim bodys 0 dtparam = bodys
runSim bodys numSteps dtparam = runSim (map (integratePos dtparam . integrateVel dtparam (calculateForce bodys)) (numSteps-1) dtparam

main = do 
      let planets = runSim [earth, sun] 100 0.05
      print planets

Ein "Body" ist nur ein Datentyp, der die Position, Geschwindigkeit usw. eines Planeten enthält, also ist der erste Parameter einfach die Liste der Planeten in der Simulation und die anderen Parameter sind die Anzahl der Schritte zum Integrieren und die Zeitschrittgröße. Meine Frage ist, wie kann ich den Code ändern, um die Position aller Körper nach jedem Aufruf von runsim auszugeben? Ich habe versucht, eine "printInfo" Funktion zu den zusammengesetzten Funktionen hinzuzufügen, die an map übergeben werden, so wie:

printInfo :: Body -> Body
printInfo b = do
        putStrLn b
        b

aber es kompiliert nicht, kann mir jemand ein paar Hinweise geben?

Danke!

7voto

Nathan Shively-Sanders Punkte 17999

Yairchu hat eine gute Antwort für Ihr Problem mit printBody. Ihre zentrale Frage, wie Sie Ihr Programm strukturieren können, um jeden Schritt auszugeben, ist etwas schwieriger. Sie möchten vermutlich runSim oder etwas Ähnliches rein halten, da es nur die Simulation ausführt und die Ein-/Ausgabe wirklich nicht seine Aufgabe ist.

Es gibt zwei Möglichkeiten, wie ich das angehen würde: Entweder lassen Sie runSim eine unendliche Liste von Simulationsschritten zurückgeben, oder der I/O-Wrapper führt nur einen Schritt auf einmal aus. Ich bevorzuge die erste Option, also werde ich damit anfangen.

Ändern Sie runSim, um eine Liste von Schritten zurückzugeben:

runSim :: [Body] -> Double -> [[Body]]
-- gibt jetzt eine Liste von Körpern zurück, aber keine Abbruchbedingung
runSim bodys numSteps dtparam = nextBodys : runSim nextBodys dtparam
    where nextBodys = map (integratePos dtparam . integrateVel dtparam) 
                          (calculateForce bodys)

Jetzt kann main so viele Simulationsschritte wie gewünscht durchlaufen und sie ausgeben:

main = mapM_ (mapM_ print) (take 100 $ runSim [earth, sun] 0.05)

Ich gehe wieder davon aus, dass Sie der Empfehlung von yairchu folgen und Body deriving Show haben, damit print funktioniert. mapM_ ist wie map, aber es akzeptiert eine monadische (hier, nebenwirkende) Funktion zum Abbilden (endet mit M) und gibt die Liste nicht zurück (endet mit _). Es ist also eher wie for-each in Scheme oder ähnlichem.

Die Alternative besteht darin, Ihr runSim beizubehalten und eine Druckschleife zu schreiben, die nur einen Schritt auf einmal ausführt:

printLoop :: Integer -> [Body] -> IO [Body]
printLoop 0 bodies = return bodies
printLoop n bodies = do
    let planets = runSim bodies 1 0.05
    mapM_ print planets -- Sie müssen immer noch Body mit deriving Show haben
    printLoop (n-1) planets

main = do
    printLoop 100 [earth, sun]
    return ()

4voto

yairchu Punkte 21749

Betreffend

printInfo :: Body -> Body
printInfo b = do
    putStrLn b
    b

Es sei denn, "type Body = String", kannst du kein putStrLn zu einem Body machen.

ghci> :t putStrLn
putStrLn :: String -> IO ()

putStrLn erfordert einen String. Du kannst putStrLn . show verwenden, oder

$ hoogle "Show a => a -> IO ()"
Prelude print :: Show a => a -> IO ()

print verwenden.

Jetzt ist die Annahme über den Typ Body sicherlich vernünftig, aber der Typ von printInfo ist falsch. Da es putStrLn aufruft, sollte es mit "-> IO Something" enden.

Hier:

printBody :: Body -> IO Body
printBody b = do
  print b
  b

Jetzt ist die letzte Zeile hier falsch. b ist vom Typ Body, aber der Inhalt sollte vom Typ IO Body sein. Wie können wir diese Transformation machen? Mit return :: Monad m => a -> m a.

Also hier ist eine funktionierende Version:

printBody :: Body -> IO Body
printBody b = do
  print b
  return b

2voto

David Crawshaw Punkte 10077

Um IO zu machen, musst du im IO Monad sein:

printInfo :: Body -> IO Body
printInfo b = do
  putStrLn b
  return b

Und um diese Funktion innerhalb deiner runSim Funktion aufzurufen, muss sie im IO Monad sein:

runSim :: [Body] -> Integer -> Double -> IO [Body]

(Es gibt möglicherweise bessere Möglichkeiten, deine Funktion zu organisieren.)

Dieses Monadengeschäft ist nicht trivial. Es ist die größte Stärke von Haskell, aber es ist schwierig zu begreifen, wenn du es zum ersten Mal siehst. Ich empfehle, ein Tutorial durchzuarbeiten, wie dieses hier:

http://learnyouahaskell.com/

Speziell wird dir das den Einstieg erleichtern:

http://learnyouahaskell.com/input-and-output

Es gibt viele Tutorials über Monaden da draußen, die viel detaillierter sind (das Schreiben eines Tutorials ist das Erste, was jeder macht, nachdem er sie verstanden hat). Die Links von haskell.org sind deine Freunde.

1voto

JS. Punkte 596

Für die Aufzeichnung, hier ist die Lösung, die ich gefunden habe. Ich denke jedoch, dass ich sie jetzt mit unendlichen Listen neu codieren werde:

runSim :: ([Body], [IO ()]) -> Integer -> Double -> ([Body], [IO ()]) 
runSim (bodys,bodyActions) 0 dtparam = (bodys, bodyActions)
runSim (bodys,bodyActions) numSteps dtparam = runSim (movedBodys, newBodyActions) (numSteps-1) dtparam
                where movedBodys = (map (integratePos dtparam . integrateVel dtparam) (calculateForce bodys))
                      newBodyActions = bodyActions ++ map print bodys

main = do 
      let planets = runSim ([earth, sun],[]) 100 0.05
      sequence $ snd planets

Nochmals vielen Dank an alle!

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X