純粋に機能的なものと伝えるもの、尋ねないでください?


14

「関数の理想的な引数の数はゼロです」は明らかに間違っています。引数の理想的な数は、関数に副作用がないようにするために必要な数です。それよりも少ないと、機能が不必要に不必要になり、成功の穴から離れて痛みの勾配を登らざるを得なくなります。時々、「ボブおじさん」が彼のアドバイスにスポットを当てています。時々彼は見事に間違っています。彼のゼロ引数アドバイスは後者の例です

ソース:このサイトの別の質問の下での@David Arnoによるコメント

コメントには133の賛成票があります。そのため、そのメリットにもっと注意を払いたいと思います。

私が知っている限りでは、プログラミングには2つの別々の方法があります:純粋な関数型プログラミング(このコメントが奨励するもの)と伝える、尋ねない(このWebサイトでも時々推奨されています)。私の知る限り、これらの2つの原則は基本的に互換性がなく、互いに反対に近いものです。副作用しかありません」。また、純粋なファンシトンが機能的パラダイムのコアと考えられているのに、聞かないで、OOパラダイムのコアと考えられていると思ったので、私はちょっと困惑しています。

開発者はおそらくこれらのパラダイムのいずれかを選択し、それに固執すべきでしょうか?まあ、私は自分を決して従わせることができなかったことを認めなければなりません。多くの場合、値を返すのが便利だと思われ、副作用でのみ達成したいことをどのように達成できるか本当にわかりません。多くの場合、副作用があると便利に思えますが、値を返すだけで達成したいことをどのように達成できるか本当にわかりません。また、しばしば(これは恐ろしいことだと思います)両方を行うメソッドがあります。

しかし、これらの133の賛成票から、私は現在純粋な関数型プログラミングが「勝っている」と推論しています。これは正しいです?

したがって、このアンチパターンに乗ったゲームの例で私が作ろうとしているのは、純粋に機能的なパラダイムに準拠させるためにそれを実現したい場合、どうすればいいのでしょうか?!

戦闘状態にあることは私にとって理にかなっているようです。これはターンベースのゲームであるため、私は辞書に戦闘状態を保持します(マルチプレイヤー-同時に多くのプレイヤーがプレイする多くの戦闘があるかもしれません)。プレーヤーが順番を回すたびに、(a)状態を適宜変更し、(b)更新をプレーヤーに返します。これは、JSONにシリアル化され、基本的に、ボード。これは、両方の原則に同時に違反していると思われます。

OK-本当にやりたいなら、メソッドをその場で修正するのではなく、戦闘状態に戻すことができます。だが!それから、完全に新しい状態を変更するのではなく、単に戻すために、戦闘状態のすべてを不必要にコピーする必要がありますか?

おそらく、移動が攻撃である場合、HPが更新されたキャラクターを返すことができますか?問題は、それほど単純ではないことです。ゲームのルール、動きは、プレイヤーのHPの一部を削除するだけでなく、多くの場合、より多くの効果をもたらします。たとえば、キャラクター間の距離を広げたり、特殊効果を適用したりすることができます。

私はその場で状態を変更して更新を返す方がはるかに簡単に思えます...

しかし、経験豊富なエンジニアはこれにどのように取り組むでしょうか?


9
パラダイムに従うことは、失敗への確実な方法です。政策は決して知性に勝るものではありません。問題の解決策は、問題解決に対するあなたの宗教的信念ではなく、問題に依存すべきです。
ジョンドゥーマ

1
以前に言ったことについて、ここで質問したことはありません。私は光栄です。:)
デビッドアルノ

回答:


14

ほとんどのプログラミングの格言のように、「教えて、聞かないで」は簡潔さを得るために明快さを犠牲にします。計算の結果を求めることを推奨することはまったく意図されておらず、計算の入力を求めることを推奨するものではありません。「取得してから計算してから設定しないでください。ただし、計算から値を返すことはできます」というのはそれほど賢明ではありません。

以前は、ゲッターを呼び出して計算を行い、その結果を使用してセッターを呼び出すことが一般的でした。これは、計算が実際にゲッターを呼び出したクラスに属するという明確な兆候です。「聞かないでください」という言葉は、人々にそのアンチパターンに目を向けることを思い出させるために作られました。排除します。ただし、格言はその1つの状況にのみ有効に適用されます。

純粋な関数型プログラムは、そのスタイルにセッターがいないという単純な理由から、その正確なアンチパターンに決して悩まされることはありませんでした。ただし、同じ関数に異なるセマンティック抽象化レベルを混在させないというより一般的な(そして見づらい)問題は、すべてのパラダイムに当てはまります。


「教えてください」と正しく説明してくれてありがとう。
user949300

13

