是函数签名中的scala分解类型



我正在学习马丁·奥德斯基(Martin Odersky)的FP课程,其中一节课通过牛顿法演示了高阶函数,用于寻找某些函数的不动点。讲座中有一个关键步骤,我认为打字签名被违反了,所以我想请你解释一下。(对入站的冗长介绍表示歉意——它觉得这是必要的。)

实现这种算法的一种方法是这样给出的:

  val tolerance = 0.0001
  def isCloseEnough(x: Double, y: Double) = abs((x - y) / x) / x < tolerance
  def fixedPoint(f: Double => Double)(firstGuess: Double) = {
    def iterate(guess: Double): Double = {
      val next = f(guess)
      if (isCloseEnough(guess, next)) next
      else iterate(next)
    }
    iterate(firstGuess)
  }

接下来,我们尝试通过fixedPoint函数计算平方根,但通过的天真尝试

def sqrt(x: Double) = fixedPoint(y => x / y)(1)

由于这种方法振荡(因此,对于sqrt(2),结果将在1.0和2.0之间无限交替),被挫败

为了解决这个问题,我们引入了平均阻尼,因此本质上我们计算两个最近计算值的平均值,并收敛到解,因此

def sqrt(x: Double) = fixedPoint(y => (y + x / y) / 2)(1)

最后,我们介绍了averageDamp函数,任务是用fixedPointaverageDamp编写sqrtaverageDamp定义如下:

def averageDamp(f: Double => Double)(x: Double) = (x + f(x)) / 2

我不明白的部分来了——我最初的解决方案是:

def sqrt(x: Double) = fixedPoint(z => averageDamp(y => x / y)(z))(1)

但奥德斯基教授的解决方案更为简洁:

def sqrt(x: Double) = fixedPoint(averageDamp(y => x / y))(1)

我的问题是——为什么它有效?根据函数签名,fixedPoint函数应该接受一个函数(Double => Double),但它不介意被传递一个普通的Double(这就是averageDamp返回的值——事实上,如果你试图将Double的返回类型显式指定给averageDamp,编译器不会抛出错误)。

我认为我的方法正确地遵循了类型——那么我在这里缺少了什么呢?averageDamp返回函数在哪里指定或暗示(?),特别是在右侧明显返回标量的情况下?如何将标量传递给一个显然只期望函数的函数?您如何解释似乎不支持类型签名的代码?

您的解决方案是正确的,但它可以更简洁。

让我们更仔细地研究一下averageDamp函数。

def averageDamp(f: Double => Double)(x: Double): Double = (x + f(x)) / 2

添加了返回类型注释以使其更加清晰。我想你错过的是这里:

但它并不介意被传递一个普通的Double(这就是averageDump返回的值——事实上,如果您试图显式指定将Double类型返回到averageDump,编译器不会抛出错误)。

但是averageDamp(y => y/x)确实返回了一个Double => Double函数!averageDamp需要传递两个参数列表才能返回Double

如果函数只接收一个参数,它仍然希望完成另一个参数。因此,它不是立即返回结果,而是返回一个函数,说"我这里仍然需要一个参数,给我这个,这样我就会返回你想要的"。

MO教授确实向其传递了ONE函数参数,而不是两个,因此averageDamp部分应用,因为它返回了一个Double => Double函数。

本课程还将告诉您具有多个参数列表的函数是以下语法形式的糖:

def f(arg1)(arg2)(arg3)...(argN-1)(argN) = (argN) => f(arg1)(arg2)(arg3)...(argN-1)

如果你给出的参数比f需要的少一个,它只会返回方程的右边,也就是一个函数。因此,注意传递给fixPoint的参数averageDamp(y => x / y)实际上是一个函数,这应该有助于您理解这个问题。

注意:部分应用函数(或函数currying)和多参数列表函数之间存在一些差异

例如,您不能像这样声明

val a = averageDamp(y => y/2)

编译器会抱怨"方法不是部分应用的函数"。

区别在这里解释:What';Scala中多个参数列表和每个列表多个参数的区别是什么?。

多个参数列表是返回另一个函数的函数的语法糖。你可以在scala shell中看到这一点:

scala> :t averageDamp _
(Double => Double) => (Double => Double)

我们可以在没有语法糖的情况下编写相同的函数——这是我们在例如Python:中的做法

def averageDamp(f: Double => Double): (Double => Double) = {
   def g(x: Double): Double = (x + f(x)) / 2
   g
}

返回一个函数一开始可能看起来有点奇怪,但它是对将函数作为参数传递的补充,并支持一些非常强大的编程技术。函数只是另一种类型的值,如IntString

在最初的解决方案中,您重用了变量名y,我认为这会使它有点混乱;我们可以把你写的东西翻译成:

def sqrt(x: Double) = fixedPoint(z => averageDamp(y => x / y)(z))(1)

有了这个表格,你有望看到模式:

def sqrt(x: Double) = fixedPoint(z => something(z))(1)

希望现在很明显,这与相同

def sqrt(x: Double) = fixedPoint(something)(1)

这是奥德斯基的版本。

最新更新