…就这样开始了 …And so It Begins

Haskell

  • 函数式 Functional:程序完全由函数构成,函数可以以函数为参数或返回函数,循环结构是递归
  • 纯净 Pure:函数没有副作用
  • 懒惰 Lazy:只有需要时才会计算值
  • 强类型 Strongly typed:每个值和表达式都有类型,在编译时检查类型错误
  • 类型推断 Type inferred:编译器可以推断类型
  • 垃圾收集 Garbage-collected:不需要手动管理内存
  • 编译 Compiled: Haskell 是编译型语言

GHCi

命令行界面运行 stack ghci 进入交互模式

ghci> 1+1
2
ghci> "asdf"
"asdf"
ghci> reverse "asdf"
"fdsa"
ghci> :type "asdf"
"asdf" :: String
ghci> tail "asdf"
"sdf"
ghci> :type tail "asdf"
tail "asdf" :: String
ghci> :type tail
tail :: [a] -> [a]
ghci> :quit
Leaving GHCi.

: 开头的命令只有在 GHCi 界面才能使用

  • :type:t:给出类型
  • :quit:q:退出界面

表达式和类型

| 类型 | 字面值 | 使用 | 运算符 | | :------------------- | :------------------------------ | :-------------- | :-------------------------- | --- | -------- | | Int | 1, 2, -3 | 64 位有符号整数 | +, -, *, div, mod | | Integer | 1, -2, 900000000000000000 | 无限大的整数 | +, -, *, div, mod | | Double | 0.1, 1.2e5 | 浮点数 | +, -, *, /, sqrt | | Bool | True, False | 真值 | &&, | |, not | | String[Char] | "abcd", "" | 字符串 | reverse, ++ |

类型名大写字母开头,函数名和变量名小写字母开头

函数类型用 -> 表示:

参数 1 类型-> 参数 2 类型-> 参数 3 类型 -> 返回值类型

注意 Num a => a 表示任何数字类型

Haskell 程序结构

-- Haskell 程序中有一个 module,module 由定义组成
module Gold where

-- 定义常量 phi
phi :: Double -- 类型签名或类型注释,可以不写,但推荐写上
phi = (sqrt 5 + 1) / 2

-- 定义函数 polynomial
-- 函数接收一个 Double 类型的值,返回一个 Double 类型的值
polynomial :: Double -> Double
-- “-” 左边的 x 是函数的参数,注意 ^ 表示乘方
polynomial x = x^2 - x - 1

-- 定义了函数 f,没有类型注释的版本
f x = polynomial (polynomial x)

-- 运行程序时做的事,用到了 do 和 IO Monad
main = do
print (polynomial phi)
print (f phi)
> stack runhaskell Gold.hs
0.0
-1.0

在 GHCi 中,可以使用 :{ :} 实现多行定义

ghci> :{
ghci| polynomial :: Double -> Double
ghci| polynomial x = x^2 - x - 1
ghci| :}

如果你有一个 .hs 文件,则可以把它 :load:l 进 GHCi,可以使用 :reload:r 更新

一些值得注意的地方

可以出现如下错误

halve :: Int -> Int
halve x = x / 2
error:
No instance for (Fractional Int) arising from a use of ‘/'
In the expression: x / 2
In an equation for ‘halve': halve x = x / 2
ghci> div 7 2
3
ghci> 7.0 / 2.0
3.5

因为 / 表示小数除法,div 表示整除

/= 表示“不等于”

在 Haskell 中,没有语句 statement,它的 if表达式 expression,必须有 else

price = if product == "milk" then 1 else 2

定义

where 把本地定义加到定义中

circleArea :: Double -> Double
circleArea r = pi * square r
where pi = 3.1415926
square x = x * x

let...in 是一个表达式

circleArea r = let pi = 3.1415926
square x = x * x
in pi * square r

变量定义后是不可变 immutable 的,所以其本质上是一个定义 definition

本地定义会屏蔽 shadow 外部同名定义

模式匹配

按照顺序匹配直到找到对应的