ボブおじさんとデビッドアルノ(引用の著者)の両方が、彼らが書いたものから私たちが収集できる重要な教訓を持っています。レッスンを学び、それがあなたとあなたのプロジェクトにとって本当に意味するものを推定する価値があると思います。

最初:ボブおじさんのレッスン

ボブおじさんは、関数/メソッドの引数が多いほど、それを使用する開発者が理解しなければならないことを指摘しています。その認知的負荷は無料ではありません。引数の順序などに一貫性がない場合、認知的負荷は増加するだけです。

それは人間であるという事実です。ボブおじさんのクリーンコードブックの重要な間違いは、「関数の引数の理想的な数はゼロです」というステートメントだと思います。ミニマリズムは、そうなるまで素晴らしいものです。Calculusの限界に到達しないように、「理想的な」コードにも到達することはありません。

アルバート・アインシュタインが言ったように、「すべてはできる限り単純であるべきですが、単純ではありません」。

2番目:デビッドアルノのレッスン

記述されたDavid Arnoの開発方法は、オブジェクト指向よりも機能的なスタイル開発です。ただし、関数型コードは、従来のオブジェクト指向プログラミングよりもはるかに優れています。どうして?ロックのため。オブジェクトの状態が変更可能なときはいつでも、競合状態またはロック競合のリスクがあります。

シミュレーションやその他のサーバー側アプリケーションで使用される高度に同時実行可能なシステムを作成したことで、機能モデルは驚くほど機能します。アプローチが行った改善を証明できます。ただし、要件とイディオムが異なる非常に異なるスタイルの開発です。

開発は一連のトレードオフです

あなたは私たちよりもあなたのアプリケーションをよく知っています。関数型プログラミングに伴うスケーラビリティは必要ないかもしれません。上記の2つの理想の間には世界があります。高スループットととんでもない並列処理を処理する必要のあるシステムを扱う私たちは、関数型プログラミングの理想に向かっていくでしょう。

つまり、データオブジェクトを使用して、メソッドに渡す必要がある一連の情報を保持できます。これは、David Arnoが取り組んでいた機能的な理想を引き続きサポートしながら、Uncle Bobが取り組んでいた認知負荷の問題に役立ちます。

必要な並列処理が制限されたデスクトップシステムと、高スループットのシミュレーションソフトウェアの両方で作業しました。彼らは非常に異なるニーズを持っています。私がよく知っているデータ隠蔽の概念に基づいて設計された、よく書かれたオブジェクト指向コードを高く評価できます。いくつかのアプリケーションで機能します。ただし、すべてのユーザーに有効なわけではありません。

だれ?この場合、デビッドはボブおじさんよりも正しいです。ただし、ここで強調したい基本的なポイントは、メソッドが意味のある引数を多く持つ必要があるということです。


平行性があります。異なる戦闘を並行して処理できます。ただし、はい:処理中の単一のバトルはロックする必要があります。
gaazkam

はい、読者(あなたのアナロジーのリーパー)が彼らの(種をまく人の)著作から収集することを意味しました。そうは言っても、私は過去に書いたいくつかのことを振り返り、何かを再学習したか、以前の自分に反対しました。私たちは皆、学習し、進化しています。それが、学習した何かをどのように、どのように適用するかを常に推論すべき理由の一番の理由です。
ベリンロリチュ

8

OK-本当にやりたいなら、メソッドをその場で修正するのではなく、戦闘状態に戻すことができます。

はい、それがアイデアです。

次に、バトル状態のすべてをコピーして、完全に新しい状態に戻すのではなく、完全に新しい状態に戻す必要がありますか?

いいえ。「戦闘状態」は不変データ構造としてモデル化できます。不変データ構造には、他の不変データ構造が構築ブロックとして含まれ、不変データ構造のいくつかの階層にネストされる場合があります。

そのため、戦闘状態には、1ターン中に変更する必要のない部分と、変更する必要のある部分があります。変更されない部分は不変であるため、コピーする必要はありません。副作用を引き起こすリスクなしに、それらの部分への参照をコピーするだけです。これは、ガベージコレクションされた言語環境で最適に機能します。

「Efficient Immutable Data Structures」のGoogle。これがどのように一般的に機能するかについての参考文献を必ず見つけるでしょう。

私は、その場で状態を変更して更新を返す方がはるかに簡単に思えます。

特定の問題については、これは実際に簡単になります。ゲームとラウンドベースのシミュレーションは、ゲームの状態がラウンドごとに大きく変化することを考えると、このカテゴリに分類される場合があります。しかし、実際に「よりシンプル」なものの認識はある程度主観的であり、人々が何に慣れているかに大きく依存します。


8

コメントの著者として、ここでそれを明確にする必要があると思います。もちろん、私のコメントが提供する簡略版以外にもあります。

私の知る限り、これらの2つの原則は基本的に互換性がなく、互いに反対に近いものです。副作用しかありません」。

