この記事は Haskell Advent Calendar 2017 (その1) の 10 日目の記事です。
スーパーモナド (supermonad) という気になる概念を見かけたので、ちょっと調べてみました。
スーパーモナドとは
世の中には、私たちがよく知っている普通のモナド1 以外にも、モナド的概念がいろいろと存在します。たとえば、こんなのがあります。2
- 指標付きモナド (indexed monad)
- 作用モナド (effect monad)
- 制約付きモナド (constrained monad)
スーパーモナドは、こうしたモナド的概念をすべて包含する概念 として提案されました。
スーパーモナドを提案した論文が、次の論文です。実装の話や、Agda で定式化した話なんかも載っています。
- J. Bracker and H. Nilsson. Supermonads: one notion to bind them all. In Proceedings of the 9th International Symposium on Haskell, pages 258-269, 2016.
実装は、supermonad パッケージ として提供されています。
スーパーモナドの定義
まずは普通のモナドのおさらいから。普通のモナドは次のように定義されているのでした。3
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
指標付きモナドの定義も覗いてみます。指標付きモナドは次のように定義されています。3
class IxMonad m where
ireturn :: a -> m i i a
(>>>=) :: m i j a -> (a -> m j k b) -> m i k b
そして、これらを一般化した概念であるスーパーモナドは、次のように定義されています。3
class Return m where
return :: a -> m a
class Bind m n p where
(>>=) :: m a -> (a -> n b) -> p b
クラスが Return
と Bind
の 2 つに分かれているのが特徴的です。また、バインド演算 (>>=)
も一般化されています。
スーパーモナドが普通のモナドの一般化であることは明らかです。スーパーモナドが指標付きモナドの一般化になっていることも、次のような対応がとれることからわかります。
-
Return
クラスのm
がIxMonad
クラスのm i i
に対応 -
Bind
クラスのm
,n
,p
がそれぞれIxMonad
クラスのm i j
,m j k
,m i k
に対応
なお、普通のモナドがモナド則を満たす必要があるように、スーパーモナドもスーパーモナド則を満たす必要があります。スーパーモナド則は、普通のモナド則の自然な一般化です。
スーパーモナドを使ってみる
スーパーモナドを使うには、supermonad パッケージ をインストールします。モジュールファイルの冒頭には、最低限、次のような宣言を書く必要があります。
{-# LANGUAGE RebindableSyntax #-}
{-# OPTIONS_GHC -fplugin Control.Supermonad.Plugin #-}
import Control.Supermonad.Prelude
また、コンパイルは GHC に -dynamic
オプションを指定して行います。4
ここから先は、Maybe
モナドと IxState
モナドをスーパーモナドとして扱うプログラムの例を紹介します。
例 1: Maybe モナド
Maybe
モナドのような普通のモナドについては、Control.Supermonad.Prelude
内に次のような instance
宣言が書かれています。
import qualified Prelude as P
instance Bind Maybe Maybe Maybe where
(>>=) = (P.>>=)
instance Return Maybe where
return = P.return
そのため、Maybe
モナドなどをスーパーモナドとして扱う際は、私たちは instance
宣言を書く必要はありません。
例として、(スーパーモナドを使わない)次のプログラムを、スーパーモナドで書き直してみます。
foo :: Maybe Int -> Maybe String
foo x = x >>= (\x -> Just (succ x))
>>= (\x -> Just (show x))
main = print $ foo (Just 0)
これをスーパーモナドで書き直すと、次のようになります。
{-# LANGUAGE RebindableSyntax #-}
{-# OPTIONS_GHC -fplugin Control.Supermonad.Plugin #-}
import Prelude hiding ((>>=), return)
import Control.Supermonad.Prelude
foo :: Maybe Int -> Maybe String
foo x = x >>= (\x -> Just (succ x))
>>= (\x -> Just (show x))
main = print $ foo (Just 0)
$ runghc -dynamic test.hs
[SM] Invoke (super) plugin...
[SM] Unification solve constraints...
Just "1"
例 2: IxState モナド
指標付きモナドである IxState
をスーパーモナドとして扱ってみます。IxState
モナドは、indexed-extras パッケージ に定義されています。
まずは、IxState
モナドを普通に使うバージョンから。このプログラムをコンパイルするには、indexed-extras パッケージ が必要です。
import Control.Monad.Indexed
import Control.Monad.Indexed.State
foo :: IxState Int String ()
foo = imodify succ >>>= \_ -> imodify show
main = print $ runIxState foo 0
スーパーモナドを使うようにこれを書き換えると、次のようになります。
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# OPTIONS_GHC -fplugin Control.Supermonad.Plugin #-}
import Prelude hiding ((>>=), return)
import Control.Supermonad.Prelude
import Control.Monad.Indexed
import Control.Monad.Indexed.State
instance Bind (IxState i j) (IxState j k) (IxState i k) where
(>>=) = (>>>=)
instance Return (IxState i i) where
return = ireturn
foo :: IxState Int String ()
foo = imodify succ >>= \_ -> imodify show
main = print $ runIxState foo 0
$ runghc -dynamic test1.hs
[SM] Invoke (super) plugin...
[SM] Unification solve constraints...
((),"1")
instance
宣言を書くことで、指標モナドもスーパーモナドとして扱えることが確認できました。