StateT が使えるようになったらグローバル変数みたいなものが簡潔に書けるのかなーと考えました。グローバル変数たちを集めた data 型を定義して、トップレベルは基本的に StateT GlobalVars IO () 型にする感じ。
module Main(main) where import Control.Monad.State -- グローバル変数たち data GlobalVars = GlobalVars { fooField :: Int , barField :: Int } deriving Show -- トップレベルの型 type GlobalM = StateT GlobalVars IO -- トップレベル myMain :: GlobalM () myMain = do foo <- gets fooField -- foo を読み出す modify $ \g -> g { barField = 1 } -- bar に 1 を書く g <- get liftIO $ print g -- グローバル変数たちを dump する -- 本当のトップレベル (myMain を呼ぶだけ) main = evalStateT myMain initGlobalVars -- グローバル変数たちの初期状態 initGlobalVars = GlobalVars 1 2
この方法の欠点はグローバル変数一覧を列挙しないといけないことです。上の例ではグローバル変数が 2 つしかないのでさほど問題ではありませんが、普通はもっと多数になります。これを全部フラットに並べるのは何となく避けたいです。以下のように、まとめて使うことの多い変数をまとめた data 型を定義して、階層的にしたいと思います。
data GlobalVars = GlobalVars { module1Vars :: Module1Vars , module2Vars :: Module2Vars } deriving Show data Module1Vars = Module1Vars { fooField :: Int , barField :: Int } deriving Show data Module2Vars = Module2Vars { bazField :: Int , quxField :: Int } deriving Show
すると、読み出しや書き込みがひどく煩雑になってしまいます。とくに書き出し。そのくせ変数をまとめた意味がほとんどありません。
myMain = do -- module1 の foo を読み出す foo <- gets $ fooField . module1Vars -- module1 の bar に 1 を書く modify $ \g -> g { module1Vars = module1Vars g { barField = 1 } }
「この範囲では module1 の変数しかいじらない!」と決めるなら、以下のように書けます。
myMain = do -- ここから GlobalVars の世界 (get や put の対象は GlobalVars) -- Module1Vars を取り出す m1vs <- gets module1Vars -- ここまで GlobalVars の世界 m1vs' <- (`execStateT` m1vs) $ do -- ここから Module1Vars の世界 (get や put の対象は Module1Vars) foo <- gets fooField -- foo を読み出す modify $ \g -> g { barField = 1 } -- bar に 1 を書く g <- get liftIO $ print g -- グローバル変数たちを dump する -- と思ったらできない -- ここまで Module1Vars の世界 -- ここから GlobalVars の世界 -- GlobalVars 中の Module1Vars に更新結果を反映する modify $ \g -> g { module1Vars = m1vs' } -- ここまで GlobalVars の世界
しかしこの方法では、コメントにあるように、Module1Vars の世界からグローバル変数たち全部を dump することができません。Module1Vars の世界から GlobalVars を参照できないためです。ある意味隠蔽なので良い性質なのですが、世の中そんなにかっちりモジュール分けできる人間はほとんどいないと思います。なので、「デフォルトでは Module1Vars をいじるけど、特別なときだけ覗き穴経由で GlobalVars もいじれるような仕組み」が欲しいと思います。以下のようなの。
myMain = do localModule1 $ do -- ここから Module1Vars の世界 (get や put の対象は Module1Vars) -- foo を読み出す foo <- gets fooField -- bar に 1 を書く modify $ \g -> g { barField = 1 } -- 覗き穴を開いて GlobalVars の世界をつつく globalFromModule1 $ do -- ここだけ GlobalVars の世界 -- (get や put の対象は GlobalVars) -- グローバル変数たちを dump する get >>= liftIO . print
上の例は小さいのでありがたみが見えないかもしれませんが、規模が大きくなればありがたみが感じられると思います。まだ自分でも使ったことないのでわかりませんが。さて、これをどうやって実現するかというと、以下のように自分でモナドとかをインスタンス化します。コンパイルには -fglasgow-exts をつけてください。
-- Module1Vars の世界を表すモナドを作る型 newtype Module1T m a = Module1T { runModule1T :: m a } localModule1 = runModule1T globalFromModule1 = Module1T instance Monad (Module1T GlobalM) where return = globalFromModule1 . return m >>= k = globalFromModule1 $ localModule1 m >>= localModule1 . k instance MonadState Module1Vars (Module1T GlobalM) where put v = globalFromModule1 . modify $ \d -> d { module1Vars = v } get = globalFromModule1 $ gets module1Vars instance MonadIO (Module1T GlobalM) where liftIO = globalFromModule1 . liftIO
Functor とかもインスタンス化したほうがいいのかもしれませんが、とりあえずこれで望みの挙動が得られました。でも、これと同じ物を Module2Vars に対しても書かないといけません。Module3Vars が増えたらまた書かないといけません。こういうところは最小化したいです。そこで次のようなものを書きました (本題) 。コンパイルには -fallow-undecidable-instances をつけてください *1 。
class Scope v t | v -> t, t -> v where global :: GlobalM x -> t GlobalM x local :: t GlobalM x -> GlobalM x update :: v -> GlobalVars -> GlobalVars extract :: GlobalVars -> v instance Scope v t => Monad (t GlobalM) where return = global . return m >>= k = global $ local m >>= (local . k) instance Scope v t => MonadState v (t GlobalM) where put = global . modify . update get = global $ gets extract instance Scope v t => MonadIO (t GlobalM) where liftIO = global . liftIO
以下のようにするだけ (というほど少なくないけど) で localModule1 と localModule2 と global が使えるようになりました。
newtype Module1T m a = Module1T { runModule1T :: m a } instance Scope Mod1Vars Module1T where global = Module1T local = runModule1T update v w = w { Mod1Vars = v } extract = Mod1Vars localModule1 = local :: Module1T GlobalM x -> GlobalM x newtype Module2T m a = Module2T { runModule2T :: m a } instance Scope Mod2Vars Module2T where global = Module2T local = runModule2T update v w = w { Mod2Vars = v } extract = Mod2Vars localModule2 = local :: Module2T GlobalM x -> GlobalM x
localModule1 などをいちいち型注釈つけて定義しているのは、オーバーロードが解決できないといってコンパイラが泣くからです。globalFromModule1 なんかはコンパイラが解決してくれるので global だけでいいです。
以上、試行錯誤してたらたまたまコンパイルが通ったコードを晒してみました。functional dependency?なにそれ、おいしい?
こんなことぐだぐだ考えずに IORef 使ったり unsafePerformIO 使ったり implicit parameter 使ったりする方がいいのかもしれません。実行速度も速そうだし。
ほんのちょっとだけ調べた先人の記録。
- unsafePerformIO を使ってグローバル変数を定義する方法
- Global Vars in Haskell (Programming Pearl)
最後に全部のコード。
{-# OPTIONS -fglasgow-exts #-} {-# OPTIONS -fallow-undecidable-instances #-} module Main(main) where import Control.Monad.State data GlobalVars = GlobalVars { Mod1Vars :: Mod1Vars, Mod2Vars :: Mod2Vars } deriving Show data Mod1Vars = Mod1Vars { mod1Field1 :: Int, mod1Field2 :: Int } deriving Show data Mod2Vars = Mod2Vars { mod2Field1 :: Int, mod2Field2 :: Int } deriving Show type GlobalM = StateT GlobalVars IO class Scope v t | v -> t, t -> v where global :: GlobalM x -> t GlobalM x local :: t GlobalM x -> GlobalM x update :: v -> GlobalVars -> GlobalVars extract :: GlobalVars -> v instance Scope v t => Monad (t GlobalM) where return = global . return m >>= k = global $ local m >>= (local . k) instance Scope v t => MonadState v (t GlobalM) where put = global . modify . update get = global $ gets extract instance Scope v t => MonadIO (t GlobalM) where liftIO = global . liftIO newtype Module1T m a = Module1T { runModule1T :: m a } newtype Module2T m a = Module2T { runModule2T :: m a } instance Scope Mod1Vars Module1T where global = Module1T local = runModule1T update v w = w { Mod1Vars = v } extract = Mod1Vars instance Scope Mod2Vars Module2T where global = Module2T local = runModule2T update v w = w { Mod2Vars = v } extract = Mod2Vars localMod1 = local :: Module1T GlobalM x -> GlobalM x localMod2 = local :: Module2T GlobalM x -> GlobalM x sampleGlobalVars = GlobalVars sampleMod1Vars sampleMod2Vars sampleMod1Vars = Mod1Vars 1 2 sampleMod2Vars = Mod2Vars 3 4 main :: IO () main = do (`evalStateT` sampleGlobalVars) $ do localMod1 $ do modify $ \d -> d { mod1Field1 = mod1Field1 d + 10 } global $ do get >>= liftIO . print modify $ \d -> let d2 = Mod2Vars d in d { Mod2Vars = d2 { mod2Field1 = mod2Field1 d2 + 20 } } localMod2 $ do modify $ \d -> d { mod2Field2 = mod2Field2 d + 30 } get >>= liftIO . print
*1:って、undecidable とかどんな影響があるんでしょう。よくわかんないので自己責任で。