generated at
KokaでAlgebraic Effectsに触れる

Algebraic Effectsをざっくり言うと、
プログラムの副作用をEffectとして表現し、
Effectの発生箇所と処理箇所を分離するような抽象化の手法
Algebraic Effectsを扱える言語にはいくつかあるが、今回はKokaを触ってみた
Effect TypesとEffect Handlersを用いてEffectを制御する
関数型言語だが、気持ち的にはあまりビビることなく手続き型っぽく書ける
Algebraic Effectsを理解したいという動機で触れたが正解だったmrsekut
転がっている文書を読むのも良いが、実際に動くものに触れるのが一番早い
疑似Haskellとか疑似JavaScriptのコードを読む必要はない
最初からそのためにデザインされた言語に触れたほうがノイズが少ない
あと、基本的なことを抑えるだけならKokaってかなり難易度低いと思う
覚えることが少ない
docsに「Minimal but General」と書かれてるけど本当にそのとおり
使いこなせるかどうかはさておき、「Algebraic Effectsを知りたい」だけならコスパが良い
Kokaの基本文法を抑えた後に、具体例をいくつか見ればイメージを掴める
KokaはAlgebraic Effects以外にもすごい点がいくつかあって面白いが、ここでは触れない
以下ではKoka v2.4.0を使っている



まずTypeScriptで例外をthrowする関数を書いてみる
ts
function safeDivide(a: number, b: number): number { if (b === 0) { throw new Error("Division by zero"); } return a / b; }
これは例外が発生しうる関数だが、そのことが関数の型に表れない
そのため、関数の使用者は内部を把握しないと、その仕様の存在に気づけない
また、例外が送出された場合、どこかでそれを捕捉する必要があるが、気付けない場合はそもそも捕捉のしようがない
更に、一度、例外がthrowされると、この関数の実行は中断されてしまう
そのため、例外が発生した地点から処理を再開することができない


