利用JavaScript推導(dǎo)Y組合子

關(guān)于Y組合子

Y組合子是lambda演算的一部分秉扑,屬于存粹理論上的東西。

因為lambda演算不能定義名字气忠,但是遞歸是需要通過名字來調(diào)用函數(shù)本身的邻储。那么如何構(gòu)建一個匿名遞歸函數(shù)呢?這就是Y組合子需要解決的問題旧噪。

舉個簡單的例子吨娜,一個普通的遞歸函數(shù):

// 利用遞歸計算 n+(n-1)+(n-2)+...+1+0
var sum = (n) => {
  if (n===0) {
    return 0
  } else {
    // 通過“sum”標(biāo)志函數(shù)本身,調(diào)用自身實現(xiàn)遞歸
    return n+sum(n-1)
  }
}

有沒有某種神奇的力量(Y組合子)淘钟,把遞歸函數(shù)變成下面的形式:

// Y 就是 Y組合子宦赠,sumR 表示 sum recursion
var sum = Y((sumR) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      // 通過調(diào)用傳進(jìn)來的參數(shù)實現(xiàn)遞歸
      // sumR 代表 sum recursion
      return n+sumR(n-1)
    }
  }
})

從而在不引用自身而實現(xiàn)遞歸呢。

這就是今天我們需要做的事情米母。把這個神奇力量找出來勾扭。


推導(dǎo)過程

由于推導(dǎo)過程比較難理解,所以將這個過程分成兩個部分铁瞒。

第一部分妙色,對一個普通函數(shù)進(jìn)行一系列演變,推導(dǎo)出一個能實現(xiàn)遞歸的函數(shù)慧耍。

第二部分身辨,對這個遞歸的函數(shù)做等價的轉(zhuǎn)換,從這個遞歸函數(shù)中提取出 Y組合子芍碧。

第一部分

因為不能在函數(shù)中引用自身煌珊,所以我們先定義幾個函數(shù)

// eternity 函數(shù),是個執(zhí)行之后無限循環(huán)的函數(shù)泌豆。所以最好別去執(zhí)行定庵。
// 在這里用 console.error 代替無限循環(huán),以防手賤執(zhí)行了。蔬浙。
var eternity = () => {
  // while(true) {}
  console.error('you are in eternity')
}

// sum0 函數(shù)猪落,只能處理參數(shù)為 0 的情況,傳其他參數(shù)的話它會瘋掉敛滋,進(jìn)入無限循環(huán)许布。兴革。
var sum0 = (n) => {
  if (n===0) {
    return 0
  } else {
    return n+eternity()
  }
}

// sum1 函數(shù)绎晃,只能處理參數(shù)為 0, 1 的情況
var sum1 = (n) => {
  if (n===0) {
    return 0
  } else {
    return n+sum0(n-1)
  }
}

// sum2 函數(shù),只能處理參數(shù)為 0, 1, 2 的情況
var sum2 = (n) => {
  if (n===0) {
    return 0
  } else {
    return n+sum1(n-1)
  }
}

假如可以這樣一直定義下去杂曲,我們就能定義能過處理足夠大的 n 的函數(shù)庶艾。


觀察上面的 sum0,sum1擎勘,sum2 發(fā)現(xiàn)它們有很多相似的地方咱揍,很自然想到需要定義一個 mkSum 來生成。

// 生成 sumN 函數(shù)
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)

這樣看起來就順眼很多對不(→_→)


接下來關(guān)鍵的地方要來了棚饵,注意集中力了( ̄0  ̄)y

因為 eternity 函數(shù)是永遠(yuǎn)不能執(zhí)行的煤裙,既然如此,我干嘛不換成其他函數(shù)呢噪漾。換什么函數(shù)都是一樣的硼砰,那我就把

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)))

仔細(xì)看看上面的三個函數(shù)定義诈胜,看出來什么了么豹障?

沒錯!這三個函數(shù)全部由 mkSum 組合而成的=剐佟血公! Σ(っ °Д °;)っ

仔細(xì)觀察,每次當(dāng) sum 要處理的 n 變成 n+1 的時候缓熟,只需要把最里面的 mkSum 變成 mkSum(mkSum) 就行了累魔。

只要吧 sum0 最里面的mkSum 替換成 mkSum(mkSum) ,就是 sum1 了荚虚。同理薛夜, sum1 最里面的 mkSum 替換成 mkSum(mkSum) ,就成了 sum2 版述。

