当前位置: 首页 > news >正文

[Real world Haskell] 中文翻译:第一章 快速上手

第一章 快速上手

请注意在阅读本书前面几章时,我们有时会用受限制的简单的形式来介绍一些概念。Haskell是一种很有深度的语言,如果一次把给定主题的所有方面都展现出来的话,会压得初学者喘不过气来。在我们拥有扎实的Haskell基础后,将对最初提及的这些概念作进一步阐述。

Haskell环境

Haskell语言有很多实现,其中有两个用得较广。Hugs是一个解释器,主要用于教学。实际应用中,Glasgow Haskell编译器(GHC)更加流行。GHC比Hugs更适合实际的工作:可以编译生成本地代码,支持并行执行,提供实用的性能分析与调试工具。因此,本书将使用GHC的Haskell实现。

GHC有三个主要部件:
1. ghc : 一个优化的编译器,生成快速的本地可执行代码
2. ghci : 一个交互解释器和调试器
3. runghc: 将Haskell 程序作为脚本执行,不需要预先编译。

本书假定读者使用的是2007年发布的GHC 6.8.2及以上版本。我们的很多示例程序可以不加修改的运行在更老的版本上。尽管如此,我们建议读者使用所在系统平台上可用的最新版本。如果你使用的是Windows或者Mac OSX,可以简单的用打包好的安装包快速开始。要获得这些平台上的GHC拷贝,请访问GHC的下载页面,从二进制包和安装包列表中寻找。

很多Linux发行版,BSD和其他Unix变种的提供者,提供定制的GHC二进制包。因为这些安装包是为每个系统环境特别定制的,因此比GHC下载页面上提供的通用二进制包更容易安装和使用。你可以从GHC的分发包页面找到定制编译的GHC列表。

在附录A 安装GHC和Haskell库中,我们提供了在一些流行的平台上安装GHC的更详细信息。

开始使用解释器ghci

GHC的交互解释器叫做 ghci。它可以让我们输入和执行Haskell表达式,查看模块,调试代码。如果你熟悉Python或者Ruby的话,ghci有点类似于Python和Ruby的解释器:python和irb。

在Unix类操作系统上,我们在shell窗口中用ghci命令来运行ghci解释器。在Windows上,可以通过开始菜单执行。例如,如果你使用的是GHC的Windows XP安装包,你可以打开“所有程序”,之后到“GHC”程序组,然后你会在列表中看到ghci。

运行ghci时,它显示一个开始标题,之后跟 Prelude> 提示符。下面显示的是Linux上的6.8.3版本界面:

$ ghci
GHCi, version 6.8.3: http://www.haskell.org/ghc/ :? for help
Loading package base ... linking ... done.
Prelude>

提示符中的Prelude一词表示Prelude这个模块已经载入可以使用了,Prelude是一个有很多有用函数的标准库。载入其他的模块或者源文件后,它们也会显示在提示符中。

Prelude模块有时也表示标准预定义库,因为它的内容是Haskll 98标准中定义的。它经常被简称为标准库。

perlude标准库总是默认可用的;我们不需要做什么操作就可以使用它定义的类型,值或函数。要使用其他模块中的定义,我们必须用 :module 命令来将它们载入到ghci中。

ghci> :module + Data.Ratio

现在可以使用 Data.Ratio模块中的功能,可以让我们操作有理数(分数)。

基本交互:把ghci当成计算器

ghci除提供了一个方便的测试代码块的界面外,还可以当成快捷可用的桌面计算器使用。我们可以在ghci中容易的表示任何操作符,并且当我们对Haskell更熟悉后,可以增加复杂的多的操作符。即使用这种简单的方式来使用解释器,也可以帮我们变得更适应Haskell的工作方式。

简单算术

我们可以立刻开始输入表达式,看ghci如何处理它们。基本的算术与C和Python这样的语言相类似:用中缀格式书写表达式,操作符在它的操作值之间。

ghci> 2 + 2
4
ghci> 31337 * 101
3165037
ghci> 7.0 / 2.0
3.5


