Creatable a => a -> IO b

Haskellと数学とちょびっと音楽

Haskellオブジェクト指向に触れてみよう〜中級編〜

PRITZサラダ味は神、異論は認めない。

はいどうも、直接本題に入ったら負けだと思っているタイプのHaskeller、ちゅーんさんですこんにちは。 今日も前回に引き続き、「Haskellオブジェクト指向」と題しまして、objectiveを紹介したいと思います。

本題に入る前に

前回の記事に、objective開発者の@fumievalさんからコメント頂きました。

最新のobjectiveでは`stateful handle s where handle` …の代わりに、

{-# LANGUAGE LambdaCase #-}
stringObject :: String -> Object StringObject IO
stringObject s = s @~ \case
   GetString -> get
   SetString s -> put s
   PrintString -> get >>= liftIO . putStrLn

のような書き方が可能で、記述量がかなり減るので実際に使う場合はこちらがおすすめです。  

という事です。
((@~)演算子の存在には気づいていましたけど、そうか、こうやって使えば良いのか・・・)

あとsequentialについては、

`sequential`を使うと一度にたくさんのメッセージを送るメッセージカスケードが実現できます。
採用している言語は少ないですが、自分自身を返すメソッドのチェインより美しくかける上、
対象のインスタンスとは独立してメッセージを組み合わせられるため有用性が高いです。

という事です。

下手にメソッドチェインを使って、

human.birthday().birthday().birthday();

とするよりは、

human.- do
  birthday
  birthday
  birthday

のように書けたほうが綺麗ですし、「メソッドそのものを拡張出来る」という点で有用かもしれません。

中級編でやる事

  • 初級 - objectiveを使ってオブジェクトを実装でき、写経プログラミングで簡単な拡張が行える
  • 中級 - オブジェクトの合成を理解し、自在に拡張できる
  • 上級 - 定命のオブジェクトやストリーム等の高度な応用を使えるようになる
  • 番外 - オブジェクトの圏を理解し、圏論の言葉でオブジェクト指向を定式化できる

初級編では、基本的なオブジェクトの作り方や、オブジェクトを拡張する具体的なコードを紹介しました。

中級編ではぐっと本質に踏み込んで、objective自信の「オブジェクト」に対する考え方や、 オブジェクトの合成の説明を行い。 objectiveを使って、Java等のオブジェクト指向言語における「継承」と同等の拡張を行う方法について議論します。

オブジェクトとメッセージ

前回までは、特に良く知られたクラスベースオブジェクト指向言語の考え方に合わせるため、 オブジェクトに対する振る舞いを指示する操作を「メソッド呼び出し」と呼びましたが、 objectiveにおけるオブジェクトの基本的な考え方はメッセージパッシング方式です。

Object F Gという型は、「何処か」からFという型のメッセージを受け取ると、 自身の状態を変更して、Gという型のメッセージを「何処か」へ送ります。

尚、この時FGという型自体の事は、元論文でも「インターフェイス」と読んでいるようです。

Object HumanObject IOという型のオブジェクトは、 HumanObjectという型のメッセージを何処かから受け取ると、 IOというメッセージを何処かへ送信します。 (.-)演算子の役割は、オブジェクトのインスタンスに対して、 直接メッセージを送信し、受信したMonadIO m => mのメッセージを実行する事なのです。

以下のようなインターフェイスFGを考えましょう。

data F a where
  MessageX :: F ()
  MessageY :: F ()

data G a where
  MessageA :: G ()
  MessageB :: G ()

ひとまず、オブジェクトの「状態を保持する」という性質を忘れて、 受信メッセージを元に送信メッセージを作成するという性質に着目すると、 次のような関数を考える事ができます。

messagePassing :: F a -> G a
messagePassing MessageX = MessageA
messagePassing MessageY = MessageB

objectiveパッケージには、このような「受信メッセージを元に送信メッセージを作成する関数」を、 直接オブジェクトに変換するliftO関数が定義されています。

*Main> :t liftO
liftO :: Functor g => (forall x. f x -> g x) -> Object f g

しかし、GFunctor型クラスのインスタンスになっていないため、うまくオブジェクトにliftする事ができません。 メッセージはGADTsを使って定義される事が多いため、簡単にFunctorインスタンスにする事もできなさそうです。

そこで、Operationalを使い、messagePassing型の返却値の型をProgram G aとします。

messagePassing :: F a -> Program G a
messagePassing MessageX = singleton MessageA
messagePassing MessageY = singleton MessageB

こうすると、ProgramMonadであり、同時にFunctorでもありますから、 一度に複数のメッセージを送れるようになるだけでなく、 以下のように、liftOによってオブジェクトにする事ができるのです。

objX :: Object F (Program G)
objX = liftO messagePassing

Object F GFGがそれぞれメッセージである、という考え方に則ると、 liftOのFunctor制約は、暗にメッセージはFunctorでなくてはならないという事を述べています。

というと、少々小難しい話になってしまいますので、次の一文は最悪読み飛ばしてしまっても良いのですが・・・(´・ω・`)

FProgram Gも「メッセージ」ですから、気持ちとしてはFもFunctorであって欲しいわけですよね。
しかし結局の所Object (Program F) (Program G)というオブジェクトはsequential objXとする事で簡単に作ることができ、 このオブジェクトの成す事は本質的にobjXそのものと違い無いので、基本的に同じものと考えても良さそうです。
Program F -> Program Gという関数を作る事の面倒さを考えれば、このように定義したほうが楽ですし、さしあたりobjXの定義はこれで問題ありません。

これ以上突っ込むと、ちょっとアカデミックな空気感漂う話をしなくてはいけなくなってしまう気がするので、 そのへんはまた、機会があればするかもしれません。しない気もします。

はい、本題に戻りましょう。

messagePassingは、基本的にオブジェクトを作るために作成したものですから、 LambdaCase言語拡張を使って、以下のようにliftOに直接渡してしまったほうが綺麗ですね。

objX :: Object F (Program G)
objX = liftO $ \case 
    MessageX -> singleton MessageA
    MessageY -> singleton MessageB

尚、型の関係がわかりやすいため、本エントリでは次のように書くようにします。

objX :: Object F (Program G)
objX = liftO handle
  where
    handle :: F a -> Program G a
    handle MessageX = singleton MessageA
    handle MessageY = singleton MessageB

オブジェクトの合成

objectiveによるオブジェクト指向がメッセージパッシングであるという説明をした際に、 オブジェクトは「何処か」からメッセージを受け取り「何処か」へとメッセージを送信するという話をしましたが、 この「何処か」というのは、自分以外の別のオブジェクトの事を表しています。

前の章で定義したobjXオブジェクトは、Fというメッセージを受け取りGというメッセージをを送るわけですが。 メッセージの型を辺とすると、次のような図で表す事ができます。

            objX
      F ------------> G

続いて、Object G IOという型を持ったobjYを定義します。

data G a where
  MessageA :: G ()
  MessageB :: G ()

objY :: Object G IO
objY = liftO handle
  where
    handle :: G a -> IO a
    handle MessageA = putStrLn "MessageA"
    handle MessageB = putStrLn "MessageB"

このオブジェクトを先ほどの図に加筆しましょう。

            objX             objY
      F ------------> G ------------> IO

このように並べると、objXFのメッセージを受け取って、GのメッセージをobjYに対して送信する事が可能で、 全体としてFのメッセージを受け取り、IOのメッセージを送信する大きな流れが出来ている事がわかります。 この流れは、全体としてFを受信メッセージ、IOを送信メッセージとする新たなオブジェクトに見えるでしょう。

実際に、そのような合成を行うのが、(@>>@)演算子です。

Main> :t (@>>@)
(@>>@) :: Functor h => Object f g -> Object g h -> Object f h

objectiveではこの演算の事をオブジェクトの合成と呼んでいます。

オブジェクトの合成を具体的なコードにして見てみましょう。 objXが送信するメッセージの実際の型はProgram Gなので、 sequential関数でobjYの受信メッセージをモナドにする必要があります。

objZ :: Object F IO
objZ = objX @>>@ sequential objY

main :: IO ()
main = do
  y <- new objY
  y.-MessageA
  y.-MessageB

  putStrLn "----"
  z <- new objZ
  z.-MessageX
  z.-MessageY

実行結果:

MessageA
MessageB
----
MessageA
MessageB

基本的には、前回Sum(@||@)を使って行ったようなメッセージの合成と オブジェクトの合成を組み合わせる事によって、 さまざまな拡張が可能になるというのが、objectiveの骨子になります。

状態を扱うオブジェクト

さて、内部状態を持つという事は、オブジェクトには欠かすことの出来ない要件です、 前回、stateful関数を使って状態を扱うオブジェクトを作りましたが、 ここでは少しだけこのstateful関数の仕組みを分解してみましょう。

次のような、送信メッセージがStateT Int IOという型となるオブジェクトを考えてみます。

data S a where
  GetS :: S Int
  SetS :: Int -> S ()
  PrintS :: S ()

objS :: Object S (StateT Int IO)
objS = liftO handle
  where
    handle :: S a -> StateT Int IO a
    handle GetS = get
    handle (SetS x) = put x
    handle PrintS = get >>= liftIO . print

StateT s IO aMonadIOインスタンスですから、objSはこのままでも次のようにして使う事ができます。

main :: IO ()
main = runStateT stateInt 100 >>= print

stateInt :: StateT Int IO ()
stateInt = do
  s <- new objS
  s.-PrintS
  s.-SetS 200
  s.-PrintS
  put 300
  s.-PrintS

実行結果:

100
200
300
((),300)

objSというオブジェクトは、自分のインスタンスが使われている文脈に直接手を入れて状態を操作します。 何故このような事が起こるかと言えば簡単で、objSはそれ自身が内部状態を持っているわけではなく、 あくまでSというメッセージを受け取るとそれに応じてStateTのメッセージを送信しているだけだからです。

もちろん、Readerモナド等と組み合わせればこのような仕組みが有用な場合もあるかもしれませんが、 基本的にオブジェクトの状態はカプセル化され、スコープはオブジェクトで完結しているべきでしょう。

直接内部状態を書き換える事ができるオブジェクトを作るには、Object型自体の仕組みを理解している必要がありますが、 実際には自分で作るまでもなく、objectiveパッケージで用意されています。

*Main> :t variable
variable :: Monad m => s -> Object (StateT s m) m

このオブジェクトはStateTメッセージが送られてくると、自分自身の状態を書き換える事で、カプセル化を行います。

試しに単体で動かしてみましょう。

main :: IO ()
main = do
  v <- new $ variable "Hoge"
  -- vのインスタンスにStateTをメッセージとして送信
  v.- do
    x <- get
    put $ "~~~" ++ x ++ "~~~"
  -- 先ほど送ったメッセージにより、
  -- vの内部状態が書き換えられている
  a <- v.-get
  putStrLn a

実行結果:

~~~Hoge~~~

そして、先ほど作成したobjSと、variableオブジェクトを以下のように合成する事によって、 Sをメッセージとして受け取り、IOをメッセージとして送信するオブジェクトが定義できるのです。

objT :: Object S IO
objT = objS @>>@ variable 0

objTの使い方を説明する必要は、もうありませんね。 RankN多相の知識が必要にはなってしまいますが、variableオブジェクトを使えば、stateful関数の再実装も簡単です。

stateful' :: (Functor m, Monad m) 
  => (forall a. f a -> StateT s m a) -> s -> Object f m
stateful' f x = liftO f @>>@ variable x

echoとオーバーライド

objectiveパッケージには、受信メッセージをそのまま返すechoという関数が用意されています。

*Main> :t echo
echo :: Functor f => Object f f

このオブジェクトは、送られてきたメッセージをそのまま送信するだけです。

main :: IO ()
main = do
  -- IO上でインスタンス化されれば、
  -- 単純にIOをメッセージとして受け取り、そのまま送信するため、
  -- 以下のコードはただ単にHello, Worldを実行する
  e <- new echo
  e.-do
    putStrLn "Hello, World!"

次に、話を進めるために、 本エントリで最初に紹介したobjXおよびobjYを再掲します。

data F a where
  MessageX :: F ()
  MessageY :: F ()

data G a where
  MessageA :: G ()
  MessageB :: G ()

objX :: Object F (Program G)
objX = liftO $ \case 
    MessageX -> singleton MessageA
    MessageY -> singleton MessageB

objY :: Object G IO
objY = liftO handle
  where
    handle :: G a -> IO a
    handle MessageA = putStrLn "MessageA"
    handle MessageB = putStrLn "MessageB"

objZ :: Object F IO
objZ = objX @>>@ sequential objY

ここに、受信メッセージも送信メッセージもFである、objFを導入する事を考えましょう。
受信メッセージのFと送信メッセージのProgram Fは同一視しても良いのでしたね。

objF :: Object F (Program F)
objF = liftO handle
  where
    handle :: F a -> Program F a
    handle MessageX = do
    -- MessageXを二回送信
      singleton MessageX
      singleton MessageX
    -- MessageX以外のメッセージはそのまま
    handle t = singleton t

このオブジェクトとobjZを合成する事で、オブジェクトのインターフェイスをそのままに、 既存のオブジェクトの動作が書き換えられます。

この働きは、まさにメソッドのオーバーライドです。

objZ' :: Object F IO
objZ' = objF @>>@ sequential objZ

main :: IO ()
main = do
  z1 <- new objZ
  z2 <- new objZ'
  invoke z1
  invoke z2
    where
      invoke :: Instance F IO -> IO ()
      invoke z = do
        putStrLn "----"
        z.-MessageX
        z.-MessageY

実行結果:

----
MessageA
MessageB
----
MessageA
MessageA
MessageB

ところで、echoとオーバーライドを一度に説明したのには理由があります。

このような、送信メッセージと受信メッセージが同じオブジェクトには、もうひとつ興味深い特徴があるのです。 それは、自分自身を何度も合成する事ができるという事です。
(このような特徴に目を向けるのは、HaskellHaskellらしく書く上でとても重要な事ですね。)

objF' :: Object F (Program F)
objF' = objF @>>@ sequential objF @>>@ echo @>>@ sequential objF

さて、当たり前の事のようですが、echoはいくら合成させても合成させたオブジェクトとまったく同じ振る舞いをします。 そして、(@>>@)演算子結合則を満たします。(詳しい証明は元論文の付録を参照してください)

即ち、送信元と送信先のメッセージが同じオブジェクトは、モノイドとしての性質を持っているのです!

継承の実現

さて、これでobjective上で継承を実現する道具立てが揃いました。 まず、親となるオブジェクトを次のように定義しましょう。

data A a where
  SetA :: String -> A ()
  GetA :: A String
  PrintA :: A ()

objA :: Object A IO
objA = stateful handle ""
  where
    handle :: A a -> StateT String IO a
    handle (SetA s) = put s
    handle GetA = get
    handle PrintA = get >>= liftIO . putStrLn . ("PrintA : "++)

objAを親として作成するobjBは、Aのメッセージだけでなく、 以下に示すBのメッセージも受信できるようにします。

data B a where
  PrintB :: B ()
  PrintA2 :: B ()

従って、objBの型は次のようになりますね。

objB :: Object (Sum A B) IO

今回は、話を簡単にするために、Aのメッセージのオーバーライドは行わない事にします。

PrintBでは単純なIO処理を行いましょう。 PrintA2インターフェイスBの要素ですが、 親オブジェクトとなるobjAPrintAメッセージを二回呼ぶ事にします。

少々複雑な手順を踏む必要があるので、 いきなり全体を作ろうとするのではなく、順番に考えていくと良いです。 まずはBのメッセージそれぞれの振る舞いだけ定義したオブジェクトを作って、 最終的に合成する事を考えましょう。

受信メッセージはBで問題無いとして、送信メッセージはどうなっているべきでしょうか。 objBに新たに追加されるメッセージはそれぞれ、 親オブジェクトのメッセージPrintAと、IOのメッセージのどちらも送信できなくてはいけません。 従って、Sum A IOが送りたいメッセージの型です。

当然、SumはFunctorではないのでliftO出来ませんし、Monadでも無いので何度もメッセージを送ることが出来ませんので、 Operationalの力を借りる必要があります。 結果、次のような部品が出来ます。

mg :: Object B (Program (Sum A IO))
mg = liftO handle
  where
    handle :: B a -> Program (Sum A IO) a
    handle PrintB = do
      x <- singleton $ InL GetA
      singleton . InR . putStrLn $ "PrintB : " ++ x
    handle PrintA2 = do
     singleton $ InL PrintA
     singleton $ InL PrintA

さて、このオブジェクトを上手く合成して、objBの型を満足させる事を考えます。 まず、受信メッセージをSum A Bにするためには、前回の記事で定義した@||@演算子が使えそうですね。

(@||@) :: Functor m => Object f m -> Object g m -> Object (Sum f g) m

これを使うためには、Object A (Program (Sum A IO))という型のオブジェクトが必要です。 もっとも、Aの操作を変えたいわけではないので簡単です。

mf :: Object A (Program (Sum A IO))
mf = liftO handle
  where
    handle :: A a -> Program (Sum A IO) a
    handle x = singleton $ InL x

mfmgの受信メッセージを合成したオブジェクトを、objA'として定義します。

objA' :: Object (Sum A B) (Program (Sum A IO))
objA' = mf @||@ mg

あとはこの、objA'を何か別のオブジェクトと合成して、 最終的にIOを送信するように出来れば、目的のobjBが作成できそうですね。

そもそも今回の目的はobjAを継承して新たなオブジェクトを作る事でしたから、 mfmgの送信するAのメッセージはobjAに送られる事を想定しています。 一方、IOのメッセージは、特に何かしちあわけではありません。 送ったものをそのまま送りなおしてくれれば良いのですが、 そのような働きをするオブジェクトがありましたね。そう、echoの事です。

これをそのまんま定義すると、次のようなobjA''オブジェクトとなります。

objA'' :: Object (Sum A IO) IO
objA'' = objA @||@ echo

後は、objA'objA''を合成すれば、最終目標のobjBが完成です。

objB :: Object (Sum A B) IO
objB = objA' @>>@ sequential objA''

このobjBは、ちゃんとobjAの動作を引き継ぎつつ、 新たに追加されたBのメッセージも、ちゃんと期待通りに動作します。

main :: IO ()
main = do
  a <- new objA
  a.-SetA "Hoge"
  a.-PrintA 
  
  putStrLn "----"
  b <- new objB
  b.-InL (SetA "Piyo")
  b.-InL PrintA 
  b.-InR PrintB
  b.-InL (SetA "Fuga")
  b.-InR PrintA2

実行結果:

PrintA : Hoge
----
PrintA : Piyo
PrintB : Piyo
PrintA : Fuga
PrintA : Fuga

実際には、継承に伴って新たな変数(状態)の追加を行いたい場合も多いでしょうから、 もう少々複雑になるかもしれません。

いまいち手順を整理し切れない方は、次の図を見て考えてみると良いでしょう。 (汚い手書きで申し訳ないです。あと裏面の落書きは親戚のようぢょに描いてあげてたミ◯ーちゃんです。)

f:id:its_out_of_tune:20150330044342j:plain

赤い点線が@||@による受信メッセージの合成を表します。

尚、この継承はobjAobjBの間に多相性が無いという意味で不完全です。 しかしこの問題は、現在Sumとなっている部分がより良い方法に置き換わる事により、 解決されるでしょう。

まとめ

というわけで、中級編では、objectiveにおけるオブジェクト指向は、メッセージパッシングであること。 メッセージの送受信先を定める「オブジェクトの合成」という演算があり、 オブジェクトの合成により、大きなオブジェクトを構築していく事ができるのだと言うこと。 オブジェクトの合成とSum等を組み合わせて、オブジェクトの継承を行う方法等を説明しました。

くどいようですが、objective自体はまだ研究段階ですので、使い勝手という点ではこれからどんどん改善されて行くでしょう。 それよりも、ここまで説明した範囲でも、以下のような凄み(という言い方はアレですが)があると考えています。

  • 大きくわけて2つの演算で、OOPの要求に答えている
    • オブジェクトの合成とインターフェイスの合成
    • 必要な拡張に対して最小の道具立てを選択できる
    • 意味論との親和性がめちゃんこ高い
      • 今までの感覚だとOOPの定式化とかほぼ無理っぽいし・・・
      • 圏論的な話を、機会があれば。
  • 状態の拡張がファーストクラス
    • その気になれば動的にどんな拡張でも出来そう
      • 動的に継承できる静的型付けOOPLなんて今まで無かった
      • オブジェクトのリストの全要素を一斉にオーバーライドとか
  • Haskellの型システムの上で動作する
    • なんかもう、これだけでもやばい

今後書くかもしれない上級編では、Object型の内部実装についてさらに追求し、 定命のオブジェクト、ストリームの表現等、高度な応用について説明しようと思います。

それでは皆様、良いOOPライフをノシノシ