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


28

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

それを考えると、なぜ私のチームメイトが機能パラメーターに注釈を付けないで逃げることができるのですか?はるかに複雑なコードの例として、次のコードをご覧ください。

let foo x y = x + y

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

これを例として見てみましょう。

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

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

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

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

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

これは深刻な質問です...私はまだF#が初めてであり、会社の道を切り開いています。私が採用する標準は、今後のF#コーディングの基礎となり、今後数年間、文化に浸透していくと確信している無限のコピーペーストに組み込まれます。

それで... F#の型推論に特別な何かがあり、それを保持する価値のある機能にし、必要な場合にのみ注釈を付けますか?または、専門のF#-ersは、自明ではないアプリケーションのパラメーターに注釈を付ける習慣をつけていますか?


注釈付きバージョンはあまり一般的ではありません。同じ理由で、SortedSet <>ではなく、シグネチャでIEnumerable <>を使用することが望ましいです。
パトリック

2
@Patrick-いいえ、F#は文字列型を推測しているため、そうではありません。関数のシグネチャは変更せず、明示的にしました。
JDB

1
@Patrick-たとえそれが本当だったとしても、それがなぜ悪いことなのか説明できますか?おそらくプログラマーが意図したものであれば、署名それほど一般的ではないはずです。おそらく、より一般的な署名が実際に問題を引き起こしているのかもしれませんが、多くの研究をせずにプログラマーがどのように具体的に意図したのかはわかりません。
JDB

1
関数型言語では、一般性を好み、より具体的なケースでは注釈を付けると言ってもいいと思います。たとえば、パフォーマンスを向上させたい場合や、タイプヒンティングを使用して取得できます。注釈付き関数をどこでも使用するには、特定のタイプごとにオーバーロードを記述する必要がありますが、一般的なケースでは保証されません。
ロバートハーベイ

回答:


30

私はF#を使用しませんが、Haskellでは、言語に一般的な型推論がありますが、(少なくとも)トップレベルの定義、場合によってはローカルな定義に注釈を付けるのが良いと考えられています。これにはいくつかの理由があります。

  • 読み取り
    関数の使用方法を知りたい場合は、タイプシグネチャを使用できると非常に便利です。自分で推測したり、ツールに頼ってあなたのためにしたりするのではなく、単に読むことができます。

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

  • パフォーマンス
    Haskellでは、関数の推論された型が(型クラスを介して)オーバーロードされる場合があり、これは実行時ディスパッチを意味する場合があります。数値型の場合、デフォルトの型は任意精度の整数です。これらの機能の完全な汎用性が必要ない場合は、必要な特定のタイプに関数を特化することでパフォーマンスを改善できます。

ローカル定義、let-bound変数、およびラムダへの仮パラメーターについては、通常、型シグネチャは追加する値よりもコードのコストが高くなることがわかります。そのため、コードレビューでは、最上位の定義の署名を主張し、他の場所で賢明な注釈を要求することをお勧めします。


3
これは非常に合理的でバランスの取れた応答のように聞こえます。
JDB

1
リファクタリングの際に、明示的に定義された型の定義が役立つことは同意しますが、ユニットテストでもこの問題を解決できることがわかりました。関数を別の関数と組み合わせることで、定義から型を除外し、重大な変更を加えた場合にそれがキャッチされるという自信を得ることができます。
ガイコーダ

4
Haskellでは、関数に注釈を付けることは正しいと考えられていますが、あいまいさを取り除く必要がない限り、慣用的なF#とOCamlは注釈を省略する傾向があると思います。それは有害だと言っているわけではありません(ただし、F#で注釈を付ける構文はHaskellよりもinいです)。
KChaloux

4
@KChalouxでOCamlのは、既にインタフェース(に注釈が入力されている.mli、あなたは強くすることが推奨されるものを、書く場合()ファイル彼らはインターフェイスを持つ冗長であると思いますので、注釈が定義から省略される傾向があるタイプ。。
ジル」をSO-悪であるのをやめる

1
確かに@Gilles @KChaloux、F#の署名(で同じ.fsiF#は、実装の中で何かがあいまいなのであれば、あなたはよ、型推論のためのシグネチャファイルを使用していません:1つの警告付き)ファイル、まだそれを再度注釈を付ける必要があります。books.google.ca/...
Yawar Aminの

1

ジョンはここでは繰り返さない合理的な答えを与えました。しかし、私はあなたのニーズを満たすかもしれないオプションを表示し、その過程でyes / no以外の異なるタイプの答えが表示されます。

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

例えば:

/// 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
汎用バージョンを関数に書き込まないのはなぜですか?うまくいかないでしょうか?
svick

@svickあなたの質問がわかりません。関数定義から型を除外しましたが、ジェネリック型は関数の意味を変更しないため、ジェネリック型を関数定義に追加できたでしょうか?もしそうなら、その理由は個人的な好みです。F#を初めて使用したときに、それらを追加しました。私がF#のより高度なユーザーと作業を始めたとき、署名なしでコードを変更する方が簡単だったので、彼らはコメントとして残すことを好みました。...
ガイコーダ

長い目で見れば、コードが機能していたときにコメントが機能するように、私はそれらをすべての方法で示しました。関数の作成を開始すると、型を入力します。関数を機能させたら、タイプシグネチャをコメントに移動し、できるだけ多くのタイプを削除します。F#では、推論を支援するために型を残す必要がある場合があります。しばらくの間、テスト用の行のコメントを外してコメントを作成できるようにするため、letと=を使用してコメントを作成しようとしていましたが、それは愚かに見え、letと=の追加はそれほど難しくありません。
ガイコーダ

これらのコメントがあなたの質問に答えたら、私に知らせてください。そうすれば、それらを答えに移すことができます。
ガイコーダ

2
したがって、コードを変更しても、コメントは変更されず、古いドキュメントになりますか?それは私にとって利点のように聞こえません。厄介なコードよりも厄介なコメントの方が良いとは思えません。私には、このアプローチは2つのオプションの欠点を兼ね備えているようです。
svick

-8

バグの数は、プログラムの文字数に正比例します!

これは通常、コードの行に比例するバグの数として表されます。ただし、コードの量が同じで行数が少ないと、同じ量のバグが発生します。

そのため、明示的にするのは良いことですが、入力した余分なパラメーターはバグにつながる可能性があります。


5
「バグの数は、プログラムの文字数に正比例します!」だから、すべてAPLに切り替える必要がありますか?あまりに冗長であることは、冗長すぎるように問題です。
svick

5
この質問に答えられるようにするために、プログラマの心の中にある曖昧さのコストは、いくつかの余分なキーストロークよりもはるかに高価であると仮定しましょう。
JDB
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.