Effモナドの効果のうち1つだけをrunする

Posted on 2017/07/09
Tags: [Haskell]

 例えばこのようなpartialContext関数とtotalContext関数があるとき

partialContext :: forall s. (Member (Reader Logs) s, SetMember Lift (Lift IO) s) => Eff s ()

partialContext :: Eff (Reader Logs :> Lift IO :> Void) ()
totalContext :: Eff (Writer Logs :> Reader Logs :> Lift IO :> Void) ()

partialContext内でtotalContextを走らせることができる。 (今回の試みではpartialContext
partialContext :: (Member (Reader Logs) s, SetMember Lift (Lift IO) s) => Eff s ()
のように多相化することには失敗していて、ただしtotalContextは以下のように多相化してもよい
totalContext :: (Member (Writer Logs) r, Member (Reader Logs) r, SetMember Lift (Lift IO) r) => Eff r ()

コード(実例)

output

["nozomi","eli"] was added
(["nozomi","eli","nico","maki"],())
()

実例

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeOperators #-}

import Control.Eff (Eff, Member, run, (:>), SetMember)
import Control.Eff.Lift (Lift, lift, runLift)
import Control.Eff.Reader.Lazy (Reader, ask, runReader)
import Control.Eff.State.Lazy (State, get, put, runState)
import Control.Eff.Writer.Lazy (Writer, tell, runWriter, runMonoidWriter)
import Data.Void (Void)

type Logs = [String]

-- Writer Logs, Reader Logs, Lift IOを使う文脈があるじゃろ
totalContext :: (Member (Writer Logs) r, Member (Reader Logs) r, SetMember Lift (Lift IO) r) => Eff r ()
totalContext = do
  logs <- ask
  tell (logs :: Logs)
  lift . putStrLn $ show logs ++ " was added"

-- そのうちReader Logs, Lift IOのみを使う文脈がある
--partialContext :: forall s. (Member (Reader Logs) s, SetMember Lift (Lift IO) s) => Eff s ()
partialContext :: Eff (Reader Logs :> Lift IO :> Void) ()
partialContext = do
  x <- runWriter' totalContext
  lift $ print x
  where
    -- そうすると、なんとWriter Logs効果だけを消費することができるのじゃ!
    --TODO: totalContextを単相化する必要があるので、それを含むpartialContextを多相化できない。どうすればいい?
    runWriter' :: Eff (Writer Logs :> s) () -> Eff s (Logs, ())
    runWriter' = runWriter (++) ["nico", "maki"]

main :: IO ()
main = do
  x <- runLift $ runReader partialContext ["nozomi", "eli"]
  print x

解説

 まずここではEff型コンストラクタの第一引数に指定される型を効果と呼んでいる🐕
(Monad……と呼称するのが一番しっくりくると思うけど、残念ながら現時点でextensible-effectsのWriterReaderはMonadインスタンスになってないので!)

で、partialContextは効果の数が少ないという意味でtotalContextよりも小さい。
なのでtotalContextの効果のうちpartialContextの持っていないWrite Logs効果を引いてしまえば、 partialContextの効果とtotalContextの持つ効果が等しくなるので、同じ文脈として(同じEff aモナドとして)使えるのではないかと思った。 そして、やったらできた。

 件の、効果の引き算になっているのはこれ

runWriter' :: Eff (Writer Logs :> s) () -> Eff s (Logs, ())
runWriter' = runWriter (++) ["nico", "maki"]

引き算の性質は型に表れていて、Writer Logs :> sからWriter Logsを引いている。 なのでrunWriter' totalContext

runWriter' totalContext :: (Member (Reader Logs) s', SetMember Lift (Lift IO) s') => Eff s' (Logs, ())

という型になって、単相化するとこれはpartialContextの文脈(Eff aモナドの形)と等しいので、

runWriter' totalContext :: Eff (Reader Logs :> Lift IO :> Void) (Logs, ())
--                         ^==================================v
partialContext          :: Eff (Reader Logs :> Lift IO :> Void) ()

runWriter' totalContextpartialContextの中で<-できる! (partialContextが単相的なので、runWriter' totalContextは型推論により単相化される)

何に使えるの?

base :: (Member (State Foo) r, SetMember Lift (Lift IO) r) => Eff r ()

という文脈がある中で、局所的に失敗する文脈を扱える。 (以下は、局所的に失敗する文脈child

child :: (Member (Exc String) r, Member (State Foo) r, SetMember Lift (Lift IO) r) => Eff r ()

(と言いたいんだけど実際は、実用するにはbaseを単相化する必要があって……誰かbase…… 実例におけるpartialContext……を多相的なままにしておける方法知らない?)



この記事はこちらから修正リクエストを送ることができます。
Effモナドの効果のうち1つだけをrunする - github
ゴミ箱ボタンの左にある、鉛筆ボタンを押してね!