中缀风格的表达式只是一个便捷形式:我们也可以用前缀格式来写表达式,操作符出现在它的参数前面。要这样做我们必须把操作符用括号括起来。

ghci> 2 + 2
4
ghci> (+) 2 2
4


上面的表达式显示,Haskell有整数和浮点数的概念。整数可以任意大。这里 (^) 操作符提供了整数的幂运算。

ghci> 313 ^ 15
27112218957718876716220410905036741257


算术表示的怪癖:表示负数

Haskell在书写数字的方式上有个怪异的地方:经常要把负数用括号括起来。这一点在我们开始写稍微复杂的表达式时就会显现出来。

我们从写一个负数开始。

ghci> -3
-3


上面的 - 是一个一元操作符。换句话说,我们并不是写了一个单独的数字"-3"; 而是写了一个数字"3",并给他应用了一个 - 操作符。-操作符是Haskell中唯一的一元操作符,我们不能把它和中缀操作符混用。

ghci> 2 + -3

<interactive>:1:0:
precedence parsing error
cannot mix `(+)' [infixl 6] and prefix `-' [infixl 6] in the same infix

如果我们要在一个中缀操作符旁使用一元的负号操作符,我们必须把负号和它所操作的表达式括起来。

ghci> 2 + (-3)
-1
ghci> 3 + (-(13 * 37))
-478

这可以避免解析的歧义。在Haskell中应用函数时,我们写出函数的名称,后跟它的参数,比如 f 3。如果我们不用括号把负数括起来的话,将会有两种方式来解读 f - 3: 可以是在 -3上应用函数f,也可以是从变量f中减去3 。

大多数时候我们可以省略表达式中的空白(空格或tab这样的空白字符)。Haskell可以如我们预期解析它们。但并不总是如此,下面这个表达式可以工作:

ghci> 2*3
6


下面这个类似于前面有问题的负数例子,但是结果产生不同的错误信息。

ghci> 2*-3

<interactive>:1:1: Not in scope: `*-'

这里,Haskell的实现把 *- 当作单独的操作符来读取。Haskell可以让我们定义新的操作符(后面会回到这个主题),但是我们并没有定义*-。同样,加一些括号可以让我们和ghci对表达式的看法一致。

ghci> 2*(-3)
-6

与其他语言相比,这种处理负数的不常见方式可能看上去比较烦,不过这是事出有因的公平交易。Haskell可以让我们随时定义新的操作符。这并不是语言的什么深奥特性,我们可以在后面的章节中看到很多用户定义的操作符。语言的设计者接受负数的麻烦语法以此换取强大的表达能力。


布尔逻辑,操作符,值的比较


Haskell中的布尔逻辑值是 True和False。注意开头的大写。对布尔值应用的操作符受C的风格影响: (&&)表示逻辑“与”,(||)是逻辑或。

ghci> True && False
False
ghci> False || True
True


有些编程语言把0值当作False 的同义词。Haskell并不这样做,并且它也不认为非0值是True。


ghci> True && 1

<interactive>:1:8:
No instance for (Num Bool)
arising from the literal `1' at <interactive>:1:8
Possible fix: add an instance declaration for (Num Bool)
In the second argument of `(&&)', namely `1'
In the expression: True && 1
In the definition of `it': it = True && 1


我们又一次看到了大量的错误信息。简单来说,它告诉我们布尔类型 Bool 并不是数字类型 Num 家族的成员。错误信息很长因为ghci指出了问题出现的位置,并给我们提示了怎样修改可能解决问题。

这是更详细的错误信息分析。
* “No instance for (Num Bool)”
告诉我们ghci尝试将数字值1当作Bool类型来处理,但是没有成功。

* “arising from the literal `1'”
指出是数字1的使用造成了这个问题。