次にHaskellを用いた除算関数を見る
hs
safeDivide :: Float -> Float -> Either String Float safeDivide a b = if b == 0 then Left "Division by zero" else Right (a / b)
TypeScriptの例とは異なり、失敗する可能性があることを Either 型で表現できた
型があることで、失敗時のhandlingを強制することができる
しかし、Eitherという文脈が値に付くことで、通常の値との互換性がなくなってしまった
この関数の返り値は Either String Float 型であり、通常の Float 型とはそのまま計算することができない
計算する場合は、値を取り出したり何なりする必要がある
更に、複数のモナドを組み合わせる時はモナド変換子を使うことが多いが、これもまた複雑になりがち
hs
{-# LANGUAGE FlexibleContexts #-} import Control.Monad.Writer (MonadWriter, WriterT, tell, runWriterT) import Control.Monad.Trans.Class (lift) type Log = [String] safeDivide :: (Monad m, MonadWriter Log m) => Float -> Float -> m (Either String Float) safeDivide a b = do if b == 0 then do tell ["Division by zero attempted"] return $ Left "Division by zero" else do tell ["Performing division"] return $ Right (a / b) main :: IO () main = do (result, log) <- runWriterT (safeDivide 1 0) putStrLn $ "Result: " ++ show result putStrLn $ "Log: " ++ show log
これはWriterとEitherを組み合わせた例
モナドをネストさせたモナドスタックを構成することになるが、
順序を入れ替えたいときに型の構成をごちゃごちゃ入れ替えないといけなかったり、
組み合わせ自体が面倒だったりする


ここで、Kokaで除算関数を書くと以下のようになる
koka(js)
// raiseというEffectの定義 effect raise ctl raise(msg: string): int // 除算する関数の定義 fun safe-divide(x: int, y: int): raise int if y==0 then raise("div-by-zero") else x / y
以下のようにして使う
koka(js)
fun raise-const(): int with handler ctl raise(msg) 42 8 + safe-divide(1,0)
result
42
以下で1箇所ずつ順に見ていく



Kokaの関数の型を見る
上記の safe-divide 関数の型は以下の通り
fun safe-divide(x: int, y: int): raise int
引数が2つあって、両方 int
返り値も int
そして、Effectとして raise を持つ
これを「Effect Type」と呼んだりする
これら3つはいずれも型推論の対象になるmrsekut
そのため、省略しても良い


特殊なのはやはりEffect Typeだろう
Effect Typeはbuilt-inで用意されているものもあれば、自分で定義することもできる
用意されているKokaのEffect Typesには以下のようなものがある
total
Effectが存在しないことを表す
exn
例外が発生する可能性がある
div
関数が終了しない可能性がある
console
consoleに書き込む可能性がある
etc.
また、これらの複数の組み合わせに対してaliasをつけることもできる
pure = <exn,div>
純粋関数であることを表す
io = <exn,div,ndet,console,net,fsys,ui,st<global>>
io はEffectもりもりであることがわかるmrsekut
etc.
具体的な関数の型を見てみてもわかりやすいかも
以下は repeat 関数と、 while 関数の型
fun repeat(n :int, action: () -> e ()): e () ref
fun while(predicate: () -> <div|e> bool, action: () -> <div|e> ()): <div|e> () ref
repeat は実行する回数が決まっており必ず停止するため div が含まれてないが、
while の方は無限ループする可能性があるので div が含まれている


自分でEffect Typeを定義することもできる ref
koka(js)
effect raise // ① ctl raise(msg: string): a // ②
①で raise という名前のeffect typesを定義してる
②でそのoperationとして raise(msg: string): a を定義している
これらは異なる名前でも良いし、複数のoperationを持っても良い


このeffectを使った関数を定義
koka(js)
fun safe-divide(x: int, y: int): raise int if y==0 then raise("div-by-zero") else x / y
safe-divide という、安全に除算する関数を定義している
effectとして raise を持っている
型シグネチャに表れている方がeffect type(①)で、
関数内に書かれているのがoperaion(②)ということ
この時点では、 raise() を呼んだときに、何を行うのかは定義されていないmrsekut
interfaceのみ示されており、実装はまだ決まっていないということ
Reactユーザ向けの説明だと、感覚的には、 useContext() を呼んでるが、その中身はこの時点では決定できないのに近い




safe-divide() を使用する
Effect Handlerとともに使用する
Effect Handlerは、内部でEffectが呼ばれたときに、どういう挙動をするかを定義する場所
内部で用意されたInterfaceに、実装をInjectionするイメージ
koka(js)
fun raise-const(): int with handler // ③ ctl raise(msg) 42 8 + safe-divide(1,0) // ④
③でhandlingの仕方を定義した上で、④で実際に関数を呼んでいる
handlerの内容は、 raise() が呼ばれたら常に 42 を返す、というもの
従って、④を実行がされると、 1 ÷ 0 でraiseが呼ばれ、結果は 42 を返す
TypeScriptのtry/catchを順序を逆にして書いてると捉えれば良い
ts
try { return safeDivide(1, 0); // ④に相当 } catch (msg: any) { // ③に相当 return 42 }
もう1つのポイントは、 raise-const() 自体の返り値の型が
raise int ではなく、 int になっている点
raise というeffectを消化しているのでその文脈が型からも消える


Handleされた時点から処理を再開することもできる
例えば上記のコードを以下のように書き換える
koka(js)
fun raise-const() with ctl raise(msg) resume(42) // resumeしながら42 8 + safe-divide(1,0)
resume はhandler内で使えるbuilt-inの関数のようなもの
このコードを実行すると、 8 + 42 の結果として 50 が得られる



また、複数のEffectを扱う関数も簡単に書ける
koka(js)
effect ctl raise(msg: string): a effect ctl log(msg: string): () fun safe-divide(x: int, y: int): <log,raise> int if y==0 then log("Division by zero attempted") raise("div-by-zero") else log("Performing division") x / y fun raise-const(): console int with ctl raise(msg) 42 // ① with fun log(msg) // ② println(msg) 8 + safe-divide(1,0)
logを表示する log というEffectをを追加した
safe-divide では2つのEffectを含んでいることが型 <log,raise> を見ればわかる
raise-const の方を見ると、
①では raise に対して、②では log に対してhandlerを書いている
また、 raise-const 内ではprintlnを使っているので、 raise-const 自体のEffectは console になっている
更に、handleする順序の入れ替えも、行を入れ替えるだけで良いのでかなり楽
koka(js)
fun raise-const(): console int with ctl raise(msg) 42 with fun log(msg) println(msg) ...
koka(js)
fun raise-const(): console int with fun log(msg) println(msg) with ctl raise(msg) 42 ...
今回のコード例では順序を入れ替えても挙動は変わらないが、組み合わせるEffectによっては変化するものもある
このように、Effectの組み合わせや、handlingの順序の変更などがかなり簡易にできる





具体例をいくつか





継続

listモナド?
たぶん違う
というか非決定を表すndetがあるのでそれを用いた例を書いたほうがおもろそう
以下は、resumeを2回呼んでる例として良さそう
koka
effect ctl choice(): bool fun xor(): choice bool val p = choice() // ① val q = choice() // ② if p then !q else q fun choice-all(action: () -> <choice|e> a): e list<a> with handler return(x) [x] ctl choice() resume(False) ++ resume(True) action() // xor.choice-all()
実行順番
①でhandleされて、1つめのreusmeにより①にFalseが入る
②で再びhandlerされて、1つめのreusmeにより②にFalseが入る
2つめのreusmeにより②にTrueが入る
①でhandleされて、2つめのreusmeにより①にFalseが入る
②で再びhandlerされて、1つめのreusmeにより②にFalseが入る
2つめのreusmeにより②にTrueが入る
つまり
(p,q) が、 (False,False) , (False,True) , (True,False) , (True,True) の順で呼ばれる
resume(False) ++ resume(True) の意味
安直に考えると以下で良さそうだが
koka(js)
ctl choice() resume(False) // A resume(True)
これだと、Aで再開されてそのまま終わってreturnに行ってしまう
++ のようにすることで結果を結合することを意味するため、逐次的に両方実行されることになる
ってことかな