世界を動かす技術を、日本語で。

なぜ代数的効果なのか?

概要

  • Algebraic effects (効果ハンドラ)は、次世代言語で注目される機能
  • 例外やコルーチンなど多様な制御フローを ライブラリレベルで実現
  • 依存性注入やAPI設計の簡素化 にも有用
  • 副作用の抽象化 と柔軟な合成性を提供
  • 実用的な活用例と、ビジネスアプリにも役立つ理由を解説

Algebraic Effects(効果ハンドラ)とは何か、なぜ使うのか

  • Algebraic effects は、例外処理を「再開可能」にしたような仕組み
  • AnteやKoka、Effektなど多くの研究言語で 中核機能 として採用
  • 関数がどんな副作用を持つかを型で明示し、 柔軟にハンドリング可能
  • 例:effect SayMessage with say_message: Unit -> Unit のように宣言
  • handle ... | say_message () -> ... resume ()例外風に制御フローを記述

効果ハンドラの活用例

  • ジェネレータ、例外、コルーチン、非同期処理 などを、言語組み込みでなくライブラリとして実装可能
  • map関数のような高階関数も、 副作用に多態的 に対応できる
  • 例:map (input: Vec a) (f: a -> b can e): Vec b can e
    • fがどんな副作用eを持っていてもmapが同じ副作用を持つことを型で表現

例外の実装例

  • 例外=再開しない効果 として実装可能
    • effect Throw a with throw: a -> never_returns
    • handle ... | throw msg -> print msg で例外メッセージ処理

ジェネレータの実装例

  • effect Yield a with yield: a -> Unit を使い、 イテレーションやフィルタ もハンドラで記述
  • 例:yield_all_elements_of_vecfiltermy_for_each などを簡潔に実装

スケジューラやコルーチン

  • yield: Unit -> Unit などで 協調的マルチタスク も実現
  • Effektなどの例では、 複数の効果の合成も容易

抽象化としての効果ハンドラ

ビジネスアプリでの利点

  • 依存性注入 を副作用として抽象化可能
    • 例:データベースアクセスをeffect Database with query: String -> DbResponseとして抽象化
    • テスト時には モックDBロギングの差し替え も容易
  • 出力(print/log)も効果として抽象化
    • テスト時にprint出力を文字列として収集・検証が可能

柔軟なロギング

  • effect Log with log: LogLevel -> String -> Unitロギング出力の制御
  • ログレベルによる 出力のフィルタリング もハンドラで簡単に実装

よりクリーンなAPI設計

Contextの自動伝播

  • 典型的な「Contextオブジェクトの手渡し」を 効果として抽象化
  • effect Use a with get: Unit -> a set: a -> Unit状態の管理 も実現
  • 例:state (f: Unit -> a can Use s) (initial: s): a で初期状態を提供
  • これにより、 Contextの明示的な受け渡しが不要 となり、APIがシンプルに

状態管理の一般化

  • 内部状態を隠蔽しつつ、必要な操作だけをエクスポート
  • ライブラリ設計や抽象化の際に 内部実装の切り替えが容易

効果ハンドラの合成性・拡張性

  • 複数の効果(例:ロギング+DBアクセス+状態管理)が 簡単に合成可能
  • 他の副作用抽象(モナドなど)より 直感的で扱いやすい のが大きな強み

まとめ

  • Algebraic effectsは、 制御フロー・副作用・依存性注入 の抽象化に非常に有効
  • API設計やテスト容易性、拡張性 の向上に寄与
  • 今後のプログラミング言語設計で 主流となる可能性が高い技術

Hackerたちの意見

数年前にOCaml 5 alphaでプロトハッカーをやったことがあるんだけど、エフェクトを使ってね。楽しかったけど、その時のツールチェーンはちょっと使いづらかったんだ。今のはすごく似てる感じがするね。進展を楽しみにしてるよ。

OCaml 5.3の効果は、数年前よりかなりクリーンになったね(でもまだ型付けはされてないけど)。

