《Haskell 趣学指南》 笔记
《Haskell 趣学指南》是一本非常简洁明了的入门书。
Introduction
Haskell 是一门纯函数式编程语言。
函数式,相对于命令式,解决问题的方式是定义问题,而不是定义如何做。
纯函数,说的是没有副作用。也就是函数的执行不会依赖和改变状态(变量的值等)。只依赖输入。两次相同输入对应同一个输出。
Haskell 是惰性的。
表达式的值只会在需要的时候才会求值。所以使用无限长度的列表也不会有问题。
Haskell 基于静态类型。
也就是 Haskell 编译期能够在运行前推断出值的类型。
Haskell 是一帮博士设计的语言。第一版语言报告在 2003 年公布。
Haskell 的一个运行平台是 GHC (Glasgow Haskell Complier) 。文件后缀为 .hs
类型名称使用大驼峰。
变量使用小驼峰。
变量名称中可以使用 '
和 _
字符。
'
表示稍微修改的版本。
_
表示不关注该变量。
GHC 的一些命令
命令 | 作用 |
---|---|
:l | 加载文件 (Load) |
:r | 重新加载文件 |
:t | 查看类型 |
:set prompt | 改变 prelude 提示字符 |
:m + 模块名称 | 加载模块的某一部分 |
it | 上一个表达式结果 |
ghc –make xxx.hs | 编译文件 |
runhaskell xxx.hs | 跳过编译临时运行 |
Starting Out
支持大部分算术符号。使用负数需要用括号括起来。使用括号可以改变优先级
符号 | 操作 |
---|---|
+ | 加法 |
- | 减法,负数 |
* | 乘法 |
/ | 除法,结果为小数 |
() | 括号,改变优先级 |
True | 布尔值真 |
False | 布尔值假 |
&& | 布尔运算且 |
|| | 布尔运算或 |
not | 布尔运算非 |
== | 判断是否相等 |
/= | 判断是否不等 |
Haskell 不会自动隐式类型转换。所以 5 + True
会报错。5 + 4.0
也是会报错的。因为一个是整数一个是浮点数。
以上操作符均为中缀操作符。
函数调用使用前缀表达式。函数名后跟参数,以空格区分。
1 | ghci> succ 8 |
函数调用优先级最高。
1 | ghci> succ 9 + max 5 4 + 1 |
函数使用反引号「`」包裹后可使用中缀的表达式
1 | ghci> div 92 10 |
函数嵌套需要括号括住
1 | ghci> succ (succ 7) |
函数定义格式为,函数名,参数列表,=,表达式。
1 | doubleMe x = x + x |
函数定义不需要手动提升。先定义的函数可以调用后定义的函数
条件判断操作符为 if ... then ... else ...
。没有大括号,必须有 else
分支以保证 if
表达式一定有值。
条件判断操作符可以换行,但要注意缩进。
1 | doubleSmallNumber x = if x > 100 |
通常在函数后面加 '
表示稍微修改的函数。
字符串使用双引号,字符使用单引号。
变量绑定使用 let
关键字。不需要声明类型。
1 | let a = 1 |
列表使用 []
,元素用 ,
分隔
1 | ['h','e','l','l','o'] |
列表使用 ++
函数拼接。字符串是字符列表所以用 ++
拼接
1 | ghci> [1,2,3,4] ++ [9,10,11,12] |
:
用于向列表最前端插入一个元素。
1 | ghci> 'A':" SMALL CAT" |
!!
用于取出元素,长度不够会报错。
1 | ghci> "Steve Buscemi" !! 6 |
head
获得列表头元素。 tail
获得尾列表
1 | ghci> head [5,4,3,2,1] |
elem
用于判断元素是否在列表中
1 | ghci> 4 `elem` [3,4,5,6] |
..
在列表中表示范围
1 | ghci> [1..20] |
replicate
用于重复相同元素一定的次数。
1 | ghci> replicate 3 10 |
其他函数参见常用函数
列表解析式(List comprehensions)是 Haskell 中一种特殊的列表构造方式,类似于数学中的集合。
比如集合 $S = \lbrace 2 \cdot x | x \in \Bbb{N} , x \le 10 \rbrace $ 对应的 Haskell 代码
1 | ghci> [x*2 | x <- [1..10]] |
|
左边为集合元素表达式,右边为过滤条件。
1 | ghci> [x*2 | x <- [1..10], x*2 >= 12] |
支持多个元素(变量)
1 | ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]] |
元组用于保存不同类型变量。
1 | ("Christopher", "Walken", 55) |
Types and Typeclasses
Haskell 使用 Hindley-Milner 类型系统。格式为
1 | 函数名字 :: (类型约束) => 参数1 -> 参数2 ... -> 参数n |
比如加法的类型签名为
1 | ghci> :t (+) |
其中 a
为类型变量。Num a
表示 a
为数字类型,可以是 int
,Integer
等。
Haskell 的常见类型
类型 | 描述 |
---|---|
Int | 整数,[$ -2^{31}$,$2^{31} - 1 $ ] |
Integer | 整数,无范围限制 |
Float | 单精度浮点数 |
Double | 双精度浮点数 |
Bool | 布尔类型 |
Char | 字符 |
类型类(Typeclass) 指的是类型的类型。是对类型的再一次归类。如 int
可以是 Eq
类,也可以是 Ord
类。
Haskell 的常见类型类
类型类 | 描述 | 支持的操作符 |
---|---|---|
Eq | 可比较是否相等 | == /= elem |
Ord | 可比较顺序 | < > <= >= |
Show | 任意类型转为可显示的字符串 | show |
Read | 字符串解析为特定类型 | read |
可枚举类型类 | .. succ pred | |
Bounded | 有界类型类 | minBound maxBound |
Num | 数字类型类 | + - * |
Integral | ||
Floating | 小数类型类 | |
Functor | 函子 | fmap |
Applicative Functor | 可相互应用的函子 | pure <*> |
Monad | 单子 | return >>= |
Syntax in Functions
模式匹配类似于命令式语言中的 Switch ... case
匹配,不过匹配的源表达式更复杂。
1 | lucky :: (Integral a) => a -> String |
匹配顺序从上到下,匹配失败会报错。
模式匹配经常于递归搭配使用,先写出递归的边界条件,然后匹配剩下的递归
1 | factorial :: (Integral a) => a -> a |
模式匹配还可用于元组和列表
1 | addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a) |
模式匹配支持 all@pattern
的方式匹配。
1 | capital :: String -> String |
如果说模式匹配类似于 Switch ... case
,那么条件匹配(Guards)类似于 if ... then ... else
条件匹配可以支持一个范围区间,而模式匹配不可以,除非手写范围区间内所有情况。
条件匹配用 |
分隔条件。
1 | bmiTell :: (RealFloat a) => a -> String |
与 if
类似,条件匹配必须有 otherwise
保证表达式一定有值。
定义函数时可以使用反引号「`」
1 | myCompare :: (Ord a) => a -> a -> Ordering |
限定语句(Where) ,可用于声明变量。
1 | bmiTell :: (RealFloat a) => a -> a -> String |
绑定语句(Let),与限定语句对应,用于在开头绑定变量。
形式为 Let <binding> in <expression>
。
1 | cylinder :: (RealFloat a) => a -> a -> a |
绑定语句很灵活,不仅仅在函数定义中使用,还可以在列表等多处地方使用。
1 | ghci> [let square x = x * x in (square 5, square 3, square 2)] |
case
语句。类似于命令式语言的 case
。
1 | head' :: [a] -> a |
Recursion
Haskell 的函数定义支持递归
1 | quicksort :: (Ord a) => [a] -> [a] |
有了递归之后常见函数都可以使用递归实现
1 | replicate' :: (Num i, Ord i) => i -> a -> [a] |
Higher order functions
柯里化是一种将多参函数化为多个单参函数的奇数。Haskell 中函数只支持一个参数,通过柯里化就能实现支持多个参数的函数。多参函数的调用过程可以视为单参函数的逐步填充过程。调用函数却没有给够参数的过程称为部分应用(partially apply)
1 | ghci> max 4 5 |
接收或返回函数的一类函数称为高阶函数。
1 | applyTwice :: (a -> a) -> a -> a |
Haskell 常见的高阶函数
函数 | 描述 |
---|---|
map | 对列表所有元素应用函数,返回所有元素应用函数后的列表 |
filter | 对列表所有函数应用过滤函数,返回符合条件的列表 |
foldl | 从最左边的元素开始,从左往右应用函数,返回累积值。 |
foldr | 从最右边的元素开始,从右往左应用函数,返回累积值。 |
foldl1 | 与 foldl 类似,不用提供初始值 |
foldr1 | 与 foldr 类似,不用提供初始值 |
scanl | 与 foldl 类似,不过保留每个步骤的结果 |
scanr | 与 foldr 类似,不过保留每个步骤的结果 |
例子
1 | ghci> sum (takeWhile (<10000) (filter odd (map (^2) [1..]))) |
列表操作大部分都可以使用 fold
1 | maximum' :: (Ord a) => [a] -> a |
匿名函数(Lambda),没有名字的函数。
1 | numLongChains :: Int |
$
函数应用符。右结合,优先级很低,所以可以用来省略括号。
1 | sum (filter (> 10) (map (*2) [2..10])) |
还可以这么玩
1 | ghci> map ($ 3) [(4+), (10*), (^2), sqrt] |
(.)
函数组合。 (f . g) x = f (g x)
1 | ghci> map (negate . abs) [5,-3,-6,7,-3,2,-19,24] |
Modules
Haskell 的模块与其他语言的模块概念相似。
导入模块 import <module name>
1 | import Data.List |
只引用模块中部分函数
1 | import Data.List (nub, sort) |
不用模块中的某些函数
1 | import Data.List hiding (nub) |
取别名
1 | import qualified Data.Map as M |
编写模块与编写普通的 Haskell 代码一样,只不过需要在文件头部加上需要导出的函数模块声明。
1 | module Geometry |
Making Our Own Types and Typeclasses
代数数据类型(Algebraic data types),是 Haskell 中的一种自定义数据类型。注意不要缩写成 ADT 。因为有好几个缩写都可以是 ADT 。
纯数据类型
1 | data Bool = False | True |
带构造器类型
1 | data Shape = Circle Float Float Float | Rectangle Float Float Float Float |
注意类型首字母大写,类型变量首字母小写的规定。
为了能显示需要继承 Show
类型类,使用 deriving
关键字,多个类型类用 ,
分隔。
1 | data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show) |
实例化
1 | ghci> Circle 10 20 5 |
定义了抽象数据类型后通常需要定义相应的 Getter
。Haskell 中有记录语法(Record Syntax)避免手写这些重复的代码。
1 | data Person = Person { firstName :: String |
定义完后,可以直接取值
1 | ghci> :t flavor |
代数数据类型中的类型构造函数可以接受类型参数,这点类似泛型中的参数变量
1 | data Maybe a = Nothing | Just a -- a 为类型参数 |
接受不同的类型
1 | ghci> Just "Haha" |
既然有类型参数,那么类型参数也是可以有的
1 | data (Ord k) => Map k v = ... |
有时候新的类型跟已有类型内容一样只是名字不同,可以使用类型别名(Type Synoyms)。
1 | type String = [Char] |
类型别名也可以带类型参数
1 | type AssocList k v = [(k,v)] |
代数数据类型还支持递归
1 | data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord) |
实例化
1 | ghci> Empty |
递归最典型的数据结构是树
1 | data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq) |
如果想自定义类型类的行为,可以实现相应的实例
1 | instance Show TrafficLight where |
实例中也可以有类型约束
1 | instance (Eq m) => Eq (Maybe m) where |
关于 data ,type 和 new type 的区别
data 用于声明全新的类型
type 用于声明已有的类型的别名
new type 用于声明已有的类型的封装
Input and Output
Haskell 是纯函数语言,而输入输出是有副作用的,也就不是纯粹的函数。先来看最应该写在文章开头的 Hello World
1 | main = putStrLn "hello, world" |
直接拼接输入是不可以的,因为类型不一致
1 | nameTag = "Hello, my name is " ++ getLine -- getLine :: IO String |
这样也是不行的,相当于给 getLine
取了一个 name
的别名
1 | name = getLine |
需要使用 do 语法。这是因为使用了 Monad
1 | main = do |
do 语法中变量赋值需要使用 let
1 | import Data.Char |
打开文件有相应的函数。但是如果只是读写可以使用 readFile
writeFile
1 | import System.IO |
Functors, Applicative Functors and Monoids
具体的定义参见之前写的文章
fmap
的作用是能让普通函数应用到被 functor
包裹的值
1 | ghci> fmap (replicate 3) [1,2,3,4] |
IO
也是一个 functor
1 | instance Functor IO where |
IO
的示例用法
1 | main = do line <- getLine |
也可以使用 fmap
1 | main = do line <- fmap reverse getLine |
列表的 fmap
是 map
1 | ghci> fmap (replicate 3) [1,2,3,4] |
还有一个 functor
说出来你可能不信。就是 (->)
。对,就是函数签名里面的 ->
1 | instance Functor ((->) r) where |
functor
能使正常的函数应用到 functor
里面的值。applicative functor
能使内部的值相互应用
1 | ghci> Just (+3) <*> Just 9 |
遇到多个参数可以使用 (<$>)
应用函数提升为 functor
再使用 <*>
1 | ghci> ((+) $ 2) 3 -- 普通版 |
如果觉得麻烦,还有专门用于提升的函数。提升两个参数的函数为 Control.Applicative.liftA2
1 | ghci> liftA2 (+) [3] [4] |
IO
使用 <*>
1 | myAction :: IO String |
(->)
的 <*>
比较奇怪,看起来像 S 组合子
1 | instance Applicative ((->) r) where |
应用
1 | ghci> (+) <$> (+3) <*> (*100) $ 5 |
sequenceA
是一个很有趣的函数,把可遍历的函子转为函子包裹的可遍历结构。
1 | ghci> :t sequenceA |
应用
1 | ghci> sequenceA [Just 3, Just 2, Just 1] |
A Fistful of Monads
终于到了这个该死的单子了。
monad
的定义有好几种。比如有用 return
和 >>=
(bind)定义的
1 | class Monad m where |
我更偏向于用 return
和 join
定义
1 | class Monad m where |
Haskell 中有很多类型都是 Monad 。例如列表,元组,Maybe
和 IO
等。
Monad 可以说是一种设计模式,用层次表达或者记录内容。
Maybe
表达了值可能不存在的情况
[]
表达了有多种可能的情况
IO
表达了与外界交互的情况
应用
1 | ghci> return "WHAT" :: Maybe String |
因为 Monad 的 >>=
太常用了。所以引入了 do 语法(糖)
1 | main = do |
等价于
1 | putStr "Please enter something dirty:" >> getLine >>= \line -> let line' = reverse line in putStrLn line' |
所以 <-
其实就是 ->
绑定。
For a Few Monads More
Writer
用于附着日志的 monad
1 | import Control.Monad.Writer |
Reader
用于处理函数的 monad 。也就是说函数也是一种 monad
1 | instance Monad ((->) r) where |
>>=
的实现看起来很奇怪。先看例子
1 | ghci> ((+2) >>= (+)) 1 |
从 >>=
的签名 (>>=) :: Monad m => m a -> (a -> m b) -> m b
看出
h >>= f
也就是 h
是 m a
即 (-> a)
,是一个 monad 值,而这种 monad 是函数。
f
也就是 (a -> mb)
,说明 f
是一个能够返回 monad 值的函数,这个返回的 monad 是一个函数。
在 (+2) >>= (+)
中,h
是 (+2)
,f
是 (+)
由 h >>= f = \w -> f (h w) w
有
1 | (+2) >>= (+) |
最后的结果是一个 monad ,这种 monad 是函数 \w -> ((w+2) + ) w
有些常用的辅助函数
1 | ghci> (+1) $ 2 |
最后
有些章节没好好看就跳过了所以没有笔记。