今天在微博上看到了有人分享了下面的這段函數式程式碼,我把程式碼貼到下面,不過我對原來的程式碼略有改動,對於函數式的版本,咋一看,的確令人非常費解,仔細看一下,你可能就暈掉了,似乎完全就是天書,看起來非常裝逼,哈哈。不過,我感覺解析那段函數式的程式碼可能會一個比較有趣過程,而且,我以前寫過一篇《函數式程式設計》的入門式的文章,正好可以用這個例子,再昇華一下原來的那篇文章,順便向大家更好的介紹很多基礎知識,所以寫下這篇文章。
先看程式碼
這個程式碼平淡無奇,就是從一個陣列中找到一個數,O(n)的演算法,找不到就回傳 null。
下面是正常的 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))
為了講清這個程式碼,需要先補充一些知識。
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); } }
如果用箭頭函數,可以寫成:
reee更簡潔(如果用表達式的話,就不需要{ 和}, 以及return 語句):
MakePowerFn = power => base => Math.pow(base, power)
我還是加上括號,和換行可能會更清楚:
MakePowerFn = power => { return base => { return Math.pow(base, power); } }
好了,有了上面的知識,我們就可以進入一個更高級的話題——匿名函數的遞歸。
匿名函數的遞歸
函數式程式設計立志於用函數表達式消除有狀態的函數,以及for/while循環,所以,在函數式程式設計的世界裡是不應該用for/while循環的,而要改用遞歸(遞歸的效能很差,所以,一般是用尾遞歸來做優化,也就是把函數的計算的狀態當成參數一層一層的往下傳遞,這樣語言的編譯器或解釋器就不需要用函數堆疊來幫你保存函數的內部變數的狀態了)。
好了,那麼,匿名函數的遞歸該怎麼做?
一般來說,遞歸的程式碼就是函數自己呼叫自己,例如我們求階乘的程式碼:
MakePowerFn = (power) => ( (base) => (Math.pow(base, power)) )
在匿名函數下,這個遞歸該怎麼寫呢?對匿名函數來說,我們可以把匿名函數當成一個參數傳給另一個函數,因為函數的參數有名字,所以就可以呼叫自己了。 如下:
function fact(n){ return n==0 ? 1 : n * fact(n-1); }; result = fact(5);
這個是不是有點作弊的嫌疑? Anyway,我們再往下,把上面這個函數整成箭頭函數式的匿名函數的樣子。
function combinator(func) { func(func); }
現在你似乎就不像作弊了吧。把上面那個求階乘的函數套進來是這個樣子:
首先,先重構一下fact,把fact中自己調用自己的名字去掉:
(func) => (func(func))
然後,我們再把上面這個版本變成箭頭函數的匿名函數版:var fact = (func, n) => ( n==0 ? 1 : n * func(func, n-1) )
function fact(func, n) { return n==0 ? 1 : n * func(func, n-1); } fact(fact, 5); //输出120
在這裡,我們仍可使用這個一個一個匿名來保存這個一個函數,我們繼續,我們要讓匿名函數宣告的時候,就自己呼叫自己。
也就是說,我們要把
fact(fact, 5)
這個函數當成呼叫參數,傳給下面這個函數:
(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 //第二调用参数 );
HighOrderFact = function(func){ return function(n){ return n==0 ? 1 : n * func(func)(n-1); }; };
fact = HighOrderFact(HighOrderFact); fact(5);看懂了嗎?沒事,我們繼續。
HighOrderFact ( HighOrderFact ) ( 5 )🎜我們可以看,上面的程式碼簡單說來就是,需要一個函數做參數,然後回傳這個函數的遞歸版本。那麼,我們要怎麼調用呢? 🎜🎜🎜
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》