【问题标题】:Can I create a function in Haskell that will encapsulate reading data from file and returning me a simple list of data?我可以在 Haskell 中创建一个函数来封装从文件中读取数据并返回一个简单的数据列表吗?
【发布时间】:2014-05-04 21:44:32
【问题描述】:

请考虑以下代码,该代码取自我为帮助我学习 Haskell 而构建的一个工作示例。这段代码将从雅虎下载的包含股票报价的 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

【问题讨论】:

  • 类型必须是Filename -&gt; IO OHLCBarList。能否请您正确格式化代码并删除多余的屏幕截图?

标签: haskell


【解决方案1】:

如果你真的希望你的函数有Filename -&gt; OHLCBarList的类型,那是做不到的。*读取文件的内容是一个IO操作,而Haskell的IO monad是专门设计的,所以IO monad中的值可以永远不离开。如果这个限制被打破,它(通常)会搞砸很多事情。除了这样做,您有两个选择:将getBarsFromFile 的类型设为Filename -&gt; IO OHLCBarList — 从而基本上复制main 的前四行 — 或者编写一个C.ByteString -&gt; OHLCBarList 类型的函数,将getContentsOfFile 的输出可以通过管道封装main 的第 2 到第 4 行。

* 从技术上讲,它可以完成,但你真的,真的,真的甚至不应该尝试,尤其是如果你是Haskell 新手。

【讨论】:

  • 谢谢 ---(以及所有其他回复的人)。我认为它无法完成,因为我确实理解(尽管我仍然是初学者并不完全)类型错误消息,但我想知道是否有我遗漏的东西。在这一点上,我想我的斗争更具哲学性。我理解分离纯功能和不纯功能的原因,并且我得到(并且喜欢)强类型系统的好处。然而,限制阻止了看似非常合理的结构抽象,即对于任何 X 和 Y,将 X 转换为 Y。
  • “然而,限制阻止了看似非常合理的结构抽象,即对于任何 X 和 Y,将 X 转换为 Y。”。嗯,对我来说,这似乎与合理的抽象相去甚远。并非所有事物都应被视为“可转换为任何其他事物”……
  • @David:我不明白什么抽象被阻止了。你可以写你想写的函数,你只需要在它们的类型中输入一些IOs。有什么问题?
  • 问题(至少从我有限的理解)是必须在所有函数中传播该 IO。我使用过的所有其他语言都允许我从方法或过程中提取代码块并使其独立,随之而来的是可读性、模块化等方面的改进,但语义行为没有任何变化(至少如果你做对了!)例如,我正在考虑 Martin Fowler 的重构工作。但是,我仍在尝试学习 Haskell 周围的“最佳实践”,也许有一天这不会打扰我。
  • @David 您所说的那种重构是好的,并且在 Haskell 中强烈鼓励(纯粹性和非常强大/富有表现力的类型系统的部分理由实际上是为了让这更容易)。但是对于 monad(包括 IO),您可以通过将函数“提升”进入 monad 来操作那里的值,而不是通过获取 monad 的值 out 然后馈送到你的功能。
【解决方案2】:

其他人已经解释说,您的函数的正确类型必须是 Filename -&gt; IO OHLCBarList,我想尝试让您了解为什么编译器会对您施加这种严厉措施。

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

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

这使得编译器可以非常聪明地优化你的程序(例如,它可以安全地为你折叠重复的代码),并帮助程序员不犯错误:你不能让系统处于无效状态,如果空无一人!我们喜欢纯函数。

规则的例外是 IO。根据定义,执行 IO 的代码是不纯的:您可以调用 getLine 一百次,但永远不会得到相同的结果,因为它取决于用户键入的内容。 Haskell 使用类型系统处理这个问题:所有不纯函数都被 @​​987654327@ 类型破坏。 IO 可以被认为是对现实世界状态的依赖,有点像 World -&gt; (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 行提取到另一个函数中。

【讨论】:

  • 是的,我明白这一切。请参阅我对此主题的进一步评论。
  • 最后一行的“数字”之前应该有一个 1(累加器)。更重要的是,在您的示例的上下文中,我希望这里有一个函数来获取该字符串(从某个地方,独立定义,可以来自磁盘,交互式,侦听套接字等)并将其作为列表返回准备处理的数字(或 IO [Int],我想)。现在也许当我提高我的 Haskell 技能时,我不应该这样,但这是一个不同的问题。
  • 感谢您发现我的错字;) 这种编程风格的一个关键优势 - 将所有内容分解为小的纯函数 - 可以轻松地将它们组合成你喜欢的任何方式。您描述的函数 - getNumbers :: IO [Int] - 可以定义为 fmap readNumbers getLine。如果您需要calculateString :: String -&gt; Int,那只是calculate . readNumbers。它就像乐高积木 - 很多小积木,您可以拆开并重新组合在一起。
【解决方案3】:

换句话说,我如何实现(不会出现关于 IO 的各种错误)类型为的函数

getBarsFromFile :: Filename -> OHLCBarList

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

你不能在没有得到关于 IO 内容的各种错误的情况下执行此操作,因为getBarsFromFile 的这种类型错过了IO。可能这就是关于 IO 的错误试图告诉你的。您是否尝试了解并修复错误?

在您的情况下,我将首先在函数中抽象您 main 的第二到第四行:

parseBars :: ByteString -> OHLCBarList

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

getBarsFromFile :: FilePath -> IO OHLCBarList

我会打电话给main。

【讨论】:

    猜你喜欢
    • 2022-01-09
    • 2021-12-17
    • 2020-09-27
    • 1970-01-01
    • 2017-11-05
    • 1970-01-01
    • 1970-01-01
    • 2020-10-17
    • 2020-08-27
    相关资源
    最近更新 更多