今天來談談 Julia 的多重分派(multiple dispatch)。

很顯然地,多重分派有別於單一分派(single dispatch),而單一分派是在現今主流的物件導向程式設計中常見的方式。

在現今的物件導向程式設計當中,我們會將函式歸納到某個類別底下成為其類別的方法,而這個方法就屬於這個類別。

多重分派 v.s. 單一分派

這樣的觀點就是單一分派的精神,單一分派是指一個方法的呼叫,要如何決定呼叫的是哪一個方法實作呢?

1
2
3
4
5
6
7
8
9
10
class Foo:
def abc(self, x):
return str(x)

class Bar:
def abc(self, x):
return int(x)

foo = Foo()
bar = Bar()

像以上的 python 程式碼,foo.abc(x) 就是去呼叫 Foo 的方法,而 bar.abc(x) 則是去呼叫 Bar 的方法。

也就是說,方法的呼叫是由第一個參數所決定的,這邊要注意的是第一個參數並不是 x,而是 self 喔!也就是物件本身!

相對於單一分派,多重分派是指他會參考所有的參數型別及其組合來決定到底要呼叫哪一個方法。

所以我們也有了更細緻的選擇。

1
2
3
4
abc(foo::Foo, x::String) = x
abc(foo::Foo, x::Int64) = String(x)
abc(bar::Bar, x::String) = Meta.parse(x)
abc(bar::Bar, x::Int64) = x

當第一個參數型別是 Foo 時,就回傳字串,而當第一個參數型別是 Bar 時,就回傳整數。

我們可以去區別,當 x 已經是整數或是字串時就不用去處理它,直接回傳即可。若是有不同型別時,個別處理。

這邊我們並沒有使用到第一個型別的參數,在 Julia 裡可以省略變數本身,只寫型別。

1
2
3
4
abc(::Foo, x::String) = x
abc(::Foo, x::Int64) = String(x)
abc(::Bar, x::String) = Meta.parse(x)
abc(::Bar, x::Int64) = x

多重分派的自由與限制

使用多重分派可以讓語言有更細緻的定義,也有更高的自由度。

我們可以看看以下的例子,這邊使用了常常被使用的 null pattern:

1
2
3
push!(ls::List, obj::NullObject) = ls

psuh!(ls::List, obj::Object) = push!(ls.array, obj)

當我要去表示一類物件,我們會實作 Object,但往往我們會想要表示一個為空的物件,這時候我們就會有一個 NullObject 來表達這件事。

當我們想要將一個 Object 放到 List 中的時候,我們會實作 push!,然而當一個 NullObject 被放入 List 的時候,我們並不需要真的做什麼樣的動作。實作就會如同上面的程式碼。

相對在 python 或是一般物件導向語言當中,我們就會看到以下的程式碼:

1
2
3
4
5
6
class List:
def append(self, obj):
if type(obj) == NullObject:
return self
elif type(obj) == Object:
self.array.append(obj)

我們需要自己去判斷接收進來的參數型別,然後進一步做處理。這樣的話會讓我們的程式碼充滿 if-else,當條件一多時,會變得相當難以閱讀及修改。

使用多重分派並不是只有優點,他也有缺點。多重分派會引入模糊性(ambiguity)。

考慮以下程式碼:

1
2
3
4
5
foo(a::A, b) = a
foo(a, b::B) = b

a = A()
b = B()

請問 foo(a, b) 該呼叫哪一個方法呢?

這樣的呼叫在 Julia 會產生錯誤,也就是編譯器不知道該呼叫哪一個方法。

這時候編譯器會建議修改的方式,就是定義一個更明確的方法:

1
foo(a::A, b::B)

應用情境

我們來寫個簡單的剪刀石頭布遊戲!

1
2
3
struct Paper end
struct Scissor end
struct Stone end

我們分別有三個型別分別代表剪刀石頭布。

接下來我們定義運算,先定義贏的狀況:

1
2
3
play(::Paper, ::Stone) = 1
play(::Scissor, ::Paper) = 1
play(::Stone, ::Scissor) = 1

當雙方是相同時就平手:

1
play(::T, ::T) where {T} = 0

這邊我們利用了 Julia 的參數化方法,當兩個有相同的型別時,就回傳 0。

最後,是輸的狀況:

1
play(a, b) = -1

也就是,如果沒有符合以上的狀況的方法呼叫,就會落到這個狀況來,所以這邊我們允許最廣義的 Any 型別。

希望大家可以經由這個簡單的例子來理解 Julia 的多重分派有多好用,甚至是搭配上參數化方法根本是逆天阿!