第14章 既知のバグと問題点

目次

14.1. Haskell 標準とGlasgow Haskell、言語上の不準拠点
14.1.1. Haskell 98およびHaskell 2010からの逸脱
14.1.1.1. 字句的構文
14.1.1.2. 文脈自由文法
14.1.1.3. 式とパターン
14.1.1.4. 宣言と束縛
14.1.1.5. モジュールシステムとインタフェースファイル
14.1.1.6. 数値、基本型、組込みクラス
14.1.1.7. Preludeサポートに関すること
14.1.1.8. 外部関数インタフェース
14.1.2. Haskell 98とHaskell 2010の未定義動作についてのGHCの解釈
14.1.3. FFI仕様からの逸脱
14.2. 既知のバグと問題点
14.2.1. GHCのバグ
14.2.2. GHCi(対話的GHC)のバグ

14.1. Haskell 標準とGlasgow Haskell、言語上の不準拠点

この節では、Haskell 98およびHaskell 2010の実装についてのGlasgow Haskellの問題点を列挙する。クラッシュ、スペースリークなどの良くない現象については「うまくいかないとき」の節(第11章. 何かがうまくいかないとき)も参照せよ。

この制限一覧は、(大体)Haskellレポートの順で並んでいる。

14.1.1. Haskell 98およびHaskell 2010からの逸脱

デフォルトではGHCは(大部分)Haskell 2010のコンパイラのように振る舞おうとするが、-XHaskell98-XHaskell2010フラグを使うことで特定の言語バージョンのように振る舞うようにすることもできる。標準からの逸脱は以下に記述する。特に述べていない場合、逸脱はHaskell 98、Haskell 2010、デフォルトの各モードに関するものである。

14.1.1.1. 字句的構文

  • GHCでは、修飾された識別子に関して、ある種の字句規則がHaskellレポートのものとわずかに異なっている。module.reservedopという形のもの、例えばM.\があったとき、M.\という二つの字句要素ではなく、一つの修飾された演算子と解釈される。

14.1.1.2. 文脈自由文法

  • Haskell 98モードおよびデフォルトモード(Haskell 2010モードはそうでない)において、GHCは、do式でのレイアウト規則について、多少寛容である。具体的には、「入れ子の内側の文脈は、外側の文脈よりも多くインデントされなければならない」という規則を緩和して、外側の文脈がdoの場合、内側の文脈が外側の文脈と同じ水準でも良いとする。

    例えば、GHCは以下のコードを受け付ける。

    main = do args <- getArgs
              if null args then return [] else do
              ps <- mapM process args
              mapM print ps
    

    この振る舞いはNondecreasingIndentation拡張によって制御される。

  • Haskell 98は(Haskell 2010は違う)パース中に式の中で結合性の解決を行なうことを要求するが、GHCはこれを行なわない。例えば、Haskell レポートによれば、以下のものは合法的なHaskellである。

    let x = 42 in x == 42 == True

    そして、これは以下のようにパースされるとする。

    (let x = 42 in x == 42) == True

    これは、レポートによれば、let式はできるだけ右へ拡張されるからである。二番目の等号を越えて拡張しようとするとパースエラーが発生する(==は非結合的なので)ため、let式はそこで終わる。GHCは単純に式全部を飲み込む。パースは次のようになる。

    (let x = 42 in x == 42 == True)

14.1.1.3. 式とパターン

デフォルトのモードでは、GHCはある種のプログラムの定義され度合いを本来よりも僅かに大きくする。例として以下を考えよ。

f :: [a] -> b -> b
f [] = error "urk"
f (x:xs) = \v -> v

main = print (f [] `seq` True)

これはerrorを呼ぶべきだが、実際にはTrueを表示する。理由: GHCはfを以下のようにη展開する。

f :: [a] -> b -> b
f []     v = error "urk"
f (x:xs) v = v

大部分のプログラムについては、これは僅かだが有意に効率を改善する。これが害になるようなプログラムは少ない。この偽の「最適化」を無効にするには-fpedantic-bottomsを使う。

14.1.1.4. 宣言と束縛

データ型の文脈は、次の版の言語標準では取り除かれることが決まったので、デフォルトモードにおいてGHCはこれを受け付けない。この振る舞いはDatatypeContexts拡張で制御できる。7.4.2. データ型文脈を見よ。

14.1.1.5. モジュールシステムとインタフェースファイル

4.7.9. 相互再帰的なモジュールをコンパイルするにはにある通り、相互再帰的なモジュールのループがある場合、それをhs-bootファイルを使って断つことをGHCは要求する。これはバグと言うより不幸な出来事である。Haskellレポートは次のように述べている(5.7節和訳)。「使うHaskell処理系によっては、相互再帰的なモジュールを分割コンパイルするとき、インポートされるモジュールに追加の情報を持たせ、そのモジュールがコンパイルされる前に参照できるようにすることが要求されるかもしれない。相互再帰に対処するため、エクスポートされる値全てについて、明示的な型シグネチャが必要とされるかもしれない。分割コンパイルの精密な詳細はこのレポートでは定義しない。」

14.1.1.6. 数値、基本型、組込みクラス

Numのスーパークラス

Numクラスは、スーパークラスとしてShowおよびEqを持たない。

以下のようにすることで、Haskell98/Haskell2010とGHCの両方で動くコードを作ることができる。

  • Numのインスタンスを作るときは必ず、ShowEqのインスタンスにもする。

  • Num tという制約を持つ関数またはインスタンスまたはクラスを与える場合は必ず、Show tEq t制約も与える。

