有害と考えられる返品?コードがなくてもコードは機能しますか?


45

さて、タイトルは少しクリックされていますが、真剣に私は言いました、しばらくキックを求めないでください。私は、メソッドが真のオブジェクト指向の方法でメッセージとして使用されることを奨励する方法が好きです。しかし、これには頭の中のガタガタする問題があります。

よく書かれたコードは、オブジェクト指向の原則と機能の原則を同時に満たすことができると疑うようになりました。私はこれらの考えを調和させようとしています、そして、私が着陸した大きなこだわりポイントはreturnです。

純関数には次の2つの性質があります。

  1. 同じ入力で繰り返し呼び出すと、常に同じ結果が得られます。これは、不変であることを意味します。その状態は一度だけ設定されます。

  2. 副作用はありません。呼び出しによって引き起こされる唯一の変更は、結果の生成です。

それでは、return結果を伝える方法として使用することを誓った場合、どうすれば純粋に機能的になりますか?

TELLは、聞いていない、いくつかの副作用を検討するものを使用してアイデアの作品を。オブジェクトを扱うとき、その内部状態については尋ねません。何をする必要があるかを伝え、その内部状態を使用して、私がそれを行うように指示したことで何をすべきかを見つけます。一度言ったら、何をしたのかは聞かない。私は、それがするように言われたことについて何かをしたと思っています。

Tell、Den's Askは単にカプセル化の別の名前以上のものだと思います。私が使うとき、私はreturn何が私を呼んだのかわかりません。私はそれのプロトコルを話すことができません、私はそれが私のプロトコルに対処することを強制する必要があります。多くの場合、これは内部状態として表されます。公開されているものが正確な状態ではない場合でも、通常は状態と入力引数に対して実行される計算にすぎません。応答するインターフェースがあると、結果を内部状態や計算よりも意味のあるものにマッサージする機会が与えられます。それはメッセージパッシングです。この例を参照してください

昔、ディスクドライブには実際にディスクが搭載されていて、指で触れるにはホイールが冷たすぎるときに親指ドライブが車で行っていたものでした。void swap(int *first, int *second)とても便利に見えましたが、結果を返す関数を書くことが奨励されました。それで私はこれを信仰で心に留め、それを追い始めました。

しかし、今では、オブジェクトを構築する方法によって結果の送信先を制御できるアーキテクチャを構築する人々がいます。以下に実装例を示します。出力ポートオブジェクトを注入することは、出力パラメーターの考え方に似ています。しかし、それが、tell-don't-askオブジェクトが他のオブジェクトに何をしたかを伝える方法です。

副作用について最初に学んだとき、それを出力パラメーターのように考えました。私たちは、仕事のいくつかを驚くべき方法で、つまりreturn result慣習に従わないことによって人々を驚かせないように言われていました。確かに、副作用が発生する並列非同期スレッドの問題が山積みになっていることはわかっていますが、戻り値は実際には単に結果をスタックにプッシュしたままにしておくため、呼び出されたものは後でポップオフできます。それだけです。

私が本当に求めていること:

あるreturnすべてのその副作用の悲惨さを回避し、ロックせずに、スレッドの安全性を取得する唯一の方法は、など、または私は従うことができます聞いていない、言う純粋に機能的な方法で?


3
コマンドクエリの分離を無視しないことを選択した場合、問題は解決したと思いますか?
-rwong

30
キックで自分自身を見つけることは、各特定の状況の長所と短所を推論するのではなく、ドグマ駆動型の設計に取り組んでいることを示している可能性があることを考慮してください。
Blrfl

3
人々は有害であること、「リターン」について話すとき、一般的に、彼らはそれが構造化プログラミングではなく、機能、および反対だと言っているシングル(およびルーチンの最後にreturn文多分ことのif / elseブロックの両側の終わりにそれ自体が最後の要素です)は含まれません。
Random832

16
@jameslarge:誤った二分法。独断的な思考によってデザインに追い込まれないようにすることは、あなたが話しているワイルドウェスト/すべてが近づくと同じことではありません。要点は、ドグマが良い、シンプルで明白なコードの邪魔にならないようにすることです。普遍法は欠乏のためのものです。文脈は王のためです。
ニコルボーラス

