Haskell 学习笔记

开启传送门

开始

预备,准备,开始!

好,让我们开始!如果你因为某些奇奇怪怪的原因没有阅读介绍,还是建议你阅读以下介绍中的最后一节,因为它介绍了为什么你要阅读这个教程,以及为什么我们要写这个教程。首先我们要做的就是云心$ghc$的互动窗口,运行一些函数,以便对$haskell$有一个比较基础的认识。打开你的终端,然后输入 ghci 并回车,你会看到类似于下面的东西:

1
2
3
GHCi, version 6.8.2: http://www.haskell.org/ghc/  :? for help  
Loading package base ... linking ... done.
Prelude>

恭喜你,进入到了GHCI!这里的引导是 Prelude> ,但是当你输入一些东西的时候它会显得很长,所以我们打算用 ghci> 。如果你也有同样的诉求,可以尝试输入 set prompt “ghci> “

这里有一些简单的数学算式的例子:

1
2
3
4
5
6
7
8
9
ghci> 2 + 15  
17
ghci> 49 * 100
4900
ghci> 1892 - 1472
420
ghci> 5 / 2
2.5
ghci>

这是不言自明的。我们同样可以在一行中进行多个操作,并且这些运算遵循现实中的运算优先级。可以用括号来明确运算优先级或者改变运算优先级。例如:

1
2
3
4
5
6
ghci> (50 * 100) - 4999  
1
ghci> 50 * 100 - 4999
1
ghci> 50 * (100 - 4999)
-244950

相当酷,不是吗?是的,我知道这很一般,但是还是容忍我啰嗦两句。要小心负数。如果我们想使用一个负数,最好给他加上括号。例如,如果在$GHCI$中输入表达式 $5 * -3$ 将会得到报错,但是输入 $5 * (-3)$ 将运行的很好。

bool代数同样是很直接明了的。正如你以前知道的一样, && 表示bool代数 || 表示bool代数 , not 会翻转bool表达式。例如:

1
2
3
4
5
6
7
8
9
10
ghci> True && False  
False
ghci> True && True
True
ghci> False || True
True
ghci> not False
True
ghci> not (True && True)
False

对“等于”的判断测试如下

1
2
3
4
5
6
7
8
9
10
ghci> 5 == 5
True
ghci> 1 == 0
False
ghci> 5 /= 5
False
ghci> 5 /= 4
True
ghci> "hello" == "hello"
True

如果我们输入 5 + “llama” 或者 5 == True 会发生什么呢?如果我们尝试了第一个式子,我们将会得到一个大大的错误!

1
2
3
4
5
No instance for (Num [Char])  
arising from a use of `+' at <interactive>:1:0-9
Possible fix: add an instance declaration for (Num [Char])
In the expression: 5 + "llama"
In the definition of `it': it = 5 + "llama"

$GHCI$告诉我们, “llama” 不是一个数字,所以它不知道如何把这两个东西加起来。或者说,就算把 “llama” 换成 “four” 或者 “4” ,$haskell$同样不会认为这是一个数字。 + 运算符期望它的左边和右边都是数字。同理,如果我们尝试输入 True==5 ,$GHCI$会告诉我们他们两个的类型不匹配。+ 作用于两个数字, == 作用于两个同类型可比较的事物。你不能把苹果和橘子进行比较。我们将会在后文中更直观的感受类型的区别。注意:你可以写出 5 + 4.0 这样的式子,这是因为 5 很特别,它可以是一个整数也可以是一个浮点数。 4.0不能表示为整数,所以 5 就被解释为浮点数。

你可能没有注意到,但是我们确实是一直在使用函数。更确切的说,* 是一个将两个数乘起来的函数。正如你所见到的那样,我们把它放在两个数中间以使用它。我们将这种调用方式称为 “中缀函数”。大多数函数都是 “前缀函数”。让我们看看。


