web-dev-qa-db-ja.com

型チェッカーは非常に間違った型の置換を許可しており、プログラムはまだコンパイルします

私のプログラムで問題をデバッグしようとすると(等しい半径の2つの円がGloss*を使用して異なるサイズに描画されている)、奇妙な状況に遭遇しました。オブジェクトを処理するファイルで、Playerを次のように定義しています。

type Coord = (Float,Float)
data Obj =  Player  { oPos :: Coord, oDims :: Coord }

また、Objects.hsをインポートするメインファイルには、次の定義があります。

startPlayer :: Obj
startPlayer = Player (0,0) 10

これは、プレーヤーのフィールドを追加および変更し、startPlayerを更新するのを忘れたために起こりました(その寸法は、半径を表す単一の数値によって決定されましたが、Coordに変更しました(幅、高さ)を表します。プレーヤーオブジェクトを非円にした場合に備えて)。

驚くべきことは、2番目のフィールドの型が間違っているにもかかわらず、上記のコードがコンパイルされて実行されることです。

私は最初、ファイルの異なるバージョンを開いている可能性があると最初に思いましたが、ファイルへの変更はコンパイルされたプログラムに反映されました。

次に、何らかの理由でstartPlayerが使用されていない可能性があると思いました。ただし、startPlayerをコメントアウトするとコンパイラエラーが発生し、さらに奇妙なことに、startPlayer10を変更すると適切な応答が発生します(Playerの開始サイズが変更されます) ;再び、それが間違ったタイプであるにもかかわらず。データ定義が正しく読み取られていることを確認するために、入力ミスをファイルに挿入したところ、エラーが発生しました。だから私は正しいファイルを見ています。

上記の2つのスニペットを独自のファイルに貼り付けてみたところ、PlayerstartPlayerの2番目のフィールドが正しくないという予想されるエラーが発生しました。

これが起こる可能性があるのは何ですか?これこそが、Haskellの型チェッカーが防ぐべきことだと思います。


*私の元の問題に対する答えは、半径が等しいと思われる2つの円が異なるサイズで描かれることですが、半径の1つが実際には負であるというものでした。

98
Carcigenicate

これがコンパイルできる可能性がある唯一の方法は、Num (Float,Float)インスタンスが存在する場合です。これは標準ライブラリでは提供されていませんが、使用しているライブラリの1つがなんらかの理由で追加した可能性があります。プロジェクトをghciにロードして10 :: (Float,Float)が機能するかどうかを確認してから、:i Numを試してインスタンスがどこから来ているかを調べ、定義した人に怒鳴りつけます。

補遺:インスタンスをオフにする方法はありません。 notをモジュールからエクスポートする方法すらありません。これが可能であるとすれば、moreの混乱を招くコードになる可能性もあります。ここでの唯一の実際の解決策は、そのようなインスタンスを定義しないことです。

128
Cubic

Haskellの型チェッカーは妥当です。問題は、あなたが使用しているライブラリの作者が何かをしたということです...あまり合理的ではありません。

簡単な答えは次のとおりです。はい、10 :: (Float, Float)がある場合、Num (Float, Float)は完全に有効です。コンパイラーまたは言語の観点から、それについて「非常に間違っている」ことは何もありません。それは、数値リテラルが何をするかについての私たちの直感とは関係ありません。あなたはあなたが作った種類のエラーを捕まえるタイプシステムに慣れているので、あなたは正当に驚かれ、失望します!

NumインスタンスとfromInteger問題

コンパイラが_10 :: Coord_、つまり10 :: (Float, Float)を受け入れることに驚いています。 _10_のような数値リテラルは「数値」型を持っていると推測されると想定するのは理にかなっています。デフォルトでは、数値リテラルはIntIntegerFloat、またはDoubleとして解釈できます。他のコンテキストがない数値のタプルは、これらの4つのタイプが数値である点で数値のようには見えません。 Complex については話していません。

幸いにも、残念ながら、Haskellは非常に柔軟な言語です。この規格では、_10_のような整数リテラルは_fromInteger 10_型の_Num a => a_として解釈されると規定されています。したがって、_10_は、Numインスタンスが記述されたanyタイプとして推論できます。 別の答え でこれについてもう少し詳しく説明します。

そのため、質問を投稿すると、経験豊富なHaskellerが10 :: (Float, Float)を受け入れるにはNum a => Num (a, a)またはNum (Float, Float)のようなインスタンスが必要であることをすぐに発見しました。 Preludeにはそのようなインスタンスはないため、別の場所で定義されている必要があります。 _:i Num_を使用すると、glossパッケージがどこから来たかがすぐにわかります。

同義語と孤立インスタンスを入力する

しかし、ちょっと待ってください。この例では、glossタイプを使用していません。 glossのインスタンスがなぜ影響を受けたのですか?答えは2つのステップで来ます。

まず、 キーワードtypeで導入された型の同義語は新しい型を作成しません 。モジュールでのCoordの記述は、単に_(Float, Float)_の省略形です。同様に _Graphics.Gloss.Data.Point_ では、Pointは_(Float, Float)_を意味します。言い換えると、CoordglossPointは文字通り同等です。

