関数型プログラミングでは、副作用のないローカル可変変数は依然として「悪い習慣」と見なされますか?


23

内部でのみ使用される関数内に可変ローカル変数を持っていますか(たとえば、少なくとも意図的に副作用がないなど)、まだ「非機能的」と見なされますか?

たとえば、「Scalaをvar使用した関数型プログラミング」のコーススタイルチェックでは、使用法が不適切であると見なされます。

私の質問、関数に副作用がない場合、命令型のコードを書くことはまだ推奨されていませんか?

たとえば、アキュムレータパターンでテール再帰を使用する代わりに、入力が変更されない限り、ローカルforループを実行し、ローカルミュータブルListBufferを作成して追加することの何が問題になっていますか?

答えが「はい、副作用がなくても常に落胆している」場合、その理由は何ですか?


3
私がこれまでに聞いたトピックに関するすべてのアドバイス、推奨などは、複雑な原因として共有可能な可変状態を参照しています。そのコースは初心者のみが使用することを意図していますか?それはおそらく、意図的な過度の単純化です。
キリアンフォス

3
@KilianFoth:共有可変状態はマルチスレッドコンテキストの問題ですが、非共有可変状態はプログラムの推論も困難になる可能性があります。
マイケルショー

1
ローカルの可変変数を使用することは必ずしも悪い習慣ではないが、それは「機能的なスタイル」ではない。機能スタイルと命令スタイルを明確に区別できたら、どちらを使用するかを決定できます(プログラミング言語で両方が許可されている場合)。var常に機能しません。Scalaには遅延値と末尾再帰の最適化があり、変数を完全に回避できます。
ジョルジオ

回答:


17

ここで明確に悪い習慣である1つのことは、何かが純粋な関数であると主張することです。

可変変数が真に完全に独立した方法で使用される場合、関数は外部的に純粋であり、誰もが満足しています。実際 Haskell はこれを明示的サポートしており、型システムは、可変参照がそれらを作成する関数の外部で使用できないようにします。

そうは言っても、「副作用」について話すことはそれを見る最良の方法ではないと思います(そして、私が上記で「純粋」と言った理由です)。関数と外部状態の間に依存関係を作成するものはすべて、物事の推論を難しくします。これには、現在の時刻を知ることや、非スレッドセーフな方法で隠された可変状態を使用することなどが含まれます。


16

問題はそれ自体可変性ではなく、参照の透明性の欠如です。

参照透過的なものとその参照は常に等しくなければならないので、参照透過関数は与えられた入力セットに対して常に同じ結果を返し、参照透過的な「変数」は実際には変数ではなく値です。変更できません。内部に可変変数を持つ参照透過的な関数を作成できます。それは問題ではありません。ただし、何をしているのかにもよりますが、関数が参照的に透過的であることを保証するのは難しいかもしれません。

非常に機能的なものを実行するために、可変性を使用する必要がある場所について考えることができるインスタンスが1つあります。それはメモ化です。メモ化は関数からの値をキャッシュするため、再計算する必要はありません。参照的に透過的ですが、突然変異を使用します。

しかし、一般に、参照透過関数とメモ化のローカル可変変数以外に、参照透過と不変性が一緒になっている場合、これが当てはまらない他の例があるかどうかはわかりません。


4
メモ化についてのあなたのポイントはとても良いです。Haskellはプログラミングの参照透過性を強く強調していますが、遅延評価のメモのような動作には、背後で言語ランタイムによって行われる膨大な量の突然変異が含まれます。
CAマッキャン

@CA McCann:あなたの言うことは非常に重要だと思います。関数型言語では、ランタイムは突然変異を使用して計算を最適化できますが、プログラマーが突然変異を使用できる言語の構造はありません。もう1つの例は、ループ変数を使用したwhileループです。Haskellでは、(スタックの使用を避けるために)可変変数で実装できる末尾再帰関数を記述できますが、プログラマーは、1つから渡される不変の関数引数次を呼び出します。
ジョルジオ

