如何讀懂並寫出裝逼的函數式代碼

今天在微博上看到了 有人分享了下面的這段函數式代碼,我把代碼貼到下面,不過我對原來的代碼略有改動,對於函數式的版本,咋一看,的確使人很是費解,仔細看一下,你可能就暈掉了,彷佛徹底就是天書,看上去很是裝逼,哈哈。不過,我感受解析那段函數式的代碼可能會一個比較有趣過程,並且,我之前寫過一篇《函數式編程》的入門式的文章,正好能夠用這個例子,再昇華一下原來的那篇文章,順即可以向你們更好的介紹不少基礎知識,因此寫下這篇文章。算法

先看代碼

這個代碼平淡無奇,就是從一個數組中找到一個數,O(n)的算法,找不到就返回 null。express

下面是正常的 old-school 的方式。不用多說。編程

//正常的版本
function find (x, y) {
  for ( let i = 0; i < x.length; i++ ) {
    if ( x[i] == y ) return i;
  }
  return null;
}
 
let arr = [0,1,2,3,4,5]
console.log(find(arr, 2))
console.log(find(arr, 8))

結果到了函數式成了下面這個樣子(好像上面的那些代碼在下面若影若現,不過又有點不太同樣,爲了消掉if語言,讓其看上去更像一個表達式,動用了 ? 號表達式):數組

//函數式的版本
const find = ( f => f(f) ) ( f =>
  (next => (x, y, i = 0) =>
    ( i >= x.length) ?  null :
      ( x[i] == y ) ? i :
        next(x, y, i+1))((...args) =>
          (f(f))(...args)))
 
let arr = [0,1,2,3,4,5]
console.log(find(arr, 2))
console.log(find(arr, 8))

爲了講清這個代碼,須要先補充一些知識。app

Javascript的箭頭函數
首先先簡單說明一下,ECMAScript2015 引入的箭頭表達式。箭頭函數其實都是匿名函數,其基本語法以下:函數式編程

