基本上,我們知道 Julia 的命名空間的概念不是很強,命名空間的概念是由模組(module)所建構的。

介紹模組前先要介紹幾個比較基本的語法:usingimportinclude

基本語法

using

using 是用在一般要載入一個套件的時候用的。像是要使用標準函式庫(stdlib)中的 LinearAlgebra 的時候,會這樣做

1
using LinearAlgebra

如此一來,你就可以使用像是

1
tr

或是

1
LinearAlgebra.tr

然而,像是 LinearAlgebra.tr 這樣由 [module].[function] 構成的,則是去呼叫 [module] 底下的 [function]。像是 tr 則是由 LinearAlgebra 模組,藉由 using LinearAlgebra 語法,匯出(export)到我們載入的空間。

例如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
julia> tr
ERROR: UndefVarError: tr not defined

julia> Main.tr
ERROR: UndefVarError: tr not defined
Stacktrace:
[1] top-level scope
@ REPL[2]:1

julia> using LinearAlgebra

julia> tr
tr (generic function with 7 methods)

julia> Main.tr
tr (generic function with 7 methods)

基本上,無論是由腳本(script)執行或是 REPL 執行,我們所在的是在 Main 這個模組下,可以用 parentmodule(Base) 測試。

import

如果要重複定義同名的函式,會被視為是要延伸(extend)該函式的定義,幫該函式新增更多的方法(method)。我們先來看看 string 這個函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
julia> parentmodule(string)
Base

julia> methods(string)
# 20 methods for generic function "string":
[1] string() in Base at strings/basic.jl:221
[2] string(x::T) where T<:Union{Float16, Float32, Float64} in Base.Ryu at ryu/Ryu.jl:121
[3] string(f::Printf.Spec{T}; modifier) where T in Printf at /usr/share/julia/stdlib/v1.6/Printf/src/Printf.jl:35
[4] string(u::Base.UUID) in Base at uuid.jl:86
[5] string(a::Symbol) in Base at strings/io.jl:176
[6] string(a::String) in Base at strings/substring.jl:177
[7] string(t::Dates.Time) in Dates at /usr/share/julia/stdlib/v1.6/Dates/src/io.jl:44
[8] string(hash::Base.SHA1) in Base at loading.jl:106
[9] string(b::Bool) in Base at intfuncs.jl:779
[10] string(a::SubString{String}) in Base at strings/substring.jl:178
[11] string(a::Union{Char, SubString{String}, String}...) in Base at strings/substring.jl:208
[12] string(s::AbstractString) in Base at strings/basic.jl:222
[13] string(b::BigFloat) in Base.MPFR at mpfr.jl:999
[14] string(id::LibGit2.GitShortHash) in LibGit2 at /usr/share/julia/stdlib/v1.6/LibGit2/src/oid.jl:181
[15] string(f::Random.DSFMT.GF2X) in Random.DSFMT at /usr/share/julia/stdlib/v1.6/Random/src/DSFMT.jl:108
[16] string(x::Dates.CompoundPeriod) in Dates at /usr/share/julia/stdlib/v1.6/Dates/src/periods.jl:337
[17] string(n::BigInt; base, pad) in Base.GMP at gmp.jl:682
[18] string(n::Integer; base, pad) in Base at intfuncs.jl:760
[19] string(mode::Pkg.GitTools.GitMode) in Pkg.GitTools at /usr/share/julia/stdlib/v1.6/Pkg/src/GitTools.jl:171
[20] string(xs...) in Base at strings/io.jl:174

例如,想要延伸 string 這個函式的定義,我們先得從 Base 模組匯入所有關於 string 的方法,以便延伸方法。所以需要

1
import Base: string

如此會把 Base 裡頭關於 string 的定義都匯出,如此一來,就可以定義新的方法。

1
2
3
4
5
6
7
julia> struct Foo end

julia> string(::Foo) = "foo"
string (generic function with 21 methods)

julia> string(Foo())
"foo"

include

include 則是對於檔案的操作,它可以將其他檔案中的程式碼包含(include)進來。例如有個 a.jl 這個檔案,其內容有:

1
2
3
println("a.jl file is included.")

foo(x) = x

這時候可以在另一個檔案 main.jl 中寫:

1
2
3
include("a.jl")

println(foo(5))

執行 main.jl 之後,會看到:

1
2
a.jl file is included.
5

代表在包含 a.jl 檔案的過程中,裡頭的程式碼都會被執行過,所以會看到 a.jl file is included.,並且可以使用 foo 函式,則代表 foo 函式的定義一併被帶到 main.jl 當中。

這就是 include 的作用了。

模組

一個模組是由以下的程式碼定義的

1
2
3
4
5
6
7
8
9
module Foo

export foo

foo(x) = x
foo(x::String) = "!!" * x * "!!"
bar(x::String) = "bar: " * x

end

模組中可以定義型別或是函式等等程式碼,而模組中所定義的型別或是函式能不能自動地匯出,主要是由 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 版本控制。

例子

這邊我分別做了 AB 兩個套件,並且讓 B 有加 A 為其相依套件。

