「Learn Haskell」#4 输入输出与文件
Input/Output
运行Haskell程序
不在GHCi中运行一个Haskell程序有两种方式:
- 编译运行:
1
2$ ghc --make code
$ ./code - 通过
runhaskell
命令直接运行:1
$ runhaskell code.hs
输出文本
在一个Haskell程序中输出文字需要定义一个main函数:
1 | main = putStrLn "Hello World" |
其中putStrLn的类型是:
putStrLn :: String -> IO ()
putStrLn接收一个String类型,并返回一个结果为()类型的IO动作(I/O action)。所以main函数的类型为IO ()。(IO的Kind是* -> *)
除此之外,还有其他默认提供的输出文本的函数:
putStr
:输出文本,结尾不换行putChar
:输出单个字符,结尾不换行。接收的参数为单个Char,不是String(用单引号不是双引号)print
:可以接收任何Show的成员,先用show转化为字符串然后输出。等同于putStrLn . show
do block
在main函数中使用多个putStrLn需要使用do语句:
1 | main = do |
其中最后一行一定要返回IO ()类型的值
输入文本
输入文字需要在do块中使用getLine:
1 | main = do |
getLine的类型是:
getLine :: IO String
而<-操作符将getLine中的String提取了出来给到了line,使line变成了String类型的一个字符串。
而且使用输入的字符串必须要经过一次<-,不能直接使用getLine作为字符串,因为getLine不是String类型,而是IO String类型。
除此之外,还可以使用getChar来获取单个字符,但仍然需要使用<-操作符来提取Char
其他IO相关函数用法
return
Haskell中的return和其他命令式语言中的return完全不同,它不会使函数直接结束并返回一个值。
main函数必须定义为类型为IO ()的函数,所以在main函数中使用if语句,如果不输出的话也不可以直接放下什么都不干,因为这时候main函数的类型不是IO ()。所以这时需要使用return ()来为main函数指定为IO ()类型,例如:
1 | main = do |
使用<-操作符也可以直接将return语句中的内容提取出来,比如a <- return ‘A’,执行后a就是’A’。
when
when包含在Control.Monad
模块中,它表示在满足第一个参数的条件下会执行第二个函数,否则会return ()。比如:
1 | import Control.Monad |
等同于:
1 | main = do |
sequence
sequence在IO中使用时可以达成[IO a] -> IO [a]的效果,所以可以用作:
1 | [a, b, c] <- sequence [getLine, getLine, getLine] |
mapM & mapM_
在IO相关的地方使用map,可以使用mapM和mapM_,其中mapM有返回值而mapM_直接扔掉了返回值:
1 | ghci> mapM print [1,2,3] |
forever
forever函数包含在Control.Monad
模块中。在main函数开头加上forever函数可以使后面的do块一直重复执行直到程序被迫终止,如:
1 | import Control.Monad |
forM
forM函数包含在Control.Monad
模块中,它的功能和mapM类似,从第一个参数中逐个取出元素传入第二个参数(一个接收一个参数的函数)中,并且第二个参数可以返回IO a类型。比如:
1 | import Control.Monad |
getContents
getLine获取一整行,而getContents从标准输入中获取全部内容直到遇到EOF,并且它是lazy的,在执行了foo <- getContents后,它并不会读取标准输入并且赋值到foo,而是等到需要使用foo的时候再从标准输入读取。
getContents在使用管道传入文字时很常用,可以代替forever+getLine使用,比如一个Haskell程序文件code.hs:
1 | import Data.Char |
使用ghc –make code编译后,通过管道传入文字:
1 | cat text.txt | ./code |
会将text.txt中的所有字母转为大写并输出
interact
上述功能还可以转化为一个String -> String的函数:
1 | upperStrings = unlines . map (map toUpper) . lines |
而在main中使用这个函数就需要:
1 | main = do |
但是String -> String类型的函数在输入输出中的使用太常见了,所以可以使用interact函数来简化。interact的类型是:
interact :: (String -> String) -> IO ()
可以看出它接收一个String -> String的函数,并返回一个IO ()类型,所以可以直接用在main上。
于是整个转换为大写的程序就可以简化为:
1 | main = interact $ unlines . map (map toUpper) . lines |
文件和流
以下与文件和流相关的函数都包含在System.IO
模块中
openFile
openFile函数可以用来打开一个文件,它的类型是:
openFile :: FilePath -> IOMode -> IO Handle
其中FilePath
是String的type synonyms,用一个字符串来表示需要打开的文件的路径
IOMode
的定义是:
1 | data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode |
所以它一共只有四个值,用来表示进行IO操作的模式
openFile返回一个IO Handle类型的值,将其用<-操作符提取后会出现一个Handle的值。但不能从Handle中直接使用文字,还需要使用一系列函数:
hGetContents
:: Handle -> IO String ,从Handle中读取全部内容,返回一个IO StringhGetChar
:: Handle -> IO Char ,从Handle中读取一个字符hGetLine
:: Handle -> IO String ,从Handle中读取一行,返回一个IO StringhPutStr
:: Handle -> String -> IO () ,向Handle中输出字符串hPutStrLn
:: Handle -> String -> IO () ,同上
在使用openFile进行文件操作后,需要使用hClose手动关闭Handle。hClose :: Handle -> IO (),接收一个Handle并返回IO (),可以直接放在main函数末尾
所以使用openFile读取一个文件中的全部内容并输出的全部代码是:
1 | import System.IO |
withFile
withFile类似Python中的with open,它在读取文件使用之后不需要手动close文件。它的类型是:
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
可以看出,它接收三个参数:
FilePath
:一个表示文件路径的StringIOMode
:打开文件的模式(Handle -> IO a)
:一个函数,表示对读取文件后的Handle索要进行的操作,需要返回一个I/O action;而这个返回值也将作为withFile的返回值
现在使用withFile来改写上述代码:
1 | import System.IO |
withFile的功能相当于以下函数:
1 | withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a |
readFile
readFile可以更加简化读取文件内容的操作,它的类型:
readFile :: FilePath -> IO String
它只需要输入一个表示文件路径的字符串,返回其中以其中内容为内容的I/O action:
1 | import System.IO |
writeFile
writeFile简化了写入文件的操作,它的类型:
writeFile :: FilePath -> String -> IO ()
传入的第一个参数是要写入的文件的路径,第二个参数是要写入的字符串,返回一个IO ()
appendFile
appendFile类似writeFile,但使用它不会覆盖文件中原来内容,而是直接把字符串添加到文件末尾
buffer
文件以流的形式被读取,默认文字文件的缓冲区(buffer)大小是一行,即每次读取一行内容;默认二进制文件的缓冲区大小是以块为单位,如果没有指定则根据系统默认来选择。
也可以通过hSetBuffering
函数来手动设置缓冲区大小,这个函数的类型:
hSetBuffering :: Handle -> BufferMode -> IO ()
它接收一个handle,和一个BufferMode,并返回IO ()。其中BufferMode有以下几种:
NoBuffering
:没有缓冲区,一次读入一个字符LineBuffering
:缓冲区大小是一行,即每次读入一行内容BlockBuffering (Maybe Int)
:缓冲区大小是一块,块的大小由Maybe Int指定:BlockBuffering (Nothing)
:使用系统默认的块大小BlockBuffering (Just 2048)
:一块的大小是2048字节,即每次读入2048bytes的内容
缓冲区的刷新是自动的,也可以通过hFlush
来手动刷新
hFlush :: Handle -> IO ()
传入一个handle,返回IO (),即刷新对应handle的缓冲区
openTempFile
openTempFile可以新建一个临时文件:
openTempFile :: FilePath -> String -> IO (FilePath, Handle)
FilePath
指临时文件要创建的位置路径,String
指临时文件名字的前缀,返回一个I/O action,其内容第一个FilePath
是创建得到的临时文件的路径,Handle
是临时文件的handle
例如:
1 | import System.IO |
"."
指临时文件要在当前目录创建,"temp"
指临时文件名字以temp开头。最终得到的tempFile就是./temp…….,temp后为随机数字,如./temp43620-0
路径操作
相关函数都包含在System.Directory
模块中,全部内容见System.Directory
getCurrentDirectory
getCurrentDirectory :: IO FilePath
直接返回一个I/O action,其内容是一个字符串表示当前路径的绝对路径
removeFile
removeFile :: FilePath -> IO ()
输入一个文件路径,并删除掉它
renameFile
renameFile :: FilePath -> FilePath -> IO ()
输入一个原路径,一个新路径,为原路径的文件重命名为新路径的名
doesFileExist
doesFileExist :: FilePath -> IO Bool
检查文件是否存在,返回一个包含布尔值的I/O action
Command line arguments
System.Environment
模块中提供了两个函数可以用来处理传入命令行的参数
getArgs
getArgs :: IO [String]
不需要输入参数,直接返回一个I/O action,内容为传入命令行的参数(一个由String组成的列表)。相当于C语言中的argv[1:]
getProgName
getProgName :: IO String
返回I/O action,内容为程序的名字,相当于C语言中的argv[0]
Randomness
和随机数有关的函数都包含在System.Random
模块中。GHCi启动时可能不会包含System.Random的配置,导致无法找到模块。需要通过stack打开:
1 | stack ghci --package random |
Haskell要求同样的程序需要运行出同样的结果,除了用到了I/O action,所有会造成不同结果的函数都要交给I/O action来完成
那要使随机数脱离IO存在,就要用到随机生成器(random generator)
System.Random
模块提供了几个生成随机数的函数:
random
random :: (Random a, RandomGen g) => g -> (a, g)
其中又有两个新的typeclass,Random表示可以取随机,RandomGen表示随机数生成器。random函数接收一个随机数生成器,返回一个元组,其中第一个元素是生成的随机数,第二个元素是一个新的随机数生成器
获取随机数生成器可以使用mkStdGen
函数:
mkStdGen :: Int -> StdGen
其中StdGen
是一个RandomGen的实例
运用random生成随机数需要指定类型,不然程序无法确定a
是什么类型。例如:
1 | ghci> random (mkStdGen 100) :: (Int, StdGen) |
再次运行同样的函数,会得到同样的结果。所以如果需要生成其他的随机数,需要更换生成器,就可以使用上一次调用结果返回的新随机数生成器:
1 | threeCoins :: StdGen -> (Bool, Bool, Bool) |
randoms
randoms :: (Random a, RandomGen g) => g -> [a]
randoms接收一个RandomGen,返回一个随机的无穷列表。因为它是无穷的,所以不会返回新的随机数生成器
randomR
randomR :: (Random a, RandomGen g) => (a, a) -> g -> (a, g)
可以用来生成有范围的随机数,第一个参数是一个元组,表示生成随机数的范围(闭区间)
randomRs
randomRs :: (Random a, RandomGen g) => (a, a) -> g -> [a]
同上两个,生成有范围的无穷随机数列表
getStdGen
如果想要让程序每次运行得到不同的随机结果,需要使用getStdGen
来获取全局随机数生成器,它会在每次运行的时候产生不同的值,也因此,它返回的是一个I/O action,而不是一个直接的StdGen
getStdGen :: Control.Monad.IO.Class.MonadIO m => m StdGen
即可以看成getStdGen :: IO StdGen,需要使用<-操作符将StdGen提取出来
但是在同一个程序中,getStdGen的结果是相同的,全局随机数生成器不会自动更新,所以就需要另一个函数newStdGen
newStdGen
newStdGen :: Control.Monad.IO.Class.MonadIO m => m StdGen
执行newStdGen会进行两个操作:
- 更新全局随机数生成器,下次执行getStdGen会获得不同的结果
- 返回一个I/O action,包含一个新的StdGen(但是这个生成器和全局生成器也不同)
Exceptions
程序在运行失败时会抛出异常,可以通过Control.Exception
模块中的catch
函数来捕获异常:
catch :: Exception e => IO a -> (e -> IO a) -> IO a
第一个参数是要进行的操作,以IO a为返回值的类型,第二个参数是一个函数,它接收异常并进行操作,例如:
1 | import Control.Exception |
也可以利用守卫(guard)语法和System.IO.Error
中的函数来判断IO异常的类型来进行不同操作:
1 | import System.Environment |
具体相关全部函数见文档:System.IO.Error、Control.Exception
Reference
目录
#0 | 总章
#1 | 基础语法与函数
#2 | 高阶函数与模块
#3 | 类型与类型类
#4 | 输入输出与文件
#5 | 函子、应用函子与单子
#6 | 半群与幺半群
#7 | 一些其它类型类
#A | Haskell与范畴论
「Learn Haskell」#4 输入输出与文件