遭遇

遭遇

學習練習

簡介

課程進行到這裡,你已經看過很多函式定義了。 通常,這些定義在呈現時,並沒有針對 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 個方法的函式)。
  • 呼叫函式時,Julia 會檢查呼叫簽章:引數的數量,以及每個引數當下的型別。
  • 接著會選出與呼叫簽章最相符的方法。
    • 一般來說,引數的數量必須完全相同(不過展開/收集運算子...可以部分繞過這項限制)。
    • 型別與呼叫簽章完全相符的方法會優先採用。
    • 必要時,Julia 會查看呼叫簽章中各個引數的超型別,找出最相符、最具體的方法。
  • 被選中的方法會以給定的引數被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))."

首先,我們會模擬貓和狗相遇時會發生什麼事。

1. 定義自訂型別

首先新增一個抽象型別 Pet。

然後定義型別 Cat 和 Dog,作為 Pet 的子型別。 每個型別都有一個欄位 name。

2. 取得寵物的名稱。

實作一個函式 name(),回傳寵物的名稱。

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. 定義貓和狗相遇時會發生什麼事

針對以下遭遇實作 meets() 方法:

  • 兩隻狗相遇時,牠們會互相sniff。
  • 貓遇到狗時,牠會對狗hisses。
  • 狗遇到貓時,牠會chases貓。
  • 兩隻貓相遇時,牠們會slink。
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. 定義兩個實體之間的遭遇。

用一個方法實作 encounter() 函式,回報兩個具名實體相遇時會發生什麼事。

julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."

5. 定義寵物之間遭遇的備援反應

如果牠們在散步時遇到不同的寵物,例如馬,會發生什麼事?

  • 如果一隻寵物遇到另一隻牠不認識的寵物,牠會is cautious。
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. 定義寵物遇到不認識的東西時的備援

寵物可能會遇到許多不是寵物的其他東西:車子、人類、植物、天然災害、小行星……那會發生什麼事?

  • 如果寵物遇到牠從未見過的東西,牠會runs away。
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. 定義通用的備援

我們的模擬中還可能發生許多其他遭遇。 車子遇到狗、兩個電子彼此遭遇並交互作用……

不可能事先涵蓋所有遭遇,因此我們會實作一個通用的備援。

  • 如果兩個未知的事物或存在彼此遭遇,nothing happens。
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.
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Julia Exercism

準備好開始 遭遇 了嗎?

註冊 Exercism,透過 35 個概念128 個練習 和真人引導來學習並精通 Julia,全部免費。