🎉

Algebraic Effectsで実現するシンプルなDI

2022/09/03に公開

成果物

まずは成果物から紹介します。これが動きます。

from typing import Protocol

class Animal:
    def __init__(self):
        self._sound = use(AnimalSound)

    def make_sound(self):
        self._sound.make_sound()


class AnimalSound(Protocol):
    def make_sound(self):
        ...


class CatSound:
    def make_sound(self):
        print("Meow")


class DogSound:
    def make_sound(self):
        print("Bark")


if __name__ == "__main__":
    with register(AnimalSound, CatSound):
        cat = Animal()
        cat.make_sound() # Output: Meow

Algebraic EffectsとDI

Algebraic Effectsについては前の記事で参考文献としてあげた記事
https://nymphium.github.io/2019/12/07/ruby-ae.html
やDan Abramov氏の記事
https://overreacted.io/ja/algebraic-effects-for-the-rest-of-us/
を見ていただくとして、Algebraic Effectsの応用としてdependency injection (DI)があります。
Dan Abramov氏の記事の記法で言うなら、欲しくなった時にperformして要求して、注入するものをresume withで渡せば良いわけですね。

try {
  const db = perform "db";
} handle(effect) {
  if (effect === "db") {
    resume with TestDB();
    // resume with HonbanDB(); 本番環境はこっち
  }
}

この手法の良いところは、シンプルなところだと思います。特にPythonでReader Monadを使ってDIしようとなると大変です。比べる相手がおかしいな?

ともかく、折角PythonでAlgebraic Effectsしてる身としては、いい感じにDIしてみたいわけです。

実装

実装は私の前の記事のAlgebraic Effectsの実装ではなく、既存のライブラリを使っています。
https://pypi.org/project/affection/
私の実装はcoroutineベースですが、affectionはstack frameに手を入れるタイプです。

と言うのも、Pythonのcoroutineはstackless coroutineであり、関数の中の関数の中の関数、のような「深いところ」でyieldされると、それを受け取るのが困難です。これはruffが使用しているRubyのFiberがStackful coroutineであることとは対照的です。
もちろんfrom yieldがあるのですが、これも曲者でした。実は同じstackless coroutineであるJSのGeneratorを使ったAlgebraic Effectsのライブラリ
https://github.com/makenowjust/eff.js
もあるのですが、ここで使用されているreturn yield*とは微妙に挙動に差があり、Pythonで再現するのは結構きついです。腕に覚えのある方は是非やってみてください[1]
というわけで、自分の実装を使うのは諦め、affectionを使用しています。
ちなみにaffectionを使ったDIの実装はとても短いです。

from affection import perform, effect, Handle
import contextlib

def use(T):
    return perform(effect(T.__name__, T))

@contextlib.contextmanager
def register(T, U, args=None):
    with Handle() as h:
        @effect(T.__name__, T).handle(h)
        def _(_) -> U:
            if args is None:
                return U()
            return U(*args)
        yield

affectionをちょっとラップしただけですね。

おわりに

PythonにおけるDIとしてAlgebraic Effectsを利用する手法を紹介しました。

脚注
  1. https://zenn.dev/catminusminus/articles/a417b8ee7cf1f6 の最後の実装で、mefef2が値を返すケースで問題となります。{value, done}が帰ってくるJSのGeneratorと異なり、「終わったこと」を検知しつつ戻り値を伝播させていくのがかなり難しいと思います。 ↩︎

Discussion