高阶函数与 DRY

前几章介绍了 Scala 容器类型的可组合性特征。
接下来,你会发现,Scala 中的一等公民——函数也具有这一性质。

组合性产生可重用性,虽然后者是经由面向对象编程而为人熟知,但它也绝对是纯函数的固有性质。
(纯函数是指那些没有副作用且是引用透明的函数)

一个明显的例子是调用已知函数实现一个新的函数,当然,还有其他的方式来重用已知函数。
这一章会讨论函数式编程的一些基本原理。
你将会学到如何使用高阶函数,以及重用已有代码时,遵守 DRY 原则。

高阶函数

和一阶函数相比,高阶函数可以有三种形式:

  1. 一个或多个参数是函数,并返回一个值。
  2. 返回一个函数,但没有参数是函数。
  3. 上述两者叠加:一个或多个参数是函数,并返回一个函数。

看到这里的读者应该已经见到过第一种使用:我们调用一个方法,像 mapfilterflatMap ,并传递另一个函数给它。
传递给方法的函数通常是匿名函数,有时候,还涉及一些代码冗余。

这一章只关注另外两种功能:一个可以根据输入值构建新的函数,另一个可以根据现有的函数组合出新的函数。
这两种情况都能够消除代码冗余。

函数生成

你可能认为依据输入值创建新函数的能力并不是那么有用。
函数组合非常重要,但在这之前,还是先来看看如何使用可以产生新函数的函数。

假设要实现一个免费的邮件服务,用户可以设置对邮件的屏蔽。
我们用一个简单的样例类来代表邮件:

  1. case class Email(
  2. subject: String,
  3. text: String,
  4. sender: String,
  5. recipient: String
  6. )

想让用户可以自定义过滤条件,需有一个过滤函数——类型为 Email => Boolean 的谓词函数,
这个谓词函数决定某个邮件是否该被屏蔽:如果谓词成真,那这个邮件被接受,否则就被屏蔽掉。

  1. type EmailFilter = Email => Boolean
  2. def newMailsForUser(mails: Seq[Email], f: EmailFilter) = mails.filter(f)

注意,类型别名使得代码看起来更有意义。

现在,为了使用户能够配置邮件过滤器,实现了一些可以产生 EmailFilter 的工厂方法:

  1. val sentByOneOf: Set[String] => EmailFilter =
  2. senders =>
  3. email => senders.contains(email.sender)
  4. val notSentByAnyOf: Set[String] => EmailFilter =
  5. senders =>
  6. email => !senders.contains(email.sender)
  7. val minimumSize: Int => EmailFilter =
  8. n =>
  9. email => email.text.size >= n
  10. val maximumSize: Int => EmailFilter =
  11. n =>
  12. email => email.text.size <= n

这四个 vals 都是可以返回 EmailFilter 的函数,
前两个接受代表发送者的 Set[String] 作为输入,后两个接受代表邮件内容长度的 Int 作为输入。

可以使用这些函数来创建 EmailFilter

  1. val emailFilter: EmailFilter = notSentByAnyOf(Set("johndoe@example.com"))
  2. val mails = Email(
  3. subject = "It's me again, your stalker friend!",
  4. text = "Hello my friend! How are you?",
  5. sender = "johndoe@example.com",
  6. recipient = "me@example.com") :: Nil
  7. newMailsForUser(mails, emailFilter) // returns an empty list

这个过滤器过滤掉列表里唯一的一个元素,因为用户屏蔽了来自 johndoe@example.com 的邮件。
可以用工厂方法创建任意的 EmailFilter 函数,这取决于用户的需求了。

重用已有函数

当前的解决方案有两个问题。第一个是工厂方法中有重复代码。
上文提到过,函数的组合特征可以很轻易的保持 DRY 原则,既然如此,那就试着使用它吧!

对于 minimumSizemaximumSize ,我们引入一个叫做 sizeConstraint 的函数。
这个函数接受一个谓词函数,该谓词函数检查函数内容长度是否OK,邮件长度会通过参数传递给它:

  1. type SizeChecker = Int => Boolean
  2. val sizeConstraint: SizeChecker => EmailFilter =
  3. f =>
  4. email => f(email.text.size)

这样,我们就可以用 sizeConstraint 来表示 minimumSizemaximumSize 了:

  1. val minimumSize: Int => EmailFilter =
  2. n =>
  3. sizeConstraint(_ >= n)
  4. val maximumSize: Int => EmailFilter =
  5. n =>
  6. sizeConstraint(_ <= n)

