ES6函數與Lambda演算

緣起

造了一個輪子,根據GitHub項目地址,生成項目目錄樹,直觀的展示項目結構,以便於介紹項目。歡迎Star。javascript

repository-tree

技術棧:java

  • ES6
  • Vue.js
  • Webpack
  • Vuex
  • lodash
  • GitHub API

應用涉及到了展示目錄樹,實現方法不可或缺的必定是遞歸遍歷。進而開啓了我對lambda演算的探索發現之旅。git

探索發現之旅

本次乘坐的是 斐波那契 號郵輪,下面會涉及到一些 JavaScript 函數式編程中的一些基本概念。若是出現眩暈、噁心(kan bu dong)等不良反應,想下船的旅客純屬正常。常旅客請安心乘坐。es6

高階函數

函數式編程中,接受函數做爲參數,或者返回一個函數做爲結果的函數一般就被稱爲高階函數github

mapfilterreduce 均屬於高階函數,高階函數並不神祕,咱們平常編程也會用到。編程

ES6 中的 map 例子閉包

const arr = [1, 2, 3, 4, 5, 6]

const powArr = arr.map(v => v * v)

console.log(powArr) // [ 1, 4, 9, 16, 25, 36 ]

尾調用

尾調用(Tail Call)是函數式編程的一個重要概念,自己很是簡單,是指某個函數的最後一步是調用另外一個函數。尾調用便是一個做爲返回值輸出的高階函數。ide

例如:函數式編程

function f(x) {
  return g(x);
}

函數f()在尾部調用了函數g()函數

尾調用的重要性在於它能夠不在調用棧上面添加一個新的堆棧幀,而是更新它,如同迭代通常。

尾遞歸

遞歸咱們都不陌生,函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。一般被用於解釋遞歸的程序是計算階乘

// ES5
function factorial(n) {
  return n === 1 ? 1 : n * factorial(n - 1);
}

factorial(6) // => 720

// ES6
const factorial = n => n === 1 ? 1 : n * factorial(n - 1)

factorial(6) // => 720
遞歸很是耗費內存,由於須要同時保存成千上百個調用記錄,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用記錄,因此永遠不會發生「棧溢出」錯誤。對函數調用在尾位置的遞歸或互相遞歸的函數,因爲函數自身調用次數不少,遞歸層級很深,尾遞歸優化則使本來 O(n) 的調用棧空間只須要 O(1)

尾遞歸於是具備兩個特徵:

  • 調用自身函數(Self-called);
  • 計算僅佔用常量棧空間(Stack Space)。

再看看尾遞歸優化過的階乘函數:

// ES5
function factorial(n, total) {
  return n === 1 ? total : factorial(n - 1, n * total);
}

factorial(6, 1) // => 720

// ES6
const factorial = (n, total) => n === 1 ? total : factorial(n - 1, n * total)

factorial(6, 1) // => 720

Alt text

在ES6中,只要使用尾遞歸,就不會發生棧溢出,相對節省內存。

上面的階乘函數factorial,尾遞歸優化後的階乘函數使用到了total這個中間變量,爲了作到遞歸實現,確保最後一步只調用自身,把這個中間變量改寫成函數的參數,這樣作是有缺點的,爲何計算6的階乘,還要傳入兩個變量6和1呢?解決方案就是柯里化

柯里化

柯里化(Currying),是把接受多個參數的函數變換成接受一個單一參數的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。

維基百科上的解釋稍微有點繞了,簡單來講,一個 currying 的函數只傳遞給函數一部分參數來調用它,讓它返回一個閉包函數去處理剩下的參數。

// 階乘尾遞歸優化寫法
function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

const factorial = currying(tailFactorial, 1);

factorial(6) // => 720

下面看下 ES6 中的 柯里化:

const fact = (n, total) => n === 1 ? total : fact(n - 1, n * total)

const currying = f => n => m => f(m, n)

const factorial = currying(fact)(1)

factorial(6) // => 720

上面代碼經過柯里化,將尾遞歸變爲只接受單個參數的 factorial,獲得了想要的factorial(6) 獨參函數。

思考?,有木有更簡單的方法實現上面獨參尾遞歸栗子。固然有,利用ES6的函數新特性,函數默認值。

簡單化問題:

const fact = (n, total = 1) => n === 1 ? total : fact(n - 1, n * total)

factorial(6) // => 720

Lambda表達式

JavaScript 中,Lambda表達式能夠表示匿名函數。

恆等函數在 JavaScript 中的栗子:

// ES5
var f = function (x) {
  return x;
};

// ES6
const f = x => x

lambda表達式 來寫是這樣子的:λx.x

如今試着用lambda表達式寫出遞歸(匿名函數遞歸),使用具備遞歸效果的lambda表達式,將lambda表達式做爲參數之一傳入其自身。

// ES5
function factorial(f, n) {
  return n === 1 ? 1 : n * f(f, n - 1)
}

factorial(factorial, 6) // => 720

// ES6
const factorial = (f, n) => n === 1 ? 1 : n * f(f, n - 1)

factorial(factorial, 6) // => 720

是的,這麼作仍是太難看了,沒人但願寫一個階乘函數還要傳入其餘參數。解決方案仍然是柯里化。尾調用優化後的Lambda表達式遞歸:

const fact = (f, n ,total = 1) => n === 1 ? total : f(f, n - 1, n * total)

const currying = f => n => m => f(f, m ,n)

