Working with Legacy Code 的作者Michael Feathers 用寥寥数语 (https://twitter.com/mfeathers/status/29581296216)捕捉到了函数式抽象和面向对象抽象的关键区别: 面向对象编程通过封装不确定因素来使代码能被人理解;函数式编程通过尽量减少不确定因素来使代码能被人理解。 ——Michael Feathers 请回想一下你熟悉的封装、作用域、可见性等面向对象编程(OOP)构造,这些机制的存在意义,都是为了精细地控制谁能够感知状态和改变状态。而当涉及多线程的时候 ,对状态的控制就更复杂了。这些机制就属于Michael Feathers 所谓的“不确定因素”(movingparts)。大多数函数式语言在这个问题上采取了另一种做法,它们认为,与其建立种种机制来控制可变的状态,不如尽可能消灭可变的状态这个不确定因素。其立论的根据是这样的:假如语言不对外暴露那么多有出错可能的特性,那么开发者就不那么容易犯错。我会展示各种例子来说明函数式编程是怎样消除变量、抽象和其他不确定因素的。 在面向对象的命令式编程语言里面,重用的单元是类和类之间沟通用的消息,这些都 可以用类图(class diagram)来表述。这个领域的代表性著作《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software,作者ErichGamma、Richard Helm、Ralph Johnson、John Vlissides)就在每一个模式的说明里都附上了至少一幅类图。OOP 的世界提倡开发者针对具体问题建立专门的数据结构,相关的专门操作以“方法”的形式附加在数据结构上。函数式编程语言实现重用的思路很不一样。函数式语言提倡在有限的几种关键数据结构(如list、set、map)上运用针对这些数据结构高度优化过的操作,以此构成基本的运转机构。开发者再根据具体用途,插入自己的数据结构和高阶函数去调整机构的运转方式。 我们来分析下面截取自例1-2 的片段: regexToList(words, "\b\w+\b").stream() .filter(w -> !NON_WORDS.contains(w)) 这里为了取得列表的一个子集而调用了filter() 方法,并向filter() 方法传入已被 转换为stream 的列表内容,以及定义了筛选条件的高阶函数(即行中裹上了语法糖衣的(w →!NON_WORDS.contains(w))))。运转机构高效率地按照指定的条件实行筛选,返回筛选后的列表。 比起一味创建新的类结构体系,把封装的单元降低到函数级别,更有利于达到细粒度 的、基础层面的重用。反面例子如Java 世界的数十种XML 类库,每一种都有自己定义的内部数据结构。相比之下,Clojure 就享受到了使用高层次抽象的好处。不久前Clojure 库中的map 方法经过创造性的重写,获得了自动并行的能力,也就是说,所有Clojure 开发者不需要动一行代码,就自动享受到了map 操作的性能提升。 函数式程序员喜欢用少数几个核心数据结构,围绕它们去建立一套充分优化的运转机 构。面向对象程序员喜欢不断地创建新的数据结构和附属的操作,因为压倒一切的面向对象编程范式就是建立新的类和类间的消息。把所有的数据结构都封装成类,一方面压制了方法层面的重用,另一方面鼓励了大粒度的框架式的重用。函数式编程的程序构造更方便我们在比较细小的层面上重用代码。 例1-3 取自为Java 提供大量辅助工具类的Apache Commons (http://commons.apache.org/proper/commons-lang)框架,请观察下面的indexOfAny() 方法。 indexOfAny() 方法的参数是一个String 和一个数组,它会在String 中查找数组里的字符,并返回任意一个字符第一次出现的索引位置。其文档中举了一些例子来说明输入与输出的关系,见例1-4。 例1-4 indexOfAny() 的用法示例 我们看到,字符串zzabyycdxx 中第一次出现字符z 或a 是在索引位置0,第一次出现字符b 或y 是在索引位置3。 这个问题的实质可以表述为:对于searchChars 中的任意字符,在目标字符串中查找 该字符第一处匹配的索引位置。假如换成Scala 语言,同样的方法实现起来要直接得多,请看例1-5 的firstIndexOfAny 方法。 在本例中,我为输入字符串制作了一个添加了索引的版本。Scala 的zip() 方法将( 从0 到输入字符串长度值的)数字集合与String 对象中所含字符的集合对位结合,组成一个新的、由数字和字符对构成的集合。例如当输入字符串为zabycdxx 时,indexedInput 将取值为Vector ((0,z), (1,a), (2,b), (3,y), (4,c), (5,d), (6,x), (7,x))。zip 方法得名于它像拉链(zipper)一样让两个集合对齐咬合在一起。 准备好索引集合之后,我使用Scala 的for comprehension 首先查看待搜索字符的集 合,然后取出索引集合中的索引字符对。由于Scala 允许快捷访问集合的元素,所以我可以直接将当前搜索的字符与集合的第二个元素进行比较((if (char == pair._2))))。如果两个字符相同,那么返回索引字符对的索引部分(pair._1)。 null 的存在是Java 语言的一大混乱来源:它到底是一个有效的返回值,还是表明返 回值缺失了?包括Scala 在内的很多函数式语言通过Option 类来避免这种语义上的含混,其取值要么是表示没有返回值的None,要么是容纳了返回值的Some。因为例1-5 的需求只要求找到第一处匹配,所以我返回了结果集合的第一个元素result.head。 从原本需求的第一处匹配改为返回所有的匹配是轻而易举的事情。只要修改一下返回 类型,并去掉返回值外面的包装就可以了,修改后的代码见例1-6。 修改后的API 去掉了限制,让用户自己决定需要多少个返回值。执行firstIndexOfAny("zzabyycdxx", "by") 会得到返回值3,而indexOfAny("zzabyycdxx", "by") 的返回值则是Vector(3, 4, 5)。
函数式编程思维——1.4 简洁
书名: 函数式编程思维
作者: [美] Neal Ford
出版社: 人民邮电出版社
原作名: Functional Thinking
译者: 郭晓刚
出版年: 2015-8
页数: 162
定价: 49.00元
装帧: 平装
ISBN: 9787115400413