怎么樣梯澜,是不是嗅到一點遞歸的氣息啦。最關(guān)鍵的就是怎么在需要的時候把 mkSum 轉(zhuǎn)成 mkSum(mkSum)

如果可以做到這點晚伙,我們就能實現(xiàn)遞歸了吮龄。(ΦωΦ)


接下來放慢腳步,讓我們看下 sum0咆疗,因為在 sum0 中漓帚,傳給 mkSum 的參數(shù)就是它自身。那我們不妨改下 mkSum 的參數(shù)名午磁。為了區(qū)別 mkSum尝抖,我們改成 mkSum1(其實不區(qū)別也可以)。

// 改成參數(shù)名 mkSum1 提醒我們傳進(jìn)來的參數(shù)就是 mkSum
var mkSum = (mkSum1) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      return n+mkSum1(n-1)
    }
  }
}

var sum0 = mkSum(mkSum)

之前已經(jīng)提到迅皇,推導(dǎo)思想的關(guān)鍵就是在需要的時候昧辽,把 mkSum 轉(zhuǎn)成 mkSum(mkSum)。

那什么時候是需要的時候呢登颓?沒錯搅荞,就是遞歸函數(shù)在想要調(diào)用自身的時候!框咙。

所以咕痛,我們把 mkSum 改造成下面的樣子??:

// 請注視這個函數(shù)至少一分鐘。好好參悟喇嘱。( ̄ω ̄)
var mkSum = (mkSum1) => {
  return (n) => {
    if (n===0) {
      return 0
    } else {
      // 姨媽大茉贡!就是這里!婉称!把 mkSum 改成 mkSum(mkSum)
      return n+mkSum1(mkSum1)(n-1)
    }
  }
}

// 已經(jīng)實現(xiàn)遞歸块仆,所以 sum0 已經(jīng)變成 sum 了
var sum = mkSum(mkSum)

這里就是推導(dǎo)過程中最關(guān)鍵最難理解的地方了,到這一步王暗,已經(jīng)實現(xiàn)遞歸的效果了悔据,sum 已經(jīng)完美可運行。

接下來的就是在對 mkSum 函數(shù)進(jìn)行等價的轉(zhuǎn)換俗壹,從而抽象出真正的Y組合子科汗,但請務(wù)必弄懂上面的部分才繼續(xù)看下面的內(nèi)容。

第二部分

抽象出真正的Y組合子

一個小方法

在下面的推導(dǎo)之前绷雏,為了讓大家更好理解下面的內(nèi)容头滔,先寫個小方法,下面的推導(dǎo)中很多地方都用的了涎显。

那就是如何把函數(shù)中的某些內(nèi)容提取出來作為參數(shù)坤检。

var d = 1, e = 2
// 原函數(shù)
var a = c => {
  var b = d + e
  return b + c
}

// 變換形式,將 b 提取出來變成參數(shù)形式期吓,和上面的原函數(shù)等價
var aCreator = b => c => b+c
var a = aCteator(d+e)

a(10)

在接下來的內(nèi)容中早歇,凡是涉及到提取某某變成參數(shù)形式的說法,都是利用這種方法。


首先箭跳,我們簡單轉(zhuǎn)換下 mkSum 函數(shù)的形式晨另,把 mkSum1(mkSum1) 提取出來變成參數(shù)形式

var mkSum = (mkSum1) => {
  var sumR = mkSum1(mkSum1)
  
  // sumFn 不就是傳給 Y組合子 的參數(shù)么谱姓?借尿!
  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)

很簡單的提取屉来,用這個函數(shù)試下執(zhí)行 sum(3) 試試路翻?

