課程進行到這裡,你已經看過很多函式定義了。 通常,這些定義在呈現時,並沒有針對 REPL 中的輸出多做說明:
julia> f(x) = x^2
f (generic function with 1 method)
為什麼是 "generic function with 1 method"?
假設我們想更精確地指定引數的型別。
julia> f(x::Integer) = x^2
f (generic function with 2 methods)
julia> f(x::Real) = x^2
f (generic function with 3 methods)
或者,我們也可以改變引數的數量:
julia> f(x, y) = x * y
f (generic function with 4 methods)
看來,同一個函式的新定義被稱為「方法」。 方法會被加入一份清單,而不是覆蓋掉舊的定義。
我們可以輕鬆列出所有方法:
julia> methods(f) # output edited for brevity
# 4 methods for generic function "f" from Main:
[1] f(x, y)
[2] f(x::Integer)
[3] f(x::Real)
[4] f(x)
如果我們呼叫函式f,Julia 不知怎麼地就知道該用哪個方法:
julia> f(2.5) # Float64
6.25
julia> f(2) # Int64
4
julia> f(2, 3) # Int64, Int64
6
根據手冊,函式是一個物件,把引數的元組對應到一個回傳值。
我們通常把f(2, 3)裡的引數想成兩個獨立的項目,但編譯器把它們看成一個元組。
即使只有一個引數也是如此(不過我們得加上一個逗號,才能看得更清楚):
julia> typeof( (2, 3) )
Tuple{Int64, Int64}
julia> typeof( (2,) )
Tuple{Int64}
把函式看成一個黑箱,我們可以說:一個元組進去,一個回傳值出來。 更正式地說,元組被_對應_到一個回傳值。
這通常被稱為「多型」。
這在電腦科學裡已經是個討論了幾十年的大主題,也採用了各種不同的做法。
Julia 的做法很不尋常,但也不是獨一無二。
+運算子就是一個有 161 個方法的函式)。...可以部分繞過這項限制)。dispatched,並回傳結果。MethodError。注意,Julia 是動態型別的,所以方法分派發生在_執行期_。
前一節講了很多文字,現在讓我們把這些想法化成程式碼吧。 為了簡單起見,下面的函式都只有一個引數,但有數十個引數的函式並不罕見(例如繪圖套件或機器學習)。
首先定義一些自訂型別,用來表示幾何圖形。
julia> abstract type Shape end
julia> struct Circle <: Shape
radius::Float64
end
julia> struct Square <: Shape
side::Float64
end
julia> struct Rectangle <: Shape
length::Float64
width::Float64
end
接著,我們可以定義方法來計算每個圖形的面積。 每個方法都只接受一個引數,而每個引數都是不同的具體自訂型別。
julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)
julia> area(s::Square) = s.side^2
area (generic function with 2 methods)
julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)
現在我們可以建立一些圖形並計算面積,交給 Julia 的分派機制為每個圖形選用正確的公式。
julia> circle = Circle(2.3)
Circle(2.3)
julia> area(circle) # call signature Tuple{Circle}
16.619025137490002
julia> square = Square(1.6)
Square(1.6)
julia> area(square) # call signature Tuple{Square}
2.5600000000000005
julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)
julia> area(rectangle) # call signature Tuple{Rectangle}
2.94
方法簽章可以接受虛設的型別值,方式和參數化型別大致相同。
語法很特別,需要where子句:
julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)
julia> same_type([1, 2], 3)
true
julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)
上面的例子要求兩個引數都使用相同的型別T,無論T是什麼。
我們可以對T加上限制,讓它更具體:
julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)
使用這個方法時,只有完全相同的數字型別才會回傳true。
虛設變數可以有好幾個,用逗號分隔,寫在where子句中。
julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)
多重分派在 Julia 裡非常靈活、效能又高,因此被廣泛使用。
從+有 161 個方法這個例子可以看出,Julia 程式設計師的目標是定義型別範圍明確的方法。
在位元組碼的層級,兩個整數相加和兩個浮點數相加截然不同,而把整數和浮點數相加,還多了一層型別提升的複雜度。
型別越具體,編譯器能做的最佳化就越多:在 Exercism 的小練習裡這或許微不足道,但對 Julia 所針對的大型數值模擬來說,卻是效能的關鍵。
如果你知道一個方法會收到什麼輸入,請告訴編譯器!
在這個練習中,你將會實作一個遭遇的模擬。 這會讓你熟悉多重分派的基本概念,也就是 Julia 的主要典範。
For this exercise, all function definitions should be a single-line declaration with no if/else logic.
一般來說,遭遇是指一個實體 a 遇見另一個實體 b,並對其做出反應。
encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."
首先,我們會模擬貓和狗相遇時會發生什麼事。
首先新增一個抽象型別 Pet。
然後定義型別 Cat 和 Dog,作為 Pet 的子型別。
每個型別都有一個欄位 name。
實作一個函式 name(),回傳寵物的名稱。
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
針對以下遭遇實作 meets() 方法:
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
用一個方法實作 encounter() 函式,回報兩個具名實體相遇時會發生什麼事。
julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."
如果牠們在散步時遇到不同的寵物,例如馬,會發生什麼事?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
寵物可能會遇到許多不是寵物的其他東西:車子、人類、植物、天然災害、小行星……那會發生什麼事?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
我們的模擬中還可能發生許多其他遭遇。 車子遇到狗、兩個電子彼此遭遇並交互作用……
不可能事先涵蓋所有遭遇,因此我們會實作一個通用的備援。
julia> γ1 = Photon("γ1")
Photon("γ1")
julia> γ2 = Photon("γ2")
Photon("γ2")
julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens."
# This is true, photons just pass through each other.