Javascript推导Y-Combinator (来自Jim Weirich)

熟悉函数式编程的同学都了解lambda表达式,程序设计语言里的lambda表达式来源于1936年邱奇发明的lambda演算。Y-Combinator正是lambda演算里最富有神秘色彩的一种函数。它的作用是在只有匿名函数的lambda演算里实现递归函数调用。推导Y-Combinator很多人都做过了,比如这篇Javascript推导 Deriving
the Y Combinator in 7 Easy Steps
(中文) 还有这篇原文使用Scheme中文翻译为Javascript的推导 The
Why of Y
 (中文)。读过我发现推导过程思维跳跃性有点大,且每个步骤没有给出足够的解释。本篇向读者揭示了如何反复重构一个普通的阶乘函数从而推导出Y-Combinator。文章的推导方法来源于Jim
Weirich
ruby
conference 2012
上的一次分享(下载地址 优酷),Jim
Weirich是Ruby社区的重要贡献者,开发了非常流行的 Rake —— 几乎被所有Ruby 开发者使用的开发工具。非常可惜的是Jim Weirich已经于今年2月19日离世。这篇推导非常棒,因为在推导前对重构方法进行了充分介绍,推导整个过程就变得非常平坦。于是我也顺便学习了下写了这篇文章。文章分三部分:1关于lambda演算和Y-Combinator的简单介绍,了解的人可以直接跳过;2重构的方法;3推导Y-Combinator过程。后两部分主要摘取自Jim Weirich的分享但是为了便于理解进行一些修改。

1 关于lambda演算

lambda演算

本文不详细介绍lambda演算和Y-Combinator了,那可能需要相当的篇幅。不熟悉的同学可以自己通过网上材料学习(参考英文wiki 中文wiki Programming
Languages and Lambda Calculi
 G9的blog)。按照wiki的定义:lambda演算(lambda calculus,λ-calculus)是一套用于研究函数定义、函数应用和递归的形式系统。邱奇运用lambda演算在1936年给出判定性问题的一个否定的答案。这种演算可以用来清晰地定义什么是一个可计算函数。lambda演算是一套形式系统,在计算机被建造出之前就已经存在很久了。lambda演算和图灵机是从两个不同方向对计算能力的抽象,前者发源于理论而后者来自硬件设计。比较之下lambda演算更为简单优美并接近数学,一句话能很好的概括它:lambda演算可以被称为最小的通用程序设计语言。

lambda演算与Y-Combinator的关系

Y-Combinator是一个函数,他可以为匿名函数生成一个递归调用自己的函数。Y-Combinator使得lambda演算有能力表达递归逻辑。因为lambda演算和图灵机具有相同的计算能力,所以lambda演算当然可以表示任何常见的编程概念,比如用来表示自然数的邱奇数和当作谓词使用的邱奇布尔值(参考邱奇数 Church_encoding),或者是用单参数lambda表示多参数函数的Currying(参考 中文wiki 或者 谈谈Currying)。

了解Y-Combinator的意义

我们了解Y-Combinator有用吗?负能量的答案是:几乎没有任何用处。在具有命名能力的编程语言里完全不需要这种制造递归的函数,且编程语言的底层实现都是基于图灵机模型的,与lambda演算和Y-Combinator没有半点关系。正能量的答案是:有点用处可以帮助你更好的了解函数式编程。

lambda与Javascript

现代程序语言中的lambda表达式只是取名自lambda演算,已经与原始的lambda演算有很大差别了。而Javascript里没有任何语法专门代表lambda只写成这样的嵌套函数"function{ return function{...} }"

有趣的事

SICP封面画了lambda,MIT的计算机科学系徽就是Y-Combinator,创业教父Paul Graham的孵化器叫Y-Combinator。

   

2 重构的方法

下面5种方法都是Jim Weirich提到的,5种方法在后续的推导种都被使用了至少一次。第一种方法使用的最频繁。

1 Tennent Correspondence Principle

