请考虑以下代码,该代码来自我为帮助我学习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
答案 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
。