Haskell 简易指南

安装

用任何你喜欢的方法安装 Glasgow Haskell Compiler (a.k.a. GHC)。Cabal 之类的
依赖管理系统就用不着了。 因为我也不会用。 保证能够执行ghcghci命令就行。

GHCi基础

首先,把以下文件保存成helloworld.hs

helloworld.hs
1
foo = "hello, world"

然后执行ghci helloworld.hs,然后在>提示符后输入foo并回车

1
2
3
4
5
6
GHCi, version 8.6.3: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main ( helloworld.hs, interpreted )
Ok, one module loaded.
*Main> foo
"hello, world"
*Main>

你可以使用:r来重新载入文件,也可以使用:l <文件名>来载入代码。

基础表达式

你可以修改你的代码文件,并在GHCi中观察程序行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
-- 这里是注释

foo = "hello, world" -- 名称绑定

-- 定义一个函数,返回参数加一
plus1 x=x+1
-- 函数调用的格式为 <函数名> <参数1> <参数2> ……
-- *Main> plus1 41
-- 42

-- 递归调用,定义的顺序很重要,在前面的定义优先考虑
fact 0 = 1
fact n = n * fact (n-1)

-- 列表 (a.k.a. 数组)
list1 = ["I", "am", "a", "list"]

-- 元组
tuple1 = (1, 2)
triple1 = (1, 2, 3)

-- 元组的模式匹配
sumOfTuple (x1, x2) = x1+x2
-- *Main> sumOfTuple tuple1
-- 3

-- 列表与列表合并
list2 = [1,2,3]
list3 = [4,5,6]
list4 = list2 ++ list3
-- *Main> list4
-- [1,2,3,4,5,6]

-- 字符串是字符组成的列表
str1 = "foo" ++ ['b','a','r']
-- *Main> str1
-- "foobar"

-- 列表头部插入, 以下四种表达方式等价
list5 = [1,2,3]
list6 = 1:[2,3]
list7 = 1:2:[3]
list8 = 1:2:3:[]

-- 列表的模式匹配
sumOfList [] = 0
sumOfList (headElement:remainingElements) = headElement + sumOfList remainingElements

-- 强制前缀表达式
three = (+) 1 2

-- 强制中缀表达式,mod为取模函数
four = 18 `mod` 7

-- 函数部分求值 (Partial application),以下两个函数等价
-- 单引号没有特殊意义,是合法函数名的一部分
plus2 x = (+) 2 x
plus2' = (+) 2

-- if 条件 (类似三目运算符)
five = if False then 4 else 5

-- let 名称绑定
six = let x = 7 in x-1

-- Guard
sign x
| x < 0 = -1
| x == 0 = 0
| otherwise = 1

-- 函数复合
plus4 x = plus2 (plus2 x)
plus4' x = (plus2 . plus2) x
plus4'' = plus2 . plus2

-- Lambda表达式 (a.k.a. 匿名函数)
sum' = (\x y -> x+y)
plus5 = (\x -> sum' x 5)

这里仅列出了极少数基本用法。更多关于语言本身的以及数据结构的特定语法规则请参考相关Haskell教程。

类型标记

Haskell 是一门具有类型推导的静态类型语言。每个表达式都有自己的类型,在 GHCi 的交互模式下,可以使用:t <表达式>来检查表达式的类型。类型注记通常写为<表达式> :: <类型>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 布尔型
*Main> :t True
True::Bool

-- 元组
*Main> :t (True, False)
(True, False) :: (Bool, Bool)

-- 列表
*Main> :t "123"
"123" :: [Char]

-- 函数
*Main> :t (+)
(+) :: Num a => a -> a -> a

-- 部分求值后,新的函数只需要一个参数
*Main> :t (+) 1
(+) 1 :: Num a => a -> a

函数类型以 (参数1的类型) -> (参数2的类型) -> ... -> (返回值的类型) 形式表达。不明显区分参数和返回值。Num是代表数字的类型类,可以近似理解成Java中的接口。Num a => 表示 “在后续的类型定义中,a可以被替换成任何满足Num的类型”。整数Int和浮点数Float都是Num类型类的成员,所以加法函数既可以将整数相加,也可以将浮点数相加。

类型箭头都是右结合,但是你可以手动添加括号来改变类型的意义

1
2
3
4
5
6
7
8
9
10
11
12
-- 接受一个整型参数,返回一个新函数。这个新函数接受一个整型,返回一个整型
product' :: Int -> (Int -> Int)
product' x y = x * y
timesThree = product' 3
nine = timesThree 3
nine' = (product' 3) 3

-- 接受一个参数,该参数是“接受一个整型,返回一个整型”的函数,然后返回一个整型
some_func :: (Int -> Int) -> Int
some_func f = f 42
-- *Main> some_func (* 2)
-- 84

数据结构

Haskell 中定义新类型的基本语法是data <新类型名> [类型参数..] = <构造函数1> [成员类型...] | <构造函数2> [成员类型...] | ...。类型名和构造函数都需要首字母大写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
data NameAndAge = MakeNameAndAge String Int
-- *Main> :t MakeNameAndAge
-- MakeNameAndAge :: String -> Int -> NameAndAge

data IntOrBool = MakeInt Int | MakeBool Bool
-- *Main> :t MakeInt
-- MakeInt :: Int -> IntOrBool
-- *Main> :t MakeBool
-- MakeBool :: Bool -> IntOrBool

