到目前為止,課程大綱還沒怎麼提到型別,但型別顯然存在於 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 編譯器會瀏覽(所有)程式碼,觀察變數怎麼被使用,然後_推論_出一個與該用法相容的合適預設型別。
在先前幾個概念裡,已經有很多例子把數值型別強制轉成一致的形式,從最簡單的算術開始。
julia> 2 + 1.3
3.3
我們把一個整數(Int64)和一個浮點數(Float64)相加,得到的是 Float64 的結果。同樣地:
julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)
julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}
julia> [nums...]
3-element Vector{Float64}:
3.0
4.1
0.25
在上面的例子中,元組保留了每個元素的型別,但轉成向量之後,全部都被改成一致的 Float64。
Julia 編譯器知道哪些型別轉換是可行的:整數轉浮點數沒問題,浮點數轉整數會失去精度,因此會引發 InexactError。
型別提升會把運算式中所有的值轉成一個共同的型別,這個型別要夠通用,才能與所有輸入相容。
同樣的轉換也可以用promote()函式明確地完成:
julia> promote(nums...)
(3.0, 4.1, 0.25)
對解決簡單的教學練習來說,仰賴型別推論和型別提升沒問題,但程式一大,你大概就需要更精確的控制。
在多數現代處理器上,整數預設是 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,而且永遠都會是Int16。
如此一來,編譯器就知道要為它保留多少位元組,也能把其餘程式碼最佳化成依賴這個穩定型別的形式。
就這點來說,這種變數和 C 這類靜態型別語言裡的變數差不多。
前面介紹的型別指定,用在變數指定的左側,用來限制該變數的型別。
把::運算子用在某個值、或會求值成某個值的東西上,通常就是在_斷言_這個值必須是這種型別,否則就應該拋出錯誤。
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
常見的用法是把這用在函式的回傳值上,當作簡單的最後一道檢查,確認函式行為符合預期。
注意,還有個@assert巨集可以用來做其他形式的斷言。
Int64、Int16、String、Char:這些型別究竟是_「從哪來的」_?
在許多物件導向(OO)語言裡,每個型別都是一個類別,子類別化把它們排成類別階層,而類別方法則定義了各種行為。
Java 和 Ruby 是這種模式的明顯例子,但就連 Python 在內部也類似。
Julia 沒有類別。
官方文件給的理由是,物件導向的特性會干擾 JIT 編譯器,損害執行效能。
誰知道呢,也許他們也讀過這段話:
「物件導向程式設計是個格外糟糕的主意,這種主意只可能源自加州。」
這句話一般歸於 Edsger Dijkstra,他從 1950 年代起便是位傑出的電腦科學家,活躍數十年(雖然大家通常不認為他特別樂觀,也不覺得他特別擅長委婉的外交手腕)。
可是,看看這段程式碼:
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,而 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
型別測試可以用來控制函式內部的流程,但在道地的 Julia 裡相對少見。
我們會在多重派發概念中看到,把型別加在函式引數上、讓 Julia 的派發機制處理這類邏輯,往往更有效率。 不過,在談到多重派發之前,我們還有好幾個和型別相關的概念要討論。
我們看過,型別階層形成樹狀結構(以電腦科學的意義來說,根在頂端)。
樹裡的每一項都是一個node,而它們可以分成幾類:
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。
至少它防止了變數被指定成非整數:
julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.
因此,用抽象型別做型別指定,就是對變數型別的_限制_,限定它必須是(以上例來說)Integer 的任一子型別。
這在程式語言的世界裡並不尋常:它比 C 的型別指定弱,比近年 Python 版本的型別提示強。
isabstracttype()和isconcretetype()這兩個函式可以用來測試。
注意,這兩個函式並不只是彼此的否定:我們之後的概念裡會看到,有些型別既不抽象也不具體。
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
雖然 Vector 不是具體型別,但它的元素是具體的。
eltype()(元素型別)函式會取出這個型別:
julia> eltype([1, 2.3])
Float64