C#、Visual Basic、C++、Java などのオブジェクト指向プログラミング (OOP) 言語を含む主流言語のほとんどは、主に命令型 (手続き型) プログラミングをサポートするように設計されていますが、Haskell/gofer のような言語は純粋に機能的です。これら 2 つのプログラミング方法の違いを詳しく説明できる人はいますか?
プログラミングの方法を選択するのはユーザーの要件に依存することはわかっていますが、関数型プログラミング言語の学習が推奨されるのはなぜですか?
C#、Visual Basic、C++、Java などのオブジェクト指向プログラミング (OOP) 言語を含む主流言語のほとんどは、主に命令型 (手続き型) プログラミングをサポートするように設計されていますが、Haskell/gofer のような言語は純粋に機能的です。これら 2 つのプログラミング方法の違いを詳しく説明できる人はいますか?
プログラミングの方法を選択するのはユーザーの要件に依存することはわかっていますが、関数型プログラミング言語の学習が推奨されるのはなぜですか?
違いは次のとおりです。
必須:
...などなど...
宣言的、その機能的はサブカテゴリです:
...などなど...
概要: 命令型言語では、メモリ内のビット、バイト、単語を変更する方法とその順序をコンピュータに指示します。機能的なものでは、物やアクションなどが何であるかをコンピューターに伝えます。たとえば、0 の階乗は 1 であり、1 つおきの自然数の階乗はその数とその前の階乗の積であると言います。n の階乗を計算するには、メモリ領域を予約してそこに 1 を格納し、そのメモリ領域の数値に 2 から n までの数値を掛けて、結果を同じ場所に格納し、最後に、メモリ領域には階乗が含まれます。
定義: 命令型言語は、一連のステートメントを使用して、特定の目標に到達する方法を決定します。これらのステートメントは、それぞれが順番に実行されるときにプログラムの状態を変更すると言われています。
例: Java は命令型言語です。たとえば、一連の数値を加算するプログラムを作成できます。
int total = 0;
int number1 = 5;
int number2 = 10;
int number3 = 15;
total = number1 + number2 + number3;
各ステートメントは、各変数への値の割り当てからそれらの値の最終的な追加まで、プログラムの状態を変更します。一連の 5 つのステートメントを使用して、5、10、15 の数字を足し合わせる方法をプログラムに明示的に指示します。
関数型言語: 関数型プログラミングのパラダイムは、問題解決への純粋な関数型アプローチをサポートするために明示的に作成されました。関数型プログラミングは、宣言型プログラミングの一種です。
純粋関数の利点: 関数変換を純粋関数として実装する主な理由は、純粋関数が構成可能であること、つまり、自己完結型でステートレスであることです。これらの特性により、次のような多くの利点がもたらされます。 可読性と保守性の向上。これは、各関数が引数を指定して特定のタスクを実行するように設計されているためです。この関数は、外部状態に依存しません。
反復的な開発が容易になります。コードはリファクタリングしやすいため、多くの場合、設計の変更を実装しやすくなります。たとえば、複雑な変換を記述した後、一部のコードが変換で数回繰り返されていることに気付いたとします。純粋なメソッドを介してリファクタリングする場合、副作用を心配することなく、純粋なメソッドを自由に呼び出すことができます。
テストとデバッグが容易になります。純粋関数は分離してより簡単にテストできるため、典型的な値、有効なエッジ ケース、および無効なエッジ ケースを使用して純粋関数を呼び出すテスト コードを作成できます。
OOP People または命令型言語の場合:
オブジェクト指向言語は、物事に対する一連の操作が固定されていて、コードが進化するにつれて主に新しいものを追加する場合に適しています。これは、既存のメソッドを実装する新しいクラスを追加することで実現でき、既存のクラスはそのままにしておきます。
関数型言語は、固定されたもののセットがあり、コードが進化するにつれて、主に既存のものに新しい操作を追加する場合に適しています。これは、既存のデータ型で計算する新しい関数を追加することで実現できます。既存の関数はそのままにしておきます。
短所:
プログラミング方法の選択はユーザーの要件に依存するため、ユーザーが適切な方法を選択しない場合にのみ害があります。
進化が間違った方向に進むと、次の問題が発生します。
最新の言語のほとんどは、命令型と関数型の両方の程度が異なりますが、関数型プログラミングをよりよく理解するには、Haskell のような純粋な関数型言語の例を取り上げ、java/C# のようなそれほど関数型でない言語の命令型コードとは対照的に取り上げるのが最善です。例で説明するのは常に簡単だと思うので、以下に例を示します。
関数型プログラミング: n ie n の階乗を計算します! つまり、nx (n-1) x (n-2) x ...x 2 X 1
-- | Haskell comment goes like
-- | below 2 lines is code to calculate factorial and 3rd is it's execution
factorial 0 = 1
factorial n = n * factorial (n - 1)
factorial 3
-- | for brevity let's call factorial as f; And x => y shows order execution left to right
-- | above executes as := f(3) as 3 x f(2) => f(2) as 2 x f(1) => f(1) as 1 x f(0) => f(0) as 1
-- | 3 x (2 x (1 x (1)) = 6
Haskel では、引数値のレベルまで関数のオーバーロードが許可されていることに注意してください。以下は、命令性の度合いが増す命令コードの例です。
//somewhat functional way
function factorial(n) {
if(n < 1) {
return 1;
}
return n * factorial(n-1);
}
factorial(3);
//somewhat more imperative way
function imperativeFactor(n) {
int f = 1;
for(int i = 1; i <= n; i++) {
f = f * i;
}
return f;
}
この記事は、命令型コードがどのように部分、マシンの状態 (for ループの i)、実行順序、フロー制御に重点を置いているかを理解するための良い参考資料となります。
後者の例は Java/C# 言語コードとして大まかに見て、最初の部分は言語自体の制限として、関数を値 (ゼロ) でオーバーロードする Haskell とは対照的に、純粋な関数型言語ではないと言えます。機能的なプログラムをサポートしていると言えます。ある程度。
開示:上記のコードはいずれもテスト/実行されていませんが、概念を伝えるのに十分なはずです。また、そのような修正についてコメントをいただければ幸いです:)
関数型プログラムとは何か、命令型プログラムとは何かについては、多くの意見があるようです。
関数型プログラムは、「遅延評価」志向として最も簡単に説明できると思います。プログラムカウンターが命令を反復する代わりに、言語は設計上、再帰的なアプローチを採用しています。
関数型言語では、関数の評価はreturn ステートメントから始まり、最終的に値に到達するまでバックトラックします。これは、言語構文に関して非常に大きな影響を及ぼします。
必須: コンピューターの発送
以下では、郵便局のアナロジーを使用して説明しようとしました。命令型言語は、コンピューターにさまざまなアルゴリズムを送信し、コンピューターに結果を返すようにします。
機能的: レシピの発送
関数型言語がレシピを送信し、結果が必要になると、コンピューターがレシピの処理を開始します。
このようにして、結果の計算に使用されない作業に CPU サイクルを浪費しすぎないようにします。
関数型言語で関数を呼び出すと、戻り値はレシピで構築されたレシピであり、さらにレシピで構築されています。これらのレシピは、実際にはクロージャーとして知られているものです。
// helper function, to illustrate the point
function unwrap(val) {
while (typeof val === "function") val = val();
return val;
}
function inc(val) {
return function() { unwrap(val) + 1 };
}
function dec(val) {
return function() { unwrap(val) - 1 };
}
function add(val1, val2) {
return function() { unwrap(val1) + unwrap(val2) }
}
// lets "calculate" something
let thirteen = inc(inc(inc(10)))
let twentyFive = dec(add(thirteen, thirteen))
// MAGIC! The computer still has not calculated anything.
// 'thirteen' is simply a recipe that will provide us with the value 13
// lets compose a new function
let doubler = function(val) {
return add(val, val);
}
// more modern syntax, but it's the same:
let alternativeDoubler = (val) => add(val, val)
// another function
let doublerMinusOne = (val) => dec(add(val, val));
// Will this be calculating anything?
let twentyFive = doubler(thirteen)
// no, nothing has been calculated. If we need the value, we have to unwrap it:
console.log(unwrap(thirteen)); // 26
unwrap 関数は、スカラー値を持つポイントまですべての関数を評価します。
言語設計の結果
命令型言語のいくつかの優れた機能は、関数型言語では不可能です。たとえばvalue++
、関数型言語では評価が難しい式です。関数型言語は、評価方法のために、構文がどうあるべきかについて制約を課します。
一方、命令型言語を使用すると、関数型言語から優れたアイデアを借りてハイブリッドにすることができます。
関数型言語では、値をインクリメントするなどの単項演算子を扱うのが非常に困難です。関数型言語が「逆に」++
評価されることを理解しない限り、この問題の理由は明らかではありません。
単項演算子を実装するには、次のように実装する必要があります。
let value = 10;
function increment_operator(value) {
return function() {
unwrap(value) + 1;
}
}
value++ // would "under the hood" become value = increment_operator(value)
unwrap
上記で使用した関数は、javascript が関数型言語ではないため、必要に応じて値を手動でアンラップする必要があることに注意してください。
これで、インクリメントを 1000 回適用すると、値が 10000 個のクロージャでラップされることになり、これは無価値であることが明らかになりました。
より明白なアプローチは、実際にその場で値を直接変更することですが、ほら、変更可能な値、つまり変更可能な値を導入して、言語を命令型にするか、実際にはハイブリッドにします。
内部的には、インプットが提供されたときにアウトプットを生み出すための 2 つの異なるアプローチに要約されます。
以下では、次のアイテムで都市のイラストを作成してみます。
タスク: 3 番目のフィボナッチ数を計算します。手順:
コンピューターを箱に入れ、付箋で印を付けます。
分野 | 価値 |
---|---|
メールアドレス | The Fibonaccis |
差出人住所 | Your Home |
パラメーター | 3 |
戻り値 | undefined |
そしてコンピュータを送ります。
フィボナッチは、ボックスを受け取ると、いつものように次のことを行います。
パラメータは < 2 ですか?
はい:付箋を交換し、コンピューターを郵便局に返却します。
分野 | 価値 |
---|---|
メールアドレス | The Fibonaccis |
差出人住所 | Your Home |
パラメーター | 3 |
戻り値 | 0 または1 (パラメーターを返す) |
送信者に戻ります。
さもないと:
新しい付箋を古い付箋の上に置きます。
分野 | 価値 |
---|---|
メールアドレス | The Fibonaccis |
差出人住所 | Otherwise, step 2, c/o The Fibonaccis |
パラメーター | 2 (パラメーター-1 を渡す) |
戻り値 | undefined |
して送信します。
返却した付箋をはがします。最初の付箋の上に新しい付箋を貼って、コンピューターをもう一度送信します。
分野 | 価値 |
---|---|
メールアドレス | The Fibonaccis |
差出人住所 | Otherwise, done, c/o The Fibonaccis |
パラメーター | 2 (パラメーター-2 を渡す) |
戻り値 | undefined |
ここまでで、依頼者からの最初の付箋と 2 つの使用済み付箋があり、それぞれの [戻り値] フィールドが入力されています。戻り値をまとめて、最終的な付箋の戻り値フィールドに入れます。
分野 | 価値 |
---|---|
メールアドレス | The Fibonaccis |
差出人住所 | Your Home |
パラメーター | 3 |
戻り値 | 2 (戻り値1 + 戻り値2) |
送信者に戻ります。
ご想像のとおり、呼び出した関数にコンピューターを送信した直後に、かなり多くの作業が開始されます。
プログラミング ロジック全体は再帰的ですが、実際には、コンピュータが一連の付箋の助けを借りてアルゴリズムからアルゴリズムへと移動する際に、アルゴリズムが順次実行されます。
タスク: 3 番目のフィボナッチ数を計算します。手順:
付箋に次のことを書き留めます。
分野 | 価値 |
---|---|
指示 | The Fibonaccis |
パラメーター | 3 |
それは本質的にそれです。その付箋は、 の計算結果を表すようになりましたfib(3)
。
パラメータ 3 を という名前のレシピに追加しましThe Fibonaccis
た。誰かがスカラー値を必要としない限り、コンピューターは計算を実行する必要はありません。
私はCharmという名前のプログラミング言語の設計に取り組んでおり、これがその言語でフィボナッチがどのように見えるかです。
fib: (n) => if (
n < 2 // test
n // when true
fib(n-1) + fib(n-2) // when false
)
print(fib(4));
このコードは、命令型と機能型の両方の「バイトコード」にコンパイルできます。
必須の JavaScript バージョンは次のようになります。
let fib = (n) =>
n < 2 ?
n :
fib(n-1) + fib(n-2);
HALF 機能の JavaScript バージョンは次のようになります。
let fib = (n) => () =>
n < 2 ?
n :
fib(n-1) + fib(n-2);
JavaScriptには機能的に同等のものがないため、PUREの機能的なJavaScriptバージョンははるかに複雑になります。
let unwrap = ($) =>
typeof $ !== "function" ? $ : unwrap($());
let $if = ($test, $whenTrue, $whenFalse) => () =>
unwrap($test) ? $whenTrue : $whenFalse;
let $lessThen = (a, b) => () =>
unwrap(a) < unwrap(b);
let $add = ($value, $amount) => () =>
unwrap($value) + unwrap($amount);
let $sub = ($value, $amount) => () =>
unwrap($value) - unwrap($amount);
let $fib = ($n) => () =>
$if(
$lessThen($n, 2),
$n,
$add( $fib( $sub($n, 1) ), $fib( $sub($n, 2) ) )
);
手動で JavaScript コードに「コンパイル」します。
"use strict";
// Library of functions:
/**
* Function that resolves the output of a function.
*/
let $$ = (val) => {
while (typeof val === "function") {
val = val();
}
return val;
}
/**
* Functional if
*
* The $ suffix is a convention I use to show that it is "functional"
* style, and I need to use $$() to "unwrap" the value when I need it.
*/
let if$ = (test, whenTrue, otherwise) => () =>
$$(test) ? whenTrue : otherwise;
/**
* Functional lt (less then)
*/
let lt$ = (leftSide, rightSide) => () =>
$$(leftSide) < $$(rightSide)
/**
* Functional add (+)
*/
let add$ = (leftSide, rightSide) => () =>
$$(leftSide) + $$(rightSide)
// My hand compiled Charm script:
/**
* Functional fib compiled
*/
let fib$ = (n) => if$( // fib: (n) => if(
lt$(n, 2), // n < 2
() => n, // n
() => add$(fib$(n-2), fib$(n-1)) // fib(n-1) + fib(n-2)
) // )
// This takes a microsecond or so, because nothing is calculated
console.log(fib$(30));
// When you need the value, just unwrap it with $$( fib$(30) )
console.log( $$( fib$(5) ))
// The only problem that makes this not truly functional, is that
console.log(fib$(5) === fib$(5)) // is false, while it should be true
// but that should be solveable