準備
モナドを作るには、どんなモナドを作りたいか考える。モナドは一般に、どのようなアクションが使えるかによって特徴付けられる。その点ではオブジェクト指向におけるインターフェイスとよく似ている。
では、foo :: String -> M Bool
とbar :: M Int
という二種類のアクションを持つモナドを作るとしよう。まず、どんなアクションが使えるかを表すデータ型を定義する。
{-# LANGUAGE GADTs #-} data MBase x where Foo :: String -> MBase Bool Bar :: MBase Int
GADT(一般化代数的データ型)の各データコンストラクタがアクションに対応する。GADTsを使ったことがなくても心配してはいけない。引数の型と結果の型に着目しよう。
モナドにする
monad-skeletonをインストールする。
$ stack install monad-skeleton
モジュールをインポートし、先ほどのMBaseを使い、Mを定義する。bone :: t a -> Skeleton t a
でMBase
の値をM
の値にする。
{-# LANGUAGE GADTs #-} import Control.Monad.Skeleton data MBase x where Foo :: String -> MBase Bool Bar :: MBase Int type M = Skeleton MBase foo = bone . Foo bar = bone Bar
Mはもう、モナドになっている。拍子抜けするほど簡単だ。
:t bar >>= foo . show bar >>= foo . show :: Skeleton MBase Bool
モナドを使う
作っても使わなければ意味がない -- 誰か
アクションにdebone :: Skeleton t a -> MonadView t (Skeleton t) a
を適用すると、MonadViewというデータ型の値が得られる。
data MonadView t m x where Return :: a -> MonadView t m a (:>>=) :: t a -> (a -> m b) -> MonadView t m b
Return a
はそのアクションが実質的にreturn a
であること、t :>>= k
は最初に実行すべきアクションがt
であることを意味する。k
にt
の結果を渡すと、その次に実行すべきアクションが得られる。これらを使うと、Mを実際に解釈する関数を定義できる。ここでは、foo
を「与えられた文字列の長さがある値より短いか判定する」、bar
を「判定基準を返す」ものとして定義しているが、型さえ合えば実際はなんでもよく、一つのM
に対して複数の解釈を与えることさえ可能だ。
runM :: Int -> M a -> a runM n m = case debone m of Foo str :>>= k -> runM n $ k $ length str <= n Bar :>>= k -> runM n $ k n Return a -> a
monad-skeletonはDSL(ドメイン特化言語)を作るのに適している。何より、簡単に使えるのが売りなので、初心者にもおすすめだ。
runMのような「モナドを解釈する関数」に内部状態を持たせられたら便利なのに、と思う人もいるかもしれない。実はこれにも既に解決法があるので、次の機会に紹介しよう。