Scala编程(第4版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第11步 识别函数式编程风格

正如第1章提到的,Scala允许采用指令式编程,但鼓励采用函数式编程风格。如果你之前的编程背景是指令式的(比方说你是个Java程序员),那么当你学习Scala时的一个主要挑战是搞明白如何使用函数式风格编程。我们意识到这个风格可能对你来说一开始并不熟悉,本书将致力于引导你做出这个转变。这也需要你自己的努力,我们鼓励你这样做。如果你之前更多的是采用指令式的编程风格,我们相信学习函数式编程不仅能让你成为更好的Scala程序员,同样还能帮助你拓宽视野,成为更好的程序员。

首先是从代码层面识别出两种风格的差异。一个显著的标志是如果代码包含任何var变量,它通常是指令式风格的;而如果代码完全没有var(也就是说代码只包含val),那么它很可能是函数式的。因此,一个向函数式风格转变的方向是尽可能不用var

如果你之前用的是指令式的编程语言,比如Java、C++或C#,你可能认为var是常规的变量而val是特例。而如果你之前更多使用函数式编程,比如Haskell、OCaml或Erlang,你可能会认为val是常规的变量而var简直是对编程的亵渎。在Scala看来,valvar不过是你的工具箱中两种不同的工具,都有相应的用途,没有哪一个本质上是不好的。Scala鼓励你更偏向于使用val,但最终要根据手里的工作选择最适用的工具。就算你认同这个平衡的观点,你仍可能在一开始难以想明白如何从你的代码中去掉var

参考如下这个while循环的例子(改编自第2章),使用了var,因此是指令式风格的:

可以将这段代码转换成函数式风格,去掉var,就像这样:

或者这样:

这个例子展示了编程中使用更少的var的好处。经过重构的(更函数式的)代码,跟原始的(更指令式的)代码相比,更清晰、更精简,也更少出错。Scala鼓励使用函数式风格的原因就是这样能帮助你实现更易读、更少出错的代码。

不过你可以走得更远。重构后的printArgs方法并不是“”的函数式代码,因为它有副作用(本例中它的副作用是向标准输出流打印)。带有副作用的函数的标志性特征是结果类型为Unit。如果一个函数并不返回任何有意义的值,也就是Unit这样的结果类型所表达的意思,那么这个函数存在于世上唯一的意义就是产生某种副作用。函数式编程的做法是定义一个将传入的args作为格式化(用于打印)的方法,但只是返回这个格式化的字符串,如示例3.9所示:

示例3.9 一个没有副作用或var的函数

现在你真的实现了函数式编程:没有副作用,也没有varmkString方法可以被用于任何可被迭代访问的集合(包括数组、列表、集和映射),返回一个包含了对所有元素调用toString的结果的字符串,以传入的字符串分隔。因此,如果args包含三个元素"zero""one""two"formatArgs将返回"zero\none\ntwo"。当然,这个函数实际上并不像printArgs那样打印出任何东西,但是可以很容易地将它的结果传给println来达到这个目的:

每个有用的程序都会有某种形式的副作用。否则,它对于外部世界就没有任何价值。倾向于使用无副作用的函数鼓励你设计出将带有副作用的代码最小化的程序。这样做的好处之一是让你的程序更容易测试。

例如,要测试本节给出的三个printArgs方法,需要重新定义println,捕获传给println的输出,确保它是你预期的样子。而要测试formatArgs则很简单,只需要检查它的结果即可:

Scala的assert方法检查传入的Boolean,如果是false,则抛出AssertionError。如果传入的Booleantrueassert就安静地返回。你将在第14章了解到更多关于断言(assertion)和测试的内容。

尽管如此,请记住var或副作用从本质上讲并不邪恶。Scala并不是一门纯函数式编程语言,强制你只能用函数式风格来编程。Scala是指令式/函数式混合(hybrid)编程语言。你会发现有些场景下对于要解决的问题而言指令式更为适合,这个时候不要犹豫,使用指令式的风格就好。为了让你学习如何不使用var完成编程任务,我们将在第7章向你展示许多具体的用到var的代码示例,并告诉你如何将这些var转换成val

Scala程序员的平衡心态

倾向于使用val、不可变对象和没有副作用的方法,优先选择它们。不过当你有特定的需要和理由时,也不要拒绝var、可变对象和带有副作用的方法。