高阶函数与 DRY
最后更新于:2022-04-01 02:13:39
前几章介绍了 Scala 容器类型的可组合性特征。接下来,你会发现,Scala 中的一等公民——函数也具有这一性质。
组合性产生可重用性,虽然后者是经由面向对象编程而为人熟知,但它也绝对是纯函数的固有性质。(纯函数是指那些没有副作用且是引用透明的函数)
一个明显的例子是调用已知函数实现一个新的函数,当然,还有其他的方式来重用已知函数。这一章会讨论函数式编程的一些基本原理。你将会学到如何使用高阶函数,以及重用已有代码时,遵守 [DRY](http://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 原则。
### 高阶函数
和一阶函数相比,高阶函数可以有三种形式:
1. 一个或多个参数是函数,并返回一个值。
1. 返回一个函数,但没有参数是函数。
1. 上述两者叠加:一个或多个参数是函数,并返回一个函数。
看到这里的读者应该已经见到过第一种使用:我们调用一个方法,像 `map` 、 `filter` 、 `flatMap` ,并传递另一个函数给它。传递给方法的函数通常是匿名函数,有时候,还涉及一些代码冗余。
这一章只关注另外两种功能:一个可以根据输入值构建新的函数,另一个可以根据现有的函数组合出新的函数。这两种情况都能够消除代码冗余。
### 函数生成
你可能认为依据输入值创建新函数的能力并不是那么有用。函数组合非常重要,但在这之前,还是先来看看如何使用可以产生新函数的函数。
假设要实现一个免费的邮件服务,用户可以设置对邮件的屏蔽。我们用一个简单的样例类来代表邮件:
~~~
case class Email(
subject: String,
text: String,
sender: String,
recipient: String
)
~~~
想让用户可以自定义过滤条件,需有一个过滤函数——类型为 `Email => Boolean` 的谓词函数,这个谓词函数决定某个邮件是否该被屏蔽:如果谓词成真,那这个邮件被接受,否则就被屏蔽掉。
~~~
type EmailFilter = Email => Boolean
def newMailsForUser(mails: Seq[Email], f: EmailFilter) = mails.filter(f)
~~~
注意,类型别名使得代码看起来更有意义。
现在,为了使用户能够配置邮件过滤器,实现了一些可以产生 `EmailFilter` 的工厂方法:
~~~
val sentByOneOf: Set[String] => EmailFilter =
senders =>
email => senders.contains(email.sender)
val notSentByAnyOf: Set[String] => EmailFilter =
senders =>
email => !senders.contains(email.sender)
val minimumSize: Int => EmailFilter =
n =>
email => email.text.size >= n
val maximumSize: Int => EmailFilter =
n =>
email => email.text.size <= n
~~~
这四个 _vals_ 都是可以返回 `EmailFilter` 的函数,前两个接受代表发送者的 `Set[String]` 作为输入,后两个接受代表邮件内容长度的 `Int` 作为输入。
可以使用这些函数来创建 `EmialFilter` :
~~~
val emailFilter: EmailFilter = notSentByAnyOf(Set("johndoe@example.com"))
val mails = Email(
subject = "It's me again, your stalker friend!",
text = "Hello my friend! How are you?",
sender = "johndoe@example.com",
recipient = "me@example.com") :: Nil
newMailsForUser(mails, emailFilter) // returns an empty list
~~~
这个过滤器过滤掉列表里唯一的一个元素,因为用户屏蔽了来自 `johndoe@example.com` 的邮件。可以用工厂方法创建任意的 `EmailFilter` 函数,这取决于用户的需求了。
### 重用已有函数
当前的解决方案有两个问题。第一个是工厂方法中有重复代码。上文提到过,函数的组合特征可以很轻易的保持 DRY 原则,既然如此,那就试着使用它吧!
对于 `minimumSize` 和 `maximumSize` ,我们引入一个叫做 `sizeConstraint` 的函数。这个函数接受一个谓词函数,该谓词函数检查函数内容长度是否OK,邮件长度会通过参数传递给它:
~~~
type SizeChecker = Int => Boolean
val sizeConstraint: SizeChecker => EmailFilter =
f =>
email => f(email.text.size)
~~~
这样,我们就可以用 `sizeConstraint` 来表示 `minimumSize` 和 `maximumSize` 了:
~~~
val minimumSize: Int => EmailFilter =
n =>
sizeConstraint(_ >= n)
val maximumSize: Int => EmailFilter =
n =>
sizeConstraint(_ <= n)
~~~
### 函数组合
为另外两个谓词(`sentByOneOf`、 `notSentByAnyOf`)介绍一个通用的高阶函数,通过它,可以用一个函数去表达另外一个函数。
这个高阶函数就是 `complement` ,给定一个类型为 `A => Boolean` 的谓词,它返回一个新函数,这个新函数总是得出和谓词相对立的结果:
~~~
def complement[A](predicate: A => Boolean) = (a: A) => !predicate(a)
~~~
现在,对于一个已有的谓词 `p` ,调用 `complement(p)` 可以得到它的补。然而, `sentByAnyOf` 并不是一个谓词函数,它返回类型为 `EmailFilter` 的谓词。
Scala 函数的可组合能力现在就用的上了:给定两个函数 `f` 、 `g` , `f.compose(g)` 返回一个新函数,调用这个新函数时,会首先调用 `g` ,然后应用 `f` 到 `g` 的返回结果上。类似的, `f.andThen(g)` 返回的新函数会应用 `g` 到 `f` 的返回结果上。
知道了这些,我们就可以重写 `notSentByAnyOf` 了:
~~~
val notSentByAnyOf = sentByOneOf andThen (g => complement(g))
~~~
上面的代码创建了一个新的函数,这个函数首先应用 `sentByOneOf` 到参数 `Set[String]` 上,产生一个 `EmailFilter` 谓词,然后,应用 `complement` 到这个谓词上。使用 Scala 的下划线语法,这短代码还能更精简:
~~~
val notSentByAnyOf = sentByOneOf andThen (complement(_))
~~~
读者可能已经注意到,给定 `complement` 函数,也可以通过 `minimumSize` 来实现 `maximumSize` 。不过,先前的实现方式更加灵活,它允许检查邮件内容的任意长度。谓
#### 谓词组合
邮件过滤器的第二个问题是,当前只能传递一个 `EmailFilter` 给 `newMailsForUser` 函数,而用户必然想设置多个标准。所以需要可以一种可以创建组合谓词的方法,这个组合谓词可以在任意一个标准满足的情况下返回 `true` ,或者在都不满足时返回 `false` 。
下面的代码是一种实现方式:
~~~
def any[A](predicates: (A => Boolean)*): A => Boolean =
a => predicates.exists(pred => pred(a))
def none[A](predicates: (A => Boolean)*) = complement(any(predicates: _*))
def every[A](predicates: (A => Boolean)*) = none(predicates.view.map(complement(_)): _*)
~~~
`any` 函数返回的新函数会检查是否有一个谓词对于输入 `a` 成真。`none` 返回的是 `any` 返回函数的补,只要存在一个成真的谓词, `none` 的条件就无法满足。最后, `every` 利用 `none` 和 `any` 来判定是否每个谓词的补对于输入 `a` 都不成真。
可以使用它们来创建代表用户设置的组合 `EmialFilter` :
~~~
val filter: EmailFilter = every(
notSentByAnyOf(Set("johndoe@example.com")),
minimumSize(100),
maximumSize(10000)
)
~~~
#### 流水线组合
再举一个函数组合的例子。回顾下上面的场景,邮件提供者不仅想让用户可以配置邮件过滤器,还想对用户发送的邮件做一些处理。这是一些简单的 `Emial => Email` 函数,一些可能的处理函数是:
~~~
val addMissingSubject = (email: Email) =>
if (email.subject.isEmpty) email.copy(subject = "No subject")
else email
val checkSpelling = (email: Email) =>
email.copy(text = email.text.replaceAll("your", "you're"))
val removeInappropriateLanguage = (email: Email) =>
email.copy(text = email.text.replaceAll("dynamic typing", "**CENSORED**"))
val addAdvertismentToFooter = (email: Email) =>
email.copy(text = email.text + "\nThis mail sent via Super Awesome Free Mail")
~~~
现在,根据老板的心情,可以按需配置邮件处理的流水线。通过 `andThen` 调用实现,或者使用 Function 伴生对象上的 `chain` 方法:
~~~
val pipeline = Function.chain(Seq(
addMissingSubject,
checkSpelling,
removeInappropriateLanguage,
addAdvertismentToFooter))
~~~
### 高阶函数与偏函数
这部分不会关注细节,不过,在知道了这么多通过高阶函数来组合和重用函数的方法之后,你可能想再重新看看偏函数。
#### 链接偏函数
匿名函数那一章提到过,偏函数可以被用来创建责任链:`PartialFunction` 上的 `orElse` 方法允许链接任意个偏函数,从而组合出一个新的偏函数。不过,只有在一个偏函数没有为给定输入定义的时候,才会把责任传递给下一个偏函数。从而可以做下面这样的事情:
~~~
val handler = fooHandler orElse barHandler orElse bazHandler
~~~
#### 再看偏函数
有时候,偏函数并不合适。仔细想想,一个函数没有为所有的输入值定义操作,这样的事实还可以用一个返回 `Option[A]` 的标准函数代替:如果函数为一个输入定义了操作,那就返回 `Some[A]` ,否则返回 `None` 。
要这么做的话,可以在给定的偏函数 `pf` 上调用 `lift` 方法得到一个普通的函数,这个函数返回 `Option` 。反过来,如果有一个返回 `Option` 的普通函数 `f` ,也可以调用 `Function.unlift(f)` 来得到一个偏函数。总
### 总结
这一章给出了高阶函数的使用,利用它可以在一个新的环境里重用已有函数,并用灵活的方式去组合它们。在所举的例子中,就代码行数而言,可能看不出太多价值,这些例子都很简单,只是为了说明而已,在架构层面,组合和重用函数是有很大帮助的。
下一章,我们继续探索函数组合的方式:_函数部分应用和柯里化(Partial Function Application and Currying)_。