解释Scala中Y组合子的这种实现?

时间:2016-01-13 19:43:47

标签: scala combinators y-combinator

这是Scala中Y-combinator的实现:

scala> def Y[T](func: (T => T) => (T => T)): (T => T) = func(Y(func))(_:T)
Y: [T](func: (T => T) => (T => T))T => T

scala> def fact = Y {
     |           f: (Int => Int) =>
     |             n: Int =>
     |               if(n <= 0) 1
     |               else n * f(n - 1)}
fact: Int => Int

scala> println(fact(5))
120

Q1:结果120如何逐步推出?由于Y(func)定义为func(Y(func)),因此Y应该会越来越多,Y在哪里丢失以及120在执行过程中是如何出现的?

Q2:

之间有什么区别
def Y[T](func: (T => T) => (T => T)): (T => T) = func(Y(func))(_:T)

def Y[T](func: (T => T) => (T => T)): (T => T) = func(Y(func))

它们与scala REPL中的类型相同,但第二个不能打印结果120

scala> def Y[T](func: (T => T) => (T => T)): (T => T) = func(Y(func))
Y: [T](func: (T => T) => (T => T))T => T

scala> def fact = Y {
     |           f: (Int => Int) =>
     |             n: Int =>
     |               if(n <= 0) 1
     |               else n * f(n - 1)}
fact: Int => Int

scala> println(fact(5))
java.lang.StackOverflowError
  at .Y(<console>:11)
  at .Y(<console>:11)
  at .Y(<console>:11)
  at .Y(<console>:11)
  at .Y(<console>:11)

3 个答案:

答案 0 :(得分:7)

首先,请注意,这不是Y- 组合子,因为该函数的lambda版本使用自由变量Y.它是Y的正确表达式,但不是组合子。

所以,让我们首先将计算阶乘的部分放入一个单独的函数中。我们可以称之为comp:

def comp(f: Int => Int) =
  (n: Int) => {
    if (n <= 0) 1
    else n * f(n - 1)
  }

现在可以像这样构建阶乘函数:

def fact = Y(comp)

Q1:

Y定义为func(Y(func))。我们调用fact(5),它实际上是Y(comp)(5),Y(comp)求值为comp(Y(comp))。这是关键点:我们停在这里因为comp需要一个函数,在需要之前不会对其进行评估。因此,运行时将comp(Y(comp))视为comp(???),因为Y(comp)部分是一个函数,只有在需要(if)时才会计算。

您是否了解Scala中的按值调用和按名称调用参数?如果将参数声明为someFunction(x: Int),则会在调用someFunction时立即对其进行求值。但是如果你将它声明为someFunction(x: => Int),则不会立即评估x,而是在使用它时。第二个调用是“按名称调用”,它基本上将你的x定义为“不带任何东西并返回Int的函数”。因此,如果你传入5,你实际上是传入一个返回5的函数。这样我们就可以实现函数参数的延迟评估,因为函数是在它们被使用的时候进行评估的。

因此,comp中的参数f是一个函数,因此它仅在需要时进行评估,即在else分支中。这就是为什么整个事情都有效--Y可以创建一个无限的func链(func(func(func(...))))但链是懒惰的。每个新链接仅在需要时计算。

因此,当您调用fact(5)时,它将通过body进入else分支,仅在此时f将被评估。不是之前。由于你的y作为参数f传递给comp(),我们将再次进入comp()。在comp()的递归调用中,我们将计算4的阶乘。然后我们将再次进入comp函数的else分支,从而有效地潜入另一个递归级别(计算阶乘3)。请注意,在每个函数调用中,Y提供了一个comp作为comp的参数,但它仅在else分支中进行求值。一旦我们达到计算阶乘0的水平,if分支将被触发,我们将停止进一步下跌。

Q2:

func(Y(func))(_:T)

是这个

的语法糖
x => func(Y(func))(x)

这意味着我们将整个事物包装成一个函数。这样做我们没有失去任何东西,只是获得了。

我们获得了什么?嗯,这与上一个问题的答案中的伎俩相同;这样我们就可以实现func(Y(func))仅在需要时进行评估,因为它包含在函数中。这样我们就可以避免无限循环。将(单参数)函数f扩展为函数x =&gt; f(x)被称为 eta-expansion (你可以阅读更多关于它的here)。

这是另一个简单的eta扩展示例:假设我们有一个方法getSquare(),它返回一个简单的square()函数(即计算数字平方的函数)。我们可以返回一个取x并返回square(x)的函数,而不是直接返回square(x):

def square(x: Int) = x * x
val getSquare: Int => Int = square
val getSquare2: Int => Int = (x: Int) => square(x)

println(square(5)) // 25
println(getSquare(5)) // 25
println(getSquare2(5)) // 25

希望这有帮助。

答案 1 :(得分:1)

补充接受的答案,

首先,请注意这不是Y组合器,因为该函数的lambda版本使用自由变量Y。尽管它不是Y组合器,但它是Y的正确表达式。

不允许组合器显式递归;它必须是没有自由变量的lambda表达式,这意味着它不能在其定义中引用其自身的名称。在lambda演算中,不可能在函数体中引用函数的定义。递归只能通过将函数作为参数来实现。

鉴于此,我从Rosetta代码中复制了以下实现,该代码使用某种类型的技巧来实现Y组合器,而无需显式递归。参见here

  def Y[A, B](f: (A => B) => (A => B)): A => B = {
    case class W(wf: W => A => B) {
      def get: A => B =
        wf(this)
    }
    val g: W => A => B = w => a => f(w.get)(a)

    g(W(g))
  }

希望这有助于理解。

答案 2 :(得分:0)

我不知道答案,但会尝试猜测。由于您有def Y[T](f: ...) = f(...)编译器可以尝试用Y(f)替换f。这将创建f(f(f(...)))的无限序列。部分应用f您创建一个新对象,这种替换变得不可能。