@Michael Shaw:「問題はそれ自体可変性ではなく、参照の透明性の欠如です」の+1。たぶん、一意性タイプを持つClean言語を引用できます。これらは、可変性を許可しますが、参照の透明性を保証します。
ジョルジオ

@Giorgio:Cleanについては何もよく知りませんが、時々言及されていると聞きました。たぶん私はそれを調べる必要があります。
マイケルショー

@Michael Shaw:Cleanについてはあまり知りませんが、参照の透明性を確保するために一意性タイプを使用することは知っています。基本的に、変更後に古い値への参照がなければ、データオブジェクトを変更できます。IMOこれはあなたのポイントを示しています:参照の透明性は最も重要なポイントであり、不変性はそれを保証する唯一の可能な方法です。
ジョルジオ

8

これを「良い練習」対「悪い練習」に要約するのは本当に良くありません。Scalaは不変値、つまり本質的に反復的な値よりも特定の問題をはるかによく解決するため、可変値をサポートします。

見通しのために、私はCanBuildFromscalaによって提供されるほぼすべての不変の構造を介して内部的に何らかの突然変異を行うと確信しています。ポイントは、彼らが公開するものは不変であるということです。できるだけ多くの不変値として保つことはプログラムを作ることができますについての理由を簡単にし、エラーが発生しにくいです

これは、可変性に適した問題がある場合に、内部で可変構造と値を避ける必要があるという意味ではありません。

これを念頭に置いて、Scalaのような言語が提供する多くの高階関数(map / filter / fold)を使用すると、通常可変変数(ループなど)を必要とする多くの問題をより適切に解決できます。それらに注意してください。


2
うん、Scalaのコレクションを使用するときにforループはほとんど必要ありません。mapfilterfoldLeftforEach そうではない場合に最も時間のトリックを行う、しかし、私はブルートフォース不可欠コードに戻るまで「OK」だ感じることができることがいいです。(もちろん副作用がない限り)
エランメダン

3

スレッドセーフに関する潜在的な問題は別として、通常、多くのタイプセーフも失います。命令型ループの戻り値の型はUnit、ほとんどすべての式を入力に使用できます。高階関数や再帰でさえ、より正確な意味論と型を持っています。

また、命令型ループを使用する場合よりも、機能的なコンテナー処理のオプションが多くあります。不可欠で、あなたは基本的に持っているforwhileなどが挙げられるこれら二つの小さな変化do...whileforeach

機能的には、Scalaのより一般的なものをいくつか挙げると、集約、カウント、フィルター、検索、flatMap、fold、groupBy、lastIndexWhere、map、maxBy、minBy、partition、scan、sortBy、sortWith、span、およびtakeWhileがあります。標準ライブラリ。それらを利用できることに慣れると、命令型forループは比較して基本的すぎるように見えます。

ローカル可変性を使用する唯一の本当の理由は、パフォーマンスのためです。


2

ほとんど大丈夫だと思います。さらに、この方法で構造を生成することは、場合によってはパフォーマンスを改善する良い方法かもしれません。Clojureは、Transientデータ構造を提供することでこの問題に対処しています

基本的な考え方は、限られた範囲で局所変異を許可し、構造を返す前に構造を凍結することです。このようにして、ユーザーはコードが純粋であるかのように推論できますが、必要なときにインプレース変換を実行できます。

リンクが言うように:

木が森に落ちた場合、音がしますか?純粋な関数が不変の戻り値を生成するためにいくつかのローカルデータを変更する場合、それは大丈夫ですか?


2

ローカル可変変数を持たないことには、1つの利点があります。つまり、関数がスレッドに対してより使いやすくなります。

このようなローカル変数に焼かれて(コード内にもソースも持っていなかった)、低確率のデータ破損が発生しました。スレッドの安全性については何ら言及されていませんでした。呼び出し間で持続する状態はなく、副作用もありませんでした。スレッドセーフではないことは私にはありませんでした。100,000のランダムデータ破損の1つを追いかけるのは王室の痛みです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.