读书笔记——

Functional Programming Patterns in Scala and Clojure (Write Lean Programs for the JVM)

作者:[美] Michael Bevilacqua-Linn 著 (赵震一 译)

第1章 模式和函数式编程

模式和函数式编程可以通过两种方式结合在一起。

  • 对于很多面向对象的设计模式来说,采用函数式编程来实现更简单。
    • 函数式语言为我们提供了更加简单的方式来完成一些计算的传递,而无需创建新的类。
    • 使用表达式(expression)而非语句(statement)可以让我们消除那些额外的变量。
    • 声明性是很多函数式解决方案所拥有的一个特质,这种特质可以让我们在一行代码中完成原本在命令式语言中需要五行代码才能完成的工作。我们甚至可以用函数式语言特性的简单应用来替代某些面向对象模式。
  • 函数式的世界也有一套它自己的有用的模式。这些模式专注于编写避免可变性且偏好声明性风格的代码,可以帮助我们编写出更加简单且更易维护的代码。

1.1 什么是函数式编程

函数式编程特征:

  • 拥有头等(first-class)函数:头等函数是指那些可以被传递、动态创建并可以存储于数据结构中的函数。
  • 偏好纯函数(pure function):纯函数是指那些没有副作用的函数。副作用是指函数的某种行为,这种行为会对函数之外的状态进行修改。
  • 组合函数:函数式编程支持通过将函数进行组合来自底向上地构建程序。
  • 使用表达式:函数式编程偏爱表达式胜于语句。表达式会产生值,而语句则不然,它的存在仅仅是为了控制程序的执行流程。
  • 使用不变性:因为函数式编程偏好纯函数,而纯函数不会修改数据,它同时又大量使用了不可变数据。所以程序不会去修改一个已经存在的数据结构,而是有效地创建一份心数据。
  • 转换数据而非修改数据:函数式编程使用函数来转换数据。

1.2 模式词汇表

第2章 TinyWeb:让模式协同工作

2.1 TinyWeb简介

2.2 采用Java来编写TinyWeb

HttpResponse testResponse = HttpResponse.Builder.newBuilder()
    .responseCode(200)
    .body("responseBody")
    .build();

不可变性:不只是函数式程序员的专享

2.3 采用Scala来编写TinyWeb

2.4 采用Clojure来编写TinyWeb

第3章 替代面向对象模式

模式1 替代函数式接口

目的:将一些程序逻辑进行封装,以支持对这些程序逻辑的传递,以及将其存储于数据结构中,通常还可以将这段封装后的程序逻辑作为任何其他头等的程序构造元素来处理。

在函数式语言中,函数都是高阶的:它们可以作为其他函数的结果返回,也可以作为其他函数的入参。

高阶函数相对于函数式接口的一个优势:你不需要为每一种函数式接口都定义新的类型,因为现有的函数类型就可以满足你的需要。

模式2 替代承载状态的函数式接口

目的:将一些状态与程序逻辑封装到一起,以支持对这些程序逻辑的传递,以及将其存储于数据结构之中,通常还可以将这段封装后的程序逻辑作为任何其他头等的程序构造元素来处理。

凭借着JSR 335(即Lambda表达式)的旗号,闭包和高阶函数成为了即将到来的Java 8中的主要功能特性之一。

模式3 替代命令模式

目的:将方法调用转变成一个对象,并在集中的地方运行该对象,以保持对调用的跟踪,从而方便我们对调用进行撤销、记录以及重做。

  • Command类本身是一个通常承载了状态的函数式接口,所以可以用闭包来替代Command类。
  • 用一个简单的函数来替代调用者执行命令,我们称它为执行函数。
  • 最后,将创建一个函数生成器,它负责创建我们的命令,让创建过程变得简单且一致。

模式4 替代生成器模式来获得不可变对象

目的:创建不可变对象时,我们通常会采用一种友好的语法来为对象设置属性——因为我们无法在创建完成之后再对其进行修改。同时,我们也需要一种简单的方式基于现有对象来创建新的对象,并为新对象的一些属性设置新值。

模式5:替代迭代器模式

目的:在无需对序列中的元素进行索引的情况下,有序地对元素进行迭代访问。

模式6:替代模板方法模式

目的:指定算法的大致轮廓,并让调用者完成对某些细节的插入。

替代方案:不再使用类来实现子步骤方法,转而采用高阶函数;同时,也不再依赖于子类继承的方式,而是依赖于函数的组合。我们将把所有的子操作传入一个函数生成器(Function Builder),该生成器会返回一个执行所有操作的新函数。

模板方法模式的函数式替代方案实现了与原有模式一样的目的,但是它们在操作上有些不同。我们不再使用子类型来实现特定的子操作,而是选择使用函数式的组合和高阶函数。

模式7:替代策略模式

目的:以抽象的形式来定义算法,使其可以由不同的方式来实现。同时允许将算法注入到客户端,便于被多个不同的客户端所使用。

策略模式和模板方法模式都服务于相似的目的。它们都可以向一个较大规模的框架或算法注入一些自定义代码。不同的是策略模式采用了组合,而模板方法模式采用的是继承。而我们都是基于函数式组合来代替了这两种模式。

模式8 替代空对象

目的:为了避免将空检查的逻辑散落在代码中,我们将专门用于处理null引用的措施封装进了一个起代理作用的空对象中。

模式9 替代装饰器模式

目的:将行为添加到单独的对象上,而不是对象所属的整个类上,让我们可以对一个既有的类的行为进行更改。

函数式替代方案:装饰器模式的本质是采用一个新的类来对既有类进行包装,以便该新的类可以对既有类的行为进行调整。在函数式的世界里,一种简单的替代方案就是创建一个高阶函数,该高阶函数以既有的函数作为入参,然后返回一个新的经过包装的函数。

