在Scala中使用局部函数 - 它是如何工作的?

49

我是Scala新手,正在使用2.9.1版本,尝试理解如何使用偏函数。我对柯里化函数有基本的理解,知道偏函数有点像柯里化函数,只能是2nary或类似的形式。显然,我还很不熟悉。

在某些情况下,例如XML过滤,使用偏函数会非常有利,所以我希望更好地了解如何使用它们。

我有一个使用RewriteRule结构的函数,但我需要它能够处理两个参数,而RewriteRule结构只接受一个参数,或者是偏函数。我认为这就是我认为偏函数有用的其中一种情况。

欢迎任何建议、链接、智慧之言等!

到目前为止,答案都非常好,并解决了我一些基本的误解。我认为它们也解释了我遇到的困难-我想发表一个新问题,更具体一些,希望可以帮助,所以我也会这样做。


4
我猜你在谈论部分应用函数,即通过提供arg1(arg1, arg2 => result)转化为arg2 => result。因为“偏函数”可能意味着完全不同的事情,在Scala中也使用相同的名称来表示那个意思(令人遗憾)。此外,你的问题是什么?虽然标题中有一个问号,但是你的帖子内容并不匹配。 - user395760
为了更清楚,我建议将Scala中的PartialFunction重命名为HalfFunction:)。 - ssj
2个回答

144

部分函数是一种仅对某些类型的值有效的函数。例如:

val root: PartialFunction[Double,Double] = {
  case d if (d >= 0) => math.sqrt(d)
}

scala> root.isDefinedAt(-1)
res0: Boolean = false

scala> root(3)
res1: Double = 1.7320508075688772

当你有某些东西可以检查函数是否被定义时,这将是非常有用的。例如收集:

scala> List(0.5, -0.2, 4).collect(root)   // List of _only roots which are defined_
res2: List[Double] = List(0.7071067811865476, 2.0)

这并不会帮助你将两个参数放置在你真正想要的位置。

相比之下,一个“部分应用的函数”是一个已经填充了一些参数的函数。

def add(i: Int, j: Int) = i + j
val add5 = add(_: Int,5)

现在您只需要一个参数——要加 5 的内容,而不是两个参数:

scala> add5(2)
res3: Int = 7

你可以从这个示例中了解如何使用它。

但是,如果你需要指定那两个参数,那么这个方法仍然不能实现。例如说你想使用 map,并且你需要给它一个带有一个参数的函数,但你又想让它加上两个不同的东西。那么,你可以:

val addTupled = (add _).tupled

这将部分应用函数(实际上只是从方法中创建一个函数,因为没有填充任何内容),然后将单独的参数组合成元组。现在,您可以在需要单个参数的地方使用它(假设类型正确):

scala> List((1,2), (4,5), (3,8)).map(addTupled)
res4: List[Int] = List(3, 9, 11)

相比之下,柯里化则是另一种不同的方式;它将形如(A,B) => C的函数转换为A => B => C。也就是说,对于具有多个参数的函数,它将产生一系列函数,每个函数都需要一个参数并返回一个较短的链(您可以将其视为逐个部分应用一个参数)。

val addCurried = (add _).curried

scala> List(1,4,3).map(addCurried)
res5: List[Int => Int] = List(<function1>, <function1>, <function1>)

scala> res5.head(2)   // is the first function, should add 1
res6: Int = 3

scala> res5.tail.head(5)   // Second function should add 4
res7: Int = 9

scala> res5.last(8)  // Third function should add 3
res8: Int = 11

1
很棒的解释!你应该写一本Scala的书! :-) - cfischer
你能否展示另一个使用下划线而不是.tuplied方法的例子吗?我仍然不知道它的作用。 - Jwan622
@Jwan622 - 为什么不在REPL中试试呢?只要你理解方法(例如def add(a: Int, b: Int) = a + b)和函数(例如val add: (a: Int, b: Int) => Int = (a, b) => a + b)之间的区别,我敢打赌你会弄明白它的。 - Rex Kerr

36

Rex Kerr的解释非常好 - 这也不足为奇。问题显然混淆了partial functionspartially applied functions。无论值不值得一提,当我学习Scala时我自己也犯过同样的错误。

然而,既然这个问题引起了对partial functions的关注,我想稍微谈谈它们。

许多人说,部分函数是指并非针对所有输入都定义的函数,在数学上它是正确的,但在Scala中并非如此。在Scala中,某个函数可能并非针对所有输入都有定义。实际上,由于部分函数继承自函数,因此函数也包括所有部分函数,这是不可避免的。

其他人提到了方法isDefinedAt,这确实是一个区别,但主要是关于实现的。这是如此真实,以至于Scala 2.10可能会发布一个“快速部分函数”,它不依赖于isDefinedAt

还有一些人甚至暗示部分函数的apply方法与函数的apply方法执行不同的操作,例如仅执行已定义的输入 - 这离事实相去甚远。 apply方法是完全相同的

部分函数真正涉及的是另一种方法:orElse。这比isDefinedAt更好地概括了部分函数的所有用例,因为部分函数实际上可以用于以下任一操作:

  • 链接部分函数(这就是orElse所做的),以便将输入尝试在每个部分函数中,直到其中一个匹配为止。
  • 如果部分函数不匹配,则执行不同的操作,而不是抛出异常,如果使用orElse链接该不同操作,则会发生这种情况。

我并不是说每件事都可以轻松地用orElse实现。我只是说,部分函数关注的是当它的输入没有定义时要做些其他事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接