如何为IO结构化Haskell代码?

5

我正在尝试学习Haskell,所以我决定编写一个简单的程序来模拟行星绕太阳运行的轨道,但我在从模拟中打印出坐标时遇到了问题。我的代码中顶层函数如下:


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

主函数 = do let 行星 = 运行模拟 [地球,太阳] 100 0.05 打印 行星

“Body”只是一个数据类型,它保存了行星的位置、速度等信息,因此第一个参数只是模拟中行星的列表,其他参数分别是要集成的步数和时间步长。我的问题是,我如何修改代码以在每次调用runsim后打印所有body的位置?我尝试向传递给map的组合函数中添加“printInfo”函数,如下所示:


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

但是它无法编译,有人能给我一些提示吗?
谢谢!
4个回答

7
yairchu对于你的printBody问题有很好的答案。你的核心问题是如何构建程序,以便可以打印出每个步骤,这有点难。你可能想保持runSim或类似的东西是纯的,因为它只是在运行模拟,而I/O并不是它的工作。
我会采用两种方法来解决这个问题:要么使runSim返回一个无限的模拟步骤列表,要么使I/O包装器一次只运行一个步骤。我更喜欢第一种选择,所以我从那里开始。
runSim更改为返回步骤列表:
runSim :: [Body] -> Double -> [[Body]]
-- now returns a list of bodys, but no terminating condition
runSim bodys numSteps dtparam = nextBodys : runSim nextBodys dtparam
    where nextBodys = map (integratePos dtparam . integrateVel dtparam) 
                          (calculateForce bodys)

现在,main可以随意执行模拟的步骤并将其打印出来:
main = mapM_ (mapM_ print) (take 100 $ runSim [earth, sun] 0.05)

假设你已经按照yairchu的建议加上了`Body deriving Show`,这样 `print`就可以使用了。 `mapM_`与`map`类似,它需要一个单子(这里是有副作用的)函数来进行映射(以M结尾),并且不返回列表(以_结尾)。因此它更像是Scheme中的`for-each`。

另一种选择是保留您的 `runSim` 代码,并编写一个只运行一步的打印循环:

printLoop :: Integer -> [Body] -> IO [Body]
printLoop 0 bodies = return bodies
printLoop n bodies = do
    let planets = runSim bodies 1 0.05
    mapM_ print planets -- still need to have Body deriving Show
    printLoop (n-1) planets

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

谢谢,这正是我所需要的!是的,无限列表方法似乎是最干净的。 - JS.

4

关于

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

除非“type Body = String”,否则您无法对 Body 进行 putStrLn 操作。
ghci> :t putStrLn
putStrLn :: String -> IO ()

putStrLn要求一个String类型的参数。你可以使用putStrLn . show,或者

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

使用print命令。

现在,对于类型Body做出合理的假设,printInfo函数的类型是错误的。因为它调用了putStrLn,所以应该以“-> IO Something”结束。

代码如下:

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

现在这里的最后一行是错误的。 b 的类型是 Body,但那里的内容需要是 IO Body 类型的。我们该如何进行转换?使用 return :: Monad m => a -> m a

因此,下面是一个可用的版本:

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

谢谢,非常有帮助。我认为我需要更深入地学习单子和IO,然后再回来看这个。 - JS.

2

要进行IO操作,必须处于IO单子中:

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

要从您的runSim函数中调用此函数,它必须位于IO单子内:

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

(虽然可能有更好的组织函数的方法。)
这个单子业务是非常复杂的。它是Haskell最大的优势,但是当你第一次遇到它时,很难理解。我建议通过教程来学习,比如这个: http://learnyouahaskell.com/ 具体来说,这将让您开始: http://learnyouahaskell.com/input-and-output 有许多关于单子的教程,它们会进入更多的细节(写一个教程是每个人在掌握它们之后做的第一件事)。从haskell.org的链接是你的朋友。

谢谢,我会去读更多的资料。只是有点厌倦理论,想要实践一下 :-) - JS.

1

记录一下,这是我想出来的解决方案,不过我觉得现在我会用无限列表重新编码它:


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

主函数 = do let 行星 = runSim ([地球, 太阳],[]) 100 0.05 sequence $ snd 行星

再次感谢大家!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接