4
あなたの質問には不明確なことが1つあります:あなたは「リターン」を使用して誓ったと言いますが、あなたが他の概念に明示的に関連付けていないか、あなたがこれをした理由を言うことはできません。結果の生成を含む純粋な関数の定義と組み合わせると、解決不可能な何かを作成します。
ダニコフ

回答:


96

関数に副作用がなく、何も返さない場合、その関数は役に立ちません。それはそれと同じくらい簡単です。

しかし、ルールの文字に従い、根本的な推論を無視したい場合は、いくつかのチートを使用できると思います。たとえば、出力パラメータを使用すると、厳密にはリターンを使用しません。しかし、さらに複雑な方法で、リターンとまったく同じように機能します。したがって、理由が理由でリターンが悪いと思われる場合は、同じ基本的な理由でoutパラメータを使用することは明らかに悪いです。

より複雑なチートを使用できます。たとえば、HaskellはIOモナドトリックで有名で、実際には副作用がありますが、厳密に言えば理論的には副作用があります。継続渡しスタイルは別のトリックで、コードをスパゲッティに変えるという犠牲を払ってリターンを回避できます。

肝心なのは、愚かなトリックがなければ、副作用のない関数と「リターンなし」という2つの原則は、単に互換性がないということです。さらに、私はそれらの両方がそもそも本当に悪い原則(本当に教義)であることを指摘しますが、それは別の議論です。

「伝える、尋ねない」、「副作用なし」などのルールは、普遍的に適用することはできません。常にコンテキストを考慮する必要があります。副作用のないプログラムは文字通り役に立たない。純粋な関数型言語でさえ、それを認めています。むしろ、副作用のある部分からコードの純粋な部分を分離するよう努めています。HaskellのStateまたはIOモナドのポイントは、副作用を回避できないことではありません-できないからです-しかし、副作用の存在は関数シグネチャによって明示的に示されます。

tell-dont-askルールは、異なる種類のアーキテクチャ-プログラム内のオブジェクトが互いに通信する独立した「アクター」であるスタイルに適用されます。各アクターは基本的に自律的でカプセル化されています。それにメッセージを送信して、それに対してどのように反応するかを決定できますが、外部からアクターの内部状態を調べることはできません。これは、メッセージがアクター/オブジェクトの内部状態を変更するかどうかを判断できないことを意味します。状態と副作用は、設計によって隠されています


20
@CandiedOrange:メソッドの副作用が直接または間接にあるかどうか。ただし、多くの呼び出し層は概念的には何も変更しません。それはまだ副作用です。しかし、副作用が注入されたオブジェクトを通じてのみ発生するという点が重要な場合、どのような副作用が発生する可能性があるかを制御する場合、これは良いデザインのように聞こえます。副作用がないわけではありません。それは良いオブジェクト指向ですが、純粋に機能的ではありません。
ジャックB

12
@LightnessRacesinOrbit:クワイエットモードのgrepには、使用されるプロセス戻りコードがあります。それがなかったら、それは本当に役に立たないでしょう
unperson325680

10
@progo:戻りコードは副作用ではありません。それ効果です。副作用と呼ばれるものは、リターンコードではないため、副作用と呼ばれます;)
Monicaとの明度の

8
@Cubicがポイントです。副作用のないプログラムは役に立ちません。
イェンスシャウ

5
@mathreadlerそれは副作用です:)
アンドレスF.

32

教えてください、聞かないでくださいいくつかの基本的な仮定が付属しています:

  1. オブジェクトを使用しています。
  2. オブジェクトには状態があります。
  3. オブジェクトの状態は、その動作に影響します。

これらはどれも純粋な関数には当てはまりません。

それでは、「教えて、聞かないで」というルールがある理由を見てみましょう。このルールは警告であり、リマインダーです。次のように要約できます。

クラスが独自の状態を管理できるようにします。その状態を要求せずに、その状態に基づいてアクションを実行します。クラスにあなたが望むものを伝え、それ自身の状態に基づいて何をすべきかを決定させます。

