最简单的 IO 程序
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 类型
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 惰性列表是一样的。
import System.Environment
main :: IO ()
main = do
args <- getArgs
mapM_ putStrLn args
.\sum 1 2 3 4 5
1
2
3
4
5
这里使用 mapM_
是因为需要忽略掉结果,mapM
操作后返回的是列表 [()]
,但是我们需要的是 ()
。
import System.Environment
main :: IO ()
main = do
args <- getArgs
let linesToRead = if length args > 0
then read (head args)
else 0 :: Int
print linesToRead
我们将传入一个数字作为需要读取的数字的数量,然后根据这个数量进行读取值,最后将得到的数据进行处理。这里同样使用了 Monad。
import System.Environment
import Control.Monad
main :: 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 流视为字符列表。
main :: IO ()
main = do
userInput <- getContents
mapM_ print userInput
.\sum_lazy
hello
'h'
'e'
'l'
'l'
'o'
'\n'
hi
'h'
'i'
'\n'
由于我们现在拿到的是字符,所以我们需要将它转换成 Int
。
toInts :: String -> [Int]
toInts = map read . lines
main :: IO ()
main = do
userInput <- getContents
let numbers = toInts userInput
print (sum numbers)
这样就不用担心需要时输入多少个数字了。
Text 类型
String
基于 List
,效率比较低下。这就是为什么需要有 Text
的原因。对于实际和商业程序编程中,处理文本数据首选的类型是 Text
。
import qualified Data.Text as T
Text
的底层实现是数组,这样字符串的操作更快,内存效率也更高。和 String
的另外一个区别是它不使用惰性计算,因为这样效率会比较低。如果需要惰性计算,可以使用 Data.Text.Lazy
。
从此之后,应该使用 Text
去替代 String
。
首先需要学习的两个方法是 pack
和 unpack
,可以在 String
和 Text
之间进行转换。为了将字面量的类型换成 Text
,可以在使用 ghc 的时候写上 -XOverloadedStrings
:
import qualified Data.Text as T
aWord :: T.Text
aWord = "Cheese"
main :: IO ()
main = do
print aWord
-- ghc .\text.hs -XOverloadedStrings
作为用户角度,可能容易忘记加上这个参数,所以更加推荐在代码里指定,Haskell 提供了一个 LANGUAGE
的编译注解。
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as T
文件操作
import System.IO
-- 定义
openFile :: FilePath -> IOMode -> IO Handle
type FilePath = String
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
基于这个定义,openFile
会返回一个句柄 Handle
,最后需要关闭文件,也是使用 hClose
处理这个句柄。
import System.IO
main :: IO ()
main = do
myFile <- openFile "hello.txt" ReadMode
hClose myFile
putStrLn "done!"
如果只是打开和关闭文件毫无意义,读取和写入文件和常规的输入和输出很类似,使用 hPutStrLn
和 hGetLine
。和普通的标准输入输出 putStrLn
和 getLine
的区别仅仅在于文件的操作需要传入文件句柄。
其实,putStrLn
是 hPutStrLn
的实例,而 getLine
是 hGetLine
的实例,句柄就是 stdout
和 stdin
。
import System.IO
main :: 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
来进行判断。
import System.IO
main :: 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):
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
appendFile :: FilePath -> String -> IO ()
创建一个程序,用来对文件内容进行计数。
import System.Environment
import System.IO
getCounts :: 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