Haskell/更进一步
Haskell文件
[编辑]到现在,我们已经多次使用了GHC解释器。它的确是一个有用的调试工具。但是接下来的课程如果我们直接输入所有表达式是麻烦的。所以现在我们开始写第一个Haskell源文件。
在你最喜爱的编辑器中打开一个文件Varfun.hs(hs是Haskell的简写)然后把下面的定义粘贴进去。Haskell使用缩进和空格来决定函数(及其它表达式)的开始和结束,所以确保没有前置的空格并且缩进是正确的,否则GHC将报错。
area r = pi * r^2
(为避免你的疑虑,pi
事实上已经被Haskell定义,不需要在这里包含它了)。现在转到你保存文件的目录,打开ghci,然后使用 :load(或者简写为 :l):
Prelude> :load Varfun.hs Compiling Main ( Varfun.hs, interpreted ) Ok, modules loaded: Main. *Main>
如果ghci给出一个错误,"Could not find module 'Varfun.hs'"(“找不到模块'Varfun.hs'”),那么使用:cd来改变当前目录到包含Varfun.hs的目录:
Prelude> :cd c:\myDirectory Prelude> :load Varfun.hs Compiling Main ( Varfun.hs, interpreted ) Ok, modules loaded: Main. *Main>
现在你可以执行文件中的函数:
*Main> area 5 78.53981633974483
如果你修改了文件,只需使用:reload(简写为 :r)来重新载入文件。
注解
GHC也可以用作编译器。就是说你可以用GHC来把你的Haskell文件转换为一个独立的可执行程序。详情见其文档。 |
你将注意到直接在ghci输入语句与从文件载入有一些不同。这些不同现在看来可能非常随意,但它们事实上是十分明智的范围的结果,其余的,我们保证将晚点解释。
没有let
[编辑]初学者要注意,源文件里不要像这样写
let x = 3 let y = 2 let area r = pi * r ^ 2
而是要像这样写
x = 3 y = 2 area r = pi * r ^ 2
关键字let
事实上在Haskell中用的很多,但这里不强求。在这一章讨论let
绑定的用法后,我们将看得更远。
不能定义同一个量两次
[编辑]先前,解释器高兴地允许我们像这样写
Prelude> let r = 5 Prelude> r 5 Prelude> let r = 2 Prelude> r 2
另一方面,在源文件里像这样写不对
--这不对 r = 5 r = 2
像我们先前提及的那样,变量不能改变,并且当你写一个源文件时甚至更关键。这里有一个漂亮的暗示。它意味着:
顺序不重要
[编辑]你声明变量的顺序不重要。例如,下面的代码片段可以得到完全同样的结果:
y = x * 2 x = 3 |
x = 3 y = x * 2 |
这是Haskell和其它函数式编程语言的一个独特的特性。变量不可改变的事实意味着我们随意选择以任何顺序写代码(但是这也是我们不能声明一个量一次以上的原因--否则那将是模棱两可的的)。
练习 |
---|
把你在上一章写的作业保存在一个Haskell文件中。在GHCi中载入这个文件并用一些参数测试里面的函数 |
关于函数的更多特性
[编辑]当我们开始使用源文件工作而不只是在解释器中敲一些代码时,定义多个子函数会让工作变得简单。 让我们开始见识Haskell函数的威力吧。
条件表达式
[编辑]if / then / else
[编辑]Haskell支持标准的条件表达式。我们可以定义一个参数小于时返回、参数等于时返回、参数大于时返回的函数。事实上,这个函数已经存在(被称为符号(signum)函数),但是让我们定义一个我们自己的,把它叫做mySignum
。
mySignum x = if x < 0 then -1 else if x > 0 then 1 else 0
我们可以这样像这样测试它:
例子:
*Main> mySignum 5 1 *Main> mySignum 0 0 *Main> mySignum (5-10) -1 *Main> mySignum (-1) -1
注意最后一个测试“-1”两边的括弧是必需的;如果没有,系统将认为你正试图将值“mySignum”减去“1”,而这是错误的。
Haskell中的结构与大多数其它编程语言非常相似;无论如何,你必须同时有一个then和一个else子句。在执行条件语句后如果它的值为True
,接着会执行then部分;在执行条件语句后如果它的值为False
,接着会执行else部分。
你可以把程序修改到文件里然后重新装载到GHCI中,除了可以用命令:l Varfun.hs 重新装载外,你还可以通过更快捷更简单的方法:reload 或 :r 把当前已经载入的文件重新载入。
case
[编辑]Haskell跟很多语言一样提供了 case 结构用于组合多个条件语句。(case其实有更强大的功能 -- 详情可以参考 模式匹配).
假设我们要定义一个这样的函数,如果它的参数为它会输出;如果它的参数为它会输出;如果它的参数为它会输出;如果它的参数为其它,它会输出。如果使用if 语句我们会得到一个可读性很差的函数。但我们可以用case语句写出如下形式可读性更强的函数:
f x = case x of 0 -> 1 1 -> 5 2 -> 2 _ -> (-1)
在这个程序中,我们定义了函数f
它有一个参数x
,它检查x
的值。如果它的值符合条件那么f
的值为,如果它的值符合条件那么f
的值为,如此类推。如果x
不符合之前任何列出的值那么f
的值为("_"
为“通配符”表示任何值都可以符合这个条件)
注意在这里缩进是非常重要的。Haskell 使用一个叫“layout"的布局系统对程序的代码进行维护(Python 语言也使用一个相似的系统)。这个布局系统允许我们可以不需要像C,Java语言那样加分号跟花括号来对代码段进行分割。
缩进
[编辑]更多了解请到 缩进。 有人不喜欢使用使用缩进的布局方法,而使用分号跟花括号的方法。如果使用这种方法,以上的程序可以写成以下的形式:
f x = case x of { 0 -> 1 ; 1 -> 5 ; 2 -> 2 ; _ -> -1 }
当然, 如果你使用分号跟花括号的布局方法, 你可以更随意地编写你的代码。以下的方式是完全可以的。
f x = case x of { 0 -> 1 ; 1 -> 5 ; 2 -> 2 ; _ -> -1 }
但是用这种方法有时候代码可读性是很差的。(譬如在以上情况下)
为不同参数定义一个函数
[编辑]函数也可以通过分段定义的方法进行定义,也就是说你可以为不同个参数定义同一个函数的不同版本。例如,以上的函数f
可以写成一下方式
f 0 = 1 f 1 = 5 f 2 = 2 f _ = -1
就跟以上的case
语句一样,函数的执行是与定义的顺序有关的。如果我们把最后的一行移到最前面,那么无论参数是什么,函数f
的值都会是-1
,无论是0 ,1 ,2 (大部分编译器会对这种参数定义重合发出警告)。如果我们不使用f _ = -1
,当函数遇到 0 ,1 ,2 以外的参数时会抛出一个错误(大部分编译器也会会发出警告)。这种函数分段定义的方法是非常常用的,而且会经常在这个教程里面使用。以上的方法跟case
语句实质上是等价的 —— 函数分段定义将被翻译成case
语句
函数合成
[编辑]复杂的函数可以通过简单的函数相互合成进行构建。函数合成就是把一个函数的结果作为另一个函数的参数。其实我们曾经在起步那一章见过,5*4+3
就是两个函数合成。在这个例子中先执行,然后执行的结果作为 参数,最后得出结果。我们也可以把这种方法应用到square
与 f
:
square x = x^2
例子:
*Main> square (f 1) 25 *Main> square (f 2) 4 *Main> f (square 1) 5 *Main> f (square 2) -1
每一个函数的结果都是显而易见的。在例子的第一句中括号是非常必要的;不然,解释器认为你要尝试得到square f
的值,但这显然没有意义。函数的这种合成方式普遍存在于其它编程语言中。在Haskell中有另外一种更数学化的表达方法:(.
) 点函数。点函数源于数学中的()符号。
注解
在数学里我们用表达式 表达 "f following g." 在Haskell , 代码 其中 等同于 。 |
(.
)函数(函数的合成函数),将两个函数合称为一个函数。例如,(square . f)
表示一个有一个参数的函数,先把这个参数代入函数f
,然后再把这个结果代入函数square
得到最后结果。相反地,(f . square)
表示一个有一个参数的函数,先把这个参数代入函数square
,然后再把这个结果代入函数f
得到最后结果。我们可以通过一下例子中的方法进行测试:
例子:
*Main> (square . f) 1 25 *Main> (square . f) 2 4 *Main> (f . square) 1 5 *Main> (f . square) 2 -1
在这里,我们必须用括号把函数合成语句括起来;不然,如(square . f) 1
Haskell的编译器会认为我们尝试把square
与f 1
的结果合成起来,但是这是没有意义的因为f 1
甚至不是一个函数。
现在我们可以稍稍停下来看看在Prelude中已经定义的函数,这是十分明智的。因为很有可能把已经在Prelude中定义了的函数,自己却不经意再定义一遍(我已经不记得我这样做了多少遍了),这样浪费掉了很多时间。
let绑定
[编辑]我们经常在函数中声明局部变量。 如果你记得中学数学课的内容,这里有一个例子, 一元二次方程可以用下列等式求解
我们将它转换为函数来得到:的两个值
roots a b c = ((-b + sqrt(b*b - 4*a*c)) / (2*a), (-b - sqrt(b*b - 4*a*c)) / (2*a))
请注意我们的定义中有一些冗余,不如数学定义优美。在函数的定义中我们重复了sqrt(b*b - 4*a*c)
。 用Haskell的局部绑定(local binding)可以解决这个问题。也就是说我们可以在函数中定义只能在本函数中使用的值。 我们来创建sqrt(b*b-4*a*c)
的局部绑定disc
,并在函数中替换sqrt(b*b - 4*a*c)
我们使用了let/in来声明disc
:
roots a b c = let disc = sqrt (b*b - 4*a*c) in ((-b + disc) / (2*a), (-b - disc) / (2*a))
在let语句中可以同时声明多个值,只需注意让它们有相同的缩进就可以:
roots a b c = let disc = sqrt (b*b - 4*a*c) twice_a = 2*a in ((-b + disc) / twice_a, (-b - disc) / twice_a)
更进一步 |
习题解答 |
Haskell基础 |
起步 >> 变量和函数 >> 列表和元组 >> 更进一步 >> 类型基础 >> 简单的输入输出 >> 类型声明 |
Haskell |
Haskell基础
>> 初级Haskell
>> Haskell进阶
>> Monads
|