型推論が役立つのはなぜですか?


37

私はコードを書くよりも頻繁にコードを読みますが、産業用ソフトウェアに取り組んでいるプログラマーのほとんどがこれを行うと仮定しています。型推論の利点は、冗長性が少なく、コードが少ないことです。しかし一方で、コードをより頻繁に読む場合は、おそらく読み取り可能なコードが必要になります。

コンパイラーは型を推測します。これには古いアルゴリズムがあります。しかし、本当の質問は、プログラマーがコードを読むときに変数のタイプを推測したいのはなぜですか?誰がタイプを読むだけで、どのタイプがあるかを考えるよりも速くないですか?

編集:結論として、なぜそれが役立つのかを理解しています。しかし、言語機能のカテゴリでは、演算子がオーバーロードしているバケツの中にあります-場合によっては便利ですが、乱用された場合は読みやすさに影響します。


5
とき私の経験では種類が非常に多くの重要な書き込み、それを読むより、コードを。コードを読むとき、私はアルゴリズムと特定のブロックを探しています。これは通常、適切な名前の変数から指示されます。ひどく不完全に書かれていない限り、それが何をしているのかを読んで理解するために、チェックコードをタイプする必要はありません。ただし、私が探していない余分な不必要な詳細(膨大な型注釈など)で肥大化したコードを読むと、探しているビットを見つけることが難しくなります。型の推論は、コードを書くよりもはるかに多くを読むための大きな恩恵だと思います。
ジミーホファ14

探しているコードの一部を見つけたら、タイプチェックを開始するかもしれませんが、いつでも10行以上のコードに集中するべきではありません。そもそもブロック全体を精神的に選んでいるので、自分で推論してください。とにかくそうするためにツールを使用する可能性があります。ゾーンインしようとしている10行のコードのタイプを把握するのに時間がかかることはめったにありませんが、これは読み取りから書き込みに切り替えた部分であり、あまり一般的ではありません。
ジミーホファ14

プログラマがコードを書くよりもコードを読む頻度が高い場合でも、コードの一部が書くよりも頻繁に読み取られることを意味するものではないことを覚えておいてください。多くのコードは短命かもしれませんし、そうでなければ二度と読まないかもしれませんし、どのコードが生き残り、最大限の読みやすさで書かれるべきかを見分けるのは簡単ではないかもしれません。
JPA

2
@JimmyHoffaの最初のポイントについて詳しく説明し、一般的な読書を検討してください。個々の単語の品詞に焦点を合わせた場合、文は理解しやすいだけでなく、構文解析と読み取りが容易ですか?「(記事)牛(名詞-単数形)は(記事)月(名詞)。(句読点)を(前置詞)飛び越えました(動詞-過去)。
ゼフスピッツ

回答:


46

Javaを見てみましょう。Javaは、推論された型を持つ変数を持つことはできません。これは、たとえタイプが何であるかが人間の読者に完全に明らかであるとしても、頻繁にタイプを説明する必要があることを意味します。

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

そして時々、タイプ全体を綴るのは単純に迷惑なだけです。

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

この冗長な静的型付けは、プログラマーである私を邪魔します。ほとんどのタイプアノテーションは、既に知られている内容の繰り返しのある、行を埋める、コンテンツのない変更です。しかし、バグを発見するのに本当に役立つので、静的型付けが好きです。したがって、動的型付けを使用することは必ずしも良い答えではありません。型推論は両方の長所です。無関係な型は省略できますが、プログラム(型)がチェックアウトされることを確認してください。

型推論はローカル変数には本当に便利ですが、明確に文書化する必要があるパブリックAPIには使用しないでください。また、コードで何が起こっているのかを理解するために、型が本当に重要な場合もあります。そのような場合、型推論だけに依存するのは愚かなことです。