把任何表达式包装在一个lambda里并立刻调用这个lambda表达式,不会对原表达式的值产生影响。这个方法在推导Y-Combinator的过程中被使用了很多次。

// Tennent Correspondence Principle
mul3 = function(n) { return n * 3 }
mul3 = function(n) { return function() { return n * 3 }() }

make_adder = function(x) {
  return function(n) { return n + x }
}
make_adder = function(x) {
  return function() { return function(n) { return n + x } }()
}

2 Introduce Binding

可以为一个无参数的lambda表达式添加一个参数,当然这个参数是未被绑定到任何lambda的,然后就可以在调用的时候为这个新参数传入任何值,这样修改后的代码不会对原表达式产生影响。这个也容易理解,因为新参数是后添加的,lambda里根本没有被用到嘛,没有被用到的值当然不会对表达式产生任何影响了。

// Introduce Binding
make_adder = function(x) {
  return function() { return function(n) { return n + x } }()
}
make_adder = function(x) {
  return function(xyz) { return function(n) { return n + x } }(123456)
}

3 Rebind

当几个存在嵌套关系的lambda存在时,可以在中间层没有参数的lambda加一个参数,然后把外部的参数通过新加的参数传递给内部lambda,Rebind从名字看就是重新绑定的意思,下面代码里的n原来是绑定到最外层的n的,Rebind修改之后就绑定到中间层的n了,外层的n已经影响不到它,他是通过中间层调用的时候传入的n来获取值的。

// Rebind
mul3 = function(n) { return function() { return n * 3 }() }
mul3 = function(n) { return function(n) { return n * 3 }(n) }

4 Function Wrap

这和第一种方法Tennent Correspondence Principle有点相似但又不太一样,可以使用一个lambda来包装原有的lambda,只要调用一下被包装的lambda即可。

// Function Wrap
x = function(x) {
  return function(n) { return n + x }
}
x = function(x) {
  return function(z) { return function(n) { return n + x }(z) }
}

5 Inline Function

内联是最好理解的,把原来有变量名的地方直接用变量的内容替换掉,也就是说把命名变量变成了匿名。

//5 Inline Function
compose = function(f, g) {
  return function(n) { return f(g(n)) }
}
mul3add1 = compose(mul3, add1)

compose = function(f, g) {
  return function(n) { return f(g(n)) }
}
mul3add1 = function(f, g) {
  return function(n) { return f(g(n)) }
} (mul3, add1)

3 推导Y-Combinator过程

说明一下,每次的结果都是用这样的输出形式,我用nodejs跑挺方便,直接浏览器里也没问题。同时为了便于理解,固定用途的变量从开始到结束尽量不修改命名(Jim Weirich推导时经常重命名变量,容易把人搞晕)。

console.log(function(){
  //return xxx;
}())

先看看如果推导出了Javascript版的Y-Combinator,它大概是什么样子,后续就可以方便的进行对照。

第一个版本,这是一个很普通的递归求阶乘函数,没有任何特别之处。

console.log(function(){

  function fact(n){
    return n == 1 ? 1 : n * fact(n-1);
  }
  return fact(5)

}())

我们把fact放到参数中去,这样函数体里就少了一个自由变量,把命名变量从自由变量改成绑定变量是一个很重要的重构手段。

// method ==> parameter
console.log(function(){

  function fact(g, n){
    return n == 1 ? 1 : n * g(g, n-1);
  }
  return fact(fact, 5)

}())

进一步,我们把fact的返回值从数字改成函数,也就是说fact从“计算一个数的阶乘的函数”,变成了“返回一个可以计算阶乘的函数的函数”,这次重构后,n和fact被分割开了,这样逻辑更清晰,计算阶乘这个动作被分解成了两步:第一步是制造一个能计算阶乘的函数,这由fact(fact)来完成;第二步是让这个函数计算5的阶乘。调用方式发生了细微的变化,从fact(fact, 5)变成了fact(fact)(5)。