* “In the definition of `it'”
ghci中引用的快捷方式,稍后几页会再次介绍。

Haskell的大部分比较表达式与C语言及受C语言影响的语言中的用法相似。

ghci> 1 == 1
True
ghci> 2 < 3
True
ghci> 4 >= 3.99
True

与C语言中的对应物不同的一个操作符是“不等于”。在C语言中写作 !=。 这Haskell中,我们写作 (/=) ,模仿数学中的≠记号。

ghci> 2 /= 3
True


同时,C类型的语言中经常用 ! 表示逻辑非,Haskell使用 not 函数。

ghci> not True
False


表达式优先级和结合性

就像代数或其他编程语言中的中缀表达式一样,Haskell有表达式优先级的概念。我们可以用括号来显式的把表达式分组,优先级可以让我们省略一些括号。比如,乘法操作比加法操作拥有更高的优先级,所以Haskell中把下面两个表达式同等对待:

ghci> 1 + (4 * 4)
17
ghci> 1 + 4 * 4
17

Haskell给操作符分配数字的优先级,1表示最低优先级,9表示最高优先级。高优先级的函数先于低优先级的函数执行。我们可以这ghci中用 :info 命令来看某个操作符的优先级等级。

ghci> :info (+)
class (Eq a, Show a) => Num a where
(+) :: a -> a -> a
...
-- Defined in GHC.Num
infixl 6 +
ghci> :info (*)
class (Eq a, Show a) => Num a where
...
(*) :: a -> a -> a
...
-- Defined in GHC.Num
infixl 7 *


我们要找的信息在 "infixl 6 +" 这一行,它指出 (+) 操作符的优先级是6.(后面章节解释其他输出信息)"infixl 7 *" 告诉我们 (*) 操作符优先级是7。因为 (*)比(+)优先级更高,因此可以知道为什么 1 + 4 * 4 等价于 1 + (4 * 4),而不同于 (1 + 4) * 4。

Haskell也定义操作符的结合性。这决定了一个多次使用某操作符的表达式是从左到右求值还是从右到左求值。上面ghci的输出中 infixl 说明(+)和(*)操作符是左结合性的。右结合性操作符用 infixr 表示。

ghci> :info (^)
(^) :: (Num a, Integral b) => a -> b -> a -- GHC.Real 中定义
infixr 8 ^

优先级和结合性的组合经常称为不动点规则。 (fixity rule ?)

未定义值和导出变量

之前提到的Haskell标准库prelude至少为我们定义了一个熟知的数学常量。

ghci> pi
3.141592653589793


但是它的数学常量覆盖的并不广,我们可以很快看到。让我们看下欧拉常数:

ghci> e

<interactive>:1:0: Not in scope: `e'

好吧,我们必须自己定义它了。

用ghci的let结构,可以构造我们自己的e的临时定义。

ghci> let e = exp 1

这是指数函数exp的应用,我们在Haskell中的第一个函数应用例子。Python之类的语言需要把函数的参数用括号括起来,而Haskell不需要。

定义了e之后,我们就可以在算术表达式里使用它。我们之前介绍的幂运算操作符(^)只能用在一个数的整数幂。要使用浮点数作为幂,需要使用 (**)幂运算操作符。

ghci> (e ** pi) - pi
19.99909997918947

处理优先级和结合性规则

有时保留一些括号会好一些,即使当Haskell允许我们省略它们的时候也是如此。它们的存在可以帮助以后的读者(包括我们自己)理解我们所要表达的意思。

更重要的是,完全依赖运算符优先级的复杂表达式是众所周知的bug源头。一个哪怕很短的,但完全没有括号的表达式,其所要表达的意图很容易被编译器或者人理解成不同的意思。

没必要记住所有的优先级和结合性规则:如果你不确定的话简单的加上括号就好了。

ghci中的命令行编辑

在大多数系统中,ghci都有一些命令行编辑能力。如果你不熟悉命令行编辑的话,简单说它可以极大的节省时间。基本操作在Unix类的系统和Windows系统中是通用的。按下键盘上的向上方向键,调出上一条输入的命令;重复按向上方向键会在早前输入的行中循环。按左和右方向键在一行中移动。在Unix上(很不幸,Windows上不行)用tab键可以自动补全输入了一部分的标识符。

列表

列表用方括号括起来;列表的元素用逗号分隔。

ghci> [1, 2, 3]
[1,2,3]


列表可以任意长。空列表写作 []。
ghci> []
[]
ghci> ["foo", "bar", "baz", "quux", "fnord", "xyzzy"]
["foo","bar","baz","quux","fnord","xyzzy"]


