Haskell/变量和函数

(本章的所有例子都可以写进Haskell源代码文件并利用GHC或Hugs加载进行求值运算。请注意不要输入开头的提示符,如果代码前面有提示符,可以在GHCi等环境中输入;如果没有,则写进文件再运行。)

变量 编辑

我们已经了解如何把GHCi当作计算器来使用,当然只是极其简短的例子。对于更长的计算以及Haskell程序而言,需要对运算的中间结果进行追踪。

中间结果可以存入变量,通过名字来访问。一个变量包含一个,当使用变数时,变数内的值会被代入。举个例子,请考虑以下情况

ghci> 3.1416 * 5^2
78.53999999999999

在前一章节中,我们知道了如何做像加法和减法这样的算术运算。提问:半径是5厘米的圆的面积是多少?别担心,你并不是错看了几何课本。这个圆的面积是 ,其中 是半径(5厘米), 简单地说就是 。那么,让我们在GHCi中试一下吧:

   ___         ___ _
  / _ \ /\  /\/ __(_)
 / /_\// /_/ / /  | |      GHC Interactive, version 6.4.1, for Haskell 98.
/ /_\\/ __  / /___| |      http://www.haskell.org/ghc/
\____/\/ /_/\____/|_|      Type :? for help.

Loading package base-1.0 ... linking ... done.
Prelude>

我们要把pi(3.14)乘以半径的平方,那就这样

Prelude> 3.14 * 5^2
78.5

太棒了!我们让这台奇妙而又伟大的计算机帮我们计算出了我们想要的结果。但我们不想pi仅仅保留2位小数。那么就让我们再试一次,这次pi变得有点长。

Prelude> 3.14159265358979323846264338327950 * (5 ^ 2)
78.53981633974483

很好,这次我们来计算一下圆的周长(提示: )。

Prelude> 2 * 3.14159265358979323846264338327950 * 5
31.41592653589793

那么半径是25的圆的面积呢?

Prelude> 3.14159265358979323846264338327950 * (25 ^ 2)
1963.4954084936207

我们迟早会厌倦把上述代码一遍又一遍的输入(或拷贝粘贴)的。如我们所言,编程的目的就是摆脱类似于把pi的前20位输入一亿遍这样愚蠢、乏味的重复劳动。我们要做的就是让解释器记住pi的值:

Prelude> let pi = 3.14159265358979323846264338327950
注解

如果这条命令没用的话,你可能使用的是hugs而不是GHCi,hugs的语法有一点不同。

这里你就告诉了Haskell:“让pi等于3.14159……”。这里引入了一个新的变量pi,它被定义为一个数字3.14159265358979323846264338327950。这样就变得非常方便了,我们只要再次输入pi就可以了:

Prelude> pi
3.141592653589793

别在意那些丢失的小数位,他们仅仅是不显示罢了。所有这些小数位都会在将来的计算中被使用到的。

有了变量,事情就变得轻松了。半径是5厘米的圆的面积是多少呢?半径是25厘米的呢?

Prelude> pi * 5^2
78.53981633974483
Prelude> pi * 25^2
1963.4954084936207
注解

我们这里所说的“变量”("variables"),在其它函数式编程的书籍中经常被叫做是“符号”("symbols")。那是因为在其它的语言中,也就是命令式语言中,变量被赋予了完全不同的使用方式:跟踪状态(keeping track of state)。在Haskell中的变量不是这样的:变量保存了一个值,然后再也不可以修改它了。

类型 编辑

按照上面的示例,你可能会想把半径保存在一个变量里。让我们看看会发生什么吧!

Prelude> let r = 25
Prelude> 2 * pi * r

