関数型プログラミング自体のさまざまな概念を理解しています: 副作用、不変性、純粋関数、参照透過性。しかし、頭の中でそれらを結びつけることはできません。たとえば、次のような質問があります。
refの間の関係は何ですか。透明性と不変性。一方は他方を意味しますか?
副作用と不変性は同じ意味で使用されることがあります。それが正しいか?
関数型プログラミング自体のさまざまな概念を理解しています: 副作用、不変性、純粋関数、参照透過性。しかし、頭の中でそれらを結びつけることはできません。たとえば、次のような質問があります。
refの間の関係は何ですか。透明性と不変性。一方は他方を意味しますか?
副作用と不変性は同じ意味で使用されることがあります。それが正しいか?
この質問には、一般的な語彙の定義に関するものであるため、特に細かい回答が必要です。
まず、関数は、入力の「ドメイン」と出力の「範囲」(またはコドメイン) の間の一種の数学的関係です。すべての入力は、明確な出力を生成します。たとえば、整数加算関数+
は定義域で入力を受け入れInt x Int
、範囲 で出力を生成しますInt
。
object Ex0 {
def +(x: Int, y: Int): Int = x + y
}
と に任意の値を指定するx
とy
、明らか+
に常に同じ結果が得られます。これは関数です。コンパイラが非常に賢い場合、入力の各ペアに対してこの関数の結果をキャッシュするコードを挿入し、最適化としてキャッシュ ルックアップを実行できます。ここは明らかに安全です。
問題は、ソフトウェアでは「関数」という用語が多少乱用されていることです。関数は、署名で宣言されているように引数を受け入れて値を返しますが、外部コンテキストに対して読み書きすることもできます。例えば:
class Ex1 {
def +(x: Int): Int = x + Random.nextInt
}
x
の特定の値に対して、異なる結果が生成される可能性があるため、これを数学関数と考えることができなくなりました (ランダムな値に応じて、の署名の+
どこにも表示されません)。+
の結果は、+
上記のように安全にキャッシュできません。これで語彙の問題があり、それEx0.+
はpureであり、Ex1.+
not であると言って解決します。
さて、ある程度の不純物を受け入れたので、どのような種類の不純物について話しているのかを定義する必要があります! この場合の違いはEx0.+
、入力x
およびy
に関連付けられた の結果をキャッシュできることと、入力 に関連付けられた の結果をキャッシュできないことです。キャッシュ可能性 (または、より適切には、関数呼び出しとその出力の代用可能性) を説明するために使用する用語は、参照透過性です。Ex1.+
x
すべての純粋関数は参照透過的ですが、一部の参照透過関数は純粋ではありません。例えば:
object Ex2 {
var lastResult: Int
def +(x: Int, y: Int): Int = {
lastResult = x + y
lastResult
}
}
ここでは、外部コンテキストからは読み取らず、 によって生成された値はEx2.+
、 のx
ようy
に常にキャッシュ可能Ex0
です。これは参照透過的ですが、関数によって計算された最後の値を格納するという副作用があります。他の誰かが後でやって来て、 をつかむことができlastResult
ますEx2.+
。
補足:キャッシングは関数の結果に関しては安全ですが、キャッシュが「ヒット」した場合、副作用は暗黙のうちに無視されるため、参照透過的で
Ex2.+
はないと主張することもできます。つまり、副作用が重要な場合、キャッシュを導入するとプログラムの意味が変わります (したがって、Norman Ramsey のコメント)。この定義を好む場合、関数は参照透過性を保つために純粋でなければなりません。
ここで注目すべきことの 1 つはEx2.+
、同じ入力で連続して 2 回以上呼び出してlastResult
も変化しないということです。メソッドをn回呼び出した場合の副作用は、メソッドを 1 回だけ呼び出した場合の副作用と同等であるため、べき等であるとEx2.+
言えます。次のように変更できます。
object Ex3 {
var history: Seq[Int]
def +(x: Int, y: Int): Int = {
result = x + y
history = history :+ result
result
}
}
これで、 を呼び出すたびEx3.+
に履歴が変更されるため、関数は冪等ではなくなります。
さて、これまでのおさらい:純粋な関数とは、外部コンテキストからの読み取りも書き込みも行わない関数です。参照透過性であり、副作用もありません。一部の外部コンテキストから読み取る関数は参照透過的ではなくなりましたが、一部の外部コンテキストに書き込む関数には副作用がなくなりました。最後に、同じ入力で複数回呼び出されたときに、1 回だけ呼び出した場合と同じ副作用を持つ関数は、べき等と呼ばれます。純粋な関数など、副作用のない関数も冪等であることに注意してください。
では、可変性と不変性は、これらすべてにどのように影響するのでしょうか? さて、 と を振り返ってEx2
くださいEx3
。それらは変更可能な s を導入しvar
ます。Ex2.+
andの副作用はEx3.+
、それぞれvar
の を変異させることです! したがって、可変性と副作用は密接に関連しています。不変データのみを操作する関数は、副作用がないようにする必要があります。まだ純粋ではない可能性があります (つまり、参照透過性ではない可能性があります) が、少なくとも副作用は発生しません。
これに対する論理的なフォローアップの質問は、「純粋に機能的なスタイルの利点は何ですか?」というものかもしれません。その質問への答えはもっと複雑です;)
最初に「いいえ」 - 一方は他方を暗示しますが、その逆ではなく、2 番目に修飾された「はい」です。
「プログラムの動作を変更せずに式をその値に置き換えることができる場合、その式は参照透過的であると言われます」。
不変の入力は、式 (関数) が常に同じ値に評価されることを示唆しているため、参照透過的です。
ただし、(mergeconflict はこの点について親切に訂正してくれました)参照透過であることは必ずしもimmutabilityを必要とするわけではありません。
定義上、副作用は関数の側面です。つまり、関数を呼び出すと、何かが変更されます。
不変性はデータの一側面です。変更することはできません。そのような関数を呼び出すことは、副作用がないことを意味します。(Scala では、これは「不変オブジェクトへの変更なし」に限定されます。開発者には責任と決定があります)。
副作用と不変性は同じ意味ではありませんが、関数と関数が適用されるデータの側面と密接に関連しています
。
Scala は純粋な関数型プログラミング言語ではないため、「不変入力」などのステートメントの意味を考慮するときは注意が必要です。関数への入力のスコープには、パラメーターとして渡される要素以外の要素が含まれる場合があります。同様に、副作用を考慮する場合。
むしろ、使用する特定の定義に依存します (意見の相違がある可能性があります。たとえば、純度と参照の透過性を参照してください) が、これは合理的な解釈だと思います。
参照透過性と「純度」は、関数/式のプロパティです。関数/式には、副作用がある場合とない場合があります。一方、不変性はオブジェクトのプロパティであり、関数/式ではありません。
参照透過性、副作用、および純度は密接に関連しています。「純粋」と「参照透過性」は同等であり、これらの概念は副作用がないことと同等です。
不変オブジェクトには、参照透過ではないメソッドが含まれる場合があります。これらのメソッドは、オブジェクト自体を変更しませんが (オブジェクトが可変になるため)、I/O の実行やそれらの (可変) パラメータの操作などの他の副作用がある場合があります。 .