列表的元素必须是同一种类型。这里,我们违反这个规则:我们的列表开始是两个Bool值,但是最后跟一个字符串。

ghci> [True, False, "testing"]

<interactive>:1:14:
Couldn't match expected type `Bool' against inferred type `[Char]'
Expected type: Bool
Inferred type: [Char]
In the expression: "testing"
In the expression: [True, False, "testing"]


ghci的错误信息依然很冗长,不过它简单的告诉我们无法把字符串转成布尔值,因此列表表达式不能正确的类型化。


如果我们用枚举标记来写一个元素序列,Haskell将给我们填充列表的内容。
ghci> [1..10]
[1,2,3,4,5,6,7,8,9,10]


这里 .. 专门用于枚举。我们只能在其成员可以被枚举的类型上使用这个标记。在字符串上使用是没有意义的,比如,没有任何明显的一般的方法来枚举 ["foo" .. "quux"]。

另外注意上面的使用的范围记号给出一个闭区间:列表包含两端的元素。

我们写一个枚举时,可以选择提供前两个元素来指定步长,后面跟停止生成枚举的结束值。

ghci> [1.0,1.25..2.0]
[1.0,1.25,1.5,1.75,2.0]
ghci> [1,4..15]
[1,4,7,10,13]
ghci> [10,9..1]
[10,9,8,7,6,5,4,3,2,1]

上面第二个例子,列表显然缺少了枚举的结束值,因为它不是我们定义的序列的元素。


我们可以省略枚举的结束点。如果一个类型没有一个自然的“上限”将会产生无穷的值。比如,如果在ghci的提示符上输入 [1..],ghci将打印无限增大的数列,除非你中断或者杀掉ghci的进程。如果你尝试这么做的话,键入Ctrl+C来中止枚举。后面我们会发现Haskell中无限列表经常很有用。

列表的操作符

使用列表时有两个普遍存在的操作符。用 (++)操作符来连接两个列表。

ghci> [3,1,3] ++ [3,7]
[3,1,3,3,7]
ghci> [] ++ [False,True] ++ [True]
[False,True,True]



更基本的是 (:) 操作符,它把一个元素加到一个列表的前面。发音为 "cons" , 是 "construct" 的缩略。

ghci> 1 : [2,3]
[1,2,3]
ghci> 1 : []
[1]


你可以尝试用 [1,2]:3 来把一个元素加到列表的结尾,但ghci将会拒绝并给出错误信息,因为(:)操作符的第一个参数必须是一个元素,第二个必须是一个列表。

字符串和字符

如果你了解Perl或C之类的语言,会发现 Haskell中字符串的概念与之类似。

文本字符串用双引号括起来。

ghci> "This is a string."
"This is a string."

在很多语言中,我们可以用转义符来表示难以看到的字符。Haskell所用的转义符和转义规则遵从由C语言建立的广泛使用的约定。比如,'\n'是换行符, '\t'是 tab 符。详情请参见附录B, 字符,字符串和转义规则。

ghci> putStrLn "Here's a newline -->\n<-- See?"
Here's a newline -->
<-- See?


putStrLn 函数打印一个字符串。

Haskell区分单个字符和文本字符串。单个字符用单引号括起来。

ghci> 'a'
'a'


实际上,字符串只是单独字符的列表。这里用一种痛苦的方式来写一个简短的字符串,ghci将会返回给我们熟悉的格式。

ghci> let a = ['l', 'o', 't', 's', ' ', 'o', 'f', ' ', 'w', 'o', 'r', 'k']
ghci> a
"lots of work"
ghci> a == "lots of work"
True


空字符串写作 "",与 []同义。

ghci> "" == []
True

因为字符串是字符的列表,因此我们可以使用常规的列表操作符来构建新的字符串。

ghci> 'a':"bc"
"abc"
ghci> "foo" ++ "bar"
"foobar"

类型初步

尽管我们已经讨论了一点类型,但我们与ghci的交互至今没有过多的考虑类型相关的事。我们没有告诉ghci我们用了什么类型,而它也乐于接受我们的输入。

Haskell的类型名开头字母要大写,变量名开头字母要小写。继续阅读时记住这个,会让那些名字更容易被掌握。

开始探索类型的世界,我们首先可以让ghci告诉我们关于它正在做什么的更多信息。ghci有一个命令 :set 可以让我们改变它的一些默认行为。我们可以让他打印更多的类型信息。
ghci> :set +t
ghci> 'c'
'c'
it :: Char
ghci> "foo"
"foo"
it :: [Char]


+t 告诉ghci在表达式后面打印出它的类型。输出中隐含的 it 很有用:它实际上是ghci存储上一个表达式求值结果的特殊变量。(这不是haskell语言的特性,这是ghci特有的)。来解析下ghci输出的最后一行的意思:

* 它告诉我们这是关于特殊变量 it 的。

* 我们可以把 x :: y 形式的内容理解成: 表达式x具有类型y。

* 这里表达式 "it" 有类型 [Char]。 (经常用String这个名字来代替[Char]。它只是[Char]的同义词)


从我们已经见过的表达式里,有更多的Haskell类型名称。

ghci> 7 ^ 80
40536215597144386832065866109016673800875222251012083746192454448001
it :: Integer


Haskell的整数类型名称是 Integer。Integer值的大小只受限于你的系统的内存容量。

有理数看上去与整数很不相同。使用 (%)操作符构造有理数。分子在左边,分母在右边。

ghci> :m +Data.Ratio
ghci> 11 % 29
11%29
it :: Ratio Integer


方便起见,ghci可以缩写很多命令,我们可以写 :m 代替 :module 来加载一个模块。

注意上面 :: 右手边的两个词。可以读作“整数的比值” (Ratio of Integer)。我们可以猜测一个比值的分子和分母一定都是整数。有把握的说,如果构造一个比值,其分子和分母是不同类型,或者是相同类型但不是整数类型,那么ghci将会报错。

ghci> 3.14 % 8

<interactive>:1:0:
Ambiguous type variable `t' in the constraints:
`Integral t' arising from a use of `%' at <interactive>:1:0-7
`Fractional t'
arising from the literal `3.14' at <interactive>:1:0-3
Probable fix: add a type signature that fixes these type variable(s)
ghci> 1.2 % 3.4