函数通常是前缀的,所以从现在开始我们不会明确的说明一个函数式前缀形式,我们假定这是对的。在大多数命令式语言中,通过编写函数名称然后将参数写在括号中来调用函数,参数表通常用逗号分隔。在 $Haskell$ 中,函数的调用方式是直接写函数名称,一个空格然后是参数表,参数之间用空格分隔。为了有个直观认识,我们尝试去调用 $Haskell$ 中一个贼无聊的函数。

1
2
ghci> succ 8  
9

$succ$函数介绍任何定义过的参数,并且返回它的老大。正如你看到的,我们直接把函数名和参数用一个空格分开。调用一个多参数的函数同样很简单。例如,函数 minmax 接受两个可以排序的东西(例如数字!)。 min 函数返回二者中的较小的, max返回二者较大的。你可以自己看看:

1
2
3
4
5
6
ghci> min 9 10  
9
ghci> min 3.4 3.2
3.2
ghci> max 100 101
101

函数的调用(通过加一个空格和添加参数来调用函数)有最高优先级。也就是说下面两种表达方式是等价的:

1
2
3
4
ghci> succ 9 + max 5 4 + 1  
16
ghci> (succ 9) + (max 5 4) + 1
16

但是,如果我们想得到 $9$ 和 $10$ 乘积的老大,我们不能写成 succ 9 * 10,这是因为这样写,会得到 $9$ 的老大,然后乘以 $10$,也就是说我们会得到 $100$。我们必须写成 succ (9 * 10) 才能得到 $91$。

如果一个函数接受两个参数,我们同样用中缀方式调用它。例如, div 函数接受两个参数,然后把两个参数相除。调用 div 92 10 将会得到 $9$。但是我们这样调用的时候总给人一种奇怪的感觉——不能很清晰的知道哪个是被除数,哪个是除数。所以我们可以用一种中缀的方式调用它—— 92 `div` 10 ,这样就清晰多了。

很多来自命令式语言的人更倾向于用括号表示功能应用。例如,在 $C$ 中,你能用括号来调用像 foo(),bar(1),baz(3, \”haha\”) 这样的函数。就像我们之前提到的,在$Haskell$中用空格来区分参数以调用函数。所以上述函数在$Haskell$中可能写成 foo,bar 1,baz 3 \”haha\”。所以当你看到像 bar (bar 3) 这样的调用,它不意味着 barbar3 为参数。它意味着我们先以$3$为参数调用了 bar 并以返回结果再次调用了 bar 。在 $C$ 中,你可能写成 bar(bar(3))这样。

天才第一步

在前一节中,我们对函数调用有了一个基本的认识。现在让我们尝试写自己的函数!打开你最喜爱的编辑器,写下这个接受一个参数并返回它的两倍的函数。

1
doubleMe x = x + x

函数的定义和他们被调用的方式大致相同。函数名后面接参数表,并用空格分隔。但是我们在定义函数的时候,这里有一个 = ,并且函数的功能会写在后面。保存这个并命名为 baby.hs 或者其他。然后跳到这个文件所在的目录,然后在该目录下运行 ghci 。进入之后运行 :l baby 。现在我们的文件就加载进去了,然后我们可以调用我们之前定义的函数。

1
2
3
4
5
6
7
ghci> :l baby  
[1 of 1] Compiling Main ( baby.hs, interpreted )
Ok, modules loaded: Main.
ghci> doubleMe 9
18
ghci> doubleMe 8.3
16.6

因为 + 无论是作用于整数还是浮点数(理论上任何是数字的东西都可以)都可以得到正确的结果,所以我们的函数可以正常工作于任何数字。让我们写一个函数,它有两个参数,并返回他们的两倍的和。

1
doubleUs x y = x*2 + y*2

简单。我们同样可以定义成 doubleUs x y = x + x + y + y 。测试一下我们的函数是否能返回正确的结果(记得把这个函数加到 baby.hs 中,保存,然后在 $GHCI$ 中运行 :l baby )。

1
2
3
4
5
6
ghci> doubleUs 4 9  
26
ghci> doubleUs 2.3 34.2
73.0
ghci> doubleUs 28 88 + doubleMe 123
478

