《Haskell趣学指南》笔记之I/O

1,694 阅读7分钟

系列文章


Hello World

  1. 在 helloworld.hs 里写入 main = putStrLn "hello, world!
  2. 运行 ghc --make helloworld
  3. 运行 ./helloworld
  4. 得到输出 hello, world!

然后来看看函数的类型

ghci> :t putStrLn 
putStrLn :: String -> IO () 
ghci> :t putStrLn "hello, world" 
putStrLn "hello, world" :: IO () 
ghci> :k IO
IO :: * -> *
ghci> :k IO()
IO() :: *

IO () 返回的类型为 (),即空元组,也叫单元。下一节会出现的 IO String 返回的类型为 String。

() 即使一个类型,也是对应类型的值。

do 语法

do 语法可以将多个 I/O 操作合成一个。

main = do   
    putStrLn "Hello, what' s your name?"   
    name <- getLine &emsp; 
    putStrLn ("Hey " ++ name ++ ", you rock!") 
  • getLine 的类型为 IO String
  • getLine 是一个产生字符串的 I/O 操作
  • 注意 name <- 并没有写成 name =
  • 只有在 I/O 操作的上下文中才能读取 I/O 操作的内容,这就是 Haskell 隔离『纯代码』和『不纯的代码』的方式
  • do语法会自动将最后一个操作的值作为自己的返回值

IO String 与 String 的区别

nameTag = "Hello, my name is " ++ getLine 

++ 左边的类型是 String,右边的类型为 IO String,所以上面的代码会报错。必须通过 name <- getLine 取出这个 String,才能继续。只能在不纯的环境中处理不纯的数据。不然不纯的代码会像污水那样污染其余的代码,保持 I/ O 相关的代码尽可能小,这对我们的健康有好处。

myLine = getLine

如果代码写成了这样

myLine = getLine 

这只是给 getLine 增加了一个别名!从 I/O 操作获取值的唯一方法是使用 <- 。每次我们在 GHCi 中按下回车键,GHCi 都会对返回值应用 show,再将生成的字符串交给 putStrLn,从而输出到终端。

return 是不一样的

Haskell 的 return 跟其他语言不一样,return 能够基于一个纯的值来构造 I/O 操作。而且 return 不会中断代码。所以,在 I/O 上下文中,return "hi" 的类型就是 IO String。那么将一个纯值转换为一个什么都不做的 I/ O 操作又有什么意义呢?作用之一是 return () 能够构建一个什么都不做的 I/O 操作。

几个 I/O 函数

  • putStr
  • putStr
  • print (相当于 putStrLn . show)
  • when <condition> $ do IO操作
  • sequence [getLine getLine]<- 取多个 I/O 操作的结果组成一个列表
  • mapM print [1, 2, 3] 等价于 sequence $ map print [1, 2, 3]
  • mapM_ 则是不保留返回值版本的 mapM
  • forever $ do IO操作
  • forM 则是把 mapM 的参数位置对换,某些时候比较方便
  • getContents 从标准输入里读取所有的东西直到遇到一个 end-of-file 字符,而且 getContents 是惰性的
  • interact fn 取一个类型为 String -> String 的函数 fn 作为参数,返回这样一个 I/ O 操作:接受输入,把一个函数作用在输入上,然后输出函数运行结果
  • getArgs 获取命令行参数
  • getProgName 获取程序名

读入文件流是随着时间连续地进入、离开程序的一组数据片。

  1. 创建文件 test.txt,内容如下

     Hi! How are you?
     I'm Fine. Thank you. And you?
     I'm Fine too.
    
  2. 创建文件 capslocker.hs,内容如下

     import Control.Monad 
     import Data.Char 
     main = forever $ do
          l <- getLine
          putStrLn $ map toUpper l 
    
  3. 编译:运行 ghc --make capslocker

  4. 将 test.txt 的内容传给 capslocker:运行 ./capslocker < test.txt

  5. 然后你就会看到所有字母变成了大写上面代码也能用 getContents 简化

import Data.Char 
main = do
    contents <- getContents
    putStr $ map toUpper contents 

也可以不传入 test.txt 文件,直接运行 ./capslocker,然后输入一行行文本,但是注意,最终你要按 Ctrl+D 来表示内容结束。

stdout 和 stdin

一种理解从终端读入数据的方式是设想我们在读取一个文件。输出到终端也可以同样理解——它就像在写文件。我们可以把这两个文件叫做 stdout 和 stdin,分别表示标准输出和标准输入。

用 openFile 打开文件

  1. 创建 gf.txt,内容如下:

     Hey! Hey! You! You! 
     I don' t like your girlfriend! 
     No way! No way! 
     I think you need a new one! 
    
  2. 创建 gf.hs,内容如下:

     import System.IO 
     main = do
         handle <- openFile "gf. txt" ReadMode
         contents <- hGetContents handle
         putStr contents
         hClose handle
     -- 注意 openFile ReadMode hGetContents hClose 这几个函数,其中的 h 前缀表示它接收 handle
    
  3. 编译并运行如何知道 openFile 的各个参数的意思呢?

