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

我们可以这样像这样测试它:

 
Example

例子:

*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就是两个函数合成。在这个例子中 先执行,然后执行的结果作为   参数,最后得出结果。我们也可以把这种方法应用到squaref

square x = x^2
 
Example

例子:

*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 , 代码f . g 也表达为 "f following g."

其中  等同于  


(.)函数(函数的合成函数),将两个函数合称为一个函数。例如,(square . f)表示一个有一个参数的函数,先把这个参数代入函数f,然后再把这个结果代入函数square得到最后结果。相反地,(f . square)表示一个有一个参数的函数,先把这个参数代入函数square,然后再把这个结果代入函数f得到最后结果。我们可以通过一下例子中的方法进行测试:

 
Example

例子:

*Main> (square . f) 1
25
*Main> (square . f) 2
4
*Main> (f . square) 1
5
*Main> (f . square) 2
-1

在这里,我们必须用括号把函数合成语句括起来;不然,如(square . f) 1Haskell的编译器会认为我们尝试把squaref 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
高级Haskell >> 类型的乐趣 >> 理论提升 >> Haskell性能


库参考 >> 普通实务 >> 特殊任务