アルジェブラ的エフェクトは、基本的に再開可能な例外だと思ってもらえればいいよ。ApplicativeErrorやMonadError[0]の型クラスを使うのと、実質的に何が違うの? > エフェクトを「投げる」には関数を呼び出す必要があって、その関数内ではそのエフェクトを使えると宣言しなきゃいけない。これはチェックされた例外と似てるね… これは上記の型クラスの一つで宣言されたエラータイプと、そのraiseErrorメソッドになるよ。 > そして、ハンドル式を使ってエフェクトを「キャッチ」できる(これをtry/catch式だと思って)。これがまさにこれらの型クラスが提供するもので、"handle式"はhandleErrorhandleErrorWithを使う(必要に応じて)。 > アルジェブラ的エフェクト(別名エフェクトハンドラ)は、非常に便利な新しい機能で、個人的には未来のプログラミング言語で大きな人気を得ると思ってるよ。「アルジェブラ的エフェクト」は未来のプログラミング言語だけじゃなく、実際に今のプログラミング言語でも人気があるんだ。

よくわからないけど、これは区切られた継続とも関係あるの?

ApplicativeErrorやMonadError[0]の型クラスを使うのと、実質的に何が違うの?静的な動作と動的な動作の違いだと思う。モナディックプログラミングでは、モナド内で関連するすべてのメソッドを実装しなきゃいけないけど、エフェクトを使うと、必要なところにエフェクトハンドラを動的にインストールできるんだ。二つのシステムの組み合わせが役立つと思うよ。例えば、テストやサンドボックス用に特注のIO互換モナドを使っても、下にあるエフェクトハンドラは、あなたのIOライクなモナドを呼び出すことができる。

アルジェブラ的エフェクトは区切られた継続の領域にあって、プログラムスタックで動作するんだ。モナドのトリックでは、コールスタックの5レベル上のエフェクトハンドラにすぐにジャンプして、そのスタックフレーム内のローカル変数を更新して、また5レベル下で実行を再開することはできないよ。

モナドと効果は、計算コンテキストについて考えるための補完的なアプローチとして見るのが一番いいと思うな、ライバルとしてではなくて。例えば、https://goto.ucsd.edu/~nvazou/koka/padl16.pdf を見てみて。

http://abstractionlogic.com

ApplicativeErrorやMonadError[0]型クラスを使うのと、これが実質的にどう違うの?もし単一の効果に限定するなら、あまり違いはないかもしれないけど、複数の効果を同時に持つと、明示的なサポートがあった方がネストされたモナドよりも扱いやすくなるよね(モナドの順序を選ぶ必要があったり、呼び出し関数の出力が使われるモナドのセットや順序と一致しない場合に再配置する必要があるから)。

HaskellのMonadErrorについては、かなり似てるよ。ただ、mtlスタイルにはいくつか問題があって、効果システムについては、著者が「mtlについては?」のところであんまり説明してないんだよね。https://hackage.haskell.org/package/effectful

アルジェブラ的エフェクトは、基本的に再開可能な例外だと思ってもらえればいいよ。つまり、Common Lispの条件みたいなもの?古いアイデアを名前を変えて繰り返すのが好きなんだ。

Smalltalkでは文字通り「再開可能な例外」だね。

いや、代数効果はLISPの条件システムよりも多くのケースをサポートする一般化だから、継続がマルチショットなんだ。最も近いのはSchemeのcall/ccだね。時々、これらの並列性を作ることは、最初からそれがないよりも辛いこともある。

あと、依存性注入ね。

二つの欠点があると思う。このスニペットを見てみて:my_function (): Unit can AllErrors = x = LibraryA.foo () y = LibraryB.bar () まず最初に注目すべきは、fooやbarが失敗する可能性があることを示すものがないってこと。これらの呼び出しがエラーハンドラを呼び出すかもしれないことを知るには、型シグネチャを調べるか(少なくともIDEでホバーする必要がある)しないといけない。次に、fooとbarが失敗することがわかったら、失敗した時に実行されるコードをどうやって見つけるの?コールスタックを上にたどって「with」式を見つけて、そこからハンドラに降りていかなきゃいけない。これが静的にできるわけじゃない(つまり、IDEが定義にジャンプできない)から、my_functionはどこからでも呼ばれる可能性があって、それぞれ異なるハンドラを持ってるからね。これは本当に面白いコンセプトだと思うけど、結果的なコードの可読性やデバッグのしやすさには大きな懸念があるよ。

