到目前為止,課程大綱還沒有深入談到型別,但很明顯 Julia 裡確實有型別:
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
我們從未指定型別,但 Julia 還是自動幫它們指定了。
types,這是它設計上的核心。Type Inference。JIT 編譯器會檢視(所有)程式碼,看看變數如何被使用,然後_推斷_出一個適合該用途的預設型別。
依賴型別推斷來解簡單的教學練習沒問題,但對於較大的程式,你很可能需要更精確的控制。
在多數現代處理器上,整數的預設型別是Int64。
我們在Numbers概念中看過,值可以轉換成特定的非預設型別。
julia> x = Int16(42)
42
julia> typeof(x)
Int16
不過,變數x仍然可以重新指定成不同的型別:
julia> x = "changed"
"changed"
julia> typeof(x)
String
這叫做type instability,它的特點是:
我們可以用::運算子來設定x的型別:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.
現在y的型別是Int16,而且永遠都會是。
因此,編譯器知道要為它保留多少位元組,也能放心把其餘程式碼最佳化,依賴這個穩定的型別。
這樣一來,這個變數就和 C 這類靜態型別語言裡的變數差不多。
Int64、Int16、String、Char:這些型別到底_從哪來的_。
在許多物件導向(OO)語言中,每個型別都是一個類別,子類別化把它們排進類別階層,而類別方法則定義了各種行為。
Java 和 Ruby 是這種模式的明顯例子,但就連 Python 在內部也類似。
Julia 沒有類別。
官方文件給的理由是,OO 功能會干擾 JIT 編譯器,並損害執行時的效能。
不過,看看這段程式碼:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
補充一些細節:
Int16是一種型別,我們可以建立這種型別的變數。Int16是Signed的子型別,supertype()函式會告訴我們這件事。Integer、Real和Number,最頂端是Any,而subtypes()會為我們列出階層的這個分支。所有分支最後都終止於Any,它的獨特之處在於它是自己的父型別。
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
所以,Julia 沒有class階層,但它_確實_有type階層。
之後我們會試著拆解它的運作方式,但首先還有很多東西要探索。
我們可以用typeof()依照平常的方式測試是否相等。
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
型別相等必須完全一致,因為這種比較方式並不理解型別階層。
更有彈性的是,isa會告訴我們某個值是否和比較對象同型別,或是它的子型別。
它可以用函式形式或中綴形式來寫。
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
注意,isa左邊要放的是value,不是type。
用這種方式比較兩個_型別_會得到意想不到的結果。
正確的運算子是<:,之後的概念我們還會經常看到它。
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
我們看到型別階層會形成樹狀結構(就電腦科學的意義來說,根在最上面)。
樹裡的每一項都是一個node,而它們可以分成幾類:
abstract。concrete。julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
這是個重要的區別,因為只有具體型別可以instantiated成變數。
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
注意,嘗試使用抽象型別(在這個情況下)不會產生錯誤訊息,但編譯器會建立一個適當的具體型別:Int64,而不是Integer。
在這道練習中,你會做一點資料工程,預先處理一些成績資料。 有一所學校,學生多得不得了(太多了嗎?),校方想對學生拿到的成績做些資料分析,而且希望做得有效率,因此重點是盡量精簡並整理這些資料。
評分等級通常是 0 到 10,所以每個成績只佔 8 位元的資料型態最有效率。
UInt8資料型態。Int8資料型態。等你實作出轉換成績的函式之後,還要再寫一個函式來處理存放成績的集合,把成績降級,並以遞減順序回傳。
Vector來存放成績,因為他們對自己的資料很講究。Set來存放成績,因為他們比較懶一點。這兩個函式中,你都需要處理無效的輸入,方法是丟出 MethodError。
例外處理會在之後的概念單元中介紹,所以在這道練習裡,你可以使用以下語法:
throw(MethodError(f, args))
其中f是函式,args是傳入該函式的引數所組成的元組。
以demote(n)函式為例,f是demote,而args是(n,):
throw(MethodError(demote, (n,)))
實作demote(n)方法。
Float64時,應該四捨五入到最接近的整數,並回傳UInt8資料型態。Integer時,應該以Int8資料型態回傳同一個整數。julia> demote(4.2)::UInt8
5
julia> demote(4)::Int8
4
julia> demote("hi")
MethodError: no method matching demote(::String) #output truncated
實作preprocess(coll)方法。
Vector時,應該將所有數字降級,並反轉該向量。Set時,應該將所有數字降級,並回傳依遞減順序排序的向量。julia> preprocess([1, 2, 3])
3-element Vector{Int8}:
3
2
1
julia> preprocess(Set([2.2, 5.8, 3.4]))
3-element Vector{UInt8}:
6
4
3
julia> preprocess(42)
MethodError: no method matching preprocess(::Int64) #output truncated