greet :: String -> String -> String
greet "Finland" name = "Hei, " ++ name
greet "Italy" name = "Ciao, " ++ name
greet "England" name = "How do you do, " ++ name
greet _ name = "Hello, " ++ name

_ 表示匹配所有东西,也可以用一个定义 s 代替

循环

尾递归代替

factorial :: Int -> Int
factorial 1 = 1
factorial n = n * factorial (n-1)

缩进

总的来说是:

  • 一组的东西从同一列开始
  • 如果表达式不得不分到不同行,则增加缩进
i x = let y = x+x+x+x+x+x in div y 5

j x = let y = x+x+x
+x+x+x
in div y 5

k = a + b
where a = 1
b = 1

l = a + b
where
a = 1
b = 1

要么作为英雄而死… Either You Die a Hero…

Either you die a hero, or you live long enough to see yourself become the villain.

要么像英雄一般死去,要么看着自己变成恶棍。

——《蝙蝠侠·黑暗骑士》

我不知道这个标题是什么意思

递归与帮手函数

通常在原函数后加 表示帮手函数

fibonacci :: Integer -> Integer
fibonacci n = fibonacci' 0 1 n

fibonacci' :: Integer -> Integer -> Integer -> Integer
fibonacci' a b 1 = b
fibonacci' a b n = fibonacci' b (a+b) (n-1)

警卫(多分支)

f x y z
| 条件 1 = something
| 条件 2 = other
| otherwise = somethingother

列表

列表及其类型:

[True,True,False] :: [Bool]
["Moi","Hei"] :: [String]
[] :: [a] -- 任何列表
[[1,2],[3,4]] :: [[Int]] -- 列表的列表
[1..7] :: [Int] -- 即[1,2,3,4,5,6,7]
函数及注释 功能 函数及注释 功能
head :: [a] -> a 返回第一个元素 length :: [a] -> Int 返回列表的长度
tail :: [a] -> [a] 返回除第一个元素以外的所有元素 init :: [a] -> [a] 返回除最后一个元素以外的所有元素
take :: Int -> [a] -> [a] 返回前 n 个元素 drop :: Int -> [a] -> [a] 返回除前 n 个元素以外的所有元素
(++) :: [a] -> [a] -> [a] 合并列表 (!!) :: [a] -> Int -> a 索引列表
reverse :: [a] -> [a] 反转列表 null :: [a] -> Bool 判断列表是否为空

这些函数都不会改变原来的列表,而是返回一个新列表

Maybe 类型

用于处理错误的情况,使用 JustNothing 构造:

login :: String -> Maybe String
login "f4bulous!" = Just "unicorn73"
login "swordfish" = Just "megahacker"
login _ = Nothing

使用模式匹配接收

perhapsMultiply :: Int -> Maybe Int -> Int
perhapsMultiply i Nothing = i
perhapsMultiply i (Just j) = i*j

Either 类型

Left 用于传递错误信息,右 Right 用于传递正确信息因为 Right 意为“正确的”

发出:

readInt :: String -> Either String Int
readInt "0" = Right 0
readInt "1" = Right 1
readInt s = Left ("Unsupported string: " ++ s)

接收:

iWantAString :: Either Int String -> String
iWantAString (Right str) = str
iWantAString (Left number) = show number

case-of 表达式

相当于多分支

describe :: Integer -> String
describe n = case n of 0 -> "zero"
1 -> "one"
2 -> "an even prime"
n -> "the number " ++ show n

变形的 Catamorphic

函数式编程

以函数为参数的函数

applyTo1 :: (Int -> Int) -> Int
applyTo1 f = f 1

map 将函数作用于列表中的每一个元素

map :: (a -> b) -> [a] -> [b]
map addThree [1,2,3]
==> [4,5,6]

filter 从列表中选择符合条件的元素

filter :: (a -> Bool) -> [a] -> [a]
filter positive [0,1,-1,3,-3]
==> [1,3]

都使用到了多态 polymorphism

部分应用

如果定义了 add :: Int -> Int -> Int,则 addThree 可以用 (add 3) 代替,而不需要再写一个函数