これは静的にはできないんだよね(つまり、IDEが定義にジャンプできない)。なぜなら、my_functionはどこからでも呼ばれる可能性があって、それぞれ異なるハンドラーがあるから。静的にできると思うんだけど(それが代数効果の重要なポイントの一つだよね)。基本的には「呼び出し元にジャンプする」みたいに機能して、IDEが選択肢を提示してくれるから、興味のある呼び出し元やハンドラーを見つけられるんだ。

自分の学士論文は、レキシカル効果とハンドラーに対するIDEのサポートについて書いたんだ。https://se.cs.uni-tuebingen.de/teaching/thesis/2021/11/01/ID... あなたが言ってることは全部実現可能だよ。

最初に注目すべきは、fooやbarが失敗する可能性が示されていないことだと思う。これがポイントの一部だよね:直接スタイルで書けて、効果のあるコンテキストについて全く心配しなくていい。 > 失敗したときに実行されるコードはどうやって見つけるの?私の理解では、これもポイントだと思う:効果がどう処理されるかの特定の実装から抽象化できるんだ。失敗したときに実行されるコードは、実行方法を決めるときに後で決まるよ。f : g:(A -> B) -> t(A) -> Bのように、gが実行されるときに「その」コードを見つける方法はないんだ。なぜなら、gの特定の実装について抽象化しているから。

[...] 失敗したときに実行されるコードはどうやって見つけるの?それを探すには[...] 私は.NETの世界で働いていて、多くの開発者が「インターフェース化する」という悪い習慣を持ってるんだ。たった1つの具体的な実装しかないのにね;中にはDTOのためにやる人もいる。メソッドの「実装に移動」すると、インターフェースの宣言に行き着いて、そこからさらに手間をかけないといけない。実装が別のアセンブリにあるときは運が悪いよ。IDEは直接参照ならデコンパイルできるかもしれないけど、見つけてはくれない。運が悪いときは、デバッグして中に入るしかない。でも、これが依存性注入コンテナの話につながるんだ。もっと強力なもの(例えば、Autofac)は階層的なスコープを確立できて、新しいスコープが登録を(再)定義できる。LISPの動的スコープ変数に似てるね。サービスが実行時に解決されるものは、現在のDIスコープ階層に依存する。ここで言いたいのは、効果はある程度、ISomeEffectHandlerのインスタンスをクラスやメソッドに注入して、そのメソッドを呼び出すことでシミュレートできるってこと。効果がどう処理されるかは、現在のISomeEffectHandlerのDI登録によって決まるから、プログラム全体で動的に変えられるんだ。だから、void DoSomething(...) { throw SomeException(...); }を書く代わりに、インターフェースIErrorConditionsを通じてエラープロトコルを確立して、void DoSomething(IErrorConditions ec, ...) { ec.Report(...); }って書くんだ。(あるいは、クラスメンバーとして注入することもできる。)今、現在インストールされているIErrorConditionsの実装は、スローしたり、ログを取ったり、何でもできるよ。この考え方をyieldみたいなもので完全には追求してないけど。

https://math.andrej.com/2010/09/27/programming-with-effects-... 新しいバージョンは静的型だけど、型やカテゴリを紹介せずにこのアイデアをうまく説明してる。彼らは「自由代数」や「ユニークホモモルフィズム」を使ってるけど、単に「項」と「評価」と考えればいいよ。Andrej Bauerが「一般化された引数を持つパラメータ化された操作」と説明しているのが、私は単に形の抽象[0, 1]だと思う([2]を見て)。だから、代数効果の概念を使って抽象代数をプログラミング言語に変えるのに役立つかもしれないね。

