Julia の多重ディスパッチは OO で言うところの多態化となるか? についてのポエム。
target version: Julia 0.6.2
普段は Scala, Java, Python, Ruby あたりを使っていますが、Julia はまだ言語仕様を見ながらコードを書いている状況なので、Julia を使いこなせる方が「えっそれをするためにこういう機能があるよ?」(知ってるかどうか問題だったオチ) であれば話は終わります。多分、それ以上読み進めるのは時間の無駄です。
Julia の多重ディスパッチは実行時の型から対応する関数 (振る舞い) を推定して選択するが、関数を使用する時点で全ての可能性のある型について自明でなければならない制約があります。
例えば以下のソースでは save()
に渡される可能性のある型 A
, B
に対して、それぞれ JSON 形式の文字列を参照する関数 to_json(::A)
, to_json(::B)
が save()
時点で決定しているため多態のように振る舞うことができます。
module Sample1
# A と B の型があり
struct A x::Int end
struct B y::Int end
# それぞれにディスパッチ可能な関数が自明なら
to_json(a::A) = "{x:$(a.x)}"
to_json(b::B) = "{y:$(b.y)}"
# 多重ディスパッチで obj の型から対応する関数を選択/実行することができる
save(obj) = println(to_json(obj))
save(A(2018)) # {x:2018} → OK
save(B(0116)) # {y:116} → OK
end
これは to_json()
の挙動が型に合わせて適切に置き換わっているように見えます。では開発担当を複数人に分けてモジュールももう少し複雑になったときも同じ方法でできるでしょうか。
他人が作ったフレームワークや共通機能を使う場合、save()
の実装時点で obj
として渡される可能性のある (どこかの馬の骨が作ったか分からない) 型 B
および対応する to_json(::B)
は参照できません。
module Sample2
module Framework
export A, save
# 型 A のみ対応する関数が定義されている
struct A x::Int end
to_json(a::A) = "{x:$(a.x)}"
# 実行時に obj の型から解決できるのは A だけ
save(obj) = println(to_json(obj))
end
module Application
using Sample2.Framework
# 型 B (本来は A のサブタイプにしたいところだが) に対応する関数は当然ながら Framework.save()
# からは不可視
struct B y::Int end
to_json(b::B) = "{y:$(b.y)}"
save(A(2018)) # {x:2018} → OK
save(B(0116)) # ERROR: MethodError: no method matching to_json(::Sample2.Application.B)
end
end
この場合 save(obj, to_json::Function)
のように定義して obj
に対応する振る舞いの関数 to_json(::B)
をセットで渡さなければならず、多重ディスパッチは多態化を代替する機能にはなっていません。
一方で OO の場合は to_json(::B)
関数は obj::B
に付随するため save()
内で obj
の具体型が何かを認識する必要はなく、save()
は (A
の
サブタイプであれば) 任意の型を取ることができます。つまり save()
に渡される可能性のある型が実装時点で全て自明である必要がない点が違います。OO でライブラリ設計を行う開発者はこの動作を前提としています。
擬似コードで表すなら以下のような感じ。
module Framework
export A, save
class A
x::Int
to_json() = "{x:$(this.x)}"
end
save(a::A) = println(a.to_json())
end
module Application
using Framework
class B(x) <: A(x)
y::Int
to_json() = "{x:$(super.x),y:$(this.y)}"
end
save(A(2018)) # {x:2018}
save(B(2018, 0116)) # {x:2018,y:116} ← 期待する動作
end
「オブジェクトごと B.to_json()
を渡すことと to_json(::B)
関数で別に渡すことは実質同じだ」という意見はごもっともです。ただ、こういった問題を簡潔かつ安全に解決できるよう設計された言語かという点が OO のパラダイムを持つ言語かの判断になります。でないと関数ポインタがあるんだから C 言語は実質多態化が可能だ (第一級関数を持つ言語は全て多態化が可能だ) ということになり OO is 何? となってしまうので「○○すれば××できる」論議は不毛です。
以上より、私としては「多重ディスパッチは特定の状況下で多態化の代替として機能するが、多態化そのものの代替となりうる機能ではない」と考えています。
いまのところ Julia は計算科学方面のペラッとしたコードを書く用途が多いようなので多重ディスパッチでも十分機能すると思いますが、今後汎用言語として Python や Ruby の代替を目指すなら、複雑にモジュール化した実装を行う上で露呈してくる部分かなと思っています。
なお Pyhon や Ruby のように将来の Julia のバージョンでクラスやインターフェース等の OO 的なパラダイムが追加されても問題のない言語仕様になっているとは思います (開発側にその意思があるかは分かりませんが)。
この方法な何も特殊な方法ではなくて,Juliaでは日常的に使われています。こうした仕組みは,Juliaでは一般にprotocolと呼ばれています。RustのtraitやHaskellのtype classと(静的検査・動的検査の違いはあれど)基本的には同じ仕組みです。例えば,ある型についてfor文で反復をしたい場合には
Base.start
,Base.done
,Base.next
という3つのメソッドを定義します。それぞれの引数と返り値はドキュメントに記載されています。これは苦肉の策でもなんでもなく,ドキュメントに従って正しく実装すれば問題なく使えます。私のパッケージでも,I/Oのインターフェイスとエンコーダ・デコーダの実装を分離するためにプロトコルを定義しています (https://github.com/bicycle1885/TranscodingStreams.jl/blob/3c869fac2e3cc2051340dfaccb6c07582f2b414a/src/codec.jl)。具体的に挙げてらっしゃる2つの例がよく分からないのですが,これらはpolymorphismだとどのように解決されるのでしょうか。