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 は OO がないからダメだ!」という考えはありません (むしろそういった原理主義とは対極のポジションです)。
Julia に限らず新しい言語やシステム等に着手するときには:
を手探りしています。そしてその過程から出る「Julia は OO がないな、手続き型で書くのは辛いな」というような何かを試行錯誤をしているときの一連のツイートは:
といったことを目的とした可視化作業です (Twitter はそれを気軽にできる最適なツールです; まぁ誰かに「それ○○でできるよ」とキーワードを頂けるかもなスケベ心も否定はしませんが)。
ただ、それらを逐一検索で拾って「こいつは何も分かっていない! ロクに調べもせずに Julia を非難している!」と何らかの悪意を見いだされているような方がいらっしゃいますが、それは考えすぎです (正確に言うときっかけとなった引用ツイートにそういった意図を感じたためこの記事といくつかのツイートに関してはいささかうんざりしながら(+深夜遅くもあり)後ろ向きな気分で書いています)。