型推論をサポートする多くの言語があります。例えば:

  • C ++。autoキーワードトリガーは推論を入力します。それがなければ、ラムダのタイプやコンテナ内のエントリのタイプを書くのは地獄だろう。

  • C#。を使用して変数を宣言できますvar。これにより、限定された形式の型推論がトリガーされます。型推論を必要とするほとんどの場合を引き続き管理します。特定の場所では、型を完全に除外できます(ラムダなど)。

  • Haskell、およびMLファミリーの任意の言語。ここで使用される型推論の特定のフレーバーは非常に強力ですが、多くの場合、関数の型注釈が表示されることが多く、2つの理由があります。1つ目はドキュメントで、2つ目は型推論が実際に期待する型を見つけたことを確認することです。矛盾がある場合は、何らかのバグがある可能性があります。


13
また、C#には匿名型、つまり名前のない型がありますが、C#には名義型システム、つまり名前に基づく型システムがあることに注意してください。型推論がなければ、それらの型は決して使用できません!
ヨルグWミットタグ14

10
私の意見では、いくつかの例は少し不自然です。42への初期化は、変数がであることを自動的に意味しませんint、それは偶数を含むどんな数値タイプであるかもしれませんchar。またEntry、クラス名を入力するだけでIDEに必要なインポートを行わせることができる場合に、タイプ全体を綴りたい理由がわかりません。名前全体を綴る必要があるのは、自分のパッケージに同じ名前のクラスがある場合だけです。しかし、とにかく悪いデザインが好きなようです。
マルコム14

10
@Malcolmはい、私の例はすべて不自然です。ポイントを説明するのに役立ちます。このint例を書いたとき、型推論を特徴とするほとんどの言語の(私の意見ではかなり正気な振る舞い)について考えていました。彼らは通常、intまたはIntegerその言語で呼ばれているものを推測します。型推論の利点は、常にオプションであるということです。必要に応じて別のタイプを指定できます。Entry例:良い点については、に置き換えますMap.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>。Javaには型エイリアスさえありません:(
amon

4
@ m3th0dmanタイプが理解にとって重要である場合、明示的に言及することができます。型推論は常にオプションです。しかし、ここでは、のタイプcolKeyは明らかであり、無関係です:の2番目の引数として適切であることにのみ注意しますdoSomethingWith。そのループを-トリプルの(key1, key2, value)Iterable を生成する関数に抽出する場合、最も一般的な署名はになります<K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table)。その関数内では、colKeyIntegerではなくK2)の実際の型は完全に無関係です。
アモン14

4
@ m3th0dmanそれは、「大部分」のコードがこれまたはそれであるということについての、非常に広範な抜本的なステートメントです。逸話的な統計。初期化子で型を2回記述することは確かに意味がありませんView.OnClickListener listener = new View.OnClickListener()。プログラマーが「怠け者」で、それを短縮したとしてもvar listener = new View.OnClickListener(可能な場合)、そのタイプをまだ知っているでしょう。この種の冗長性は一般的です-ここでは推測のリスクはありません-そしてそれを取り除くこと将来の読者について考えることから生じます。すべての言語機能は注意して使用する必要がありますが、私はそれを疑っていません。
コンラッド・モラウスキー14

26

コードが書かれているよりもはるかに頻繁に読まれることは事実です。ただし、読み取りにも時間がかかり、2画面のコードは1画面のコードよりもナビゲートして読み取るのが難しいため、最適な有用な情報/読み取り努力の比率を詰めるために優先順位を付ける必要があります。これは、一般的なUXの原則です。一度に大量の情報があふれ、実際にインターフェイスの有効性が低下します。

そして、私の経験では、多くの場合、正確なタイプ重要ではありません。確かx + y * zmonkey.eat(bananas.get(i))、式をネストすることもあります:、、factory.makeCar().drive()。これらにはそれぞれ、タイプが書き出されていない値に評価される部分式が含まれています。しかし、それらは完全に明確です。コンテキストから把握するのは十分に簡単であるため、このタイプを明記しないでかまいませんし、それを書き出すことは、良いことよりも害をもたらすでしょう(データフローの理解を混乱させ、貴重な画面と短期のメモリスペースを取ります)。

明日がないように式をネストしない理由の1つは、行が長くなり、値の流れが不明確になることです。一時変数の導入はこれに役立ち、順序を課し、部分的な結果に名前を付けます。ただし、これらの側面から恩恵を受けるものすべてが、そのタイプを綴ることから恩恵を受けるわけではありません。

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

