javascript関数式プログラミングを深く検討する

11383 ワード


  ,          。    。   。    。    。 
                      - John Carmack,  《    》     


関数式プログラミングはすべて1つの問題をどのように一連の関数に分解するかについてです.通常、関数はリンクされ、互いにネストされ、往復伝達され、ファーストクラスの公民と見なされます.jQueryやNodeなどを使ったことがあるならjsのようなフレームワークは、このような技術を使ったことがあるはずですが、意識していません.
私たちはJavascriptの小さな気まずいことから始めました.
通常のオブジェクトに値を割り当てる値のリストが必要です.これらのオブジェクトには、データ、HTMLオブジェクトなどが含まれます.

var
  obj1 = {value: 1},
  obj2 = {value: 2},
  obj3 = {value: 3};
var values = [];
function accumulate(obj) {
 values.push(obj.value);
}
accumulate(obj1);
accumulate(obj2);
console.log(values); // Output: [obj1.value, obj2.value]

このコードは使えますが不安定です.任意のコードはaccumulate()関数でvaluesオブジェクトを変更しないことができます.そしてvaluesに上空配列[]を割り当てるのを忘れたら、このコードはまったく働かない.
しかし、変数が関数内に宣言されている場合、彼はいたずらのコードに変更されません.

function accumulate2(obj) {
 var values = [];
 values.push(obj.value);
 return values;
}
console.log(accumulate2(obj1)); // Returns: [obj1.value]
console.log(accumulate2(obj2)); // Returns: [obj2.value]
console.log(accumulate2(obj3)); // Returns: [obj3.value]

だめだよ.最後に入力されたオブジェクトの値のみが返されます.
この問題は,最初の関数の内部に関数をネストすることによって解決できるかもしれない.

var ValueAccumulator = function(obj) {
 var values = []
 var accumulate = function() {
  values.push(obj.value);
 };
 accumulate();
 return values;
};

しかし、問題は依然として存在し、accumulate関数とvalues変数にアクセスできません.
自己呼び出し関数が必要です
セルフコール関数と閉パッケージ
values配列を順次返す関数式を返すことができたらどうですか.関数内で宣言された変数は、自己呼び出し関数を含む関数内のすべてのコードにアクセスできます.
自己呼び出し関数を使用することで、前の気まずいことがなくなりました.

var ValueAccumulator = function() {
 var values = [];
 var accumulate = function(obj) {
  if (obj) {
   values.push(obj.value);
   return values;
  } else {
   return values;
  }
 };
 return accumulate;
};
//This allows us to do this:
var accumulator = ValueAccumulator();
accumulator(obj1);
accumulator(obj2);
console.log(accumulator());
// Output: [obj1.value, obj2.value]
ValueAccumulator = ->
 values = []
 (obj) ->
  values.push obj.value if obj
  values

これらはすべて役割ドメインに関するものです.変数valuesは、外部のコードがこの関数を呼び出すときでも、内部関数accumulate()で表示されます.これを閉バッグと言います.
Javascriptの閉パッケージは、親関数が実行済みであっても、親ドメインにアクセスできる関数です.
閉パッケージは、すべての関数言語が持つ特徴です.従来のコマンド言語には閉パッケージはありません.
高次関数
自己呼び出し関数は実際には高次関数の形式である.高次関数とは、他の関数を入力としたり、出力として関数を返したりする関数です.
高次関数は従来のプログラミングでは一般的ではない.コマンドプログラマがループを使用して配列を反復する場合、関数プログラマはまったく異なる実装方法を採用します.高次関数により、配列内の各要素を関数に適用し、新しい配列を返すことができます.
これは関数式プログラミングの中心思想である.高次関数は論理をオブジェクトのように関数に伝達する能力を持つ.
Javascriptでは関数がファーストクラスとして扱われており,これはScheme,Haskellなどの古典的な関数と言語である.この話は少し変に聞こえるかもしれませんが、実際には関数が数字やオブジェクトのように基本的なタイプとされていることを意味します.数値とオブジェクトが往復可能であれば、関数も可能です.
実際に見てみましょう.前節のValueAccumulator()関数を高次関数と組み合わせて使用します://forEach()を使用して配列を巡り、各要素にコールバック関数accumulator 2 var accumulator 2=ValueAccumulator()を呼び出します.var objects = [obj1, obj2, obj3];//この配列は大きいforEach(accumulator2); console.log(accumulator2());
じゅんかんすう
純粋な関数が返す計算結果は、入力されたパラメータにのみ関連します.ここでは外部の変数やグローバル状態は使用されず、副作用はありません.すなわち,入力として入力される変数は変更できない.したがって,プログラムでは純関数で返される値しか使用できない.
数学関数で簡単な例を挙げる.Math.sqrt(4)は、常に2を返し、設定やステータスなどの隠し情報を使用せず、副作用をもたらさない.
純粋な関数は数学上の「関数」の実演であり、入力と出力の関係である.それらの考え方は簡単で再利用しやすい.純粋な関数は完全に独立しているため、何度も使用するのに適しています.
例を挙げて、非純関数と純関数を比較します.