函数组合

为另外两个谓词(sentByOneOfnotSentByAnyOf)介绍一个通用的高阶函数,通过它,可以用一个函数去表达另外一个函数。

这个高阶函数就是 complement ,给定一个类型为 A => Boolean 的谓词,它返回一个新函数,
这个新函数总是得出和谓词相对立的结果:

  1. def complement[A](predicate: A => Boolean) = (a: A) => !predicate(a)

现在,对于一个已有的谓词 p ,调用 complement(p) 可以得到它的补。
然而, sentByAnyOf 并不是一个谓词函数,它返回类型为 EmailFilter 的谓词。

Scala 函数的可组合能力现在就用的上了:给定两个函数 fgf.compose(g) 返回一个新函数,
调用这个新函数时,会首先调用 g ,然后应用 fg 的返回结果上。
类似的, f.andThen(g) 返回的新函数会应用 gf 的返回结果上。

知道了这些,我们就可以重写 notSentByAnyOf 了:

  1. val notSentByAnyOf = sentByOneOf andThen (g => complement(g))

上面的代码创建了一个新的函数,
这个函数首先应用 sentByOneOf 到参数 Set[String] 上,产生一个 EmailFilter 谓词,
然后,应用 complement 到这个谓词上。
使用 Scala 的下划线语法,这短代码还能更精简:

  1. val notSentByAnyOf = sentByOneOf andThen (complement(_))

读者可能已经注意到,
给定 complement 函数,也可以通过 minimumSize 来实现 maximumSize
不过,先前的实现方式更加灵活,它允许检查邮件内容的任意长度。

谓词组合

邮件过滤器的第二个问题是,当前只能传递一个 EmailFilternewMailsForUser 函数,而用户必然想设置多个标准。
所以需要可以一种可以创建组合谓词的方法,这个组合谓词可以在任意一个标准满足的情况下返回 true ,或者在都不满足时返回 false

下面的代码是一种实现方式:

  1. def any[A](predicates: (A => Boolean)*): A => Boolean =
  2. a => predicates.exists(pred => pred(a))
  3. def none[A](predicates: (A => Boolean)*) = complement(any(predicates: _*))
  4. def every[A](predicates: (A => Boolean)*) = none(predicates.view.map(complement(_)): _*)

any 函数返回的新函数会检查是否有一个谓词对于输入 a 成真。
none 返回的是 any 返回函数的补,只要存在一个成真的谓词, none 的条件就无法满足。
最后, every 利用 noneany 来判定是否每个谓词的补对于输入 a 都不成真。

可以使用它们来创建代表用户设置的组合 EmailFilter

  1. val filter: EmailFilter = every(
  2. notSentByAnyOf(Set("johndoe@example.com")),
  3. minimumSize(100),
  4. maximumSize(10000)
  5. )

流水线组合

再举一个函数组合的例子。回顾下上面的场景,
邮件提供者不仅想让用户可以配置邮件过滤器,还想对用户发送的邮件做一些处理。
这是一些简单的 Email => Email 函数,一些可能的处理函数是:

  1. val addMissingSubject = (email: Email) =>
  2. if (email.subject.isEmpty) email.copy(subject = "No subject")
  3. else email
  4. val checkSpelling = (email: Email) =>
  5. email.copy(text = email.text.replaceAll("your", "you're"))
  6. val removeInappropriateLanguage = (email: Email) =>
  7. email.copy(text = email.text.replaceAll("dynamic typing", "**CENSORED**"))
  8. val addAdvertismentToFooter = (email: Email) =>
  9. email.copy(text = email.text + "\nThis mail sent via Super Awesome Free Mail")

现在,根据老板的心情,可以按需配置邮件处理的流水线。
通过 andThen 调用实现,或者使用 Function 伴生对象上的 chain 方法:

  1. val pipeline = Function.chain(Seq(
  2. addMissingSubject,
  3. checkSpelling,
  4. removeInappropriateLanguage,
  5. addAdvertismentToFooter))

高阶函数与偏函数

这部分不会关注细节,不过,在知道了这么多通过高阶函数来组合和重用函数的方法之后,你可能想再重新看看偏函数。

链接偏函数

匿名函数那一章提到过,偏函数可以被用来创建责任链:
PartialFunction 上的 orElse 方法允许链接任意个偏函数,从而组合出一个新的偏函数。
不过,只有在一个偏函数没有为给定输入定义的时候,才会把责任传递给下一个偏函数。
从而可以做下面这样的事情:

  1. 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)