Scala新手指南中文版 -第十一篇 Currying and partially applied functions(科里化和部分应用函数)

来源:互联网 发布:淘宝七了个三什么梗 编辑:程序博客网 时间:2024/05/21 22:48

译者注:原文出处http://danielwestheide.com/blog/2013/01/30/the-neophytes-guide-to-scala-part-11-currying-and-partially-applied-functions.html,翻译:Thomas

上一篇文章是关于消除代码重复的内容,通过利用现有的函数来匹配你的新需求或者通过组合一些函数来实现。在本篇中,我们来看一下在Scala里可用于重用函数的另外两种方法:偏函数和柯里化。

部分应用函数

和所有其它函数式程序语言一样,Scala让你可以部分应用一个函数。具体含义是,当调用一个函数时,你不传递该函数所定义的所有参数,而只是提供一部分参数,让另一部分为空白。你会得到一个新函数,新函数的参数就是你调用前面函数时空白的参数。

不要将部分应用函数和偏函数混淆了,偏函数在Scala里是PartialFunction类型。

为了说明部分应用函数的工作方式,我们再来回顾一下前一篇中的例子:作为先进的免费邮箱服务商,我们想要让用户定义自己的过滤器,以便在收件箱里只显示他想好要看到的邮件,其它的则屏蔽掉。

我们的Email case class还是原样:

case class Email(  subject: String,  text: String,  sender: String,  recipient: String)type EmailFilter = Email => Boolean

用于设定邮件过滤的条件表示为Email => Boolean判定函数类型,我们给它起了个别名EmailFilter,我们可以通过调用合适的工厂函数来生成一个判定函数。

在前一篇中我们定义了两个工厂函数来生成两个判定函数检查邮件长度是否满足最小或最长长度。现在我们要用部分用用函数来实现这两个工厂函数。我们想要现有一个通用的方法叫做sizeConstraint,并且可以通过指定它的一些参数来生成更多的指定长度的限制。

下面是我们修改过的sizeConstraint方法:

type IntPairPred = (Int, Int) => Booleandef sizeConstraint(pred: IntPairPred, n: Int, email: Email) = pred(email.text.size, n)

我们还定义了一个通过检查两个整型参数来判断邮件大小是否合适的函数类型,给它设定了一个别名IntPairPred。

你应该注意到和上周的sizeConstraint不同的是,我们的函数没有返回一个EmailFilter判定函数,而是简单地检查传递给它的函数,最后返回一个Boolean。它的诀窍在于通过部分应用sizeConstraint就可以得到一个EmailFilter判定函数。

首先,既然我们严肃的遵从DRY原则,我们定义了IntPairPred的所有常用情景。这样当我们调用sizeConstraint时,我们不必重复的写同样的匿名函数,而是简单地传递它们:

val gt: IntPairPred = _ > _val ge: IntPairPred = _ >= _val lt: IntPairPred = _ < _val le: IntPairPred = _ <= _val eq: IntPairPred = _ == _

最后,我们准备好了来生成一些sizeContraint的部分应用函数,用一个IntPairPred来固化它的第一个参数:

val minimumSize: (Int, Email) => Boolean = sizeConstraint(ge, _: Int, _: Email)val maximumSize: (Int, Email) => Boolean = sizeConstraint(le, _: Int, _: Email)

正如你所看到的,你必须用一个占位符_来表示空白参数。不幸的是,你同时还不得不指定空白参数的类型,这一点让Scala下的部分应用函数显得有点啰嗦。

这其中的原因是Scala的编译器无法推测到数据类型,至少不是在所有情况下都能正确推测 -  考虑一下函数重载的情况下,编译器就无法知道你究竟在引用哪个函数。

另一方面,这让你可以绑定或空白任意的参数,例如,我们可以让第一个参数保持空缺,而给第二个参数传递一个常量:

val constr20: (IntPairPred, Email) => Boolean = sizeConstraint(_: IntPairPred, 20, _: Email)val constr30: (IntPairPred, Email) => Boolean = sizeConstraint(_: IntPairPred, 30, _: Email)

现在我们有两个函数,它们需要传递一个IntPairPred和一个Email参数,并且会分别拿邮件的长度和20及30去做比较,不过比较的逻辑还没有指定,这通过传递IntPairPred类型的参数来完成。

这一方面来说,Scala里的部分应用函数比起Clojure要灵活一些,在后者,你只能从左到右的指定参数,而不能在中间空出参数。

从方法到函数对象

当对一个方法进行部分应用时,你甚至可以决定不绑定任何参数。所返回的函数对象的参数和被部分应用的方法是一致的。你得到了一个将方法转为函数对象的有效方式:

val sizeConstraintFn: (IntPairPred, Int, Email) => Boolean = sizeConstraint _

处理EMailFilters

我们任然没有得到任何能返回EmailFIlter类型的函数 - 像sizeConstraint,minimumSize和maximumSize,它们都不返回判定函数,而是一个Boolean,这从它们的原型很容易看出来。

然而,我们的邮件过滤器也是另一种部分应用函数,通过固定minimumSize和maximumSize的整型参数,我们可以获得一个EmailFilter类型的函数:

val min20: EmailFilter = minimumSize(20, _: Email)val max20: EmailFilter = maximumSize(20, _: Email)

当然,我们亦可以对constr20函数做同样的转化:

val min20: EmailFilter = constr20(ge, _: Email)val max20: EmailFilter = constr20(le, _: Email)

为你的函数添点料

你可能会感觉Scala里的部分应用函数显得有点冗长,或者写起来和看上去都不是那么的优雅。幸运的是,你还有替代方案。

