セッション型 on Haskell

セッション型は プロトコルを表現する型で, 15年くらい前から π計算の型理論の1つとして研究されてきました(文献リスト(一部)).並行・分散ソフトウェアの普及を鑑みるに,セッション型のような仕組みは次世代の型安全かつ表現力の高いプログラミングのための強力なツールとして役に立つと考えます.
しかしその直観的なわかりやすさにも関わらず,セッション型を提供しているプログラミング言語は皆無(おそらく…)です.
そこで,今 流行(?)の型レベル計算を駆使して Haskell にセッション型を導入してみました.

ダウンロード

晒してみる.
まだ α版 full-sessions ダウンロードページへ

  • test.hs を ghci でロードしてください いくつかの関数は 型を見たり runS で走らせたりできます.
  • test.hs の一部で ixdo 記法を使っているので, cabal install ixdopp するか,ixdo 記法を手で展開してください.
  • 実装は,以下の記事で説明する実装よりもすこし(かなり)複雑です (再帰,チャネル渡し(一部)をサポート)
  • これは型付かないの?というような質問は何でもどうぞ.

セッション型の例

例として次のHaskellプログラムを与える. これは私の実装でちゃんと型がつく:

proc1 = ixdo
  k <- new
  forkIO (ixdo send k (12345::Int); str <- recv k; io (putStrLn str))
  i <- recv k
  send k (show (i*10))

newは新しいチャネルを生成する.このチャネルでの通信は同期的であり,送信は相手が受信するまでブロックする.send k v はチャネルkを介した値vの送信, recv k はチャネルkを介した受信である. forkIO は新しいスレッドを開始する. ixdo は do記法の一種である.

このプログラムを実行すると,

123450

という結果が得られる.

上の例では,forkIO で分岐したスレッドが, Int型の値 12345 を送り,次に String型の値を受信する.一方,メインスレッドは 逆に Int型の値を受け取り,showで文字列表現に変換した String型の値を送信する.このような,状況に応じて異なる型を扱えるのがセッション型である.

このセッション型は,ある時点におけるプロトコルを表現する. 具体的には, send k (12345::Int) の直前では,チャネル k は セッション型 (Send Int (Recv String End)) をもつ. このsendの直後,recv k の直前 では,チャネル k は セッション型 (Recv String End) を持つ.ここで Send は送信を, Recv は受信を表す.

セッション型の性質

  1. チャネルは,正確に 2つのスレッドに所有されてから使われなければならない (チャネルは渡すこともできる)
  2. チャネルは,互いに セッション型がdualとなるように使わなければならない

1. により,たとえば

ixdo 
  k <- new
  send k True 

という式は拒否される (生成したチャネルが単一のスレッドにしか所有されないため).

ixdo 
  k <- new
  forkIO (send k True)

のようにして,2つのスレッドにチャネルが所有されるようにする.

2. により,たとえば

ixdo 
  k <- new
  forkIO $ send k True
  send k True

という式は拒否される (sendとsendはdualでないため).

このように,セッション型は2者間のチャネル通信に格段の柔軟さと安全性を両立する.

私の "セッション型 on Haskell" 基礎編

セッション型を Haskellの型レベル計算 で無理やり実現するにあたって, いくつかの型と型クラスを導入する.

(基礎1) indexed monad

私の実装は,indexed monad を使い,セッション型の制約がついた計算を表現する.
(indexed monad は, Hackage では category-extrasControl.Monad.Indexed で提供されている. が,べつに自分で実装してもよい). indexed-monad は,例えばOlegさんの限定継続の実装でも使われている.

indexed monad は,(私の理解では) 事前条件と事後条件を持つモナドだ.モナドにおなじみのみなさんはindexed monadを表現した次の型クラスで何かわかるかもしれない:

class IxMonad m where
  (>>>=) :: m i j a -> (a -> m j k b) -> m i k b
  ireturn :: a -> m i i a

普通のMonadクラスの型構築子と比べて型パラメータが2つ増えていることに注目されたい. 型 m i j a において, iが事前条件,jが事後条件を表すパラメータで, (>>>=) の型は, m >>>= f のような計算において m の事後条件と f の事前条件が一致しなければならず,さらに合成された計算における 事前条件が >>>= の左手の事前条件と, 事後条件が 右手の事後条件と一致しなければならないことを表している.
私のセッション型の実装では,この indexed monad によって, 「ある時点」におけるチャネル群のセッション型を管理している.

また,上の例で使っている ixdo 記法は,プリプロセッサ ixdopp (Tovらの実装) により,次の通り indexed monad の計算に展開される.

