《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={2⋅x|x∈N,x≤10}S={2⋅x|x∈N,x≤10} 对应的 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 | 整数,[−231−231,231−1231−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
, lastName :: String
, age :: Int
, height :: Float
, phoneNumber :: String
, flavor :: String
} deriving (Show)
|
定义完后,可以直接取值
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
具体的定义参见之前写的文章
functor 的作用是能让普通函数应用到被 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 :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
|
我更偏向于用 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
|
最后
有些章节没好好看就跳过了所以没有笔记。

浙公网安备 33010602011771号