利用JavaScript推导Y组合子

关于Y组合子

Y组合子是lambda演算的一部分,属于存粹理论上的东西。

因为lambda演算不能定义名字,但是递归是需要通过名字来调用函数本身的。那么如何构建一个匿名递归函数呢?这就是Y组合子需要解决的问题。

举个简单的例子,一个普通的递归函数:

// 利用递归计算 n+(n-1)+(n-2)+...+1+0
var sum = (n) => {
  if (n===0) {
    return 0
  } else {
    // 通过“sum”标志函数本身,调用自身实现递归
    return n+sum(n-1)
  }
}

有没有某种神奇的力量(Y组合子),把递归函数变成下面的形式:

// Y 就是 Y组合子,sumR 表示 sum recursion
var sum = Y((sumR) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      // 通过调用传进来的参数实现递归
      // sumR 代表 sum recursion
      return n+sumR(n-1)
    }
  }
})

从而在不引用自身而实现递归呢。

这就是今天我们需要做的事情。把这个神奇力量找出来。


推导过程

由于推导过程比较难理解,所以将这个过程分成两个部分。

第一部分,对一个普通函数进行一系列演变,推导出一个能实现递归的函数。

第二部分,对这个递归的函数做等价的转换,从这个递归函数中提取出 Y组合子。

第一部分

因为不能在函数中引用自身,所以我们先定义几个函数

// eternity 函数,是个执行之后无限循环的函数。所以最好别去执行。
// 在这里用 console.error 代替无限循环,以防手贱执行了。。
var eternity = () => {
  // while(true) {}
  console.error('you are in eternity')
}

// sum0 函数,只能处理参数为 0 的情况,传其他参数的话它会疯掉,进入无限循环。。
var sum0 = (n) => {
  if (n===0) {
    return 0
  } else {
    return n+eternity()
  }
}

// sum1 函数,只能处理参数为 0, 1 的情况
var sum1 = (n) => {
  if (n===0) {
    return 0
  } else {
    return n+sum0(n-1)
  }
}

// sum2 函数,只能处理参数为 0, 1, 2 的情况
var sum2 = (n) => {
  if (n===0) {
    return 0
  } else {
    return n+sum1(n-1)
  }
}

假如可以这样一直定义下去,我们就能定义能过处理足够大的 n 的函数。


观察上面的 sum0,sum1,sum2 发现它们有很多相似的地方,很自然想到需要定义一个 mkSum 来生成。

// 生成 sumN 函数
var mkSum = (fn) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      return n+fn(n-1)
    }
  }
}

var sum0 = mkSum(eternity)
var sum1 = mkSum(sum0)
var sum2 = mkSum(sum1)

这样看起来就顺眼很多对不(→_→)


接下来关键的地方要来了,注意集中力了( ̄0  ̄)y

因为 eternity 函数是永远不能执行的,既然如此,我干嘛不换成其他函数呢。换什么函数都是一样的,那我就把

eternity 换成 mkSum 吧。

var sum0 = mkSum(mkSum)
var sum1 = mkSum(sum0)
var sum2 = mkSum(sum1)

为了方便观察,我们把 sum1,sum2 展开,得到如下:

var sum0 = mkSum(mkSum)
var sum1 = mkSum(mkSum(mkSum))
var sum2 = mkSum(mkSum(mkSum(mkSum)))

仔细看看上面的三个函数定义,看出来什么了么?

没错!这三个函数全部由 mkSum 组合而成的!! Σ(っ °Д °;)っ

仔细观察,每次当 sum 要处理的 n 变成 n+1 的时候,只需要把最里面的 mkSum 变成 mkSum(mkSum) 就行了。

只要吧 sum0 最里面的mkSum 替换成 mkSum(mkSum) ,就是 sum1 了。同理, sum1 最里面的 mkSum 替换成 mkSum(mkSum) ,就成了 sum2 。

怎么样,是不是嗅到一点递归的气息啦。最关键的就是怎么在需要的时候把 mkSum 转成 mkSum(mkSum)

如果可以做到这点,我们就能实现递归了。(ΦωΦ)


