虽然我理解什么是部分应用/柯里化函数,但我仍然不完全理解为什么我会使用这样的函数而不是简单地重载函数。 IE。给定:
def add(a: Int, b: Int): Int = a + b
val addV = (a: Int, b: Int) => a + b
两者之间的实际区别是什么
def addOne(b: Int): Int = add(1, b)
和
def addOnePA = add(1, _:Int)
// or currying
val addOneC = addV.curried(1)
请注意,我不是在询问柯里化与部分应用函数,因为之前已经有人问过这个问题,并且我已经阅读了答案。我问的是柯里化/部分应用函数 VS 重载函数
您可能更喜欢部分应用函数的原因有几个。最明显也可能是最肤浅的一点是,您不必编写中间函数,例如
addOnePA
。
List(1, 2, 3, 4) map (_ + 3) // List(4, 5, 6, 7)
比
好def add3(x: Int): Int = x + 3
List(1, 2, 3, 4) map add3
相比之下,即使是匿名函数方法(下划线最终由编译器扩展到)也感觉有点笨拙。
List(1, 2, 3, 4) map (x => x + 3)
从表面上看,当您真正将函数作为一流值传递时,部分应用程序会派上用场。
val fs = List[(Int, Int) => Int](_ + _, _ * _, _ / _)
val on3 = fs map (f => f(_, 3)) // partial application
val allTogether = on3.foldLeft{identity[Int] _}{_ compose _}
allTogether(6) // (6 / 3) * 3 + 3 = 9
想象一下,如果我没有告诉你
fs
中的功能是什么。提出命名函数等效项而不是部分应用的技巧变得更难使用。
对于柯里化,柯里化函数通常可以让您自然地表达产生其他函数的函数的转换(而不是在最后简单地产生非函数值的高阶函数),否则可能不太清楚。
例如,
def integrate(f: BigDecimal => BigDecimal, delta: BigDecimal = 0.01)(x: BigDecimal): BigDecimal = {
val domain = Range.BigDecimal(0, x, delta)
domain.foldLeft(0: BigDecimal){ case (acc, a) => delta * f(a) + acc }
}
可以按照您实际学习微积分中的积分的方式来思考和使用,即作为一个函数的转换来产生另一个函数。
def square(x: BigDecimal): BigDecimal = x * x
// Ignoring issues of numerical stability for the moment...
// The underscore is really just a wart that Scala requires to bind it to a val
val cubic = integrate(square) _
val quartic = integrate(cubic) _
val quintic = integrate(quartic) _
// Not *utterly* horrible for a two line numerical integration function
cubic(1) // 0.328350
quartic(1) // 0.08004150
quintic(1) // 0.0154496265
柯里化还缓解了一些围绕固定函数数量的问题。
implicit class LiftedApply[A, B](fOpt: Option[A => B]){
def ap(xOpt: Option[A]): Option[B] = for {
f <- fOpt
x <- xOpt
} yield f(x)
}
def not(x: Boolean): Boolean = !x
def and(x: Boolean)(y: Boolean): Boolean = x && y
def and3(x: Boolean)(y: Boolean)(z: Boolean): Boolean = x && y && z
Some(not _) ap Some(false) // true
Some(and _) ap Some(true) ap Some(true) // true
Some(and3 _) ap Some(true) ap Some(true) ap Some(true) // true
通过柯里化函数,我们能够“提升”一个函数来处理
Option
并处理我们需要的尽可能多的参数。如果我们的逻辑函数没有被柯里化,那么我们就必须有单独的函数来将 A => B
提升到 Option[A] => Option[B]
,将 (A, B) => C
提升到 (Option[A], Option[B]) => Option[C]
,将 (A, B, C) => D
提升到 (Option[A], Option[B], Option[C]) => Option[D]
,依此类推。我们关心。
柯里化在类型推断方面还具有其他一些好处,并且如果您的方法同时具有
implicit
和非 implicit
参数,则需要使用柯里化。
最后,这个问题的答案列出了更多你可能想要柯里化的次数。
您的示例中的区别在于,重载函数将为
1
的第一个参数提供硬编码值 add
,即在编译时设置,而部分应用或柯里化函数旨在动态捕获其参数,即在运行时。否则,在您的特定示例中,因为您在两种情况下都进行了硬编码1
,所以它几乎是同一件事。
当您通过不同的上下文传递函数时,您将使用部分应用/柯里化函数,并且它会动态捕获/填充参数,直到完全准备好进行评估。在 FP 中,这很重要,因为很多时候您不传递值,而是传递函数。它允许更高的可组合性和代码可重用性。