如你所知,Scala里函数可以有不止一个的参数列表。让我们来定义我们的sizeConstraint函数,让它的每个参数都有自己的参数列表:

def sizeConstraint(pred: IntPairPred)(n: Int)(email: Email): Boolean =  pred(email.text.size, n)

如果我们将它转成一个函数对象,该函数对象的原型会是这样的:

val sizeConstraintFn: IntPairPred => Int => Email => Boolean = sizeConstraint _

这样的单参数函数的链式调用被称作柯里函数,以哈斯凯尔.加里命名,他发现了这个技术并以此扬名。事实上在Haskell程序语言里,所有的函数默认都是科里形式的。

在我们的例子中,函数接收一个IntPairPred参数并且返回一个函数,这函数接收一个Int参数并且返回一个新函数。最终的函数,接受一个Email参数并返回一个Boolean。

现在,如果我们想要绑定IntPairPred,我们只需应用函数,它接收IntPairPred参数并且返回一个单参数函数:

val minSize: Int => Email => Boolean = sizeConstraint(ge)val maxSize: Int => Email => Boolean = sizeConstraint(le)

你无须为空缺参数放置任何占位符,因为我们并不在做部分应用函数。

现在我们可以像在进行部分函数应用一样来通过柯里函数得到EmailFilter:

val min20: Email => Boolean = minSize(20)val max20: Email => Boolean = maxSize(20)

当然,如果你想要一次绑定多个参数,你可以将这些步骤合并在一起完成。你省却了中间的常量:

val min20: Email => Boolean = sizeConstraintFn(ge)(20)val max20: Email => Boolean = sizeConstraintFn(le)(20)

柯里化现有函数

有时候你并不能提前判断将你的函数写成科里式是否合理 - 毕竟科里式看起来不如单参数列表的样式那样清爽。 有时候你也想要柯里化一个已有的第三方提供的函数,他们可能是单参数列表的格式。

将一个单参数列表的函数转化成柯里化的功能乃是一个高阶函数的所为 - 从现有函数生成新函数。这个转化逻辑可以通过调用函数的curried方法来实现,因此,如果我们有一个需要两个参数的函数sum,我们可以调用它的curried方法得到柯里化的版本:

val sum: (Int, Int) => Int = _ + _val sumCurried: Int => Int => Int = sum.curried
如果你需要逆转,你可以调用Function.uncurried,这样你就会得到uncurried的格式。

函数式的依赖注入

作为本章的结尾,我们来看看全局上柯里化函数能做些什么。如果你是来自企业级的Java或.NET的世界,你应该体会到了使用依赖注入容器的必要性,它替我们承担了大量的重活来为你提供对象的依赖注入。在Scala里,你不需要任何额外的工具来实现此功能,语言本身已经提供了若干特性让你以更少痛苦来实现依赖注入。

即使以纯粹的的函数式的方式来编程,你也会发现我们仍然需要注入依赖:部署在应用上层的函数需要调用其它函数。简单地在你函数中硬编码需要调用的函数会令单独测试变得困难。因而,你需要将你的上层函数所依赖的函数当成上层函数的参数来传递。

如果每次都需要传递一个同样的函数名字看起来不是那么的DRY,不是吗?柯里化函数可以帮你来优化!柯里化和部分函数应用是函数式编程中用来实现依赖注入的其中一种方式。 

下面的简短代码为我们演示了这个技术:

case class User(name: String)trait EmailRepository {  def getMails(user: User, unread: Boolean): Seq[Email]}trait FilterRepository {  def getEmailFilter(user: User): EmailFilter}trait MailboxService {  def getNewMails(emailRepo: EmailRepository)(filterRepo: FilterRepository)(user: User) =    emailRepo.getMails(user, true).filter(filterRepo.getEmailFilter(user))  val newMails: User => Seq[Email]}

MailboxService服务依赖两个不同的repository,这个依赖关系定义在getNewMails的参数中,每个repository有自己的参数列表。

MailboxService已经实现了getNewMails方法,而未定义newMails字段。该字段的类型是User=>Seq[Mail]  -- 这是依赖于MailboxService的组件将会调用的函数。

我们需要一个扩展MailboxService的对象。为的是演示通过柯里化getNewMails来实现newMails,并且通过实现EmailRepository和FilterRepository来固化它:

object MockEmailRepository extends EmailRepository {  def getMails(user: User, unread: Boolean): Seq[Email] = Nil}object MockFilterRepository extends FilterRepository {  def getEmailFilter(user: User): EmailFilter = _ => true}object MailboxServiceWithMockDeps extends MailboxService {  val newMails: (User) => Seq[Email] =    getNewMails(MockEmailRepository)(MockFilterRepository) _}

现在我们可以直接调用MailboxServiceWithMoxDeps.newMails(User("daniel"))而无需指定两个repository。当然在实际的应用中,我们基本上不会直接引用一个服务的具体实现,而是也通过注入实现。

这也许不是在Scala里最高效和可伸缩的实现依赖注入的方式,由Debasish Ghosh编写的”Dependency Injection in Scala”,这也是我第一次接触到这技术的地方。

总结

在这一篇中,我们讨论了函数式编程的两个传统技术,它们让你保持代码的无重复,并且更重要的是让你以更灵活的多种方式实现函数重用。部分函数应用和柯里化多少有差不多的功效,不过有写场景下其中某一个可能会是更好地解决方案。

在下一篇中,我们会继续来讨论保持灵活性的其它方式,涉足Scala里的type class是什么及如何使用它。

0 0