effect ってなんですのん
effect ってなんですのん
それはある日のこと
ぼく「あ、Claude Code の週制限叩いちゃったな……」
ぼく「仕事はいったんやめて遊ぼうかな」
ぼく「何をしよう?」
ぼく「そうだ! Python に Algebraic effects and handlers を実装しよう!」
ということで始まります。シリーズ「Python に Algebraic effects and handlers を実装したいぞ」。
皆様よろしくお願いいたします。
- ハブ記事はこちら: https://zenn.dev/hnmr/articles/b9d091caf736c1
- 成果物はこちら: aleff
この記事の目的
- effect (作用)、handler (作用ハンドラ) について整理する
1. 前置き
世の中にはいろいろな変態がいます。その中でも一等の変態と目されるのが関数型プログラミング言語の愛好家たちです。彼らは普段から「モナドタソハァハァ」とか「依存型萌え~」とか言いながら関数の純粋性がどうとか議論しています。全員処女厨に違いありません。
さて、そんな彼らの間で近年話題になっているのが Algebraic effects and handlers という長ったらしい名前の存在です。検索するとだいたいのところ「継続をともなった例外みたいなもの」とか書いてあります。どゆこと?
ちょっと分解してみましょう。「Algebraic effects」、「effects」、「handlers」が構成要素です。まずは effect から考えてみます。
2. effect ってなんじゃらほい
関数のシグネチャというものがあります。たとえば str 型を受け取って int 型を返す関数 to_int があったとします。このとき、to_int のシグネチャは str -> int である、とか言います。シグネチャは、関数がどんな引数を受け取ってどんな値を返すのかを表します。
to_int :: str -> int
ところで、関数/計算には、これだけでは言い表しきれない性質を付与したくなることがあります。たとえば例外。to_int("123") は 123 を返すでしょうが、to_int("abc") は失敗して欲しいです。そういう性質もシグネチャに書いておきたいです。
関数が投げうる例外を明示すれば、呼び出し側が「この関数はワンチャン失敗するかもしれん……」と分かるので、より適切にハンドリングしやすいでしょう。え、「別にどうでもいい」って? もっと変態になりましょう。
あるいは、to_int はどこかに保持されている状態を読み書きするかも知れません。日本だったら文字列 "10,000" も 10000 になって欲しいわけですが、アメリカだったら "10.000" でしょう。python語を話す人は "10_000" かもしれませんし、C++語を話す人なら "10'000" でしょう。つまり処理内容は、明示的に渡していないロケールに依存するかもしれません。そのことも明示したいですよね。ね?
ということで、effect というのは、関数が持つそういう「値を返す以外の性質」を表すための概念です。たとえば、to_int が例外を投げる可能性があるなら、シグネチャは str -> int throws ArgumentError のように書けるとよいでしょう。あるいは、ロケールに依存するなら str -> int with Locale のように書けるとよいでしょう。
# 例外を明示したい
to_int :: str -> int throws ArgumentError
# ロケールへの依存を明示したい
to_int :: str -> int with Locale
さらに、例外についてもうちょっと考えてみましょう。例外が発生すると、その後の計算が捨てられて、例外ハンドラに処理が移ります。つまり、処理フローが変わるわけです。冷静に考えると、これってスゴイことです。いきなり別の場所に処理が飛ぶなんて、例外以外だとイマドキあんまりありません。COME FROM って聞いたことあります?
せっかくなので、これも effect の機能に入れてしまおうと思います。関数内で effect を発生させることを perform と呼ぶことにします。「perform は後続の計算の意味を変える(かもしれない)」、ということです。
結局、effect とは、暗黙に存在する環境によって値とその後の計算方法が決まる処理のことです。日本語だと「作用」とか言いますが、あまりにも紛らわしいので、このシリーズでは effect で統一します。
ここまでをまとめてみます。effect と perform という考え方を、以下の目的で導入しましょう。
- 目的①: 関数に環境への依存を表明させて静的検査を行いたい。
- 目的②: 処理の途中で計算の意味が変わることを表現したい。
- 実例① 検査例外: 関数に、実行時に発生しうる「失敗」を表明させる
- 実例②
Stateモナド: 型に、実行時に発生しうる「状態への依存」を表明させる - 実例③
throw: 後続の計算を捨てる。 - 実例④
getとput: 後続の計算を「状態付きの計算」に変える。 - 「暗黙に存在する環境によって値と実行後の計算方法が決まる」処理を effect (作用) と呼ぶことにする。
- 検査例外や
Stateモナドは、関数のシグネチャに effect を表明する例 -
throwやget、putは、関数内で effect を発生させる (perform する) 例
- 検査例外や
3. Effect handlers とは? 恋人がいるって本当? 調べてみました!
さて、先ほどぬるっと perform という言葉を導入しました。perform は、処理の途中で計算の意味が変わることを表現するためのものです。後続の計算の意味が変わる(かもしれない)とか言ってもよく分かりませんね。その「意味を変える」処理はどこに書けばいいのでしょう?
throw を例に出したので、try/catch で考えてみましょう。try/catch は、例外が発生したときの処理を外側で定義するための構造です。try ブロック内で throw が呼ばれると、後続の計算は捨てられて、対応する catch ブロックに処理が移ります。
function foo() {
// ここで throw が呼ばれると、後続の計算は捨てられて catch ブロックに処理が移る
throw new ArgumentError("invalid argument");
console.log("この行は実行されない");
}
try {
foo();
console.log("この行も実行されない");
} catch (e) {
// ここに来る
console.log("例外が発生しました: " + e.message);
}
つまり、try/catch は、effect が発生したときの処理を外側で定義するための構造です! これをパク……もとい、一般化したのが Effect handlers です。
catch されない throw はプログラムの終了を引き起こします。適切なプログラムでは、必ず throw に対応する catch が存在しなければなりません。そう、まるで恋人のように。
あるいは、State モナドの get と put を考えてみましょう。get は状態を読み取る effect で、put は状態を書き換える effect です。じゃあ get と put の意味を定義するのはどこでしょう? そう、runState が、get と put が発生したときの処理を定義しているのでした。throw ちゃん / catch ちゃんの関係と同様に、get・put ちゃん / runState ちゃんの関係も、恋人のような存在と言えるでしょう。State モナドちゃんちょっと爛れてませんか?
さておき重要なのは、throw や get、put といった effect の発生に対して、その意味や処理を定義するのは、関数の外側であるということです。定義と実装を分離することに嬉しみがあるのです。
このように、effect が発生したときの処理を外側で定義するための構造を Effect handlers と呼ぶことにします。先ほど effect の目的に書いた「暗黙に存在する環境」、「処理の途中で計算の意味が変わる」という部分の実装を担当するのがこいつです。
ここまでをまとめてみます。Effect handlers を、以下の目的で導入します。
- 目的: 関数実装から effect の処理を分離したい
- 関数は effect を使うだけで、その実装詳細は知らないようにしたい
- effect の発生を表明している関数を呼び出す側で、effect が発生したときの処理を定義したい
- 実例①:
try/catch:throwに対して、呼び出し側で例外が発生したときの処理を定義する - 実例②:
runState:getとputに対して、状態を管理する処理を定義する - 「この中で effect が発生しうる関数を呼び出す」という区切りと、「effect が発生したときの処理」を書けるようにした構造を Effect handlers と呼ぶことにする。
いかがでしたか?
4. Algebraic effects とは?
effect と Effect handlers について見てきました。では最後に Algebraic effects とは何かを説明したい……のですが、ちょっと長くなってきたので次回以降にしたいと思います。簡単に先出ししておくと、effect と Effect handlers を簡潔かつ合成可能に書くための条件というものがあります。その条件のことを 代数性 (algebraic property) と言います。あらゆる effect のうち、代数性を満たすものだけを集めたものが、Algebraic effects です。
5. 実際の例
実際の例として Effekt という言語のコードを見てみましょう。内容は State モナドの例です。
※残念ながら Effekt 用のシンタックスハイライトが提供されていないようなので、JavaScript のものを使用しています。
State モナドに似つかわしくない exit という謎の存在がいますがとりあえず無視してください。次回以降で実際に実装していくうえで最低限の説明のために追加したものです。
// effect を宣言
interface State {
def get(): Int
def put(x: Int): Unit
def exit(x: Int): Int
}
// action を実行して値をそのまま返す
def runState() { action: => Int / State }: Int =
var state = 0
// 以下がハンドラ
try {
// computation を実行
action()
} with State {
// get に対する operation clause
def get() = resume(state)
// put に対する operation clause
def put(x) = { state = x; resume(()) }
// exit に対する operation clause
def exit(x) = { x }
}
def main() =
val x = runState() {
// computation の実装
// `do effect()` みたいな感じで effect を発生させる
// `do` が perform に相当する感じ
// effect の詳細を知らなくても実行できる
val x = do get()
do put(x + 2000)
val y = do get()
//do exit(-100)
y
}
println(x)
なんとなく気持ちは分かりますね。まず main から見ていきます。runState にビジネスロジックのブロック { ... } を渡しています。このブロックがハンドラ内で実行されることになります。ハンドラ内で実行される処理のことを computation と呼ぶようです。
runState では、try { ... } with State { ... } でハンドラを定義しています。ここは特に何となくの気持ちで見てください。def get() が get という effect に対する処理、def put(x) が put という effect に対する処理を定義しています。それぞれを operation clause と呼ぶようです。中で resume という関数を呼んでいますが、これは継続です。本当は限定継続というやつなのですが、とりあえず今回は return みたいなものだと思ってください。詳しくは今後触れていきます。
main のブロック内に戻ります。effect である get と put を、do get()、do put(...) のように呼び出しています。Effekt では do が perform に相当するんですね。do get() でハンドラの operation clause に処理を移し、その中の resume(state) で継続を呼び出すと、val x = [] の部分に値が返ってくる、みたいな感じでしょうか。
最終的に、computation 全体の値は 2000 になります。
では次に、これ見よがしに書いてある //do exit(-100) のコメントアウトを外してみます。すると……何ということでしょう! computation 全体の値が -100 になってしまいました! ……ちょっとわざとらしいですね。
exit に対する operation clause を見てみます。get、put と違って resume を呼んでいません。次回以降で説明しようと思いますが、resume を呼ばない場合、後続の計算がすべて捨てられてしまうのです。するとどうなるか。
exit(-100) が呼ばれて def exit(x) = { x } が実行されます。この式全体の値は -100 になりますね。それが呼び出し元に返されるのですが、後続の計算 (computation の最後にある y の評価) はすでに捨てられてしまっています。したがって do exit(-100) の値が computation 全体の値になります。それは -100 でした。したがって action() の値も -100 になり、runState の値も -100 になります。最終的に main の x の値も -100 になったのでした。
以上、effect を用いたコードの例でした。exit の部分だけちょっと複雑でしたが…… 何がやりたいか、なんとなく読み取れたのではないでしょうか?
ここまでのまとめ
- なんか effect というものがあるっぽい
- 環境との相互作用とかを表明するためのものっぽい
-
try/catchみたいに処理を外で定義できるっぽい
次回予告
- 例外で作れ最小限の Algebraic effects and handlers(もどき)
- Python で簡単な effect とハンドラを実装してみます
Discussion