函数类型 Integer -> Integer -> Integer -> Bool 意思是 Integer -> (Integer -> (Integer -> Bool)),即多参数函数只是一个返回函数的函数,所以 between 1 2 3 可以写成 ((between 1) 2) 3

这种表示多参数函数的方法叫做咖喱 currying是印度人发明的吗?

运算符也可以使用:

ghci> map (*2) [1,2,3]
[2,4,6]
ghci> map (2*) [1,2,3]
[2,4,6]
ghci> map (1/) [1,2,3,4,5]
[1.0,0.5,0.3333333333333333,0.25,0.2]

前缀和中缀符号

中缀符号可以转化为前缀符号(越来越像 Lisp 了):

(+) 1 2 ==> 1 + 2 ==> 3

给运算符加括号可以将其当作函数使用:

ghci> zipWith (+) [0,2,5] [1,3,3]
[1,5,8]

二元函数也可以通过给其周围加上 ``` 转为中缀符号:

(+1) `map` [1,2,3] ==> map (+1) [1,2,3] ==> [2,3,4]

Lambda

相当于匿名函数,与 λ 演算有关

ghci> (\x -> x*x) 3
9
Prelude> filter (\x -> reverse x == x) ["ABBA","ACDC","otto","lothar","anna"]
["ABBA","otto","anna"]
Prelude> (\x y -> x^2+y^2) 2 3
13

\ 表示希腊字母 λ

可以使用 letwhere 定义函数代替

.$ 运算符

. 是函数组合 composition 操作符

(.) :: (b -> c) -> (a -> b) -> a -> c

作用相当于 (f.g) x ==> f (g x)

$ 实际上将右结合改成了左结合

($) :: (a -> b) -> a -> b

两者结合可以去掉很多括号Lisp 你是来挑事的是吧?

reverse (map head (map reverse (["Haskell","pro"] ++ ["dodo","lyric"])))
reverse . map head . map reverse $ ["Haskell","pro"] ++ ["dodo","lyric"]

有时可以使用 $ 将列表中的函数映射到一个值上

map ($"string") [reverse, take 2, drop 2]
==> ["gnirts", "st", "ring"]

更多列表缠绕函数

takeWhile :: (a -> Bool) -> [a] -> [a]   -- 当满足谓词时,从列表中取出元素
takeWhile even [2,4,1,2,3] ==> [2,4]

dropWhile :: (a -> Bool) -> [a] -> [a] -- 当满足谓词时,从列表中倒掉元素
dropWhile even [2,4,1,2,3] ==> [1,2,3]

elem 检查列表是否包含某个元素:

elem 3 [1,2,3]   ==> True
elem 4 [1,2,3] ==> False

zipWith 一个一个元素将两个列表合并

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (++) ["John","Mary"] ["Smith","Cooper"]
==> ["JohnSmith","MaryCooper"]

id :: a -> a 返回自己

const :: a -> b -> a 总是返回第一个元素

列表和递归

: 用一个头和尾构造新的列表

每一个列表本质上都是由 :[] 构成的,[x,y,z] 实际上是 x:(y:(z:[])) 的简写,++ 实际上是用递归的 : 定义的

-- 构造列表
descend 0 = []
descend n = n : descend (n-1)

列表可应用模式匹配

describeList :: [Int] -> String
describeList [] = "an empty list"
describeList (x:[]) = "a list with one element"
describeList (x:y:[]) = "a list with two elements"
describeList (x:y:z:xs) = "a list with at least three elements"

mapfilter 的实现:

map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs

filter :: (a -> Bool) -> [a] -> [a]
filter _pred [] = []
filter pred (x:xs)
| pred x = x : filter pred xs
| otherwise = filter pred xs

尾递归和非尾递归:

-- 非尾递归版本
doubleList :: [Int] -> [Int]
doubleList [] = []
doubleList (x:xs) = 2*x : doubleList xs

-- 尾递归版本,注意,生成的列表是反着的
doubleList :: [Int] -> [Int]
doubleList xs = go [] xs
where go result [] = result
go result (x:xs) = go (result++[2*x]) xs

列表解析

map

[2*i | i<-[1,2,3]]
==> [2,4,6]

filter

[i | i <- [1..7], even i]
==> [2,4,6]

自定义操作符和类型洞

操作符和函数定义没什么区别

(+++) :: String -> String -> String
a +++ b = a ++ " " ++ b

= 右边使用 __name,解释器可以推断空缺处的表达式

Prelude> filter _hole [True,False]

<interactive>: error:
Found hole: _hole :: Bool -> Bool

真正的优雅 Real Classy

Tuples

Tuples 相当于固定长度的列表,但是 tuple 中的元素类型可以不同

用函数 fstsnd 获取 tuple pair 中的第一个和第二个元素

zip :: [a] -> [b] -> [(a, b)]    -- 把两个列表组合成 pair 的列表
unzip :: [(a, b)] -> ([a], [b]) -- 把 pair 的列表分解成两个列表
partition :: (a -> Bool) -> [a] -> ([a], [a]) -- 把列表分成满足和不满足谓词的两个列表

Folding

把一个列表变成了一个值,foldr 函数就是这个用处

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f y [] = y
foldr f y (x:xs) = f x (foldr f y xs)

可以看到,是从后往前递归执行的

例如求和可以这么写 sum = foldr (+) 0

类型类

== 运算符需要对象是 Eq 类,Eq类型类,即支持相似操作的类型的组合,和面向对象编程中的类不是一个东西

(==) :: (Eq a) => a -> a -> Bool
(/=) :: Eq a => a -> a -> Bool

类型约束:

f :: (Eq a) => (a -> a) -> a -> Bool
f g x = x == g x

其他标准类型类:

Ord 类表示支持排序的类型,Ordering 类型的值 LT 表示小于,RT 表示大于,EQ 表示等于,支持 sort

compare :: Ord a => a -> a -> Ordering
(<) :: Ord a => a -> a -> Bool
(>) :: Ord a => a -> a -> Bool
(>=) :: Ord a => a -> a -> Bool
(<=) :: Ord a => a -> a -> Bool
max :: Ord a => a -> a -> a
min :: Ord a => a -> a -> a

Num 包括整数算术

(+) :: Num a => a -> a -> a
(-) :: Num a => a -> a -> a
(*) :: Num a => a -> a -> a
negate :: Num a => a -> a -- 0-x
abs :: Num a => a -> a -- absolute value
signum :: Num a => a -> a -- -1 for negative values, 0 for 0, +1 for positive values
fromInteger :: Num a => Integer -> a

Integral 表示整数,包括 IntInteger,该类型属于 Num

div :: Integral a => a -> a -> a
mod :: Integral a => a -> a -> a

Fractional 是支持除法的类,属于 Num

(/) :: Fractional a => a -> a -> a

Floating 属于 Num,支持一些只有浮点数才支持的操作

sqrt :: Floating a => a -> a
sin :: Floating a => a -> a

ReadShow 类支持把字符串转为值和把值转字符串

show :: Show a => a -> String
read :: Read a => String -> a

Foldable 表示可以 foldr 的类

数据结构

Data.Map 类似于 [(k, v)],但支持更多操作

引入:

import qualified Data.Map as Map

支持的函数:

-- 从列表中创建键-值对
Map.fromList :: Ord k => [(k, a)] -> Map.Map k a

-- 向 map 中插入一对键值对,会覆盖原有的
-- 返回一个新的 map,不会修改原来的
Map.insert :: Ord k => k -> a -> Map.Map k a -> Map.Map k a

-- 使用一个键返回值,如果没有,返回 Nothing
Map.lookup :: Ord k => k -> Map.Map k a -> Maybe a

-- 空 map
Map.empty :: Map.Map k a

Data.Array 有点不同,是一个二元类

构造方法:

array :: Ix i => (i, i) -> [(i, e)] -> Array i e
listArray :: Ix i => (i, i) -> [e] -> Array i e -- 从列表创建,更简单

操作:

-- Array 查找
(!) :: Ix i => Array i e -> i -> e
-- Array 更新,注意一次可以更新多个
(//) :: Ix i => Array i e -> [(i, e)] -> Array i e

查找资料

打结需要绳子 You Need String for a Knot

代数数据类型

data Color = Red | Green | Blue

BoolOrderingColor 都是一种枚举 enum 类型

定义其他类型:

data Report = ConstructReport Int String String

ConstructReportRed 等是构造函数 Constructors,可以与类型名同名,可以有多个 constructors,通常和类型名一样首字母大写,构造函数后面跟的叫域 fields,``ConstructReport 有 3 个域,Red` 有 0 个域