(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression函數

// 等於 :  => { return expression; }

// 只有一個參數時,括號才能夠不加:
(singleParam) => { statements }
singleParam => { statements }性能

//若是沒有參數,就必定要加括號:
() => { statements }
下面是一些示例:優化

var simple = a => a > 15 ? 15 : a; 
simple(16); // 15
simple(10); // 10
 
let max = (a, b) => a > b ? a : b;
 
// Easy array filtering, mapping, ...
 
var arr = [5, 6, 13, 0, 1, 18, 23];
var sum = arr.reduce((a, b) => a + b);  // 66
var even = arr.filter(v => v % 2 == 0); // [6, 0, 18]
var double = arr.map(v => v * 2);       // [10, 12, 26, 0, 2, 36, 46]

看上去不復雜吧。不過,上面前兩個 simple 和 max 的例子都把這箭頭函數賦值給了一個變量,因而它就有了一個名字。有時候,某些函數在聲明的時候就是調用的時候,尤爲是函數式編程中,一個函數還對外返回函數的時候。好比下在這個例子:代理

function MakePowerFn(power) {
  return function PowerFn(base) {
    return Math.pow(base, power);
  } 
}

power3 = MakePowerFn(3); //製造一個X的3次方的函數
power2 = MakePowerFn(2); //製造一個X的2次方的函數

console.log(power3(10)); //10的3次方 = 1000
console.log(power2(10)); //10的2次方 = 100
其實,在 MakePowerFn 函數裏的那個 PowerFn 根本不須要命名,徹底能夠寫成:

function MakePowerFn(power) {
  return function(base) {
    return Math.pow(base, power);
  } 
}

若是用箭頭函數,能夠寫成:

MakePowerFn = power  => {
  return base => {
    return Math.pow(base, power);
  } 
}

咱們還能夠寫得更簡潔(若是用表達式的話,就不須要 { 和 }, 以及 return 語句 ):

MakePowerFn = power => base => Math.pow(base, power)
我仍是加上括號,和換行可能會更清楚一些:

MakePowerFn = (power) => (
  (base) => (Math.pow(base, power))
)

好了,有了上面的知識,咱們就能夠進入一個更高級的話題——匿名函數的遞歸。

匿名函數的遞歸
函數式編程立志於用函數表達式消除有狀態的函數,以及for/while循環,因此,在函數式編程的世界裏是不該該用for/while循環的,而要改用遞歸(遞歸的性能不好,因此,通常是用尾遞歸來作優化,也就是把函數的計算的狀態當成參數一層一層的往下傳遞,這樣語言的編譯器或解釋器就不須要用函數棧來幫你保存函數的內部變量的狀態了)。

好了,那麼,匿名函數的遞歸該怎麼作?

通常來講,遞歸的代碼就是函數本身調用本身,好比咱們求階乘的代碼:

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

在匿名函數下,這個遞歸該怎麼寫呢?對於匿名函數來講,咱們能夠把匿名函數當成一個參數傳給另一個函數,由於函數的參數有名字,因此就能夠調用本身了。 以下所示:

function combinator(func) {
  func(func);
}

這個是否是有點做弊的嫌疑?Anyway,咱們再往下,把上面這個函數整成箭頭函數式的匿名函數的樣子。

(func) => (func(func))
如今你彷佛就不像做弊了吧。把上面那個求階乘的函數套進來是這個樣子:

首先,先重構一下fact,把fact中本身調用本身的名字去掉:

function fact(func, n) {
  return n==0 ? 1 :  n * func(func, n-1);
}
 
fact(fact, 5); //輸出120

而後,咱們再把上面這個版本變成箭頭函數的匿名函數版:

var fact = (func, n) => ( n==0 ? 1 : n * func(func, n-1) )
fact(fact, 5)
這裏,咱們依然還要用一個fact來保存這個匿名函數,咱們繼續,咱們要讓匿名函數聲明的時候,就本身調用本身。

也就是說,咱們要把

(func, n) => ( n==0 ? 1 : n * func(func, n-1) )
這個函數當成調用參數,傳給下面這個函數:

(func, x) => func(func, x)
最終咱們獲得下面的代碼:

( (func, x) => func(func, x) ) (  //函數體
  (func, n) => ( n==0 ? 1 :  n * func(func, n-1) ), //第一個調用參數
  5 //第二調用參數
);

好像有點繞,anyway, 你看懂了嗎?沒事,咱們繼續。

動用高階函數的遞歸
可是上面這個遞歸的匿名函數在本身調用本身,因此,代碼中有hard code的實參。咱們想實參去掉,如何去掉呢?咱們能夠參考前面說過的那個 MakePowerFn 的例子,不過這回是遞歸版的高階函數了。

HighOrderFact = function(func){
  return function(n){
    return n==0 ? 1 : n * func(func)(n-1);
  };
};

咱們能夠看,上面的代碼簡單說來就是,須要一個函數作參數,而後返回這個函數的遞歸版本。那麼,咱們怎麼調用呢?

fact = HighOrderFact(HighOrderFact);
fact(5);

連起來寫就是:

HighOrderFact ( HighOrderFact ) ( 5 )

可是,這樣讓用戶來調用很不爽,因此,以咱們一個函數把 HighOrderFact ( HighOrderFact ) 給代理一下:

fact = function ( hifunc ) {
  return hifunc ( hifunc );
} (
  //調用參數是一個函數
  function (func) { 
    return function(n){
      return n==0 ? 1 : n * func(func)(n-1);
    };
  }
);

fact(5); //因而咱們就能夠直接使用了
用箭頭函數重構一下,是否是簡潔了一些?

fact = (highfunc => highfunc ( highfunc ) ) (
  func => n =>  n==0 ? 1 : n * func(func)(n-1)
);

上面就是咱們最終版的階乘的函數式代碼。

回顧以前的程序
咱們再來看那個查找數組的正常程序:

//正常的版本
function find (x, y) {
  for ( let i = 0; i < x.length; i++ ) {
    if ( x[i] == y ) return i;
  }
  return null;
}

先把for幹掉,搞成遞歸版本:

function find (x, y, i=0) {
  if ( i >= x.length ) return null;
  if ( x[i] == y ) return i;
  return find(x, y, i+1);
}

而後,寫出帶實參的匿名函數的版本(注:其中的if代碼被重構成了 ?號表達式):

( (func, x, y, i) => func(func, x, y, i) ) (  //函數體
  (func, x, y, i=0) => (
      i >= x.length ?  null :
         x[i] == y  ?  i : func (func, x, y, i+1)
  ), //第一個調用參數
  arr, //第二調用參數
  2 //第三調用參數
)

最後,引入高階函數,去除實參:

const find = ( highfunc => highfunc( highfunc ) ) (
   func => (x, y, i = 0) => (
     i >= x.length ?  null :
           x[i] == y  ?  i : func (func) (x, y, i+1)
   )
);

注:函數式編程裝逼時必定要用const字符,這表示我寫的函數裏的狀態是 immutable 的,天生驕傲!

再注:我寫的這個比原來版的那個簡單了不少,原來版本的那個又在函數中套了一套 next, 並且還動用了不定參數,固然,若是你想裝逼裝到天上的,理論上來講,你能夠套N層,呵呵。

如今,你能夠體會到,如此逼裝的是怎麼來的了吧?。

其它
你還別說這就是裝逼,簡單來講,咱們可使用數學的方式來完成對複雜問題的描述,那怕是遞歸。其實,這並非新鮮的東西,這是Alonzo Church 和 Haskell Curry 上世紀30年代提出來的東西,這個就是 Y Combinator 的玩法,關於這個東西,你能夠看看下面兩篇文章:

《The Y Combinator (Slight Return)》,

《Wikipedia: Fixed-point combinator》

(全文完)

相關文章
相關標籤/搜索