A 中有(src/A.jl)

1
2
3
4
5
6
7
8
9
module A

export foo

foo(x) = x
foo(x::String) = "!!" * x * "!!"
bar(x::String) = "bar: " * x

end

B 中有(src/B.jl)

1
2
3
4
5
6
7
module B

using A

foo(::Number) = 5

end

後續的介紹裡會用到這邊的例子進行解說。

匯入其他套件

讓我們來假設幾種狀況,接下來會討論這些狀況,以及一些解決方法。

子模組與命名空間

using A

如果按照以上的例子,在其他的檔案中 using B 的話,會有什麼呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
julia> using B

julia> B
B

julia> B.foo
foo (generic function with 1 method)

julia> methods(B.foo)
# 1 method for generic function "foo":
[1] foo(::Number) in B at /media/yuehhua/Workbench/workspace/B/src/B.jl:7

julia> B.A
A

julia> B.A.foo
foo (generic function with 2 methods)

julia> methods(B.A.foo)
# 2 methods for generic function "foo":
[1] foo(x::String) in A at /home/yuehhua/.julia/packages/A/dbyNq/src/A.jl:6
[2] foo(x) in A at /home/yuehhua/.julia/packages/A/dbyNq/src/A.jl:5

julia> B.A.bar
bar (generic function with 1 method)

如此一來,A 套件就會變成 B 下的其中一個子模組,所以可以使用 B.A 來找到所有 A 底下的函式。也可以進一步發現,在 B 中定義的 foo 跟在 A 中定義的 foo 是分開來的。

import A: foo

那如果我們把 B 套件中的 using A 改成 import A: foo 會發生什麼事情呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
julia> using B

julia> B
B

julia> B.A
ERROR: UndefVarError: A not defined
Stacktrace:
[1] top-level scope
@ REPL[3]:1

julia> B.foo
foo (generic function with 3 methods)

julia> methods(B.foo)
# 3 methods for generic function "foo":
[1] foo(x::String) in A at /home/yuehhua/.julia/packages/A/dbyNq/src/A.jl:6
[2] foo(::Number) in B at /media/yuehhua/Workbench/workspace/B/src/B.jl:7
[3] foo(x) in A at /home/yuehhua/.julia/packages/A/dbyNq/src/A.jl:5

julia> B.bar
ERROR: UndefVarError: bar not defined
Stacktrace:
[1] top-level scope
@ REPL[6]:1

我們可以發現 B 套件中的子模組 A 就消失了,而且所有 A 套件中 foo 都會跑到 B 套件當中。當然,bar 則不會在 B 套件當中。

import A

那如果我們把 B 套件中的 using A 改成 import A 呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
julia> using B

julia> B
B

julia> B.foo
foo (generic function with 1 method)

julia> B.bar
ERROR: UndefVarError: bar not defined
Stacktrace:
[1] top-level scope
@ REPL[4]:1

julia> B.A
A

julia> B.A.foo
foo (generic function with 2 methods)

julia> B.A.bar
bar (generic function with 1 method)

看來 import Ausing A 沒有太大的差別。

有子模組也有所有方法的定義

如果要讓 A 子模組存在,並且可以在 B 套件中使用到所有 A 子模組中的方法,則需要

1
2
3
using A

import A: foo

如此則會變成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
julia> using B

julia> B
B

julia> B.foo
foo (generic function with 3 methods)

julia> B.A
A

julia> B.A.foo
foo (generic function with 3 methods)

julia> B.A.bar
bar (generic function with 1 method)

julia> B.bar
ERROR: UndefVarError: bar not defined
Stacktrace:
[1] top-level scope
@ REPL[7]:1

我想定義自己的方法,但是會跟其他的套件衝突

常常發生的問題就是,開發中會用到某 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
2
3
4
5
6
7
8
9
10
module B

using A

foo(::Number) = 5

println(A.foo(4))
println(B.foo(4))

end

載入 B 套件時會有

1
2
3
julia> using B
4
5

讓一個套件去補充其他套件的功能

像我前一陣子的工作是將 scattergather 搬到 NNlib 套件下。

主要當然是把 cpu 運算的部份寫在 NNlib 套件中,但是會有 cuda 運算的部份。然而,cuda 運算的部份則是寫在 NNlibCUDA 套件下,NNlibCUDA 套件則是 NNlib 套件的一個依賴套件。

這時候 NNlibCUDA 套件的角色就是去延伸 NNlib 套件下,scattergather 的 cuda 版本,所以這時候我會在 NNlibCUDA 套件中寫:

1
2
3
4
5
6
7
function NNlib.scatter(::CuArray, ...)
...
end

function NNlib.gather(::CuArray, ...)
...
end

如此,在載入 NNlibCUDA 套件時,就會自動去延伸scattergather 的方法了。

結論

這邊整理了一些關於命名空間跟一些基本的模組概念,沒有特別實驗或是在開發套件的朋友可能會一團混亂。

這時候就真的有需要一篇文章來澄清這些行為了呢!