// parameter ==> lambda
console.log(function(){

  function fact(g) {
    return function(n) {
      return n == 1 ? 1 : n * g(g)(n-1);
    }
  }
  return fact(fact)(5)

}())

休息一下,来一个简单的重构,为fact(fact)命个名就叫fx,后面再调用fx。

// naming fact(fact)
console.log(function(){

  function fact(g) {
    return function(n) {
      return n == 1 ? 1 : n * g(g)(n-1);
    }
  }
  fx = fact(fact)
  return fx(5)

}())

这一步第一次使用了前面学习的重构5方法之Tennent Correspondence Principle,简称TCP了,把return fact(fact)包装起来调用一下。

// Tennent Correspondence Principle
console.log(function(){

  function fact(g) {
    return function(n) {
      return n == 1 ? 1 : n * g(g)(n-1);
    }
  }
  fx = function() { return fact(fact) }()
  return fx(5)

}())

这一步把自由变量fact换成绑定变量g,之前用过了。

// free variable ==> parameter
console.log(function(){

  function fact(g) {
    return function(n) {
      return n == 1 ? 1 : n * g(g)(n-1);
    }
  }
  fx = function(g) { return g(g) }(fact)
  return fx(5)

}())

重构5方法之Inline,Inline的结果是fact这个命名变量消失了,在需要fact的地方,用一个lambda表达式代替了。

// Inline
console.log(function(){

  fx = function(g) {
    return g(g)
  } (
    function(g) {
      return function(n) {
        return n == 0 ? 1 : n * g(g)(n-1)
      }
    }
  )

  return fx(5)
}())

再来一次TCP,把最内层的lambda包裹一下并执行,执行的时候参数是空的。

// Tennent Correspondence Principle
console.log(function(){

  fx = function(g) {
    return g(g)
  } (
    function(g) {
      return function(n) {
        return function() {
          return n == 0 ? 1 : n * g(g)(n-1)
        }()
      }
    }
  )

  return fx(5)
}())

来一次Rebind,把外层的n传递给内层lambda。

// Rebind
console.log(function(){

  fx = function(g) {
    return g(g)
  } (
    function(g) {
      return function(n) {
        return function(n) {
          return n == 0 ? 1 : n * g(g)(n-1)
        }(n)
      }
    }
  )

  return fx(5)
}())

再来一次TCP,注意,这次被包裹的lambda是return function(n) { return n == 0 ? 1 : n*g(g)(n-1) },所以第13行写成}()(n)而不是}(n)()。

// Tennent Correspondence Principle
console.log(function(){

  fx = function(g) {
    return g(g)
  } (
    function(g) {
      return function(n) {
        return function() {
          return function(n) {
            return n == 0 ? 1 : n * g(g)(n-1)
          }
        }()(n)
      }
    }
  )

  return fx(5)
}())

使用Introduce Binding为无参的函数添加一个参数recursion_in_mind,并将其代替g(g),当然了需要把g(g)传递进去。因为recursion_in_mind这个函数是有特殊含义的,所以我们使用了一个特殊的名字,这个变量将代表一个假想中的可以递归计算阶乘的函数。它的用途在后面的步骤中介绍。

// Introduce Binding
console.log(function(){

  fx = function(g) {
    return g(g)
  } (
    function(g) {
      return function(n) {
        return function(recursion_in_mind) {
          return function(n) {
            return n == 0 ? 1 : n * recursion_in_mind(n-1)
          }
        }(g(g))(n)
      }
    }
  )

  return fx(5)
}())

再来一次TCP,注意这次被包裹的是整个fx所代表的lambda。所以调用的括号在18行的末尾。

// Tennent Correspondence Principle
console.log(function(){

  fx = function() {
    return function(g) {
      return g(g)
    } (
      function(g) {
        return function(n) {
          return function(recursion_in_mind) {
            return function(n) {
              return n==0 ? 1 : n * recursion_in_mind(n-1)
            }
          }(g(g))(n)
        }
      }
    )
  }()

  return fx(5)
}())