別の言い方をすれば、クラスは自身の状態を維持し、それに基づいて行動する責任を単独で負います。これがカプセル化のすべてです。

ファウラーから:

Tell-Don't-Askは、オブジェクト指向とは、データを操作する機能をデータにバンドルすることであるということを人々が思い出すのに役立つ原則です。オブジェクトにデータを要求し、そのデータに基づいて行動するのではなく、オブジェクトに何をすべきかを伝える必要があることを思い出させます。これにより、データに合わせて動作をオブジェクトに移動することができます。

繰り返しになりますが、クラスの状態を外部に公開しない限り、これは純粋な関数とは関係がなく、不純な関数ですらありません。例:

TDA違反

var color = trafficLight.Color;
var elapsed = trafficLight.Elapsed;
If (color == Color.Red && elapsed > 2.Minutes)
    trafficLight.ChangeColor(green);

TDA違反ではない

var result = trafficLight.ChangeColor(Color.Green);

または

var result = await trafficLight.ChangeColorWhenReady(Color.Green);     

後者の例の両方で、信号機はその状態と動作の制御を保持します。


ちょっと待ってください、閉鎖は純粋なものです。状態があります(レキシカルスコープと呼ばれます)。そしてそのレキシカルスコープは彼らの行動に影響を与える可能性があります。TDAは、オブジェクトが関係している場合にのみ関連しますか?
candied_orange

7
@CandiedOrangeクロージャーは、クローズドオーバーバインディングを変更しない場合にのみ純粋です。そうしないと、クロージャから返された関数を呼び出すときに参照の透過性が失われます。
ジャレッド・スミス

1
@JaredSmithそしてオブジェクトについて話しているときは同じではありませんか?これは単に不変性の問題ではありませんか?
candied_orange

1
@bdsl-そして、このタイプのすべての議論がtrafficLight.refreshDisplayの例でどこに行くのかを正確に指摘しました。ルールに従うと、元のコーダー以外には理解できない非常に柔軟なシステムになります。数年の休止の後、オリジナルのコーダーでさえ、おそらく彼らが何をしたのか理解できないだろうと私は賭けさえしました。
ダンク

1
「他のオブジェクトを開かず、その内臓を見るのではなく、自分の内臓(もしあれば)を他のオブジェクトのメソッドにこぼしてください。これがザ・ウェイ」です。
Joker_vD

30

オブジェクトを扱うとき、その内部状態については尋ねません。何をする必要があるかを伝え、その内部状態を使用して、私がそれを行うように指示したことで何をすべきかを見つけます。

あなただけのために質問しない内部状態、それはどうかは聞かない、すべての内部状態を持っているのどちらか。

また、尋ねないでください!(メソッド内のステートメントによって提供される)戻り値の形式で結果が得られないことを意味しませreturn。それはあなたがそれをどうするか気にしないが、その処理をすること意味するだけです!。そして、時々あなたはすぐに処理結果が欲しい...


1
ただし、CQSは、状態の変更と結果の取得を分離する必要があることを意味します
jk。

7
@jk。通常どおり、状態の変更結果の返送を分離する必要がありますが、まれにそれを組み合わせる正当な理由があります。たとえば、イテレータnext()メソッドは現在のオブジェクトを返すだけでなく、次の呼び出しが次のオブジェクトを返すようにイテレータの内部状態も変更する必要があります...
Timothy Truckle

4
まさに。OPの問題は、単に「教えて、聞かないでください」という誤解/誤用によるものだと思います。そして、その誤解を修正することで、問題はなくなります。
コンラッドルドルフ

@KonradRudolph Plus、それがここでの唯一の誤解だとは思わない。「純粋な機能」の説明には、「その状態は一度だけ設定されます」が含まれます。別のコメントは、クロージャのコンテキストを意味する可能性があることを示していますが、フレージングは​​奇妙に聞こえます。
イズカタ

17

return「写真にとどまる」「有害」と考える場合、代わりに次のような関数を作成します

ResultType f(InputType inputValue)
{
     // ...
     return result;
}

メッセージを渡す方法でビルドします。

