其實是受到 Julia 核心開發者之一 Lyndon White 的文章的啟發來撰寫本文的。

文章有 CSDNnews 簡中翻譯

本文並非以上文章的繁中版本,而是本人的一些心得及觀察。

載入套件

比較深入了解 Julia 的朋友,會了解到這個語言的設計與其他語言的不同,像是 using

當我們使用 using 來載入套件的時候,會發現有不少存在在 Base 中的函式是可以使用的。像是 length 可以用來取得陣列的長度,當你載入 DataStructures.jl 時,你同樣可以用 length 來取得 StackQueue 的長度。

或者是原文中的例子。使用者可以使用 NamedDims.jl 來為你的陣列的維度命名,而你的陣列需要使用 CuArrays.jl 送到 CUDA,這時候你不需要一個可以為 CUDA array 命名的套件來達成這件事。

你會發現在 Julia 語言中似乎不存在套件跟套件之間的差別,或是套件跟標準函式庫之間的區別。這是由於在 Julia 中對於命名空間(namespace)的概念較為薄弱,Julia 各模組之間仍然存在著命名空間,但是 Julia 在 using 時會將這些命名空間去除。這在工程上或許不是一個好的典範,因為會造成命名空間的汙染(namespace pollution)。反過來說,它促使人們相互溝通及協調,來提供更好的套件之間的可組合性。

在其他語言中,常常會告訴你,使用某個套件只需要載入你需要的部份,像是 using Foo: bar, baz,然而 Julia 並不去特別強調這點,using Foo 會載入套件開發者有導出的部份。如果有兩個套件都提供了 predict 來支援他們的模型,這邊借原文的例子再次說明:

1
2
3
4
5
6
7
using Foo
using Bar
training_data, test_data = ...
mbar = BarModel(training_data)
mfoo = FooModel(training_data)
evaluate(predict(mbar), test_data)
evaluate(predict(mfoo), test_data)

這兩個 predict 分別來自雙方套件的各自定義及實作。

1
2
Bar.predict(mbar)
Foo.predict(mfoo)

如此使用者便可以無縫地使用同樣一個介面 predict,而功能來自兩個不同的套件。

這樣的特性大概會有人聯想到介面(interface)。然而,Julia 並不明顯使用介面來規範開發者,Julia 隱含地使用鴨子定型(duck typing)。

在載入套件之後,Julia 允許使用者不冠上套件名稱來使用這些函式,像 Bar.predict,你可以自由地使用 predict 即可。總是會有套件之間的命名衝突,當衝突發生的時候,就是開發者需要負起責任彼此溝通及協調的時候。不過使用者仍然可以以 Bar.predict 的方式使用套件,只是每次載入套件的時候都會有警告。

1
2
3
4
5
6
7
julia> using Plots

julia> using Gadfly

julia> plot
WARNING: both Gadfly and Plots export "plot"; uses of it in module Main must be qualified
ERROR: UndefVarError: plot not defined

鴨子定型及多重分派

在原文中提到鴨子定型及多重分派是成就 Julia 成為一個可組合(composable)語言的基石。我個人也認為可組合是 Julia 提供最重要的特性之一,但卻鮮少被人提及。多數人仍然熱衷於語言效能及開發便利性。

鴨子定型的一個很好的比喻是,當一個東西會呱呱叫的時候,那麼他就是鴨子。當一個 bar(x) 可以呼叫時,我不需要去檢查 x 的型別為何,我就直接使用就是了。這樣會構成一種隱性的介面。

當我需要提供一個矩陣相乘的功能時,我會寫以下程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
function multiply(A, B)
C = zeros(size(A, 1), size(B, 2))
A = A'
for i = 1:size(C, 1)
for i = 1:size(C, 2)
for k = 1:size(A, 2)
C[i, j] = sum(A[:, k] .* B[:, k])
end
end
end
return C
end

這可以用在一般的矩陣,但如果今天我有自定義的一個新的矩陣呢?

1
2
3
struct NewArray
...
end

在上述的 multiply 中我只需要檢查 NewArray 是否提供相關的介面即可。像是裡頭用到了 sizeA[:, k]sum。我只需要支援這些介面的實作即可,如此,Julia 就可以以鴨子定型的方式,讓 multiply 接受 NewArray 了。

對於不同的型別需要有不同的行為,這在一般的物件導向語言中稱之為多型。多型在多數物件導向典範中使用的是單一分派(single dispatch),然而 Julia 也支援多型,但是以多重分派(multiple dispatch)的方式支援。因此,廣義而言,Julia 支援物件導向的方式是多重分派,卻不是典型的、語法上的封裝、繼承及(單一分派)多型。

多重分派,可以在需要更細緻行為定義時幫上忙。當 NewArray 支援 sum

1
sum(::NewArray) = ...

這是單一分派的方式,也可以有多重分派的方式。

1
sum(::NewArray, ::Array) = ...

不過這樣只支援 Array 這個特定型別。或是我們可以乾脆這樣做。

1
sum(::NewArray, ::AbstractArray) = ...

這樣只要是 AbstractArray 的子型別都可以接受。

總結

以上種種的特性成為了可組合性的基石。一個具有可組合性的語言能夠成為各種東西。Julia 的個別的套件都不俱備所有的定義,需要依賴 Julia 語言及標準函式庫的介面及實作。

例如當 Julia 中載入了深度學習框架,那它,連同語言本身,就是一個完整支援深度學習功能的引擎。當同時載入了資料庫與深度學習相關套件,那它就成為了支援深度學習功能的 DBMS。當載入了科學計算及機器學習套件,那它就會變成一個強大的數值計算引擎。以 Julia 的可組合性出發,來打造各式各樣不同的引擎,就像一個單純的編輯器搭配有豐富的外掛(plug-in)一樣。這樣衍生出的生態帶來了各式各樣不同的可能性,也讓套件的 reusability 提升到最高的境界。