わかりました、それでタイトルは少しクリックベイトですが、真剣に私は 話をしてきました、しばらくは尋ねないでください キック。私はそれがメソッドを messages として真のオブジェクト指向の方法で使用することを奨励する方法が好きです。しかし、これは私の頭の中ではがらがらしている、しつこい問題を抱えています。
よく書かれたコードがOOの原則と機能の原則を同時に満たすことができるのではないかと疑いました。これらのアイデアと私が着手した大きな固執点を調和させようとしていますはreturn
です。
純粋関数には2つの性質があります。
同じ入力で繰り返し呼び出すと、常に同じ結果が得られます。これは不変であることを意味します。その状態は一度だけ設定されます。
副作用はありません。呼び出しによって引き起こされる唯一の変更は、結果を生成することです。
では、結果の伝達手段としてreturn
を使用することを誓約した場合、どのようにして純粋に機能するようになりますか?
tell、do n't ask のアイデアは、副作用と見なされるものを使用して機能します。オブジェクトを処理するとき、その内部状態については尋ねません。私は何をする必要があるかを伝え、その内部状態を使用して、何をすべきかを何をすべきかを理解します。いったん言ったら、何をしたかは問わない。私はそれが何をするように言われたことについて何かをしたと期待しています。
私は、カプセル化の単なる別の名前以上のものとして、Tell、Do n't Askを考えています。 return
を使用するとき、何と呼ばれているのかわかりません。そのプロトコルを話すことはできません。自分のプロトコルを処理するように強制する必要があります。多くの場合、これは内部状態として表現されます。公開されているものが正確に状態でなくても、通常は状態と入力引数に対して実行されるいくつかの計算です。応答するインターフェースがあると、結果を内部状態や計算よりも意味のあるものにマッサージする機会が与えられます。つまり、 メッセージパッシング です。 この例を参照してください 。
昔、ディスクドライブに実際にディスクがあり、ホイールが冷たすぎて指で触れることができないときにサムドライブを車で操作していたとき、迷惑な人々がパラメータのない機能をどのように考えるかを教えられました。 void swap(int *first, int *second)
はとても便利に思えましたが、結果を返す関数を書くように勧められました。だから私はこれを信仰に心から持ち、それに従い始めました。
しかし今、私は人々が アーキテクチャ を構築し、オブジェクトがどのように構築されたかを制御して、結果をどこに送信するかを制御しているのを見ます。 ここに実装例があります 。出力ポートオブジェクトの挿入は、outパラメータのアイデアに少し似ています。しかし、それは、オブジェクトが他のオブジェクトに何をしたかを伝えるための「教えてはいけない」オブジェクトです。
副作用について初めて知ったとき、出力パラメーターのように考えました。一部の作業を驚くべき方法で、つまりreturn result
規約に従わないことで、人々を驚かせないように言われていました。確かに、私は副作用が積み重なっている並列非同期スレッドの問題の山があることを知っていますが、戻り値は、スタックにプッシュされた結果をそのままにして、呼び出されたものを後でポップすることができる規則にすぎません。それだけです。
私が実際に尋ねようとしていること:
return
は、その副作用の悲惨さをすべて回避し、ロックなしでスレッドの安全性を確保するための唯一の方法です。あるいは、私は 話してもいいですか、尋ねないでください 純粋に機能的な方法で?
関数に副作用がなく、何も返されない場合、その関数は役に立ちません。それはそれと同じくらい簡単です。
しかし、ルールのletterに従い、根本的な理由付けを無視したい場合は、いくつかのチートを使用できると思います。たとえば、outパラメータを使用することは、厳密にはreturnを使用しないことです。しかし、それでも正確にリターンと同じですが、より複雑な方法で行われます。したがって、戻り値がreasonの場合に悪いと思われる場合、同じ根本的な理由から、outパラメータの使用は明らかに悪いです。
より複雑なチートを使用できます。例えば。 Haskellは、IOモナドトリックで有名ですが、実際には副作用がありますが、厳密には理論的な観点からの副作用はありません。継続渡しスタイルは、もう1つのトリックです。コードをスパゲッティに変える代償を払ってリターンを回避します。
肝心なのは、ばかげたトリックがなければ、副作用のない関数と「リターンなし」という2つの原則に互換性がないということです。さらに、そもそもどちらも本当に悪い原則(本当にドグマ)であることを指摘しておきますが、それは別の議論です。
「教えて、聞かないで」または「副作用なし」のような規則は、普遍的に適用することはできません。常にコンテキストを考慮する必要があります。副作用のないプログラムは文字通り役に立たない。純粋な関数型言語でさえそれを認めています。むしろ、彼らはseparate副作用のあるコードからの純粋なコード部分に努めています。国家またはIOモナドのポイントは、副作用を回避できないことではありません-不可能だからですが-副作用の存在は明示的に示されています関数シグネチャによって。
Tell-dont-askルールは、異なる種類のアーキテクチャ(プログラム内のオブジェクトが相互に通信する独立した「アクター」であるスタイル)に適用されます。各アクターは基本的に自律的でカプセル化されています。メッセージを送信して、それに反応する方法を決定することができますが、外部からアクターの内部状態を調べることはできません。これは、メッセージがアクター/オブジェクトの内部状態を変更するかどうかを判断できないことを意味します。状態と副作用は非表示設計による。
Tell、Do n't Askにはいくつかの基本的な前提条件があります:
これらのことは純粋な関数には当てはまりません。
それでは、「Tell、Do n't Ask」というルールがある理由を確認しましょう。このルールは警告と注意です。次のように要約できます。
クラスが独自の状態を管理できるようにします。状態を要求しないで、その状態に基づいてアクションを実行してください。クラスに必要なことを伝え、クラスの状態に基づいてクラスが何をすべきかを決定させます。
別の言い方をすれば、クラスは、自分の状態を維持し、それに作用する責任があります。これがカプセル化のすべてです。
Fowler から:
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);
後者のどちらの例でも、信号機はその状態と動作の制御を保持しています。
オブジェクトを処理するとき、その内部状態については尋ねません。私は何をする必要があるかを伝え、その内部状態を使用して、何をすべきかを何をすべきかを理解します。
内部状態を要求するだけでなく、が内部状態を持っているかどうかも確認しませんallのいずれか。
また、教えて、聞かないでください!はnotを返します(戻り値(メソッド内のreturn
ステートメント)。 私はあなたがそれをどのように行うかは気にしませんが、その処理を行います!そして、時にはすぐに処理結果が欲しい...
return
を「有害」(写真にとどまる)と見なす場合、次のような関数を作成する代わりに
ResultType f(InputType inputValue)
{
// ...
return result;
}
メッセージパッシング方式でビルドします。
void f(InputType inputValue, Action<ResultType> g)
{
// ...
g(result);
}
f
とg
に副作用がない限り、これらをチェーンしても副作用はありません。このスタイルは、いわゆる 継続渡しスタイル と似ていると思います。
これが実際に「より良い」プログラムにつながるかどうかは、いくつかの慣習に違反するため、議論の余地があります。ドイツのソフトウェアエンジニアであるラルフウェストファルは、これに関連するプログラミングモデル全体を作成しました。彼は、「フローデザイン」と呼ばれるモデリング手法を用いて、これを「イベントベースのコンポーネント」と呼びました。
いくつかの例を見るには、 このブログエントリ の「イベントへの変換」セクションから始めてください。完全なアプローチについては、彼の電子書籍 "プログラミングモデルとしてのメッセージング-OOPを意味するかのように行うこと" をお勧めします。
メッセージパッシングは本質的に効果的です。オブジェクトをtellにdoするように伝えると、何かへの影響。メッセージハンドラーが純粋である場合は、メッセージを送信する必要はありません。
分散アクターシステムでは、操作の結果は通常、元のリクエストのsenderにメッセージとして返されます。メッセージの送信者は、アクターランタイムによって暗黙的に利用可能になるか、または(慣例により)メッセージの一部として明示的に渡されます。同期メッセージパッシングでは、単一の応答はreturn
ステートメントに似ています。非同期メッセージパッシングでは、応答メッセージを使用すると、結果を配信しながら複数のアクターでの同時処理が可能になるため、特に便利です。
結果が明示的に配信される「送信者」を渡すことは、基本的には 継続渡しスタイル または恐ろしいパラメータをモデル化します。ただし、メッセージを直接変更するのではなく、それらにメッセージを渡します。
この質問全体が「レベル違反」だと私は思います。
主要なプロジェクトには、(少なくとも)次のレベルがあります。
個々のトークンについても同様です。
メソッド/関数レベルのエンティティが(this
を返すだけであっても)返さないようにする必要は実際にはありません。 (説明では)Actorレベルのエンティティが何かを返す必要はありません(言語によっては不可能かもしれませんが)。混乱はこれらの2つのレベルを混同することにあると私は思います、そして私はそれらが明確に推論されるべきだと主張します(たとえ与えられたオブジェクトが実際に複数のレベルにわたる場合でも).
OOP「教えてください」の原則と純粋な関数の機能原則の両方に準拠したいとおっしゃっていますが、それがどのようにしてあなたを導いたのかはよくわかりませんreturnステートメントを避けます。
これらの両方の原則に従う比較的一般的な代替方法は、returnステートメントにオールインし、ゲッターのみで不変オブジェクトを使用することです。次に、元のオブジェクトの状態を変更するのではなく、一部のゲッターに新しい状態の同様のオブジェクトを返すようにするアプローチがあります。
このアプローチの1つの例は、Python builtin 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})
すべて:frozenset({0、1、2、3、4、5、6、7、8、9})
同様の不変データ構造の別の広範な例は、Facebookの Immutable.js ライブラリです。どちらの場合も、これらのビルディングブロックから始めて、同じ原理に従う高レベルのドメインオブジェクトを構築し、機能的なOOP=アプローチを実現します。これにより、データとその理由をより簡単にカプセル化できます。また、不変性により、ロックを気にすることなく、スレッド間でそのようなオブジェクトを共有できるというメリットを享受できます。
よく書かれたコードがOOの原則と機能の原則を同時に満たすことができるのではないかと疑いました。これらのアイデアと私が着手した大きな固執点を調和させようとしています返却です。
私は最善を尽くして、より具体的には、命令型プログラミングと関数型プログラミングのメリットの一部を調整しています(当然、すべてのメリットが得られるわけではありませんが、両方のシェアを獲得しようとしています)。ただし、return
は多くの場合、私にとってそれを簡単な方法で行うための実際の基礎です。
return
ステートメントを完全に回避しようとすることに関して、私はこれを過去1時間かそこらにつぶそうとし、基本的にスタックが何度も頭をオーバーフローさせました。何をすべきかを単に指示されるだけの非常に自律的なオブジェクトを支持して、最高レベルのカプセル化と情報の隠蔽を強制するという点で、その魅力を理解できます。さらに、より良いものを得ようとするだけなら、アイデアの先端を探るのが好きです。それらがどのように機能するかの理解。
信号機の例を使用する場合、すぐに素朴な試みは、信号機を取り巻く世界全体のそのような信号機の知識を提供することを望み、それは結合の観点からは確かに望ましくありません。したがって、私が正しく理解している場合は、それを抽象化して分離し、データではなくメッセージと要求をパイプライン経由でさらに伝播するI/Oポートの概念を一般化して分離し、基本的にこれらのオブジェクトに相互間の望ましい相互作用/要求を注入しますお互いに気づかないうちに。
ノードパイプライン
そして、その図は、私がこれをスケッチしようとした限りです(そして、単純ですが、私はそれを変更し、再考し続けなければなりませんでした)。すぐに、このレベルのデカップリングと抽象化を備えたデザインは、コード形式でその方法を推論するのが非常に難しくなると思う傾向があります。なぜなら、複雑な世界にこれらすべてを結びつけるオーケストレーターは、必要なパイプラインを作成するために、これらすべての相互作用と要求を追跡します。ただし、視覚的な形式では、これらのものをグラフとして描画し、すべてをリンクして、対話的に起こっていることを確認することは、かなり簡単かもしれません。
副作用に関しては、これらの要求がコールスタックで、各スレッドが実行するコマンドのチェーンにつながる可能性があるという意味で、これには「副作用」がないことがわかります。 (このようなコマンドが実際に実行されるまで外界に関連する状態を変更しないので、私はこれを実用的な意味での「副作用」として数えません-ほとんどのソフトウェアにおける私にとっての実際的な目標は、副作用を排除することではありません影響はありますが、延期して集中化します)。さらに、コマンド実行は、既存の世界を変更するのではなく、新しい世界を出力する可能性があります。私の脳は、これらすべてを理解しようとするだけで本当に負担がかかりますが、これらのアイデアをプロトタイピングする試みはありません。また、これらのすべての要求を、統一されたシグネチャ/インターフェイスを持つヌル関数と考えることから始めて、臆病なアプローチを試みることを優先して、要求とともにパラメーターを渡す方法に取り組みませんでした。
仕組み
明確にするために、私はあなたがこれを実際にどのようにプログラムするかを想像していました。私がそれが機能しているのを見る方法は、実際にはユーザーエンド(プログラマー)のワークフローをキャプチャした上の図でした。信号機を世界にドラッグし、タイマーをドラッグして、経過時間を与えることができます(「構築」すると)。タイマーにはOn Interval
イベント(出力ポート)、それを信号機に接続して、そのようなイベントでライトがその色を循環するように指示することができます。
信号機は、特定の色に切り替わると、On Red
、その時点で、歩行者を私たちの世界にドラッグして、そのイベントに歩行者に歩行を開始するように指示することができます...または、鳥をシーンにドラッグして、ライトが赤に変わったら鳥に開始するように指示することができます彼らの翼を飛ばして羽ばたきます...あるいは光が赤くなったとき、私たちは爆弾に爆発するように伝えます-私たちが望むものは何でも、そしてオブジェクトは互いに完全に気づかず、間接的にお互いに何をすべきかを伝えるだけですこの抽象的な入出力の概念を通じて。
そして、彼らは自分たちの状態を完全にカプセル化し、それについて何も明らかにしません(これらの「イベント」がTMIと見なされない限り、その時点で私は多くのことを再考する必要があります)、彼らは互いに間接的に行うように指示し、尋ねません。そして、それらは超分離されています。この一般化された入出力ポートの抽象化以外は何も知りません。
実用的なユースケース?
このタイプのものは、特定のドメインで高レベルのドメイン固有の埋め込み言語として有用であり、周囲の世界について何も知らないこれらすべての自律型オブジェクトを調整し、内部状態の構築後に何も公開せず、基本的にリクエストを伝達するだけであることがわかりましたお互いに変更できるので、心の中で調整してください。現時点では、これは非常にドメイン固有であると感じています。あるいは、私が定期的に開発している種類のことで頭を抱え込むことは非常に難しいので、十分に考えていなかったのかもしれません(私はむしろ中低レベルのコード)。Tellを解釈する場合は、そのような端部に尋ねないでください。そして、想像できる最高レベルのカプセル化が必要です。しかし、特定のドメインで高レベルの抽象化を扱っている場合、これは、それをプログラムし、状態で混乱しないかなり均一な方法で相互にどのように相互作用するかを表現する非常に便利な方法かもしれません。または、そのオブジェクトの計算/出力。類推的な呼び出し元でさえ、呼び出し先について、もしあれば、多くのことを知る必要がない種類の超デカップリング、またはその逆。
信号とスロット
この設計は、実装方法のニュアンスをあまり考慮しない場合、基本的にはシグナルとスロットであることに気付くまで、奇妙に馴染みがありました。私への主な質問は、return
ステートメントを回避する程度まで、Tell、Do n't Askに厳密に従って、グラフ内のこれらの個々のノード(オブジェクト)をどれほど効果的にプログラムできるか、そして評価できるかどうかです。変異のない上記のグラフ(並行して、たとえば、ロックなし)。魔法の利点は、これらを潜在的に一緒に配線する方法ではなく、突然変異のないカプセル化のこの程度に実装する方法にあります。これらはどちらも実現可能であるように見えますが、どれほど広く適用できるかはわかりません。そのため、潜在的なユースケースを処理しようとするのに少し困っています。
ここに確実性の漏れがはっきりと見えます。 「副作用」はよく知られており、よく理解されている用語のようですが、実際にはそうではありません。定義によっては(実際にはOPにない)、副作用が完全に必要になる(@JacquesBで説明されているように)か、容赦なく受け入れられない場合があります。または、明確化に向けて一歩を踏み出すと、望ましい非表示にしたくない副作用を区別する必要があります(この時点で、有名なHaskellのIOが現れます:明示する方法にすぎません)および不要としての副作用コードのバグとそのようなものの結果これらはかなり異なる問題であり、したがって異なる推論が必要です。
したがって、私は自分自身を言い換えることから始めることをお勧めします。「副作用をどのように定義し、それが「return」ステートメントとの相互関係について、与えられた定義は何を言うのですか?」。