フレームワークの概要
はじめに
関数型プログラミングとは?
関数は純粋関数
- 純粋関数とは
- 関数の副作用
- 純粋関数使用の利点
- テスト可能性
- キャッシュ可能性
- 移植性
- 純粋関数の特徴
- 不変性
- 参照の透明性
- 宣言的プログラミング
機能は一流市民。
- クロージャ
- 高階関数
- 関数のコリエリゼーション
- 関数の合成
结合使用
拡張機能
はじめに
関数型プログラミングの歴史は長いですが、ここ数年で頻繁に表舞台に登場するようになり、関数型プログラミングをサポートしていない多くの言語が、クロージャや匿名関数など、関数型プログラミングらしい機能を積極的に追加しています。フロントエンドフレームワークの多くも、関数型プログラミングの機能の利用を誇っており、関数型プログラミングの利用率は非常に高く、RxJS、cycleJS、ramdaJS、lodashJS、underscoreJSなど、関数型プログラミングに特化したフレームワークやライブラリが数多く存在します。関数型プログラミングはますます普及しており、このプログラミングパラダイムをマスターすることは、高品質でメンテナンスしやすいコードを書くために必要です。
関数型プログラミングとは?
ウィキペディアの定義
関数型プログラミングは、一般化プログラミングとも呼ばれ、コンピュータ操作を数学的な関数計算として扱い、状態の変化や変更可能なデータを避けるプログラミングパラダイムです。
関数型プログラミングの考え方の簡単な理解
関数型プログラミングでは、オブジェクト指向プログラミングの考え方の違いに触れてきました:
- オブジェクト指向の世界では、物事をクラスやオブジェクトに抽象化し、カプセル化、継承、ポリモーフィズムによってそれらの関係を示します。
- 機能的世界は、世界を事物と事物間の関係に抽象化し、このように世界をモデル化します。
オブジェクト指向プログラミングは、より人間的で、より社会的です。例えば、あなたが車を買いたいと思い、誰かに値段の相談をしたいとします。たまたま、あなたの同級生の友達のいとこの次女が4Sショップで働いているとします。必要なのは
import "
import "
import "ビッグ・カズン"
import "二番目の義理の姉"
次に、2番目の義姉を呼び出します。バーゲン();
ファンクショナル・プログラミングはもう少し "冷たい "もので、工場の組み立てラインが常に動いているようなものです。また、外部環境に依存することもなく、組み立てラインを動かすだけでどこでも生産することができます。同級生の友達のいとこの二番目の義理のお姉さんの助けを借りる必要はありません。
データに重点を置くオブジェクト指向プログラミングとは対照的に、関数型プログラミングはアクションに重点を置き、現在のアクションを抽象化した思考のプロセスです。
例えば、ある数字に4を足した値に4を掛ける計算をしたい場合、通常のコードの書き方は次のようになります。
function calculate(x){
return (x + 4) * 4;
}
console.log(calculate(1)) // 20
通常の開発では、繰り返しが必要な操作を関数にカプセル化し、別の場所から呼び出せるようにすることがよくあるからです。しかし、関数型プログラミングの考え方からすると、最初に「4を足す」、次に「4を掛ける」という一連の操作の動作に焦点が置かれます。関数をカプセル化するベストプラクティスとは?どうすれば関数をより汎用的で快適に使えるようになるでしょうか?関数型プログラミングにおける合成がヒントになるかもしれません。そういえば
関数型プログラミングには2つの基本的な特徴があります。
- 関数は純粋関数
- 機能は一流の市民です。
関数型プログラミングには2つの基本操作があります。
- コリア化
- 合成
関数は純粋関数
純粋関数とは
関数型プログラミングを理解しようとするときに知っておくべき最初の基本概念は純粋関数です。
ある関数が純粋かどうかを知るにはどうすればいいのでしょうか?ここに非常に厳密な定義があります:
- 1.この関数は、同じ入力値に対して常に同じ出力を生成します。
- 2.副作用がありません。
関数が返す結果は、その引数にのみ依存します。
let xs = [1,2,3,4,5]
//
xs.slice(0,3) //[1,2,3]
xs.slice(0,3) //[1,2,3]
xs.slice(0,3) //[1,2,3]
// 不純な関数
xs.splice(0,3) //[1,2,3]
xs.splice(0,3) //[4,5]
xs.splice(0,3) //[]
ある関数を何度も繰り返し実行し、値が変更されて後の関数操作に影響を与えること。純粋関数とは、入力が何であろうと一意な値を出力することに対応する関数です。
これは純粋関数の第一条件です。関数が返す結果は、その引数のみに依存します。
次に、ポイント2「関数実行に副作用はない」について説明します。
副作用とは、結果の計算中にシステムの状態が変化すること、または外部事象との相互作用が観察されること。 別の例を考えてみましょう:
var min = 21;
// 不純な関数
var ckeck = function(age){
return age >= min;
}
上のコードでは、変数が関数のスコープ外で定義されているため、関数は「不純」と呼ばれています。
//
var check = function(age){
var min = 21;
return age >= min;
}
上記のコードでは、スコープ内のローカル変数のみが評価され、スコープ外の変数は変更されません。
外部変数を変更する以外にも、DOM APIを呼び出してページを変更するとき、Ajaxリクエストを送信するとき、window.reloadを呼び出してブラウザをリフレッシュするとき、console.logが副作用としてコンソールにデータを出力するときなど、関数が実行中に外部から観察可能な変化を生み出す方法はたくさんあります。
機能の副作用とは?
関数副作用とは、関数呼び出しが関数関数値を返す以外に、呼び出し元の関数付加的な影響を与えることです。副作用を持つ関数は単に値を返すだけでなく、次のような他のことも行います:
- 1.変数の変更
- 2、データ構造を直接修正
- 3.オブジェクトのメンバーの設定
- 4.例外をスローするか、エラーで終了します。
- 5.ターミナルへの印刷またはユーザー入力の読み取り
- 6.ファイルの読み書き
- 画面上にグラフを描くことができます。
関数副作用は、プログラムの設計に不要なトラブルをもたらす、プログラムをもたらすエラーを見つけることは非常に困難であり、プログラムの可読性を低下させる、厳格な関数型言語では、関数は副作用がない必要があります。
純粋関数を使用する利点
なぜわざわざ純粋関数を作るのでしょうか?なぜなら純粋関数はとても "信頼できる "からです。 純粋関数を実行しても悪いことをする心配はありませんし、予測不可能な振る舞いをすることもありませんし、外部への影響もありません。純粋関数は予測できない振る舞いをしませんし、外的な影響も受けません。 関数は、いつでも、どこでも、あなたが与えたものを吐き出すだけです。アプリケーションのほとんどの関数が純粋な関数で構成されていれば、プログラムのテストやデバッグはとても簡単になります。
テスト可能性
要約すると、これはとてもシンプルで、他の外部情報を気にする必要はなく、ただ関数に特定の入力を与え、その出力を主張するだけです。単純な例としては、数字の集合を受け取り、それぞれの数字に1を足すような砂のような操作を実行するようなものです。
let list = [1, 2, 3, 4, 5];
const incrementNumbers = (list) => list.map(number => number + 1);
数値の配列を受け取り、map を使って各数値をインクリメントし、インクリメントされた数値の新しいリストを返します。
incrementNumbers(list); // [2, 3, 4, 5, 6]
入力[1,2,3,4,5]に対して、期待される出力は[2,3,4,5,6]です。
キャッシュ可能性
純粋な関数は、入力に基づいてキャッシュすることができます。
// 次のコードは同じ入力で見つけることができ、2番目の呼び出しはキャッシュから直接取り出される。
let squareNumber = memoize((x) => { return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 入力値4の結果をキャッシュから読み出す
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // 入力値5の結果をキャッシュから読み出す
//=> 25
それを実現するには? 以下のコードを見てください。
const memoize = (f) => {
// クロージャを使用するため、関数が実行された直後にキャッシュが再生されることはない。
const cache = {};
return () => {
var arg_str = JSON.stringify(arguments);
// キーはここにある、同じ入力と同じ出力のロジックの純粋な関数の使用は、単純なキャッシュを行うためにキャッシュの使用は、このパラメータが古く使用されている場合、すぐに行の結果を返す!
cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
return cache[arg_str];
};
};
移植性
純粋関数は自己完結型なので、必要なものはすべて入力パラメータですでに宣言されており、どこにでも移植できます。
そして純粋関数は、その正式な参照からわかるように、依存関係について正直です。 純粋関数は、とても立派な恋人なのです。
//
var signUp = function(attrs){
// 副作用のある演算
var user = saveUser(attrs);
welcomeUser(user);
}
//
// 同じ入力は常に関数を返す
var signUp = function(Db, Email, attrs){
return function(){
//副作用のある演算
var user = saveUser(Db, attrs);
welcomeUser(Email, user);
}
}
純粋関数の特徴
不変性
イミュータビリティは純粋関数の特徴です。データがイミュータブルである場合、純粋関数が作成された後にその状態を変更することはできません。
フロントエンドの開発者であれば、JSにおけるオブジェクトの概念の威力を感じていることでしょう。JSではすべてがオブジェクトです」。Stringのようなコア機能から、配列、ブラウザAPIまで、「オブジェクト」という概念はあらゆるところにあります。
JSのオブジェクトはとても素晴らしいもので、自由にコピーしたり、プロパティを変更したり削除したりすることができます。しかし、1つだけ覚えておいてください:
"特権には大きな責任が伴う"
この方法ではオブジェクトを変更することになり、副作用が生じます。これは関数型プログラミングの考え方に反しており、だからこそ関数型プログラミングは不変データという概念を考え出したのです。
イミュータブルデータとは、作成後に変更できないデータのことです。他の多くの言語と同じように、JavaScript にももともとイミュータブルな基本型がありますが、例えばオブジェクトは任意の場所で変更可能なだけです:
let arr = [1, 2, 3, 4, 5];
arr.splice(1, 1); // [2];
console.log(arr); // [1, 3, 4, 5];
これは通常の「配列から項目を削除する」操作です。まあ、何の問題もないんですけどね。
問題は、可変性を「乱用」することで、プログラムに「副作用」を引き起こすことです。副作用」が何であるかについては心配しないでください。
まずはコード例を見てみましょう:
const student1 = {
school: '',
name: 'HOU Ce',
birthdate: '1995-12-15',
}
const changeStudent = (student, newName, newBday) => {
const newStudent = student;
newStudent.name = newName;
newStudent.birthdate = newBday;
return newStudent;
}
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "", name: "YAN Haijing", birthdate: "1990-11-10"}
// Object {school: "", name: "YAN Haijing", birthdate: "1990-11-10"}
新しいオブジェクトstudent2が作成されましたが、古いオブジェクトstudent1も変更されていることがわかりました。これは、代入されたJSオブジェクトが「参照代入」、つまり、代入処理において、参照のメモリに渡されたためです。具体的には、「スタックストレージ」と「ヒープストレージ」の問題です。
不変データの力と実現
イミュータブル」とは、オブジェクトの状態が変化しないという意味です。この利点は、開発が容易になること、トレーサビリティが確保できること、テストが容易になること、そして起こりうる副作用を軽減できることです。
そして、副作用を避けるために、不変データを作成する主な実装アイデアは、更新処理は元のオブジェクトを変更せず、新しいデータ状態をホストする新しいオブジェクトを作成するだけでよいということです。
不変性には純粋関数を使いましょう。純粋関数とは、副作用を持たない関数のことです。では、具体的にどのように純粋関数を作るのでしょうか?上の例に合わせたコードの実装を見てみましょう:
const student1 = {
school: "",
name: 'HOU Ce',
birthdate: '1995-12-15',
}
const changeStudent = (student, newName, newBday) => {
return {
...student, // 脱構築を使う
name: newName, // name属性をオーバーライドする
birthdate: newBday // birthdate属性を上書きする
}
}
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "", name: "HOU Ce", birthdate: "1995-12-15"}
// Object {school: "", name: "YAN Haijing", birthdate: "1990-11-10"}
ES6から分解された代入を使用したことに注意してください。こうすることで、望みの効果が得られます。パラメータに基づいて新しいオブジェクトが生成され、正しく割り当てられます。これはObject.assignを使っても実現できます。
同様に、配列を扱う場合は、.map、.filter、または.reduceを使用して目的を達成することができます。これらのAPIに共通する特徴は、元の配列を変更せず、新しい配列を生成して返すことです。これは純粋関数の考え方と似ています。
しかし、もう一度Object.assignの使い方に戻りますが、以下の点に注意してください。つまり、列挙不可能なプロパティはコピーできません。2) オブジェクトに未定義やNULL型の内容が含まれている場合、エラーが報告されます。3) 最も重要なポイント:Object.assignメソッドはディープコピーではなく、シャローコピーを実行します。つまり、ソースオブジェクトのプロパティの値がオブジェクトである場合、ターゲットオブジェクトのコピーはこのプロパティのオブジェクトへの参照となります。これは、オブジェクトが入れ子になっている場合にも問題があることを意味します。例えば、次のようなコードです:
const student1 = {
school: "",
name: 'HOU Ce',
birthdate: '1995-12-15',
friends: {
friend1: 'ZHAO Wenlin',
friend2: 'CHENG Wen'
}
}
const changeStudent = (student, newName, newBday, friends) => Object.assign({}, student, {name: newName, birthdate: newBday})
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "", name: "HOU Ce", birthdate: "1995-12-15", friends: Object}
// Object {school: "", name: "YAN Haijing", birthdate: "1990-11-10", friends: Object}
student2.friends.friend1 = 'MA xiao';
console.log(student1.friends.friend1); // "MA xiao"
生徒2の友達リストの友達1への変更は、生徒1の友達リストの友達1にも影響します。上記のObject.assignは典型的な浅いコピーです。深いネスト構造に遭遇した場合、手動で再帰する必要があります。これにはパフォーマンスの問題があります。
例えば、再帰を使って自分でディープコピーを実装する場合、循環参照による「デッドループ」の問題を考慮する必要がありますし、さらに大規模なデータ構造を使う場合、パフォーマンス上のデメリットは明らかです。お馴染みのjquery extendsメソッドも、あるバージョンでは3階層コピーとして実装されていますが、完全なディープコピーではありません。
要するに、イミュータブル・データを実装するには、パフォーマンスの問題を気にしなければなりません。そこで、イミュータブル・データを扱うための、すでに "有名 "なライブラリであるimmutable.jsをお勧めします。
彼の実装は、不変性とパフォーマンスの大幅な最適化の両方を保証します。その原理は興味深いもので、以下の一節ではcamsong氏の前任者の関数抜粋しています:
永続的なデータ構造の原則のImmutable実装では、つまり、新しいデータを作成する古いデータの使用は、同時に古いデータが利用可能で変更されていないことを確認します。同時に、パフォーマンスの低下によってもたらされるすべてのノードをコピーするdeepCopyを避けるために、Immutableは、構造共有を使用して、つまり、オブジェクトツリー内のノードが変更された場合、唯一のノードとそれによって影響を受ける親ノードを変更し、他のノードが共有されます。
興味のある読者は、これを掘り下げてみてください。また、必要であれば、immutable.jsのソースコード解析をまた書きたいと思っています。
参照の透明性
関数は、同じ入力に対して常に同じ結果を生成する場合、参照透過であると言われます。
次に、正方形関数を実装します:
const square = (n) => n * n;
同じ入力があれば、この純粋関数は常に同じ出力を持ちます。
square(2); // 4
square(2); // 4
square(2); // 4
// ...
このように、square(2)を4で置き換えることができ、関数は参照透過になります。
基本的に、関数は同じ入力に対して常に同じ結果を生成する場合、透明であると見なすことができます。
この概念を念頭に置いて、私たちができるクールなことの一つは、この関数を覚えておくことです。次のような関数があるとします。
const sum = (a, b) => a + b;
以下のパラメータで呼び出します。
sum(3, sum(5, 8));
sum(5, 8) 合計が13に等しいから、ちょっと意地悪な演算ができる:
sum(3, 13);
この式は常に16となり、式全体を数値定数に置き換えて書くことができます。
宣言的プログラミング
まず概念を統一すると、プログラミングには命令型と宣言型の2種類があります。
両者の違いは次のように定義できます:
- 命令型プログラミング:あなたが望むことは何でもできるように、「マシン」に物事の進め方を指示します。宣言型プログラミング:あなたが何をしたいかをマシンに伝え、それをどうするかをマシンに考えさせること。
宣言型プログラミングと命令型プログラミングのコード例
簡単な例として、配列の値を2倍にしたいとします。
以下のような命令型プログラミングスタイルで実装されています:
var numbers = [1,2,3,4,5]
var doubled = []
for(var i = 0; i < numbers.length; i++) {
var newNumber = numbers[i] * 2
doubled.push (newNumber)
}
console.log (doubled) //=> [2,4,6,8,10]
配列全体を走査し、各要素を取り出し、それを2倍し、その2倍した値を新しい配列に入れ、すべての要素を数えるまでその都度この2倍した配列を操作するのは簡単です。
その代わりに、宣言的プログラミングのアプローチを使って、以下のようにArray.map関数を使うことができます:
var numbers = [1,2,3,4,5]
var doubled = numbers.map (function (n) {
return n * 2
})
console.log (doubled) //=> [2,4,6,8,10]
map は現在の配列を使って新しい配列を作成し、新しい配列の各要素は map に渡された関数 { return n*2 }) で処理されます。
上記の定義と同じように分析してください:
- コマンドスタイル:forループで配列を走査、動作原理を重視、Howを強調
- 宣言的: Array.mapは、出力に重点を置いて配列を走査します。
関数型プログラミングを特徴とするいくつかの言語では、リストデータ型を操作するための一般的な宣言的関数型メソッドが他にもあります。例えば、リスト内のすべての値の合計を求めるには、命令型プログラミングではこうします:
var numbers = [1,2,3,4,5]
var total = 0 for(var i = 0; i < numbers.length; i++) {
total += numbers[i]
}
console.log (total) //=> 15
そして宣言的プログラミングのアプローチでは、reduce関数が使われます:
var numbers = [1,2,3,4,5]
var total = numbers.reduce (function (sum, n) {
return sum + n
});
console.log (total) //=> 15
reduce 関数は、渡された関数を使用してリストを値に変換します。この関数を引数にとり、配列の各要素を処理します。この関数が呼び出されるたびに、最初の引数は前の値の処理結果で、2 番目の引数は現在の要素です。この関数で処理された各要素は sum に合計され、最終的に配列全体の合計が得られます。
同様に、reduce関数は配列をトラバースする実装や状態管理部分を帰納的に抽象化し、リストを単一の値にマージする汎用的な方法を提供します。必要なのは、何が必要かを示すことだけです。
機能は一流市民
第一級市民とは、他のデータ型と同等の立場にあり、他の変数に代入したり、他の関数のパラメータとして渡したり、他の関数の戻り値として使用したりできる関数のことです。
//
var a = function fn1() { }
// 引数としての関数
function fn2(fn) {
fn()
}
// 戻り値としての関数
function fn3() {
return function() {}
}
以下の用語は、この機能の適用を中心に展開されます:
クロージャ
クロージャはjs開発の常套手段ですが、クロージャとは何でしょうか?クロージャとは、他の関数のスコープ内の変数にアクセスできる関数のことである。
関数型プログラミングではローカル変数は関数内で定義され、キャッシュ可能な関数に返されます。 変数は、返された関数内でもアクセス可能で、クロージャが作成されます。
// test1 これは普通の関数なのだ。
function test1() {
var a = 1;
// test2 これは内部関数なのだ。
// test1スコープで変数aを参照している。
// だからクロージャなのだ。
return function test2() {
return a + =1;
}
}
var result = test1();
console.log(result()); // 2
console.log(result()); // 3
console.log(result()); // 4
変数aは常にメモリに保持され、ゴミ収集メカニズムによって回収されることはありません。クロージャは関数型プログラミングの最も基本的な操作の2つ、カーニングとコンポジションで使われます。
クロージャの詳しい解説はこちら: JSクロージャの様々な落とし穴を徹底理解
高階関数
高階関数:関数を引数として受け取り、結果として関数を返す関数。上記の高階関数の概念は、実は間違っています。(高階関数限り、パラメータまたは関数を満たすために戻り値が高階関数になることができるのではなく、必ずしも確立されるために同じ時間を満たす)。
簡単な例です:
function add(a,b,fn){
return fn(a)+fn(b);
}
var fn=function (a){
return a*a;
}
add(2,3,fn); //13
高階関数を使う理由
- 一般的な問題を抽象化するために高階関数を使用
- 抽象化することで、細部を遮断し、目標のみに集中することができます。
- forEach
例:例えば、今、あなたはforループを使用する必要がある場合、プロセス指向の方法に従って、配列を横断する必要があり、ループ変数を定義し、ループの条件やその他の操作を決定します。あなたがforEach関数の上記の実装のようなこのステップの抽象化を横断する高次関数を使用する場合は、あなただけのループを達成するためにforEach内部ヘルプを知っている必要があり、その後、データを渡すためににデータを渡します。
独自の高階関数を書いてください。
ES6の多くを再生すると、高次関数が付属しており、そのようなスロットリング関数を記述するための関数的な方法として、独自の高次関数を記述する段階にアップグレードすることができ、スロットリング関数は、それを単刀直入に言えば、それはイベントをトリガする頻度を制御するための関数であり、以前は秒、無制限のトリガすることができ、現在500ミリ秒に制限されている1回トリガされます!
function throttle(fn, wait=500) {
if (typeof fn != "function") {
// 関数は
throw new TypeError("Expected a function")
}
//
let timer,
// 最初の呼び出しは
firstTime = true;
// ここでアロー関数を使えないのは、コンテキストをバインドするためだ。
return function (...args) {
//
if (firstTime) {
firstTime = false;
fn.apply(this,args);
}
if (timer) {
return;
}else {
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
fn.apply(this, args);
},wait)
}
}
}
// そうでなければ、関数型算術の法則を満たすことができないからだ。単独で使用すると、急激な連続クリックが制限され、ボタンは500ミリ秒ごとに規則的にクリックされるだけになる。
button.addEventListener('click', throttle(() => {
console.log('hhh')
}))
よく使われる高階関数
- forEach
- filter
- every
- some
- find/findIndex
- reduce
- sort ......
- ソート
reduxのコネクトメソッドには、現在一般的に使われている高次関数もあります。
地図を見せてください。
var pow = function square(x) {
return x * x;
};
var array = [1, 2, 3, 4, 5, 6, 7, 8];
var newArr = array.map(pow); //関数メソッドを直接渡す
console.log(newArr); // [1, 4, 9, 16, 25, 36, 49, 64]
console.log(array); // [1, 2, 3, 4, 5, 6, 7, 8]
マップの実装
var arr = [1,2,3];
var fn = function(item,index,arr){
console.log(index);
console.log(arr);
return item*2;
};
Array.prototype.maps = function(fn) {
let newArr = [];
for (let index = 0; index < this.length; index++) {
newArr.push(fn.call(this,this[index],index,this));
}
return newArr;
}
var result = arr.maps(fn);
関数のコリエリゼーション
関数コリエリゼーションは、関数型プログラミングにおける高階関数の重要な使用法です。
コリエリゼーションとは、関数Aを引数として受け取り、それを実行し、新しい関数を返すことができる関数のことです。そしてこの新しい関数は、関数Aの残りの引数を処理することができます。
このような定義はあまり理解されておらず、次のような例を挙げて説明することができます。
3つの引数を取る関数Aがあります。
function A(a, b, c) {
// do something
}
barを引数にとり、Aをカレー関数に変換し、変換された関数を返す、ラップされた汎用関数createCurryがあるとします。
var _A = createCurry(A);
そして、_AはcreateCurry実行の戻り関数として機能し、彼はAへの残りの引数を処理することができます。つまり、以下の実行結果はすべて等価です。
_A(1, 2, 3);
_A(1, 2)(3);
_A(1)(2, 3);
_A(1)(2)(3);
A(1, 2, 3);
関数Aは、createCurryによって変換され、Aの残りのすべての引数を処理することができるカレー関数_Aを取得します。したがって、Curryisationは部分評価としても知られています。
簡単なシナリオでは、コリオリの一般化形式の助けを借りることなく、コリオリ関数を得るために変換することが可能です。
例えば、3つの引数を足して計算結果を返す単純な加算関数があります。
function add(a, b, c) {
return a + b + c;
}
そうすると、add関数のコリエリゼーション関数_addは次のようになります:
function _add(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
次の算数は等価です。
add(1, 2, 3);
_add(1)(2)(3);
もちろん、目によってカプセル化されたコリー化関数の自由度は低く、コリー化された一般化式の方がはるかに強力な能力を持っています。したがって、このようなコリエリゼーションの一般化された形式をカプセル化する方法を知る必要があります。
まず_addでわかることは、Curry関数を実行する処理は実際にはパラメータ収集処理であり、入力されるパラメータはそれぞれ最内層の内部で収集・処理されるということです。createCurryの実装では、この考え方を使ってカプセル化することができます。
パッケージは以下の通り。
function add() {
var _args = [].slice.call(arguments);
// 関数を内部で宣言し、クロージャの特性を利用して_argsそうでなければ関数算術の法則は満たされない。
var adder = function() {
// [].push.apply(_args, [].slice.call(arguments));
_args.push(...arguments);
return adder;
};
// 暗黙の変換特性を利用し、最終的な実行時に暗黙のうちに変換を行い、最終的な値を計算する。
adder.toString = function() {
return _args.reduce(function(a, b) {
return a + b;
});
}
return adder;
}
var a = add(1)(2)(3)(4); // f 10
var b = add(1, 2, 3, 4); // f 10
var c = add(1, 2)(3, 4); // f 10
var d = add(1, 2, 3)(4); // f 10
// 暗黙の変換プロパティを使って計算に参加できる
console.log(a + 10); // 20
console.log(b + 20); // 30
console.log(c + 30); // 40
console.log(d + 40); // 50
// そうでなければ、関数型算術の法則が満たされないからだ。パラメータを渡し続けることも可能で、得られた結果は暗黙の変換を使って再び計算に使われる。
console.log(a(10) + 100); // 120
console.log(b(10) + 100); // 120
console.log(c(10) + 100); // 120
console.log(d(10) + 100); // 120
関数の合成
関数合成とは、個々のアクションを表す複数の関数を1つの関数にまとめることです。前述したように、関数型プログラミングは処理を抽象化し、動作に焦点を当てたものです。上の計算例の場合、「4を足す」→「4を掛ける」という動作に注目します。コードの実装は次のようになります。
function add4(x) {
return x + 4
}
function multiply4(x) {
return x * 4
}
console.log(multiply4(add4(1))) // 20
関数composeの定義によると、2つのアクションを表す2つの関数を1つの関数に合成することが可能です。合成されたアクションを関数composeに抽象化すると、composeのコードは以下のようになります。
function compose(f,g) {
return function(x) {
return f(g(x));
};
}
したがって、合成関数は次のように求めることができます。
var calculate=compose(multiply4,add4); //動作の実行順序は、右から左の順である。
console.log(calculate(1)) // 20
このように、それぞれのアクションを表す関数をcompose関数に渡すだけで、最終的な合成関数を得ることができます。しかし、compose関数の限界は2つの関数しか合成できないことです。 2つ以上の関数を合成する必要がある場合はどうすればよいでしょうか。そこで、汎用的なcompose関数が必要になります。汎用合成関数のコードを以下に示します。
function compose(...args) {
return function(x) {
var composeFun = args.reduceRight(function(funLeft, funRight) {
console.log(funLeft);
return funRight(funLeft)
}, x);
return composeFun;
}
}
上記の汎用compose関数を実際に使ってみましょう。
function addHello(str){
return 'hello '+str;
}
function toUpperCase(str) {
return str.toUpperCase();
}
function reverse(str){
return str.split('').reverse().join('');
}
var composeFn=compose(reverse,toUpperCase,addHello);
console.log(composeFn('ttsy')); // YSTT OLLEH
上記の処理には、"hello"、"大文字に変換"、"反転 "という3つのアクションがあり、上記の3つのアクションで表される関数がcomposeによって1つにまとめられ、最終的に正しい結果が出力されていることがわかります。
结合使用
さて、ここでは、関数型プログラミングの概念の予備的な理解されている、次にどのようにコードを記述するために、関数型プログラミングの方法を使用するには、例えば: // 擬似コード、アイデア // 例えば、要求のバックエンドは、データを取得し、その後、データを数回フィルタリングする必要があり、内部の一部を取り出し、並べ替え
//
const res = {
status: 200,
data: [
{
id: xxx,
name: xxx,
time: xxx,
content: xxx,
created: xxx
},
...
]
}
// カプセル化されたリクエスト関数
const http = xxx;
// '伝統的な書き方はこうだ。
http.post
.then(res => データを取得する)
.then(res => フィルタを作る)
.then(res => フィルタを作る)
.then(res => 一部を抜粋)
.then(res =>
// '関数型プログラミングはこのように機能する」。
// フィルター関数を宣言する
const a = curry()
// 取り出し関数を宣言する
const b = curry()
// ソート関数を宣言する
const c = curry()
// すべてをまとめる
const shout = compose(c, b, a)
//
shout(http.post)
プロジェクトで関数型プログラミングを正式に使うには プロジェクトで関数型プログラミングを正式に使おうとすると、いくつかのステップがあると思います:
- 1, まず、ES6 の高階関数を使用してみてください。
- 2、ES6には高階関数が付属しています。
- 3.このプロセスでは、コードを書くために純粋な関数を使用してみてください。
- 4、関数型プログラミングを理解したら、ramdaのようなライブラリを使ってコードを書いてみてください!
- 5、ramdaを使う過程で、そのソースコードを勉強してみることができます。
- 6、独自のライブラリ、コリオリ関数、組み合わせ関数などを書こうとします。
関数型プログラミングは、そのためだけに選ぶべきではありません。関数型プログラミングがあなたの助けになり、プロジェクトの効率と質を向上させることができるのであれば、それを使いましょう。
拡張機能
関数型プログラミングはカテゴリー理論に基づいて開発されたもので、関数型プログラミングとカテゴリー理論の関係をよく表しているのが、グエン・イーフェン兄貴の記事です。
本質的には、関数型プログラミングは算術に対するカテゴリー理論的なアプローチにすぎず、数学的論理学、微積分学、行列式と同じようなものです。
では、なぜ関数型プログラミングでは関数が副作用のない純粋なものでなければならないのか、おわかりでしょうか?なぜなら、関数は数学的な演算であり、その本来の目的は値を求めることであり、それ以外のことは何もしないからです。