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

简介:

本节书摘来自异步社区《深入理解Scala》一书中的第1章,第1.2节Scala的当函数式编程遇见面向对象,作者[美]Josh Suereth,更多章节内容可以访问云栖社区“异步社区”公众号查看

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


b9dd968af2dc304787803498243f2235225b1f18
  • 模式匹配
    函数式编程和面向对象编程从不同的视角看待软件。这种视角上的差异使得它们非常互补。面向对象可以处理名词而函数式编程能够处理动词。其实近年来很多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下会是什么样子。


26d7aac98850ea074a0cea9b05f27624e770086b

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


e242c5305d3472c5e3c05e25cc94aa60f91472f8

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


3f415feaf44d326cadea470dc3b9294a1bdab9eb

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


2031ecb553e5a9d8fb938a3758e1cd9f70a91adb

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


4662388031fe1359d135a74e418f4f54c5b31846

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


ed97b0647aee17f9236d8c4c712b60395c9ece65

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

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


974210f9ddf1b62be5d5012d218c7ad086e1cd95

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

f629c64e0bb65126e9d1201b6301b3b49e08589a

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


2f9c42b2bbbf9ede395703514ca7294185d1877c

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


52fce83a9b59fa0a32b1f1c7e7b314297fc8d1ed

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

相关文章
|
4月前
|
设计模式 Java Scala
Scala 面向对象【中】
Scala 面向对象【中】
|
4月前
|
Java Scala Python
Scala面向对象【上】
Scala面向对象【上】
|
4月前
|
消息中间件 分布式计算 Java
Scala函数式编程【从基础到高级】
Scala函数式编程【从基础到高级】
|
2月前
|
消息中间件 分布式计算 大数据
Scala学习--day03--函数式编程
Scala学习--day03--函数式编程
63 2
|
4月前
|
数据采集 监控 安全
通过Scala实现局域网监控上网记录分析:函数式编程的优雅之路
在当今数字时代,网络监控成为保障信息安全的必要手段之一。本文将介绍如何使用Scala编程语言实现局域网监控上网记录分析的功能,重点探讨函数式编程的优雅之路。通过一系列代码示例,我们将展示如何利用Scala的函数式特性和强大的语法来实现高效的监控和分析系统。
218 1
|
4月前
|
分布式计算 Java Scala
Scala:面向对象、Object、抽象类、内部类、特质Trait(二)
Scala:面向对象、Object、抽象类、内部类、特质Trait(二)
62 0
|
4月前
|
大数据 Scala
Scala面向对象练习题34道
Scala面向对象练习题34道
42 0
|
4月前
|
Java Scala
Scala面向对象【下】
Scala面向对象【下】
|
7月前
|
Java Scala
Scala面向对象4
Scala面向对象4
27 0
|
7月前
|
Scala
Scala面向对象3
Scala面向对象3
24 1