data Weekends = Saturday | Sunday
-- Saturday :: Weekends
-- Sunday :: Weekends

data TupleOf a = MakeTupleOf a a
-- MakeTupleOf :: a -> a -> TupleOf a
-- MakeTupleOf False True :: TupleOf Bool
-- MakeTupleOf 1 2 :: Num a => TupleOf a
-- MakeTupleOf plus1 plus2 :: Num a => TupleOf (a -> a)

-- 你可以使用函数的模式匹配来提取数据结构中的成员
printNameAndAge :: NameAndAge -> String
printNameAndAge (MakeNameAndAge name age) =
"I'm " ++ name ++ " and I'm " ++ (show age) ++ " years old."

-- 你也可以使用模式匹配来判断是哪一个构造函数
printIntOrBool :: IntOrBool -> String
printIntOrBool (MakeInt n) = "Wow, an integer: " ++ (show n)
printIntOrBool (MakeBool b) = "Wow, a boolean: " ++ (show b)

-- 你甚至可以进行递归类型定义 。当然,你需要一个终止条件。
data ListOf a = EmptyList | AppendList (ListOf a) a

-- 相关操作也需要使用递归函数来完成
contains :: (Eq a) => ListOf a -> a -> Bool
contains EmptyList _ = False
contains (AppendList list x') x =
if x == x' then True else contains list x

-- 为 ListOf a 类型实现 Eq 类型类
instance Eq a => Eq (ListOf a) where
EmptyList == EmptyList = True
(AppendList l1 a1) == (AppendList l2 a2) = a1 == a2 && l1 == l2
_ == _ = False

注意:a是一个类型,ListOf a是一个类型,但是 ListOf 不是类型,ListOf 不是类型,ListOf 不是类型。 这个定义表示:ListOf a满足Eq 仅当 a满足Eq。要查看类型的相关信息,可以在 GHCi 中执行:info指令。

1
2
3
4
5
6
7
8
*Main> :info Eq
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
{-# MINIMAL (==) | (/=) #-}
-- Defined in ‘GHC.Classes’
instance [safe] Eq a => Eq (ListOf a) -- Defined at [omitted]
[... omitted ...]

也许是个单子

Maybe a类似Java中的Optional<T>,常用于表示“会失败”的函数。

1
2
3
4
5
data Maybe a = Nothing | Just a

-- 一些函数
func1 :: Float -> Maybe Int
func2 :: Int -> Maybe Bool

Monad是个类型类

1
2
3
4
5
6
*Main> :info Monad
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a

简化一下

1
2
3
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a

当我们说Maybe是一个Monad的时候,一方面指 Maybe 属于 Monad 这个类型类instance Monad Maybe where ...。另一方面指Maybe(数据结构)(>>=)::Maybe a -> (a -> Maybe b) -> Maybe b (函数)return::a -> Maybe a (函数)这三者构成了一个满足某些条件的数学结构,这些条件被称为Monad Laws。事实上,Haskell编译器不会检查 Monad Laws 是否满足,你可以胡乱写一些数据结构和函数,然后将其塞入 Monad 这个类型类中。换句话说,Haskell中的Monad就是一个接口,任何实现接口的数据类型都可以称其为Monad。

回到Maybe上,现在你想把这两个会失败的函数连接在一起

1
2
3
4
5
6
7
func3 :: Float -> Maybe Bool
-- 错误示范,类型不匹配
-- func3 = func2.func1
-- 正确示范
func3 n = case (func1 n) of
Nothing -> Nothing
Just n' -> func2 n'

看上去不错,我们需要一种操作,能把任意两个可失败的函数连在一起,这样以后再碰到这种情况直接复用就行了。如果第一个函数类型是a->Maybe b,第二个函数类型是b->Maybe c,那么复合函数的类型应该是a->Maybe c

1
2
3
4
5
6
7
composite :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
composite f g = \x ->
case g x of
Nothing -> Nothing
Just y -> f y

func3' = composite func2 func1

看上去不错,不过有个小问题,执行的第一步g x并不需要composite函数来操心,完全可以由调用者算好了传进来,于是我们再简化下

1
2
3
4
5
6
7
composite :: (b -> Maybe c) -> (Maybe b) -> (Maybe c)
composite f gx =
case gx of
Nothing -> Nothing
Just y -> f y

func3'' x = composite func2 (func1 x)

只要交换一下两个参数的顺序,我们就有了Monad Maybe>>=函数。对于Maybe来说,它恰好有一个操作能满足Monad的定义,于是Maybe就是一个Monad。

做得越多,写得越少

do是Haskell中的一个关于>>=的语法糖,考虑有多个“可失败”函数需要调用的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
f1 x = Just (x+1)
f2 x = Just (x*2)
f3 x = Just (x-3)

-- 使用 >>= 函数
just16 = (Just 5)
>>= (\x -> f1 (x+1))
>>= (\y -> f2 (y+2))
>>= (\z -> f3 (z+1))

-- 使用 do 语法
just16' = do
x <- Just 5
y <- f1 (x+1)
z <- f2 (y+2)
f3 (z+1)

do会按照规则展开成>>=函数

1
2
3
4
do x <- expr
more_exprs
会被展开成
expr >>= (\x -> more_exprs)

因此两者是等价的。但是do语法更加整齐易于阅读。

扩展阅读