Haskellでオブジェクト指向

2015/9/13 関数プログラミング交流会
by ちゅーん(@its_out_of_tune)

自己紹介

  • 野生のHaskller(28♂)
  • 東京都近郊に生息(休職中)

  • クルージング(スケボー)
  • ボルダリング

  • スプラトゥーン
  • えいえんのB帯ガール

HN: ちゅーん

Twitter:
 @its_out_of_tune
Github:
 tokiwoousaka

自己紹介

  • Takahashi Monad:
     スライド作成用の言語内DSL
     本スライドもこれで作成
  • Sarasvati:
     永遠に開発中のオーディオインターフェイス
     PortAudioのラッパーのラッパー
  • ブログ:http://tune.hateblo.jp/
     ポーカーゲームを作る連載やってます

そもそもの話

Haskellという言語

  • 純粋だとか
  • ラムダ計算だとか
  • 型システムだとか
  • 圏論的にはどーだとか

  • こ難しい事はさておき・・・

Haskellという言語

まぁ
とにかくすごい

Haskellと状態

  • Haskellはすごい
  • でも状態を扱うのは苦手?
  • そこでモナドですよ!

  • モナドとか良くわからん(´・ω・`)

モナドは手続き

「たまたま」モナドという数学上の概念が
手続きプログラミングに「欲しい」性質を
持っているのでそれを応用しているだけ。

詳しくは後述

モナドは手続き

  • Haskellで状態を扱える
  • スコープの制御も自由自在
  • 実態はただのデータ構造
  • →つまり手続きがファーストクラス!
  • →しかも多相!!!

モナドは手続き

モナドすげぇ!

何が問題?

  • ゲームの仕掛けやキャラクター
  • GUIのコンポーネント
  • 音/絵など副作用の制御構造
  • とかとか

  • オブジェクトが有効なシーンも無くはない

オブジェクト指向(OOP)とは

  • モノとモノが云々
  • メッセージがメソッドが
  • 継承関係がどーとかこーとか
  • 動物クラスに猫クラス
  • アラン・ケイがなんたら
  • ストラウストラップがどうたら

オブジェクト指向(OOP)とは

なるほどわからん
(´・ω・`)

実際の所

  • プログラマの数だけOOPがある
  • 歴史/本質の話は数あれど・・・
  • 何か共通のコンテクストはある?

  • 下手なこと言うと論争になるよねorz

実際の所

よし
不毛な話はやめよう

OOPLにありがちな機能

  • クラスとインスタンス
  • メソッド呼び出し
  • 手続き/制御構造
  • 抽象クラスと継承
  • インターフェイスと実装

  • ・・・ってあれ?

OOPLにありがちな機能

オブジェクト指向に
必要なものって
やったら多くね?

今日やる事



我々が「欲しい」と考える
OOPLの機能を得るための
Haskellらしい仕組みを考える

モナドと手続き

Haskellらしい仕組み?



まず、手続きを導入する手段として、
モナドがもてはやされている理由について、
考えてみよう。

Why Monad is best

  • 多相だから?←まぁあってる
  • ファーストクラスだから?←ちげぇねぇ
  • シンプルだから?←異論ありそう
  • 数学的だから?←注目!!!

Why Monad is best



「数学」という言葉を使うとまた荒れそうなので
「形式的」という言葉に言い換えます

ここでいう「形式的」は
「簡単な定義の組み合わせで曖昧性/矛盾なく説明できる」
くらいの意味です

皆様

突然ですが、
手続きプログラミングを
説明できますか?

皆様

そもそも

皆様