自定义类型后添加 deriving Show 可以加到 Show 类中

data Card = Joker | Heart Int | Club Int | Spade Int | Diamond Int
deriving Show

为什么叫代数 Algebraic

每个数据类型是所有 constructors 的,每个 constructor 是域的

类型参数

data Maybe a = Nothing | Just a

可以递归定义:

data List a = Empty | Node a (List a)
deriving Show

记录语法

data Person = MkPerson String Int String String String deriving Show

太多混乱,若定义成

data Person = MkPerson { name :: String, age :: Int, town :: String, state :: String, profession :: String}
deriving Show

不光可读性大大增强,还可以快速取得某个参数:

GHCi> profession (MkPerson "Jane Doe" 21 "Houston" "Texas" "Engineer")
"Engineer"

工作机制

因为 Haskell 数据不可变,所以在内存中修改某个递归的数据,实际上新增了要改变的结点,共享不变的结点

工人阶级英雄 Working Class Hero

类和实例的语法

定义一个有方法 size 的类 Size 并完成 Int[Size] 的实例

class Size a where
empty :: a -- 可以包含常数
size :: a -> Int
sameSize :: a -> a -> Bool

instance Size (Maybe a) where
empty = Nothing

size Nothing = 0
size (Just a) = 1

sameSize x y = size x == size y