<interactive>:1:9:
    Couldn't match `Double' against `Integer'
      Expected type: Double
      Inferred type: Integer
    In the second argument of `(*)', namely `r'
    In the definition of `it': it = (2 * pi) * r

怎么回事!这里,你遇到了程序设计上的一个概念:类型(types)。类型是许多程序设计语言都有的一个特性,它被设计用来在你自己发现之前,尽早的捕获程序设计上的错误。我们将在之后的类型基础详细讨论型别,现在让我们把它想像成插头和插座。例如,计算机背后的不同功能的插头被设计成不同的形状和大小,这样你就不必担心插错插头了。类型也有着类似的目的,但是在这个特殊的例子里,类型好像不怎么有用啊。

这里的诡异之处在于,像25这样的数字会被解释成Double(双精度浮点型)或Integer(整型),或者可能是其它类型。由于缺少必要的信息,Haskell“猜测”它的类型是Integer(它不能和一个Double型别的数字相乘)。为了解决这个问题,我们只需简单的强调一下,把它作为Double就可以了。

Prelude> let r = 25 :: Double
Prelude> 2 * pi * r
157.07963267948966

请注意,Haskell的这里“猜测”只会发生在当它缺乏足够的信息来推断出型别的时候。下面我们会看到,在大多数情况下,Haskell是能够根据上下文的信息来作出推断的。也就是说,是否把一个数字作为Integer,或是其它型别。

注解

事实上在这个难题背后有一点点微妙。它涉及一个叫作单一同态限定(monomorphism restriction)的语言特性。事实上现在你不需要知道这个,所以如果只想快一点你可以跳过这条提示。除了指定Double类型,你也可以给它一个多态(polymorphic)类型,像Num a => a,意思是"属于Num类的任意类型a"。示例代码如下,它们运行起来像先前的代码一样:

Prelude> let r = 25 :: Num a => a
Prelude> 2 * pi * r
157.07963267948966

Haskell可以从理论上系统地指定这样的多态类型,而不是做缺省的存在潜在错误的猜测,比如整数。但在现实世界,这可能导致数值被不必要地复制或重复计算。要避免这个潜在的陷阱,Haskell的设计者赞同更谨慎的“单一同态限定”。这意味着如果可以从上下文推断出来,或者你明确指定了一个,那么数值只能有一个多态类型。否则编译器会强制选择一个默认的单一同态(也就是非多态)类型。无论如何,这个特性是有争议的。甚至可以使用GHC参数(-fno-monomorphism-restriction)来禁用它,但是这带来了一些低效率的风险。除此之外,多数情况下它就像明确指定类型一样容易。


变量中的变量 编辑

变量不仅可以保存像3.14这样的数值,还可以保存任何Haskell表达式。那么,为了方便的表达半径是5的圆的面积,我们可以写如下代码:

Prelude> let area = pi * 5^2

多么有意思啊,我们在一个变量里面保存了一个复杂的Haskell程序块(一个包含变量的算术表达式)。

我们可以在变量里面保存任意的Haskell代码。那么,让我们继续吧!

Prelude> let r = 25.0
Prelude> let area2 = pi * r ^ 2
Prelude> area2
1963.4954084936207

到现在为止,一直都还不错。

Prelude> let r = 2.0
Prelude> area2
1963.4954084936207
变量不可以被修改

等一下,怎么不对?为什么我们改了半径,面积还是原来的结果?原因就是Haskell里的变量是不可以被修改的[1]。在你第二次定义r的时候,实际上,操作的是另一个r。这种事情在现实生活中也经常碰到。名字叫John的人有多少呢?很多人名字都会是John。当你向朋友们提起“John”的时候,你朋友会根据当时的情况知道到底是哪一个John。在程序设计上也有类似于“当时的情况”这样的概念:作用域。我们并不在这里解释作用域(至少现在不)。Haskell作用域的诡异之处在于可以定义两个不同的r,而总能取用正确的那个。遗憾的是,作用域并没有解决当前的问题。我们要的是定义一个通用的area函数,来告诉我们圆的面积。我们现在能做的,只能把它再定义一遍:

Prelude> let area3 = pi * r ^ 2
Prelude> area3
12.566370614359172

我们是程序员,程序员是讨厌重复劳动的。有更好的解决方法吗?

函数 编辑

我们为了完成一个通用的求面积功能,其实就是定义一个函数。就如同定义一个变量那样,在Haskell中定义一个函数十分的简单。不同之处在于等号的左边有一些额外的东西。例如,下面我们定义一个pi,接着就是面积函数area:

Prelude> let pi = 3.14159265358979323846264338327950
Prelude> let area r = pi * r ^ 2

要计算两个圆的面积,只要把不同的半径传入就可以了:

Prelude> area 5
78.53981633974483
Prelude> area 25
1963.4954084936207

函数的出现使得我们对代码的重用方面有了极大的飞跃。先等一下,让我们来剖析一下上面事物的本质。注意到area r = ...中的r了吗?这就是所谓的参数。参数用来表示一个函数的输入。当Haskell在执行这个函数的时候,参数的值是来自于外部的。在area这个例子中,当你调用area 5的时候,r5,而调用area 25的时候,r25.

练习

在Haskell中输入如下代码:(先不要在解释器中输入)

Prelude> let r = 0
Prelude> let area r = pi * r ^ 2
Prelude> area 5
  1. 你认为会发生什么?
  2. 实际发生了什么为什么(提示:记得以前我们提过的“作用域”吗?))

作用域和参数 编辑

警告:此章节包含上面练习的一些信息

我们希望你完成上面那个小小的练习(也可说是一个实验)。那么,下面的代码并不会让你出乎意料:

Prelude> let r = 0
Prelude> let area r = pi * r ^ 2
Prelude> area 5
78.53981633974483

你可能认为会得到一个0,但结果出乎你的预料。原因就上我们上面所说的,一个命名参数的值是你调用这个函数时传入的。那就是我们的老朋友,作用域。let r = 0中的r和我们定义的函数area中的r,并不是同一个r。在area中的r覆盖了其它的r。你可以认为Haskell选取了一个最近的一个r。如果你有很多朋友都叫John,那么和你在一起的那个John就是你在说话中提到的那个Jonh。类似的,r的值是什么取决于作用域。

多重参数 编辑

关于函数你也许应该知道另一项知识。那就是,函数可以接受一个以上的参数。例如,你想要计算一个矩形的面积。这很容易表达:

Prelude> let areaRect l w = l * w
Prelude> areaRect 5 10
50

又或者你想要计算一个直角三角形的面积 

Prelude> let areaTriangle b h = (b * h) / 2
Prelude> areaTriangle 3 9
13.5

参数传入的方式很直接:只需要根据和定义时一样的顺序传入参数就可以了。因此,areaTriangle 3 9会给出底为3而高为9的三角形面积,而areaTriangle 9 3将给出底为9而高为3的三角形面积。

练习
写一个计算立方体体积的函数。立方体有长、宽、高。把它们乘起来就可以了。


函数中的函数 编辑

我们可以在其他函数的内部调用函数以进一步删减重复的数量。一个简单的例子将用于展示怎样利用这些创建一个计算正方形面积的函数。我们都知道正方形是矩形的一个特殊情况(面积仍然是宽乘以长);然而,我们知道它的宽和长是相同的,因此为什么我们需要键入两次呢?

Prelude> let areaRect l w = l * w
Prelude> let areaSquare s = areaRect s s
Prelude> areaSquare 5
25
练习
编写一个计算圆柱体体积的函数. 圆柱体的体积是圆形底的面积 (你已经在这章编写了这个函数, 所以重新利用它) 乘以高.

总结 编辑

  1. 变量保存着值。其实,他们可以保存任意的Haskell表达式。
  2. 变量不可以改变。
  3. 函数可以帮助你编写可重复利用的代码。
  4. 函数可以接受一个以上的参数。

注释 编辑

  1. 依据读者以前的编程经验:变量不可以改变?我只能得到常量?震惊!恐怖!不……相信我们,我们希望在这本书的剩下部分给你说明,不改变一个单一的变量对你大有帮助!事实上,这些不能改变的变量可以使生活更加方便,因为它可以使程序变得更加可以预测。



变量和函数
习题解答
Haskell基础

起步  >> 变量和函数  >> 列表和元组  >> 更进一步  >> 类型基础  >> 简单的输入输出  >> 类型声明


Haskell

Haskell基础 >> 初级Haskell >> Haskell进阶 >> Monads
高级Haskell >> 类型的乐趣 >> 理论提升 >> Haskell性能


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