依型別分派(dispatch on types)算是在 Julia 語言中相當常見的一個技巧,它依賴 Julia 的多重分派機制(multiple dispatch),可以依據不同的型別有不一樣的行為。

依據不同的型別有不同的行為,而不是實體。直觀上看起來會有點像傳統物件導向當中的 class method,不過在 Julia 當中還有更多用途。

Convert

在 Julia 的 Base 中有許多應用到這樣技巧的例子,這邊就舉 convert 為例。

convert 是一個轉換函式,它可以將特定物件轉換成特定的型別,所以物件跟型別就分別是它的參數。

1
convert(Char, 10)

這樣可以將 10 轉換成一個字元的型別,就會變成 '\n'

1
convert(Array{Float64}, Any[1 2 3; 4 5 6])

或是一個將裝有 Any 型別元素的矩陣,轉成 Float64 型別元素的矩陣。

以上的實作大概會類似:

1
2
3
function convert(T::Type{Char}, val)
T(val)
end

基本上會試圖呼叫該型別的建構子,而第一個參數上也會加上 Type{...} 的型別。

Read

或是我們也可以在讀取二進制資料當中發現這樣的模式。

1
2
io = IOBuffer("JuliaLang is the best.")
read(io, Char)

在讀取 IOBuffer 的二進制資料當中,可以將資料解析成 Char 的型別。

1
read(io, String)

或是解析成 String 的型別。

在這邊我們看到的都是將型別作為參數給進函式中,函式可以藉由得到的型別做不同的事情。

其中一個目的是依據型別做分派(dispatch),也就是不同的型別會對應到不同的方法實作上。

read(io, Char)read(io, String) 的實作方式是截然不同的,畢竟要解析成不同的型別,方法會是不同的。

也有可能會像是 convert 一樣,再度利用給進來的參數。由於 Julia 的型別本身也是一個物件,呼叫型別本身也等同於呼叫型別的建構子,所以我們可以看到在 convert(T::Type{Char}, val) 中,呼叫 T 作為建構子的方式來轉換物件的型別。

用在哪裡?

這樣的技術可以被用在哪些場景呢?

通常需要傳型別到其它函式,可以用來呼叫其建構子,而函式則提供一個統一的介面,可以適用於創造物件的場景。

以下就來示範有不同種飲料被製作出來的過程吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract type Beverage end

struct MilkTea <: Beverage end
struct GreenTea <: Beverage end
struct BlackTea <: Beverage end

abstract type Topping end

struct TapiocaBall <: Topping end
struct Pudding <: Topping end

struct BubbleMilkTea <: Beverage end
struct PuddingMilkTea <: Beverage end
struct BubbleGreenTea <: Beverage end
struct BubbleBlackTea <: Beverage end

這邊定義了奶茶、綠茶及紅茶幾種飲料,然後還可以對飲料加料,加料之後的飲料就會變成其他種的飲料。

接下來就可以來決定加什麼料會變成什麼樣的飲料。

1
mix(::Type{MilkTea}, ::Type{TapiocaBall}) = BubbleMilkTea()

像是把奶茶跟波霸加在一起,就變成了波霸奶茶。

1
mix(::Type{MilkTea}, ::Type{Pudding}) = PuddingMilkTea()

如果把奶茶跟布丁加在一起就變成布丁奶茶囉~~

1
2
mix(::Type{GreenTea}, ::Type{TapiocaBall}) = BubbleGreenTea()
mix(::Type{BlackTea}, ::Type{TapiocaBall}) = BubbleBlackTea()

我們還有賣波霸綠茶跟波霸紅茶喔!

但是有些組合沒有在菜單上,因為老闆覺得沒有在菜單上的組合喝起來很噁心,所以不打算提供,像是布丁加綠茶這種組合。

1
2
3
4
5
julia> mix(GreenTea, Pudding)
ERROR: MethodError: no method matching mix(::Type{GreenTea}, ::Type{Pudding})
Closest candidates are:
mix(::Type{MilkTea}, ::Type{Pudding}) at REPL[13]:1
mix(::Type{GreenTea}, ::Type{TapiocaBall}) at REPL[14]:1

在這邊 mix 就提供了一個統一的介面來混合飲料跟加料的部份。

提供這樣單一的物件創造介面就類似於物件導向中的 factory method pattern。

依參數型別分派(2021.5.24 補充)

如果有個參數化型別 Foo,它帶有一個型別參數 K

1
julia> struct Foo{K} end

如果有個函式 foo 想依據不同 K 分派,例如,當 K=1 時,可以回傳一個 "BOOM!",否則就回傳 K 自己。

1
2
3
4
5
julia> foo(::Foo{K}) where K = K
foo (generic function with 1 method)

julia> foo(::Foo{1}) = "BOOM!"
foo (generic function with 2 methods)

這樣會發現我們也可以依據 K 的值分派到不同的函式。

1
2
3
4
5
julia> foo(Foo{1}())
"BOOM!"

julia> foo(Foo{2}())
2

總結

我們可以發現到使用多重分派所帶來的一些好處。如果是在單一分派(single dispatch),也就是一般物件導向的語言中,他只能依據第一個參數做分派。然而,多重分派就可以考慮參數型別的排列組合去做分派,當然參數是型別也是可行的。

類似這樣的機制,也可以對應到在物件導向中的 strategy pattern,strategy pattern 是根據不同的演算法種類來做分派的,我們可以寫成像這樣:

1
2
sort(BubbleSort, xs)
sort(Heapsort, xs)

如此一來,就是一個完整的 strategy pattern 了。

最後,這邊介紹了如何利用多重分派的機制,來依據不同的型別做分派,並且做到不同的應用。