《深入理解Scala》——第1章,第1.2节当函数式编程遇见面向对象

1.2 当函数式编程遇见面向对象
深入理解Scala
函数式编程和面向对象编程是软件开发的两种不同途径。函数式编程并非什么新概念,在现代开发者的开发工具箱里也绝非是什么天外来客。我们将通过Java生态圈里的例子来展示这一点,主要来看Spring Application framework和Google Collections库。这两个库都在Java的面向对象基础上融合了函数式的概念,而如果我们把它们翻译成Scala,则会优雅得多。在深入之前,我们需要先理解面向对象编程和函数式编程这两个术语的含义。
面向对象编程是一种自顶向下的程序设计方法。用面向对象方法构造软件时,我们将代码以名词(对象)做切割,每个对象有某种形式的标识符(self/this)、行为(方法)、和状态(成员变量)。识别出名词并且定义出它们的行为后,再定义出名词之间的交互。实现交互时存在一个问题,就是这些交互必须放在其中一个对象中(而不能独立存在)。现代面向对象设计倾向于定义出“服务类”,将操作多个领域对象的方法集合放在里面。这些服务类,虽然也是对象,但通常不具有独立状态,也没有与它们所操作的对象无关的独立行为。
函数式编程方法通过组合和应用函数来构造软件。函数式编程倾向于将软件分解为其需要执行的行为或操作,而且通常采用自底向上的方法。函数式编程中的函数概念具有一定的数学上的含义,纯粹是对输入进行操作,产生结果。所有变量都被认为是不可变的。函数式编程中对不变性的强调有助于编写并发程序。函数式编程试图将副作用推迟到尽可能晚。从某种意义上说,消除副作用使得对程序进行推理(reasoning)变得较为容易。函数式编程还提供了非常强大的对事物进行抽象和组合的能力。
表1.1 面向对象和函数式编程的一般特点

  • 模式匹配
    函数式编程和面向对象编程从不同的视角看待软件。这种视角上的差异使得它们非常互补。面向对象可以处理名词而函数式编程能够处理动词。其实近年来很多Java程序员已经开始转向这一策略(分离名词和动词)。EJB规范将软件切分为用来容纳行为的Session bean和用来为系统中的名词建模的Entity bean。无状态Session bean看上去就更像是函数式代码的集合了(尽管欠缺了很多函数式代码有用的特性)。
    这种朝函数式风格方向的推动远不止EJB规范。Spring框架的模板类(Template classes)就是一种非常函数式的风格,而Google Collections库在设计上就非常的函数式。我们先来看一下这些通用的Java库,然后看看Scala的函数式和混合面向对象编程能怎样增强这些API。

1.2.1 重新发现函数式概念
很多现代API设计时都融入了函数式编程的好东西而又不称自己是函数式编程。对于Java来说,像Google Collections和Spring应用框架以Java库的形式使Java程序员也能接触到流行的函数式编程概念。Scala更进一步,将函数式编程直接融合到了语言里。我们来将流行的Spring框架中的JdbcTemplate类简单地翻译成Scala,看看它在Scala下会是什么样子。

现在,来直译一下,我们把接口转换为有相同方法的特质(trait)。

简单的直译也很有意思,不过它还是非常的Java。我们现在来深挖一下,特别看看PreparedStatementCreator和RowMapper接口。

PreparedStatementCreator接口只有一个方法。这个方法接受JDBC连接,返回PreparedStatement. RowMapper接口看上去也差不多。

Scala提供了一等函数(first-class function),利用这个特性,我们可以把JdbcTemplate查询方法改成接受函数而不是接口作为参数。这些函数应该跟接口里的基础方法有相同的签名。本例中,PreparedStatementCreator参数可以替换为一个函数,这个函数接受Connection,返回PreparedStatement. RowMapper可以替换成一个接受ResultSet和整数,返回某种对象类型的函数。更新后的Scala版本JdbcTemplate如下。

现在query方法变得更函数式了。它使用了称为租借模式(loaner pattern)的技巧。这种技巧的大意在于让一些主控的实体(controlling entity)—本例中是JdbcTemplate—由它来构造资源,然后将资源的使用委托给另一个函数。本例中有两个函数和三种资源。同时,其名字JdbcTemplate隐含的意思是它是个模板方法,其部分行为是有待用户去实现的。在纯面向对象编程中,这一点通常通过继承来做到。在较为函数式的方法中,这些行为碎片(behavioral pieces)成为了传给主控函数的参数。这样就能通过混合/匹配参数提供更多的灵活性,而无需不断地使用子类继承。
你可能会奇怪为什么我们用AnyRef作为第二个参数的返回值。Scala中的AnyRef就相当于Java里的java.lang.Object。既然Scala支持泛型,即使要编译成jvm1.4字节码,我们也应该进一步修改接口移除AnyRef,允许用户返回特定类型。

仅稍做转换,我们就创建了一个直接使用函数参数的接口。这比之前略为函数式一点,仅仅是因为Scala的函数特质允许组合。
当你读完本书的时候,你将能做出与此接口完全不同的设计。不过我们现在还是继续查看Java生态圈里的函数式设计。尤其是Google Collections API。