失敗したときに実行されるコードをどうやって見つけるの? それがポイントの一部なんだよね:動的コード注入だよ。これを実装するために、浅いバインディングや深いバインディングの戦略を使えるんだ。動的っていうのは、呼び出し元のフレームやその呼び出し元のフレームによってバインディングが導入されるってことだから、そう、理論的にはスタックをたどる必要がある。 そして、これは静的にはできない(つまり、IDEが定義にジャンプできない)、 その通り、これは_動的_な機能だからね。でも、気にしなくていいって期待されてるんだ。なんでかっていうと、純粋なコードを書いてるけど、その効果によって変わるから。で、その効果は文脈によって純粋だったり不純だったりする。だから、プロダクションで使えるコードがあって、テスト用にモックと接続できるんだ。モックは実際のIO効果以外の効果を挿入するだけ。これが依存性注入なんだよ。普通のモナドでもできるし、それはもっと静的な機能だけど、使ってるモナドがどこで実際にインスタンス化されるかを見つけるためには、やっぱりスタックをずっと上まで見ないといけない。つまり、これらの技術からいくつかのメリットを得られるけど、代償もあるってこと。メリットと代償は表裏一体で、コード注入ができてテストやサンドボックスが可能になるけど、何が起こっているのかが分かりにくくなるってことだね。

可読性の問題は、LSPがエディタに仮想テキストを表示するように指示することで解決できると思う。fooとbarの呼び出しがエラーになるかもしれないことを示すんだ。正直、2つ目のポイントは理解できてないんだ。もしfooとbarの定義からどのコードがエラーを処理するかを静的に決定できるなら、fooやbarがエラーになる理由はないし、エラーハンドリングのコードを呼び出せばいいだけだよね。もしfooとbarがResultの合計型を返して、my_functionがそのエラーを上に渡すだけなら、何も変わらない。my_functionの呼び出し元がそのエラーで何をするかはわからないから。

fooやbarが失敗する可能性が示されていない…失敗したときに実行されるコードをどうやって見つけるの?もしそれを探しているなら、私のHaskellエフェクトライブラリBluefinを試してみるといいかも(ただし、「代数的」エフェクトライブラリではないけど)。同等のコードは、myFunction :: e :> es -> Exception String e -> Eff es r myFunction ex = do x これがあなたの質問の最初の部分に答えてるよ:ex引数(Exception Stringハンドル)の存在は、String値の例外がどこで使われてもスローされる可能性があることを示してる。例えば、LibraryC.fooはその例外をスローしないことがわかってる。これがあなたの質問の2つ目の部分にも答えてる:失敗時に実行されるコードは、正確にそのException Stringハンドルを作成したコードだよ。そのハンドルから発生する例外は、常にハンドルが作成された場所で捕まえられ、他の場所では捕まえられない。例えば、ここかもしれない:try $ \ex -> do v tryは例外を捕まえて、HaskellのEither型のLeftブランチに変換する。あるいは、ここかもしれない:myFunction :: e :> es -> Exception String e -> Eff es r myFunction ex = do catch (\ex2 -> do x logErr errMsg) だから、LibraryB.fooからスローされた例外は常にlogErrで処理され(他の場所では処理されない)、LibraryA.fooからスローされた例外は常に上位のexを作成した例外ハンドラーで処理される(他の場所では処理されない)。どう思う?

もしかしたら私は古い考え方かもしれないけど、代数効果が普及することに対する著者の希望には賛同できないな。たまには役立つこともあるけど、動的スコープとの類似性は痛い思い出を呼び起こすから。

心配しなくても大丈夫。最もシンプルでフレンドリーな効果ライブラリでも、 https://effect.website これが主流の概念にはならない理由をはっきり示してるよ。価値提案は、もっと複雑な並行性のニーズがあるときにしか存在しないからね。でも、それは今多くの人が書いているアプリケーションのほんの一部に過ぎないよ。

効果を比較するのに、ジェネレーターを使うのはどう? 効果に関しては、例外よりもずっと近いと思うよ。とにかく素晴らしい説明だった。これについて読むのは初めてだったけど、すぐに分かったよ。

例外は通常、例外を「発生させる」(throw)ための構文と、例外を処理する(try-catch)ための構文がほとんどのプログラマーにとって馴染みがあるから使われるんだ。これは、効果を「発生させる」構文と効果を処理する構文と基本的に同じだけど、後者には関数の再開も含まれるんだ。代数効果でジェネレーターがどう実装されるか見てみたいな。

代数効果はすごく面白そうだね。前にこのアイデアを聞いたことがあるけど、静的型システムの領域に属するものだと思ってた。静的型システムが好きじゃないから、あんまり深く調べなかったんだ。でも、Effの以前の動的バージョンに関するこの記事を見つけたよ。

静的型システムって何が悪いの?