const factorial = currying(fact)()

factorial(6) // => 720

最終達到了目的,獲得了獨參函數factorial。

Lambda演算

Lambda演算中的全部函數都是匿名的,它們沒有名稱,它們只接受一個輸入變量,即獨參函數。

構建一個高階函數,它接受一個函數做爲參數,並讓這個函數將自身做爲參數調用其自身:

const invokeWithSelf = f => f(f)

用Lambda演算寫出遞歸栗子:

const fact = f => (total = 1) => n => n === 1 ? total : f(f)(n * total)(n - 1)

const factorial = fact(fact)()

factorial(6) // => 720

黑魔法Y組合子

什麼是Y組合子

Y = λf.(λx.f(xx))(λx.f(xx))

η-變換後的寫法:

Y = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

用ES6箭頭函數寫出lambda演算Y組合子

const Y = f =>
    (x => f(v => x(x)(v)))
    (x => f(v => x(x)(v)))

Y組合子推導

以匿名函數遞歸開始

const fact = f => (total = 1) => n => n === 1 ? total : f(f)(n * total)(n - 1)

const factorial = fact(fact)()

factorial(6) // => 720

上面代碼有一種模式被重複了三次, f(f) 兩次, fact(fact) 一次。爲了讓代碼更加 DRY ,嘗試把 f(f) 解耦,看成參數傳遞。

const fact = f => 
  (g => (total = 1) => n => n === 1 ? total : g(n * total)(n - 1))(f(f))
  
const factorial = fact(fact)()

factorial(6) // => Maximum call stack size exceeded

固然上面代碼運行結果會棧溢出,由於 JavaScript 中參數是 按值傳遞 的,形參必須先求值再做爲實參傳入函數,f(f) 做爲參數傳遞時,會無限遞歸調用自身,致使棧溢出。這時候就須要用到 lambda 演算中的 η-變換。其原理是用到了惰性求值

η-變換

什麼是 η-變換?若是兩個函數對於任意的輸入都能產生相同的行爲(即返回相同的結果),那麼能夠認爲這兩個函數是相等的。

lambda演算中有效的η-變換f = λx.(fx)

JavaScript中的η-變換f = x => f(x)

根據η-變換f(f) 做爲函數代入,等價於 x => f(f)(x)

const fact = x => (f => (total = 1) => n => n === 1 ? total : f(n * total)(n - 1))(v => x(x)(v))

const factorial = fact(fact)()

factorial(6) // => 720

抽離共性

也許你也已經發現f => (total = 1) => n => n === 1 ? total : f(n * total)(n - 1)這就是柯里化後的遞歸方法。抽離出 fact 方法。

const fact = f => (total = 1) => n => n === 1 ? total : f(n * total)(n - 1)

const factorial = (x => fact((v => x(x)(v))))(x => fact((v => x(x)(v))))()

factorial(6) // => 720

構建Y

將具名 fact 函數變爲匿名函數,構建一個工廠函數 Y,將 fact 函數做爲參數傳入。

const fact = f => (total = 1) => n => n === 1 ? total : f(n * total)(n - 1)

const Y = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v))) // 瞧,這不就是黑魔法Y組合子嘛

const factorial = Y(fact)()

factorial(6) // => 720

用Y組合子實現的匿名遞歸函數,它不只適用於階乘函數的遞歸處理,任意遞歸工廠函數通過Y函數後,都能獲得真正的遞歸函數。


沿途風景

斐波那契數列

在數學上,斐波那契數列是以遞歸的方法定義的:

Alt text

用文字來講:就是斐波那契數列由0和1開始,以後的斐波那契係數就由以前的兩數加和。

0,1,1,2,3,5,8,13,21,34,55,89,144,233......
Alt text

用JavaScript遞歸實現:

// 非尾遞歸
function fibonacci (n) {
  if ( n <= 1 ) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

fibonacci(6) // 13

使用尾調用優化的斐波那契數列

// 尾遞歸寫法
function fibonacci (n , before , after) {
  if( n <= 1 ) return before;
  return fibonacci (n - 1, after, before + after);
}

fibonacci(6, 1, 2) // 13

使用lambda表達式的斐波那契數列

// ES6 lambda calculus
const Y = f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v)))

const fibonacci = Y(
  f => (n) => n <= 1 ? 1 : f(n - 1) + f(n - 2)
)

fibonacci(6) // 13

德羅斯特效應

在生活中,德羅斯特效應(Droste effect)是遞歸的一種視覺形式,指一張圖片部分與整張圖片相同,一張有德羅斯特效應的圖片,在其中會有一小部分是和整張圖片相似。 而這小部分的圖片中,又會有一小部分是和整張圖片相似,以此類推,……。德羅斯特效應的名稱是因爲荷蘭著名廠牌德羅斯特(Droste) 可可粉的包裝盒,包裝盒上的圖案是一位護士拿着一個有杯子及紙盒的托盤,而杯子及紙盒上的圖案和整張圖片相同

Alt text

總結

我在作repository-tree項目的過程當中學習到了不少以前沒有接觸過的東西,這也是個人初衷,想到各類各樣的idea,去想辦法實現它,過程當中天然會提高本身的見識。以此篇博文激勵本身繼續學習下去。

參考

Lambda演算

JS 函數式編程指南

《ECMAScript 6 入門》

康托爾、哥德爾、圖靈——永恆的金色對角線

原文

ES6函數與Lambda演算
相關文章
相關標籤/搜索