Algebraic Effectsで実現するシンプルなDI
成果物
まずは成果物から紹介します。これが動きます。
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については前の記事で参考文献としてあげた記事
やDan Abramov氏の記事
を見ていただくとして、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の実装ではなく、既存のライブラリを使っています。
私の実装はcoroutineベースですが、affectionはstack frameに手を入れるタイプです。と言うのも、Pythonのcoroutineはstackless coroutineであり、関数の中の関数の中の関数、のような「深いところ」でyield
されると、それを受け取るのが困難です。これはruffが使用しているRubyのFiberがStackful coroutineであることとは対照的です。
もちろんfrom yield
があるのですが、これも曲者でした。実は同じstackless coroutineであるJSのGeneratorを使ったAlgebraic Effectsのライブラリ
もあるのですが、ここで使用されている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を利用する手法を紹介しました。
-
https://zenn.dev/catminusminus/articles/a417b8ee7cf1f6 の最後の実装で、
m
やef
やef2
が値を返すケースで問題となります。{value, done}
が帰ってくるJSのGeneratorと異なり、「終わったこと」を検知しつつ戻り値を伝播させていくのがかなり難しいと思います。 ↩︎
Discussion