Haskellの循環import問題

まず基本的な症状を確認。

module Main where
import Sub
main = do return ()
module Sub where
import Main

という2つのモジュールを用意して'ghc --make Main.hs'とすると

Module imports form a cycle for modules:
  Main imports: Sub
  Sub imports: Main

となる。要するに互いを参照するimportを書くだけでエラーなのね。これはこれでけっこうひどい挙動じゃないか?少しはモジュールの中身も見てくれないかね。

で、私がやりたいモジュール分割は

module Main where
import Enemy
import Bullet
class TokenController t where
  update :: t -> t
data Token = Enemy {x :: Double, y :: Double} |
             Bullet {x :: Double, y :: Double}
instance TokenController Token where
  update enemy@Enemy{} = Enemy.update enemy
  update bullet@Bullet{} = Bullet.update bullet
instance Show Token where
  show (Enemy x y) = "enemy(" ++ (show x) ++ ", " ++ (show y) ++ ")"
  show (Bullet x y) = "bullet(" ++ (show x) ++ ", " ++ (show y) ++ ")"
ts = [Enemy 0 0, Bullet 0 0]
main = do
  putStrLn (show ts)
  putStrLn (show (map Main.update ts))

としておいて、モジュールEnemyおよびBulletにおいて

module Enemy where
import Main
update enemy = enemy {x = (x enemy) + 1}

のように更新を行いたいというもの。これらは互いにimportし合っているので当然ビルドできない。

そこでこれだ。

{-# SOURCE #-} pragmaとhs-bootというきわめて怪しげなメカニズムを用いる。まず以下のMain.hs-bootを用意した上で

module Main where
data Token = Enemy {x :: Double, y :: Double} |
             Bullet {x :: Double, y :: Double}

Enemy.hsおよびBullet.hsを

module Bullet where
import {-# SOURCE #-} Main
update bullet = bullet {y = (y bullet) + 1}

のようにする。すると

% ghc --make Main.hs
Chasing modules from: Main.hs
Compiling Main[boot]       ( Main.hs-boot, Main.o-boot )
Compiling Bullet           ( ./Bullet.hs, ./Bullet.o )
Compiling Enemy            ( ./Enemy.hs, ./Enemy.o )
Compiling Main             ( Main.hs, Main.o )
Linking ...
% ./main.exe 
[enemy(0.0, 0.0),bullet(0.0, 0.0)]
[enemy(1.0, 0.0),bullet(0.0, 1.0)]

という具合にビルドできる!素晴らし……いか?このほにゃらら-bootって要するにヘッダファイルみたいなもんだと思えばいいのかしらん。微妙に泥臭い。あと.hsと.hs-bootの両方に同じ定義が必要になるのがいただけないな。

SOURCE pragmaを使わない形に書き直せればいいんだけど……思いつかない。まだまだ経験不足ですな。