Bitsのスーパークラス

BitsクラスはスーパークラスとしてNumを持たない。従ってbittestBitpopCountのデフォルトメソッドを持たない。

以下のようにすることで、Haskell2010とGHCの両方で動くコードを作ることができる。

  • 型をBitsのインスタンスにするときは常にNumのインスタンスにもする。

  • Bits t制約を持つ関数、インスタンス、クラスを与えるときは常に、Num t制約も持たせる。

  • Bitsのインスタンスでは、常にbittestBitpopCountの各メソッドを定義する。

追加のインスタンス

以下のインスタンスが追加で定義される。

instance Functor ((->) r)
instance Monad ((->) r)
instance Functor ((,) a)
instance Functor (Either a)
instance Monad (Either e)
配列要素の複数回定義は検査されない

以下のコード断片は致命的エラーを出すべきだが、実際にはそうならない。

main = print (array (1,1) [(1,2), (1,3)])

GHCのarrayの実装では、配列要素の値はリスト中に最後に現れた(index,value)の対から採られ、重複は検査されない。これは純粋単純に効率上の理由からである。

14.1.1.7. Preludeサポートに関すること

任意の大きさのタプル

タプルの大きさは現在100までに制限されている。ただし、タプルに関する標準インスタンス(EqOrdBoundedIx ReadShow)は16要素のタプルまでについてしか使えない。

この制限は簡単に覆せるので、これで困ることがあったら教えてほしい。

整数のRead

整数型に関するReadクラスのGHCでの実装は、十六進と八進のリテラルにも対応している。(Haskell 98レポート中のコードは対応していない)。従って、GHCでは以下のものが動作する。

read "0xf00" :: Int

こうなっている理由としては、readLitCharが十六進や八進のエスケープを受け付けるのに、整数についてそうしないのは整合性に欠けるというのが考えられる。

isAlpha

Haskell 98でのisAlphaの定義は以下のようになっている。

isAlpha c = isUpper c || isLower c

GHCでの実装では、Unicodeのアルファベット文字のうち、小文字でも大文字でもない文字についても、isAlphaはこれをアルファベットだとみなすが、この点でHaskell 98から逸脱している。

hGetContents

遅延I/Oにおいてエラーが発生した場合、Haskell 98の仕様ではそれを捨てることになっている(Haskell 98レポートの21.2.2節を見よ)が、それに反して例外が投げられる。投げられるのは、原因となったIO操作がIOモナド中で実行された場合に投げられるであろう通常のIO例外であり、System.IO.Error.catchControl.Exception.catchで捕捉できる。

14.1.1.8. 外部関数インタフェース

hs_exit()の後にhs_init()を呼ぶことが許されない

FFI仕様の要求するところによると、実装は、hs_exit()によって停止した後に再び初期化を行うのに対応していなければならないが、GHCは現在これを行えない。

14.1.2. Haskell 98とHaskell 2010の未定義動作についてのGHCの解釈

この節では、Haskell 98で未定義、または実装固有とされている諸問題について、GHCの立場を解説する。

Char

ISO-10646標準に従って、GHCでのmaxBound :: Char0x10FFFFである。

大きさ付き整数型

GHCでは、Int型の精度はホストアーキテクチャでのアドレスの大きさと同じである。言い替えると、32ビットの機械では32ビットだし、64ビットの機械では64ビットである。

Intの数値演算はオーバーフローの検査を行わない。従って、Intの全ての演算は2nを法として行われる。ただしnInt型のビット数である。

fromInteger関数(従ってfromIntegralも)は、Intに変換する時は特別は場合である。fromIntegral x :: Intの値は、まず(abs x)の下位nビットを採り、xの符号を掛ける(nビットの2の補数演算で)ことで得られる。この振る舞いは、0xffffffff :: Intと書いた場合に、結果のIntにビットパターンが保存されるように選ばれた。

例えば-3のような負のリテラルは、Haskellレポートによれば(注意深く読めば)、Prelude.negate (Prelude.fromInteger 3)を意味する。従って-2147483648negate (fromInteger 2147483648)である。fromIntegerは表現の下位32ビットを採るので、fromInteger (2147483648::Integer)を型をIntとして計算すると-2147483648::Intになる。次にnegate演算はオーバーフローするが、これは検査されないので、negate (-2147483648::Int)は単に-2147483648になる。まとめると、minBound::Intをリテラルとして書いた場合、予期した通りの意味になる。(しかしこれは一般には保証されていない。)

fromIntegral関数は、大きさ固定の整数型(Int8Int16Int32Int64、および対応する符号なしのWord系の型)間で変換するときも、ビットパターンを保存する。ライブラリ説明書中のData.IntData.Wordモジュールを見よ。

検査なしの浮動小数演算

FloatDoubleの数値の演算は、オーバーフロー、アンダーフロー、その他の悲しい現象について検査されない。(ただし、アーキテクチャによっては、浮動小数オーバーフローや精度の損失を捉え、浮動小数例外として報告する場合がある。この場合おそらくプログラムは終了させられる)

14.1.3. FFI仕様からの逸脱

hs_exit()の後にhs_init()を呼ぶことが許されない

FFI仕様の要求するところによると、実装は、hs_exit()によって停止した後に再び初期化を行うのに対応していなければならないが、GHCは現在これを行えない。