Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
SlideShare a Scribd company logo
並行プログラミングと
継続モナド
関数型言語交流会 2015-09-13
@ruicc
だれ
• @ruicc
• サーバサイドエンジニア
• Haskeller
言いたいこと
• 継続モナドが新しいモジュラリティを与えてくれる
• 並行プログラミングで継続モナド便利
3
言いたいこと(裏)
• みんな並行プログラミングしようぜ!
• 知見がもっと欲しい
4
プロローグ
並列並行Haskell本
• Simon Marlow著
• 素晴らしい本なので
• とりあえず読みましょう
• 以降heyhey Haskell本と呼ぶ
12章:並行ネットワークサーバ
• 単純でスケーラブルなチャットサーバ実装
• telnetでアクセスして、部屋へ入り、チャットする
7
とりあえず実装/改良してみた
• 局所的に問題を解いていくコード
• すごい参考になる
• IO版、Cont版作って単純ベンチマーク
• VM RAM3GB
• 9000clientsさばく
• エラーなし
• profile見たらほとんど文字列でメモリ消費してた
• OOMKillerにやられた
8
例: main周辺
9
main :: IO ()
main = withSocketsDo $ do
server <- newServer
sock <- listenOn (PortNumber (fromIntegral port))
forever $ do
(handle, host, port) <- accept sock
forkFinally
(talk handle server)
(_ -> hClose handle)
main :: IO ()
main = withSocketsDo $ do
server <- newServer
sock <- listenOn (PortNumber (fromIntegral port))
forever $ do
(handle, host, port) <- accept sock
forkFinally
(talk handle server)
(_ -> hClose handle)
Portのlisten
例: main周辺
10
main :: IO ()
main = withSocketsDo $ do
server <- newServer
sock <- listenOn (PortNumber (fromIntegral port))
forever $ do
(handle, host, port) <- accept sock
forkFinally
(talk handle server)
(_ -> hClose handle)
Portのlisten
accept
例: main周辺
11
main :: IO ()
main = withSocketsDo $ do
server <- newServer
sock <- listenOn (PortNumber (fromIntegral port))
forever $ do
(handle, host, port) <- accept sock
forkFinally
(talk handle server)
(_ -> hClose handle)
Portのlisten
accept
accept毎にfork
例: main周辺
12
main :: IO ()
main = withSocketsDo $ do
server <- newServer
sock <- listenOn (PortNumber (fromIntegral port))
forever $ do
(handle, host, port) <- accept sock
forkFinally
(talk handle server)
(_ -> hClose handle)
Portのlisten
accept
accept毎にfork
その後やること
例: main周辺
13
main :: IO ()
main = withSocketsDo $ do
server <- newServer
sock <- listenOn (PortNumber (fromIntegral port))
forever $ do
(handle, host, port) <- accept sock
forkFinally
(talk handle server)
(_ -> hClose handle)
問題点
14
その後やることが直に埋め込まれている
その後やること
並行プログラミング
並列性と並行性
• 並列性
• 計算をより速くするために資源(CPUコア等)
を用いる
• 基本的に決定的計算(純粋計算)
• 並行性
• 複数のスレッドを用いてプログラムを構築する
• 非決定的(各スレッド上でIOが発生)
16
なぜ並行プログラミングか
• プログラムの構造がシンプルになる場合がある
• スレッド単位で処理を構築し、それらを組み合わせ
るというモジュラリティを提供する (heyhey
Haskell本より)
17
並行プログラミングは
設計の問題である
並行プログラミングの難しさ
• スレッド単位で構築し、それらを組み合わせる
• スレッド毎の処理は単純にかける
• 組み合わせる箇所が難しい
19
今日はスレッドの組み
合わせの話ではない
並行プログラミングの難しさ
• スレッド同士を組み合わせるのが難しい?
• 共有メモリの操作を安全に合成出来る
• STMモナド(モナドが重要)
21
並列並行Haskell本を読もう!
• 並行モデル
• 共有メモリモデル
• トランザクションモデル
(STM)
• アクターモデル
• 例外
• 詳しく書いてある
今日はさらに細かいコードの
モジュラリティの話
Chatサーバを実装してみて
• すべてのコードがほぼ一直線の数珠繋ぎになっている
ことに気づいた
• どこを切り出してもその後の処理が全て付いてくる
• これでは再利用やテストがしにくい、なぜそうなって
いる?
24
なぜコードが一直線なのか?
• サンプル用のコードだから
• 問題を局所に押し込めて解くスタイル
• 非同期例外の存在
25
「問題を局所に押し込める」
• 例えば後始末が必要なリソースの扱い
• もし後始末がコード内に散らばってしまうと…
• コードが読みづらい/把握が難しい
• エンバグしやすい
• 保守がつらい
• 拡張しづらい
26
例外を用いる
• 何かする時に後始末も同時に書いてしまう
• 以降後始末は考えなくても良い
27
例外例:チャットルームへ入る
28
readName = do
hPutStrLn handle "What is your name?"
name <- hGetLine handle
if null name
then readName
else mask $ restore -> do -- <1>
ok <- checkAddClient server name handle
case ok of
Nothing -> restore $ do -- <2>
hPrintf handle
"The name %s is in use, Choose anothern"
name
readName
Just client ->
restore (runClient server client) -- <3>
`finally` removeClient server name
readName = do
hPutStrLn handle "What is your name?"
name <- hGetLine handle
if null name
then readName
else mask $ restore -> do -- <1>
ok <- checkAddClient server name handle
case ok of
Nothing -> restore $ do -- <2>
hPrintf handle
"The name %s is in use, Choose anothern"
name
readName
Just client ->
restore (runClient server client) -- <3>
`finally` removeClient server name
チャットルームへ入る
例外例:チャットルームへ入る
29
例外例:チャットルームへ入る
30
readName = do
hPutStrLn handle "What is your name?"
name <- hGetLine handle
if null name
then readName
else mask $ restore -> do -- <1>
ok <- checkAddClient server name handle
case ok of
Nothing -> restore $ do -- <2>
hPrintf handle
"The name %s is in use, Choose anothern"
name
readName
Just client ->
restore (runClient server client) -- <3>
`finally` removeClient server name
例外処理
readName = do
hPutStrLn handle "What is your name?"
name <- hGetLine handle
if null name
then readName
else mask $ restore -> do -- <1>
ok <- checkAddClient server name handle
case ok of
Nothing -> restore $ do -- <2>
hPrintf handle
"The name %s is in use, Choose anothern"
name
readName
Just client ->
restore (runClient server client) -- <3>
`finally` removeClient server name
入った後の処理
例外例:チャットルームへ入る
31
readName = do
hPutStrLn handle "What is your name?"
name <- hGetLine handle
if null name
then readName
else mask $ restore -> do -- <1>
ok <- checkAddClient server name handle
case ok of
Nothing -> restore $ do -- <2>
hPrintf handle
"The name %s is in use, Choose anothern"
name
readName
Just client ->
restore (runClient server client) -- <3>
`finally` removeClient server name
その後の処理が
全て付いてくる
例外例:チャットルームへ入る
32
例外で得たもの、失ったもの
• 例外を用いると、局所に問題を閉じ込め、安全にプ
ログラムを書ける
• 閉じ込められるかどうかは問題による
• 例外を用いると、コードの構造が大きく制限される
33
例外でどう制限されるか?
• 例外を使ったら
• もぐるしかなくなる
• 例外スコープが必要なくなるまで
34
例外構文による制限(余談)
• Haskellでは例外機構は関数で提供されている
• 例外が構文になっている場合、厄介に思われるかも
しれない
• 関数が第1級ならbracketを用意すると便利
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
35
そもそも例外機構は必要か?
• 一般に必要かどうかは難しい問題
• 型システムで代替できないか?
• 無理
• 型システム外から飛んでくる例外が存在する
• つまり静的には捉えられないモノの存在
36
非同期例外(GHC)
• 型システムで捉えられない例外の一つ
• よってIO上で捕まえるしかない
• スレッドの外から飛んでくる例外
• ユーザの投げるシグナル
• メモリ不足等によって発生する例外
• スレッドを外から殺すための例外
• タイムアウト実装に利用する
37
非同期例外の存在(GHC)
• 常に例外が飛んでくる可能性がある
• 非同期例外を受け取らないスコープの必要性
(mask)
• 例外補足の必要性
• 先と同様にコードが制限される
38
例外の制限の回避は?
• 例外は(少なくともGHCでは)使わないといけないこ
とがわかった
• 例外を用いるとその後に実行することがプログラム
(関数)内に直に埋め込まれてしまう
• どうする?
39
高階関数を使う
• 関数型言語(!!)なので高階関数が使える
• その後にすることを引数で渡す
40
readName = do
hPutStrLn handle "What is your name?"
name <- hGetLine handle
if null name
then readName
else mask $ restore -> do -- <1>
ok <- checkAddClient server name handle
case ok of
Nothing -> restore $ do -- <2>
hPrintf handle
"The name %s is in use, Choose anothern"
name
readName
Just client ->
restore (runClient server client) -- <3>
`finally` removeClient server name
その後の処理
その後の処理の高階関数化
41
readName' cont = do
hPutStrLn handle "What is your name?"
name <- hGetLine handle
if null name
then readName
else mask $ restore -> do -- <1>
ok <- checkAddClient server name handle
case ok of
Nothing -> restore $ do -- <2>
hPrintf handle
"The name %s is in use, Choose anothern"
name
readName
Just client ->
restore (cont server client) -- <3>
`finally` removeClient server name
その後の処理
その後の処理の高階関数化
42
引数で渡す
高階関数化によって
• 例外使うたびに似たような特殊な高階関数がたくさ
ん出来る
• うまく扱う方法はないか?
43
readName' :: (Server -> Client -> IO ()) -> IO ()
そこで継続モナドですよ
継続モナド
モナド?
モナドとは(Haskell)
• 「モナド則を満たすもの」
47
モナド則とは(Haskell)
• Monad mとそのメソッド(>>=), returnに対して以
下が成立すること
1. return x >>= f ≡ f x
2. m >>= return ≡ m
3. (m >>= f) >>= g ≡
m >>= (x -> f x >>= g)
48
モナドとは(Haskell)
• さっきのモナド則を満たす任意のものはモナド
49
モナド則の実用上の意味
• returnは何もしないアクション(モナド則1,2)
• (>>=)は二つのアクションを組み合わせる
• アクションの組み合わせ方は結合的(モナド則3)
50
整数の積の結合則
(X * Y) * Z == X * (Y * Z)
モナド則の嬉しさ
• IOアクション3つ(act1, act2, act3)を考える
• act1 :: IO A
• act2 :: A -> IO B
• act3 :: B -> IO C
• これらの組み合わせは2通りの構造が考えられる
• (act1 >>= act2) >>= act3
• act1 >>= (b -> act2 b >>= act3)
• モナド則3より、これら2構造は同一のものとして扱っ
て良い
51
モナド則の嬉しさ(2)
• IOアクションn個(act1, act2, ... ,actn)を考える
• act1 :: IO A
• act2 :: A -> IO B
...
• actn :: X -> IO Y
• これらの組み合わせはX通りの構造が考えられる
• (...(act1 >>= act2) >>= ... >>= actn)
• モナド則3より、これらX個の構造は同一のものとして
扱って良い
52
モナド則の嬉しさ(3)
• 複数の異なる構造を同一視して良い
➡ 構造の複雑さが軽減される
53
モナド則の嬉しさ(補足)
• パフォーマンス(動的性能)が同じとは言ってない
• 一般に、同じ結果になるプログラムが複数通りあっ
たらどれかが速い
54
代数的性質の嬉しさ
• みんな沢山知ってる代数的性質
• 交換則
• X * Y == Y * X
• 分配則
• X * (Y + Z) == X * Y + X * Z
• 結合則
• (X * Y) * Z == X * (Y * Z)
• 上記はどれも複数の構造を同一視して良い性質
• 複雑さと戦うための武器の一つ
55
そして圏論へ
• 数学史に現れてきた代数的構
造をいろいろ包含する概念圏
を扱う
• プログラムの複雑さと戦おう
• 9/9発売
そして圏論へ
• 数学史に現れてきた代数的構
造をいろいろ包含する概念圏
を扱う
• プログラムの複雑さと戦おう
• 9/19発売
継続モナド
継続モナドのアクション
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
type Cont' r a = (a -> r) -> r
• 関数を受け取って結果を返す関数、というアクション
• 引数の関数はアクションの最後で実行される
59
継続?
継続とは
• 「その後に実行すること」
61
62
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
• 関数を受け取って結果を返す関数、というアクション
• 引数の関数はアクションの最後で実行される
継続モナドのアクション(再)
継続
継続
• 継続 =「その後に実行すること」が表現されている
継続
継続モナドの定義
instance Monad (ContT r m) where
return x = ContT ($ x)
m >>= k =
ContT $ c -> runContT m (x -> runContT (k x) c)
63
継続モナドの定義
instance Monad (ContT r m) where
return x = ContT ($ x)
m >>= k =
ContT $ c -> runContT m (x -> runContT (k x) c)
64
mを走らせる
mに渡す継続
継続モナドの定義
instance Monad (ContT r m) where
return x = ContT ($ x)
m >>= k =
ContT $ c -> runContT m (x -> runContT (k x) c)
65
(k x)を走らせる
(k x)に渡す継続xはmが渡す
継続モナドの定義
instance Monad (ContT r m) where
return x = ContT ($ x)
m >>= k =
ContT $ c -> runContT m (x -> runContT (k x) c)
66
全体の結果はアクションを返す
継続cは外からもらう
(k x)に渡す継続
継続モナド例(trivial)
action1, action2, action3 :: Cont r Int
action1 = return 42
action2 = return 13
action3 = return 2
cont_ex = do
x <- action1
y <- action2
z <- action3
return (x + y + z)
67
action1の継続は
どれか?
継続モナド例(trivial)
action1, action2, action3 :: Cont r Int
action1 = return 42
action2 = return 13
action3 = return 2
cont_ex = do
x <- action1
y <- action2
z <- action3
return (x + y + z)
68
action1の継続
(action1後に実行)
69
action1, action2, action3 :: Cont r Int
action1 = return 42
action2 = return 13
action3 = return 2
cont_ex = do
x <- action1
y <- action2
z <- action3
return (x + y + z)
継続モナド例(trivial)
action2の継続
(action2後に実行)
70
action1, action2, action3 :: Cont r Int
action1 = return 42
action2 = return 13
action3 = return 2
cont_ex = do
x <- action1
y <- action2
z <- action3
return (x + y + z)
継続モナド例(trivial)
action3の継続
(action3後に実行)
71
cont_ex = do
x <- action1
y <- action2
z <- action3
return (x + y + z)
main = do
print $ runCont cont_ex id
継続モナド例(trivial)
最後に実行される
継続
つまりどういうこと?
• 継続モナドを使うと
• アクションが並べた順に実行される
72
IOと何が違うのか?
• CPS(Continuation Passing Style)
• スタイルが違う
• できることは同じ
73
継続モナドの動的性能
• 同じことができるプログラムは、一般にどちらかが
速い
• 継続モナドやCPSを使うと速くなることがある
• 継続モナドはクロージャを大量に生成する
• GC頻度が上がるかも
74
継続モナドの他の特徴
はどうなのか?
継続モナドと例外
例外の補足
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
77
例外の補足
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
78
リソース取得
リソース解放
(必ず実行される) アクション
bracketとは
• 関数化された例外機構
• 例外構文を持たないだけで、同様の特徴を持つ
• コード構造が限定される
• とはいえそれでも便利
79
例外の補足
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
80
継続っぽい!
簡単な例
81
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
簡単な例
82
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
ContT r IO a
簡単な例
83
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
bracketを
継続モナドアクションに
簡単な例
84
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
例外機構を
(ネストではなく)
縦に並べている
簡単な例
85
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
わざと例外を投げる
さてどう動くか?
86
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
動作
87
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
順々にリソース取得が
実行される
動作
88
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
例外が投げられる
動作
89
bracket_demo :: ContT r IO Int
bracket_demo = do
fh <- ContT $ bracket
(openFile "tmp" WriteMode)
hClose
n <- ContT $ bracket
(hPutStrLn fh "Gain 42" >> return 42)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
l <- ContT $ bracket
(hPutStrLn fh "Gain 13" >> return 13)
(n -> hPutStrLn fh $ "Finalize: " ++ show n)
liftIO $ throwIO (ErrorCall "heyhey")
return $ n + l
逆順でfinalizerが
実行される
どういうこと?
• 継続モナド上(ContT r IO a)では例外はアクションの
逆順で伝播する
90
継続モナドのアクション
newtype ContT r m a =
ContT { runContT :: (a -> m r) -> m r }
•継続はアクションの最後に実行される
•継続内で例外が発生したら、アクションに戻ってくる
•つまり、例外はアクションを逆順に伝播する
91
継続モナドと例外
• 継続モナドによって例外機構の制約から抜けること
が出来た
• モナドなのでアクションが組み合わせやすい
92
継続モナドを考える
• 継続モナドは新しいモジュラリティを提供する
93
関数呼び出しイメージ
94
f
g
hcall
call
return
return
関数呼び出しイメージ
95
f
g
hcall
call
return
return
使いまわせる粒度
継続モナドイメージ
96
f
g
h
tail call
tail call
継続モナドイメージ
97
f
g
h
tail call
tail call
使いまわせる粒度
使いまわせる粒度
使いまわせる粒度
継続モナドと例外イメージ
98
f
g
h
call
call
finalizer
finalizer
継続モナドと例外イメージ
99
f
g
h
call
call
finalizer
finalizer
使いまわせる粒度
使いまわせる粒度
使いまわせる粒度
まとめ
• Haskellでは並行プログラミングに例外は必須
• 例外を使うとコード構造に制約ができる
• 継続モナドで例外が頻発するコードでもモジュラリ
ティを高く保つことができる
100
エピローグ
101
-- sketch
launchServer :: Port -> IO ()
launchServer port = (`runContT` return) $ do
client <- ContT $ acceptLoop port
loginedClient <- ContT $ login client
roomId <- ContT $ joinRoom loginedClient
ContT $ chat loginedClient roomId
エピローグ
102
-- Add logger
launchServer :: Port -> IO ()
launchServer port = (`runContT` return) $ do
logger <- newLogger
client <- ContT $ acceptLoop port
loginedClient <- ContT $ login client
roomId <- ContT $ joinRoom loginedClient
ContT $ chat loginedClient roomId logger

More Related Content

並行プログラミングと継続モナド