啊咧?奶躯?剛剛明明還很正常的啊帚桩,怎么突然就爆棧了?(`谇°Д°)

具體的原因就不解釋了,自己好好想想就能得出答案了莫瞬。

為了解決爆棧的問題儡蔓,mkSum1(mkSum1) 外面裹一層函數(shù)就可以了。

var mkSum = (mkSum1) => {
  // 外面裹一層匿名函數(shù)疼邀,解決棧溢出問題
  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提取出來變成參數(shù)形式

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) 變成了 函數(shù)w
var sum = w(mkSum)

我們再把sumFn提取到mkSum外面變成參數(shù)形式

// 被提取出來的 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 已經(jīng)變成形參了
}
var mkSum = mkSumCreator(sumFn)

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

再外面包一層函數(shù)喂江,作為 Y組合子的原型

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

// 將所有元素包在一個函數(shù)中,這個函數(shù)就是 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)

要怎么才能變成這種形式呢获询?這一步不是很明顯。所以可能需要仔細(xì)觀察一下拐袜。

我們只需要給 poorY 添加一個參數(shù) sumFn吉嚣,將最外部的 sumFn 作為參數(shù)傳進(jìn)來即可。

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

// 為 poorY 添加一個參數(shù) sumFn
var poorY = (sumFn) => {
   // poorY 函數(shù)內(nèi)部不需要有任何變化
  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 以參數(shù)的形式傳進(jìn) poorY蹬铺,而不是作為自由變量
var sum = poorY(sumFn)

這時候尝哆,千呼萬喚的 Y組合子 終于要出來了,看見沒有甜攀?秋泄!

因為 poorY 內(nèi)部的 mkSumCreator 是直接調(diào)用的,所以可以把它簡化一下规阀,這樣我們就得到真正的 Y組合子了恒序。

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

var y = (sumFn) => {
  // 去掉一層調(diào)用
  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組合子:

// 參數(shù)名替換
// w -> (f => f(f))
// mkSum1 -> b
// sumFn -> fn
var Y = fn => 
  (f => f(f))
  (b => fn( x => b(b)(x) ))

之前講過 mkSum 和 mkSum1 其實并不需要嚴(yán)格區(qū)別谁撼。所以歧胁,為了更(故)加(作)簡(神)化(秘),我們就把它們都替換成 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) ))

細(xì)心的朋友應(yīng)該注意到了与帆,這個 Y 組合子生成的遞歸函數(shù)了赌,只能處理一個參數(shù),也就是 Y 組合子里面的 x 玄糟。所以我們可以修改一下勿她,讓 Y 組合子生成的遞歸函數(shù)不受參數(shù)限制。

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

結(jié)尾

推導(dǎo)到此結(jié)束阵翎,看懂了么逢并?如果沒有的話,

可以參考下另一篇文章:https://zhuanlan.zhihu.com/p/20616683 (這篇文章講得更加透徹)郭卫,

或者閱讀下 《The Little Schemer》的第九章(本文就是基于這一章寫出來的)砍聊。

最后要指出的是,在實際應(yīng)用中贰军,遞歸的實現(xiàn)并不是靠Y組合子實現(xiàn)的玻蝌,而是先保存函數(shù)的引用(比如指針),然后在需要的時候再通過指針調(diào)用自身函數(shù)词疼。這和我們的直覺是相符合俯树。

所以,理解Y組合子并沒有什么卵用贰盗,只是無聊找虐而已许饿。╮(╯▽╰)╭

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市舵盈,隨后出現(xiàn)的幾起案子陋率,更是在濱河造成了極大的恐慌,老刑警劉巖秽晚,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瓦糟,死亡現(xiàn)場離奇詭異,居然都是意外死亡爆惧,警方通過查閱死者的電腦和手機(jī)狸页,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扯再,“玉大人芍耘,你說我怎么就攤上這事∠ㄗ瑁” “怎么了斋竞?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秃殉。 經(jīng)常有香客問我坝初,道長浸剩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任鳄袍,我火速辦了婚禮绢要,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拗小。我一直安慰自己重罪,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布哀九。 她就那樣靜靜地躺著剿配,像睡著了一般。 火紅的嫁衣襯著肌膚如雪阅束。 梳的紋絲不亂的頭發(fā)上呼胚,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天,我揣著相機(jī)與錄音息裸,去河邊找鬼蝇更。 笑死,一個胖子當(dāng)著我的面吹牛界牡,可吹牛的內(nèi)容都是我干的簿寂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宿亡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纳令?” 一聲冷哼從身側(cè)響起挽荠,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎平绩,沒想到半個月后圈匆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡捏雌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年跃赚,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片性湿。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡纬傲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肤频,到底是詐尸還是另有隱情叹括,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布宵荒,位于F島的核電站汁雷,受9級特大地震影響净嘀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜侠讯,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一挖藏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厢漩,春花似錦膜眠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至粱胜,卻和暖如春柄驻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背焙压。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工鸿脓, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涯曲。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓野哭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親幻件。 傳聞我的和親對象是個殘疾皇子拨黔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,974評論 2 355

推薦閱讀更多精彩內(nèi)容