TOC
Open TOC
环境配置
在 Windows 上安装 Haskell
相关资源
Chapters - Learn You a Haskell for Great Good!
Real World Haskell 中文版 — Real World Haskell 中文版 (cnhaskell.com)
HaskellWiki
Hoogle (haskell.org)
图解 Functor, Applicative 和 Monad
基础
进入交互模式
装载脚本
编译程序
或
执行程序
列表和元组
列表
单类型数据结构
拼接
构建
字符串只是字符列表的语法糖
索引
区间
列表推导
元组
异构、长度固定
函数
函数调用
条件分支
模式匹配
元组的模式匹配
列表的模式匹配
as-pattern
guard
where
作用域
只对本函数(模式)可见
let
let [bindings] in [expressions]
最后一个例子是模式匹配
列表推导
交互模式
case
模式匹配是 case 表达式的语法糖
高阶函数
Curried functions
只用箭头来分隔参数和返回值类型(左侧为参数,右侧为返回类型)
部分应用(截断)
lambda
在 lambda 中使用模式匹配
另一个例子
zipWith
测试
flip
使用 lambda
map and filter
map
filter
fold and scan
fold
左折叠
可以简化为
需要生成新列表时,倾向于使用右折叠(注意右折叠二元函数的参数顺序)
无限列表的折叠
- 右折叠可以处理无限列表,左折叠无法处理(利用 Haskell 的惰性)
原因:
foldr f init [1,2,3]
等价于执行 f 1 (f 2 (f 3 init))
foldl f init [1,2,3]
等价于执行 f (f (f init 1) 2) 3
测试
scan
函数应用
定义
普通的函数调用符有最高的优先级,而 $
的优先级则最低。用空格的函数调用符是左结合的,如 f a b c
与 ((f a) b) c
等价,而 $
则是右结合的。
减少括号
映射一个函数应用到一组函数组成的列表
此处 ($ 3)
取一个函数作为参数,然后将该函数应用到 3
上
函数组合
定义
单参数
多参数
部分应用
Point-free Style
无参数
左 .
右 $
模块
在交互模式中导入
GHC 7.0 之后,支持在 GHCi 环境直接使用 import
语法
在脚本中导入
qualified
,限定导入,处理命名冲突
示例
测试
若使用限定导入
测试
构造自己的模块
模块首字母必须大写
测试
模块的层次结构
测试
使用标准库中的模块
Data.List
惰性实现的严格版本
在用 fold 处理较大的 List 时,经常会遇到堆栈溢出的问题
非严格版本的计算过程
严格版本的计算过程
Data.Char
Data.Map
关联列表
findKey
实现了 Data.Map
中的 lookup
测试
映射
fromList
将一个关联列表转换为与之等价的映射
Data.Set
略
I/O
Haskell 实际上设计了一个非常聪明的系统来处理有副作用的函数,它漂亮地将我们的程序区分成纯粹和非纯粹部分。非纯粹的部分负责和键盘还有屏幕沟通。有了这区分的机制,在和外界沟通的同时,我们还是能够有效运用纯粹所带来的好处,像是惰性求值、容错性和模组性。
I/O action
基础
编译并运行
一个 I/O action
会在我们把它绑定到 main
这个名字并且执行程序的时候触发
组合
do blocks
我们写了一个 do
并且接着一连串指令,每一步都是一个 I/O action
。将所有 I/O action
用 do
绑在一起变成了一个大的 I/O action
。这个大的 I/O action
的类型是 IO ()
,这完全是由最后一个 I/O action
所决定的。
名字绑定
getLine
是一个返回 String
的 I/O action
,name <- getLine
执行一个 I/O action
并将它的结果绑定到 name
这个名字。
getLine
在这样的意义下是不纯粹的,因为执行两次的时候它没办法保证会返回一样的值。需要注意的是,只能在不纯粹的环境(I/O action
)中处理不纯粹的数据(name <- getLine
)。
每个 I/O action
都有一个值封装在里面。这也是为什么之前的程序可以这么写:
foo
只会有一个 ()
的值。另外,最后一个 action
不能绑定任何名字。
可以在 do blocks
中使用 let bindings
(为纯粹的值绑定名字),与 list comprehensions
中的使用类似:
而 let firstName = getLine
只不过是把 getLine
这个 I/O action
起了一个不同的名字罢了。
总结:
- 绑定
I/O action
的结果时用 <-
- 对于纯粹的
expression
使用 let bindings
分支与递归
if condition then I/O action else I/O action
return
在 Haskell 中,return
的意义是利用某个 pure value
构建返回某值的 I/O action
return
不会中断 do blocks
的执行
配合 <-
与 return
来绑定名字:
可以发现,可以看到 <-
与 return
作用相反。之前的程序也可以这么写:
交互模式
在交互模式中输入一个 I/O action
并按下 Enter
键,I/O action
也会被执行:
I/O functions
putStr and putChar
putStr
相当于不换行的 putStrLn
putStr
实际上就是 putChar
递归定义出来的
getChar
注意缓冲区,只有当 Enter 被按下的时候才会触发读取字符的行为
编译并运行
print
print
相当于 putStrLn . show
在交互模式中,在终端显示结果利用的就是 print
print
和 putStrLn
的一点小区别
when
将 if something then do some I/O action else return ()
这样的模式封装起来
sequence
sequence :: [IO a] -> IO [a]
示例
等同于
另一个示例
解释 [(),(),(),(),()]
:在交互模式中对 I/O action
求值,它会被执行,并将结果返回,除非结果是 ()
mapM and mapM_
forever
forever
接受一个 I/O action
并返回一个永远执行该 I/O action
的 I/O action
forM
和 mapM
的作用一样,只是参数的顺序相反而已
这里的 (\a -> do ...)
是接受一个数字并返回一个 I/O action
的函数
文件和流
输入重定向
在 Linux 环境中测试
测试
从输入流获取字符串
使用 getContents
重写上述程序,getContents
从标准输入里读取所有的东西直到遇到 EOF
getContents
类型签名为 getContents :: IO String
getContents
使用 Lazy I/O(似乎表现为按行处理)
重定向测试
直接输入测试
转换输入
使用 interact
重写上述程序,interact
取一个类型为 String->String
的函数作为参数,返回一个 I/O action
interact
类型签名为 interact :: (String -> String) -> IO ()
文件读写
openFile
openFile :: FilePath -> IOMode -> IO Handle
注解:
type FilePath = String
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
示例
hGetContents
接受一个文件句柄作为参数,其类型签名为 hGetContents :: Handle -> IO String
,类似 getContents
hClose
接受一个文件句柄作为参数,其类型签名为 hClose :: Handle -> IO ()
注:就像 hGetContents 对应 getContents 一样,只不过是针对某个文件。我们也有 hGetLine、hPutStr、hPutStrLn、hGetChar 等等。他们分别是少了 h 的那些函数的对应。只不过他们要多拿一个 handle 当参数,并且是针对特定文件而不是标准输出或标准输入。
withFile
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
withFile
会确保文件句柄被关闭
使用 withFile
重写上述程序
我们考虑实现 withFile
,使用 bracket
,其类型签名为:
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
- 第一个参数为一个请求资源的
I/O action
- 第二个参数为一个释放资源的函数,无论是否出现异常
- 第三个参数是主要部分,如读写文件
readFile
readFile :: FilePath -> IO String
使用 readFile
重写上述程序
注意,此处我们没有得到文件句柄,readFile
的内部实现帮助我们关闭了文件句柄
writeFile and appendFile
writeFile :: FilePath -> String -> IO ()
如果我们尝试如下操作,会报错 Main: haiku.txt: openFile: resource busy (file is locked)
我们需要一个临时文件来传递数据,为此使用函数 openTempFile
,其类型签名为:
openTempFile :: FilePath -> String -> IO (FilePath, Handle)
其中还使用了 System.Directory
中的:
removeFile :: FilePath -> IO ()
renameFile :: FilePath -> FilePath -> IO ()
为了确保在发生异常时临时文件能够被清理掉,我们将使用 bracketOnError
,其类型签名为:
bracketOnError :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
类似 bracket
,只不过 bracketOnError
当出现异常时才会释放资源,重写上述程序
命令行参数
测试
这里用到了 getArgs
和 getProgName
,其类型签名分别为:
getArgs :: IO [String]
getProgName :: IO String
随机性:Random
需要先安装 PS C:\Users\VGalaxy> stack install random
random
random :: (Random a, RandomGen g) => g -> (a, g)
接受一个随机性生成器,返回一个随机值和一个新的随机性生成器
为了使用 random
,System.Random
导出了一个类型叫做 StdGen
,通过 :info
查看其信息
可知 StdGen
为 RandomGen
类型类的一个实例
为了得到一个 StdGen
的随机性生成器,我们使用 mkStdGen
函数
mkStdGen :: Int -> StdGen
来试一试,第一次使用会有一些提示信息(下面的示例会去掉提示信息)
参数相同,返回的随机值也相同
我们换一个参数
也可以使用类型注解
我们来写一个掷硬币的小程序
测试,注意这次是装载脚本,而非编译程序
randoms
randoms :: (Random a, RandomGen g) => g -> [a]
返回一个无限长的随机值序列,因为无限长,所以无法返回最后得到的新随机性生成器
在交互界面试一试
我们可以写一个函数,返回一个有限长的随机值序列和一个新随机性生成器,注意此处 Eq n
的类型约束
装载脚本来试一试(似乎装载脚本前导入模块就不会有提示信息了)
randomR and randomRs
randomR :: (Random a, RandomGen g) => (a, a) -> g -> (a, g)
第一次参数是一个下界和上界的序对
randomRs :: (Random a, RandomGen g) => (a, a) -> g -> [a]
类似 randomR
,返回一个有界的随机值序列
随机性与 I/O
getStdGen :: IO StdGen
,向系统索要初始数据,来启动全局生成器
newStdGen :: IO StdGen
,更新全局生成器
写一段程序,让用户猜测它想的数是什么
第一种方法,使用 randomR
返回的新随机性生成器显式改变参数,注意两处 prompt
,处理 Lazy I/O
第二种方法,通过 newStdGen
更新全局生成器
再谈惰性:ByteString
目前为止,我们都是用字符串处理文件,而字符串不过是列表的语法糖,注意列表的是惰性的,所以并不高效。
关于惰性的进一步解释,可以参阅 Thunk - HaskellWiki
ByteString 很像列表,每个元素占一个字节,它们处理惰性的方法也不一样。
ByteString 有两种风格,严格的和惰性的:
- 严格的:完全废除了惰性,不能有无限长的严格的 ByteString,对一个严格的 ByteString 的第一个字节求值,就必须对整个 ByteString 求值
- 惰性的:惰性程度不如列表,可以视其为严格的 ByteString 的列表,它会按块 (chunk) 读取,每个块大小为 64 KB,对一个惰性的 ByteString 的第一个字节求值,就会对最开头的块求值
导入 ByteString 时必须使用限定导入
一些类似的函数
pack and unpack
先看类型签名
将列表转换为 ByteString,其中的 Word8
表示无符号的 8 位整数,如果越界,会有警告
将 ByteString 转换为列表
B.fromChunks and B.toChunks
先看类型签名
严格的 ByteString 列表与惰性的 ByteString 之间互相转换
cons
类似 :
接受一个字节和一个 ByteString
用 ByteString 复制文件
所有的 I/O functions
都为 ByteString 版本,其余部分没有区别
这里使用了命令行参数