void f(InputType inputValue, Action<ResultType> g)
{
     // ...
     g(result);
}

fおよびg副作用がない限り、それらを連結すると副作用もなくなります。このスタイルは、Continuation-passingスタイルとも呼ばれるものに似ていると思います。

これが本当に「より良い」プログラムにつながる場合は、いくつかの慣習に違反するため、議論の余地があります。ドイツのソフトウェアエンジニアであるラルフウェストファールは、これを中心にプログラミングモデル全体を作成しました。

いくつかの例を見るには、このブログエントリの「イベントへの翻訳」セクションから始めてください。完全なアプローチについては、彼の電子書籍「プログラミングモデルとしてのメッセージング-意図したとおりにOOPを実行する」をお勧めします


24
If this really leads to "better" programs is debatable今世紀の最初の10年間に、JavaScriptで記述されたコードを見るだけで済みます。Jqueryとそのプラグインは、このパラダイムコールバック...あらゆる場所でのコールバックの傾向がありました。ある時点で、ネストされたコールバックが多すぎるため、デバッグが悪夢になりました。ソフトウェアエンジニアリングとその「原則」の
違いに

1
それでも、ある時点で、副作用を実行するアクションを提供する必要があるか、CPSパートから抜け出すための何らかの方法が必要です
jk。

12
@Laiv CPSはコンパイラーの最適化テクノロジーとして発明されたもので、プログラマーがこの方法でコードを手書きすることを実際に期待した人はいませんでした。
Joker_vD

3
@CandiedOrangeスローガン形式では、「リターンは単に継続することを伝えるだけです」。実際、Schemeの作成は、ヒューイットのアクターモデルを理解しようとすることに動機付けられ、アクターとクロージャーは同じものであるという結論に達しました。
デレクエルキンス

2
仮に、確かに、アプリケーション全体を何も返さない一連の関数呼び出しとして書くことができます。密結合であることを気にしないのであれば、ポートさえ必要ありません。しかし、ほとんどの正気なアプリケーションは、物事を返す関数を利用しています。そして、答えで十分に実証したと思いますがreturn、オブジェクトの状態を指揮しない限り、関数からデータを取得し、Tell Do n't Askに従うことができます。
ロバートハーヴェイ

7

メッセージの受け渡しは本質的に効果的です。あなたがいる場合伝えるためにオブジェクトを行う何かを、あなたはそれが何かに影響を与えることを期待しています。メッセージハンドラが純粋な場合、メッセージを送信する必要はありません。

分散アクターシステムでは、通常、操作の結果が元の要求の送信者にメッセージとして送信されます。メッセージの送信者は、アクターランタイムによって暗黙的に利用可能になるか、(慣例により)メッセージの一部として明示的に渡されます。同期メッセージパッシングでは、単一の応答はreturnステートメントに似ています。非同期メッセージパッシングでは、応答メッセージを使用すると、結果を配信しながら複数のアクターでの同時処理が可能になるため、特に便利です。

結果を明示的に配信する「送信者」を渡すと、基本的に継続の受け渡しスタイルまたは恐ろしいパラメーターがモデル化されます。ただし、メッセージを直接変更するのではなく、メッセージを渡します。


5

この質問全体が、「レベル違反」だと思います。

主要なプロジェクトには(少なくとも)次のレベルがあります。

  • システムレベル(eコマースプラットフォームなど)
  • サブシステムレベル(例:ユーザー検証:サーバー、AD、フロントエンド)
  • 上記のコンポーネントの1つなど、個々のプログラムレベル
  • アクター/モジュールレベル[これは言語によって不明瞭になります]
  • メソッド/機能レベル。

個々のトークンまで続きます。

メソッド/関数レベルのエンティティが返されないようにする必要はありません(単に返されるだけでもthis)。(説明では)Actorレベルのエンティティが何かを返す必要はありません(言語によっては不可能な場合もあります)。混乱はこれらの2つのレベルを混同することにあると思うので、(特定のオブジェクトが実際に複数のレベルにまたがっていても)明確に推論する必要があると主張します。


2

