最简单的 IO 程序
1 2 3 4 5 6 7 8 9 10 helloPerson :: String -> String helloPerson name = "Hello" ++ " " ++ name ++ "!" main :: IO ()main = do putStrLn "Hello! What's your name?" name <- getLine let statement = helloPerson name putStrLn statement
main :: IO ()
中的 ()
就是一个空的元组。putStrLn
只是将消息输出,它没有什么有意义的返回值,所以这里用 ()
。
main
并不是一个函数 。它仅仅执行了一个动作 action,没有返回值也没有函数参数。
IO 类型
1 2 3 4 ghci > :kind Maybe Maybe :: * -> *ghci > :kind IO IO :: * -> *
Maybe
和 IO
的一个共同点是它们描述的是参数的上下文。而不是容器。IO
的上下文是这个值来自输入/输出操作。
由于 IO 的不可预测性,只能在 IO
的 action 中使用这个值。换言之,Haskell 将 IO 的逻辑和其它的逻辑分开了。
do
为了更加便利地处理 IO,就有了 do
,可以看到有的变量使用 let
进行赋值,而有的却使用 <-
。使用 <-
的时候,会将 IO a
看作为 a
。
所以通过 <-
的转换,name
才是 String
,所以才可以使用 helloPerson
。
Monad
IO
类型是 Monad
的类型。Maybe
也是。
Lazy IO
在其它语言中,通常会提到一个所谓的 stream
的概念。可以将 IO stream 理解成惰性计算的字符列表。STDIN(标准输入)时,用户输入流式传入到程序,但不一定知道哪里结束。这和 Haskell 惰性列表是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 import System.Environmentmain :: IO ()main = do args <- getArgs mapM_ putStrLn args .\sum 1 2 3 4 5 1 2 3 4 5
这里使用 mapM_
是因为需要忽略掉结果,mapM
操作后返回的是列表 [()]
,但是我们需要的是 ()
。
1 2 3 4 5 6 7 8 9 import System.Environmentmain :: IO ()main = do args <- getArgs let linesToRead = if length args > 0 then read (head args) else 0 :: Int print linesToRead
我们将传入一个数字作为需要读取的数字的数量,然后根据这个数量进行读取值,最后将得到的数据进行处理。这里同样使用了 Monad。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import System.Environmentimport Control.Monadmain :: IO ()main = do args <- getArgs let linesToRead = if length args > 0 then read (head args) else 0 :: Int numbers <- replicateM linesToRead getLine let ints = map read numbers :: [Int ] print (sum ints) .\sum 4 1 2 3 4 10
replicateM
接受一个 Int n
和一个 IO action,重复执行 n
次 action。
这个程序有个问题,就是必须先输入需要的总数。但是很多时候我们并不能知道有多少个,比如统计有多少个访客的时候。
回想一下,IO 是一种特殊逻辑,但在这里我们所有的逻辑都放到了 IO,这证明没有很好地进行抽象。我们把程序的行为和 IO 的逻辑都混到了一起。
在这个例子中,根本问题在于我们把 IO 序列当作需要立刻处理的值,从而就没有 Lazy 的优势。如果把输入看成是字符串列表,那么就可以不考虑这些混乱了。只需要使用 getContents
,它可以将 STDIN 的 IO 流视为字符列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 main :: IO ()main = do userInput <- getContents mapM_ print userInput .\sum_lazy hello 'h' 'e' 'l' 'l' 'o' '\n ' hi 'h' 'i' '\n '
由于我们现在拿到的是字符,所以我们需要将它转换成 Int
。
1 2 3 4 5 6 7 8 toInts :: String -> [Int ]toInts = map read . linesmain :: IO ()main = do userInput <- getContents let numbers = toInts userInput print (sum numbers)
这样就不用担心需要时输入多少个数字了。
Text 类型
String
基于 List
,效率比较低下。这就是为什么需要有 Text
的原因。对于实际和商业程序编程中,处理文本数据首选的类型是 Text
。
1 import qualified Data.Text as T
Text
的底层实现是数组,这样字符串的操作更快,内存效率也更高。和 String
的另外一个区别是它不使用惰性计算,因为这样效率会比较低。如果需要惰性计算,可以使用 Data.Text.Lazy
。
从此之后,应该使用 Text
去替代 String
。
首先需要学习的两个方法是 pack
和 unpack
,可以在 String
和 Text
之间进行转换。为了将字面量的类型换成 Text
,可以在使用 ghc 的时候写上 -XOverloadedStrings
:
1 2 3 4 5 6 7 8 9 10 import qualified Data.Text as TaWord :: T .Text aWord = "Cheese" main :: IO ()main = do print aWord
作为用户角度,可能容易忘记加上这个参数,所以更加推荐在代码里指定,Haskell 提供了一个 LANGUAGE
的编译注解。
1 2 {-# LANGUAGE OverloadedStrings #-} import qualified Data.Text as T
文件操作
1 2 3 4 5 6 import System.IOopenFile :: FilePath -> IOMode -> IO Handle type FilePath = String data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
基于这个定义,openFile
会返回一个句柄 Handle
,最后需要关闭文件,也是使用 hClose
处理这个句柄。
1 2 3 4 5 6 7 import System.IOmain :: IO ()main = do myFile <- openFile "hello.txt" ReadMode hClose myFile putStrLn "done!"
如果只是打开和关闭文件毫无意义,读取和写入文件和常规的输入和输出很类似,使用 hPutStrLn
和 hGetLine
。和普通的标准输入输出 putStrLn
和 getLine
的区别仅仅在于文件的操作需要传入文件句柄。
其实,putStrLn
是 hPutStrLn
的实例,而 getLine
是 hGetLine
的实例,句柄就是 stdout
和 stdin
。
1 2 3 4 5 6 7 8 9 10 11 12 13 import System.IOmain :: IO ()main = do helloFile <- openFile "hello.txt" ReadMode firstLine <- hGetLine helloFile putStrLn firstLine secondLine <- hGetLine helloFile goodbyeFile <- openFile "goodbye.txt" WriteMode hPutStrLn goodbyeFile secondLine hClose helloFile hClose goodbyeFile putStrLn "done!"
如果需要读取文件的每一行并进行打印,那么就需要知道文件是否结束。Haskell 提供了 hIsEOF
来进行判断。
1 2 3 4 5 6 7 8 9 10 11 import System.IOmain :: IO ()main = do helloFile <- openFile "hello.txt" ReadMode hasLine <- hIsEOF helloFile firstLine <- if not hasLine then hGetLine helloFile else return "empty" putStrLn firstLine putStrLn "done!"
简单的 I/O 工具
很多时候其实也可以直接使用 Haskell 提供的工具,而不直接处理句柄(handle):
1 2 3 readFile :: FilePath -> IO String writeFile :: FilePath -> String -> IO ()appendFile :: FilePath -> String -> IO ()
创建一个程序,用来对文件内容进行计数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import System.Environmentimport System.IOgetCounts :: String -> (Int , Int , Int )getCounts input = (charCount, wordCount, lineCount) where charCount = length input wordCount = (length . words) input lineCount = (length . lines) input countsText :: (Int , Int , Int ) -> String countsText (cc, wc, lc) = unwords ["chars: " , show cc, " words: " , show wc, " lines: " , show lc]main :: IO ()main = do args <- getArgs let fileName = head args input <- readFile fileName let summary = (countsText . getCounts) input appendFile "stats.dat" (mconcat [fileName, " " , summary, "\n" ]) putStrLn summary
二进制文件
stack