🪆

Go でも python みたいなデコレータを書きたい!

2024/02/12に公開

どうも、youta-t です。
最近は https://github.com/youta-t/its をつくってます。

さて、今日も今日とて go のコードを捏ねながら遊んでいたわけですが、go でも python のデコレータみたいなやつがかけるな、ってことに気が付きました。
しかも、「どんな関数でも包めるやつ」がつくれます!

あの言語のアレ: python のデコレータ

突然ですが python の話をします!

python には「デコレータ」という機能があって、まあ要するに「関数を引数に取って関数を返す関数」のことですね。

@deco
def func1():
    ...

こう書いてやると、関数 func1 が関数 deco に渡されて、その戻り値が最終的な func1 になるのでした。

いろんな関数に、その本質にかかわらない機能を付加したりするのに便利です。

本体の実行前後にログを書かせる、とかね。

def log(fn):
    def wrapper(*args, **kwargs):
        print("before!")
        ret = fn(*args, **kwargs)
        print("after!")
        return ret
    return wrapper

@log
def func1(...):
   ...

この log みたいなやつは、どんな関数でも引数にとれて、とにかくソレを実行する前後に余計な処理(ここでは print)を差し挟んでくれるわけです。

で、これを go でやろうってワケ

さすがに go には

@deco
func F() { ...

みたいな文法は ない ので諦めるとして、できる範囲のことをやりましょう。

「どんな関数でもラップできる」。これを目指すことにします。

......ということで、いきなり手法を出すと、こうです。

func deco[T any](fn T) T {
	rfn := reflect.ValueOf(fn)

	switch rfn.Kind() {
	case reflect.Func:
		// ok!
	default:
		panic(fn should be a function")
	}

	wrapper := reflect.MakeFunc(rfn.Type(), func(args []reflect.Value) (results []reflect.Value) {
		fmt.Printf("before!")
		defer fmt.Printf("after!")
		return rfn.Call(args)
	})

	return wfn.Interface().(T)
}

正解はリフレクション!

これは何をやってるんですか?

reflect.MakeFunc[1] という、実行時に関数を生成できる関数を使っています。

  • 引数として reflect.MakeFunc(元の関数の型, リフレクション的な関数)を取ることになっていて、第一引数が関数のシグネチャ、第二引数が実装を決めています。
  • 第二引数の関数には、実際に呼び出された際の引数が reflect.Value の形で与えられ、また reflect.Value の形で返すということになっています。

要するに、第二引数で好き勝手なことをしたらいいんだ、ということですね。
そういうわけで、元々の関数(のリフレクション版)を第二引数の中で呼び出すことで、 「関数を包む」を実現しています。

しかもこの実装は、包まれる側の関数の型に依存せず、とにかく関数であればなんでもいいようになっています。 deco自体は型引数 T について「Tを取ってTを返す」関数なので、decoを通しても、元の関数の型情報は保たれます。

これでシグネチャごとにラッパを繰り返し書く生活からはおさらばだぜ!

ただし、デコレータ関数(deco)の引数 T の型制約は any とせざるを得ないことには注意してください。「任意の関数」という型を言い表す方法が、go にはないからです[2]

これはなんの役に立つのですか?

...さあ...?

脚注
  1. https://pkg.go.dev/reflect#MakeFunc ↩︎

  2. python でいうところの Callable みたいな概念ですね。go にもあったら良かったのですが、リフレクションの世界にしか存在しないようです。
    入力値が、関数かどうかは確認しておきましょう。 ↩︎

Discussion