Julia 的命名空間
基本上,我們知道 Julia 的命名空間的概念不是很強,命名空間的概念是由模組(module)所建構的。
介紹模組前先要介紹幾個比較基本的語法:using
、import
跟 include
。
基本語法
using
using
是用在一般要載入一個套件的時候用的。像是要使用標準函式庫(stdlib)中的 LinearAlgebra
的時候,會這樣做
1 | using LinearAlgebra |
如此一來,你就可以使用像是
1 | tr |
或是
1 | LinearAlgebra.tr |
然而,像是 LinearAlgebra.tr
這樣由 [module].[function]
構成的,則是去呼叫 [module]
底下的 [function]
。像是 tr
則是由 LinearAlgebra
模組,藉由 using LinearAlgebra
語法,匯出(export)到我們載入的空間。
例如,
1 | julia> tr |
基本上,無論是由腳本(script)執行或是 REPL 執行,我們所在的是在 Main
這個模組下,可以用 parentmodule(Base)
測試。
import
如果要重複定義同名的函式,會被視為是要延伸(extend)該函式的定義,幫該函式新增更多的方法(method)。我們先來看看 string
這個函式。
1 | julia> parentmodule(string) |
例如,想要延伸 string
這個函式的定義,我們先得從 Base
模組匯入所有關於 string
的方法,以便延伸方法。所以需要
1 | import Base: string |
如此會把 Base
裡頭關於 string
的定義都匯出,如此一來,就可以定義新的方法。
1 | julia> struct Foo end |
include
include
則是對於檔案的操作,它可以將其他檔案中的程式碼包含(include)進來。例如有個 a.jl 這個檔案,其內容有:
1 | println("a.jl file is included.") |
這時候可以在另一個檔案 main.jl 中寫:
1 | include("a.jl") |
執行 main.jl 之後,會看到:
1 | a.jl file is included. |
代表在包含 a.jl 檔案的過程中,裡頭的程式碼都會被執行過,所以會看到 a.jl file is included.
,並且可以使用 foo
函式,則代表 foo
函式的定義一併被帶到 main.jl 當中。
這就是 include
的作用了。
模組
一個模組是由以下的程式碼定義的
1 | module Foo |
模組中可以定義型別或是函式等等程式碼,而模組中所定義的型別或是函式能不能自動地匯出,主要是由 export
語法指定。
export
會匯出那些被指定的型別或是函式,會在有 using Foo
的作用域可以見到 foo
,並且可以使用,但是沒有匯出的,像是 bar
就沒辦法直接使用,但是還是可以藉由 Foo.bar
來使用它。
命名空間
從以上的例子,我們不難發現,Foo
模組形成了一個命名空間(namespace),這個命名空間所定義的變數、函式或是型別等等程式元素,都有自己的識別子(identifier),也就是命名是唯一的。然而,Julia 模組中的 export
會自動把該模組中的特定識別子匯出到 Main
這個命名空間中,所以可以在 Main
裡直接使用 foo
,而沒有匯出的則需要使用 Foo.bar
來使用它。
在 Julia 是沒有硬性的命名空間的,命名空間則是由模組名稱所組成的,例如像先前介紹的 LinearAlgebra
就是由 LinearAlgebra
模組構成。
套件
套件跟模組的概念是不一樣的。套件包含了一系列定義的型別或是函式,最重要的是,套件的資料夾中含有 Project.toml 來對套件進行管理。
而且模組是沒有辦法被 using
或是 import
的,要 ]add PACKAGE
才行。
套件主要會有一個主模組,其中可以包含其他子模組,變得有點像資料夾結構的樣子。
一個套件要可以被其他套件加為相依套件的話,則需要該套件有 git 版本控制。
例子
這邊我分別做了 A
跟 B
兩個套件,並且讓 B
有加 A
為其相依套件。
在 A
中有(src/A.jl)
1 | module A |
在 B
中有(src/B.jl)
1 | module B |
後續的介紹裡會用到這邊的例子進行解說。
匯入其他套件
讓我們來假設幾種狀況,接下來會討論這些狀況,以及一些解決方法。
子模組與命名空間
using A
如果按照以上的例子,在其他的檔案中 using B
的話,會有什麼呢?
1 | julia> using B |
如此一來,A
套件就會變成 B
下的其中一個子模組,所以可以使用 B.A
來找到所有 A
底下的函式。也可以進一步發現,在 B
中定義的 foo
跟在 A
中定義的 foo
是分開來的。
import A: foo
那如果我們把 B
套件中的 using A
改成 import A: foo
會發生什麼事情呢?
1 | julia> using B |
我們可以發現 B
套件中的子模組 A
就消失了,而且所有 A
套件中 foo
都會跑到 B
套件當中。當然,bar
則不會在 B
套件當中。
import A
那如果我們把 B
套件中的 using A
改成 import A
呢?
1 | julia> using B |
看來 import A
跟 using A
沒有太大的差別。
有子模組也有所有方法的定義
如果要讓 A
子模組存在,並且可以在 B
套件中使用到所有 A
子模組中的方法,則需要
1 | using A |
如此則會變成
1 | julia> using B |
我想定義自己的方法,但是會跟其他的套件衝突
常常發生的問題就是,開發中會用到某 A
套件,而自己定義的函式中有跟 A
套件衝突的部份。
1 | using A |
例如,A
套件中匯出了 foo
函式,而自己開發的套件也定義了同樣的函式名稱。這時候就會發生衝突,也就是出現類似以下的錯誤:
1 | ERROR: error in method definition: function A.foo must be explicitly imported to be extended |
這時候就需要明確地延伸定義,也就是修改或是增加以下程式碼:
1 | import A: foo |
如此一來,就可以定義自己的 foo
了。
不想要 import
會有基於一些緣故不想要匯入 A
套件的功能。這時候很單純,那就是用以上 using A
的方式而已。
為了跟 A
套件的功能區別,可以在 B
套件中,指定要呼叫的套件。
在 src/B.jl 中,
1 | module B |
載入 B
套件時會有
1 | julia> using B |
讓一個套件去補充其他套件的功能
像我前一陣子的工作是將 scatter
跟 gather
搬到 NNlib
套件下。
主要當然是把 cpu 運算的部份寫在 NNlib
套件中,但是會有 cuda 運算的部份。然而,cuda 運算的部份則是寫在 NNlibCUDA
套件下,NNlibCUDA
套件則是 NNlib
套件的一個依賴套件。
這時候 NNlibCUDA
套件的角色就是去延伸 NNlib
套件下,scatter
與 gather
的 cuda 版本,所以這時候我會在 NNlibCUDA
套件中寫:
1 | function NNlib.scatter(::CuArray, ...) |
如此,在載入 NNlibCUDA
套件時,就會自動去延伸scatter
與 gather
的方法了。
結論
這邊整理了一些關於命名空間跟一些基本的模組概念,沒有特別實驗或是在開發套件的朋友可能會一團混亂。
這時候就真的有需要一篇文章來澄清這些行為了呢!