ghcでの相互依存のあるモジュールのimport方法について

1. 相互依存のあるモジュールは、そのままではコンパイル出来ない。

以下のような相互依存のあるコードのコンパイルを試みる。

Foo.hs:

module Foo(Foo, getData, idProduct) where

import qualified Bar as B

data Foo = Foo {
      name :: String,
      id_  :: Int
} deriving (Show, Eq)

getData :: Foo -> (String, Int)
getData foo = (name foo, id_ foo)

idProduct :: Foo -> B.Bar -> Int
idProduct foo bar = (id_ foo) * (B.id_ bar)

Bar.hs:

module Bar (Bar, name, id_, getData) where

import qualified Foo as F

data Bar = Bar {
      name :: String
      ,id_  :: Int
      ,foo  :: F.Foo
} deriving (Show, Eq)

getData :: Bar -> (String, Int)
getData bar = (name bar, id_ bar)

すると、以下のように相互依存エラーが出てコンパイル出来ない。

$ ghc Foo.hs
Module imports form a cycle for modules:
  Foo (Foo.hs)
    imports: Bar
  Bar (./Bar.hs)
    imports: Foo

個別にコンパイルしようとしてもエラーとなる。

$ ghc -c Foo.hs

Foo.hs:3:1:
    Failed to load interface for `Bar':
      Use -v to see a list of the files searched for.
$ ghc -c Bar.hs

Bar.hs:3:1:
    Failed to load interface for `Foo':
      Use -v to see a list of the files searched for.

2. ghcは相互依存解決方法を提供してはいる。

ghcの提供する相互依存解決方法は、http://www.haskell.org/ghc/docs/latest/html/users_guide/separate-compilation.html#mutual-recursionにて説明されている。

これを要約すると、以下の2点になる。

    • 相互依存するモジュールをimportする際には、{-# SOURCE #-}プラグマを付ける。
    • importされるモジュールは、型情報等を提供する.hs-boot拡張子を持ったファイルを用意する。(C言語のヘッダファイルのようなもの)

以下にこれらを素直に適用してみたコードを示す。
これらのコードはコンパイルに失敗する。

Foo.hs-boot:

module Foo(Foo, getData, idProduct) where

import qualified Bar as B

data Foo = Foo {
      name :: String,
      id_  :: Int
} deriving (Show, Eq)

getData :: Foo -> (String, Int)
idProduct :: Foo -> B.Bar -> Int

Bar.hs:

module Bar (Bar, name, id_, getData) where

import {-# SOURCE #-} qualified Foo as F

data Bar = Bar {
      name :: String
      ,id_  :: Int
      ,foo  :: F.Foo
} deriving (Show, Eq)

getData :: Bar -> (String, Int)
getData bar = (name bar, id_ bar)

以下にコンパイル時のエラーを示す。

$ ghc -c Foo.hs-boot 

Foo.hs-boot:3:1:
    Failed to load interface for `Bar':
      Use -v to see a list of the files searched for.
$ ghc -c Bar.hs 

Bar.hs:3:1:
    Failed to load interface for `Foo':
      Use -v to see a list of the files searched for.

どこが悪いのかさっぱりわからない。

3. .hs-bootを修正してコンパイルが通るようにする。

上記のFoo.hs-bootのコンパイルが失敗するのは以下の理由による。

    1. Foo.hs-bootがBarに依存している。
    2. .hs-boot内ではデータ型の定義にderivingを使えない。

どうやら.hs-bootファイルには、.hsから依存している型情報を取り除いたものを記述しなければならないようである。
derivingが使えない点は、代わりにinstanceを使って解決する。

Foo.hs-boot:

module Foo(Foo, getData) where

data Foo = Foo {
      name :: String,
      id_  :: Int
} 

instance Show Foo
instance Eq Foo

getData :: Foo -> (String, Int)

.hs-bootファイルを修正後、以下の順番でコンパイルする。
エラーが出なくなり、コンパイルが成功した。

$ ghc -c Foo.hs-boot
$ ghc -c Bar.hs 
$ ghc -c Foo.hs

4. 強い依存関係がある場合

相互に強い依存関係がある場合には、Foo.hs-bootからBarモジュールの要素を取り除けないだろう。
このような場合には、依存関係を解消するように設計し直すか、FooとBar を一つのモジュールにするか、どちらかを選択しなければならない。