関数型プログラミング言語
関数型プログラミング言語は、関数型プログラミング パラダイムの使用を容易にする言語です。簡単に言えば、関数型プログラミングに必要な特性を備えていれば、関数型言語と呼ぶことができます。ほとんどの場合、実際にはプログラムが機能するかどうかはプログラミング スタイルによって決まります。
言語を機能させるものは何ですか?
関数型プログラミングは C 言語で実装できません。関数型プログラミングは Java では実装できません (多くの回避策を使用して関数型プログラミングに近似したものを除く)。 これらの言語には、関数型プログラミングをサポートする構成要素が含まれていません。これらは純粋にオブジェクト指向であり、厳密には非関数型言語です。
同時に、オブジェクト指向プログラミングは、Scheme、Haskell、Lisp などの純粋関数型言語では使用できません。
ただし、一部の言語は両方のモードをサポートしています。 Python は有名な例ですが、他にも Ruby、Julia、そして最も興味深いのは Javascript などがあります。 これらの言語は、この 2 つの異なる設計パターンをどのようにサポートしているのでしょうか?これらには、両方のプログラミング パラダイムに必要な機能が含まれています。 ただし、JavaScriptの場合、機能的な特徴が隠されているようです。
しかし実際には、関数型言語には上記よりももう少し必要なものが必要です。関数型言語にはどのような特徴があるのでしょうか?
特点 | 命令式 | 函数式 |
---|---|---|
编程风格 | 一步一步地执行,并且要管理状态的变化 | 描述问题和和所需的数据变化以解决问题 |
状态变化 | 很重要 | 不存在 |
执行顺序 | 很重要 | 不太重要 |
主要的控制流 | 循环、条件、函数调用 | 函数调用和递归 |
主要的操作单元 | 结构体和类对象 | 函数作为一等公民的对象和数据集 |
関数型言語の構文では、型推論システムや匿名関数などの特定の設計パターンを考慮する必要があります。一般に、言語はラムダ計算を実装する必要があります。 また、インタプリタの評価戦略は、非厳密なコールオンデマンド (遅延実行とも呼ばれます) でなければなりません。これにより、不変のデータ構造と非厳密な遅延評価が可能になります。
译注:这一段用了一些函数式编程的专业词汇。lambda演算是一套函数推演的形式化系统(听起来很晕), 它的先决条件是内部函数和匿名函数。非严格求值和惰性求值差不多一个意思,就是并非严格地按照运算规则把所有元素先计算一遍, 而是根据最终的需求只计算有用的那一部分,比如我们要取有一百个元素的数组的前三项, 那惰性求值实际只会计算出一个具有三个元素是数组,而不会先去计算那个一百个元素的数组。
メリット
関数型プログラミングは、最終的に習得すると大きなインスピレーションとなるでしょう。この種の経験は、実際にフルタイムの職務プログラマーになるかどうかに関係なく、プログラマーとしての将来のキャリアをより高いレベルに引き上げます。
しかし、私たちは今、瞑想を学ぶ方法について話しているのではなく、より良いプログラマーになるための非常に便利なツールを学ぶ方法について話しているのです。
全体として、関数型プログラミングを使用する実際の実際的な利点は何ですか?
クリーナーコード
関数型プログラミングは、よりクリーン、シンプル、そして小規模です。デバッグ、テスト、メンテナンスが簡素化されます。
たとえば、2 次元配列を 1 次元配列に変換する関数が必要です。命令的手法のみを使用する場合は、次のように記述します:
function merge2dArrayIntoOne(arrays) { var count = arrays.length; var merged = new Array(count); var c = 0; for (var i = 0; i < count; ++i) { for (var j = 0, jlen = arrays[i].length; j < jlen; ++j) { merged[c++] = arrays[i][j]; } } return merged }
関数型テクニックを使用すると、次のように記述できます:
merge2dArrayIntoOne2 = (arrays) -> arrays.reduce (memo, item) -> memo.concat item , []
var merge2dArrayIntoOne2 = function(arrays) { return arrays.reduce( function(p,n){ return p.concat(n); }, []); };
译注:原著中代码有误,调用reduce函数时少了第二个参数空数组,这里已经补上。
どちらの関数も同じ入力を受け取り、同じ出力を返しますが、関数例はより明確です。
モジュラー
関数型プログラミングでは、大きな問題を同じ問題を解決する小さなケースに分割する必要があります。これは、コードがよりモジュール化されることを意味します。 モジュール型プログラムは説明が明確で、デバッグが容易で、保守も簡単です。各モジュールのコードの正確性を個別にチェックできるため、テストも容易になります。
再利用性
関数型プログラミングにはモジュール形式の性質があるため、多くの共通の補助関数があります。ここにある関数の多くは、さまざまなアプリケーションで再利用できることがわかります。
次の章では、最も一般的な機能の多くについて説明します。ただし、関数型プログラマーとして、何度も使用される独自の関数ライブラリを必然的に作成することになります。たとえば、行間の構成ファイルを検索するために使用される関数は、適切に設計されていれば、ハッシュ テーブルの検索にも使用できます。
カップリングを軽減します
結合とは、プログラム内のモジュール間の多数の依存関係です。関数型プログラミングは、グローバル変数に副作用がなく、互いに完全に独立した、第一級の高次の純粋関数の記述に従うため、結合が大幅に軽減されます。 もちろん、関数は必然的に相互に依存しますが、入力と出力の 1 対 1 マッピングが正しい限り、1 つの関数を変更しても他の関数には影響しません。
数学的正しさ
最後の点はより理論的なものです。関数型プログラミングはラムダ計算に根ざしているため、数学的に正しいと証明できます。 これは、成長率、時間計算量、数学的正しさを証明するプログラムを必要とする一部の研究者にとって、大きな利点となります。
フィボナッチ数列を見てみましょう。概念実証の問題以外ではめったに使用されませんが、概念を説明する優れた方法です。 フィボナッチ数列を評価する標準的な方法は、次のような再帰関数を作成することです:
fibonnaci(n) = fibonnaci(n-2) + fibonnaci(n–1)
一般的な状況も追加する必要があります:
return 1 when n < 2
これにより、再帰が終了し、再帰呼び出しスタックの各ステップがここから蓄積できるようになります。
詳細な手順を以下に示します
var fibonacci = function(n) { if (n < 2) { return 1; }else { return fibonacci(n - 2) + fibonacci(n - 1); } } console.log( fibonacci(8) ); // Output: 34
しかし、遅延実行関数ライブラリの助けを借りて、数式を通じてシーケンス全体のメンバーを定義することにより、無限シーケンスを生成することが可能です。 最終的に必要なメンバーのみが最後に計算されます。
var fibonacci2 = Lazy.generate(function() { var x = 1, y = 1; return function() { var prev = x; x = y; y += prev; return prev; }; }()); console.log(fibonacci2.length()); // Output: undefined console.log(fibonacci2.take(12).toArray()); // Output: [1, 1, 2, 3, 5,8, 13, 21, 34, 55, 89, 144] var fibonacci3 = Lazy.generate(function() { var x = 1, y = 1; return function() { var prev = x; x = y; y += prev; return prev; }; }()); console.log(fibonacci3.take(9).reverse().first(1).toArray()); //Output: [34]
第二个例子明显更有数学的味道。它依赖Lazy.js函数库。还有一些其它这样的库,比如Sloth.js、wu.js, 这些将在第三章里面讲到。
我插几句:后面这个懒执行的例子放这似乎仅仅是来秀一下函数式编程在数学正确性上的表现。 更让人奇怪的是作者还要把具有相同内部函数的懒加载写两遍,完全没意义啊…… 我觉得各位看官知道这是个懒执就行了,不必深究。
非函数式世界中的函数式编程
函数式和非函数式编程能混合在一起吗?尽管这是第七章的主题,但是在我们进一步学习之前, 还是要弄明白一些东西。
这本书并没要想要教你如何严格地用纯函数编程来实现整个应用。这样的应用在学术界之外不太适合。 相反,这本书是要教你如何在必要的命令式代码之上使用纯函数的设计策略。
例如,你需要在一段文本中找出头四个只含有字母的单词,稚嫩一些的写法会是这样:
var words = [], count = 0; text = myString.split(' '); for (i=0; count < 4, i < text.length; i++) { if (!text[i].match(/[0-9]/)) { words = words.concat(text[i]); count++; } } console.log(words);
函数式编程会写成这样:
var words = []; var words = myString.split(' ').filter(function(x){ return (! x.match(/[1-9]+/)); }).slice(0,4); console.log(words);
如果有一个函数式编程的工具库,代码可以进一步被简化:
関数がより機能的な方法で記述できるかどうかを判断する方法は、ループと一時変数 (前の例の "words" 変数や "count" 変数など) を探すことです。 多くの場合、ループや一時変数を高次関数に置き換えることができます。これについては、この章で後ほど説明します。
JavaScript は関数型プログラミング言語ですか?
最後に自問する必要がある質問があります。JavaScript は関数型言語ですか、それとも非関数型言語ですか?
JavaScript はおそらく世界で最も人気があるが、最も理解されていない関数型プログラミング言語です。 JavaScript は C の装いをした関数型プログラミング言語です。 その構文は明らかに C に似ています。つまり、C のブロック構文と中置語順が使用されています。そして、それは現存する言語の中で最悪の名前を持っています。 想像するまでもなく、JavaScript と Java の関係について、その名前がそれが何であるかを暗示しているかのように、どれほど多くの人が混乱しているかがわかります。 しかし実際には、Java との共通点はほとんどありません。ただし、Javascript をオブジェクト指向言語に強制するアイデアはまだいくつかあります。たとえば、Dojo や easy.js などのライブラリは、JavaScript をオブジェクト指向プログラミングに適したものにするために多くの作業を行っています。 Javascript は 1990 年代に誕生しました。当時、世界中でオブジェクト指向プログラミングが叫ばれていました。私たちは、JavaScript がオブジェクト指向言語であることを望んでいたからだと言われていましたが、実際にはそうではありませんでした。
その正体は、そのプロトタイプ、つまり 2 つの古典的な関数型プログラミング言語である Scheme と Lisp に遡ります。 JavaScript は常に関数型プログラミング言語です。 その関数は第一級市民であり、入れ子にすることができ、クロージャと複合関数があり、合理化とモナドが可能です。これらはすべて関数型プログラミングの鍵です。 Javascript が関数型言語である理由をさらにいくつか挙げます:
• Javascript のレキシコンには、関数をパラメータとして渡す機能が含まれており、型推論システムがあり、匿名関数、高階関数、クロージャなどをサポートしています。 これらの特性は、関数型プログラミングを構成する構造と動作にとって重要です。
• JavaScript は純粋なオブジェクト指向言語ではありません。そのオブジェクト指向設計パターンのほとんどは、プロトタイプ オブジェクトをコピーすることで完成します。これは弱いオブジェクト指向プログラミング モデルです。欧州コンピュータ製造者協会スクリプト (ECMAScript) - Javascript の公式形式および標準実装 - の仕様のバージョン 4.2.1 には次の記述があります:
「JavaScript には C、Smalltalk、Java のような実際のクラスはありませんが、オブジェクトを作成するためのコンストラクターをサポートしています。一般的に言えば、クラスベースのオブジェクト指向言語では、状態はインスタンスによって保持され、メソッドはクラスによって保持され、継承はEMACScript では、状態とメソッドはオブジェクトによって保持され、構造、動作、および状態は継承されます。
言い換えれば、JavaScript は純粋な関数型言語ではありません。遅延評価と組み込みの不変データが欠けています。 これは、ほとんどの通訳者がオンデマンドではなく名前で呼ばれるという事実によるものです。また、JavaScript は末尾呼び出しの処理方法により、再帰の処理があまり得意ではありません。 ただし、これらの問題はすべて、いくつかの小さな考慮事項で軽減できます。無限シーケンスを必要とする非厳密な評価と遅延評価は、Lazy.js と呼ばれるライブラリを通じて実現できます。 不変性はプログラミング スキルだけで実現できますが、言語レベルに依存する制限はなく、プログラマーの自制心が必要です。 末尾再帰の除去は、トランポリンと呼ばれる方法によって実現できます。これらの問題については第 6 章で説明します。
JavaScript が関数型言語なのか、オブジェクト指向言語なのか、その両方なのか、あるいはどちらでもないのかについては常に多くの議論があり、今後も議論は続くでしょう。
最後に、関数型プログラミングは、関数を巧みに変更、組み合わせ、使用することで簡潔なコードを記述する方法です。そして、JavaScript はこれを実現するための優れた方法を提供します。 本当に Javascript の可能性を最大限に活用したい場合は、JavaScript を関数型言語として使用する方法を学ぶ必要があります。