語言應該採靜態定型(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 names = new ArrayList<>(),因為從List推斷等號右側應是ArrayList。

型態推斷其實表明了,有時程式碼中撰寫的型態是多餘資訊,對程式分析工具並沒有更多幫助,甚至經常妨礙開發者對程式碼的閱讀。

而藉由改進工具的型態推斷能力,可以減少這類多餘型態資訊的載明,甚至有像是Haskell這樣的靜態定型語言,在編譯器強大的型態推斷能力下,完全不用宣告變數也可以通過編譯。

然而,如果有型態推斷能力,不見得就要完全依賴推斷,如果單從程式碼的鄰近文脈難以看出型態資訊,確實寫出型態資訊反而是件好事。

完全依賴編譯器型態推斷能力,而不主動撰寫型態,會使得撰寫在Haskell這類語言時,相對來說更難以通過編譯,有時甚至推斷出來的型態不見得正確,因而雖然可以不宣告型態。

然而在Haskell的文化中,反而鼓勵宣告型態讓程式更加易讀,像是在與函式相鄰之處宣告函式型態,讓開發者閱讀函式時可就近獲取型態資訊,而不用像編譯器一樣從遠處程式碼文脈推斷過來,像這類的型態宣告就不是多餘,而是必要資訊。

務實地看待與運用型態資訊
有個TypeScript語言頗為有趣,這門語言在JavaScript上進行加法,加法之一就是讓變數帶有型態,也就是本身為靜態定型語言,然而具有型態推斷能力,不過有時像定義函式情況下若沒有宣告參數型態,也無法從程式文脈推斷出型態時,變數會預設為any型態,也就可以接受任何型態的值,這打破了動態與靜態定型之間的界線。

雖然它是靜態定型語言,在這種情況下實質上失去執行前檢查型態的能力,這應是為了相容JavaScript語法而作出的決定,如果要務實地運用型態資訊,方式之一是明確標示參數型態,方式之二是編譯時加上──noImplicitAny,在只能推斷出any時發出錯誤訊息。

不過,TypeScript推斷出any的作法,倒是給了鴨子定型(Duck typing)一個簡便作法,只是失去了執行前檢查型態的能力,這令人想到Scala提供的結構定型(Structural typing)語法。

舉例來說,如果函式定義為def doQuack(d: {def quack: String}),那麼任何具有quack方法的物件,都可以傳給doQuack,如果傳入的物件不具此協定,就會引發編譯錯誤。

從這幾種語言在型態上的探討看來,靜態或動態定型之間的界線不再是絕對而清晰,可以發現的是,無論使用靜態定型或動態定型,型態資訊都是必要的,只是這個資訊是在資料本身或是變數,甚至是註解或特定字串格式,問題也並非單純到只需決定採用靜態定型或動態定型,瞭解語言對型態的支援能力、確認開發者本身對型態的控制能力、調查有無工具或程式庫可在型態上進行輔助或分析,才是務實地看待與運用型態資訊的作法。

專欄作者

熱門新聞

Advertisement