結局プログラミングを理解したけりゃオブジェクト指向を学べばいいじゃんという記事を見かけたので、関連してJuliaの多重ディスパッチについて書いておこうと思います。
プログラミングの理解に必ずしもオブジェクト指向は必要ありません。もちろん、オブジェクト指向で書かれたプログラムの理解をするためにはオブジェクト指向を学ぶ必要がありますが。
オブジェクト指向との違い
Juliaでは「オブジェクトがメソッドを持つ」のではなく、「関数が振る舞いを持つ」そしてその振る舞いは 引数の型の組み合わせで決まる という設計です。
利点
数学の「関数」みたいなのでわかりやすい。引数に何を入れればいいか、だけを考えれば良いので。
Juliaの多重ディスパッチをRPGで理解する
RPGの例を使って説明をしたいと思いますが、多重ディスパッチはオブジェクト指向とは逆です。
オブジェクト指向では、
オブジェクト指向
→ キャラクターが attack() を持つ
ですが、
Juliaでは
Julia
→ attack という関数があり、キャラの型によって動作が変わる
という形になっています。
型とはデータ構造
コンピュータ上のあらゆるデータは何らかの形で存在しているわけですが、どのような形で存在しているかをみるためのものとした型があります。型は元々あるものもありますが、自分で作ることもできます。
キャラクターの型を作ってみましょう。
struct Warrior
name
hp
strength
end
struct Wizard
name
hp
magic
end
これは単なるデータ構造です。オブジェクト指向ではクラスというものがあり、それと似ている気もしますが、最大の違いはここには攻撃メソッドを書かないことです。
振る舞いは関数に書く
さて、攻撃処理を書いていきましょう。
function attack(c::Warrior)
println("$(c.name) は剣で攻撃!")
end
function attack(c::Wizard)
println("$(c.name) は火の魔法を放った!")
end
これを実行するには、
hero = Warrior("アルス",120,85)
mage = Wizard("リナ",80,120)
attack(hero)
attack(mage)
ですね。出力結果は
アルス は剣で攻撃!
リナ は火の魔法を放った!
となります。戦士か魔法使いかで、攻撃方法が変わってくるわけですね。Juliaでは
attack(hero)
というものがあった時、変数の型を見て
attack(::Warrior)
を選んでいます。
オブジェクト指向との違い
オブジェクト指向であれば、
hero.attack()
mage.attack()
となりますね。Juliaでは
attack(hero)
attack(mage)
多重ディスパッチの強さ
さて、Juliaはどの関数を呼び出すのかを決めるのに、複数の引数の型を決めます。これが多重ディスパッチです。オブジェクト指向は、そのクラスが何か、というのを見ているので一つの引数から見ているのと同じですから、多くのオブジェクト指向言語ではシングルディスパッチです。
※補足
ここで述べているのは多くのオブジェクト指向言語で採用されている典型的なスタイルです。厳密には,多重ディスパッチとオブジェクト指向は対立する概念ではありません。 例えば CLOS (Common Lisp Object System) のように,多重ディスパッチを備えたオブジェクトシステムも存在します。
多くのオブジェクト指向言語(Java, C++, Python など)ではメソッドは主に1つのオブジェクトの型によって選ばれるsingle dispatch が使われていますが,Juliaでは引数すべての型に基づいてメソッドが選ばれるmultiple dispatch が中心になっています。
さて、
function attack(a::Warrior, b::Warrior)
println("剣士同士の剣撃!")
end
function attack(a::Warrior, b::Wizard)
println("剣士が魔法使いに斬りかかる!")
end
function attack(a::Wizard, b::Warrior)
println("魔法使いが剣士に火球!")
end
という関数を書いてみましょう。これは、攻撃相手を二つ目の引数に入れています。呼び出す時は、
attack(hero, mage)
attack(mage, hero)
となりますね。実行結果は、
剣士が魔法使いに斬りかかる!
魔法使いが剣士に火球!
となっています。これは、
attack(型1, 型2)
の型の組み合わせで呼ぶ関数が決まっているということです。これが、多重ディスパッチです。
継承がなくてもコードが再利用できる
さて、Juliaでは、
abstract type Character end
struct Warrior <: Character
name
end
struct Wizard <: Character
name
end
のようにします。これCharacterという抽象型を用意しています。
つまり、
抽象型 + 多重ディスパッチ
で再利用できるコードが書けます。例えば、
function move(c::Character)
println("$(c.name) は移動した")
end
とCharacterに対する動作を書いておけば、
move(Warrior)
move(Wizard)
が使えます。
科学技術計算での良さ
物理や数学では
f(x)
g(x,y)
のような形で関数を使って世界を記述しています。
xやyにはベクトルが入ったり行列が入ったりテンソルが入ったり、その他色々入りうるわけです。
Juliaでは
f(::Vector)
f(::Matrix)
f(::Tensor)
のように、入ってくる引数によって挙動を変えることができます。つまり数学の関数の拡張としてコードが書ける、というのが良い点です。