<interactive>:1:0:
Ambiguous type variable `t' in the constraints:
`Integral t' arising from a use of `%' at <interactive>:1:0-8
`Fractional t'
arising from the literal `3.4' at <interactive>:1:6-8
Probable fix: add a type signature that fixes these type variable(s)


虽然开始时 :set +t 给我们键入的每个表达式的类型信息很有用,但这个功能我们将很快不再需要。过一段时间,我们经常知道一个表达式所应该具有的类型。我们可以随时用 :unset 命令来关闭额外的类型信息。

ghci> :unset +t
ghci> 2
2


即使关掉这个功能后,通过使用另一个ghci命令,我们依然可以方便的知道我们需要的类型信息。

ghci> :type 'a'
'a' :: Char
ghci> "foo"
"foo"
ghci> :type it
it :: [Char]


:type命令会打印出任何我们给它的表达式的类型信息(包括 it ,之前提过)。它不会真的对表达式求值,它只是检查并打印出它的类型。


为什么这两个表达式报告不同的类型?

ghci> 3 + 2
5
ghci> :type it
it :: Integer
ghci> :type 3 + 2
3 + 2 :: (Num t) => t


Haskell有几个数字类型。比如,根据出现的场景不同,像1这样的数字常量可以当作整数或者浮点数。当我们强制 ghci 对表达式 3 + 2 求值时,它必须选择一种类型以打印它的值,默认是整数。在第二种情况中,我们让 ghci 打印出表达式的类型但不实际对它求值,所以它不需要那么完全确定。它的回答,大意上说,“它的类型是一个数字”。在第六章“使用类型类”中,我们将会看到更多这种形式的类型标记。

一个简单程序

