8

Expert F#2.0のコピーを入手したところ、次のステートメントに出くわしました。

たとえば、必要に応じて、アルゴリズムの開始時に割り当てられたプライベートデータ構造に副作用を使用し、結果を返す前にこれらのデータ構造を破棄できます。その場合、全体的な結果は事実上副作用のない機能になります。F#ライブラリからの分離の一例は、ライブラリのList.mapの実装です。これは、内部でミューテーションを使用します。書き込みは、他のコードがアクセスできない内部の分離されたデータ構造で行われます。

さて、明らかにこのアプローチの利点はパフォーマンスです。不利な点があるかどうかだけ知りたいのですが、副作用を伴う可能性のある落とし穴はここに当てはまりますか?並列処理は影響を受けますか?

言い換えれば、パフォーマンスが確保されている場合List.map、純粋な方法で実装することが望ましいでしょうか?

(明らかにこれは特にF#を扱っていますが、私は一般的な哲学にも興味があります)

4

7 に答える 7

14

副作用のほとんどすべての欠点は、「プログラムの他の部分との相互作用」に関係していると思います。副作用自体は悪くありません(@Gabeが言うように、純粋関数型プログラムでさえ常にRAMを変更しています)、それは問題(デバッグ/パフォーマンス/理解可能性)を引き起こす効果(非ローカル相互作用)の一般的な副次的結果です/等。)。したがって、純粋にローカルな状態(たとえば、エスケープしないローカル変数)への影響は問題ありません。

(私が考えることができる唯一の欠点は、人間がそのようなローカルミュータブルを見るとき、それが逃げることができるかどうかを推論しなければならないことです。F#では、ローカルミュータブルは決して逃げることができません(クロージャはミュータブルをキャプチャできません)。精神税」は、可変参照型についての推論から来ています。)

要約:効果は逃げられない地元の人々にのみ起こることを自分自身に納得させるのが簡単である限り、効果を使用することは問題ありません。(他の場合にエフェクトを使用することもできますが、この質問スレッドでは、合理的な場合はいつでもエフェクトを避けようとする啓発された機能プログラマーであるため、他の場合は無視します。:))

(非常に深く掘り下げたい場合は、F#のList.mapの実装のようなローカル効果は、並列化の妨げにならないだけでなく、より効率的な実装が割り当てるという観点から、実際には利点です。少ないため、GCの共有リソースへの負担が少なくなります。)

于 2010-09-13T05:50:36.497 に答える
6

サイモンペイトンジョーンズの「怠惰な機能状態のスレッド」に興味があるかもしれません。私は最初の数ページを通過しただけで、それは非常に明確です(残りも非常に明確であると確信しています)。

重要な点はControl.Monad.ST、Haskellでこの種のことを行うために使用する場合、型システム自体がカプセル化を強制するということです。Scala(そしておそらくF#)では、アプローチはもっと「あなたの中でこれを使ってここで卑劣なことを何もしていないことを私たちに信じてListBufferくださいmap」です。

于 2010-09-13T04:47:49.150 に答える
4

関数がローカルのプライベート(関数に対して)可変データ構造を使用する場合、並列化は影響を受けません。したがって、map関数がリストのサイズの配列を内部的に作成し、その配列を埋める要素を反復処理する場合でもmap、同じリストで100回同時に実行でき、の各インスタンスにmapは独自のプライベート配列があるため、心配する必要はありません。コードは、配列が設定されるまで配列の内容を確認できないため、事実上純粋です(あるレベルでは、コンピューターが実際にRAMの状態を変更する必要があることを忘れないでください)。

一方、関数がグローバルな可変データ構造を使用する場合、並列化が影響を受ける可能性があります。たとえば、Memoize関数があるとします。明らかに、それの全体的なポイントは、何らかのグローバル状態を維持することです(関数呼び出しに対してローカルではないという意味では「グローバル」ですが、関数の外部からアクセスできないという意味では「プライベート」です)。同じ引数で関数を複数回実行する必要はありませんが、同じ入力が常に同じ出力を生成するため、それでも純粋です。キャッシュデータ構造がスレッドセーフである場合(のようにConcurrentDictionary)、関数をそれ自体と並行して実行できます。そうでない場合は、同時に実行すると観察できる副作用があるため、関数は純粋ではないと主張することができます。

F#では、純粋関数型ルーチンから始めて、プロファイリングで遅すぎることが示されたときに可変状態(キャッシュ、明示的なループなど)を利用して最適化するのが一般的な手法であることを付け加えておきます。

于 2010-09-13T04:48:32.370 に答える
3

同じアプローチがClojureでも使用されています。Clojureの不変のデータ構造(リスト、マップ、ベクター)には、可変の「一時的な」対応物があります。一時的なものに関するClojureのリファレンスでは、「他のコード」では表示できないコードでのみそれらを使用するように促しています。

クライアントコードのトランジェントのリークに対するガードがあります。

  • 不変のデータ構造で機能する通常の関数は、トランジェントでは機能しません。それらを呼び出すと、例外がスローされます。

  • トランジェントは、それらが作成されたスレッドにバインドされます。他のスレッドからトランジェントを変更すると、例外がスローされます。

clojure.coreコード自体は、バックグラウンドで多くのトランジェントを使用します。

トランジェントを使用する主な利点は、トランジェントが提供する大幅なスピードアップです。

したがって、関数型言語では、厳密に制御された可変状態の使用は問題ないようです。

于 2010-09-13T05:24:27.737 に答える
2

関数を他の関数と並行して実行できるかどうかには影響しません。ただし、関数の内部を並列化できるかどうかには影響しますが、PCを対象とするほとんどの小さな関数(マップなど)では問題になる可能性はほとんどありません。

私が気付いたのは、(Web上や本の中で)優れたF#プログラマーの中には、ループに命令型の手法を使用することについて非常にリラックスしているように見える人がいることです。彼らは、複雑な再帰関数よりも、可変ループ変数を使用した単純なループを好むようです。

于 2010-09-13T04:27:16.150 に答える
2

1つの問題は、「関数型」コードを適切に最適化するために優れた関数型コンパイラーが構築されていることですが、可変のものを使用している場合、コンパイラーは他の場合ほど最適化されない可能性があります。最悪の場合、これは不変のバリアントよりも非効率的なアルゴリズムにつながります。

私が考えることができる他の問題は怠惰です-可変データ構造は通常怠惰ではないので、可変関数は引数の不必要な評価を強制する可能性があります。

于 2010-09-13T05:47:49.920 に答える
0

私はこれに質問で答えます:「あなたは関数を書いているのですか、それとも関数を使っていますか?」

関数には、ユーザーと開発者の2つの側面があります。

ユーザーとしては、関数の内部構造をまったく気にしません。それはバイトコードでコード化することができ、それが予想されるデータ入力->データ出力の契約と一致する限り、今から判断日まで内部でハード副作用を使用することができます。関数はブラックボックスまたはオラクルであり、その内部構造は無関係です(愚かで外部的なことを何もしないと仮定します)。

関数の開発者として、内部構造は非常に重要です。不変性、定数の正確性、および副作用の回避はすべて、関数の開発と維持、および関数の並列ドメインへの拡張に役立ちます。

多くの人が機能を開発し、それを使用するため、これらの両方の側面が当てはまります。

不変性と可変構造の利点は別の問題です。

于 2010-09-13T19:40:16.133 に答える