经典I/O
--file callingpure.hs
name2reply::String->String
name2reply name="Hello "++name++"\n"++"Your name contains "++charcount++" characters."
where charcount=show (length name)
main::IO ()
main=do
putStrLn "What's your name?"
inp<-getLine
let out=name2reply inp
putStrLn out
关于IO,
-
<-
绑定一个I/O动作的结果到一个名字,即从I/O动作中抽出结果,并且保存到变量中 -
putStrLn :: String -> IO ()
,接受一个String
参数,映射到一个返回值为()
的IO动作 -
getLine :: IO String
, 保存一个返回值为String
的I/O动作,即当IO动作运行后得到String
- IO something 类型指返回值为something类型的IO动作
- I/O动作可以粘合在一起来形成更大的I/O动作
- I/O动作可以被创建,赋值和传递到任何地方,但是它们只能在另一个I/O动作里面被执行
- main的机制
main函数是类型为IO ()
的I/O动作,main函数是Haskell程序开始执行的地方,程序中所有I/O动作都是由其顶部开始驱动。 main函数给程序中副作用提供隔离:在I/O动作中运行I/O,并且在那调用纯(非I/O)函数,即I/O动作运行I/O并且调用纯代码
关于do代码块,
- do 是用来定义一连串动作的方法,do 代码块的值是最后一个动作执行的结果
- do 代码块中,用 <- 去得到I/O动作的结果,用 let 得到纯代码的结果, let 声明后不要放上 in
- do 代码块中的每一个声明,除了 let ,都要产生一个I/O操作,这个操作在将来被执行
Pure vs Impure
Pure | Impure |
---|---|
输入相同时总是产生相同结果 | 相同的参数可能产生不同的结果 |
不会有副作用 | 可能有副作用 |
不修改状态 | 可能修改程序、系统或者世界的全局状态 |
文件和句柄(Handle)
--file toupper-imp.hs
import System.IO
import Data.Char (toUpper)
main::IO ()
main=do
inh<-openFile "input.txt" ReadMode--创建输入文件句柄
outh<-openFile "output.txt" WriteMode--创建输出文件句柄
mainloop inh outh
hClose inh
hClose outh--关闭文件句柄
mainloop::Handle->Handle->IO ()
mainloop inh outh=
do ineof<-hIsEOF inh--检查是否在输入文件的结尾(EOF)
if ineof
then return ()
else do inp<-hGetLine inh
hPutStrLn outh (map toUpper inp)
mainloop inh outh
-
return
是和<-
相反,return
接受一个纯的值,把它包装进IO动作
例如,return 7
会创建一个返回值为7的IO动作,即保存一个IO Int
类型的IO动作,在执行该IO动作时,将会产生结果 7 - IOMode
IOMode | 可读 | 可写 | 开始位置 | 备注 |
---|---|---|---|---|
ReadMode | 是 | 否 | 文件开头 | 文件必须存在 |
WriteMode | 否 | 是 | 文件开头 | 如果存在,文件内容会被完全清空 |
ReadWriteMode | 是 | 是 | 文件开头 | 如果不存在会新建文件,如果存在不会损害原来的数据 |
AppendMode | 否 | 是 | 文件结尾 | 如果不存在会新建文件,如果存在不会损害原来的数据 |
- 必须使用
hClose
函数手动关闭文件句柄 ,程序会因为资源耗尽而崩溃
Seek and Tell
-
hTell :: Handle -> IO Integer
,hTell 函数用于显示文件中读写的当前位置
例如,当文件刚新建的时候,位置为0;在写入5个字节之后,位置为5 -
hSeek :: Handle -> SeekMode -> Integer -> IO ()
,hSeek函数用于改变文件读写位置 -
hIsSeekable :: Handle -> IO Bool
查看给定的句柄是不是可定位
句柄通常对应文件,但若句柄对应网络连接、终端等,则不可定位
SeekMode
的三个可选项:
- AbsoluteSeek 表示这个位置是在文件中的精确位置,和 hTell 所给的是同样的信息
- RelativeSeek 表示从当前位置开始寻找,正数要求在文件中前进,负数要求后退
- SeekFromEnd 会寻找文件结尾之前特定数目的字节,
hSeek handle SeekFromEnd 0
定位至文件结尾
System.IO 中预定义句柄
IO非句柄函数就是IO句柄函数的标准快捷方式,如getLine = hGetLine stdin
- 标准输入 stdin 对应键盘
- 标准输出 stdout 对应显示器
- 标准错误 stderr 标准错误会输出到显示器
删除和重命名文件
在System.Directory
中,
-
removeFile :: FilePath -> IO ()
该函数接受一个文件路径参数,然后删除对应的文件。 -
renameFile :: FilePath -> FilePath -> IO ()
该函数接受旧和新两个文件路径参数;如果新文件名在另外一个目录中,可以把它看作移动文件;调用函数之前旧文件必须存在; 如果新文件名已存在,它在重命名前会被删除掉。
临时文件
临时文件可以用来存储大量需要计算的数据或其他程序要使用的数据。
-
getTemporaryDirectory :: IO FilePath
该函数可用于寻找指定机器上存放临时文件最好的地方。 -
openTempFile :: FilePath -> String -> IO (FilePath, Handle)
该函数接受两个参数:创建文件所在的目录(可用一个“.”表示当前目录)、一个命名文件的模板字符串(模板用做文件名的基础并随机添加字符以保证文件名是唯一的);IO (FilePath, Handle)
,FilePath
是创建的文件名,Handle
是以 ReadWriteMode 打开所创建文件的句柄 (处理完这个文件,需要 调用hClose关闭句柄 、removeFile 删除文件)。
下面是一个函数式IO的例子,
--file tempfile.hs
import System.IO
import System.Directory(getTemporaryDirectory,removeFile)
import System.IO.Error(catchIOError)
import Control.Exception(finally)
main::IO ()
main=withTempFile "mytemp.txt" myAction
myAction::FilePath->Handle->IO ()
myAction tempname temph=do
putStrLn "Welcome to tempfile.hs"
putStrLn $ "I have a temporary file at "++tempname
pos<-hTell temph
putStrLn $ "My initial position is "++show pos
let tempdata=show [1..10]
putStrLn $ "Writing one line containing "++show (length tempdata)++" bytes: "++tempdata
hPutStrLn temph tempdata
--hPutStrLn 总是在结束一行的时候在结尾处写上一个 \n
pos<-hTell temph
putStrLn $ "After writing, my new position is "++show pos
--Windows使用两个字节序列 \r\n 作为行结束标记 pos==24
hSeek temph AbsoluteSeek 0
putStrLn "The file content is:"
c<-hGetContents temph
putStrLn c
--数据使用 hPutStrLn写 c结尾处有一个换行符 putStrLn添加第二个换行符 结果会多显示一条空行
putStrLn $ "Which could be expressed as this Haskell literal:"
print c
withTempFile::String->(FilePath->Handle->IO a)->IO a
withTempFile pattern func=do
tempdir<-catchIOError (getTemporaryDirectory) (\_->return ".")
(tempfile,temph)<-openTempFile tempdir pattern
finally (func tempfile temph)
(do hClose temph
removeFile tempfile)
惰性IO处理方法
hGetContents :: Handle -> IO String
-
readFile :: FilePath -> IO String
、writeFile :: FilePath -> String -> IO ()
这两个函数是把文件当做字符串处理的快捷方式,它们在内部使用hGetContents
,并处理包括打开文件、读取文件、写入文件和关闭文件等细节。 -
interact :: (String -> String) -> IO ()
例如interact过滤器interact (unlines . filter (elem 'a') . lines)
输出所有包含字符“a”的行
The IO Monad
在Haskell中,纯函数(Pure Function)指不会被外部所影响的纯粹计算过程,动作(Action)可以理解为在被调用时执行相应IO操作、造成磁盘或网络副作用的过程。
--file actions.hs
str2action :: String -> IO ()
str2action input = putStrLn ("Data: " ++ input)
list2actions :: [String] -> [IO ()]
list2actions = map str2action
numbers :: [Int]
numbers = [1..10]
strings :: [String]
strings = map show numbers
actions :: [IO ()]
actions = list2actions strings
printitall :: IO ()
printitall = runall actions
-- printall的操作在其他地方被求值的时候才会执行
--实际上,因为Haskell惰性求值,操作直到执行时才会被生成
runall :: [IO ()] -> IO ()
runall [] = return ()
runall (firstelem:remainingelems) =
do firstelem
runall remainingelems
main = do str2action "Start of the program"
printitall
str2action "Done!"
简化写法如下,
str2message :: String -> String
str2message input = "Data: " ++ input
str2action :: String -> IO ()
str2action = putStrLn . str2message
numbers :: [Int]
numbers = [1..10]
main = do str2action "Start of the program"
mapM_ (str2action . show) numbers
str2action "Done!"
-- mapM_ 在 numbers . show 每个元素上应用 (str2action . show)
-- number . show 把每个数字转换成一个 String , str2action 把每个 String 转换成一个操作
-- mapM_ 把这些单独的操作组合成一个更大的操作,然后打印出这些行
-- map 是返回一个列表的纯函数,它不能执行操作
串联化(Sequencing)
do实际上是把操作连接在一起的快捷记号,可用于代替do的运算符有,
-
(>>) :: (Monad m) => m a -> m b -> m b
串联两个操作,并按顺序运行;丢弃第一个操作的结果,保留第二个操作的结果。 -
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
运行一个操作,然后把结果传递给一个返回操作的函数;运行第二个操作后,整个表达式的结果就是第二个操作的结果。
{-
main = do
putStrLn "Greetings! What is your name?"
inpStr <- getLine
putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!"
-}
main =
putStrLn "Greetings! What is your name?" >>
getLine >>=
(\inpStr -> putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!")
Return
在Haskell中, return 用来包装在Monad里面的数据。 在I/O时, return用来拿到纯数据并把它带入IO Monad。
import Data.Char(toUpper)
isYes :: String -> Bool
isYes inpStr = (toUpper . head $ inpStr) == 'Y'
isGreen :: IO Bool
isGreen =
do putStrLn "Is green your favorite color?"
inpStr <- getLine
return (isYes inpStr)
在上例中,
- 由于所有依赖I/O的结果都必须在一个IO Monad里面,所以对于由纯计算产生的 Bool ,需将其传给 return , 让return把它放进IO Monad
- 因为return是 do 代码块的最后的IO操作,所以它变成 isGreen 的返回值,而不是因为使用 return 函数。
--return 测试
returnTest :: IO ()
returnTest =
-- <-把东西从Monad里面拿出来,是 return 的反作用
do one <- return 1
let two = 2
putStrLn $ show (one + two)