A Quick Tour of Haskell Syntax

原文:https://prajitr.github.io/quick-haskell-syntax/

如果你想解析 Haskell 来完成一篇博文,或者想快速了解 Haskell 的大概,那本文就是你要找的

比起“在X分钟学习Y”类文章,本文可能稍长一点,而且会深入某些内容。

话不多说,我们开始吧

数据类型

我们从基本数据类型开始:

1
2
3
4
5
6
7
8
9
10
-- inline comment
{- block comment -}
7 -- numbers
3.0
True -- booleans
False
'p' -- characters
'j'
"The slow brown fox." -- strings
[1, 2, 3] -- lists

上面的 String 其实是字符的列表,因此,”Hello” 与 [‘H’,’e’,’l’,’l’,’o’] 是相同的

函数

接着,是基本函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
7 + 12 -- 19
3 * 11 -- 33
6 - 2 -- 4
11 / 8 -- 1.375

-- Integer division
11 `div` 8 -- 1

not True -- False
10 == 9 -- False
10 /= 9 -- True
True && False -- False
False || True -- True
4 > 2 -- True
18 <= 13 -- False

not 是一个函数,它接受一个参数并取反

Haskell 中的函数一般不需要小括号调用,就像这样: func arg1 arg2 arg3 ...,不过,有些情况小括号是必须的,如 not (3 < 5) 计算出 False,而 not 3 < 5 则是错误的

11 `div` 8 等价于 div 11 8,将函数名放在一对反引号内允许你以中缀风格调用。考虑到有些函数采用中缀调用可读性更好,这只是由此产生的一种语法糖

以下是用于列表的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1 : [2, 3] -- [1, 2, 3]
'C' : "at" -- "Cat" (Remember strings are lists of characters)
True : [] -- [True]
1 : 2 : 3 : [] == [1, 2, 3] -- True

elem 3 [1, 4, 73, 12] -- False
'H' `elem` "Highgarden" -- True
[1, 2] ++ [3, 4] -- [1, 2, 3, 4]
"Hello " ++ "World" -- "Hello World"
head [1, 2, 3] -- 1
tail [1, 2, 3] -- [2, 3]
last [1, 2, 3] -- 3
init [1, 2, 3] -- [1, 2],返回除最后一个元素
"Casterly Rock" !! 3 -- 't'

null [] -- True
null [1, 2, 3] -- False
length "Dorne" -- 5

(1, 2)
("Hello", True, 2)
fst (6, "Six") -- 6
snd (6, "Six") -- "Six"

1 : 2 : 3 : [] 和 [1, 2, 3] 是一样的,与 lisp 一样,列表就是元素连接到一起,最终连接到一个空列表([])

(elem1, elem2, elem3, …) 称为元组,它可以保存不同类型的元素,而列表只能保存同一类型元素

定义函数

来看如何定义函数:

1
2
id :: a -> a
id x = x

该函数接受一个参数,并直接返回

第一行是类型声明,从左到右依次是函数名(id),两个冒号,最后是类型签名。

这里类型签名的含义是:接受一个 a 类型(a 指代任意类型)参数,返回一个 a 类型的值

第二行是 id 的计算过程,它取出参数然后不加修改地返回它

为了更好地理解,再看一个例子:

1
2
const :: a -> b -> a
const x y = x

该函数接收两个参数,返回第一个参数,忽略第二个参数

类型声明的含义是:取得任意类型 a 的值,取出任意类型 b 的值(实际类型可以与 a 相同或不相同),最后返回一个 a 类型的值

最后一个 -> 后面的类型为返回值类型,前面的类型都是参数类型,均以 -> 分隔

以下是一个稍复杂的例子:

1
2
3
4
5
concatenate3 :: String -> String -> String -> String
concatenate3 x y z = x ++ y ++ z

allEqual :: (Eq a) => a -> a -> a
allEqual x y z = x == y && y == z

concatenate3 接收三个 String 参数,返回一个 String,这里 String 对比前一个例子的泛型参数 a b,为具体类型

可以在类型签名中混合具体类型和泛型类型

在 allEqual 的类型签名中,我们看到 (Eq a),它代表限制 a 的范围为能够比较相等与否的任意类型。a 可以被加以多个限制:(Eq a, Ord a, Num b, …) =>

匿名函数

使用 Haskell 创建匿名函数很容易:

1
2
3
(\x -> x + x)
(\x y -> x + y)
(\x -> 10 + x) 5 -- 15

匿名函数用小括号括起来,在小括号内以 \ 开始,接着是空格隔开的参数列表,接着是 -> 符号,最后是计算过程