让我们先做一个小的跳跃,写一个简单的程序来统计输入内容的行数。不用管是否理解,先感受下上手的乐趣。在一个文本编辑器里,输入下面的代码保存到文件中,命名为 WC.hs。

-- file: ch01/WC.hs
-- lines beginning with "--" are comments.

main = interact wordCount
where wordCount input = show (length (lines input)) ++ "\n"


找一个或者创建一个文本文件,命名为 quux.txt

$ cat quux.txt
Teignmouth, England
Paris, France
Ulm, Germany
Auxerre, France
Brunswick, Germany
Beaumont-en-Auge, France
Ryazan, Russia

在shell或者命令行里,执行下面的命令:

$ runghc WC < quux.txt
7

我们已经成功的写了一个简单的程序与真实世界交互!后面的章节,我们将继续加深理解,直到写出自己的程序。

练习


1. 在ghci中输入下面表达式。它们的类型是什么?
2. 在ghci中,键入 :? 来打印一些帮助。定义一个变量,如 let x = 1,然后键入 :show bindings 。 能看到什么?
3. words函数可以数一个字符串中包含的单词数目。修改例子程序 WC.hs 来数一个文件中的单词数目。
4. 再次修改WC.hs 例子程序,打印出一个文件的字符数量。

相关文章:

  • LeetCode -- Binary Tree Level Order Traversal
  • LeetCode -- Course Schedule II
  • [Real world Haskell] 中文翻译:第二章 类型与函数
  • LeetCode -- Next Permutation
  • [Real world Haskell] 中文翻译:第三章 定义类型,流式函数
  • LeetCode -- Search Matrix
  • LeetCode -- Sort Colors
  • 理解HTTP消息头
  • LeetCode -- Convert SortedList To BST
  • HTTP协议返回状态码表
  • LeetCode -- Insert Interval
  • 推荐WPF的好书
  • LeetCode -- Longest Valid Parentheses
  • 利用Intel博锐技术解决桌面管理难题
  • LeetCode -- Permutations
  • JavaScript 如何正确处理 Unicode 编码问题!
  • Android组件 - 收藏集 - 掘金
  • CSS进阶篇--用CSS开启硬件加速来提高网站性能
  • el-input获取焦点 input输入框为空时高亮 el-input值非法时
  • java8-模拟hadoop
  • js 实现textarea输入字数提示
  • MaxCompute访问TableStore(OTS) 数据
  • RxJS: 简单入门
  • 闭包--闭包之tab栏切换(四)
  • 高性能JavaScript阅读简记(三)
  • 缓存与缓冲
  • 使用 QuickBI 搭建酷炫可视化分析
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 数据可视化之 Sankey 桑基图的实现
  • 双管齐下,VMware的容器新战略
  • 系统认识JavaScript正则表达式
  • 小程序、APP Store 需要的 SSL 证书是个什么东西?
  • 一个JAVA程序员成长之路分享
  • 京东物流联手山西图灵打造智能供应链,让阅读更有趣 ...
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • ​LeetCode解法汇总1410. HTML 实体解析器
  • #ifdef 的技巧用法
  • $forceUpdate()函数
  • (06)金属布线——为半导体注入生命的连接
  • (2022版)一套教程搞定k8s安装到实战 | RBAC
  • (3)(3.5) 遥测无线电区域条例
  • (52)只出现一次的数字III
  • (delphi11最新学习资料) Object Pascal 学习笔记---第7章第3节(封装和窗体)
  • (带教程)商业版SEO关键词按天计费系统:关键词排名优化、代理服务、手机自适应及搭建教程
  • (顶刊)一个基于分类代理模型的超多目标优化算法
  • (附源码)python旅游推荐系统 毕业设计 250623
  • (六)软件测试分工
  • (五)c52学习之旅-静态数码管
  • (转)scrum常见工具列表
  • .NET Core 版本不支持的问题
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .net 桌面开发 运行一阵子就自动关闭_聊城旋转门家用价格大约是多少,全自动旋转门,期待合作...
  • .NET开源项目介绍及资源推荐:数据持久层 (微软MVP写作)
  • .net实现客户区延伸至至非客户区
  • /bin/bash^M: bad interpreter: No such file or directory