あなたは、OOPの「伝える、聞かない」という原則と純粋な関数の機能的原則の両方に準拠したいと述べていますが、それがどのようにしてreturnステートメントを避けるようになったのかはわかりません。

これらの両方の原則に従う比較的一般的な代替方法は、returnステートメントにオールインし、ゲッターのみで不変オブジェクトを使用することです。その場合のアプローチは、元のオブジェクトの状態を変更するのではなく、いくつかのゲッターが新しい状態で同様のオブジェクトを返すようにすることです。

このアプローチの1つの例は、Pythonの組み込みtupleおよびfrozensetデータ型です。以下は、frozensetの典型的な使用法です。

small_digits = frozenset([0, 1, 2, 3, 4])
big_digits = frozenset([5, 6, 7, 8, 9])
all_digits = small_digits.union(big_digits)

print("small:", small_digits)
print("big:", big_digits)
print("all:", all_digits)

これは、unionメソッドが古いオブジェクトに影響を与えずに独自の状態で新しいfrozensetを作成することを示す、次を出力します。

小:frozenset({0、1、2、3、4})

big:frozenset({5、6、7、8、9})

all:frozenset({0、1、2、3、4、5、6、7、8、9})

同様の不変のデータ構造の別の広範な例は、FacebookのImmutable.jsライブラリです。どちらの場合も、これらのビルディングブロックから開始し、同じ原則に従う高レベルのドメインオブジェクトを構築し、機能的なOOPアプローチを実現して、データとその理由をより簡単にカプセル化できます。また、不変性により、ロックを心配することなくスレッド間でそのようなオブジェクトを共有できるという利点も得られます。


1

よく書かれたコードは、オブジェクト指向の原則と機能の原則を同時に満たすことができると疑うようになりました。私はこれらのアイデアを調整しようとしていますが、私が着陸した大きなこだわりのポイントはリターンです。

けれども私は、より具体的には、不可欠と関数型プログラミング(当然全くすべての利点を得ることが、両方のライオンのシェアを取得しようとしていない)の利点のいくつかを調整するために全力をしようとしてきたreturn中ということに、実際の基本であります多くの場合、私にとっては簡単な方法です。

returnステートメントを完全に回避しようとすることに関して、私は過去1時間ほどこれを検討しようとしましたが、基本的には何度もスタックオーバーフローが発生しました。カプセル化と情報隠蔽の最強レベルを強制することで、何をすべきかを単に伝えられるだけの非常に自律的なオブジェクトを支持するという点でそれの魅力を見ることができます。それらがどのように機能するかの理解。

信号機の例を使用する場合、すぐに素朴な試みで、そのような信号機を取り巻く世界全体の知識を与えたいと思うでしょう。したがって、正しく理解できれば、それを抽象化し、パイプラインを介してデータではなくメッセージとリクエストをさらに伝播するI / Oポートの概念を一般化し、基本的にこれらのオブジェクトに目的の相互作用/リクエストを注入することを切り離して分離します互いに気づかないうちに。

ノーダルパイプライン

ここに画像の説明を入力してください

そして、この図は、私がこれをスケッチしようとする限りです(そして、単純ですが、それを変更し、再考し続けなければなりませんでした)。すぐに、このレベルのデカップリングと抽象化を備えたデザインは、コード形式で推論するのが非常に難しくなると思う傾向があります。なぜなら、これらすべてを複雑な世界に結びつけるオーケストレーターは、目的のパイプラインを作成するために、これらすべての対話と要求を追跡します。ただし、視覚的な形式では、これらのことをグラフとして引き出してすべてをリンクし、物事がインタラクティブに発生するのを見るのは合理的です。

副作用に関しては、これらのリクエストがコールスタック上で、各スレッドが実行するコマンドのチェーンにつながるという意味で、「副作用」がないことがわかりました。そのようなコマンドが実際に実行されるまで外部の世界に関連する状態を変更しないため、実用的な意味での「副作用」として-ほとんどのソフトウェアでの私にとっての実際的な目標は、副作用を排除せず、それらを延期し、集中化することです) 。さらに、既存の世界を変更するのではなく、コマンドを実行すると新しい世界が出力される場合があります。私の脳は、これらすべてのアイデアをプロトタイプ化しようとする試みがない限り、これらすべてを理解しようとするだけで本当に負担がかかります。私もしなかった