「手続き」って
何なんすか(´・ω・`)

Haskellの上の手続き

Haskell上では普通に、
手続きプログラミングが出来る。

main :: IO ()
main = do
putStrLn "ファーストネームを入力してね"
firstName <- getLine
putStrLn "ラストネームを入力してね"
lastName <- getLine
let name = firstName ++ " " ++ lastName
putStrLn $ "こんにちは、" ++ name ++ "さん。"

Haskellの上の手続き

手続き自体がプリミティブな機能というワケでは無い
モナドによって実現されている。

class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a

-- モナド則
return x >>= f == f x
m >>= return == m
(m >>= f) >>= g == m >>= (\x -> f x >>= g)

Haskellの上の手続き

先のコードは以下のような
純粋な関数の構文糖になっている。

main :: IO ()
main =
putStrLn "ファーストネームを入力してね" >>
getLine >>= \firstName ->
putStrLn "ラストネームを入力してね" >>
getLine >>= \lastName ->
let name = firstName ++ " " ++ lastName in
putStrLn ("こんにちは、" ++ name ++ "さん。")

手続きとは

そもそも手続きとは何か、
改めて整理しよう。

手続きとは

①手続きは命令を並べたもの
命令は次の命令に影響を与えるかもしれない。

Aする
Bする
Cする

手続きとは

②各命令は引数を取るかもしれない。
各命令は値を返すかもしれない。

Aする 引数
返却値 = Bした結果
Cする 返却値

手続きとは

③「何もしない」命令も存在する。

Aする
Bする
サボる
Cする

手続きとは

④「何もしない」命令は、
書かなくても結果に影響は無い。

Aしてサボる = Aする
サボってからAする = Aする

手続きとは

⑤命令を大きな命令にまとめた時、
どんな塊でまとめても結果には影響ない。

AしてBする
Cする



Aする
BしてCする

も一緒

手続きとは

以上を「手続きの要件」と定義すれば、
天下り的ですが、違和感は無いでしょう。

すると(トートロジーですが)この定義は、
モナドの定義と完全に一致するのです。

型に置き換えてみる

①手続きは命令を並べたもの
命令は次の命令に影響を与えるかもしれない。
②各命令は引数を取るかもしれない。
各命令は値を返すかもしれない。

-- 命令を
a -> m b
-- とすると、a が引数 b が返り値、mが次の命令に与える影響となる
(>>=) :: m a -> (a -> m b) -> m b
-- とすると、
Aした結果 >>= Bする :: Bした結果
-- となり、どんどん並べる事ができる。

型に置き換えてみる

①手続きは命令を並べたもの
命令は次の命令に影響を与えるかもしれない。
②各命令は引数を取るかもしれない。
各命令は値を返すかもしれない。

引数がいらない場合は、
受け取った引数を捨ててしまえば良い。
返却値がいらない場合は、
意味の無いUnit型の値を返せば良い。

型に置き換えてみる

③「何もしない」命令も存在する。
④「何もしない」命令は、
書かなくても結果に影響は無い。

return :: a -> m a
--を何もしない命令とする。

--モナド則
return x >>= f == f x および m >>= return == m
--により、returnは何もしてはいけない

型に置き換えてみる

⑤命令を大きな命令にまとめた時、
どんな塊でまとめても結果には影響ない。

-- モナド則
(m >>= f) >>= g == m >>= (\x -> f x >>= g)

型に置き換えてみる

完全に一致

もう一つのモナドの定義

(>>=)を使ってjoin関数を実装できる。
joinとfmapで(>>=)を実装できる。

join :: Monad m => m (m a) -> m a
join x = x >>= id

(>>=) :: Monad m => m a -> (a -> m b) -> m b
x >>= f = join (fmap f x)

もう一つのモナドの定義

Monadは次のように定義しても意味は変わらない。
証明も可能だが今回は割愛。

class Functor m => Monad m where
join :: m (m a) -> m a
return :: a -> m a

--モナド則
join . fmap join = join . join
join . fmap return = join . return = id
return . f = fmap f . return
join . fmap (fmap f) = fmap f . join

モナドの定義は形式的

①何か型をつくる
②Monadクラスのインスタンスにする
③ちゃんとモナド則を満たす事を確認する
④その型は例外なくモナドです

モナドの定義は形式的



こう言い切ってしまえば、
「モナドとは何か」なんて議論する余地は、
本来無いはずなのですよっ(`・ω・´)

とにかく

「モナド」とは唯一
手続きを形式的に説明する事に
成功した概念である

とにかく

たぶん

ちなみに

型の性質について議論するにあたり、
「実装されている事」と「実装可能な事」の間に
根本的な違いは無い。例えば・・・

何かデータ型を定義した時、そのデータ構造に対して
モナド即を満たすインスタンスを「実装可能」ならば
そのデータ型をモナドであるとしても差し支えない。

形式的である事の強み

【論争を産まない】
実情として、「モナドとは何か」という議論は、
常に学習者に対する説明の試行錯誤として行われている
(識者にとっては自明過ぎてむしろ説明が難しい)

【研究しやすい】
それが何であるか明確なため、
対象が持つ性質を探るのが容易

形式的である事の強み

【応用しやすい】
よく性質の知られた概念であれば、
その性質を利用した大胆な応用も容易い

【語彙が増える】
よく形式化された語彙を共有する事により
さらに複雑な概念を簡潔かつ正確に
説明する事ができるようになる

何が言いたいのか

OOPとは何かという議論は必要無い