再来一次Introduce Binding,为上一步18行添加的那个调用传入一个参数,同时这个参数没有使用自由变量,而是直接使用了一个lambda,代码有点拖沓,仔细看就会明白。

// Introduce Binding
console.log(function(){

  fx = function(f) {
    return function(g) {
      return g(g)
    } (
      function(g) {
        return function(n) {
          return f(g(g))(n)
        }
      }
    )
  }(
    function(recursion_in_mind) {
      return function(n) {
        return n==0 ? 1 : n * recursion_in_mind(n-1)
      }
    }
  )

  return fx(5)
}())

用一个变量temp来指代上一步直接传入的lambda,同时把fx改名为Y,是的这个Y就是Y-Combinator的一种形式了。同时为了代码便于阅读,多行的代码被整理到一行里。推导到这一步,代码结构突然变的和之前不太一样了,可以看到现在有两个命名函数存在,一个是temp,另一个是Y。计算过程是清晰的两个步骤,第一步是fact = Y(temp),这是以temp为原料,以Y为制作工艺,产出了一个fact函数,这一步是在制造函数。第二步使用5来调用这个刚制作好的函数。temp函数也非常特别,它接收一个递归函数recursion_in_mind,然后调用这个函数来计算n-1的阶乘。推导到这一步,可以认为Y-Combinator已经推导出来了,但是对照lambda演算里Y-Combinator的标准形式会发现,现在的Y的样子明显跟它不符,所以要进一步推导。

// naming function(recursion_in_mind) {...}
// Y is form one of Y-Combinator
console.log(function(){

  temp = function(recursion_in_mind) {
    return function(n) {
      return n == 0 ? 1 : n * recursion_in_mind(n-1)
    }
  }

  Y = function(f) {
    return function(g) { return g(g) } (
      function(g) { return function(n) { return f(g(g))(n) } }
    )
  }

  fact = Y(temp)

  return fact(5)
}())

这一步推导稍微麻烦,因为之前的重构5种方法都不管用了,需要新引入了一个叫做”函数的不动点“的概念,函数f的不动点是一个值x使得f(x) = x。例如,0和1是函数f(x) = x^2 (计算平方的函数)的不动点,因为0^2 = 0而1^2 = 1。鉴于一阶函数(在简单值比如整数上的函数)的不动点是个一阶值,高阶函数f的不动点是另一个函数g使得f(g) = g。

回到推导中来,现在需要一个有关不动点的结论来完成后续的推导,那就是temp(fact) = fact(可以先不考虑为什么需要这样一个中间结论,现在的任务是要证明这个结论的正确性,等到结论被使用时就可以知道为什么需要它了),这代表什么意思呢?套用刚刚说过的定义,高阶函数temp的不动点是另一个函数fact,使得temp(fact) = fact,只要这一步相等能达成。后续推导就没问题。

回头看看19行的return fact(5)可以知道fact就是一个可以独立计算出5的阶乘的函数了。而temp是什么呢,temp是一个接收一个函数作为参数,并返回一个函数的函数。temp(fact)就是把fact传入temp喽,也就是把一个计算阶乘的函数传入进去了,而通过查看temp的函数实现发现,temp正好利用这个阶乘函数来计算n-1的阶乘了。最后再乘上n,返回的正好是一个计算n!的函数,而刚才我们根据return fact(5)已经得出过结论了fact()也是一个计算n!的函数,所以可以得出这个中间结论了,那就是temp(fact)
= fact。中间结论成立!

按照原计划我们应该赶紧拿着这个得来不易的 temp(fact) = fact 结论继续上路开始推导了,上路之前再插播一个有趣的现象,因为这个中间结论,fact是函数temp的不动点了,而函数fact又是由Y函数对原料temp进行加工而成,所以我们可以从另一个角度为Y-Combinator下定义,Y-Combinator可以计算出一个函数(也就是temp函数)的不动点函数(也就是fact)。