//              
var printCenter = function(str) {
 var elem = document.createElement("div");
 elem.textContent = str;
 elem.style.position = 'absolute';
 elem.style.top = window.innerHeight / 2 + "px";
 elem.style.left = window.innerWidth / 2 + "px";
 document.body.appendChild(elem);
};
printCenter('hello world');
//           
var printSomewhere = function(str, height, width) {
 var elem = document.createElement("div");
 elem.textContent = str;
 elem.style.position = 'absolute';
 elem.style.top = height;
 elem.style.left = width;
 return elem;
};
document.body.appendChild(
printSomewhere('hello world',
window.innerHeight / 2) + 10 + "px",
window.innerWidth / 2) + 10 + "px"));

非純関数はwindowオブジェクトの状態に依存して幅と高さを計算し、自給自足の純関数はこれらの値をパラメータとして入力することを要求します.実際には、情報をどこにでも印刷することができ、この関数をより多くの用途に使用することができます.
非純粋な関数は、要素を返すのではなく、自分の内部で追加要素を実現するため、より容易な選択のように見えます.値を返した純粋な関数printSomewhere()は、他の関数プログラミング技術との組み合わせでよりよく表現されます.

var messages = ['Hi', 'Hello', 'Sup', 'Hey', 'Hola'];
messages.map(function(s, i) {
 return printSomewhere(s, 100 * i * 10, 100 * i * 10);
}).forEach(function(element) {
 document.body.appendChild(element);
});

関数が純粋で,すなわち状態や環境に依存しない場合,実際にいつ計算されるかにかかわらず計算する必要はない.後の不活性評価はこれについて述べる.
匿名関数
関数をトップオブジェクトとするもう一つの利点は匿名関数である.
名前が示すように、匿名関数は名前のない関数です.実際にはそれだけではありません.フィールドで一時的な論理を定義する能力を可能にします.通常、これは便利です.1つの関数を1回しか使用しない場合は、変数名を無駄にする必要はありません.
匿名関数の例を次に示します.

//           
function() {
 return "hello world"
};

//            
var anon = function(x, y) {
 return x + y
};

//               ,               
setInterval(function() {
 console.log(new Date().getTime())
}, 1000);
// Output: 1413249010672, 1413249010673, 1413249010674, ...

//                 ,       ,
//       undefined      :
setInterval(console.log(new Date().getTime()), 1000)
// Output: 1413249010671

匿名関数と高次関数を組み合わせて使用する例を示します

function powersOf(x) {
 return function(y) {
  // this is an anonymous function!
  return Math.pow(x, y);
 };
}
powerOfTwo = powersOf(2);
console.log(powerOfTwo(1)); // 2
console.log(powerOfTwo(2)); // 4
console.log(powerOfTwo(3)); // 8
powerOfThree = powersOf(3);
console.log(powerOfThree(3)); // 9
console.log(powerOfThree(10)); // 59049

ここで返される関数は名前を付ける必要はありません.powersOf()関数の外のどこでも使用できます.これが匿名関数です.
アキュムレータの関数を覚えていますか?匿名関数で書き換えることができます

var
 obj1 = { value: 1 },
 obj2 = { value: 2 },
 obj3 = { value: 3 };
 
var values = (function() {
 //     
 var values = [];
 return function(obj) {
  //        !
  if (obj) {
   values.push(obj.value);
   return values;
  } else {
   return values;
  }
 }
})(); //      
console.log(values(obj1)); // Returns: [obj.value]
console.log(values(obj2)); // Returns: [obj.value, obj2.value]
obj1 = { value: 1 }
obj2 = { value: 2 }
obj3 = { value: 3 }

values = do ->
 valueList = []
 (obj) ->
  valueList.push obj.value if obj
  valueList
console.log(values(obj1)); # Returns: [obj.value]
console.log(values(obj2)); # Returns: [obj.value, obj2.value]


いいですね.高次匿名純関数.どうしてこんなにラッキーなの?実際にはそれだけではありませんが、この中には自己実行の構造があります.();.関数の後ろに付いているカッコは、関数をすぐに実行することができます.上記の例では,外部valuesに与えられた値は関数実行の結果である.
匿名関数は文法糖だけでなく、lambda演算の化身である.私の話を聞いてください......lambda演算はコンピュータとコンピュータ言語が発明されたずっと前から現れています.それはただ関数を研究する数学の概念にすぎない.尋常ではないのは、変数参照、関数呼び出し、匿名関数の3つの式しか定義されていないが、図霊が完全であることが分かったことだ.現在、lambda演算はjavascriptを含むすべての関数言語の核心にあります.このため、匿名関数はlambda式と呼ばれることが多い.
匿名関数にも欠点があります.これは、呼び出しスタックで認識されにくく、デバッグに困難をもたらすことです.匿名関数の使用に注意してください.
メソッドチェーン
Javascriptでは、メソッドチェーンを一緒にするのが一般的です.jQueryを使ったことがあるなら、このテクニックを使ったことがあるはずです.「コンストラクタモード」と呼ばれることもあります.
この技術は,複数の関数が1つのオブジェクトに順次適用されるコードを簡略化するために用いられる.

