機能的なスタイルの問題を解決するには、さまざまな方法があります。多くの場合、命令型アルゴリズムを設計するときとは異なる方法で問題を分析することから始めます。そのため、命令型アルゴリズムを作成してから関数型アルゴリズムに「変換」しても、非常に自然な関数型アルゴリズムは生成されません (そして、機能的なスタイルの潜在的な利点の多く)。しかし、関数型プログラミングを始めたばかりの経験豊富な命令型プログラマーの場合は、それだけで十分であり、新しい概念を理解するための良い方法です。したがって、関数型スタイルに記述したような関数をかなり非創造的な方法 (つまり、別のアルゴリズムを考え出さない) で "変換" する方法は次のとおりです。
else
残りは問題ないので、式だけを考えてみましょう。
関数型スタイルにはループがないため、コード ブロック (命令型ループの本体) を何度も実行する必要がある場合、そのコード ブロックは関数でなければなりません。多くの場合、関数は単純な非再帰関数であり、map や fold などの高階関数を呼び出して実際の再帰を実行しますが、再帰的に考える練習が必要であり、それを明示的に見たいと思っていると思います。ループ条件は、ループ本体で手元にある量から計算されるため、まったく同じ条件に応じて、ループ置換関数を再帰的に呼び出すだけです。
} else {
var cnt = 0
var i = 0
def loop(????) : ??? = {
if (money - (i * coins.head) > 0) {
cnt += countChange_sort(money - (i * coins.head), coins.tail)
i = i + 1
loop(????)
}
}
loop(????)
cnt
}
情報は、入力引数またはその定義を介してのみ関数に伝達され、戻り値を介して関数から伝達されます。
定義を通じて関数に入力される情報は、関数が作成されるとき (コンパイル時、またはクロージャが作成される実行時) には一定です。cnt
とi
に含まれる情報は、呼び出しごとに異なる必要があるため、あまり役に立ちません。したがって、それらは明らかに引数として渡す必要があります。
} else {
var cnt = 0
var i = 0
def loop(cnt : Int, i : Int) : ??? = {
if (money - (i * coins.head) > 0) {
cnt += countChange_sort(money - (i * coins.head), coins.tail)
i = i + 1
loop(cnt, i)
}
}
loop(cnt, i)
cnt
}
cnt
しかし、関数呼び出しの後に の最終値を使用したいと考えています。情報がその戻り値を介してのみ伝達される loop
場合、最後の値はそれcnt
をloop
返すことによってのみ取得できます。それはとても簡単です:
} else {
var cnt = 0
var i = 0
def loop(cnt : Int, i : Int) : Int = {
if (money - (i * coins.head) > 0) {
cnt += countChange_sort(money - (i * coins.head), coins.tail)
i = i + 1
loop(cnt, i)
} else {
cnt
}
}
cnt = loop(cnt, i)
cnt
}
coins
、money
、およびcountChange_sort
は、「定義によって関数に入る」情報の例です。coins
およびmoney
は「変数」でもありますが、 が定義された時点では一定ですloop
。loop
の本体から離れてcountChange_sort
スタンドアロン関数になりたい場合はcoins
、money
追加の引数を作成する必要があります。これらは の最上位呼び出しから渡され、countChange_sort
内の各再帰呼び出しで変更されずに渡されますloop
。loop
ただし、それはそれ自体に依存することになります(算術演算子と! とcountChange_sort
同様)。そのため、引数を介して関数に入らない外部のものについて関数に認識させることから逃れることはできません。*
-
かなり良いですね。しかし、まだ代入ステートメントを内部で使用していますがloop
、これは正しくありません。ただし、新しい値を に代入cnt
しi
て の再帰呼び出しに渡すだけなloop
ので、これらの代入は簡単に削除できます。
} else {
var cnt = 0
var i = 0
def loop(cnt : Int, i : Int) : Int = {
if (money - (i * coins.head) > 0) {
loop(cnt + countChange_sort(money - (i * coins.head), coins.tail), i + 1)
} else {
cnt
}
}
cnt = loop(cnt, i)
cnt
}
cnt
ここで、いくつかの明らかな単純化があります。なぜなら、ミュータブルをi
初期化し、初期値を渡し、cnt
一度割り当ててからすぐに返す以外は、実際には何もしていないからです。cnt
したがって、(最終的に)変更可能なものをi
完全に取り除くことができます。
} else {
def loop(cnt : Int, i : Int) : Int = {
if (money - (i * coins.head) > 0) {
loop(cnt + countChange_sort(money - (i * coins.head), coins.tail), i + 1)
} else {
cnt
}
}
loop(0, 0)
}
これで完了です。目に見える副作用はありません!
あなたのアルゴリズムが実際に何をするかについて、私はまったく考えていないことに注意してください (私はそれが実際に正しいかどうかを判断する試みさえしていませんが、正しいと思います)。私が行ったのは、情報は引数を介してのみ関数に入り、戻り値を介して出るという一般原則を直接適用したことだけです。式にアクセスできるすべての変更可能な状態は、実際には式の余分な隠し入力と隠し出力です。それらを不変の明示的な入力と出力にすると、不要なものを取り除くことができます。たとえば、実際には何も必要ないため、i
からの戻り値に含める必要はありません。関数型スタイルへの変換により、純粋に内部にあることが明らかになりました。loop
loop
、これを推測するために命令型バージョンのコードを実際に読む必要がありました。
cnt
およびアキュムレータi
として知られているものです。アキュムレータは特別なものではなく、単なる通常の引数です。この用語は、それらの使用方法のみを指します。基本的に、アルゴリズムが進行中のデータを追跡する必要がある場合は、accumulator パラメーターを導入して、各再帰呼び出しがこれまでに行われたデータからデータを「転送」できるようにすることができます。それらはしばしば、ローカルの一時的な変更可能な変数が命令型アルゴリズムで果たす役割を果たします。
この場合のように、再帰関数の戻り値がアキュムレータ パラメーターの値になるのは非常に一般的なパターンですcnt
。
これらの種類の手法は、必ずしも適切な機能コードを生成するわけではありませんが、この手法を使用して、「ローカル」可変状態を使用して実装された関数を機能スタイルに変換するのは非常に簡単です。ほとんどの従来の OO プログラムに典型的な、広範囲にわたる非ローカルな可変性の使用は、このように変換するのが困難です。それはできますが、プログラム全体を一度に変更する必要があり、結果の関数には多数の余分な引数が含まれます (元のプログラムに存在していたすべての隠されたデータフローが明示的に公開されます)。