开始
预备,准备,开始!
好,让我们开始!如果你因为某些奇奇怪怪的原因没有阅读介绍,还是建议你阅读以下介绍中的最后一节,因为它介绍了为什么你要阅读这个教程,以及为什么我们要写这个教程。首先我们要做的就是云心$ghc$的互动窗口,运行一些函数,以便对$haskell$有一个比较基础的认识。打开你的终端,然后输入 ghci 并回车,你会看到类似于下面的东西:
1 | GHCi, version 6.8.2: http://www.haskell.org/ghc/ :? for help |
恭喜你,进入到了GHCI!这里的引导是 Prelude> ,但是当你输入一些东西的时候它会显得很长,所以我们打算用 ghci> 。如果你也有同样的诉求,可以尝试输入 set prompt “ghci> “
这里有一些简单的数学算式的例子:
1 | ghci> 2 + 15 |
这是不言自明的。我们同样可以在一行中进行多个操作,并且这些运算遵循现实中的运算优先级。可以用括号来明确运算优先级或者改变运算优先级。例如:
1 | ghci> (50 * 100) - 4999 |
相当酷,不是吗?是的,我知道这很一般,但是还是容忍我啰嗦两句。要小心负数。如果我们想使用一个负数,最好给他加上括号。例如,如果在$GHCI$中输入表达式 $5 * -3$ 将会得到报错,但是输入 $5 * (-3)$ 将运行的很好。
bool代数同样是很直接明了的。正如你以前知道的一样, && 表示bool代数 与, || 表示bool代数 或, not 会翻转bool表达式。例如:
1 | ghci> True && False |
对“等于”的判断测试如下
1 | ghci> 5 == 5 |
如果我们输入 5 + “llama” 或者 5 == True 会发生什么呢?如果我们尝试了第一个式子,我们将会得到一个大大的错误!
1 | No instance for (Num [Char]) |
$GHCI$告诉我们, “llama” 不是一个数字,所以它不知道如何把这两个东西加起来。或者说,就算把 “llama” 换成 “four” 或者 “4” ,$haskell$同样不会认为这是一个数字。 + 运算符期望它的左边和右边都是数字。同理,如果我们尝试输入 True==5 ,$GHCI$会告诉我们他们两个的类型不匹配。+ 作用于两个数字, == 作用于两个同类型可比较的事物。你不能把苹果和橘子进行比较。我们将会在后文中更直观的感受类型的区别。注意:你可以写出 5 + 4.0 这样的式子,这是因为 5 很特别,它可以是一个整数也可以是一个浮点数。 4.0不能表示为整数,所以 5 就被解释为浮点数。
你可能没有注意到,但是我们确实是一直在使用函数。更确切的说,* 是一个将两个数乘起来的函数。正如你所见到的那样,我们把它放在两个数中间以使用它。我们将这种调用方式称为 “中缀函数”。大多数函数都是 “前缀函数”。让我们看看。
函数通常是前缀的,所以从现在开始我们不会明确的说明一个函数式前缀形式,我们假定这是对的。在大多数命令式语言中,通过编写函数名称然后将参数写在括号中来调用函数,参数表通常用逗号分隔。在 $Haskell$ 中,函数的调用方式是直接写函数名称,一个空格然后是参数表,参数之间用空格分隔。为了有个直观认识,我们尝试去调用 $Haskell$ 中一个贼无聊的函数。
1 | ghci> succ 8 |
$succ$函数介绍任何定义过的参数,并且返回它的老大。正如你看到的,我们直接把函数名和参数用一个空格分开。调用一个多参数的函数同样很简单。例如,函数 min 和 max 接受两个可以排序的东西(例如数字!)。 min 函数返回二者中的较小的, max返回二者较大的。你可以自己看看:
1 | ghci> min 9 10 |
函数的调用(通过加一个空格和添加参数来调用函数)有最高优先级。也就是说下面两种表达方式是等价的:
1 | ghci> succ 9 + max 5 4 + 1 |
但是,如果我们想得到 $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) 这样的调用,它不意味着 bar 以 bar 和 3 为参数。它意味着我们先以$3$为参数调用了 bar 并以返回结果再次调用了 bar 。在 $C$ 中,你可能写成 bar(bar(3))这样。
天才第一步
在前一节中,我们对函数调用有了一个基本的认识。现在让我们尝试写自己的函数!打开你最喜爱的编辑器,写下这个接受一个参数并返回它的两倍的函数。
1 | doubleMe x = x + x |
函数的定义和他们被调用的方式大致相同。函数名后面接参数表,并用空格分隔。但是我们在定义函数的时候,这里有一个 = ,并且函数的功能会写在后面。保存这个并命名为 baby.hs 或者其他。然后跳到这个文件所在的目录,然后在该目录下运行 ghci 。进入之后运行 :l baby 。现在我们的文件就加载进去了,然后我们可以调用我们之前定义的函数。
1 | ghci> :l baby |
因为 + 无论是作用于整数还是浮点数(理论上任何是数字的东西都可以)都可以得到正确的结果,所以我们的函数可以正常工作于任何数字。让我们写一个函数,它有两个参数,并返回他们的两倍的和。
1 | doubleUs x y = x*2 + y*2 |
简单。我们同样可以定义成 doubleUs x y = x + x + y + y 。测试一下我们的函数是否能返回正确的结果(记得把这个函数加到 baby.hs 中,保存,然后在 $GHCI$ 中运行 :l baby )。
1 | ghci> doubleUs 4 9 |
正如我们期待的一样,你能调用你自己之前的函数。知道这个之后,我们可以重新定义 doubleUs 像下面这样:
1 | doubleUs x y = doubleMe x + doubleMe y |
这是一个特别简单的例子,这种模式会贯穿整个 $Haskell$.写一些基础的,一定正确的函数,然后组合他们来得到更加复杂的函数。利用这种方式你可以很轻松的避开反复的造轮子。如果某一天数学家们突然指出 $2$ 实际上是 $3$ ,你就只能去改你的代码吗?或许你可以直接重新定义 doubleMe 为 x + x + x,这样当 doubleUs 调用 doubleMe 的时候,它自动变成了适应这个把 $2$ 看成 $3$ 的奇怪世界。
$Haskell$中的函数不用按照顺序定义,也就是说如果你先定义了 doubleMe 然后定义了 doubleUs 或者其他顺序,这都没有关系。
现在我们准备写一个函数,它接受一个参数,如果这个参数小于或等于 $100$,会返回这个数的两倍,否则就会返回这个数本身,因为大于 $100$ 的书本身就够大了。
1 | doubleSmallNumber x = if x > 100 |
上面我们介绍了 $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 | ghci> let lostNumbers = [4,8,15,16,23,42] |
正如你见到的这样,
`