Scala新手指南中文版 -第十篇 Staying DRY with higher-order functions(用高阶函数来消除重复代码)

本文通过实例演示如何利用Scala中的高阶函数实现代码重用及灵活组合,包括基于输入生成新函数、组合现有函数以及组合判定函数等技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

译者注:原文出处http://danielwestheide.com/blog/2013/01/23/the-neophytes-guide-to-scala-part-10-staying-dry-with-higher-order-functions.html,翻译:Thomas

 

在前面几篇文章里,我讨论了Scala容器类型的组合功能。你会发现,不仅在Future,Try以及其他容器类型中可以享用到可组合的好处,在函数中你也会看到组合特性,谁让函数是Scala的一等公民呢。

可组合性天然的提供了可重用性,虽然后者往往是面向对象编程所鼓吹的一大优势,这实际上是纯函数的天然特性,换句话说,不带任何副作用的函数是对调用者完全透明的。

一个显而易见的重用的方式是在一个函数体内调用另一个已存在的函数。当然还有其它重用函数的方式:在本博客中,我将会讨论一些到目前为止忽视了的函数式编程的基本规则。你会学到运用高阶函数来重用现有代码以遵循DRY法则。

关于高阶函数

高阶函数是一阶函数的对立面,有三种形式:

  1. 有一个或多个函数作为参数,返回一些值。
  2. 参数都不是函数,但是返回一个函数。
  3. 以上两种的组合:一个或多个函数作为参数,并且返回一个函数。

如果你是按顺序阅读本系列文章的,你实际上已经多次见到第一种形式的高阶函数:我们调用mapfilter,或者 flatMap这些方法时传递给它们一个函数用来以某种方式转换或过滤一个集合。通常我们传递过去的是一个匿名函数,有时候会导致一点重复。

在这篇文章中,我们专注于高阶函数的后两种形式,看它们可以如何为我所用:

第一个让我们基于输入参数生成新函数,后一个给我们提供了基于现有函数组合出新函数的灵活性。

呼的一下,一个函数出生了

你也许会觉得能够基于输入来生成函数并没有什么了不起的用途。那就让我们先来看看如何基于现有函数来构造一个新函数,我们先来看看如何来使用这种基于现有函数生成新函数的函数(绕口令啊)。

假设我们我们要实现一个免费的邮箱服务,用户可以自定义邮件屏蔽规则。我们用一个简单地case class来表示邮件:

 

case class Email(
  subject: String,
  text: String,
  sender: String,
  recipient: String)

 

我们要让用户基于指定的条件来过滤邮件,所以我们需要一个类型为Email => Boolean的过滤函数来做判断是否应该屏蔽某邮件。如果判定为true,邮件是可接受的,否则将被屏蔽:

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,并传递给newMailsForUser函数:

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

这过滤器过滤掉了列表中的一封邮件,因为用户决定将该发件者放入黑名单。我们可以根据用户的要求利用四个工厂函数生成任意的EmailFilter。

重用现有函数

目前的方案有两个问题。首先,工厂函数里存在一些重复,文章一开始的时候我说了函数的可组合特性会让让我们容易遵从DRY原则,让我们来消除重复吧。

为了将minimumSize和maximumSize里的重复消除掉,我们需要新建一个叫做sizeConstraint的函数,这函数需要传入一个判断条件来检查邮件的长度是否合适,sizeConstraint函数里会将邮件的长度传递给判断条件来执行:

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)

函数的组合

对另外两个判定函数sentByOneOfnotSentByAnyOf,我们打算引入一个非常通用的高阶函数,让我们可以通过其中一个判定函数来定义另一个判定函数。

我们来实现函数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函数,然后将g函数的结果传递给f函数。类似的,f.andThen(g)返回的新函数被调用时,将会f的返回传递个g函数。

我们可以用这些函数来生成notSentByAnyOf判定函数且不产生重复代码:

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

上述代码的意思是我们要求生成一个新的函数,这函数首先会将传递给它的参数(Set[String]类型)拿来调用sentByOneOf函数,然后将sentByOneOf所返回的EmailFilter判定函数再作为参数来调用complement(complement的返回类型是A=>Boolean,在上面代码里,A就是Email,因此返回类型就是EmailFilter)。利用Scala的匿名函数占位符语法,我们还可以写得更精简一些:

val notSentByAnyOf = sentByOneOf andThen(complement(_))
//译者注:甚至更精简:
//val notSentByAnyOf = sentByOneOf andThen complement

你可能也会注意到,通过complement函数,你也可以通过组合minimumSize来生成maximumSize,而不是借助sizeConstraint函数,不过,后一种方式更具灵活性,让你可以对邮件尺寸指定任意检查。

组合判定函数

我们实现的邮件过滤的另一个问题就是我们目前只能传递单个EmailFilter给newMailsForUser函数。我们的用户当然希望可以定义多个过滤条件。我们需要某种方法能够将多个判定函数组合起来,当这些函数中得任何一个或者所有或者没有一个能够满足时,函数组合就会返回true。

下面是实现这函数的一种方法:

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参数,检查是否a满足其中一个判定函数的(让其返回true)。

none函数简单地返回any函数返回的判定条件的反函数- 如果任意判定条件得到满足(返回true),则none生成的函数将不被满足。最后,every函数通过返回none函数返回的判定条件的函数,保证所有判定条件都为true时才满足。

(译者注:上面的三个函数可以通过逻辑运算规律来理解,any好比逻辑非表达式:A+B+C,none则为any的反函数)

我们现在可以来生成一个组合的EmailFilter来代表用户期望的过滤条件:

val filter: EmailFilter = every(
    notSentByAnyOf(Set("johndoe@example.com")),
    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伙伴对象里的chian方法来组合,效果是一样的:

val pipeline = Function.chain(Seq(
  addMissingSubject,
  checkSpelling,
  removeInappropriateLanguage,
  addAdvertismentToFooter))

高阶函数和偏函数

这个话题我不会太深入去讲,不过现在既然你已经知道了如何对高阶函数进行组合和复用,你也许想再看看偏函数是否有类似方法。

串联偏函数

模式匹配匿名函数一篇中,我提到过偏函数可以用来生成责任链模式的优秀的替代方案:PartialFunction中的orElse方法让你可以串联任意数量的偏函数,以此来生成一个复合偏函数。只有当第一个偏函数没有为输入值定义case时才传递到下一个函数。因此,你可以如同下面的方式来做:

val handler = fooHandler orElse barHandler orElse bazHandler
提升偏函数

有时候PartialFunction并不是你想要的。如果你是这样认为的,这里有另一种途径来表示一个函数没有为输入类型的所有可能进行处理,那就是返回Option[A]的标准函数 - 如果一个输入值没有在函数中定义,他就返回None,否则返回Some[A]。

如果这是你在某种场景下所想要的,设定一个PartialFunction叫做pf,你可以调用pf.lift来获得相应的返回Option的标准函数。如果你手头上有的是后者(标准函数)而你想要一个偏函数,可以调用 Function.unlift(f).

总结

在这一篇,我们见识了高阶函数的价值,它让你可以在新的,未预见的场景中重用现成代码,并且可以非常灵活的组合。虽然在例子中,由于函数实在是太简单了,我们并没有减少多少代码行,我们重点是想要说明灵活性的增加。同样,组合和重用函数的价值不仅用于小函数,同样用于架构级。

在下一篇,我们将继续探讨对偏函数和柯里化函数来进行组合。

作者:Daniel Westheide,2013/1/23

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值