我可以在Haskell中创建一个函数来封装从文件读取数据并返回一个简单的数据列表吗?

时间:2014-05-04 21:44:32

标签: haskell

请考虑以下代码,该代码来自我为帮助我学习Haskell而构建的工作示例。此代码将包含从Yahoo下载的股票报价的CSV文件解析为一个很好的简单条形列表,然后我可以使用它。

我的问题:如何编写一个以文件名作为参数并返回OHLCBarList的函数,以便main中的前四行可以正确封装?

换句话说,我如何实现(没有得到关于IO东西的各种错误)类型的函数

getBarsFromFile :: Filename -> OHLCBarList

这样可以正确封装在前四行main中完成的繁重工作吗?

我自己也试过这样做,但凭借我有限的Haskell知识,我惨遭失败。

import qualified Data.ByteString as BS

type Filename = String
getContentsOfFile :: Filename -> IO BS.ByteString




barParser :: Parser Bar
barParser = do
   time <- timeParser
   char ','
   open  <-  double
   char ','
   high <- double
   char ','
   low <- double
   char ','
   close <- double
   char ','
   volume <- decimal
   char ','

   return $ Bar Bar1Day time open high low close volume



type OHLCBar = (UTCTime, Double, Double, Double, Double)

type OHLCBarList = [OHLCBar]

barsToBarList :: [Either String Bar] -> OHLCBarList



main :: IO ()
main = do


   contents :: C.ByteString <-  getContentsOfFile "PriceData/Daily/yhoo1.csv" --PriceData/Daily/Yhoo.csv"

   let lineList :: [C.ByteString] = C.lines contents -- Break the contents into a list of lines

   let bars :: [Either String Bar] =  map (parseOnly barParser) lineList  -- Using the attoparsec

   let ohlcBarList :: OHLCBarList = barsToBarList bars -- Now I have a nice simple list of tuples with which to work


   --- Now I can do simple operations like
  print $ ohlcBarList !! 0

3 个答案:

答案 0 :(得分:5)

如果您真的希望您的函数具有类型Filename -> OHLCBarList,则无法完成。*读取文件内容是IO操作,Haskell的IO monad是专门设计的这样IO monad中的值永远不会离开。如果这个限制被打破了,它(通常)会搞砸很多东西。不是这样做,而是有两个选择:使getBarsFromFile的类型为Filename -> IO OHLCBarList - 从而基本上复制main的前四行 - 或者编写类型为{{1}的函数C.ByteString -> OHLCBarList的输出可以通过管道传输来封装getContentsOfFile的第2到第4行。

*从技术上讲,可以完成,但你真的,真的,真的甚至不应该尝试,特别是如果你& #39;是Haskell的新手。

答案 1 :(得分:2)

其他人已经解释过,您的函数的正确类型必须是Filename -> IO OHLCBarList,我想尝试为您提供一些有关为什么编译器强加这个严酷衡量你。

命令式编程就是管理状态:&#34;按顺序对某些内存位执行某些操作&#34;。当它们变大时,程序性程序变得脆弱;我们需要一种限制状态变化范围的方法。 OO程序将状态封装在类中,但范例并没有根本不同:您可以调用相同的方法两次并获得不同的结果。方法的输出取决于对象的(隐藏)状态。

功能编程一路走来,完全禁止可变状态。当使用某些输入调用时,Haskell函数将始终生成相同的输出。简单的例子 纯函数是数学运算符,如+*,或大多数列表处理函数,如map。纯函数都是关于输入和输出的,而不是管理内部状态。

这使编译器能够非常智能地优化您的程序(例如,它可以安全地折叠重复的代码),并帮助程序员不要犯错:您不能将系统置于无效状态状态,如果没有!我们喜欢纯粹的功能。

规则的例外是IO。根据定义,执行IO的代码是不纯的:您可以调用getLine一百次并且永远不会得到相同的结果,因为它取决于用户键入的内容。 Haskell使用类型系统处理此问题:所有不纯函数都与IO类型相关联。 IO可以被视为对现实世界状态的依赖,有点像World -> (NewWorld, a)

总结一下:纯函数很好,因为它们易于推理;这就是Haskell默认使函数纯净的原因。任何不纯的代码都必须使用IO类型签名进行标记;这告诉编译器和读者要小心这个功能。因此,从文件中读取(从根本上是不纯的动作)但返回纯值的函数不存在。


回复您的评论的附录

您仍然可以编写纯函数来处理不纯的数据。考虑以下稻草人:

main :: IO ()
main = do
    putStrLn "Enter the numbers you want me to process, separated by spaces"
    line <- getLine
    let numberStrings = words line
    let numbers = map read numberStrings
    putStrLn $ "The result of the calculation is " ++  (show $ foldr1 (*) numbers + 10)

IO里面有很多代码。让我们提取一些函数:

main :: IO ()
main = do
    putStrLn "Enter the numbers you want me to process, separated by spaces"
    result <- fmap processLine getLine  -- fmap :: (a -> b) -> IO a -> IO b
                                        -- runs an impure result through a pure function
                                        -- without leaving IO
    putStrLn $ "The result of the calculation is " ++ result

processLine :: String -> String  -- look ma, no IO!
processLine = show . calculate . readNumbers

readNumbers :: String -> [Int]
readNumbers = map read . words

calculate :: [Int] -> Int
calculate numbers = product numbers + 10

product :: [Int] -> Int
product = foldr1 (*)

我已将main中的逻辑从纯函数中拉出来,这些函数更容易阅读,编译器更容易优化,更可重用(并且更易于测试)。整个程序仍然存在于IO内,因为数据是不可获得的(参见this answer的最后一部分,以便更彻底地处理这个论点)。可以使用fmap和其他组合器通过纯函数传输不纯的数据;你应该尝试尽可能少地使用main

您的代码似乎确实存在;正如其他人建议您可以将main的第2-4行提取到另一个函数中。

答案 2 :(得分:1)

  

换句话说,我如何实现(没有得到关于IO东西的各种错误)类型的函数

getBarsFromFile :: Filename -> OHLCBarList
     

这样可以正确封装在前四行main中完成的繁重工作吗?

如果没有收到有关IO内容的各种错误,则无法执行此操作,因为getBarsFromFile的此类型错过了IO。可能是IO的东西试图告诉你的错误。您是否尝试了解并修复错误?

在你的情况下,我会首先在一个函数中抽象你的main的第二行到第四行:

parseBars :: ByteString -> OHLCBarList

然后我将这个函数与getContentsOfFile结合起来得到:

getBarsFromFile :: FilePath -> IO OHLCBarList

我会打电话给main