继续上路,查看上面代码17行可以知道fact函数正是12行的这个g(g),因为return g(g) 就是赋值给fact了,而Y函数在17行被调用,传入的是temp,也就是Y函数里的f就是temp。拿出刚刚出炉的中间结论 temp(fact) = fact 一用,我们可以把第12行的return g(g) 替换为return f(g(g)),因为fact是temp的不动点,所以g(g)就是f的不动点,所以f(g(g)) == g(g)是成立的。所以这一步推导只修改了一行,也就是上面的12行。至此,这步重构算是完成了。

// fixed point refactor g(g) ==> f(g(g))
console.log(function(){

  temp = function(recursion_in_mind) {
    return function(n) {
      return n == 0 ? 1 : n * recursion_in_mind(n-1)
    }
  }

  Y = function(f) {
    return function(g) { return f(g(g)) } (
         function(g) { return function(n) { return f(g(g))(n) } }
    )
  }

  fact = Y(temp)
  // temp(fact) = fact

  return fact(5)

}())

最后一步,需要为上面11行的return f(g(g))来一次5重构法之Function Wrap,传入的参数是n,通过这次修改,当前Javascript版本的Y函数与标准lambda演算里的Y完全相同了,非常好。再次对照我们在推导前给出的样子

// Function Wrap
// Y is form two of Y-Combinator
console.log(function(){

  temp = function(recursion_in_mind) {
    return function(n) {
      return n == 0 ? 1 : n * recursion_in_mind(n-1)
    }
  }

  // λg.(λx.g(x x))(λx.g(x x))
  Y = function(f) {
    return function(g) { return function(n) { return f(g(g))(n) } } (
         function(g) { return function(n) { return f(g(g))(n) } }
    )
  }

  fact = Y(temp)

  return fact(5)

}())

按理说到此为止Y的推导过程已经结束了,但是我觉得这还不够完美,因为出现了一个命名变量temp,Y就是为实现匿名函数的递归调用而生的,我们这里放了temp是什么意思,男女授搜不亲啊,所以好人做到底,我们最后再使用一次Inline,直接把temp所代表的lambda丢给Y函数好了,这也就正好完美的演示了Y的真正用途:为匿名函数制造一个递归调用自己的函数(fact函数),全文完。

// Inline
console.log(function(){

  // λg.(λx.g(x x))(λx.g(x x))
  Y = function(f) {
    return function(g) { return function(n) { return f(g(g))(n) } } (
         function(g) { return function(n) { return f(g(g))(n) } }
    )
  }

  fact = Y(
    function(recursion_in_mind) {
      return function(n) {
        return n == 0 ? 1 : n * recursion_in_mind(n-1)
      }
    }
  )

  return fact(5)

}())
时间: 2024-08-19 13:09:16

Javascript推导Y-Combinator (来自Jim Weirich)的相关文章

[译] The Why of Y - 理解Y Combinator

