【问题标题】:Partially applied/curried function vs overloaded function部分应用/咖喱函数与重载函数
【发布时间】:2016-07-04 15:19:57
【问题描述】:

虽然我了解什么是部分应用/咖喱函数,但我仍然不完全理解为什么我会使用这样的函数而不是简单地重载函数。 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重载函数

【问题讨论】:

标签: scala


【解决方案1】:

您的示例中的不同之处在于,重载函数将具有硬编码值1 作为add 的第一个参数,即在编译时设置,而部分应用或柯里化函数旨在动态捕获它们的参数,即在运行。否则,在您的特定示例中,因为您在两种情况下都在硬编码1,所以它几乎是一回事。

当您通过不同的上下文传递它时,您将使用部分应用/咖喱函数,它会动态捕获/填充参数,直到它完全准备好进行评估。在 FP 中,这很重要,因为很多时候您不传递值,而是传递函数。它允许更高的可组合性和代码可重用性。

【讨论】:

    【解决方案2】:

    您可能更喜欢部分应用函数的原因有几个。最明显也可能是肤浅的一点是,你不必写出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: Double => Double, delta: Double = 0.01)(x: Double): Double = {
      val domain = Range.Double(0.0, x, delta)
      domain.foldLeft(0.0){case (acc, a) => delta * f(a) + acc      
    }
    

    可以按照您在微积分中实际学习积分的方式来思考和使用,即作为产生另一个函数的函数的转换。

    def square(x: Double): Double = 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.32835000000000014
    quartic(1) // 0.0800415
    quintic(1) // 0.015449626499999999
    

    Currying 也缓解了固定函数数量的一些问题。

    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 =&gt; B 提升到 Option[A] =&gt; Option[B](A, B) =&gt; C(Option[A], Option[B]) =&gt; Option[C](A, B, C) =&gt; D(Option[A], Option[B], Option[C]) =&gt; Option[D] 等等。我们关心。

    在类型推断方面,柯里化还有其他一些其他好处,如果方法同时具有implicit 和非implicit 参数,则需要使用柯里化。

    Finally, the answers to this question 列出更多你可能想要使用柯里化的时间。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-02
      • 2011-04-21
      • 2013-08-26
      相关资源
      最近更新 更多