λ> :t openFile
openFile :: FilePath -> IOMode -> IO Handle
λ> :info FilePath
type FilePath = String  -- Defined in ‘GHC.IO’λ> :info IOMode
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
        -- Defined in ‘GHC.IO.IOMode’λ> :info Handle
data Handle
  = GHC.IO.Handle.Types.FileHandle FilePath...

用 withFile 打开文件

import System.IO 
main = do
     withFile "girlfriend.txt" ReadMode (\handle -> do
         contents <- hGetContents handle
         putStr contents) 

bracket 函数

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c 

怎么用

bracket (openFile name mode)-- 打开文件
    (\handle -> hClose handle) -- 失败了怎么办
    (\handle -> fn handle)         -- 成功了怎么办

用 bracket 很容易实现 withFile。

生成随机数对于一个函数,如果两次调用它时使用相同的参数,它会把同样的结果返回两次。这很酷,因为它让我们能更好地理解程序,它还让我们能够延迟求值。但是,这也使得产生随机数这件事变成困难。

random :: (RandomGen g, Random a) => g -> (a, g) 

random 接受一个随机数生成器,返回一个随机数和一个新的随机数生成器。然后你可以用新的生成器再去生成一个新的随机数和一个新的生成器。以此类推。对于同一个生成器,得到的随机数是固定的。

ghci>import System.Random
ghci> random (mkStdGen 100) :: (Int, StdGen) 
(-1352021624, 651872571 1655838864) 
ghci> random (mkStdGen 100) :: (Int, StdGen) 
(-1352021624, 651872571 1655838864)
ghci> random (mkStdGen 949494) :: (Int, StdGen) 
(539963926, 466647808 1655838864)
ghci> random (mkStdGen 949488) :: (Float, StdGen) 
(0. 8938442, 1597344447 1655838864) 
ghci> random (mkStdGen 949488) :: (Bool, StdGen) 
(False, 1485632275 40692) 
ghci> random (mkStdGen 949488) :: (Integer, StdGen) 
(1691547873, 1597344447 1655838864) 

randoms 接受一个生成器,返回一个无限长的随机值列表

ghci> take 5 $ randoms (mkStdGen 11) :: [Int] 
[-1807975507, 545074951,- 1015194702,- 1622477312,- 502893664] 

并没有返回一个新的生成器,因为这个生成器在列表的末尾,而这个列表是无限长的……

randomR 在一个范围内生成随机数

ghci> randomR (1, 6) (mkStdGen 359353) 
(6, 149428957840692) 

getStdGen

之前我们每次生成随机数都要自己先写一个数字,这很傻……所以 System.Random 提供了 getStdGen,它会向系统索要初始的全局生成器。但 getStdGen 是一个 IO 操作,它返回的类型是 IO stdGen。

import System.Random 
main = do
    gen <- getStdGen
    putStr $ take 20 (randomRs ('a',' z') gen) 

但是如果你调用 getStdGen 两次,你会获得同一个 gen。第二次应该使用 newStdGen。这个函数除了返回一个 IO stdGen 类型的值,还会把全局生成器给更新了。你再调用 getStdGen 就能得到不同的随机数这时你会疑惑,getStdGen 为什么能返回不同的结果呢?因为它是一个 I/O 操作!

import System.Random

main = do
  gen <- getStdGen
  let a = fst ((random gen) :: (Int, StdGen))
  print a
  gen' <- newStdGen
  let b = fst ((random gen') :: (Int, StdGen))
  print b
  gen'' <- getStdGen
  let c = fst ((random gen'') :: (Int, StdGen))
  print c
  print "end"

这是我瞎写的代码。

字节串 bytestring

形如[ 1, 2, 3, 4] 的列表只是 1: 2: 3: 4:[] 的语法糖。当第一个元素被强制求值时(比如说输出它),列表的其余部分 2: 3: 4:[] 只是一个许诺将会产生列表的承诺。我们把这个承诺叫做 thunk。 thunk 大致上是一个延迟的计算。字节串有两种风格:严格的(strict)和惰性的(lazy)。

strict bytestring 废除了惰性,没有 thunk。在 Data.ByteString 中实现了。 lazy bytestring 是惰性的,但比列表效率高。在 Data.ByteString.Lazy 中实现了。惰性的字节串的数据存储在一些块(chunk)里(不要和 thunk 混淆了),每个块的大小是 64 KB。所以,如果你要对惰性的字节串的一个字节求值(比如说输出它),最开头的 64 KB 都会被求值。在那之后,其余块实现为一个承诺(thunk)。使用示例

import qualified Data. ByteString. Lazy as B 
import qualified Data. ByteString as S

ghci> B. pack [99, 97, 110] 
Chunk "can" Empty 
ghci> B. pack [98.. 120] 
Chunk "bcdefghijklmnopqrstuvwx" Empty 
ghci> let by = B. pack [98, 111, 114, 116] 
ghci> by 
Chunk "bort" Empty 
ghci> B. unpack by 
[98, 111, 114, 116] 
ghci> B. fromChunks [S. pack [40, 41, 42], S. pack [43, 44, 45], S. pack [46, 47, 48]] 
Chunk "()*" (Chunk "+,-" (Chunk "./0" Empty)) 
ghci> B. cons 85 $ B. pack [80, 81, 82, 84] 
Chunk "U" (Chunk "PQRT" Empty)