模式10 替代访问者模式

目的:以某种方式对在某个数据结构上执行的行为进行封装,进而在不修改原有数据结构的情况下为该数据结构添加新的操作。

访问者模式:访问者模式打破了原有的面向对象的约束,即容易为数据类型添加新的实现,却难以为其添加新的操作。

模式11 替代依赖注入

目的:采用外部的配置或代码来组合对象,而不是让对象自行初始化其依赖——这样做让我们为对象注入不同的依赖实现变得非常简单,并为我们理解给定对象有哪些依赖提供了一个集中的管理场所。

函数式替代方案:当我们以更具函数式的风格进行编程时,对此类依赖注入模式的需求将不会这么强烈。函数式编程本身就包含对函数的组合能力。

第4章 函数式模式

函数式编程也拥有一套属于它自己的模式,这些模式是从函数式风格演进而来的。

这些模式很大程度上依赖不可变性。

在这些模式中普遍存在的另一个主题是”将高阶函数作为组合的基本单元”。该主题与第一个主题十分吻合,即不可变性和对不可变数据的转换。

模式12 尾递归模式

目的:在不使用可变状态且没有栈溢出的情况下完成对某个计算的重复执行。

迭代是一个需要可变状态的命令式风格的技术。

尾递归相对迭代的主要优势就在于它消除了语言中的可变性来源。尾递归胜于迭代也有两个次要的原因:

  • 首先,尾递归可以节省一个额外的索引变量。
  • 尾递归使得在计算中需要操作什么数据结构以及会生成什么数据结构变得更加明确,因为它们都会作为参数在整个调用链中传递。

高阶函数优于尾递归

模式13 相互递归模式

目的:采用相互递归函数来表达具体的算法,包括对树形数据结构的遍历,递归下降分析和状态机操作等。

模式14 Filter-Map-Reduce模式

目的:采用filter, map和reduce函数,以声明性的方式来操作某个序列(列表、向量等),并最终产生一个新的序列——这是一种可在较高层次完成多种序列操作的强大方式。如果不采用这种方式,那么你需要编写相当冗长的代码来完成同样的工作。

Filter-Map-Reduce模式依赖于声明性的数据操作,这种方式的抽象层次较迭代方案更高,而且通常比显示的递归方式也更高。

模式15 操作链模式

目的:将一个计算序列串联成链式调用,让我们在无需存储大量临时结果的情况下,得以干净利落地处理不可变数据。

模式16 函数生成器模式

目的:创建可以生成函数的函数,从而让我们可以动态地合成行为。

  • 有一些时候,我们手头拥有一些数据,并需要将其转换成某个行为。
  • 通过函数生成器,可以编写一个以现有数据或函数作为入参的函数,并使用它来创建一个新的函数。

函数组成接受多个函数并将它们串连到一起,而部分应用函数则是接受一个函数和该函数入参的一个子集,然后返回一个新的函数。

模式17 记忆模式

目的:对纯函数的调用结果进行缓存,从而避免重复执行相同的计算。

由于在给定参数不变的情况下,纯函数始终会返回相同的值,所以我们可以采用缓存的调用结果来替代重复的纯函数调用。

记忆模式的另一个用途:解决动态规划问题。

模式18 惰性序列模式

目的:创建一个序列,该序列的成员仅在需要时才会得到计算——这让我们可以简单地将计算结果转化为流的形式,从而处理无限长的序列。

惰性序列只在被要求时才会在序列中创建一个元素。

当你使用惰性序列的时候,有一件事情值得注意:在你并非刻意的情况下,可能会凑巧保持了对该序列头部的引用。保持对一个惰性序列的头部引用将会使整个序列一直保持在内存之中。

模式19 集中的可变性

目的:在一个程序中,我们会在那些对性能敏感的小部分代码中使用可变的数据结构,同时会将这些部分隐藏在一个函数之中,而在大多数代码中,仍将使用不可变数据。

集中的可变性提示了如何通过创建函数在某些场景下使用可变数据,这些函数往往以一些不可变的数据结构作为入参,并在函数中完成对可变数据的操作,然后返回另一个不可变的数据结构。

在使用集中的可变性这一模式时有一个因素值得我们思考,即将一个可变的数据结构转型成不可变数据结构的过程中需要花费多少开销。暂态(transient)让我们讲一个不可变的数据结构在常量时间内转换成一个可变的数据结构,并在我们完成对它的处理之后同样在常量时间内将它转换回不可变的数据结构。

模式20 自定义控制流

目的:创建集中的自定义控制流抽象。

通过使用正确的控制流抽象来完成任务,可以帮助我们编写出更加整洁的代码。

模式21 领域特定语言

目的:创建一门专门用于解决某个特定问题的小型编程语言。

领域特定语言是一个非常常见的模式,它分为两个比较宽泛的大类:外部DSL和内部DSL。

外部DSL是一门成熟的编程语言,它拥有自己的语法和编译器。它的目标并不在于通用;相反,它通常只解决某些有针对性的问题。如:SQL、ANTLR等。

另一方面,我们也拥有一些内部的DSL,通常称为”内嵌语言”(embedded language)。反观该模式的一些例子,往往构建在某些通用语言之上,并且其存在形式也受限于宿主语言的语法约束。

对于这两类DSL而言,它们的目的是一样的。我们正在尝试创建这样一门语言,以一种更贴近领域的方式来表达处理问题的解决方案。相比那些通用的语言,采用DSL可以以更少的代码来表达更清晰的解决方案。它同样还能让那些非软件开发者参与到某些领域问题的解决方案中来。

第5章 结束语

  • 函数式编程工具是如何帮助我们编写更加简短且更加清晰的代码
  • 不可变数据是如何帮助我们从程序中消除掉错误的主要来源