原文:(The Why of Y) 作者: Richard P. Gabriel Lucid, Inc. and StanfordUniversity 你是否好奇Y(Y combinator,下文简称Y)的工作原理.前人是怎么发明出这玩意的?我将在这篇文章中告诉你.我将使用Scheme语言描述,因为用这种语言表达"作为参数传入另一个函数的函数被调用"更容易理解. Y存在的意义是,在不使用(某种语言提供的)特殊的内置方法的情况下写出自引用的(self-referential)(译注:也就

y combinator 做的一个调查_可以学习一下

RoR: 在网络营运平台企业中,RoR站稳使用率第一的位置.其用户包括:ZenPayroll (人力资源).Asile50 (零售平台).BackerKit (众筹平台).Rainforest (QA测试)等. Ruby on Rails是一种结合Ruby语言与Rails平台的一种网页编程语言,Ruby语言以自然.简洁.快速著称,全面支持面向对象程序设计,而Rails则是Ruby广泛应用方式之一,在Rails平台上设计出一套独特的MVC开发架构,采取模型(Model).外观(View).控制器(

Y Combinator

由于匿名函数(通常成为lambda函数但是跟lambda calculus不同)在递归时无法获得函数名,从而导致一些问题,而Y Combinator能很好地解决这个问题.利用不动点的原理,可以利用一般的函数来辅助得到匿名函数的递归形式,从而间接调用无法表达的真正的匿名函数.下面以一个阶乘的递归来说明. #Python版本,后面会加上C++版本 #F(f) = f def F(f,n): return 1 if n==0 else n*f(n-1) #或者用lambda #F = lambda f

YC(Y Combinator)斯坦福大学《如何创业》课程要点记录(粗糙)

20节课程,每节都是干货满满,时常听说理论无用,但是好的理论,绝对能帮助你少走一些弯路. YC简介: Y Combinator成立于2005年,是美国著名创业孵化器,Y Combinator扶持初创企业并为其提供创业指南.截止2012年7月,共孵化380家创业公司,这些公司累计获得投资额超过10亿美元,估值已经100亿美元.Y Combinator 只关注于最早期的创业团队,在创业团队的起步阶段介入并提供相应的帮助.Y Combinator 会定期举行 Funding Application 的

创业孵化器公司Y Combinator,将招募更多的黑人企业家

Y Combinator,是硅谷一家很有影响力的创业孵化器企业,并指出,他们正在加紧招募黑人企业家. (图片: Richard Morgenstein) 旧金山-Y Combinatorg公司想招募更多的黑人企业家,进驻硅谷的一号初创企业新兵训练营. Y Combinator公司孵化的企业家,黑人的数量不足1%.但是,在最近的一批创业者之中,三个创始人都是黑人. Y Combinator公司的董事长Sam Altman,在接受<今日美国>日报采访时表示,Y Combinator公司将致力于提高

Y combinator 的推导过程

最近在看<暗时间>,书中有Y组合子的推导过程,初看时很难理解,这里记录一下加深记忆,我们使用Scheme语言的语法. 我们知道Scheme中可以这样定义递归函数 (define (func n) (if (= n 0) 1 (* n (func (- n 1))))) 但是我们知道define这个函数只起到了一个语法糖的效果,再对应lambda表达式还未知的时候是不能使用这个函数. 接下来我们只通过使用lambda表达式来推导出递归函数. 我们先做一下尝试 (lambda (n) (if (=

诚实的力量。Paul Graham (Y Combinator 创始人)关于诚实的评论。

最近读 <Founders at Work>,读到 Paul Graham 关于他早期创立 Viaweb (被 Yahoo 收购后变为 Yahoo Store)的经历.他谈到 “ Another advantage of telling the truth is that you don’t have to remember what you’ve said. You don’t have to keep any state in your head. It’s a purely functi

(6)javascript的程序控制结构及语句-----(1)条件判断

程序控制结构及语句 编程就是将现实应用,转换为程序能够读得懂的语法语句.Javascript编程中对程序流程控制主要是通过条件判断语句.循环控制语句及continue.break来完成的,其中条件判断按预先设定的条件执行顺序,它包括if语句和switch语句:而循环控制语句则可以重复完成任务,它包括while语句.do...while语句及for语句. 一.条件判断语句: 条件判断语句就是对语句中不同条件的值进行判断,进而根据不同条件执行不同的语句.条件判断语句有"if条件语句"和&q

javascript之对象,原型链

对象 ECMAScript是高度抽象的面向对象语言,处理对象的.同样也有基本类型,但是他们在需要的情况下,也可以转型成对象. 定义: 对象是拥有一系列属性以及唯一的原型对象.那原型对象可能是对象,也可能为空. 让我们看一个基本对象的例子.对象的原型是通过内部的[[Prototype]]来引用的.然而,我们将会用 __<internal_property>__ 下化线符号替代双中括号,那么对于原型对象: __proto__. 对于下面代码: var foo = { x: 10, y:20 };