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. / /_\\/ __ / /___| | -{R|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
这里你就告诉了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
类型
按照上面的示例,你可能会想把半径保存在一个变量里。让我们看看会发生什么吧!
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,或是其它型别。
变量中的变量
变量不仅可以保存像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
Template:边注
等一下,怎么不对?为什么我们改了半径,面积还是原来的结果?原因就是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的时候,r是5,而调用area 25的时候,r是25.
作用域和参数
- 警告:此章节包含上面练习的一些信息
我们希望你完成上面那个小小的练习(也可说是一个实验)。那么,下面的代码并不会让你出乎意料:
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
总结
- 变量保存着值。其实,他们可以保存任意的Haskell表达式。
- 变量不可以改变。
- 函数可以帮助你编写可重复利用的代码。
- 函数可以接受一个以上的参数。
注释
- ↑ 依据读者以前的编程经验:变量不可以改变?我只能得到常量?震惊!恐怖!不……相信我们,我们希望在这本书的剩下部分给你说明,不改变一个单一的变量对你大有帮助!事实上,这些不能改变的变量可以使生活更加方便,因为它可以使程序变得更加可以预测。