高阶函数与 DRY
前几章介绍了 Scala 容器类型的可组合性特征。 接下来,你会发现,Scala 中的一等公民——函数也具有这一性质。
组合性产生可重用性,虽然后者是经由面向对象编程而为人熟知,但它也绝对是纯函数的固有性质。 (纯函数是指那些没有副作用且是引用透明的函数)
一个明显的例子是调用已知函数实现一个新的函数,当然,还有其他的方式来重用已知函数。 这一章会讨论函数式编程的一些基本原理。 你将会学到如何使用高阶函数,以及重用已有代码时,遵守 DRY 原则。
高阶函数
和一阶函数相比,高阶函数可以有三种形式:
- 一个或多个参数是函数,并返回一个值。
- 返回一个函数,但没有参数是函数。
- 上述两者叠加:一个或多个参数是函数,并返回一个函数。
看到这里的读者应该已经见到过第一种使用:我们调用一个方法,像 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
作为输入。
可以使用这些函数来创建 EmailFilter
:
val emailFilter: EmailFilter = notSentByAnyOf(Set("[email protected]"))
val mails = Email(
subject = "It's me again, your stalker friend!",
text = "Hello my friend! How are you?",
sender = "[email protected]",
recipient = "[email protected]") :: Nil
newMailsForUser(mails, emailFilter) // returns an empty list
这个过滤器过滤掉列表里唯一的一个元素,因为用户屏蔽了来自 [email protected]
的邮件。
可以用工厂方法创建任意的 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
都不成真。
可以使用它们来创建代表用户设置的组合 EmailFilter
:
val filter: EmailFilter = every(
notSentByAnyOf(Set("[email protected]")),
minimumSize(100),
maximumSize(10000)
)
流水线组合
再举一个函数组合的例子。回顾下上面的场景,
邮件提供者不仅想让用户可以配置邮件过滤器,还想对用户发送的邮件做一些处理。
这是一些简单的 Email => 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)。