instance Size [a] where
empty = []
size xs = length xs
sameSize x y = size x == size y

默认实现

class Eq a where
(==) :: a -> a -> Bool
x == y = not (x /= y)

(/=) :: a -> a -> Bool
x /= y = not (x == y)

可以看到 == 的默认实现用到了 /=/= 的默认实现用到了 ==,这意味着我们在实例化时只需要实现其中一个即可,即最小完全定义 Minimal complete definition

Ord 类也是类似的

class  (Eq a) => Ord a  where
compare :: a -> a -> Ordering
(<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a

compare x y | x == y = EQ
| x <= y = LT
| otherwise = GT

x <= y = compare x y /= GT
x < y = compare x y == LT
x >= y = compare x y /= LT
x > y = compare x y == GT

max x y | x <= y = y
| otherwise = x
min x y | x <= y = x
| otherwise = y

层次结构

实例层次结构:对于 Pair,需要先保证 Eq a

instance Eq a => Eq (Pair a) where
(MakePair x y) == (MakePair a b) = x==a && y==b

类的层次结构:注意与面向对象编程中的类的继承区分

class Eq a => Ord a where
...
class Num a => Fractional a where
...

新星座 New Constellations

使用箱子建模

类似于面向对象编程中的封装 encapsulation,把一些类型背后的表示隐藏了,同时也增加了可读性

data Money = Money Int
deriving Show

renderMoney :: Money -> String
renderMoney (Money cents) = show (fromIntegral cents / 100)

(+!) :: Money -> Money -> Money
(Money a) +! (Money b) = Money (a+b)

scale :: Money -> Double -> Money
scale (Money a) x = Money (round (fromIntegral a * x))

addVat :: Money -> Money
addVat m = m +! scale m 0.24

使用情况建模

用可能的情况定义类型,如

data Person = Person {name :: String, age :: Int}
deriving Show

data SortOrder = Ascending | Descending -- 两种情况
data SortField = Name | Age

sortByField :: SortField -> [Person] -> [Person]
sortByField Name ps = sortBy (comparing name) ps
sortByField Age ps = sortBy (comparing age) ps

sortPersons :: SortField -> SortOrder -> [Person] -> [Person]
sortPersons field Ascending ps = sortByField field ps
sortPersons field Descending ps = reverse (sortByField field ps)

persons = [Person "Fridolf" 73, Person "Greta" 60, Person "Hans" 65]

调用时只需要使用 sortPersons Name Ascending persons,更加清晰

NonEmpty 类型能保证列表非空,可能的值有 1 :| [2,3,4]1 :| []

幺半群

若某个运算符满足结合律,则其是关联运算符 Associative Operations

类似的,可以推广到函数:若 f x (f y z)f (f x y) z 相同,则函数 f 是关联函数

常见的关联函数有 maxmin

Semigroup 类的类型有关联运算符,实例化如下

data Sum a = Sum a
instance Num a => Semigroup (Sum a) where
Sum a <> Sum b = Sum (a+b)

data Product a = Product a
instance Num a => Semigroup (Product a) where
Product a <> Product b = Product (a*b)

幺半群 monoid 是有中性元素的半群 semigroup,中性元素是“零”,即当其与另一个元素结合时,没有作用,实例化如下

instance Monoid (Sum a) where
mempty = Sum 0

instance Monoid (Product a) where
mempty = Product 1

instance Monoid [] where
mempty = []

好处:可以更方便地使用 foldr (<>) mempty 折叠多个元素

开放和封闭抽象

封闭抽象

data Vehicle = Car String | Airplane String

sound :: Vehicle -> String
sound (Car _) = "brum brum"
sound (Airplane _) = "zooooom"

基于数据,在一个地方解决所有情况

开放抽象

data Car = Car String
data Airplane = Airplane String

class VehicleClass a where
sound :: a -> String

instance VehicleClass Car where
sound (Car _) = "brum brum"

instance VehicleClass Airplane where
sound (Airplane _) = "zooooom"

基于类,可以在各种地方添加情况

两者各有优劣

根据语言建模

可以实现一个迷你的编程语言,嵌入式领域特定语言 Embedded Domain-Specific Language (EDSL)

data Discount = DiscountPercent Int         -- A percentage discount
| DiscountConstant Int -- A constant discount
| MinimumPrice Int -- Set a minimum price
| ForCustomer String Discount -- Discounts can be conditional
| Many [Discount] -- Apply a number of discounts in row
applyDiscount :: String -> Int -> Discount -> Int
applyDiscount _ price (DiscountPercent percent) = price - (price * percent) `div` 100
applyDiscount _ price (DiscountConstant discount) = price - discount
applyDiscount _ price (MinimumPrice minPrice) = max price minPrice
applyDiscount customer price (ForCustomer target discount)
| customer == target = applyDiscount customer price discount
| otherwise = price
applyDiscount customer price (Many discounts) = go price discounts
where go p [] = p
go p (d:ds) = go (applyDiscount customer p d) ds

如此定义后我们可以像操作数据库一样

applyDiscount "Bob" 120 (DiscountPercent 50)
==> 60
applyDiscount "Bob" 60 (DiscountConstant 30)
==> 30
applyDiscount "Bob" 30 (MinimumPrice 35)
==> 35
applyDiscount "Bob" 120 (Many [DiscountPercent 50, DiscountConstant 30, MinimumPrice 35])
==> 35

余味 The Aftertaste

理论上实现 IO 必须要有副作用,但可以通过 IO Monald 实现

GHCi> :t getLine
getLine :: IO String
GHCi> line <- getLine
another line
GHCi> :t line
line :: String
GHCi> line
"another line"
GHCi> reverse line
"enil rehtona"

getLineIO action,类型是 IO String

GHCi> :t putStrLn
putStrLn :: String -> IO ()
GHCi> :t putStrLn "hello"
putStrLn "hello" :: IO ()
GHCi> val <- putStrLn "hello"
hello
GHCi> val
()

输出字符串到 IO 中,() 类型表示只有一个值

可以用 do 块把多个 action 结合起来

printTwoThings :: IO ()
printTwoThings = do
putStrLn "Hello!"
putStrLn "How are you?"

greet :: IO ()
greet = do
putStrLn "What's your name?"
name <- getLine
putStrLn ("Hello, " ++ name)

GHCi> printTwoThings
Hello!
How are you?
GHCi> greet
What's your name?
Seraphim
Hello, Seraphim