ひと通りの有用なOOPの機能を実現するために
形式的でエレガントな
最小限の定義を考える事が、
「Haskellらしい」OOPである

オブジェクトの定義

圏論

頂点A, B, Cを「対象(Object)」と呼ぶ。
下の図の、矢印f, gを「射(Arrow)」と呼ぶ。

f g
A ------> B ------> C

圏論

射は合成する事が出来る。

g○f
+-------------------+
| |
| f g v
A ------> B ------> C

圏論

射には恒等射が存在する。

Id_A
+---------+
| |
| |
A <------ +

圏論

射は結合則を満たす。
AからDへのどのルートを辿っても全て等価。

g○f
+-------------------+
| |
| f g v h
A ------> B ------> C ------> D
| ^
| |
+-------------------+
h○g

圏論

恒等射は、AからBどのルートを辿ってもfと等価。

Id_B
+ ------- +
| |
f | |
+ ------> A ------> B <------ +
| |
| |
+ ------- +
Id_A

圏論

  • 型が対象
  • 関数が射
  • 関数合成が射の合成
  • id関数が恒等射

のように当てはめる事で、
関数プログラミングの型は圏になる。

自己関手

関手は圏から圏への変換
ある圏から同じ圏への関手を自己関手と呼ぶ

HaskellのFunctorは型の圏から型の圏への自己関手

class Functor f where
fmap :: (a -> b) -> f a -> f b

--Functor則
fmap id == id
fmap (g.f) == fmap g . fmap f

自己関手

