2015/9/13 関数プログラミング交流会
by ちゅーん(@its_out_of_tune)
HN: ちゅーん
Twitter:
@its_out_of_tune
Github:
tokiwoousaka
「たまたま」モナドという数学上の概念が
手続きプログラミングに「欲しい」性質を
持っているのでそれを応用しているだけ。
詳しくは後述
我々が「欲しい」と考える
OOPLの機能を得るための
Haskellらしい仕組みを考える
まず、手続きを導入する手段として、
モナドがもてはやされている理由について、
考えてみよう。
「数学」という言葉を使うとまた荒れそうなので
「形式的」という言葉に言い換えます
ここでいう「形式的」は
「簡単な定義の組み合わせで曖昧性/矛盾なく説明できる」
くらいの意味です
Haskell上では普通に、
手続きプログラミングが出来る。
main :: IO ()
main = do
putStrLn "ファーストネームを入力してね"
firstName <- getLine
putStrLn "ラストネームを入力してね"
lastName <- getLine
let name = firstName ++ " " ++ lastName
putStrLn $ "こんにちは、" ++ name ++ "さん。"
手続き自体がプリミティブな機能というワケでは無い
モナドによって実現されている。
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)
先のコードは以下のような
純粋な関数の構文糖になっている。
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
のように当てはめる事で、
関数プログラミングの型は圏になる。
関手は圏から圏への変換
ある圏から同じ圏への関手を自己関手と呼ぶ
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")
これらの道具立てで
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!!
任意の多相型を
あっという間にモナドにしてしまう、
超便利なライブラリ。今回は詳細は割愛。
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モナドの仕組みを使って、
Counterのモナド版、CounterPモナドを定義
type CounterP = Program Counter
printCount :: CounterP ()
printCount = liftP PrintCount
increment :: CounterP ()
increment = liftP Increment
今作成したCounterPモナドを使って、
引数で指定された回数だけIncrementする命令を定義。
これを受信メッセージ(メソッド)として使いたい。
incNPrint :: Int -> CounterP ()
incNPrint x = do
forM [1 .. x] $ \i -> do
increment
printCount
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関数を使えば、
既に作成したcounterオブジェクトの受信メッセージを
モナドに変更したcounterPオブジェクトを簡単に作れる。
counterP :: Int -> Object CounterP IO
counterP = cascading . counter
こうして作られたオブジェクトは、
CounterPモナドを用いて作成したincNPrint関数を、
メソッドとして使う事ができる。
sample :: IO ()
sample = do
cntr <- newMVar $ counterP 10
cntr.-printCount -- 出力:10
cntr.-(incNPrint 100) -- 出力:110
「親クラスを元に子クラスを派生する」
程度の認識になってしまいがちだが
実際には、いくつかの拡張機能の
寄せ集めだという事がわかる。
objectiveでは、
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)
【横の合成】
左辺のオブジェクトの受信メッセージと
右辺のオブジェクトの受信メッセージを纏め上げた
新しいオブジェクトを作成する。
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
残り資料間に合わない感じなので
アドリブで頑張る
①オブジェクトを射とした圏の話
②寿命のあるオブジェクト/ストリーム操作