Julianへの道② - 多層ディスパッチ

多層ディスパッチ

多層ディスパッチとは、複数バージョンを持ち、そのバージョンが引数によって区別されるような関数のことを指すらしい。
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__()コンストラクタ呼び出し時)。

  1. self.__init__()コンストラクタにより、与えた引数名のMultiMethodオブジェクトと空のtypemap辞書を作成する。
  2. __call__()メソッドによって、MultiMethodオブジェクトは呼び出し可能となっている。

インスタンスに関数を登録する際の流れは以下のようになる(register()メソッド呼び出し時)。

  1. 既に登録しようとする型がインスタンス内のtypemap内で定義されている場合、TypeErrorを起こす。
  2. 定義されていない場合はその型と関数をtypemap辞書で対応付ける。

最後に、インスタンス内の関数が呼ばれたときの流れは、以下のようになる(__call__()メソッド呼び出し時)。

  1. 呼び出す関数を引数の型を参考に、インスタンス内のtypemap辞書内から探しだす。
  2. 見つからない場合、TypeErrorを出力する。
  3. 見つかった場合、引数とともに、その関数を実行する。

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

参考

https://www.artima.com/weblogs/viewpost.jsp?thread=101605