よーわからんよねー(´・ω・`)

とにかくあれ、モナドの時と同じようにfmapが定義できて、
Functor則を満たせば良いんです。

class Functor f where
fmap :: (a -> b) -> f a -> f b

--Functor則
fmap id == id
fmap (g.f) == fmap g . fmap f

自然変換

自然変換は関手から関手への変換

Haskellにおける自然変換とは、mとnを任意の関手とした時に
「自然性」と呼ばれる規則を満たす関数natの事

nat :: m a -> n a

--自然性
nat . fmap f == fmap f . nat

自然変換

自然性は以下のような図で考えると良い

nat
F A --------> G A
| |
| fmap f | fmap f
| |
v nat v
F B --------> G B

ミーリ・マシン

入力aを受けると出力bを返し、
自身の状態を書き換えるオートマトンの一種。

newtype Mealy a b = Mealy
{ runMealy :: a -> (b, Mealy a b) }

ミーリ・マシン

以下のような演算子とサンプルの実装を用意すれば

(.!) :: MVar (Mealy a b) -> a -> IO b
m .! i = modifyMVar m $ \o -> do
(res, next) <- return $ runMealy o i
return (next, res)

strBuffer :: Mealy String (Int, String)
strBuffer = f (0, "")
where
f :: (Int, String) -> Mealy String (Int, String)
f (i, s) = Mealy $ \t -> let n = (i + 1, s ++ t) in (n, f n)

ミーリ・マシン

IO上で状態が書き換えられてく様を確認出来る

sample :: IO ()
sample = do
inst <- newMVar strBuffer
res <- inst .! "Hoge"
print res -- (1, "Hoge")
res <- inst .! "Piyo"
print res -- (2, "HogePiyo")
res <- inst .! "Fuga"
print res -- (3, "HogePiyoFuga")

ちょっと復習

  • モナド:手続きを実現する形式的な構造
  • すべてのモナドはFunctor(関手)でもある
  • 関手:Functor則を満たすfmapが実装出来る
  • 自然変換:関手から関手への変換、自然性を満たす
  • ミーリマシン:入力/出力を返し自身の状態を持つ

これらの道具立てで
OOPの機能を形式的に実現できないだろうか

オブジェクトを表す型

@fumievalさんによる、
HaskellでOOPを実現するライブラリによる定義

newtype Object f g
= Object { runObject :: forall x. f x -> g (x, Object f g) }

オブジェクトを表す型

曰く「オブジェクトとは自然変換とミーリマシン、
双方の性質を併せ持ったものである。」

---- Natural ----
forall x. f x -> g x

---- Mealy ----
newtype Mealy a b
= Mealy { runMealy :: a -> (b, Mealy a b) }

---- Object ----
newtype Object f g
= Object { runObject :: forall x. f x -> g (x, Object f g) }

オブジェクトを表す型

  • 自然変換は関手から関手への変換
  • ミーリマシンは内部状態を保持する
  • オブジェクトは双方の性質を併せ持っている

  • 「関手」を「メッセージ」に置き換えてみよう

  • ・・・おやっ?

つまり・・・

オブジェクトは
内部状態を持ち
メッセージの変換を
行うものである

メッセージ
メソッドと手続き

メッセージを定義する

サンプルとしてカウンターを作ろう
まずGADTs言語拡張を用いて、メッセージを定義する。

data Counter a where
PrintCount :: Counter ()
Increment :: Counter ()

メッセージを定義する

尚、この段階でCounter型が、
Functorのインスタンスにならない事は、
あまり問題ではない。

その型が圏論的に関手であったとしても、
必ずFunctorのインスタンスになるとは
限らないのだが、難しい話になるので割愛。

オブジェクトを作成

実際にオブジェクトを作ってみよう。
とりあえず、簡単に再帰関数を使って実装してみる。

-- メッセージとしてCounterを受信、IOを送信
counter :: Int -> Object Counter IO
counter i = Object $ \case
Increment -> return ((), counter (i + 1))
PrintCount -> print i >> return ((), counter i)

インスタンス

ミーリマシンの時のように
MVarで状態管理するための演算子を準備。
IO上で状態管理されているオブジェクトは
インスタンスと呼べるだろう。

type Instance f g = MVar (Object f g)

(.-) :: Instance f IO -> f a -> IO a
i .- m = modifyMVar i $ \o -> do
(x, no) <- runObject o $ m
return (no, x)

動作確認

以下のようなコードを書く事によって、
実際に動作させる事が出来る。

sample :: IO ()
sample = do
cntr <- newMVar $ counter 10 -- インスタンス生成
cntr.-PrintCount -- 出力:10
cntr.-Increment
cntr.-Increment
cntr.-Increment
cntr.-Increment
cntr.-PrintCount -- 出力:14

文字列オブジェクト

続いて、以下のようなメソッドを持ち、
内部状態として文字列を保持するオブジェクトを、
作る事を考えてみよう。

data StringObject a where
PrintString :: StringObject ()
GetString :: StringObject String
SetString :: String -> StringObject ()

文字列オブジェクト

状態を持ったオブジェクトを作るのは、
Stateモナドを利用できたほうが便利なので・・・

unfoldO :: forall f r g. Functor g
=> (forall a. r -> f a -> g (a, r)) -> r -> Object f g
unfoldO h = go
where
go :: Functor g => r -> Object f g
go r = Object $ fmap (fmap go) . h r

(@~) :: Functor g
=> s -> (forall a. f a -> StateT s g a) -> Object f g
s0 @~ h = unfoldO (\s f -> runStateT (h f) s) s0

文字列オブジェクト

以下のようにして
簡単に状態を扱うオブジェクトを作成できる。

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

文字列オブジェクト

実際に動作させるコード例は次のとおり。

sample :: IO ()
sample = do
str <- newMVar $ stringObject "Hello!!"
str.-PrintString -- 出力:Hello!!
str.-SetString "Good Bye!!"
str.-PrintString -- 出力:Good Bye!!

所感

  • オブジェクトの作成はややクセがあるか
  • 使用者の使用感は一般的なOOPLに近い感じ
  • 参照されなくなったMVarはGCが破棄
  • 複雑な状態に対しLensの応用も検討できそう

受信メッセージをモナドに

  • モナドは手続きの表現だった
  • 受信メッセージとしてCounter型を定義した
  • 送信メッセージのIOはモナドである

  • もし受信メッセージもモナドだとしたら?

Counter型の手続きって?

  • PrintCount/Incrementを命令に持つ言語内DSL
  • モナドなのでdo構文により記述できる
  • Haskellの制御構文がそのまま使える
  • モナドを操作する高度な関数が使える

  • 受信メッセージがモナドなら
     メソッドを拡張できるのでは?

問題点

モナドを作るのは
けっこう大変

Operationalモナド

任意の多相型を
あっという間にモナドにしてしまう、
超便利なライブラリ。今回は詳細は割愛。

newtype Program t a = ...

instance Functor (Program t) where
instance Applicative (Program t) where
instance Monad (Program t) where
liftP :: t a -> Program t a

Operationalモナド

Operationalモナドの仕組みを使って、
Counterのモナド版、CounterPモナドを定義

type CounterP = Program Counter

printCount :: CounterP ()
printCount = liftP PrintCount

increment :: CounterP ()
increment = liftP Increment

Operationalモナド

今作成したCounterPモナドを使って、
引数で指定された回数だけIncrementする命令を定義。
これを受信メッセージ(メソッド)として使いたい。

incNPrint :: Int -> CounterP ()
incNPrint x = do
forM [1 .. x] $ \i -> do
increment
printCount

cascading関数

CounterPモナドをメッセージとして受信するオブジェクトを、
新たに作るのは効率が悪い。そこで・・・

cascadeObject :: Monad g
=> Object f g -> Program f a -> g (a, Object f g)
cascadeObject obj m = case view m of
Return a -> return (a, obj)
f :>>= k ->
runObject obj f >>= \(a, obj') -> cascadeObject obj' (k a)

cascading :: (Functor g, Monad g)
=> Object f g -> Object (Program f) g
cascading = unfoldO cascadeObject

cascading関数

cascading関数を使えば、
既に作成したcounterオブジェクトの受信メッセージを
モナドに変更したcounterPオブジェクトを簡単に作れる。

counterP :: Int -> Object CounterP IO
counterP = cascading . counter

cascading関数

こうして作られたオブジェクトは、
CounterPモナドを用いて作成したincNPrint関数を、
メソッドとして使う事ができる。

sample :: IO ()
sample = do
cntr <- newMVar $ counterP 10
cntr.-printCount -- 出力:10
cntr.-(incNPrint 100) -- 出力:110

オブジェクトの拡張

継承の話

「親クラスを元に子クラスを派生する」
程度の認識になってしまいがちだが
実際には、いくつかの拡張機能の
寄せ集めだという事がわかる。

例えば

  • メソッドの追加
  • メソッドのオーバーライド
  • 親クラスのメソッド呼び出し

2種類の合成



objectiveでは、
2種類の方法でオブジェクトを合成し
これらの機能を実現して行く

2種類の合成

【縦の合成】
左辺のオブジェクトの受信メッセージを受け
右辺のオブジェクトの送信メッセージを送る
新しいオブジェクトを作成する。

(@>>@) :: Functor h => Object f g -> Object g h -> Object f h
Object m @>>@ Object n = Object $ fmap joinO . n . m

joinO :: Functor h => ((a, Object f g), Object g h) -> (a, Object f h)
joinO ((x, a), b) = (x, a @>>@ b)

2種類の合成

【横の合成】
左辺のオブジェクトの受信メッセージと
右辺のオブジェクトの受信メッセージを纏め上げた
新しいオブジェクトを作成する。

data Sum f g a = InL (f a) | InR (g a)

(@||@) :: Functor m => Object f m -> Object g m -> Object (Sum f g) m
a @||@ b = Object $ \case
InL f -> fmap (fmap (@||@b)) $ runObject a f
InR g -> fmap (fmap (a@||@)) $ runObject b g

具体例

例として、Counterオブジェクトを親とし、
以下のTwiceIncメソッドを呼び出すと、
Incrementが二回呼ばれるような拡張を行う。

data TwiceInc a where
TwiceInc :: TwiceInc ()

具体例

まず、TwiceIncを受信し、CounterPを送信する
オブジェクトを作成する。

twiceInc :: Object TwiceInc CounterP
twiceInc = liftO $ \case
TwiceInc -> do
increment
increment

具体例

cascading関数で、twiceIncオブジェクトの
受信メッセージをモナドに変換する。

type TwiceIncP = Program TwiceInc

twiceIncrement :: TwiceIncP ()
twiceIncrement = liftP TwiceInc

twiceIncP :: Object TwiceIncP CounterP
twiceIncP = cascading twiceInc

具体例

echoと呼ばれる単位オブジェクトと横合成
最後に親であるcounterPと縦合成

echo :: Functor f => Object f f
echo = Object (fmap (\x -> (x, echo)))

twiceCounter :: Int -> Object (Sum CounterP TwiceIncP) IO
twiceCounter x = echo @||@ twiceIncP @>>@ counterP x

動作確認してみる

twiceIncrementメソッドが
期待した動作をしている事を確認。

sample :: IO ()
sample = do
tcntr <- newMVar $ twiceCounter 10
tcntr.-InL printCount -- 出力:10
tcntr.-InL (incNPrint 100)
tcntr.-InR twiceIncrement
tcntr.-InL printCount -- 出力:112

動作確認してみる

  • やりたい事のわりに手順が煩雑?
  • Haskellの抽象力でどうにでもなりそう
  • InLとかInRとかどうにかならないのか
  • これって親子間に多相性無いよね?
  • →effの応用で解決出来るらしい

  • 情報不足、申し訳ないm(__)m

残り資料間に合わない感じなので
アドリブで頑張る

①オブジェクトを射とした圏の話
②寿命のあるオブジェクト/ストリーム操作

ありがとうございました
m(__)m