//            ,  ……
arr = [1, 2, 3, 4];
arr1 = arr.reverse();
arr2 = arr1.concat([5, 6]);
arr3 = arr2.map(Math.sqrt);

// ……             
console.log([1, 2, 3, 4].reverse().concat([5, 6]).map(Math.sqrt));
//              
console.log(((([1, 2, 3, 4]).reverse()).concat([5, 6])).map(Math.sqrt));


これは、関数がターゲットオブジェクトが持つメソッドである場合にのみ有効です.2つの配列zipを一緒にするなど、独自の関数を作成する場合は、Arrayとして宣言する必要があります.prototypeオブジェクトのメンバー次のコードクリップを見てください:Array.prototype.zip = function(arr2) {  //... }
これによりarr.zip([11,12,13,14).map(function(n){return*2});//Output:2,22,4,24,6,26,8,28と書くことができます.
再帰
再帰は最も有名な関数式プログラミング技術であるべきである.関数がそれ自体を呼び出します.
関数が自分を呼び出すと、変なことが起こることがあります.その表現はループであり、同じコードを複数回実行し、関数スタックでもある.
再帰関数を使用する場合は、無限ループ(ここでは無限再帰と呼ぶべき)を十分に避ける必要があります.ループのように、停止条件が必要です.これを基準シナリオ(base case)と呼ぶ.
次に例を示します

var foo = function(n) {
 if (n < 0) {
  //     
  return 'hello';
 } else {
  //     
  return foo(n - 1);
 }
}
console.log(foo(5));

訳注:原文のコードに誤りがあり、再帰状況の関数呼び出しにreturnが欠けており、関数の実行が最後に結果が出ない.ここで修正しました.
再帰とサイクルは互いに変換できる.しかし、再帰アルゴリズムは、ループを使用するのに苦労する場合があるため、より適切であり、必要であることが多い.
明らかな例は木を巡ることです.

var getLeafs = function(node) {
 if (node.childNodes.length == 0) {
  // base case
  return node.innerText;
 } else {
  // recursive case:
  return node.childNodes.map(getLeafs);
 }
}

分けて治める
再帰はforとwhileサイクルの代わりに面白い方法だけではない.問題を解決できるまで再帰的により小さな状況に分割する分割というアルゴリズムがある.
歴史上、2つの数の最大センチ母を見つけるためのユークリッドアルゴリズムがあります.

function gcd(a, b) {
 if (b == 0) {
  //      ( )
  return a;
 } else {
  //      ( )
  return gcd(b, a % b);
 }
}

console.log(gcd(12,8));
console.log(gcd(100,20));

gcb = (a, b) -> if b is 0 then a else gcb(b, a % b)


理論的には、分けて治すのはすごいですが、現実には役に立ちますか?もちろん!Javascriptの関数で配列をソートするのはよくありません.元の配列を置き換えるだけでなく、データは変わらないわけではありません.信頼性が高く、柔軟ではありません.分けて治めることで、私たちはもっとよくすることができます.
すべての実装コードは約40行かかります.ここでは偽コードのみを示します.

var mergeSort = function(arr) {
 if (arr.length < 2) {
  //     :   0 1            
  return items;
 } else {
  //     :      、  、  
  var middle = Math.floor(arr.length / 2);
  //  
  var left = mergeSort(arr.slice(0, middle));
  var right = mergeSort(arr.slice(middle));
  //  
  // merge       ,       ,           
  return merge(left, right);
 }
}

訳注:分けて治める考え方でソートするより良い例は、ファストソートであり、Javascriptを使っても13行のコードしかありません.具体的には私の以前のブログ「優雅な関数式プログラミング言語」を参考にしてください.
ふかっせいひょうか
不活性評価は、非厳密評価とも呼ばれ、必要に応じて呼び出され、実行が遅延し、必要に応じて関数結果を計算する評価ポリシーであり、関数プログラミングに特に役立ちます.たとえば、行コードがx=func()である場合、このfunc()関数を呼び出して得られる戻り値はxに与えられます.しかし、xが何に等しいかは、xが必要になるまで重要ではありません.xが必要になるまでfunc()を呼び出すのは不活性評価である.
このポリシーは、特にメソッドチェーンや配列などの関数プログラマーが最も好むプログラムフロー技術を使用する場合、パフォーマンスを著しく向上させることができます.惰性求値がワクワクする利点の一つは、無限シーケンスを可能にすることである.本当に遅延を続けることができない前に、本当に計算する必要はありませんから.このようにすることができます.

//     JavaScript   :
var infinateNums = range(1 to infinity);
var tenPrimes = infinateNums.getPrimeNumbers().first(10);

これは多くの可能性に扉を開けた.例えば、非同期実行、並列計算、組合せであり、これはわずかに列挙されている.
しかしながら、Javascript自体が不活性評価をサポートしていない、すなわちJavascriptに不活性評価をシミュレートさせる関数ライブラリが存在するという問題がある.これは第3章のテーマである.