関数型のJavaScriptでいくつかのプロジェクトを作成したので、私の経験を共有できます。具体的な実装の問題ではなく、高レベルの考慮事項に焦点を当てます。特効薬はありません。特定の状況に最適な方法を実行してください。
純粋な機能スタイルと機能スタイル
この機能的リファクタリングから何を取得したいかを考えてください。純粋な関数型実装が本当に必要ですか、それとも、参照の透過性など、関数型プログラミングスタイルがもたらすメリットの一部だけですか。
後者のアプローチ、つまり私が関数型スタイルと呼んでいるものをJavaScriptでうまくメッシュ化できることがわかりました。また、選択的な非機能概念を、意味のあるコードで注意深く使用することもできます。
より純粋に機能的なスタイルでコードを記述することはJavaScriptでも可能ですが、常に最も適切なソリューションであるとは限りません。そして、JavaScriptが純粋に機能することは決してありません。
機能的なスタイルのインターフェース
関数型Javascriptで最も重要な考慮事項は、関数型コードと命令型コードの境界です。この境界を明確に理解して定義することが不可欠です。
関数型のインターフェイスを中心にコードを整理します。これらは、個々の機能からモジュール全体に至るまで、機能的なスタイルで動作するロジックのコレクションです。インターフェースと実装の概念的な分離は重要です。命令型の実装が境界を越えてリークしない限り、関数型のインターフェースを命令型で実装できます。
残念ながら、Javascriptでは、機能的なスタイルのインターフェースを強制する負担は完全に開発者にあります。また、例外などの言語機能により、完全に明確な分離が不可能になります。
既存のコードベースをリファクタリングするので、関数スタイルにきれいに移動できるコード内の既存の論理インターフェイスを特定することから始めます。驚くべき量のコードがすでに機能的なスタイルである可能性があります。良い出発点は、必須の依存関係がほとんどない小さなインターフェースです。コードを移動するたびに、既存の必須の依存関係が排除され、より多くのコードを移動できます。
結果に満足するまで、繰り返し、徐々に外側に向かって作業し、プロジェクトの大部分を境界の機能面に押し込みます。この漸進的なアプローチにより、個々の変更をテストでき、境界の識別と適用が容易になります。
アルゴリズム、データ構造、設計の見直し
命令型ソリューションと設計パターンは、機能的なスタイルでは意味を成さないことがよくあります。特にデータ構造の場合、命令型コードを関数型スタイルに直接移植すると、醜くて遅くなる可能性があります。簡単な例:先頭のリストのすべての要素をコピーするか、既存のリストに要素を連結しますか?
問題に対する既存の機能的アプローチを調査および評価します。関数型プログラミングは単なるプログラミング手法ではなく、問題について考え、解決する別の方法です。lispやhaskellなどのより伝統的な関数型プログラミング言語で書かれたプロジェクトは、この点で優れたリソースです。
言語とビルトインを理解する
これは、機能的な命令境界を理解する上で重要な部分であり、インターフェースの設計に影響を与えます。関数スタイルのコードで安全に使用できる機能と使用できない機能を理解します。ビルトインが例外をスローし[].push
たり、オブジェクトを変更したり、参照によって渡されるオブジェクトやプロトタイプチェーンの動作に至るまでのすべて。プロジェクトで使用する他のライブラリについても同様に理解する必要があります。
このような知識により、不適切な入力や例外を処理する方法や、コールバック関数が予期しない何かをした場合に何が起こるかなど、情報に基づいた設計決定を行うことができます これにはコードで強制できるものもありますが、適切な使用方法に関するドキュメントが必要なものもあります。
もう1つのポイントは、実装の厳密性です。最大のコールスタックエラーに対して、またはユーザーがいくつかの組み込み関数を再定義するときに、正しい動作を強制してみますか?
適切な場所で機能以外の概念を使用する
特にJavaScriptのような言語では、機能的アプローチが常に適切であるとは限りません。より大きな入力に対して最大のコールスタック例外が発生し始めるまで、再帰的ソリューションは洗練されている可能性があるため、おそらく反復的なアプローチの方が良いでしょう。同様に、コード編成、コンストラクターでの割り当て、および意味のある不変オブジェクトにも継承を使用します。
機能インターフェースに違反していない限り、意味のあることを行い、テストと保守が最も簡単なものを実行します。
例
これは、非常に単純な実際のコードを関数型スタイルに変換する例です。プロセスの大きな例はあまりありませんが、この小さな例では、いくつかの興味深い点に触れています。
このコードは、文字列からトライを構築します。John Resigのtrie-jsビルドトライモジュールの上部セクションに基づくコードから始めます。多くの簡素化/フォーマットの変更が行われ、私の意図は元のコードの品質についてコメントすることではありません(トライを構築するためのより明確でよりクリーンな方法があるため、このCスタイルのコードがGoogleで最初に登場する理由は興味深いです。ここにあります。reduceを使用する迅速な実装)。
この例が好きなのは、本当の機能的な実装、つまり機能的なインターフェースにまでわざわざ移動させる必要がないためです。ここが出発点です:
var txt = SOME_USER_STRING,
words = txt.replace(/\n/g, "").split(" "),
trie = {};
for (var i = 0, l = words.length; i < l; i++) {
var word = words[i], letters = word.split(""), cur = trie;
for (var j = 0; j < letters.length; j++) {
var letter = letters[j];
if (!cur.hasOwnProperty(letter))
cur[letter] = {};
cur = cur[ letter ];
}
cur['$'] = true;
}
// Output for text = 'a ab f abc abf'
trie = {
"a": {
"$":true,
"b":{
"$":true,
"c":{"$":true}
"f":{"$":true}}},
"f":{"$":true}};
これがプログラム全体であるとしましょう。このプログラムは2つのことを行います。文字列を単語のリストに変換し、単語のリストからトライを作成します。これらはプログラムの既存のインターフェースです。以下は、インターフェースをより明確に表現したプログラムです。
var _get_words = function(txt) {
return txt.replace(/\n/g, "").split(" ");
};
var _build_trie_from_list = function(words, trie) {
for (var i = 0, l = words.length; i < l; i++) {
var word = words[i], letters = word.split(""), cur = trie;
for (var j = 0; j < letters.length; j++) {
var letter = letters[j];
if (!cur.hasOwnProperty(letter))
cur[letter] = {};
cur = cur[ letter ];
}
cur['$'] = true;
}
};
// The 'public' interface
var build_trie = function(txt) {
var words = _get_words(txt), trie = {};
_build_trie_from_list(words, trie);
return trie;
};
インターフェースを定義するだけ_get_words
で、境界の機能スタイル側に移動できます。どちらも、replace
またはsplit
元の文字列を変更しません。また、build_trie
非常に命令的なコードと相互作用するにもかかわらず、私たちのパブリックインターフェイスも機能的なスタイルです。実際、ほとんどの場合、これは適切な停止点です。コードが多すぎると圧倒されるので、他のいくつかの変更について概説しましょう。
まず、すべてのインターフェースを機能的なスタイルにします。この場合、これは_build_trie_from_list
取るに足らないことです。オブジェクトを変更するのではなく、オブジェクトを返すだけです。
悪い入力の処理
build_trie
文字の配列で呼び出すとどうなるかを考えてみましょうbuild_trie(['a', ' ', 'a', 'b', 'c', ' ', 'f'])
。呼び出し元は、これが関数スタイルで動作すると想定していましたが.replace
、配列で呼び出されると例外をスローします。これは意図された動作である可能性があります。または、明示的な型チェックを実行して、入力が期待どおりであることを確認することもできます。しかし、私はアヒルのタイピングを好みます。
文字列は単なる文字の配列であり、配列はlength
プロパティと負でない整数をキーとする単なるオブジェクトです。したがって、文字列の配列のようなオブジェクトを操作する新しいメソッドreplace
やsplit
メソッドをリファクタリングして記述した場合、それらは文字列である必要さえありません。コードは正しいことを行うだけです。(String.prototype.*
ここでは機能しません。出力を文字列に変換します)。ダックタイピングは関数型プログラミングとは完全に別のものですが、大きな点は、悪い入力は常に考慮されるべきだということです。
根本的な再設計
さらに基本的な考慮事項もあります。関数型にもトライを構築したいとします。命令型コードでは、アプローチは一度に1つの単語を構成することでした。ダイレクトポートでは、オブジェクトの変更を回避するために挿入を行う必要があるたびに、トライ全体をコピーする必要があります。明らかにそれはうまくいきません。代わりに、トライをノードごとに、下から上に構築することができます。これにより、ノードが完了すると、ノードに再度触れる必要がなくなります。または、別のアプローチの方が完全に良いかもしれません。クイック検索で、試行の既存の機能実装の多くが示されます。
例が少し明確にしてくれることを願っています。
全体として、Javascriptで関数型のコードを書くことは、やりがいのある楽しい試みだと思います。Javascriptは、デフォルトの状態では冗長すぎますが、驚くほど機能的な関数型言語です。
プロジェクトで頑張ってください。
関数型のJavaScriptで記述されたいくつかの個人プロジェクト:
- parse.js-パーサーコンビネーター
- Nu-レイジーストリーム
- Atum-関数スタイルのJavascriptで記述されたJavascriptインタープリター。
- Khepri-関数型Javascript開発に使用するプロトタイププログラミング言語。関数型のJavascriptで実装されています。