1.2.2 Google Collections中的函数式概念
Google Collections API给标准Java集合库增加了很多功能,主要包括一组高效的不可变数据结构和一些操作集合的函数式方法,主要是Function接口和Predicate(谓词)接口。这些接口主要用在通过Iterables和Iterators类上。我们来看下Predicate接口的使用方法。

Predicate接口非常简单。除了equals方法,它就只有一个“apply”方法。apply方法接受参数,返回true或false。Iterators/Iterables的“filter”方法用到了这个接口。filter方法接受一个集合和一个谓词作为参数,返回一个新集合,仅包含被predicate的apply方法判定为true的元素。在find方法里也用到了Predicate接口。find方法在集合中查找并返回第一个满足predicate的元素。下面列出filter和find方法签名。

另外还有个Predicates类,里面有一些用于组合断言(与和或等)的静态方法,还有一些常用的标准谓词,如“not null”等。这个简单的接口让我们可以用很简洁的代码通过组合的方式实现强大的功能。同时,因为predicate本身被传入到filter函数里面(而不是把集合传入到predicate里),filter函数可以自行决定执行断言的最佳方法或时机。比如(filter背后的集合)数据结构有可能决定采用延迟计算(lazily evaluating)断言的策略,那它可以返回原集合的一个视图(view)。它也可能决定在创建新集合的时候采用某种并行策略。 关键是这些都被抽象掉了,使得库可以随时改进而不影响用户的代码。
Predicate接口自身也很有趣,因为它看上去就像个简单的函数。这个函数接受某个类型T,返回一个布尔值,在Scala里用T => Boolean表示。我们用Scala来重写一下filter/find方法,看看它们的函数签名怎样定义。

你会立刻注意到Scala里无需显示的标注“?super T”,因为Scala的Function接口已经恰当地标注了协变(Covariance)和逆变(Contravariance)。如果某类型可以强制转换为子孙类,我们称为协变(+T或? extends T),如果某类型可以强制转换为祖先类,我们称为逆变(-T或? super T)。如果某类型完全不能被强制转换,就称为不变(Invariance)。在这个例子里,断言的参数可以在需要的时候强制转换为其祖先类型。举例来说,如果猫是哺乳动物的子类,那么一个针对哺乳动物的断言也能用于猫的集合。在Scala中,你可以在类定义的时候指定其为协变/逆变/不变。
那么在Scala里怎么组合断言呢?我们可以利用函数式组合的特性非常方便地实现一些组合功能。我们来用Scala实现一个新的Predicates模块,这个模块接受(多个)函数断言作为参数,提供它们的常用组合函数。这些组合函数的输入类型应该是T => Boolean,输入类型也是T => Boolean。初始的(组合前的)断言应该也是T => Boolean类型。

现在我们开始踏入函数式编程的领域了。我们定义了一等函数(first-class function),然后把它们组合起来提供新的功能。你应该注意到了or方法接受两个断言,f1和f2,然后产生一个匿名函数,这个函数接受参数t,然后把f1(t)和f2(t)的结果“or”一下。函数式编程也更充分地利用了泛型和类型系统的能力。Scala投入了很多心血来减少使用泛型时的困难,使泛型可以被“日常使用”。
函数式编程并不仅仅就是把函数组合起来而已。函数式编程的精髓在于尽可能地推迟副作用。上例中的Predicate对象定义了一个简单的组合机制,只是用来组合谓词(而不执行)。直到实际的谓词传递给Iterables对象后才产生副作用。这个区分很重要。我们可以用Preicate对象提供的辅助方法把简单的谓词组合成很复杂的谓词。
函数式编程给我们提供了手段来推迟程序中改变状态的部分。它提供了机制让我们构造“动词”,同时又推迟副作用。这些动词可以用更方便推理(reasoning)的方式组合起来.直到最后,这些“动词”才被应用到系统中的“名词”上。传统的函数式编程风格是要求把副作用推到越晚越好。混合式面向对象-函数式编程(OO-FP),则是一种混合式风格(the idioms merge)
接下来我们看看Scala怎么解决类型系统和富有表达力的代码之间的矛盾。

时间: 2024-08-04 13:57:45

《深入理解Scala》——第1章,第1.2节当函数式编程遇见面向对象的相关文章

《深入理解Scala》——导读

目 录 第1章 Scala--一种混合式编程语言1.1节Scala一种混合式编程语言 1.2 当函数式编程遇见面向对象 1.3 静态类型和表达力 1.4 与JVM的无缝集成 1.5 总结 第2章 核心规则 2.1 学习使用Scala交互模式(REPL)2.2 优先采用面向表达式编程 2.3 优先选择不变性2.4 用None不用null2.5 多态场景下的判等2.6 总结 第3章 来点样式-编码规范 第4章 面向对象编程 第5章 利用隐式转换写更有表达力 第6章 类型系统 第7章 隐式转换和类型系

《深入理解Scala》——第1章,第1.5节总结

