関数型プログラミングの鍵は、状態がないことではなく、明示的な状態があることです。
つまり、状態はパラメータとして関数に渡されます。これは実際の値であり、実際に使用して、見て、他の関数に渡すことができます。
たとえば、フィボナッチ数列を計算する動的計画法を見てみましょう。命令型言語では、次のようなものになります。
def fib(n):
A = {}
A[0] = 0
A[1] = 1
for i in [2 .. n+1]:
A[i] = A[i-1] + A[i-2]
return A[n]
状態なしでこれを行うには、ストアを明示的に渡す必要があります。Haskell風の構文を使用する:
fib n = fibHelper 2 n {(1,1), (0,0)}
fibHelper i end cache =
if
i > end
then
lookup end cache
else
let
newVal = (lookup (i-1) cache) + (lookup (i-2) cache)
newCache = insert i newVal cache
in
fibHelper (i+1) end newCache
フィボナッチ数列の配列全体が必要ないため、これは少し工夫されていますが、以前に計算された値のセット全体が必要なナップザックなどのより複雑な動的プログラミング問題にこれを使用することを想像できます。
ここで理解しておくべき重要なことinsert
は、ストアを取り、新しい値を追加した元のストアと等しい新しいストアを返す関数であることです。の元の値はcache
破棄されないため、何らかの「取り消し」操作が必要なアプリケーションがある場合は、状態の履歴を追跡できます。
「しかし、それは非効率に見えます。毎回まったく新しいストアを作成しているのです!」ただし、通常、関数型言語では、これらは参照を使用して巧妙に実装されているため、データの新しいコピー全体が存在することはありません。
また、状態パラメータを設定し、計算の進行に応じて渡し、変更するというこのパターンは非常に一般的であることにも言及する価値があります。人々はStateモナドのような抽象化を発明しました。これにより、必須のように見えるが、純粋に「内部」で機能するものを書くことができます。