正直に言うと、これは「教えて、聞かないで」という言葉の本当に奇妙な使い方だと思います。それで、数年前にマーティン・ファウラーがこのテーマについて言ったことを読んで、それは啓発的でした。私が奇妙だと思った理由は、「tell do n't ask」は私の頭の中の依存性注入と同義であり、依存性注入の最も純粋な形式は、関数が必要とするすべてをパラメータで渡すからです。

しかし、私が「聞かないで」と言う意味は、ファウラーのオブジェクト指向に焦点を当てた定義を取り入れ、よりパラダイムにとらわれないようにすることから来るようです。その過程で、私はそれが概念をその論理的結論に導くと信じています。

簡単な始まりに戻りましょう。「ロジックの塊」(手順)があり、グローバルデータがあります。プロシージャは、アクセスするためにそのデータを直接読み取ります。単純な「質問」シナリオがあります。

少し前に進みます。これでオブジェクトとメソッドができました。そのデータはもはやグローバルである必要はなく、コンストラクタを介して渡され、オブジェクト内に含まれます。そして、そのデータに作用するメソッドがあります。ファウラーが説明しているように、今、「聞かないでください」と言っています。オブジェクトにはデータが通知されます。これらのメソッドは、データのグローバルスコープを要求する必要がなくなりました。しかし、ここに問題があります。これらのメソッドはオブジェクトスコープに問い合わせる必要があるため、これはまだ私の意見では「言わないでください」ではありません。これは私が感じる「伝える、尋ねる」シナリオに近い。

ですから、現代​​へと進み、「それはオブジェクト指向です」というアプローチを捨て、関数型プログラミングからいくつかの原則を借用してください。メソッドが呼び出されると、すべてのデータがパラメーターを介して提供されます。「要点は何ですか、それは単にコードを複雑にしているだけです」と主張することができます(そして主張しました)。そして、はい、オブジェクトのスコープを介してアクセス可能なデータを介してパラメーターを渡すと、コードが複雑になります。しかし、そのデータをグローバルにアクセス可能にするのではなく、オブジェクトに保存すると、複雑さが増します。しかし、グローバル変数は単純であるため常に優れていると主張する人はほとんどいません。重要なのは、「伝えるな、尋ねない」という利点がもたらすことは、スコープを縮小するという複雑さを上回ることです。これは、オブジェクトへのスコープを制限するよりも、パラメーターを介して渡すことに適用されます。private staticパラメータを介して必要なものをすべて渡すと、そのメソッドは、必要のないものにこっそりとアクセスしないように信頼できるようになりました。さらに、メソッドを小さく保つことをお勧めします。そうしないと、パラメーターリストが手に負えなくなります。そして、それは「純粋な関数」の基準に適合するメソッドを書くことを奨励します。

だから、私は「純粋に機能的」と「教えてはいけない」とは逆に見えません。前者は、私に関する限り、後者の唯一の完全な実装です。ファウラーのアプローチは完全ではありません。「聞かないでください」。

しかし、この「tell do n't askの完全な実装」は本当に理想的であるということを覚えておくことが重要です。本当に副作用がなければ有用なことは何もしないという単純な理由で、100%の副作用がほとんどないアプリはほとんどありません。状態を変更する必要があります。アプリを使用するにはIOなどが必要です。そして、そのような場合、メソッドは副作用を引き起こさなければならないため、純粋ではありません。しかし、ここでの経験則は、これらの「不純な」メソッドを最小限に抑えることです。彼らが必要とするのは、標準としてではなく、副作用があるだけです。

戦闘状態にあることは私にとって理にかなっているようです。これはターンベースのゲームであるため、私は辞書に戦闘状態を保持します(マルチプレイヤー-同時に多くのプレイヤーがプレイする多くの戦闘があるかもしれません)。プレーヤーが順番を回すたびに、(a)状態を適宜変更し、(b)更新をプレーヤーに返します。これは、JSONにシリアル化され、基本的に、ボード。

私にとって戦闘状態を持つことは理にかなっているようです。それは必須のようです。そのようなコードの全体的な目的は、状態を変更する要求を処理し、それらの状態の変更を管理し、それらを報告することです。その状態をグローバルに処理したり、個々のプレーヤーオブジェクト内に保持したり、一連の純粋な関数に渡すことができます。どちらを選択するかは、特定のシナリオに最適なものになります。グローバル状態はコードの設計を簡素化し、高速です。これはほとんどのゲームの重要な要件です。しかし、それによりコードの保守、テスト、デバッグが難しくなります。純粋な関数のセットにより、コードの実装がより複雑になり、過剰なデータコピーのためにコードが遅くなりすぎるリスクがあります。しかし、テストと保守が最も簡単になります。「OOアプローチ」はその中間に位置します。