1.5 总结 深入理解Scala 本章中,你学到了一些Scala的设计理念.设计Scala的初衷在于把不同语言中的多种概念融合起来.Scala融合了函数式和面向对象编程,尽管显然Java也已经这么做了.Scala精选其语法,极大地减少了语言中的繁冗之处,使一些强大的特性可以优雅地表达,比如类型推断.最后,Scala和Java能够紧密集成,而且运行在Java虚拟机上,这或许是让Scala变成一种实用选择的最重要的一点.几乎不花代价就可以把Scala用于我们的日常工作中. 因为Scala融合了多种概

《深入理解Scala》——第1章,第1.1节Scala一种混合式编程语言

第1章 Scala--一种混合式编程语言 Scala是一种将其他编程语言中的多种技巧融合为一的语言.Scala尝试跨越多种不同类型的语言,给开发者提供面向对象编程.函数式编程.富有表达力的语法.静态强类型和丰富的泛型等特性,而且全部架设于Java虚拟机之上.因此开发者使用Scala时可以继续使用原本熟悉的某种编程特性,但要发挥Scala的强大能力则需要结合使用这些有时候相互抵触的概念和特性,建立一种平衡的和谐.Scala对开发者的真正解放之处在于让开发者可以随意使用最适合手头上的问题的编程范式.

《深入理解Scala》——第1章,第1.4节与JVM的无缝集成

1.4 与JVM的无缝集成 深入理解Scala Scala的吸引力之一在于它与Java和JVM的无缝集成.Scala与Java有很强的兼容性,比如说Java类可以直接映射为Scala类.这种紧密联系使Java到Scala的迁移相当简单,但在使用Scala的一些高级特性时还是需要小心的,Scala有些高级特性是Java里没有的.在Scala语言设计时已经小心地考虑了与Java无缝交互的问题,用Java写的库,大部分可以直接照搬(as-is)到Scala里. 1.4.1 Scala调用Java 从S

《深入理解Scala》——第2章,第2.1节学习使用Scala交互模式(REPL)

第2章 核心规则深入理解Scala 本章包括的内容: • 使用Scala交互模式(Read Eval Print Loop 简称REPL) • 面向表达式编程 • 不变性(Immutability) • Option类 本章内容覆盖了每个新Scala开发者都需要知道的几个主题.本章不会深入到每个主题里,但是会讲到可以让你自己去接着探索的程度.你将学会使用REPL,学会如何利用这个工具做软件的快速原型开发.然后我们会学到面向表达式编程,并从另一个视角来看控制结构是怎么回事.在此基础上,我们来研究不

《深入理解Scala》——第1章,第1.3节静态类型和表达力

1.3 静态类型和表达力 深入理解Scala 开发人员中有一个误解,认为静态类型必然导致冗长的代码.之所以如此是因为很多继承自C的语言强制要求程序员必须在代码中多处明确地指定类型.随着软件开发技术和编译器理论的发展,情况已经改变.Scala利用了其中一些技术进步来减少样板(boilerplate)代码,保持代码简洁. Scala做了以下几个简单的设计决策,以提高代码表达力. • 把类型标注(type annotation)换到变量右边. • 类型推断. • 可扩展的语法. • 用户自定义的隐式转

《深入理解Scala》——第2章,第2.6节总结

2.6 总结 深入理解Scala 本章中我们了解了Scala编程时的第一个关键组成部分.利用REPL做快速原型是每个成功的Scala开发者必须掌握的关键技术之一.面向表达式编程和不可变性都有助于简化程序和提高代码的可推理性.Option也有助于可推理性,因为它明确声明了是否接受空值.另外,在多态的场景下实现好的判等可能不容易.以上这些实践可以帮助我们成功踏出Scala开发的第一步.要想后面的路也走得顺利,我们就必须来看一下编码规范,以及如何避免掉进Scala解析器的坑.

《深入理解Scala》——第2章,第2.2节优先采用面向表达式编程

2.2 优先采用面向表达式编程 深入理解Scala 面向表达式编程是个术语,意思是在代码中使用表达式而不用语句.表达式和语句的区别是什么?语句是可以执行的东西,表达式是可以求值的东西.在实践中这有什么意义呢?表达式返回值,语句执行代码,但是不返回值.本节我们将学习面向表达式编程的全部知识,并理解它对简化程序有什么帮助.我们也会看一下对象的可变性,以及可变性与面向表达式编程的关系. 作者注:语句VS表达式 语句是可以执行的东西,表达式是可以求值的东西. 表达式是运算结果为一个值的代码块.Scala

《深入理解Scala》——第2章,第2.3节优先选择不变性

2.3 优先选择不变性 深入理解Scala 编程中的不变性指对象一旦创建后就不再改变状态.这是函数式编程的基石之一,也是JVM上的面向对象编程的推荐实践之一.Scala也不例外,在设计上优先选择不变性,在很多场景中把不变性作为默认设置.对此,你可能一下子会不适应.本节中,我们将学到不变性对于判等问题和并发编程能提供什么帮助. Scala里首先要明白的是不变对象和不变引用(immutable referene)的区别.Scala里的所有变量都是指向对象的引用.把变量声明为val意味着它是个不变"引