今のところ比較的簡単なモナドの作り方

準備

モナドを作るには、どんなモナドを作りたいか考える。モナドは一般に、どのようなアクションが使えるかによって特徴付けられる。その点ではオブジェクト指向におけるインターフェイスとよく似ている。

では、foo :: String -> M Boolbar :: 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 aMBaseの値を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であることを意味する。ktの結果を渡すと、その次に実行すべきアクションが得られる。これらを使うと、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のような「モナドを解釈する関数」に内部状態を持たせられたら便利なのに、と思う人もいるかもしれない。実はこれにも既に解決法があるので、次の機会に紹介しよう。