他の開発者がこのフレーズを使用して、いくつかのパターンを「宣伝」したり、ベストプラクティスを開発したりすることを何度も聞いています。ほとんどの場合、このフレーズは関数型プログラミングの利点について話しているときに使用されます。
「簡単に推論できる」という表現は、説明やコードサンプルなしで、そのまま使用されています。したがって、私にとっては、次の「バズ」ワードのようになります。これは、より「経験豊富な」開発者が講演で使用する言葉です。
質問:「理由がわかりにくい」の例をいくつか挙げて、「理由がわかりやすい」の例と比較できますか?
私の考えでは、「理由付けが簡単」というフレーズは、「頭の中で実行」しやすいコードを指します。
コードの一部を見るとき、それが短く、明確に記述されていて、適切な名前と最小限の値の変化しか持たない場合、コードの機能を精神的に処理することは(比較的)簡単な作業です。
貧弱な名前、常に値を変更する変数、複雑な分岐を伴う長いコードは、通常、たとえば現在の状態を追跡するのに役立つペンと紙が必要になります。したがって、このようなコードは頭の中で簡単に処理することはできません。そのため、そのようなコードについては簡単に推論することはできません。
メカニズムまたはコードの一部は、それが何をするかを予測するためにいくつかのことを考慮する必要がある場合について簡単に推論でき、考慮すべきことは簡単に利用できます。
副作用や状態のない真の関数は、出力が入力によって完全に決定されるため、パラメーターのすぐそこにあるため、簡単に推論できます。
逆に、メソッドが呼び出されたときにオブジェクトがどのような状態にあるかを考慮する必要があるため、状態のあるオブジェクトの場合は、それを推論するのがはるかに難しくなります。つまり、オブジェクトが特定の状態。
さらに悪いのはグローバル変数です。グローバル変数を読み取るコードを推論するには、コード内のその変数を設定できる場所と理由を理解する必要があります。これらすべての場所を見つけるのは簡単ではないかもしれません。
あなたが状態を持っているだけでなく、同時にそれを変更する複数のスレッドを持っているので、共有状態を持つマルチスレッドプログラミングは、1つのスレッドによって実行されたときにコードの一部が何をするかについて推論する実行のすべてのポイントで、他のスレッド(またはいくつかのスレッド)がコードの他のほぼすべての部分を実行して、操作しているデータを目の前で変更する可能性を考慮する必要があります。理論的には、ミューテックス/モニター/クリティカルセクション/何でも呼び出すことで管理できますが、実際には、共有状態や並列処理を非常に小さく制限しない限り、単なる人間が実際にそれを確実に実行することはできません。コードのセクション。
関数型プログラミングの場合、「理由付けが簡単」の意味は、ほとんどが確定的であることです。つまり、特定の入力が常に同じ出力につながることを意味しました。プログラムに必要なことは何でもできます。そのコードに触れない限り、壊れることはありません。
一方、OOは、生成される「出力」が関連するすべてのオブジェクトの内部状態に依存するため、通常、推論するのがより困難になります。それが現れる典型的な方法は予期しないものですside効果:コードの一部を変更すると、一見無関係な部分が壊れます。
...関数型プログラミングの欠点はもちろん、実際には、IOと管理状態を管理することです。
ただし、他にも多くのことを推論するのが難しいものがあります。同時実行性が最も良い例であると@Kilianは同意します。分散システムも。
より広い議論を避け、特定の質問に取り組む:
「理由がわかりにくい」の例をいくつか挙げて、「理由がわかりやすい」の例と比較できますか?
"The Story of Mel、a Real Programmer" は、1983年にさかのぼるプログラマーの伝承の一片であり、私たちの職業では「伝説」として数えられます。
それは、プログラマーがコードを書いている話を、自己参照コードや自己変更コードを含む、可能な限り難解な手法を好むこと、そして機械的なバグの意図的な悪用を物語っています。
見かけ上の無限ループは、キャリーオーバーフローエラーを利用するような方法で実際にコーディングされていました。 「アドレスxからロード」としてデコードされた命令に1を追加すると、通常「アドレスx + 1からロード」されます。しかし、xがすでに最高のアドレスである場合、アドレスはゼロに折り返されるだけでなく、オペコードが読み取られるビットに1が運ばれ、オペコードが「ロード元」から「ジャンプ先」に変更されました。完全な命令が「最後のアドレスからのロード」から「ジャンプしてアドレス0に」に変更されたこと。
これは、「推論するのが難しい」コードの例です。
もちろん、メルは同意しません...
例と非常に一般的な例を提供できます。
次のC#コードを考えます。
// items is List<Item>
var names = new List<string>();
for (var i = 0; i < items.Count; i++)
{
var item = items[i];
var mangled = MyMangleFunction(item.Name);
if (mangled.StartsWith("foo"))
{
names.Add(mangled);
}
}
今、この代替案を検討してください。
// items is List<Item>
var names = items
.Select(item => MyMangleFunction(item.Name))
.Where(s => s.StartsWith("foo"))
.ToList();
2番目の例では、このコードが何をしているかが一目でわかります。 Select
を見ると、アイテムのリストが別のリストに変換されていることがわかります。 Where
を見ると、特定のアイテムが除外されていることがわかります。一目でnames
とは何かを理解でき、有効に活用できます。
for
ループが表示されても、実際にコードを読むまでは、何が起こっているのかわかりません。そして、時々私はそれをトレースして、私がすべての副作用を説明したことを確認する必要があります。名前が何であるか(型定義以外)を理解し、それを効果的に使用する方法を理解するためにも、少し作業をする必要があります。したがって、最初の例は2番目の例よりも推論が困難です。
結局のところ、この点について簡単に推論できるかどうかは、LINQメソッドSelect
とWhere
を理解することにもかかっています。それらがわからない場合は、2番目のコードを最初に考えるのは困難です。しかし、あなたはそれらを一度理解するための費用を支払うだけです。 for
ループを使用するたびに、また変更するたびにループを理解するためのコストを支払います。コストは支払う価値がある場合もありますが、通常は「推論しやすい」ことがはるかに重要です。
関連フレーズは(I言い換え)、
コードに「明らかなバグがない」というだけでは不十分です。代わりに、「明らかにバグがない "。
比較的「簡単に推論できる」の例は [〜#〜] raii [〜#〜] です。
別の例は deadly embrace を回避している可能性があります。ロックを保持して別のロックを取得でき、多数のロックがある場合、致命的な抱擁が発生する可能性のあるシナリオがないことを確認するのは困難です。 「(グローバル)ロックが1つしかない」、または「最初のロックを保持している間は2番目のロックを取得することはできません」などのルールを追加すると、システムの判断が比較的容易になります。
プログラミングの核心はケース分析です。アラン・ペルリスはこれをエピグラム#32で述べています:プログラマーは彼らの創意工夫と彼らのロジックによってではなく、彼らのケース分析の完全性によって測定されるべきではありません。
ケース分析が簡単であるかどうかは、状況を推測するのは簡単です。これは、考慮すべきケースが少ないか、または失敗した特殊なケースが少ないことを意味します。ケースのスペースが大きい場合がありますが、一定の規則により崩壊します。誘導などの推論手法に屈する。
たとえば、アルゴリズムの再帰バージョンは、通常、命令バージョンよりも簡単に推論できます。これは、再帰バージョンに表示されない、サポートする状態変数の変更によって生じる余分なケースに寄与しないためです。さらに、再帰の構造は、数学的帰納法による証明パターンに適合するようなものです。ループバリアントや最も弱い厳密な前提条件などの複雑さを考慮する必要はありません。
これのもう一つの側面は、ケーススペースの構造です。階層的なケースの状況(サブケースとサブサブケースがあるケースなど)と比較して、フラットな、またはほとんどフラットなケースに分割された状況について推論するのは簡単です。
推論を簡略化するシステムの特性は、直交性です。これは、サブシステムを統括するケースが、それらのサブシステムが組み合わされたときに独立したままになるという特性です。 「特別なケース」を引き起こす組み合わせはありません。 4ケースの何かを3ケースの何かと直交して組み合わせる場合、12のケースがありますが、ideally各ケースは、独立したままの2つのケースの組み合わせです。ある意味で、実際には12のケースはありません。組み合わせは、私たちが心配する必要のない単なる「緊急のケースのような現象」です。これが意味することは、他のサブシステムの他の3つを考慮せずに考えることができる4つのケースがまだあり、その逆も同様です。いくつかの組み合わせを特別に識別し、追加のロジックを付与する必要がある場合、その理由付けはより困難になります。最悪の場合、すべての組み合わせに特別な処理が行われ、元の4と3に加えて、実際には12の新しいケースがあります。
承知しました。並行性を取る:
ミューテックスによって強制されるクリティカルセクション:原則は1つしかないので理解しやすく(2つの実行スレッドが同時にクリティカルセクションに入ることができない)、非効率とデッドロックの両方が発生しやすい。
代替モデル、例えばロックフリーのプログラミングまたはアクター:潜在的にはるかにエレガントで強力ですが、「この場所にこの値を書き込む」などの(一見)基本的な概念に依存できなくなるため、地獄で理解するのは難しいです。 。
推論しやすいのはoneメソッドの側面です。ただし、使用する方法を選択するには、allの側面を組み合わせて検討する必要があります。
タスクを正式な推論に限定しましょう。ユーモラスな、発明的、または詩的な推論には異なる法則があるからです。
それでも、表現はぼんやりと定義されており、厳密に設定することはできません。しかし、それは私たちにとってそれほど薄暗いままであるべきだという意味ではありません。構造がいくつかのテストに合格し、さまざまなポイントのマークを取得していると想像してみましょう。すべての点の優れたマークは、構造があらゆる面で便利であることを意味し、したがって「理由付けが容易」です。
「簡単に推論できる」構造は、次の点で優れた評価を得るはずです。
テストは主観的ですか?はい、当然です。しかし、表現自体も主観的なものです。ある人にとって簡単なことは、別の人にとっては容易ではありません。したがって、テストはドメインごとに異なる必要があります。
関数型言語の推論が可能であるという考えは、その歴史、特に [〜#〜] ml [〜#〜] から生まれ、 論理演算機能 が推論に使用した構成に類似したプログラミング言語。ほとんどの関数型言語は命令型のものよりも正式なプログラミング計算に近いため、コードから推論システムの入力への変換はそれほど面倒ではありません。
推論システムの例として、パイ計算では、命令型言語の可変メモリの各場所を個別の並列プロセスとして表す必要がありますが、一連の関数演算は単一のプロセスです。 LFC定理証明者から40年、RAM=のGBで作業しているため、数百のプロセスがあることはそれほど問題ではありません-パイ計算を使用して、数百の行から潜在的なデッドロックを削除しました数百のプロセスを持つ表現にもかかわらず、推論は約3GBで状態空間を使い果たし、断続的なバグを修正しました。これは、70年代には不可能であったか、1990年代初頭にはスーパーコンピュータを必要としました。似たようなサイズの言語プログラムは、当時を推論するのに十分小さいものでした。
他の答えから、命令型言語について推論するのを難しくした多くの難しさはムーアの法則によって侵食されているかのように、フレーズは流行語になっています。