Scala 局部套用与部分应用函数

我意识到这里有几个关于 什么局部套用和部分应用函数的问题,但是我想知道它们是如何不同的。作为一个简单的例子,下面是一个用于查找偶数的 curry 函数:

def filter(xs: List[Int], p: Int => Boolean): List[Int] =
if (xs.isEmpty) xs
else if (p(xs.head)) xs.head :: filter(xs.tail, p)
else filter(xs.tail, p)


def modN(n: Int)(x: Int) = ((x % n) == 0)

所以你可以写下面的代码来使用它:

val nums = List(1,2,3,4,5,6,7,8)
println(filter(nums, modN(2))

返回: List(2,4,6,8)。但是我发现我可以这样做:

def modN(n: Int, x: Int) = ((x % n) == 0)


val p = modN(2, _: Int)
println(filter(nums, p))

也返回: List(2,4,6,8)

所以我的问题是,这两者之间的主要区别是什么,什么时候你会使用一个而不是另一个?这个例子是否过于简单,无法说明为什么一个例子会被用于另一个例子?

35230 次浏览

语义差异在 普拉斯蒂 · 格罗夫给出的答案中得到了很好的解释。

不过,就功能而言,似乎没有太大区别。让我们看一些例子来验证这一点。首先,一个正常的函数:

scala> def modN(n: Int, x: Int): Boolean = ((x % n) == 0)
scala> modN(5, _ : Int)
res0: Int => Boolean = <function1>

所以我们得到一个部分应用的 <function1>,它接受一个 Int,因为我们已经给了它第一个整数。目前为止还不错。现在来讨好一下:

scala> def modNCurried(n: Int)(x: Int): Boolean = ((x % n) == 0)

使用这种表示法,您可能会天真地期望下面的代码能够正常工作:

scala> modNCurried(5)
<console>:9: error: missing arguments for method modN;
follow this method with `_' if you want to treat it as a partially applied function
modNCurried(5)

因此,多参数列表多参数列表表示法似乎并没有立即创建一个 curry 函数(假设这样做是为了避免不必要的开销) ,而是等待您明确声明希望它被 curry (这个表示法也有一些 其他优势) :

scala> modNCurried(5) _
res24: Int => Boolean = <function1>

这和我们之前得到的完全一样,所以这里没有区别,除了符号。另一个例子:

scala> modN _
res35: (Int, Int) => Boolean = <function2>


scala> modNCurried _
res36: Int => (Int => Boolean) = <function1>

这演示了部分应用“普通”函数会导致一个函数接受所有参数,而部分应用带有多个参数列表的函数会创建一个函数链 每个参数列表一个,所有函数都返回一个新函数:

scala> def foo(a:Int, b:Int)(x:Int)(y:Int): Int = a * b + x - y
scala> foo _
res42: (Int, Int) => Int => (Int => Int) = <function2>


scala> res42(5)
<console>:10: error: not enough arguments for method apply: (v1: Int, v2: Int)Int => (Int => Int) in trait Function2.
Unspecified value parameter v2.

如您所见,由于 foo的第一个参数列表有两个参数,因此 curry 链中的第一个函数有两个参数。


总而言之,部分应用的函数在功能上与局部函数并没有什么不同。这很容易验证,因为您可以将任何函数转换为咖喱函数:

scala> (modN _).curried
res45: Int => (Int => Boolean) = <function1


scala> modNCurried _
res46: Int => (Int => Boolean) = <function1>

发布经文

注意: 您的示例 println(filter(nums, modN(2))modN(2)之后不使用下划线的原因似乎是 Scala 编译器简单地假定使用下划线是为了方便程序员。


补充: 正如@asflierl 正确指出的那样,Scala 似乎不能在部分应用“正常”函数时推断出类型:

scala> modN(5, _)
<console>:9: error: missing parameter type for expanded function ((x$1) => modN(5, x$1))

鉴于该信息可用于使用多参数列表表示法编写的函数:

scala> modNCurried(5) _
res3: Int => Boolean = <function1>

这个回答 说明了这是多么的有用。

咖喱与元组有关: 将接受元组参数的函数转换为接受 n 个独立参数的函数,反之亦然。记住这一点是区分 curry 和局部应用程序的关键,即使在不完全支持 curry 的语言中也是如此。

curry :: ((a, b) -> c) -> a -> b -> c
-- curry converts a function that takes all args in a tuple
-- into one that takes separate arguments


uncurry :: (a -> b -> c) -> (a, b) -> c
-- uncurry converts a function of separate args into a function on pairs.

部分应用是 对某些参数应用函数,为其余参数生成一个新函数的能力。

如果您认为咖喱是与元组有关的转换,那么很容易记住。

在默认情况下进行 curry 处理的语言(比如 Haskell)中,区别是显而易见的——您必须实际执行某些操作来传递元组中的参数。但是大多数其他语言,包括 Scala,在默认情况下都是非 curry 的——所有的参数都以元组的形式传递,所以 curry/uncurry 不太有用,也不太明显。人们甚至认为部分应用程序和局部套用是一回事——只是因为它们不能很容易地表示局部套用函数!

多变量函数:

def modN(n: Int, x: Int) = ((x % n) == 0)

咖喱(或咖喱函数) :

def modNCurried(n: Int)(x: Int) = ((x % n) == 0)

所以它不是部分应用的函数,而是多变量函数。 与部分应用函数类似的是局部函数的调用结果,局部应用函数的参数列表与局部应用函数的参数列表相同。

只是为了澄清最后一点

补充: 正如@asflierl 所正确指出的,Scala 看起来不像是 能够推断类型时,部分应用“正常” 职能:

如果所有的参数都是通配符,Scala 可以推断出类型,但是如果有些参数被指定了,而有些参数没有被指定,Scala 就不能推断出类型。

scala> modN(_,_)
res38: (Int, Int) => Boolean = <function2>


scala> modN(1,_)
<console>:13: error: missing parameter type for expanded function ((x$1) => modN(1, x$1))
modN(1,_)
^

到目前为止,我能找到的最好的解释是: https://dzone.com/articles/difference-between-currying-amp-partially-applied

咖喱: 将具有多个参数的函数分解为一系列单参数函数。 注意,Scala 允许将一个函数作为参数传递给另一个函数。

函数的部分应用: 传递给函数的参数比它声明的参数少。当你为函数提供较少的参数时,Scala 不会抛出异常,它只是应用这些参数并返回一个包含其余需要传递的参数的新函数。