接下来放慢脚步,让我们看下 sum0,因为在 sum0 中,传给 mkSum 的参数就是它自身。那我们不妨改下 mkSum 的参数名。为了区别 mkSum,我们改成 mkSum1(其实不区别也可以)。

// 改成参数名 mkSum1 提醒我们传进来的参数就是 mkSum
var mkSum = (mkSum1) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      return n+mkSum1(n-1)
    }
  }
}

var sum0 = mkSum(mkSum)

之前已经提到,推导思想的关键就是在需要的时候,把 mkSum 转成 mkSum(mkSum)。

那什么时候是需要的时候呢?没错,就是递归函数在想要调用自身的时候!。

所以,我们把 mkSum 改造成下面的样子👇:

// 请注视这个函数至少一分钟。好好参悟。( ̄ω ̄)
var mkSum = (mkSum1) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      // 姨妈大!就是这里!!把 mkSum 改成 mkSum(mkSum)
      return n+mkSum1(mkSum1)(n-1)
    }
  }
}

// 已经实现递归,所以 sum0 已经变成 sum 了
var sum = mkSum(mkSum)

这里就是推导过程中最关键最难理解的地方了,到这一步,已经实现递归的效果了,sum 已经完美可运行。

接下来的就是在对 mkSum 函数进行等价的转换,从而抽象出真正的Y组合子,但请务必弄懂上面的部分才继续看下面的内容。

第二部分

抽象出真正的Y组合子

一个小方法

在下面的推导之前,为了让大家更好理解下面的内容,先写个小方法,下面的推导中很多地方都用的了。

那就是如何把函数中的某些内容提取出来作为参数。

var d = 1, e = 2
// 原函数
var a = c => {
  var b = d + e
  return b + c
}

// 变换形式,将 b 提取出来变成参数形式,和上面的原函数等价
var aCreator = b => c => b+c
var a = aCteator(d+e)

a(10)

在接下来的内容中,凡是涉及到提取某某变成参数形式的说法,都是利用这种方法。


首先,我们简单转换下 mkSum 函数的形式,把 mkSum1(mkSum1) 提取出来变成参数形式

var mkSum = (mkSum1) => {
  var sumR = mkSum1(mkSum1)
  
  // sumFn 不就是传给 Y组合子 的参数么?!
  var sumFn = (sumR) => {
    (n) => {
      if (n===0) {
        return 0
      } else {
        // 原来的 mkSum1(mkSum1) 被提取到外面,变成 sumR
        return n+sumR(n-1)
      }
    }
  }

  return sumFn(sumR)
}

var sum = mkSum(mkSum)

很简单的提取,用这个函数试下执行 sum(3) 试试?

啊咧??刚刚明明还很正常的啊,怎么突然就爆栈了?(#°Д°)

具体的原因就不解释了,自己好好想想就能得出答案了。

为了解决爆栈的问题,mkSum1(mkSum1) 外面裹一层函数就可以了。

var mkSum = (mkSum1) => {
  // 外面裹一层匿名函数,解决栈溢出问题
  var sumR = (x) => mkSum1(mkSum1)(x)
  
  var sumFn = (sumR) => {
    return (n) => {
      if (n===0) {
        return 0
      } else {
        return n+sumR(n-1)
      }
    }
  }

  return sumFn(sumR)
}

var sum = mkSum(mkSum)

接下来把sum中的mkSum提取出来变成参数形式

var mkSum = (mkSum1) => {
  var sumR = (x) => mkSum1(mkSum1)(x)
  
  var sumFn = (sumR) => {
    return (n) => {
      if (n===0) {
        return 0
      } else {
        return n+sumR(n-1)
      }
    }
  }
  
  return sumFn(sumR)
}

var w = f => f(f)
// 原来的 mkSum(mkSum) 变成了 函数w
var sum = w(mkSum)

我们再把sumFn提取到mkSum外面变成参数形式

// 被提取出来的 sumFn
var sumFn = (sumR) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      return n+sumR(n-1)
    }
  }
}

var mkSumCreator = (sumFn) => (mkSum1) => {
  var sumR = (x) => mkSum1(mkSum1)(x)
  return sumFn(sumR)  // 原来的 sumFn 已经变成形参了
}
var mkSum = mkSumCreator(sumFn)

