最简单的 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 :: * -> *

MaybeIO 的一个共同点是它们描述的是参数的上下文。而不是容器。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.Environment

main :: 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.Environment

main :: 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.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 流视为字符列表。

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 . lines

main :: 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

首先需要学习的两个方法是 packunpack,可以在 StringText 之间进行转换。为了将字面量的类型换成 Text,可以在使用 ghc 的时候写上 -XOverloadedStrings

1
2
3
4
5
6
7
8
9
10
import qualified Data.Text as T

aWord :: T.Text
aWord = "Cheese"

main :: IO ()
main = do
print aWord

-- ghc .\text.hs -XOverloadedStrings

作为用户角度,可能容易忘记加上这个参数,所以更加推荐在代码里指定,Haskell 提供了一个 LANGUAGE 的编译注解。

1
2
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as T

文件操作

1
2
3
4
5
6
import System.IO

-- 定义
openFile :: 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.IO

main :: IO ()
main = do
myFile <- openFile "hello.txt" ReadMode
hClose myFile
putStrLn "done!"

如果只是打开和关闭文件毫无意义,读取和写入文件和常规的输入和输出很类似,使用 hPutStrLnhGetLine。和普通的标准输入输出 putStrLngetLine 的区别仅仅在于文件的操作需要传入文件句柄。

其实,putStrLnhPutStrLn 的实例,而 getLinehGetLine 的实例,句柄就是 stdoutstdin

1
2
3
4
5
6
7
8
9
10
11
12
13
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 来进行判断。

1
2
3
4
5
6
7
8
9
10
11
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):

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.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

二进制文件

stack