新しいアイデアが(おそらく)カテゴリ理論から来ているのを見ると、それが本当にメインストリームの言語に入るのか気になるんだよね。自分の経験では、言語の哲学的なレベルでの一貫性が、プログラミングとビジネスの両方に精通したプログラマーのチームで働くのが楽しい理由なんだ。問題を解決するためのプログラミングパターンのセットは、同じ「イリティ」全てを持ちながら、ビジネスの問題を解決する別のパターンのセットに置き換えられることが多い。そこで質問なんだけど、メインストリームの言語は代数的効果(ハンドラー?)を取り入れられるのか、それともこれらの抽象を基に新しい言語をゼロから作る必要があるのかな。

基本的に、Reactのフックがそれだね。言語レベルではないけど、広く使われて理解されてる。

メインストリームの言語は代数的効果(ハンドラー?)を取り入れられるのか、それともこれらの抽象を基に新しい言語をゼロから作る必要があるのかな。代数的効果は、依存性注入の変種/強化版で、言語に形式化されてるんだ。依存性注入は、ライブラリの実装だけで長い間広く使われてきたよ。

AE(代数的効果)はすごく面白い!素晴らしい記事、ありがとう。読んでて、大きなプロジェクトでの使いやすさについていくつか懸念があるんだ。主に「ジャンプする」ことに関して。> 代数的効果は、クリーンなAPIを設計するのを楽にすることもある。これは議論の余地があるね。間接的なレイヤーが追加されるから(これは多くの実際の非AEコードベースにも存在することを認めるけど)。私の主な懸念は、コードにブレークポイントを置いたとき、どこで作成されたオブジェクトを扱っているのかをどうやって見つけるかってこと。明示的な引き渡しがあれば、スタックトレースを上下にたどって見つけられるけど、AEの合成だとインスタンス化のソースを見つけるのが難しいことがある。ジャンプしなきゃいけないから、よくある「ヨーヨー問題」[1]に繋がる。AEについての個人的な経験はないけど、この記事によるとPythonのジェネレーターと同じだって(AEはジェネレーターを実装するのに使える)。大きくて複雑なジェネレーター式を扱うのは、私の経験では非常に面倒でエラーが起きやすかった。> そして、これを使って1つ以上のコンテキストオブジェクトを使うコードをきれいにするのを手伝える。関与する関数は、まだそのシグネチャにcan Use Stringsを書く必要がある。実用的な観点から見ると、明示的に文字列を渡すのとcan Use Stringsのシグネチャを追加するのとの違いが見えない。既存の関数に追加のコンテキストを渡す場合、すべての関数に行って適切な処理を追加する必要があるから。--- 私の理解では、AEは低レベルではレジスタ処理を伴うlongjmp命令として実装されている(再開できるように)。これを考えると、AEがたくさんあるコードベースでさまざまな方法で合成すると、深刻なヨーヨー問題に陥るのは避けられないだろうし、コードが何をしているのか本当に迷子になるかもしれない。これは一人でやっているプロジェクトではそれほど深刻ではないかもしれないけど、大きなチームではコードベースを頭に入れていないと、これは大きな効率の問題になるかも。ところで、AEが再開のためのメモリアロケーションをどう扱っているのか理解している人がいたら、良いリンクを教えてほしいな、ありがとう![1]: https://en.wikipedia.org/wiki/Yo-yo_problem

ちょっと鈍いかもしれないけど、よく分からなかったし、例もあんまり役に立たなかった。例えば、最初の例のSayMessageって、効果を表してるの?なんで?関数のシグネチャを見る限り、何も効果がない(noop)かもしれないし、違いが分からないよね。これは勝手に決められてるの?副作用のある操作の表記についての話なのかな?

推測するに、この関数はどこかでfopenやfwriteを呼び出す感じかな。もちろん、これについて訂正してもらえると嬉しい。

それは効果として宣言されていて、ハンドルを実装してるからだよ。もっとインターフェースみたいな感じで考えてみて。多くの一般的なパターン、例えば非同期処理やIO、yieldingなんかは、ハンドルを使って表現できることが分かったんだ。そして、その効果はシグネチャに表せる。これによって、コードが実行される時にどの効果で動いてるかが分かるようになる。他のコメントでも指摘されてたけど、依存性注入にすごく似てるね。