對於 Julia 比較不熟悉的使用者或開發者,要怎麼定函式的參數型別是一個大學問。

1
2
3
function average(xs)
return sum(xs)/length(xs)
end

例如以上的 average 函式,xs 要定成什麼樣的型別比較好?

可能有人會以他所測試的情境去定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
julia> xs = rand(10)
10-element Array{Float64,1}:
0.8261912048684596
0.4466692353358077
0.6337754306737924
0.1297961522759261
0.4699412442087936
0.6194816490253636
0.8258926319092839
0.05094839501664228
0.21008570113108194
0.16250331333741275

julia> function average(xs::Array{Float64,1})
return sum(xs)/length(xs)
end
average (generic function with 1 method)

julia> average(xs)
0.4375284957782564

這樣當然是沒有錯,但是會過於狹隘。這樣定出來只有一維矩陣可以適用,而且裡頭的元素還必須是 Float64 型別。

思考廣義化

我們能不能讓所有的陣列都適用?那就可以改成這樣:

1
2
3
function average(xs::AbstractArray{Float64})
return sum(xs)/length(xs)
end

可是這樣又限制了元素的型別,像是 Float32 或是無理數 Irrational 就不適用。

如果我們要將他廣義化到所有的整數跟浮點數都適用,也支援無理數。

那麼我們可以考慮支援實數 Real,可以寫成以下的樣子:

1
2
3
function average(xs::AbstractArray{<:Real})
return sum(xs)/length(xs)
end

或是也可以考慮將他參數化:

1
2
3
function average(xs::AbstractArray{T}) where {T<:Real}
return sum(xs)/length(xs)
end

這兩種寫法所編譯出來的 method instance 是一樣的。

這兩種寫法差異在於,如果將他參數化,開發者有機會將參數型別 T 取出來使用。

建議

為了避免大家寫出來的函式太過於狹義,所以在這邊給出一些建議。

如果要支援的型別是所有整數,那麼可以考慮使用 Integer,它可以支援 BoolInt64UInt8 之類的型別。

如果要支援的型別是所有的整數以及浮點數等等的,那麼可以考慮使用 Real,它可以支援 Float64Irrational 等等的型別。

如果還想要支援複數 Complex,那麼就直接使用數字 Number 吧!

思考點

那至於要支援什麼樣的型別以及他的範疇,這是個很好的問題。

那就要問在你的函式當中所用到的函式支援哪些型別?

例如在 average 中有 sum 以及 length 函式。

sum 支援包含 AbstractArray、iterable,甚至是 AbstractRange

length 則支援所有的陣列 AbstractArray、集合 Set、字典 Dict、所有字串 AbstractString 等等的。

接下來要思考 average 所要提供的功能是什麼?他的語義是什麼?

就字面上的意義而言,他要提供的是「某個集合體的平均」。這時候就可以去定義要支援什麼樣的集合體,以及支援集合體中的元素範疇是什麼。

最後就可以決定這個函式的型別參數應該是什麼了。

自定義型別

如果有人想要自行設計自己的型別,並且這個型別支援 sumlength,那理論上,這個型別就自動支援 average

要讓其他人去自行定義型別,並且自動支援 average,為了避免將這個函式綁得太死,那就乾脆不定義型別。

1
2
3
function average(xs)
return sum(xs)/length(xs)
end

這樣是可以的。

有沒有發現,與其加一堆型別將函式綁死(加法設計),不如把型別拿掉還來的更好(減法設計)。