今日、Weibo で次の機能コードを共有している人を見かけました。私は元のコードを少し変更しましたが、一見したところ、それは確かに非常に印象的です。よく見ると気絶してしまいます。まるで天国からの完全な本のようです(笑)。しかし、その関数コードを解析することは、より興味深いプロセスである可能性があると感じています。また、以前に「関数型プログラミング」の入門記事を書きましたが、この例を元の記事に昇華することができます。基礎知識をたくさん紹介したほうが良いと思い、この記事を書きました。
まずコードを見てみましょう
このコードは、配列から数値を見つけるための O(n) アルゴリズムです。見つからない場合は、null を返します。
以下は通常の昔ながらの方法です。言うまでもなく。
//正常的版本 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]
複雑そうには見えません。ただし、上記の最初の 2 つの単純な例と最大の例は両方ともアロー関数を変数に割り当てているため、名前が付いています。特に関数プログラミングでは、関数が外部関数も返す場合、特定の関数が宣言されたときに呼び出されることがあります。たとえば、この例では、
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 => 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); }
これは少し不正行為の疑いがありますか?とにかく、さらに進んで、上記の関数をアロー関数スタイルの無名関数に変換しましょう。
(func) => (func(func))
今のところ、あなたは浮気しているようには見えません。上記の階乗関数を挿入すると次のようになります:
まず、ファクトを再構築し、ファクト内で自分自身と呼ぶ名前を削除します:
function fact(func, n) { return n==0 ? 1 : n * func(func, n-1); } fact(fact, 5); //输出120
次に、上記のバージョンをアロー関数に変換します。 匿名関数のバージョン: var fat = (func, n) => ( n==0 ? 1 : n * func(func, n-1) )
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 //第二调用参数 );
とにかく複雑です、わかりますか?大丈夫、続けましょう。
高階関数の再帰を使用する
しかし、上記の再帰匿名関数はそれ自体を呼び出すため、コード内にはハードコードの実際のパラメータが存在します。実際のパラメータを削除したいのですが、どうすれば削除できますか?前述の 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》