iThome 網站首載:動靜定型間的務實路線
語言應該採靜態定型(Statically-typed)或是動態定型(Dynamically-typed)?這個古老的爭議至今戰火不斷,靜態定型會使語言變得囉嗦,動態定型需要完美的測試覆蓋率,兩方各有絕對的擁護者,然而有些語言開始持著型態推斷與型態標註走中間路線,令語言本身乍看之下,靜態與動態定型間的分野變得模糊,開發者得自行思考並務實取捨程式碼中的型態資訊。
- 型態資訊由誰負責與何時處理?
在我先前專欄〈靜態語言與動態語言的信任抉擇〉中談過動態與靜態定型語言的定義與優缺點,簡單來說,靜態定型語言認為開發者難以避免型態錯誤,因而希望藉由變數型態資訊,在程式「運行前」檢查出型態錯誤,然而就算型態錯誤全數在運行前被抓出糾正,實際功能正確與否,還是要程式「運行時」進行測試來確認,既然如此,動態定型語言認為藉由設計單元測試來檢查型態錯誤即可,因而程式撰寫時不需讓變數帶有型態,避免型態宣告來讓程式更簡潔且具可讀性。
實際上,就算是程式運行前可以檢查出型態錯誤,也並不代表程式中沒有型態錯誤,舉例而言,靜態定型的Java有所謂轉型(Cast)語法,實際上就是在關閉編譯器的型態檢查,如果開發者對型態的編譯錯誤資訊不明就理,只是為了通過編譯而進行轉型,那麼著名的
ClassCastException
就會常伴左右;相對地,如果有專門用於檢查型態錯誤的單元測試工具,那麼開發者確實只要心中明瞭所操作資料之型態即可,然而現實中目前還不存在這種完美的單元測試工具,依賴開發者自行撰寫型態錯誤檢查的完美單元測試,也是件極為困難的事。問題也不只在何時檢查出型態錯誤上,如果重視程式產能,靜態定型語言變數本身的型態資訊,可以提供給程式分析工具使用,像是可供程式碼編輯器製作智能提示功能,相對來說,動態定型語言在這方面的工具表現通常較弱。從這幾點來看,靜態定型或是動態定型的問題,並非在於語法上是否硬性要求撰寫型態資訊,型態資訊是需要的,問題在於由誰負責提供與何時處理的問題?開發者本身顯然必須為型態負責,然而人類在思維上終究有漏洞,一些語言有了更務實的作法,不再只是將責任單方面放在工具或開發者身上。
- 動態語言的型態標註
Python是動態定型語言,然而在Python 3的PEP-3107中提出了函式標註(function annotation),可在定義函式時選擇是否標註參數與傳回值型態,舉例來說,基本Python函式可定義為
def greet(name, age):
,將來在支援PEP-3107的Python實作中,可選擇性定義為def greet(name: str, age: int) -> str:
,表示兩個參數的型態是str
,傳回值型態亦是str
,這顯然使得Python在定義函式時語法變得冗長,然而好處是函式加註@typechecked
時,執行時期若傳給greet
函式非str
型態之引數,將會引發TypeError
。Groovy也採用類似作法,開發者可使用
def x = 10
來定義變數,因為Groovy是動態定型語言,因而後續指定x = "caterpillar"
是可行的,然而開發者可以指定x
型態,像是int x = 10
,後續指定x = "caterpillar"
的程式碼在運行時,將會引發GroovyCastException
。動態語言的型態標註可用於執行時期型態檢查,然而帶來的幫助似乎不大,開發者主動於動態語言中提供型態標註,主要考量之一是為開發者本身提供型態資訊,有時開發者就是想從程式碼中直接得知型態,而不是觀察物件行為來判斷,另一考量是提供分析工具在運行前協助型態檢查或作出相關提示,提供原本靜態定型語言才有的優勢。Groovy 2.0後提供
@TypeChecked
標註,可告知編譯器進行靜態檢查,也就是於編譯時期檢查型態錯誤,讓型態指定錯誤或呼叫不存在函式等錯誤,可以提早在運行前呈現。實際上,這類功能不見得要語言本身語法支援,有些工具或程式庫,可透過程式中特定註解或字串格式進行分析,達到執行前或後的型態檢查,例如python-rightarrow這類的程式庫。- 靜態語言的型態推斷
有些動態定型語言採取加法哲學,讓開發者在必要時可增加型態資訊,相對地,有些靜態定型語言採用減法哲學,如果可從程式文脈(Context)推斷出型態,允許程式碼中不宣告型態資訊,例如Scala是靜態語言,然而在
val text = "Hello"
這樣的程式中,可由"Hello"
得知text
應是字串型態,因而可不寫為val text: String = "Hello"
;在Java這囉嗦的語言中,也儘可能採納這樣的作法,像是JDK7的泛型可寫為List<String> names = new ArrayList<>()
,因為從List<String>
推斷等號右側應是ArrayList<String>
。型態推斷其實表明了,有時程式碼中撰寫的型態是多餘資訊,對程式分析工具並沒有更多幫助,甚至經常妨礙開發者對程式碼的閱讀,藉由改進工具的型態推斷能力,可以減少這類多餘型態資訊的載明,甚至有像是Haskell這樣的靜態定型語言,在編譯器強大的型態推斷能力下,完全不用宣告變數也可以通過編譯,然而,如果有型態推斷能力,不見得就要完全依賴推斷,如果單從程式碼的鄰近文脈難以看出型態資訊,確實寫出型態資訊反而是件好事。
完全依賴編譯器型態推斷能力,而不主動撰寫型態,會使得撰寫在Haskell這類語言時,相對來說更難以通過編譯,有時甚至推斷出來的型態不見得正確,因而雖然可以不宣告型態,然而Haskell文化中反而鼓勵宣告型態讓程式更加易讀,像是在與函式相鄰之處宣告函式型態,讓開發者閱讀函式時可就近獲取型態資訊,而不用像編譯器一樣從遠處程式碼文脈推斷過來,像這類的型態宣告就不是多餘,而是必要資訊。
- 務實地看待與運用型態資訊
有個TypeScript語言頗為有趣,這門語言在JavaScript上進行加法,加法之一就是讓變數帶有型態,也就是本身為靜態定型語言,然而具有型態推斷能力,不過有時像定義函式情況下若沒有宣告參數型態,也無法從程式文脈推斷出型態時,變數會預設為
any
型態,也就可以接受任何型態的值,這打破了動態與靜態定型之間的界線,雖然是靜態定型語言,在這種情況下實質上失去執行前檢查型態的能力,這應是為了相容JavaScript語法而作出的決定,如果要務實地運用型態資訊,方式之一是明確標示參數型態,方式之二是編譯時加上--noImplicitAny
,在只能推斷出any
時發出錯誤訊息。不過,TypeScript推斷出
any
的作法,倒是給了鴨子定型(Duck typing)一個簡便作法,只是失去了執行前檢查型態的能力,這令人想到Scala提供的結構定型(Structural typing)語法,舉例來說,如果函式定義為def doQuack(d: {def quack: String})
,那麼任何具有quack
方法的物件,都可以傳給doQuack
,如果傳入的物件不具此協定,就會引發編譯錯誤。從這幾種語言在型態上的探討看來,靜態或動態定型之間的界線不再是絕對而清晰,可以發現的是,無論使用靜態定型或動態定型,型態資訊都是必要的,只是這個資訊是在資料本身或是變數,甚至是註解或特定字串格式,問題也並非單純到只需決定採用靜態定型或動態定型,瞭解語言對型態的支援能力、確認開發者本身對型態的控制能力、調查有無工具或程式庫可在型態上進行輔助或分析,才是務實地看待與運用型態資訊的作法。