多層ディスパッチ
多層ディスパッチとは、複数バージョンを持ち、そのバージョンが引数によって区別されるような関数のことを指すらしい。
Python2.4で実装されたmmモジュール内のmultithred関数を参考に、多層ディスパッチの実装について理解を試みた。
単に実装したいだけなら、以下のように実装できる。
def foo(a, b): if isinstance(a, int) and isinstance(b, int): ...code for two ints... elif isinstance(a, float) and isinstance(b, float): ...code for two floats... elif isinstance(a, str) and isinstance(b, str): ...code for two strings... else: raise TypeError("unsupported argument types (%s, %s)" % (type(a), type(b)))
ただこれはスマートでない。モジュールとして、関数に適用できるようにしたい。
これを実現するために使用するMultiMethodクラスの実装は以下のようになっている。
registry = {} class MultiMethod(object): def __init__(self, name): self.name = name self.typemap = {} def __call__(self, *args): types = tuple(arg.__class__ for arg in args) # a generator expression! function = self.typemap.get(types) if function is None: raise TypeError("no match") return function(*args) def register(self, types, function): if types in self.typemap: raise TypeError("duplicate registration") self.typemap[types] = function
着目すべきは、関数を管理するためのregistry
辞書と、引数の型を管理するためのtypemap
辞書を定義している点だ。
インスタンス作成時の流れは以下のようになる(__init__()
コンストラクタ呼び出し時)。
self.__init__()
コンストラクタにより、与えた引数名のMultiMethod
オブジェクトと空のtypemap
辞書を作成する。__call__()
メソッドによって、MultiMethod
オブジェクトは呼び出し可能となっている。
インスタンスに関数を登録する際の流れは以下のようになる(register()
メソッド呼び出し時)。
- 既に登録しようとする型がインスタンス内の
typemap
内で定義されている場合、TypeError
を起こす。 - 定義されていない場合はその型と関数を
typemap
辞書で対応付ける。
最後に、インスタンス内の関数が呼ばれたときの流れは、以下のようになる(__call__()
メソッド呼び出し時)。
- 呼び出す関数を引数の型を参考に、インスタンス内の
typemap
辞書内から探しだす。 - 見つからない場合、
TypeError
を出力する。 - 見つかった場合、引数とともに、その関数を実行する。
multimethod
デコレータは、MultiMethod
オブジェクトの呼び出しと、register()
メソッドを用いて、以下のように実装される。
def multimethod(*types): def register(function): name = function.__name__ mm = registry.get(name) if mm is None: mm = registry[name] = MultiMethod(name) mm.register(types, function) return mm return register
こうすることで、引数の型によって呼び出されるインスタンスが変わることになる。
キーワード引数を持った関数の場合は、以下のように実装すればよい。
@multimethod(int, int) def foo(a, b): ... @multimethod(int) def foo(a): return foo(a, 10)
多層ディスパッチの実装においては、引数が1つの場合と2つの場合とで全く別の関数として考えられることに着目する。
そのうち複数のタイプを1回の実行で設定したくなるかもしれない。 そんな場合は、以下のようにデコレータをスタックすればよい。
@multimethod(int, int) @multimethod(int) def foo(a, b=10): ...
以上、pythonでの実装例でした。
Juliaでは!?
標準で多層ディスパッチをサポートしている。
Lisp言語でもサポートされているらしい。
この実装をどういうときに使うべきか、まだいまいち理解できていないがとりあえず学んでおく。
引数を<引数>::<型>
のように指定することで、関数に型を登録できる。
function add(x::Int, y::Int) return x + y end
pythonの実装で見たように多層ディスパッチを用いると、型が合う場合のみしか関数呼び出しができない。
julia > add(1.0, 2.0) ERROR: MethodError: no method matching add(::Float64, ::Float64) Stacktrace: [1] top-level scope at none:0
もちろんある型より高位の抽象型で定義すれば、その型は定義した関数により呼び出し可能となる。
例えば以下のようなNumber
型で定義した関数は
function add(x::Number, y::Number) return x + y end
Int64
型の引数でも呼び出し可能だし、
julia> add(1, 3) 4
Float64
型の引数でも呼び出し可能となる。
julia> add(1.00, 3.14) 4.140000000000001