函数是一等公民

在 Haskell 中,函数是一等公民,可以当做参数传递,从函数返回,从变量赋值,被数据结构(如列表)拥有

1
2
3
4
5
6
7
applyAndConcat :: (String -> String) -> (String -> String) -> String -> String
applyAndConcat f g x = (f x) ++ (g x)

applyAndConcat tail init "Rickon" -- "ickonRicko"
-- (tail "Rickon") ++ (init "Rickon")
-- "ickon" ++ "Ricko"
-- "ickonRicko"

applyAndConcat 接收两个函数,和一个 String,将两个函数分别应用至 String 后连接两个结果并返回

函数复合

1
2
3
4
(.) :: (b -> c) -> (a -> b) -> (a -> c)
(.) f g = (\x -> f (g x))
-- alternatively
f . g = (\x -> f (g x))

. 函数称为函数复合,类似数学中的 fog 记号。它接收两个函数 f, g,返回一个函数,该函数将参数传给 g(即调用 g),然后将结果传给 f(即调用 f)。

$函数

1
2
($) :: (a -> b) -> a -> b
f $ x = f x

$ 符号经常出现在 Haskell 中,乍一看,$ 好像没有任何作用

大多时候,$ 并不用于计算,而是为了减少括号的数量,例如:not (3 < 5) 可以写作 not $ 3 < 5,它仅仅实现一种风格,并且,它不是什么特殊的操作符,而是一个普普通通的函数

部分应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add x y = x + y

addToThree = add 3
addToThree 6 -- 9
-- add 3 6

multiplyThreeNums x y z = x * y * z

times32 = multiplyThreeNums 8 4
times32 10 -- 320
-- multiplyThreeNums 8 4 10

twoNumsTimes10 = multiplyThreeNums 10
twoNumsTimes10 6 2 -- 120
-- multiplyThreeNums 10 6 2

在 Haskell 中,你可以部分应用一个函数,也就是说,对于接收 n 个参数的函数,你可以部分应用 k (k < n)个参数,最终产生一个接收 n-k 个参数的函数

可以看到,类型签名是可以省略的,如果省略了类型参数,编译器会自动推出类型,带来灵活性

局部变量

1
2
3
4
5
6
7
8
9
10
addFour w x y z = 
let a = w + x
b = y + a
in z + b

addFour w x y z =
z + b
where
a = w + x
b = y + a

let … in … 和 … where … 可以让你构建计算并把结果保存至局部变量,这两种形式功能完全一致,如何选择只看个人喜好

模式匹配

以下是斐波那契数列典型递归实现:

1
2
3
fib 0 = 1
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

其中,fib 0 = 1, fib 1 = 1 属于模式匹配,Haskell 会从上往下遍历列表试图找到一个匹配的定义。模式匹配在 Haskell 中非常重要,你将经常看到它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fib n
| n < 2 = 1
| otherwise = fib (n - 1) + fib (n - 2)

fib n =
case n of
0 -> 1
1 -> 1
_ -> fib (n - 1) + fib (n - 2)

fib n =
if n < 2
then 1
else fib (n - 1) + fib (n - 2)

以上仅是使用不同语法实现的 fact 函数变体

第一个称作 guard expressions,用 | 列出 n 的不同情况及对应值。otherwise 能匹配所有情况(事实上它和 True 一致)

case … of … 也是模式匹配,不同的是,你还可以在计算过程中使用它

if … then … else … 就是常规的 if 语句了,Haskell 中,每个 if 都需要对应的 else 部分

Haskell必备函数

我们来定义一些必备的 Haskell 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = (f x) : (map f xs)

addToThree x = x + 3

map addToThree [1, 8, 17] -- [4, 11, 20]
-- (addToThree 1) : (map addToThree [4, 5])
-- 4 : (addToThree 8) : (map addToThree [5])
-- 4 : 11 : (addToThree 5) : (map addToThree [])
-- 4 : 11 : 20 : []
-- 4 : 11 : [20]
-- 4 : [11, 20]
-- [4, 11, 20]

map 接收一个函数和一个列表,并将函数应用至列表每个元素

第二行中的 _ 代表我们忽略该参数,它不是必要的,但可以让代码的意图更明显

(x:xs) 是将列表第一个元素(x)和其余元素(xs)分开来的模式匹配记号

1
2
3
4
5
6
7
8
9
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter valid (x:xs)
| valid x = x : filter valid xs
| otherwise = filter valid xs

gtThree x = x > 3

filter gtThree [10, 2, 5] -- [10, 5]

