如何讀懂並寫出裝逼的函式式程式碼
今天在微博上看到了 有人分享了下面的這段函式式程式碼,我把程式碼貼到下面,不過我對原來的程式碼略有改動,對於函式式的版本,咋一看,的確令人非常費解,仔細看一下,你可能就暈掉了,似乎完全就是天書,看上去非常裝逼,哈哈。不過,我感覺解析那段函式式的程式碼可能會一個比較有趣過程,而且,我以前寫過一篇《函數語言程式設計》的入門式的文章,正好可以用這個例子,再昇華一下原來的那篇文章,順便可以向大家更好的介紹很多基礎知識,所以寫下這篇文章。
先看程式碼
這個程式碼平淡無奇,就是從一個陣列中找到一個數,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); } }
如果用箭頭函式,可以寫成:
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》
相關文章
- 感悟篇:如何寫好函式式程式碼函式
- 程式碼的藝術:如何寫出小而清晰的函式函式
- 乾淨的程式碼: 編寫可讀的函式函式
- 提高程式碼質量:如何編寫函式函式
- 讀寫日誌函式函式
- 分享常用的CSS函式,助你寫出更簡潔的程式碼CSS函式
- 提升逼格的兩個函式函式
- 寫擴充套件性好的程式碼:函式套件函式
- 寫讓別人能讀懂的程式碼
- 函式式JavaScript(2):如何打造“函式式”程式語言?函式JavaScript
- 如何更好的編寫async函式函式
- 一輛車幫你讀懂python函式Python函式
- 臨時讀原始碼的函式原始碼函式
- TOM大神編寫的show_space函式程式碼函式
- jQuery函式的等價原生函式程式碼示例jQuery函式
- Linux網路程式設計--完整的讀寫函式(轉)Linux程式設計函式
- 如何寫好 C main 函式AI函式
- 如何編寫翻頁函式?函式
- 箭頭函式、簡寫函式、普通函式的區別函式
- VUE起手式-如何開始寫程式碼Vue
- 你真的懂函式嗎?函式
- 如何編寫冪等的Bash指令碼(函式)? · Fatih Arslan指令碼函式
- 原生ajax()函式封裝程式碼例項函式封裝
- 讀寫INI檔案的四個函式 (轉)函式
- python函式程式設計 返回函式 匿名函式 裝飾器 偏函式Python函式程式設計
- vfork函式建立出的父子程式函式
- 編寫讓別人能夠讀懂的程式碼
- 一圖秒懂函式防抖和函式節流函式
- js函式作為函式的引數程式碼例項JS函式
- 一段柯里化函式程式碼閱讀函式
- 讀 zepto 原始碼之工具函式原始碼函式
- [PHP原始碼閱讀]strlen函式PHP原始碼函式
- PHP原始碼閱讀:count函式PHP原始碼函式
- 用TypeScript編寫釋出函式庫TypeScript函式
- JavaScript函式體程式碼JavaScript函式
- jQuery的ready函式原始碼解讀jQuery函式原始碼
- oracle的並行管道函式Oracle並行函式
- 如何在main函式前後執行程式碼AI函式行程