使い方

明確にするために、私はあなたが実際にこれをどのようにプログラムするかを想像していました。実際に動作するのを見る方法は、実際にユーザーエンド(プログラマー)のワークフローをキャプチャする上記の図です。信号機をワールドにドラッグし、タイマーをドラッグして、経過時間を「構築」してください。タイマーにはOn Intervalイベント(出力ポート)があり、それを信号機に接続して、そのようなイベントでライトに色を循環させることができます。

信号機は、特定の色に切り替えると、などの出力(イベント)を発します。On Redこの時点で、歩行者を世界にドラッグし、そのイベントから歩行者に歩行を開始するよう指示するか、または鳥をドラッグします。光が赤になったら鳥に飛んで羽ばたきを始めるように指示します...または光が赤になったら爆弾に爆発するよう指示します-私たちが望むものとオブジェクト互いに完全に気づかず、この抽象的な入出力の概念を通じて何をすべきかを間接的に互いに伝えるだけです。

そして、彼らは状態を完全にカプセル化し、それについて何も明らかにしません(これらの「イベント」がTMIと見なされない限り、その時点で私は多くのことを再考しなければなりません)そして、それらは非常に切り離されています。この一般化された入出力ポートの抽象化を除いて、何も知りません。

実用的なユースケース?

特定のドメインの高レベルのドメイン固有の埋め込み言語として、この種のものが有用であり、周囲の世界について何も知らず、内部状態のポスト構築を公開せず、基本的にリクエストを伝播するこれらすべての自律オブジェクトを調整できることがわかりました互いに変更し、心のコンテンツを微調整できます。現時点では、これは非常にドメイン固有であると感じています。または、十分に考えていないかもしれません。なぜなら、私が定期的に開発する種類の物で脳を包むのは非常に難しいからですむしろ低中位レベルのコード)Tell、Do n't Ask to such extremesを解釈し、想像できる限り最強のカプセル化を望む場合。ただし、特定のドメインで高レベルの抽象化を使用している場合、

信号とスロット

この設計は、実装方法のニュアンスをあまり考慮しなければ、基本的に信号とスロットであることに気付くまで、奇妙に馴染みがありました。私にとっての主な質問は、これらの個々のノード(オブジェクト)を、どの程度効果的にプログラミングreturnすることができるかということです。並行、例えばロックなし)。魔法の利点は、これらのものを潜在的に結び付ける方法ではなく、突然変異のないこの程度のカプセル化に実装する方法にあります。これらはどちらも実現可能だと思われますが、どれほど広く適用できるかはわかりません。そこで、潜在的なユースケースを処理しようとして少し困惑しています。


0

私はここで確実性の漏れをはっきりと見ます。「副作用」はよく知られており、一般に理解されている用語のようですが、実際にはそうではありません。定義に応じて(実際にはOPにありません)、副作用が完全に必要な場合(@JacquesBが説明できたため)または容赦なく受け入れられない場合があります。または、明確化に向けた一歩を作り、区別する必要が希望 1が非表示にするには好きではない副作用と:(これは何もなく、明示的にする方法ません。このポイントで有名なHaskellのIOが出現)望ましくない Aとして副作用がコードバグなどの結果。これらはかなり異なる問題であるため、異なる推論が必要です。

したがって、「副作用をどのように定義し、与えられた定義は「return」ステートメントとの相互関係について何と言っているのか」という言い回しから始めることをお勧めします。


1
「この時点で有名なHaskellのIOが出現します:それは明示的な方法にすぎません」-明示性は確かにHaskellのモナドIOの利点ですが、別のポイントがあります:副作用を完全に隔離する手段を提供します言語の外側-一般的な実装は実際にはこれを行いません、主に効率の問題のために、概念的には真実です:IOモナドは、完全に外部の環境に命令を返す方法であると考えることができますHaskellプログラムと、完了後に続行する関数への参照。
ジュール
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.