読者です 読者をやめる 読者になる 読者になる

Indexed Monadの世界

もっと、モナドの力を引き出したくはないか?

え?アクションの前後で型を変えたい?いやいやいや、モナドは自己関手の圏上の単なるモノイドだよ、そんなことができ…る…!?えっ、できるの…マジで…?

できる。そう、Haskellならね。

{-# LANGUAGE QuasiQuotes #-}
import Control.Monad.Indexed.State
import Control.Monad.Indexed
import Language.Haskell.IndexedDo

hoge :: IxState Int [Int] ()
hoge = [ido|do
    imodify (*10)
    imodify show
    imodify reverse
    imodify (++"123")
    imodify $ map fromEnum
    |]
*ghci> runIxState hoge 42
((),[48,50,52,49,50,51])

明らかに、計算の途中で型が変わっている! 勿論、型さえ合えばループや条件分岐を使うこともできる。

この何やら恐ろしいモナドのようなものが一体どういう仕組みになっているのか、じっくり調べてみよう。

IxStateの定義はこうだ。

newtype IxState i j a = IxState { runIxState :: i -> (a, j) }

Stateモナドのような形をしているが、これは状態の型が変わるため、Monadインスタンスにはできない。ならば…より多くの制約を持ったモナドを作ればよい。

class IxMonad m where -- indexedパッケージで提供されているクラスとは多少異なるので注意
    ireturn :: a -> m i i a
    ibind :: (a -> m j k b) -> m i j a -> m i k b

このiやjといったパラメータ(添字)によって、アクションが正しく合成されるということが保証できる。これがIndexed monadである。

すると、IxStateをそのインスタンスにできる。

instance IxMonad IxState where
    ireturn a = IxState $ \s -> (a, s)
    ibind f m = IxState $ \s -> let (a,s') = runIxState m s in runIxState (f a) s' 

しかし、足りないものがある。そう…do記法だ。Indexed monadの力をフルに使うために、indexed-do-notationというパッケージを作った。これは、普通のdo記法とまったく同じようにindexed monadを扱うことができる。使うには、普通のdo記法を[ido|…|]で包めばよい(QuasiQuotes拡張を有効にする必要がある)。

こうして我々は、モナドを超えるモナド、インデックスド・モナドを手に入れたのだ。

Free indexed monad

モナドに対してFreeモナドがあるように、インデックス付きモナドにもインデックス付きFreeモナドがある。早速作ってみよう。

{-# LANGUAGE GADTs #-}

data IxFree f i j x where
    Pure a :: a -> IxFree i i a
    Free :: f i j (IxFree f j k a) -> IxFree f i k a

パラメータが増えた点を除けば普通のFreeモナドと変わらない。それは実装も同じだ。

instance IxFunctor f => IxMonad (IxFree f i j) where
    ireturn = Pure
    ibind k (Pure a) = k a
    ibind k (Free f) = Free $ imap (ibind k) f

IxFunctorはFunctorのindexed版。fmapの代わりに、添字の関係を保存しつつ中身だけを変えるimapが定義されている。

class IxFunctor f where
   imap :: (a -> b) -> f i j a -> f i j b

そして、IxFreeは、IxFunctorによってIxMonadを生成できるのだ。

このIxFreeを使って、Coqのタクティクスのようなものを作ってみよう。ゴールの書き換えが添え字の変化に対応している。

{-# LANGUAGE QuasiQuotes, GADTs #-}
import Control.Monad.Indexed -- indexed
import Control.Monad.Indexed.Free -- indexed-free
import Language.Haskell.IndexedDo -- indexed-do-notation
import Control.Applicative
import Data.Void -- void

data Proof i j x where
    Intro :: (p -> a) -> Proof (p -> q) q a
    Apply :: (p -> q) -> a -> Proof q p a

instance IxFunctor Proof where
    imap f (Intro g) = Intro (f . g)
    imap f (Apply e a) = Apply e (f a)

intro :: IxFree Proof (a -> b) b a
intro = Free $ Intro ireturn

apply :: (a -> b) -> IxFree Proof b a ()
apply p = Free $ Apply p (ireturn ())

exact :: a -> IxFree Proof a b ()
exact p = Free $ Apply (const p) (ireturn ())

以下のように証明が書ける。当然、証明が間違っていると型エラーになる(ここ重要)。

ex0 :: IxFree Proof ((a -> b) -> (b -> c) -> a -> c) () ()
ex0 = [ido|do
    ab <- intro
    bc <- intro
    a <- intro 
    apply bc
    apply ab
    exact a
    |]

ex1 :: IxFree Proof ((Either a b -> Void) -> (a -> Void, b -> Void)) () ()
ex1 = [ido|do
    e <- intro
    exact (e . Left, e . Right)
    |]
    
ex1' :: IxFree Proof ((a -> Void, b -> Void) -> (Either a b -> Void)) () ()
ex1' = [ido|do
    (ha, hb) <- intro
    e <- intro
    case e of
        Left a -> exact $ ha a
        Right b -> exact $ hb b 
    |]

驚くべきことに、この証明から実際のHaskellのプログラムを作ることも可能だ

runProof :: Show r => IxFree Proof a b r -> a
runProof (Pure r) = error $ "Unexpected end of proof" ++ show r
runProof (Free (Intro f)) = runProof . f
runProof (Free (Apply a cont)) = a (runProof cont)
*ghci> :type runProof ex0
runProof ex0 :: (a -> b) -> (b -> c) -> a -> c
*ghci> runProof ex0 (+11) (*3) 11
66

いやはや、indexed monad恐るべし…

オレはようやくのぼりはじめたばかりだからな このはてしなく遠いモナド坂をよ…