重要なのは、常に機能する完璧なソリューションは存在しないということです。純粋な機能の目的は、「成功の落とし穴に入る」ことです。しかし、コードに複雑さをもたらす可能性があるためにそのピットが非常に浅い場合は、その上にトリップするほど落ちないので、それはあなたにとって正しいアプローチではありません。理想を目指してください。しかし、実用的であり、その理想が今回行くのにふさわしくない場所で停止してください。

そして最後のポイントとして、繰り返しますが、純粋な機能と「伝える、聞かないで」はまったく逆ではありません。


5

これまでに言われたことには、その文を不条理なものにすることができる文脈が存在します。

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

引数としてゼロ引数アドバイスをとると、ボブおじさんは完全に間違っています。あなたがそれをすべての追加の引数がコードを読みにくくすることを意味すると考えるなら、彼は完全に正しいです。費用がかかります。関数を読みやすくするため、関数に引数を追加しません。関数に引数を追加するのは、その引数への依存関係を明確にする適切な名前を考えることができないためです。

たとえば、そのpi()ままで完全に素晴らしい機能です。どうして?私はそれがどのように計算されたとしても、あるいは計算されたとしても気にしないからです。または、eまたはsin()を使用して、返される数値に到達した場合。名前は私が知る必要があることすべてを教えてくれるのでそれでいいです。

しかし、すべての名前が私が知る必要があるすべてを教えてくれるわけではありません。一部の名前は、公開された引数と同様に、関数の動作を制御する情報を理解するために重要なことを明らかにしません。それこそが、プログラミングの機能的なスタイルを推論しやすくするものです。

完全にOOPスタイルで不変で副作用のない状態を保つことができます。Returnは、次の手順のために値をスタックに残すために使用される単純なメカニズムです。他の不変のものと値をやり取りするために出力ポートを使用して、不変のままでいることができます。これは、機能するかどうかにかかわらず、すべての言語に当てはまります。

したがって、関数型プログラミングとオブジェクト指向プログラミングが「基本的に互換性がない」と主張しないでください。関数型プログラムでオブジェクトを使用でき、オブジェクト指向プログラムで純粋な関数を使用できます。

ただし、それらを混合するにはコストがかかります。期待。両方のパラダイムのメカニズムを忠実に守っても、混乱を招く可能性があります。関数型言語を使用する利点の1つは、出力を得るために存在しなければならない副作用が予測可能な場所に置かれることです。もちろん、変更可能なオブジェクトが規律のない方法でアクセスされない限り。次に、その言語で与えられたものとしてあなたが取ったものはバラバラになります。

同様に、純粋な関数を持つオブジェクトをサポートでき、不変のオブジェクトを設計できます。問題は、関数が純粋であることやオブジェクトが不変であることを通知しないと、コードを読むのに多くの時間を費やさない限り、人々はそれらの機能から推論上の利益を得られないことです。

これは新しい問題ではありません。何年もの間、人々は「OO言語」を使用しているので、OOをやっていると考えて「OO言語」で手続き的にコーディングしてきました。自分の足元を撃てないようにするのに適した言語はほとんどありません。これらのアイデアが機能するためには、あなたの中で生きなければなりません。

どちらも優れた機能を提供します。両方を行うことができます。あなたがそれらを混ぜるのに十分勇敢なら、それらに明確にラベルを付けてください。


0

私は時々、さまざまなパラダイムのすべてのルールを理解するのに苦労しています。彼らはこの状況にあるので、時には互いに対立しています。

OOPは、危険なことが起こる世界でハサミを使って走るという必須のパラダイムです。

FPは、純粋な計算で絶対的な安全性を見つける機能的なパラダイムです。ここでは何も起こりません。

ただし、すべてのプログラムが有用であるためには、命令型の世界に橋を架ける必要があります。したがって、機能コア、命令型シェル

不変オブジェクト(コマンドが実際に変化するのではなく、変更されたコピーを返すオブジェクト)の定義を開始すると、混乱が生じます。「これはOOPです」と「オブジェクトの動作を定義しています」と自分に言います。あなたは、実証済みでテスト済みのTell、Do n't Ask原則に思い返します。問題は、それを間違った領域に適用していることです。

領域は完全に異なり、異なる規則に従います。機能的領域は、副作用を世界に放出したいところまで構築されます。これらの効果を解放するには、命令型オブジェクトにカプセル化されていたすべてのデータ(これがそのように記述されていた!)が命令型シェルの役に立つ必要があります。カプセル化によって別の世界に隠されていたこのデータへのアクセスなしでは、仕事をすることはできません。計算上不可能です。

したがって、不変オブジェクト(Clojureが永続データ構造と呼ぶもの)を書いているときは、機能ドメインにいることを思い出してください。教えて、窓の外に出ないで、命令の領域に再び入ったときだけ家に戻してください。

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