私はscalaを学ぼうとしていますが、この概念を理解することができません。オブジェクトを不変にすることが、関数の副作用を防ぐのに役立つのはなぜですか。私が5歳のように誰かが説明できますか?
7 に答える
興味深い質問ですが、答えるのは少し難しいです。
関数型プログラミングとは、数学を使ってプログラムについて推論することです。そのためには、プログラムを説明し、プログラムが持つ可能性のあるプロパティについて証明する方法を説明する形式が必要です。
ラムダ計算やチューリングマシンなど、そのような形式を提供する計算モデルはたくさんあります。そして、それらの間にはある程度の同等性があります(議論については、この質問を参照してください)。
非常に現実的な意味で、可変性やその他の副作用のあるプログラムは、関数型プログラムに直接マッピングされます。この例を考えてみましょう。
a = 0
b = 1
a = a + b
関数型プログラムにマッピングする2つの方法があります。最初のものは「状態」の一部でa
ありb
、各行は状態から新しい状態への関数です。
state1 = (a = 0, b = ?)
state2 = (a = state1.a, b = 1)
state3 = (a = state2.a + state2.b, b = state2.b)
別の例では、各変数が時間に関連付けられています。
(a, t0) = 0
(b, t1) = 1
(a, t2) = (a, t0) + (b, t1)
それで、上記を踏まえて、なぜ可変性を使用しないのですか?
さて、ここに数学についての興味深いことがあります:形式主義がより強力でないほど、それで証明をするのはより簡単です。言い換えれば、可変性のあるプログラムについて推論するのは難しいのです。
結果として、可変性を備えたプログラミングの概念に関する進歩はほとんどありません。有名なデザインパターンは、研究を通じて到達したものではなく、数学的な裏付けもありません。代わりに、それらは何年にもわたる試行錯誤の結果であり、それらのいくつかはそれ以来誤った方向に進んでいることが証明されています。いたるところに見られる他の数十の「デザインパターン」について誰が知っていますか?
一方、Haskellのプログラマーは、ファンクター、モナド、コモナド、ジッパー、Applicatives、レンズなど、数学的裏付けのある数十の概念と、最も重要なこととして、プログラムを構成するためのコードの実際のパターンを考え出しました。プログラムについて推論し、再利用性を高め、正確性を向上させるために使用できるもの。例については、Typeclassopediaをご覧ください。
関数型プログラミングに慣れていない人がこのようなものに少し怖がるのも不思議ではありません...比較すると、プログラミングの世界の残りの部分はまだ数十年前の概念で働いています。新しい概念のアイデアそのものは異質です。
残念ながら、これらすべてのパターン、これらすべての概念は、使用しているコードにのみ適用され、可変性(または他の副作用)は含まれていません。もしそうなら、それらのプロパティは有効でなくなり、あなたはそれらに頼ることができません。推測、テスト、デバッグに戻ります。
つまり、関数がオブジェクトを変更すると、副作用が発生します。突然変異は副作用です。これは当然のことです。
実際、純粋に関数型言語では、オブジェクトが技術的に変更可能であるか不変であるかは問題ではありません。言語はとにかくオブジェクトを変更しようとは決してしないからです。純粋な関数型言語では、副作用を実行する方法はありません。
ただし、Scalaは純粋な関数型言語ではなく、副作用が非常に一般的なJava環境で実行されます。この環境では、ミューテーションが不可能なオブジェクトを使用すると、副作用指向のスタイルが不可能になるため、純粋な機能スタイルを使用することをお勧めします。言語があなたのためにそれをしないので、あなたは純粋さを強制するためにデータ型を使用しています。
これがあなたにとって意味のあるものになることを願って、他にもたくさんのことを言います。
関数型言語の変数の概念の基本は、参照透過性です。
参照透過性とは、値とその値への参照の間に違いがないことを意味します。これが当てはまる言語では、プログラムが機能することを考えるのがはるかに簡単になります。停止して尋ねる必要がないため、これは値ですか、それとも値への参照ですか。Cでプログラムしたことのある人なら誰でも、そのパラダイムを学ぶという課題の大部分は、常にどれがどれであるかを知ることであることを認識しています。
参照透過性を持たせるために、参照が参照する値は決して変更できません。
(警告、私はアナロジーを作ろうとしています。)
このように考えてください:あなたの携帯電話で、あなたは他の人の携帯電話のいくつかの電話番号を保存しました。あなたは、あなたがその電話番号に電話をかけるときはいつでも、あなたが話したい相手に連絡すると思います。他の誰かがあなたの友人と話したい場合、あなたは彼らに電話番号を与え、彼らは同じ人に連絡します。
誰かが携帯電話番号を変更すると、このシステムは故障します。突然、彼らに連絡したい場合は、彼らの新しい電話番号を取得する必要があります。たぶん、あなたは6か月後に同じ番号に電話をかけ、別の人に連絡します。同じ番号に電話をかけ、別の人に連絡することは、関数が副作用を実行するときに起こります。同じように見えるものがありますが、それを使おうとすると、今では違うことがわかります。あなたがこれを期待したとしても、あなたがその番号を与えたすべての人々はどうですか、あなたは彼ら全員に電話をかけて、古い番号はもう同じ人に届かないと彼らに言うつもりですか?
あなたはその人に対応する電話番号を頼りにしましたが、実際にはそうではありませんでした。電話番号システムには参照透過性がありません。番号は実際には常に人と同じではありません。
関数型言語はこの問題を回避します。あなたはあなたの電話番号を与えることができ、人々はあなたの人生の残りの間、いつでもあなたに連絡することができ、その番号で他の人に連絡することは決してありません。
ただし、Javaプラットフォームでは、状況が変わる可能性があります。あなたが思っていたのは一つのことでしたが、1分後に別のことに変わるかもしれません。この場合、どうすればそれを止めることができますか?
Scalaは、参照透過性を持つクラスを作成することにより、型の力を使用してこれを防ぎます。したがって、言語全体が参照透過性ではありませんが、不変の型を使用している限り、コードは参照透過性になります。
実際には、不変型を使用したコーディングの利点は次のとおりです。
- 読者が驚くべき副作用に注意する必要がない場合、コードは読みやすくなります。
- 複数のスレッドを使用する場合、共有オブジェクトは変更できないため、ロックについて心配する必要はありません。副作用がある場合は、コードをよく考えて、2つのスレッドが同じオブジェクトを同時に変更しようとする可能性のあるすべての場所を把握し、これが引き起こす可能性のある問題から保護する必要があります。
- 理論的には、少なくとも、コンパイラーは、不変の型のみを使用する場合、一部のコードをより適切に最適化できます。ただし、Javaは副作用を許容するため、これを効果的に実行できるかどうかはわかりません。とにかく、これはせいぜいトスアップです。副作用を使用することではるかに効率的に解決できる問題がいくつかあるからです。
私はこの5歳の説明で実行しています:
class Account(var myMoney:List[Int] = List(10, 10, 1, 1, 1, 5)) {
def getBalance = println(myMoney.sum + " dollars available")
def myMoneyWithInterest = {
myMoney = myMoney.map(_ * 2)
println(myMoney.sum + " dollars will accru in 1 year")
}
}
私たちがATMにいて、このコードを使用してアカウント情報を提供していると仮定します。
次のことを行います。
scala> val myAccount = new Account()
myAccount: Account = Account@7f4a6c40
scala> myAccount.getBalance
28 dollars available
scala> myAccount.myMoneyWithInterest
56 dollars will accru in 1 year
scala> myAccount.getBalance
56 dollars available
現在の残高と1年分の利息をmutated
確認したいときの口座残高。アカウントの残高が正しくありません。銀行にとって悪いニュースです!
クラス定義を追跡するval
代わりにを使用していた場合、私たちはドルを稼ぎ、バランスを上げることができなかったでしょう。var
myMoney
mutate
(REPLで)クラスを次のように定義する場合val
:
error: reassignment to val
myMoney = myMoney.map(_ * 2
Scalaは、私たちがimmutable
価値を求めていたが、それを変えようとしていると言っています!
Scalaのおかげで、に切り替えてメソッドをval
書き直し、クラスがバランスを変更することは決してないmyMoneyWithInterest
ので安心できます。Account
関数型プログラミングの重要な特性の1つは、同じ引数を使用して同じ関数を2回呼び出すと、同じ結果が得られることです。これにより、多くの場合、コードについての推論がはるかに簡単になります。
content
ここで、あるオブジェクトの属性を返す関数を想像してみてください。それcontent
が変更される可能性がある場合、関数は同じ引数を使用した異なる呼び出しで異なる結果を返す可能性があります。=>関数型プログラミングはもう必要ありません。
最初のいくつかの定義:
- 副作用は状態の変化です-突然変異とも呼ばれます。
- 不変オブジェクトは、ミューテーション(副作用)をサポートしないオブジェクトです。
(パラメーターとして、またはグローバル環境で)可変オブジェクトが渡される関数は、副作用を生成する場合と生成しない場合があります。これは実装次第です。
ただし、(パラメーターとして、またはグローバル環境で)不変オブジェクトのみが渡される関数が副作用を生成することは不可能です。したがって、不変オブジェクトを排他的に使用すると、副作用の可能性が排除されます。
ネイトの答えは素晴らしいです、そしてここにいくつかの例があります。
関数型プログラミングでは、同じ引数で関数を呼び出すと、常に同じ戻り値が得られるという重要な機能があります。
これは、不変オブジェクトの作成後に変更できないため、常に当てはまります。
class MyValue(val value: Int)
def plus(x: MyValue) = x.value + 10
val x = new MyValue(10)
val y = plus(x) // y is 20
val z = plus(x) // z is still 20, plus(x) will always yield 20
ただし、可変オブジェクトがある場合、plus(x)がMyValueの同じインスタンスに対して常に同じ値を返すことを保証することはできません。
class MyValue(var value: Int)
def plus(x: MyValue) = x.value + 10
val x = new MyValue(10)
val y = plus(x) // y is 20
x.value = 30
val z = plus(x) // z is 40, you can't for sure what value will plus(x) return because MyValue.value may be changed at any point.
不変オブジェクトが関数型プログラミングを可能にするのはなぜですか?
彼らはしません。
「関数」、「prodecure」、「routine」、「method」の1つの定義を考えてみましょう。これは、多くのプログラミング言語に当てはまると思います。「通常は名前が付けられ、引数を受け入れたり、値を返したりするコードのセクション」。
「関数型プログラミング」の定義を1つ取り上げます。「関数を使用したプログラミング」です。関数を使用してプログラムする機能は、状態が変更されているかどうかに依存しません。
たとえば、Schemeは関数型プログラミング言語と見なされます。末尾呼び出し、高階関数、および関数を使用した集計操作を備えています。また、可変オブジェクトもあります。可変性はいくつかの優れた数学的性質を破壊しますが、必ずしも「関数型プログラミング」を妨げるわけではありません。