既然我们已经学会了如何应用函数,那么是时候回过头来,学习怎样去编写函数。
因为 ghci 只支持 Haskell 特性的一个非常受限的子集,因此,尽管可以在 ghci 里面定义函数,但那里并不是编写函数最适当的环境。更关键的是, ghci 里面定义函数的语法和 Haskell 源码里定义函数的语法并不相同。综上所述,我们选择将代码写在源码文件里。
Haskell 源码通常以 .hs 作为后缀。我们创建一个 add.hs 文件,并将以下定义添加到文件中:
-- file: ch02/add.hs add a b = a + b
[译注:原书代码里的路径为 ch03/add.hs ,是错误的。]
= 号左边的 add a b 是函数名和函数参数,而右边的 a + b 则是函数体,符号 = 表示将左边的名字(函数名和函数参数)定义为右边的表达式(函数体)。
将 add.hs 保存之后,就可以在 ghci 里通过 :load 命令(缩写为 :l )载入它,接着就可以像使用其他函数一样,调用 add 函数了:
Prelude> :load add.hs [1 of 1] Compiling Main ( add.hs, interpreted ) Ok, modules loaded: Main. *Main> add 1 2 -- 包载入成功之后 ghci 的提示符会发生变化 3
[译注:你的当前文件夹(CWD)必须是 ch02 文件夹,否则直接载入 add.hs 会失败]
当以 1 和 2 作为参数应用 add 函数的时候,它们分别被赋值给(或者说,绑定到)函数定义中的变量 a 和 b ,因此得出的结果表达式为 1 + 2 ,而这个表达式的值 3 就是本次函数应用的结果。
Haskell 不使用 return 关键字来返回函数值:因为一个函数就是一个单独的表达式(expression),而不是一组陈述(statement),求值表达式所得的结果就是函数的返回值。(实际上,Haskell 有一个名为 return 的函数,但它和命令式语言里的 return 不是同一回事。)
变量
在 Haskell 里,可以使用变量来赋予表达式名字:一旦变量绑定了(也即是,关联起)某个表达式,那么这个变量的值就不会改变 —— 我们总能用这个变量来指代它所关联的表达式,并且每次都会得到同样的结果。
如果你曾经用过命令式语言,就会发现 Haskell 的变量和命令式语言的变量很不同:在命令式语言里,一个变量通常用于标识一个内存位置(或者其他类似的东西),并且在任何时候,都可以随意修改这个变量的值。因此在不同时间点上,访问这个变量得出的值可能是完全不同的。
对变量的这两种不同的处理方式产生了巨大的差别: 在 Haskell 程序里面, 当变量和表达式绑定之后, 我们总能将变量替换成相应的表达式。 但是在声明式语言里面就没有办法做这样的替换,因为变量的值可能无时不刻都处在改变当中。
举个例子,以下 Python 脚本打印出值 11 :
x = 10 x = 11 print(x)
[译注:这里将原书的代码从 print x 改为 print(x) ,确保代码在 Python 2 和 Python 3 都可以顺利执行。]
然后,试着在 Haskell 里做同样的事:
-- file: ch02/Assign.hs x = 10 x = 11
但是 Haskell 并不允许做这样的多次赋值:
Prelude> :load Assign [1 of 1] Compiling Main ( Assign.hs, interpreted ) Assign.hs:3:1: Multiple declarations of `x' Declared at: Assign.hs:2:1 Assign.hs:3:1 Failed, modules loaded: none.
条件求值
和很多语言一样,Haskell 也有自己的 if 表达式。本节先说明怎么用这个表达式,然后再慢慢介绍它的详细特性。
我们通过编写一个个人版本的 drop 函数来熟悉 if 表达式。先来回顾一下 drop 的行为:
Prelude> drop 2 "foobar" "obar" Prelude> drop 4 "foobar" "ar" Prelude> drop 4 [1, 2] [] Prelude> drop 0 [1, 2] [1,2] Prelude> drop 7 [] [] Prelude> drop (-2) "foo" "foo"
从测试代码的反馈可以看到。当 drop 函数的第一个参数小于或等于 0 时, drop 函数返回整个输入列表。否则,它就从列表左边开始移除元素,一直到移除元素的数量足够,或者输入列表被清空为止。
以下是带有同样行为的 myDrop 函数,它使用 if 表达来决定该做什么。而代码中的 null 函数则用于检查列表是否为空:
-- file: ch02/myDrop.hs myDrop n xs = if n <= 0 || null xs then xs else myDrop (n - 1) (tail xs)
在 Haskell 里,代码的缩进非常重要:它会延续(continue)一个已存在的定义,而不是新创建一个。所以,不要省略缩进!
变量 xs 展示了一个命名列表的常见模式: s 可以视为后缀,而 xs 则表示“复数个 x ”。
先保存文件,试试 myDrop 函数是否如我们所预期的那样工作:
[1 of 1] Compiling Main ( myDrop.hs, interpreted ) Ok, modules loaded: Main. *Main> myDrop 2 "foobar" "obar" *Main> myDrop 4 "foobar" "ar" *Main> myDrop 4 [1, 2] [] *Main> myDrop 0 [1, 2] [1,2] *Main> myDrop 7 [] [] *Main> myDrop (-2) "foo" "foo"
好的,代码正如我们所想的那样运行,现在是时候回过头来,说明一下 myDrop 的函数体里都干了些什么:
if 关键字引入了一个带有三个部分的表达式:
- 跟在 if 之后的是一个 Bool 类型的表达式,它是 if 的条件部分。
- 跟在 then 关键字之后的是另一个表达式,这个表达式在条件部分的值为 True 时被执行。
- 跟在 else 关键字之后的又是另一个表达式,这个表达式在条件部分的值为 False 时被执行。
我们将跟在 then 和 else 之后的表达式称为“分支”。不同分支之间的类型必须相同。[译注:这里原文还有一句“the if expression will also have this type”,这是错误的,因为条件部分的表达式只要是 Bool 类型就可以了,没有必要和分支的类型相同。]像是 if True then 1 else "foo" 这样的表达式会产生错误,因为两个分支的类型并不相同:
Prelude> if True then 1 else "foo" <interactive>:2:14: No instance for (Num [Char]) arising from the literal `1' Possible fix: add an instance declaration for (Num [Char]) In the expression: 1 In the expression: if True then 1 else "foo" In an equation for `it': it = if True then 1 else "foo"
记住,Haskell 是一门以表达式为主导(expression-oriented)的语言。在命令式语言中,代码由陈述(statement)而不是表达式组成,因此在省略 if 语句的 else 分支的情况下,程序仍是有意义的。但是,当代码由表达式组成时,一个缺少 else 分支的 if 语句,在条件部分为 False 时,是没有办法给出一个结果的,当然这个 else 分支也不会有任何类型,因此,省略 else 分支对于 Haskell 是无意义的,编译器也不会允许这么做。
程序里还有几个新东西需要解释。其中, null 函数检查一个列表是否为空:
Prelude> :type null null :: [a] -> Bool Prelude> null [] True Prelude> null [1, 2, 3] False
而 (||) 操作符对它的 Bool 类型参数执行一个逻辑或(logical or)操作:
Prelude> :type (||) (||) :: Bool -> Bool -> Bool Prelude> True || False True Prelude> True || True True
另外需要注意的是, myDrop 函数是一个递归函数:它通过调用自身来解决问题。关于递归,书本稍后会做更详细的介绍。
最后,整个 if 表达式被分成了多行,而实际上,它也可以写成一行:
-- file: ch02/myDropX.hs myDropX n xs = if n <= 0 || null xs then xs else myDropX (n - 1) (tail xs)
[译注:原文这里的文件名称为 myDrop.hs ,为了和之前的 myDrop.hs 区别开来,这里修改文件名,让它和函数名 myDropX 保持一致。]
Prelude> :load myDropX.hs [1 of 1] Compiling Main ( myDropX.hs, interpreted ) Ok, modules loaded: Main. *Main> myDropX 2 "foobar" "obar"
这个一行版本的 myDrop 比起之前的定义要难读得多,为了可读性考虑,一般来说,总是应该通过分行来隔开条件部分和两个分支。
作为对比,以下是一个 Python 版本的 myDrop ,它的结构和 Haskell 版本差不多:
def myDrop(n, elts): while n > 0 and elts: n = n -1 elts = elts[1:] return elts
通过示例了解求值
前面对 myDrop 的描述关注的都是表面上的特性。我们需要更进一步,开发一个关于函数是如何被应用的心智模型:为此,我们先从一些简单的示例出发,逐步深入,直到搞清楚 myDrop 2 "abcd" 到底是怎样求值为止。
在前面的章节里多次谈到,可以使用一个表达式去代换一个变量。在这部分的内容里,我们也会看到这种替换能力:计算过程需要多次对表达式进行重写,并将变量替换为表达式,直到产生最终结果为止。为了帮助理解,最好准备一些纸和笔,跟着书本的说明,自己计算一次。
惰性求值
先从一个简单的、非递归例子开始,其中 mod 函数是典型的取模函数:
-- file: ch02/isOdd.hs isOdd n = mod n 2 == 1
[译注:原文的文件名为 RoundToEven.hs ,这里修改成 isOdd.hs ,和函数名 isOdd 保持一致。]
我们的第一个任务是,弄清楚 isOdd (1 + 2) 的结果是如何求值出的。
在使用严格求值的语言里,函数的参数总是在应用函数之前被求值。以 isOdd 为例子:子表达式 (1 + 2) 会首先被求值,得出结果 3 。接着,将 3 绑定到变量 n ,应用到函数 isOdd 。最后, mod 3 2 返回 1 ,而 1 == 1 返回 True 。
Haskell 使用了另外一种求值方式 —— 非严格求值。在这种情况下,求值 isOdd (1 + 2) 并不会即刻使得子表达式 1 + 2 被求值为 3 ,相反,编译器做出了一个“承诺”,说,“当真正有需要的时候,我有办法计算出 isOdd (1 + 2) 的值”。
用于追踪未求值表达式的记录被称为块(chunk)。这就是事情发生的经过:编译器通过创建块来延迟表达式的求值,直到这个表达式的值真正被需要为止。如果某个表达式的值不被需要,那么从始至终,这个表达式都不会被求值。
非严格求值通常也被称为惰性求值。[注:实际上,“非严格”和“惰性”在技术上有些细微的差别,但这里不讨论这些细节。]
一个更复杂的例子
现在,将注意力放回 myDrop 2 "abcd" 上面,考察它的结果是如何计算出来的:
Prelude> :load "myDrop.hs" [1 of 1] Compiling Main ( myDrop.hs, interpreted ) Ok, modules loaded: Main. *Main> myDrop 2 "abcd" "cd"
当执行表达式 myDrop 2 "abcd" 时,函数 myDrop 应用于值 2 和 "abcd" ,变量 n 被绑定为 2 ,而变量 xs 被绑定为 "abcd" 。将这两个变量代换到 myDrop 的条件判断部分,就得出了以下表达式:
*Main> :type 2 <= 0 || null "abcd" 2 <= 0 || null "abcd" :: Bool
编译器需要对表达式 2 <= 0 || null "abcd" 进行求值,从而决定 if 该执行哪一个分支。这需要对 (||) 表达式进行求值,而要求值这个表达式,又需要对它的左操作符进行求值:
*Main> 2 <= 0 False
将值 False 代换到 (||) 表达式当中,得出以下表达式:
*Main> :type False || null "abcd" False || null "abcd" :: Bool
如果 (||) 左操作符的值为 True ,那么 (||) 就不需要对右操作符进行求值,因为整个 (||) 表达式的值已经由左操作符决定了。[译注:在逻辑或计算中,只要有一个变量的值为真,那么结果就为真。]另一方面,因为这里左操作符的值为 False ,那么 (||) 表达式的值由右操作符的值来决定:
*Main> null "abcd" False
最后,将左右两个操作对象的值分别替换回 (||) 表达式,得出以下表达式:
*Main> False || False False
这个结果表明,下一步要求值的应该是 if 表达式的 else 分支,而这个分支包含一个对 myDrop 函数自身的递归调用: myDrop (2 - 1) (tail "abcd") 。
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/haskellbc/1887.html