proc1 = 
  new >>>= \k -> 
  forkIO (send k (12345::Int) >>= \_ -> recv k >>>= \str -> io (putStrLn str)) >>>= \_ -> 
  recv k >>>= \i -> 
  send k (show i)

>>>= の演算子による計算は, Monadをご存知の方にはおなじみだろう.

(基礎2) Sessionモナド, 型環境と型レベルリスト

セッション型を扱う計算を表すモナドを Session モナドと名付けている. Session モナドは IOモナドのラッパーで,次の通り定義されている.

newtype Session ss tt a = Session (IO a)

セッションモナド Session ss tt a のパラメータ ss と tt は型レベルリストで,セッション型に関する整合条件を表す.

この型レベルリストは,次の型構築子で表現される:

-- |type-level list cons
data (:|:) hd tl
infixr 3 :|:
-- |type-level list nil
data Nil

この型レベルリストは,「どのチャネルがどのセッション型をもつか」を表している.たとえば, あるチャネルに対してInt型の値を送信する計算 m は次の型をもつ:

m :: Session (Send Int c:|:tt) (c:|:tt) ()

事前条件に セッション型 Send Int c が, 事後条件に セッション型(変数) c が現れ, この計算では あるチャネルに Int型の値が送信されることを示している.

(基礎3) チャネル型, 型レベル自然数

チャネルの型レベル表現として,型レベル自然数を用いる.

newtype Channel n = ...(MVarを使って適当に実装)

-- |type-level peano number n+1
data S n
-- |type-level peano number 0
data Z

チャネル型 Channel n における型パラメータn に型レベル自然数を埋め込む. 例えば型レベル表現0 を持つチャネルの型は Channel Z, 型レベル表現 n+2 を持つチャネルは 型 Channel (S (S n)) をもつ.

(基礎4) チャネルと型環境

チャネルとセッション型の対応付けは,

  型レベル表現nのチャネルのセッション型 = 型レベルリストの後ろからn-1番目

という関係をもつ.後ろからn番目,を表現するために,次の型クラス Length を用いる.

class Length tt l | tt -> l
(インスタンス略)

Length tt l は,型レベルリスト tt が長さ l を持つことを表現する.
例えば,チャネル k :: Length tt (S n) => Channel n があるとき, send k True :: Session (Send Bool c:|:tt) (c:|:tt) () のように型がつき,チャネル k は send k True で Bool型の値の送信に使われることを示している.

実践・セッション型推論

ためしてみよう

私のセッション型は, Haskellの型検査器, 例えば GHCi などで型推論させることができる(一部).たとえば,上の例 proc1 は,次の(ような)型をもつ:

proc1 :: Length ss l => Session t ss (End :|: ss) ()

あまり型がおもしろくないのは,チャネルが既に2つのスレッドで占有されているため,それ以上使えないからである.

次の式の型を見てみよう:

proc2 = ixdo
  k <- new
  forkIO (send k 123)
  ireturn k
Main> :t proc2
proc2 
  :: (Length tt l, Num t) => 
     Session tt (Cap Nil (Recv t End) :|: tt) (Channel l)

少し面白い型が出てきた.
この proc2 は, あるチャネルを生成して, そのチャネルに 整数値を送る.ここでのセッション型は Num t => Send t End である.
一方,計算の戻り値の型Length tt l => Channel l と,事後条件 (Cap Nil (Recv t' End) :|: tt) によって,この計算が返してくる チャネル k は Num t => Recv t End を満たすように使えばよいことがわかる.

この計算を実行してみよう.

Main> :t proc2 >>>= \k -> recv k >>>= \i -> io (print i)
proc2 >>>= \k -> recv k >>>= \i -> io (print i)
  :: (Length ss1 l) =>
     Session ss (Cap Nil End :|: ss) ()
	 
Main> runS $ proc2 >>>= \k -> recv k >>>= \i -> io (print i)
123

よし!

他こんなこともできる

proc2 = ixdo
  k <- new
  forkIO (send k (123::Int))
  ireturn k

proc2' k = forkIO $ recv k >>>= \i -> io (print (i::Int))

proc3 = ixdo
  k <- proc2
  k' <- proc2
  k'' <- proc2
  proc2' k
  proc2' k'
  proc2' k''
  ireturn ()
Main> runS proc3
121132233

そのほか

よりパワフルな機能

セッション型には,ほかに

  • 分岐
  • 再帰
  • チャネル渡し (高階セッション,委譲)

の機能がある.これらについて順次説明する.

TCP/IP 通信の展望

私は,セッション型はマルチスレッドプログラミングよりも TCP/IP 通信に非常によくフィットすると考えている. 例えば Neubauer らは SMTP プロトコルHaskellのセッション型(の一部の機能)で実装している. この方向に拡張することは今後の課題である.