userエンティティオブジェクト、整数、文字列、または他の何かであるかどうかは重要ですか?ほとんどの目的では、ユーザーを表し、HTTPリクエストから取得し、名前の取得に使用して回答の右下隅に表示されることを知るだけで十分です。

そして、それ重要な場合、著者はタイプを自由に書き出すことができます。これは責任を持って使用しなければならない自由ですが、可読性を高めることができる他のすべて(変数名と関数名、フォーマット、APIデザイン、空白)にも同じことが言えます。そして実際、HaskellとML(すべてを余計な労力なしで推論できる)の慣習は、非ローカル関数関数のタイプ、および適切な場合にはローカル変数と関数のタイプを書き出すことです。すべてのタイプを推測できるのは初心者のみです。


2
+1これは受け入れられた答えです。型推論が素晴らしいアイデアである理由の核心に直結します。
クリスチャンヘイター14

user使用して何ができるかは関数によって決まるため、関数を拡張しようとする場合、の正確なタイプは重要ですuser。これは、いくつかの健全性チェックを追加する場合(たとえば、セキュリティの脆弱性が原因)、または表示するだけでなくユーザーと実際に何かを行う必要があることを忘れた場合に重要です。確かに、拡張のためのこれらの種類の読み取りは、コードを読み取るだけではありませんが、私たちの仕事の重要な部分でもあります。
cmaster

@cmasterそして、あなたは常にその型をかなり簡単に見つけることができます(ほとんどのIDEはあなたに教えてくれます、そして意図的に型エラーを引き起こし、コンパイラに実際の型を出力させるローテクソリューションがあります)一般的なケースであなたを困らせません。

4

型推論は非常に重要であり、あらゆる現代言語でサポートされるべきだと思います。私たちはすべてIDEで開発しており、推測されたタイプを知りたい場合に役立つことがありますvi。たとえば、Javaの冗長性とセレモニーコードを考えてください。

  Map<String,HashMap<String,String>> map = getMap();

しかし、あなたは私のIDEが私を助けてくれれば良いと言うことができます、それは有効なポイントかもしれません。ただし、一部の機能は、たとえばC#匿名型などの型推論の助けなしには存在しません。

 var person = new {Name="John Smith", Age = 105};

Linqは、型推論の助けなしでは今ほど良くないでしょう。Select例えば

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

この匿名型は変数にきちんと推論されます。

Scalaはあなたのポイントがここに当てはまると思うので、戻り型の型推論を嫌います。APIがより流useに使用できるように、関数が返すものが明確であるはずです


Map<String,HashMap<String,String>>?確かに、型を使用していない場合、それらをつづるのはほとんど利点がありません。Table<User, File, String>しかし、より有益であり、それを書くことには利点があります。
MikeFHay 14

4

これに対する答えは本当に簡単だと思います。冗長な情報の読み書きを節約できます。特に、等号の両側に型があるオブジェクト指向言語では。

また、情報が冗長でない場合に、使用する必要があるかどうかを示します。


3
まあ、技術的には、手動署名を省略することができる場合、情報は常に冗長です。そうしないと、コンパイラはそれらを推測できません。しかし、私はあなたが言っていることを理解します:単一のビューで複数のスポットに署名を複製するだけでは、脳にとって本当に冗長です、いくつかのうまく配置されたタイプは、おそらく非常に多くの非自明な変換。
左辺約14

@leftaroundabout:プログラマーが読んだときに冗長です。
jmoreno 14

3

コードを見たとしましょう:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

場合someBigLongGenericTypeの戻り値の型から割り当て可能でsomeFactoryMethod、型が正確に一致しない場合はどのように可能性の高いコードを読んで誰かが通知になり、そしてどのように容易に予告食い違いをした誰かが、それは意図的だったかどうかを認識することができますか?

推論を許可することにより、言語はコードを読んでいる人に、変数の型が明示的に述べられているとき、その人がその理由を見つけようとすることを提案できます。これにより、コードを読んでいる人が自分の努力に集中できるようになります。対照的に、型が指定されている時間の大部分が推測されたものとまったく同じである場合、コードを読んでいる人は微妙に異なる時間に気付かない傾向があります。