正如我们期待的一样,你能调用你自己之前的函数。知道这个之后,我们可以重新定义 doubleUs 像下面这样:

1
doubleUs x y = doubleMe x + doubleMe y

这是一个特别简单的例子,这种模式会贯穿整个 $Haskell$.写一些基础的,一定正确的函数,然后组合他们来得到更加复杂的函数。利用这种方式你可以很轻松的避开反复的造轮子。如果某一天数学家们突然指出 $2$ 实际上是 $3$ ,你就只能去改你的代码吗?或许你可以直接重新定义 doubleMex + x + x,这样当 doubleUs 调用 doubleMe 的时候,它自动变成了适应这个把 $2$ 看成 $3$ 的奇怪世界。

$Haskell$中的函数不用按照顺序定义,也就是说如果你先定义了 doubleMe 然后定义了 doubleUs 或者其他顺序,这都没有关系。

现在我们准备写一个函数,它接受一个参数,如果这个参数小于或等于 $100$,会返回这个数的两倍,否则就会返回这个数本身,因为大于 $100$ 的书本身就够大了。

1
2
3
doubleSmallNumber x = if x > 100  
then x
else x*2

上面我们介绍了 $Haskell$ 中的 if 语句。你或许已经习惯了其他语言中的 if 语句。和其他语言不同的是, $Haskell$ 中必须要有 $else$ 。其他语言中你可以不使用 $else$ 当 $if$ 条件不成立的时候,但是在 $Haskell$ 中,任何表达式和函数必须有返回值。我们可以把 $if$ 语句写在一行里面,并且这样可读性更强。还有就是 $Haskell$ 中的 $if$ 语句是一个 表达式 。一个表达式是一段基础的代码,它返回一个值。 $5$ 是一个表达式,因为它返回 $5$ 。 $4 + 8$ 是一个表达式,$x + y$是一个表达式,因为它返回 $x$ 和 $y$ 的和。因为 $else$ 语句是必须的,所以 $if$ 语句将总是返回一些东西,这也就是我们为什么说它是一个表达式的原因。如果我们想把之前定义的函数加一,我们可以写成下面这样

1
doubleSmallNumber' x = (if x > 100 then x else x*2) + 1

如果我们省略了括号,他仅仅在 $x$ 不大于 $100$ 时才会将结果加一。注意到在函数名末尾的 。这个符号在 $Haskell$ 中并没有什么特殊含义。他是函数名称中的一种合法字符。我们通常用它来表示函数的严格版本(不是懒惰版本)。或者函数或者变量的轻微修改版本。因为这个符号是合法的,我们可以创建下面这样的函数。

1
conanO'Brien = "It's a-me, Conan O'Brien!"

这里有两件值得注意的事。第一就是,我们没有利用 $Conan$ 来命名函数。这是因为函数不能用小写字母开头。我们将在后面明白为什么是这样。第二就是这个函数没有任何参数。当一个函数没有参数的时候,我们通常称为这是一个 声明 (或者 命名)。因为一旦我们定义了之后,我们不能修改名称和函数,所以 conanO’Brien 和字符串 “It’s a-me, Conan O’Brien!” 可以互换使用。

对list的介绍

在 $Haskell$ 中列表很有用,就像现实世界中的购物清单一样。它是最常使用的数据结构并且能被多种不同的方式建模,并解决一系列问题。列表是如此的优雅。在这一节我们将学习list的基础应用,字符串(实际上也是列表)和列表的推导。

在 $Haskell$ 中,list中的元素必须是同一种元素。它存放多个同种类型的元素。这意味着我们可以存放一堆整数或者是一堆字符。但是我们不能定义一个存放一部分整数和一部分字符的list。

提示 : 我们在 GHCI 中能用 let 关键字来命名,在 GHCI中执行 let a = 1 等价于在一个文件里面写 a = 1 然后加载它。

1
2
3
ghci> let lostNumbers = [4,8,15,16,23,42]  
ghci> lostNumbers
[4,8,15,16,23,42]

正如你见到的这样,

`