したがって、glossのメンテナが_instance Num Point where ..._を記述することを選択した場合、CoordタイプをNumのインスタンスにしました。これは、instance Num (Float, Float) where ...または_instance Num Coord where ..._と同等です。

(デフォルトでは、Haskellは型シノニムをクラスインスタンスにすることを許可していません。glossの作成者は、言語拡張のペアTypeSynonymInstancesFlexibleInstancesを有効にして、インスタンス。)

第二に、これはOrphan instance、つまりインスタンス宣言_instance C A_であり、CAの両方が他のモジュールで定義されているため、驚くべきことです。ここでは、関係する各部分、つまりNum、_(,)_、およびFloatPreludeから取得され、どこでもスコープ内にある可能性が高いため、特に油断できません。

あなたの期待は、NumPreludeで定義され、タプルとFloatPreludeで定義されていることです。 Prelude。完全に異なるモジュールをインポートすると何かが変わるのはなぜですか?理想的にはそうではありませんが、孤児のインスタンスはその直感を壊します。

(GHCは孤立したインスタンスについて警告することに注意してください。glossの作成者は特にその警告を上書きしました。これにより、警告が発せられ、少なくともドキュメントに警告が表示されます。)

クラスインスタンスはグローバルであり、非表示にできません

さらに、クラスインスタンスはglobalです。yourモジュールから推移的にインポートされた任意のモジュールで定義されたインスタンスはコンテキストにあり、インスタンス解決時にタイプチェッカーで使用できます。これにより、(通常)_(+)_のようなクラス関数は特定の型に対して常に同じであると想定できるため、グローバルな推論が便利になります。しかし、それはまた、ローカルな決定がグローバルな影響を与えることを意味します。クラスインスタンスを定義すると、下流のコードのコンテキストが完全に変更され、モジュール境界の背後でマスクしたり隠したりすることはできません。

インスタンスのインポートを回避するためにインポートリストを使用することはできません 。同様に、定義したモジュールからインスタンスをエクスポートすることは避けられません。

これは、Haskell言語設計の問題の多い、議論の多い領域です。 このredditスレッド に関連する問題の興味深い議論があります。たとえば、インスタンスの可視性の制御を許可することに関するエドワードクメットのコメントを参照してください。「基本的に、私が記述したほとんどすべてのコードの正確さを放棄します。」

(ちなみに この答えが示すように 、あなたはcan Orphanインスタンスを使用することで、グローバルインスタンスの仮定をいくつかの点で破ります!)

何をすべきか—ライブラリの実装者向け

Numを実装する前によく考えてください。 fromIntegerの問題を回避することはできません—いいえ、_fromInteger = error "not implemented"_を定義するとnotで問題が解決します。整数リテラルがインスタンス化している型であると誤って推測された場合、ユーザーは混乱したり驚いたりしますか? _(*)_および_(+)_を提供することは、特にハッキングが必要な場合に重要ですか?

Conal Elliottの _vector-space_ (種類の種類_*_の場合)またはEdward Kmettの linear などのライブラリで定義された代替算術演算子の使用を検討してください=(種類__* -> *_のタイプの場合)。これは私が自分で行う傾向があることです。

_-Wall_を使用します。孤立したインスタンスを実装しないでください。孤立したインスタンスの警告を無効にしないでください。

または、linearおよびその他の多くの正常に動作するライブラリの先導に従い、_.OrphanInstances_または _.Instances_ で終わる別のモジュールにOrphanインスタンスを提供します。そして他のモジュールからそのモジュールをインポートしない。その後、ユーザーは必要に応じて孤立を明示的にインポートできます。

孤児を定義していることに気付いた場合は、可能で適切であれば、代わりに上流のメンテナにそれらの実装を依頼することを検討してください。私は、OrphanインスタンスShow a => Show (Identity a)transformersに追加するまで頻繁に記述していました。私はそれについてバグ報告をしたかもしれません。覚えていません。

何をすべきか—図書館利用者向け

多くのオプションはありません。丁寧かつ建設的に!-図書館の管理者に連絡してください。この質問を指摘します。彼らは問題のあるオーファンを書くために何らかの特別な理由を持っていたかもしれません、または彼らは単に気づかないかもしれません。

より広く:この可能性に注意してください。これは、真のグローバルな影響があるHaskellの数少ない領域の1つです。インポートするすべてのモジュール、およびすべてのモジュールthoseモジュールのインポートがOrphanインスタンスを実装していないことを確認する必要があります。型注釈は問題を警告することがあります。もちろん、GHCiで_:i_を使用して確認できます。

重要な場合は、newtypeの同義語ではなく独自のtypesを定義します。あなたは誰もそれらを台無しにしないだろうとかなり確信することができます。

オープンソースライブラリに由来する問題が頻繁に発生する場合は、もちろん独自のバージョンのライブラリを作成できますが、メンテナンスがすぐに頭痛の種になる可能性があります。

63