2

すでに多くのすばらしい答えがあることがわかります。そのうちのいくつかは繰り返しますが、時々あなたは自分の言葉で物事を置きたいだけです。それは私が最も精通している言語なので、C ++のいくつかの例でコメントします。

必要なことは決して賢明ではありません。他の言語機能を実用的にするには、型推論が必要です。C ++では、発話できない型を持つことができます。

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C ++ 11は、やはり発言できないラムダを追加しました。

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

型推論もテンプレートを支えています。

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

しかし、あなたの質問は、「プログラマーが、コードを読むときに変数の型を推測したいのはなぜでしょうか。誰が型を読むだけで、どの型があるかを考えるよりも速くないのですか?」

型推論は冗長性を取り除きます。コードの読み取りに関しては、コードに冗長な情報を含める方が高速で簡単な場合がありますが、冗長な情報は有用な情報を覆い隠す可能性があります。例えば:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

C ++プログラマーがiがイテレーターであることを識別するのに標準ライブラリーにあまり慣れていないi = v.begin()ため、明示的な型宣言の値は制限されます。その存在により、より重要な詳細(iベクトルの先頭を指すなど)があいまいになります。@amonによるすばらしい回答は、重要な詳細を覆い隠す冗長性のさらに良い例を提供します。対照的に、型推論を使用すると、重要な詳細がより顕著になります。

std::vector<int> v;
auto i = v.begin();

コードの読み取りは重要ですが、十分ではありませんが、ある時点で読み取りを停止し、新しいコードの書き込みを開始する必要があります。コードの冗長性により、コードの変更がより遅く、より難しくなります。たとえば、次のコードの断片があるとします。

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

ベクトルの値型を変更してコードを二重に変更する必要がある場合:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

この場合、2つの場所でコードを変更する必要があります。元のコードが次の場合の型推論とは対照的です。

std::vector<int> v;
auto i = v.begin();

そして変更されたコード:

std::vector<double> v;
auto i = v.begin();

コードを1行変更するだけでよいことに注意してください。これを大規模なプログラムに外挿すると、型の推論は、エディターを使用した場合よりもはるかに迅速に変更を型に反映できます。

コードの冗長性により、バグが発生する可能性があります。コードが同等に保たれている2つの情報に依存しているときはいつでも、間違いの可能性があります。たとえば、このステートメントの2つのタイプの間に矛盾がありますが、これはおそらく意図されていません。

int pi = 3.14159;

冗長性は、意図を見分けるのを難しくします。型の推論は、明示的な型の指定よりも簡単であるため、読みやすく、理解しやすい場合があります。コードの断片を考えてみましょう:

int y = sq(x);

sq(x)返す場合、がの戻り型であるか、を使用するステートメントに適しているため、であるintyは明らかではありません。が返されないように他のコードを変更すると、その行だけではタイプの更新が必要かどうかがわかりません。同じコードですが、型推論を使用しているのと対照的です:intsq(x)ysq(x)inty

auto y = sq(x);

この場合、意図は明確でyあり、によって返されるのと同じ型でなければなりませんsq(x)。コードがの戻りsq(x)タイプをy変更すると、変更のタイプは自動的に一致します。

C ++では、上記の例が型推論を使用してより単純になる2番目の理由があります。型推論は暗黙的な型変換を導入できません。の戻り値の型がsq(x)でないint場合、コンパイラは暗黙的に暗黙の変換をに挿入しintます。の戻り型がsq(x)を定義する型複合型であるoperator int()場合、この非表示の関数呼び出しは任意に複雑になる可能性があります。


C ++の発せられない型についてかなり良い点があります。ただし、言語を修正する理由よりも、型推論を追加する理由の方が少ないと思います。あなたが提示する最初のケースでは、プログラマーは型推論の使用を避けるためにその名前を付ける必要があるだけなので、これは強力な例ではありません。2番目の例は、C ++がラムダ型が発声可能であることを明示的に禁止しているため、強力typeofです。そして、それは私の意見で修正されるべき言語自体の赤字です。
cmaster
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.