web-dev-qa-db-ja.com

関数パラメーターに注釈を付けないのはなぜですか?

この質問に答えられるようにするために、プログラマーの心のあいまいさのコストは、いくつかの追加のキーストロークよりもはるかに高価であると仮定しましょう。

それで、なぜチームメイトがnotに関数のパラメータに注釈を付けることを許可するのですか?次のコードを、はるかに複雑なコードの例として取り上げます。

let foo x y = x + y

ここで、ツールチップを簡単に調べると、xとyをintにするつもりであるとF#が判断したことがわかります。それがあなたが意図したものであれば、すべて順調です。しかし[〜#〜] i [〜#〜]それが意図したものかどうかはわかりません。 2つの文字列を連結するこのコードを作成した場合はどうなるでしょうか。または、おそらくダブルを追加するつもりだったとしたらどうでしょうか?または、型を判別するためにすべての関数パラメーターの上にマウスを移動する必要がない場合はどうなりますか?

これを例にとります:

let foo x y = "result: " + x + y

F#では、おそらく文字列を連結するつもりであると想定しているため、xとyは文字列として定義されます。しかし、あなたのコードを管理している貧しいシュマックとして、私はこれを見て、おそらくxとy(ints)を一緒に追加し、UIの目的で結果を文字列に追加するつもりだったのではないかと思うかもしれません。

確かにそのような単純な例では手放すことができますが、明示的な型注釈のポリシーを適用しないのはなぜですか?

let foo (x:string) (y:string) = "result: " + x + y

明確であることにはどのような害がありますか?確かに、プログラマは自分がやろうとしていることに対して間違ったタイプを選択する可能性がありますが、少なくとも私は彼らがそれを意図していたことを知っています。

これは深刻な質問です...私はまだF#に非常に慣れていないため、会社の道を切り開いています。私が採用する標準は、将来のすべてのF#コーディングの基礎になる可能性が高く、無限のコピー貼り付けに組み込まれ、今後数年間はその文化に浸透すると確信しています。

では、F#の型推論について何か特別なことはありますか?または、エキスパートのF#-erは、重要なアプリケーションのパラメーターに注釈を付ける習慣を持っていますか?

私はF#を使用していませんが、Haskellでは、言語に浸透型の推論がある場合でも、(少なくとも)トップレベルの定義、および場合によってはローカルの定義に注釈を付けることが適切な形式と見なされています。これにはいくつかの理由があります。

  • 読み取り
    関数の使い方を知りたい場合、型シグネチャを用意しておくと非常に便利です。自分で推論したり、ツールを使用してそれを代行したりするのではなく、単にそれを読むことができます。

  • リファクタリング
    関数を変更する場合、明示的な署名があると、変換によって元のコードの意図が保持されることが保証されます。型推論された言語では、多態性の高いコードでは型チェックが行われますが、意図したとおりには行われないことがあります。型シグネチャは、インターフェイスで型情報を具体化する「バリア」です。

  • パフォーマンス
    Haskellでは、推測された関数の型が(型クラスによって)オーバーロードされる可能性があり、ランタイムディスパッチを意味する場合があります。数値型の場合、デフォルトの型は任意精度の整数です。これらの機能の完全な一般性が必要ない場合は、必要な特定のタイプに関数を特化することにより、パフォーマンスを向上させることができます。

ローカル定義、let- bound変数、ラムダへの仮パラメーターの場合、型シグネチャは通常の方がコードよりもコストがかかることがわかります彼らが追加する価値。したがって、コードレビューでは、トップレベルの定義の署名を主張し、他の場所でjudiciousアノテーションを要求することをお勧めします。

30
Jon Purdy

ジョンは合理的な answer を与えましたが、ここでは繰り返さないことにします。ただし、お客様のニーズを満たす可能性のあるオプションを紹介します。その過程で、はい/いいえ以外の異なるタイプの回答が表示されます。

最近、パーサーコンビネーターを使用してパースを処理しています。解析がわかっている場合は、通常、最初のフェーズでレクサーを使用し、2番目のフェーズでパーサーを使用します。レクサーはテキストをトークンに変換し、パーサーはトークンをASTに変換します。 F#が関数型言語であり、コンビネーターが組み合わされるように設計されているため、パーサーコンビネーターは、レクサーとパーサーの両方で同じ関数を使用するように設計されていますLexまたは解析して両方ではない。

例えば:

/// Parser that requires a specific item.

// a (tok : 'a) : ('a list -> 'a * 'a list)                     // generic
// a (tok : string) : (string list -> string * string list)     // string
// a (tok : token)  : (token list  -> token  * token list)      // token

または

/// Parses iterated left-associated binary operator.


// leftbin (prs : 'a -> 'b * 'c) (sep : 'c -> 'd * 'a) (cons : 'd -> 'b -> 'b -> 'b) (err : string) : ('a -> 'b * 'c)                                                                                    // generic
// leftbin (prs : string list -> string * string list) (sep : string list -> string * string list) (cons : string -> string -> string -> string) (err : string) : (string list -> string * string list)  // string
// leftbin (prs : token list  -> token  * token list)  (sep : token list  -> token  * token list)  (cons : token  -> token  -> token  -> token)  (err : string) : (token list  -> token  * token list)   // token

コードは著作権なのでここには含めませんが、 Github で入手できます。ここにコピーしないでください。

関数が機能するためには、ジェネリックパラメーターを残しておく必要がありますが、関数の使用に応じて推論された型を示すコメントを含めています。これにより、汎用的な機能を残したまま、メンテナンス機能を理解しやすくなります。

1
Guy Coder