filter 与 map 类似,但它接受一个返回 Bool 的函数,用于保留列表中满足条件的元素

还有更多列表相关函数,例如 foldr,对它的了解及其实现就留作读者做练习吧

数据类型

让我们转到类型话题,有三种创建类型的方式:

类型同义词

1
2
3
4
5
6
7
8
9
10
type Email = String
type Name = String
type BigFunction = (c -> d) -> (b -> c) -> (a -> b) -> (a -> d)

validForms :: Name -> Email -> Bool
-- validForms :: String -> String -> Bool
crazyFunction :: BigFunction -> BigFunction -> Bool
-- crazyFunction :: ((c -> d) -> (b -> c) -> (a -> b) -> (a -> d)) ->
-- ((c -> d) -> (b -> c) -> (a -> b) -> (a -> d)) ->
-- Bool

第一种称为“类型同义词”,type 的后面是类型名(必须大写字母开头),然后是一个 = 号,最后是一个已定义类型

类型同义词其中一个意义是为类型签名提供文档,其二是为复杂类型提供一个简单的别名,以减少输入字符数

类型同义词与它所代表的类型可以交替使用,如果你认为它没用,只需记住 type String = [Char]

代数数据类型

1
2
3
4
5
6
7
8
9
10
data Bool = True | False
data Maybe a = Nothing | Just a

not :: Bool -> Bool
not True = False
not False = True

fromMaybe :: a -> Maybe a -> a
fromMaybe x Nothing = x
fromMaybe _ (Just y) = y

第二种是代数数据类型,让我们能够创建全新的类型

所有 data 和 = 之间的东西都是类型签名的一部分, = 后面的部分为值构造器(value constructors),会成为函数计算的一部分

| 的作用与“或”差不多:一个布尔值可以为 True 或 False。Bool 永远是类型签名的一部分,计算过程中,你会使用的是 True,False

在看Maybe String, Maybe Int,Maybe a,它们都是有效的,如果只有 Maybe 则是无效的,data May a = … 中的 a 被称为类型参数,类型参数让类型成为多态的(即:同时支持Maybe String,Maybe Int)。任何值构造器名(Just a 中的 Just)后面的值都是字段。每个值构造器所拥有的字段数不需要相等,可以看到,Nothing 没有任何字段,而 Just 有一个类型为 a 的字段

1
2
3
4
5
6
7
8
9
10
11
data Tree a = Empty | Node (Tree a) a (Tree a)

treeMap :: (a -> b) -> Tree a -> Tree b
treeMap _ Empty = Empty
treeMap f (Node l v r) = Node (treeMap f l) (f v) (treeMap f r)

data Tree a = Empty | Node { left :: Tree a, value :: a, right :: Tree a}

-- left :: Tree a -> Tree a
-- value :: Tree a -> a
-- right :: Tree a -> Tree a

代数数据类型还可以自引用。Node 有三个字段,其中两个都是 Tree a, Tree 会由 Node 构造起来,而 Empty 会是每个分叉的末端。

第二个 Tree 的定义使用了记录语法,记录语法可以取代模式匹配去访问 Node 的字段:传递 Node 调用自动生成的 left 函数即可访问左子树, 记录语法让我们更便捷地操作值构造器

newtype

第三种是 newtype

1
newtype State s a = State { runState :: s -> (s, a) }

newtype 可以产生其他类型的轻度包装,它只能有一个包含一个字段的值构造器。在处理 typeclass 时,newtype将非常有用。

接下来就介绍 typeclass。

typeclass

typeclass 类似 interface,它定义类型必须遵守的行为的集合。例如:如果一个类型是 Eq 这个 typeclass 实例,那就必须实现 == 或 /=,用以比较相等性

1
2
3
4
data Rectangle = Rect Int Int

instance Eq Rectangle where
(Rect w1 h1) == (Rect w2 h2) = (w1 == w2) && (h1 == h2)

以上定义了一个类型 Rect,然后通过比较两个 Rect 的字段让它成为 Eq 的实例

如同前面见过的,我们可以在类型签名中使用 typeclass 以声明类型必须拥有的行为

结束

好了,以上就是本教程所有内容。该教程节奏可能偏快,如果你有不懂的地方,可能还需要自己动手搜索一下。一些好的资源包括:Learn You a Haskell, Real World Haskell, 和 Hoogle,Hoogle允许你通过函数名(如:map)或者它的类型签名搜索((a -> b) -> [a] -> [b])。希望在它们的帮助下,你能够基本理解并阅读一些 Haskell 代码。