var w = f => f(f)
var sum = w(mkSum)

再外面包一层函数,作为 Y组合子的原型

var sumFn = (sumR) => {
  return (n) => {
    if (n === 0) {
      return 0
    } else {
      return n + sumR(n - 1)
    }
  }
}

// 将所有元素包在一个函数中,这个函数就是 Y组合子 的原型
var poorY = () => {
  var mkSumCreator = (sumFn) => (mkSum1) => {
    var sumR = (x) => mkSum1(mkSum1)(x)
    return sumFn(sumR)
  }
  var mkSum = mkSumCreator(sumFn)
  var w = f => f(f)

  return w(mkSum)
}

var sum = poorY()

最后一次,让我们再看一下最终要的效果:

var sumFn = (sumR) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      return n+sumR(n-1)
    }
  }
}
var sum = Y(sumFn)

要怎么才能变成这种形式呢?这一步不是很明显。所以可能需要仔细观察一下。

我们只需要给 poorY 添加一个参数 sumFn,将最外部的 sumFn 作为参数传进来即可。

var sumFn = (sumR) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      return n+sumR(n-1)
    }
  }
}

// 为 poorY 添加一个参数 sumFn
var poorY = (sumFn) => {
   // poorY 函数内部不需要有任何变化
  var mkSumCreator = (sumFn) => (mkSum1) => {
    var sumR = (x) => mkSum1(mkSum1)(x)
    return sumFn(sumR)
  }
  var mkSum = mkSumCreator(sumFn)
  var w = f => f(f)
  return w(mkSum)
}

// sumFn 以参数的形式传进 poorY,而不是作为自由变量
var sum = poorY(sumFn)

这时候,千呼万唤的 Y组合子 终于要出来了,看见没有?!

因为 poorY 内部的 mkSumCreator 是直接调用的,所以可以把它简化一下,这样我们就得到真正的 Y组合子了。

var sumFn = (sumR) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      return n+sumR(n-1)
    }
  }
}

var y = (sumFn) => {
  // 去掉一层调用
  var mkSum = (mkSum1) => {
    var sumR = (x) => mkSum1(mkSum1)(x)
    return sumFn(sumR)
  }

  var w = f => f(f)
  return w(mkSum)
}

var sum = y(sumFn)

最后我们把多余的命名去掉,提取出 Y组合子:

var Y = (sumFn) => {
  var w = f => f(f)
  return w(mkSum1 => {
    var sumR = (x) => mkSum1(mkSum1)(x)
    return sumFn(sumR)
  })
}

var sum = Y((sumR) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      return n+sumR(n-1)
    }
  }
})

简化 Y组合子:

// 参数名替换
// w -> (f => f(f))
// mkSum1 -> b
// sumFn -> fn
var Y = fn => 
  (f => f(f))
  (b => fn( x => b(b)(x) ))

之前讲过 mkSum 和 mkSum1 其实并不需要严格区别。所以,为了更(故)加(作)简(神)化(秘),我们就把它们都替换成 f 吧。

最终 Y组合子:

var Y = fn => 
  (f => f(f))
  (f => fn( x => f(f)(x) ))

// 或者将 f => f(f) 展开
var Y = fn => 
  (f => fn( x => f(f)(x) ))
  (f => fn( x => f(f)(x) ))

细心的朋友应该注意到了,这个 Y 组合子生成的递归函数,只能处理一个参数,也就是 Y 组合子里面的 x 。所以我们可以修改一下,让 Y 组合子生成的递归函数不受参数限制。

var Y = fn => 
  (f => f(f))
  (f => fn( (...x) => f(f)(...x) ))

结尾

推导到此结束,看懂了么?如果没有的话,

可以参考下另一篇文章:https://zhuanlan.zhihu.com/p/20616683 (这篇文章讲得更加透彻),

或者阅读下 《The Little Schemer》的第九章(本文就是基于这一章写出来的)。

最后要指出的是,在实际应用中,递归的实现并不是靠Y组合子实现的,而是先保存函数的引用(比如指针),然后在需要的时候再通过指针调用自身函数。这和我们的直觉是相符合。

所以,理解Y组合子并没有什么卵用,只是无聊找虐而已。╮(╯▽╰)╭

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容