结构化编程和面向对象编程都革新了业务应用程序构建的方式。但是还存在其他编程模型,有些梦想家还认为这些范式比面向对象编程的生产力更高。这篇文章探索 Haskell 研究函数性编程的基础。学习函数性编程可以重塑对于 Java™ 编程的思考方式。
过去 50 年中,企业所使用的语言 —— COBOL、C、C++ 和 Java 语言,都是命令式语言;它们让您告诉您的程序如何去完成其任务。函数性编程语言让您告诉程序去做什么。这篇文章通过介绍 Haskell 来研究函数性编程。(如果您阅读过我的跨越边界系列中关于使用另外一种函数性语言 Erlang 进行并发性编程的文章,可能已经有了一定的基础。)
在我研究超越 Java(请参阅参考资料) 时,我采访的三位专家提到了 Haskell,他们认为我应该探索一下这种语言。当时,我并不认为市场已经为函数性编程做好了准备,因为这一范式对于大多数程序员来说都太陌生了。我现 在仍然不认为我们已经做好了准备。但是我开始欣赏函数性语言带来的生产力和强大力量。我只是刚刚接触 Haskell,但这种语言已经影响了我使用 Java 和 Ruby 语言解决问题的方式。
命令式编程由一系列带有明确顺序的语句构成,它们的算法和编程构造严重依赖于应用程序的状态。很难想像没有这些特征的编程语言,因为命令式语言 “告诉我如何做” 的方式是如此深刻地确立在我们的日常编程哲学中。命令式编程明确塑造了您的思维方式。但通过学习替代的函数性编程工具,可以扩展您思考问题的方式。请考虑 以下这些命令式结构:
如果以前没用过函数性语言,那么就很难想像如何编写没有破坏性赋值和副作用的应用程序。但是这些基本特征给命令性语言带来了一些更严重的问题:
不管信还是不信,不必强行规定操作的顺序或承担副作用,也可以有效地编程。
在数学中,函数把每个输入映射到一个特定的输出。函数性编程范式使用数学函数表达程序。函数性语言并不执行命令,而是通过表示、计算数学函数来解决问题。函数性语言通常有以下两个特征:
对大多数函数性语言来说,这有点过于简化,但是只是过了一点点。函数叫做单体(monad),被用来以数学方式表达状态的变化,而 Haskell 这样的函数性语言则用单体来处理输入/输出并管理状态中的变化。
看到函数性编程的一些局限性后,您可能认为这种编程范式是一种倒退,但请继续往下阅读。函数性语言不是软弱无力的。实际上,语言专家们通常相信函数 性语言操作的抽象级别要比面向对象语言高。它们提供了命令式语言通常不提供的一些工具。在这篇文章中,将看到一些工具的工作效果。
使用 Haskell
有两个 Haskell 实现值得注意:Hugs 和 Glasgow Haskell Compiler(GHC)(请参阅参考资料)。 还有许多其他 Haskell 编译器和解释器,包括 Hugs 和 GHC 的分支,但是它们是主要的两个。如果刚接触 Haskell,那么 Hugs 解释器是个不错的选择,因为它安装和理解起来都比较容易。Hugs 有两方面严重的限制:它缺乏编译器,不能使用独立函数;必须从文件装入全部函数。更严谨的程序员会采用 GHC。它的解释器略慢一些,但是它有编译器模式,还允许独立函数。在这篇文章中,我使用 Hugs,所以如果您想根据本文编写代码,也应当使用它,因为这两套软件使用的术语略有不同。
用 Hugs 编码
请下载适合您操作系统的 Hugs (请参阅参考资料)并启动它。我把我信任的 Macbook Pro 放在一边,而用我的 Windows 机器,这是为了得到一个可以快速安装的环境。WinHugs 这个 Hugs 实现在 Windows 平台上有一个简单的一按即可的安装程序。
将看到一个带有 Hugs 提示符的解释器窗口。启动即可。输入一些数字和数学表达式,如清单 1 所示。将看到 Hugs 返回数学表达式的结果。这正是在函数性语言中期待的行为。
1Haskell98mode:Restartwithcommandlineoption-98toenableextensions
2
3Type:?forhelp
4Hugs>5
55
6Hugs>5+4
79
Haskell 拥有强大的类型模型。语言是强类型的,这意味着您只能在类型的某个实例上完成允许的操作。(例如,如果想把数字添加到字符串上,Hugs 会报错。)Haskell 是静态类型化的,所以一旦给变量分配了值,那么变量就会一直维持相同的类型。Haskell 会做一些类型推断,这意味着它会根据程序中的语义线索来推断元素的类型,所以您会看到我在使用 有些函数时没有声明相关的类型。如果使用类型模糊不清或者在函数中使用不支持的类型,Haskell 会报错。Haskell 还有子类型,而且完全是多态的;这些特性超出了本文的范围,但如果您对此感兴趣,它们也值得研究。
既然已经看到了一些原语类型,例如整型,现在可以继续了解一些更为复杂的类型了。通常,一个语言中可用的数据结构定义了语言的使用方式。C 使用struct、Java 使用class。Haskell 不使用这两种数据结构。
Haskell 中最突出的三种数据结构是:tuple、列表(list)和用户定义的类型。我将着重介绍前两种。tuple 要包含于括号( )之中,它有固定长度。tuple 包含固定类型的原语元素,甚至可以包含其他 tuple 或列表。相比之下,列表的长度可变,由同类元素构成。用[ ]包含列表。您可使用 Hugs 来体会 tuple 和列表,如清单 2 所示:
1Hugs>[1,2]
2[1,2]
3Hugs>[1,"a"]
4ERROR-Cannotinferinstance
5***Instance:Num[Char]
6***Expression:[1,"a"]
7
8Hugs>(1,2,3)
9(1,2,3)
10Hugs>(1,"a")
11(1,"a")
12
13Hugs>[(1,2),(3,4)]
14[(1,2),(3,4)]
15Hugs>[(1,2),(3,4),(5,6,7)]
16ERROR-Typeerrorinlist
17***Expression:[(1,2),(3,4),(5,6,7)]
18***Term:(5,6,7)
19***Type:(c,d,e)
20***Doesnotmatch:(a,b)
在清单 2 中,可以看到 tuple 中的每个元素可以是不同类型的,但列表中的元素必须是相同类型的。而且,如果您使用一个 tuple 列表,那么每个 tuple 的长度必须相同,每个 tuple 中的第n个元素必须与列表中所有其他 tuple 中的第n个元素匹配。
如您所料,Haskell 有许多在列表上操作的函数。最简单的就是head和tail。head返回列表的第一个元素,tail返回其他的元素。清单 3 显示了一些简单的列表函数:
1Hugs>[1,2,3,4,5]
2[1,2,3,4,5]
3Hugs>length[1,2,3,4,5]
45
5Hugs>head[1,2,3,4,5]
61
7Hugs>tail[1,2,3,4,5]
8[2,3,4,5]
在清单 3 中,可以看到head返回了一个元素,tail返回了一列元素。稍后还会看到(在编写函数中)这些函数如何构成了 Haskell 中众多递归函数的基础。
在构建列表时,使用:操作符,叫做构造(cons)操作符(用来构造)。构建列表时,只是把元素传递给另一个列表。可以把许多构建操作串在一起。
字符串只是字符列表的语法代称而已,像[1,2,3]这样的列表则是1:2:3:[]的语法代称。这个特性使得字符串操作更容易实现。清单 4 显示了构造操作符的的工作方式以及如何用一个字符序列构建一个字符串:
1Hugs>6:[]
2[6]
3Hugs>6:[7]
4[6,7]
5Hugs>6:7:[]
6[6,7]
7Hugs>'d':'o':'g':'':'h':'a':'s':'':'f':'l':'e':'a':'s':[]
8"doghasfleas"
在 Haskell 中,您会发现在字符串和列表中这种语法代称非常普遍。 但是只要记住:一切都是函数。
Haskell 允许把函数当成数据。这项重要的功能让众多的 Haskell 函数可以接受函数作为参数。这个策略让您能够用不同的方式把函数应用到函数的每个元素。清单 5 显示了一系列函数,它们把函数应用到列表的每个元素:
1Hugs>:lChar
2Char>maptoUpper"Hello"
3"HELLO"
font-size: 10.0pt; font-family: