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


99

私のプログラムで問題をデバッグしようとしているとき(同じ半径の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ただし、コメントアウトするとコンパイラエラーが発生し、さらに奇妙なことに、10in startPlayerを変更すると適切な応答が発生します(の開始サイズが変更されますPlayer)。再び、それが間違ったタイプであるにもかかわらず。データ定義が正しく読み取られていることを確認するために、入力ミスをファイルに挿入したところ、エラーが発生しました。だから私は正しいファイルを見ています。

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

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


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


26
@Cubicが指摘したように、この問題は必ずGlossのメンテナに報告してください。あなたの質問は、ライブラリの不適切な孤立したインスタンスがどのようにコードをめちゃくちゃにしたかをうまく説明しています。
クリスチャンコンクル2014年

1
できました。インスタンスを除外することは可能ですか?ライブラリが機能するために必要になるかもしれませんが、私は必要としません。また、Num Colorが定義されていることにも気付きました。それが私を悩ませるのは時間の問題です。
Carcigenicate 2014年

@Cubicまあ、遅すぎます。そして、更新した最新のCabalを使用して1週間ほど前にダウンロードしました。それが最新のはずです。
Carcigenicate 2014年

2
@ChristianConkle光沢の作成者がTypeSynonymInstancesの機能を理解していなかった可能性があります。いずれにせよ、これは本当に廃止する必要があります(Pointaを作成するnewtypeか、他の演算子名alaを使用しますlinear
Cubic

1
@Cubic:TypeSynonymInstances自体はそれほど問題ではありませんが(完全に無害というわけではありません)、OverlappingInstancesと組み合わせると非常に楽しいものになります。
John L

回答:


128

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

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


53
ワオ。10 :: (Float, Float)利回り(10.0,10.0)、および:i Numラインが含まれていますinstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’PointCOORDのグロスのエイリアスです)。マジ?ありがとうございました。それが眠れない夜から私を救った。
Carcigenicate 2014年

6
@Carcigenicateそれはそのような事例を許可するように軽薄なようだが、それは許さだ理由は、開発者が独自のインスタンスを作成することができますので、ということであるNumような、それは理にかなっているところをAngle拘束し、データ型Doubleの間-pipi、誰かがデータ型を書きたかった場合、またはをクォータニオンまたはその他のより複雑な数値タイプを表す場合、この機能は非常に便利です。また、String/ Text/ と同じルールに従い、ByteStringこれらのインスタンスを使いやすさの観点から理解できるようにしますが、この場合のように誤用される可能性があります。
bheklilr 2014年

4
@bheklilr Numのインスタンスを許可する必要性を理解しています。「WOW」はいくつかのことに起因しています。タイプエイリアスのインスタンスを作成できることを知りませんでした。CoordのNumインスタンスを作成することは、直感に反するように思われ、それについては考えていませんでした。まあ、教訓は学んだ。
カルシジェネート2014年

3
の代わりにのnewtype宣言を使用することで、ライブラリから孤立したインスタンスの問題を回避できます。Coordtype
ベンジャミンホジソン

3
@Carcigenicate 型シノニムのインスタンスを許可するには-XTypeSynonymInstancesが必要だと思いますが、問題のあるインスタンスにする必要はありません。拡張機能を必要としない、Num (Float, Float)または(Floating a) => Num (a,a)拡張する必要のないインスタンスでも、同じ動作になります。
crockeea 2014年

64

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。これについては、別の回答でもう少し詳しく説明します

あなたの質問を投稿する場合ので、経験豊富な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.PointPointはを意味し(Float, Float)ます。言い換えれば、あなたCoordglossのはPoint、文字通り同等です。

したがって、glossメンテナが書くことを選択したときinstance Num Point where ...、彼らはあなたのCoordタイプをのインスタンスにしましたNum。それはと同等ですinstance Num (Float, Float) where ...instance Num Coord where ...

(デフォルトでは、Haskellは型シノニムは、クラスのインスタンスにすることはできません。gloss著者は、言語拡張のペアを可能にするために、持っていたTypeSynonymInstancesFlexibleInstances、インスタンスを書くこと。)

第二に、孤立したインスタンス、つまりとのinstance C A両方が他のモジュールで定義されているインスタンス宣言であるため、これは驚くべきことです。ここでは、関係する各部分(つまり、、、および)がに由来し、どこにでも存在する可能性が高いため、これは特に油断できません。CANum(,)FloatPrelude

あなたの期待は、Numで定義されていることPrelude、そしてタプルとFloatで定義されているPreludeため、これらの3つの機能の動作に関するすべてがで定義されていPreludeます。完全に異なるモジュールをインポートすると何かが変わるのはなぜですか?理想的にはそうではありませんが、孤立したインスタンスはその直感を壊します。

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

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

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

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

これは、Haskell言語設計の問題の多い、議論の多い領域です。このredditスレッドには、関連する問題についての興味深い議論があります。たとえば、インスタンスの可視性の制御を許可することに関するエドワードクメットのコメントを参照してください。

(ちなみに、この回答が示したように、孤立したインスタンスを使用することで、いくつかの点でグローバルインスタンスの仮定を破ることができます!)

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

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

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

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

または、linear他の多くの正常に動作するライブラリの先導に従い、末尾が.OrphanInstancesまたはである別のモジュールで孤立したインスタンスを提供します.Instances。また、そのモジュールを他のモジュールからインポートしないでください。その後、ユーザーは必要に応じて孤立を明示的にインポートできます。

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

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

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.