资讯专栏INFORMATION COLUMN

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

EsgynChina / 914人阅读

摘要:生成随机数对于一个函数,如果两次调用它时使用相同的参数,它会把同样的结果返回两次。但是,这也使得产生随机数这件事变成困难。对于同一个生成器,得到的随机数是固定的。

系列文章

《Haskell趣学指南》笔记之基本语法

《Haskell趣学指南》笔记之类型(type)

《Haskell趣学指南》笔记之函数

《Haskell趣学指南》笔记之高阶函数

《Haskell趣学指南》笔记之模块

《Haskell趣学指南》笔记之自定义类型

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


Hello World

    在 helloworld.hs 里写入 main = putStrLn "hello, world!

    运行 ghc --make helloworld

    运行 ./helloworld

    得到输出 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   
    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 $ 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 获取程序名

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

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

     Hi! How are you");

    创建文件 capslocker.hs,内容如下

     import Control.Monad 
     import Data.Char 
     main = forever $ do
          l <- getLine
          putStrLn $ map toUpper l 
    

    编译:运行 ghc --make capslocker

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

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

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

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

stdout 和 stdin

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

用 openFile 打开文件

    创建 gf.txt,内容如下:

     Hey! Hey! You! You! 
     I don" t like your girlfriend! 
     No way! No way! 
     I think you need a new one! 
    

    创建 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
    

    编译并运行如何知道 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) 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7387.html

相关文章

  • Haskell趣学指南笔记模块

    摘要:系列文章趣学指南笔记之基本语法趣学指南笔记之类型趣学指南笔记之函数趣学指南笔记之高阶函数趣学指南笔记之模块趣学指南笔记之自定义类型趣学指南笔记之目前我们提到的所有函数和都是模块的一部分,默认情况下,模块会被自动导入。系列文章 《Haskell趣学指南》笔记之基本语法 《Haskell趣学指南》笔记之类型(type) 《Haskell趣学指南》笔记之函数 《Haskell趣学指南》笔记之高阶...

    187J3X1 评论0 收藏0
  • gitbook: 前端好书推荐

    摘要:它大致概述并讨论了前端工程的实践如何学习它,以及在年实践时使用什么工具。目的是每年发布一次内容更新。前端实践第一部分广泛描述了前端工程的实践。对大多数人来说,函数式编程看起来更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 这是任何人都可以用来了解前端开发实践的指南。它大致概述并...

    Ali_ 评论0 收藏0
  • gitbook: 前端好书推荐

    摘要:它大致概述并讨论了前端工程的实践如何学习它,以及在年实践时使用什么工具。目的是每年发布一次内容更新。前端实践第一部分广泛描述了前端工程的实践。对大多数人来说,函数式编程看起来更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 这是任何人都可以用来了解前端开发实践的指南。它大致概述并...

    CocoaChina 评论0 收藏0
  • gitbook: 前端好书推荐

    摘要:它大致概述并讨论了前端工程的实践如何学习它,以及在年实践时使用什么工具。目的是每年发布一次内容更新。前端实践第一部分广泛描述了前端工程的实践。对大多数人来说,函数式编程看起来更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 这是任何人都可以用来了解前端开发实践的指南。它大致概述并...

    Warren 评论0 收藏0
  • Node.js 是什么?我为什么选择它?

    摘要:单线程使用单线程来运行,而不是向之类的其它服务器,每个请求将生产一个线程,这种方法避免了上下文切换和内存中的大量执行堆栈,这也是和其它服务器为解决上一个年,著名的并发连接问题而采用的方法。 showImg(https://segmentfault.com/img/remote/1460000019968794?w=1080&h=675);当我们学习一项新的事物的时候,我们首先要知道它来...

    Joyven 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<