從靜態定型瞭解動態定型


iThome 網站首載:從靜態定型瞭解動態定型

相傳宋代文人蘇東坡有一相知甚篤的朋友佛印禪師,一日蘇東坡問佛印禪師:「你看我現在禪坐的姿勢像什麼?」佛印禪師說:「像一尊佛。」佛印禪師反問蘇東坡:「那你看我的坐姿像什麼?」蘇東坡回答:「你看起來像一堆牛糞!」在程式語言的世界中,如果帶著批判眼光看待另一門語言,只是限制自己的思維,常見的動靜定型語言之爭,多半是因為這類的理由只看到缺點,忘了從對方的優點中瞭解與學習。

擁抱動態定型的理由

擁抱動態定型的理由很多,不少開發者是因為在開發某些性質的程式時,受不了某個靜態定型語言的囉嗦、限制與缺乏彈性,因而轉為擁抱動態定型語言,更多情況下是靜態定型語言的初學者,因為不瞭解型態宣告與編譯器錯誤訊息,直接轉而投向動態定型語言,也沒有再回頭對靜態定型語言有更多接觸與認識,在這樣的情況下,產生了許多模糊、不完全正確的理由,支持著自身繼續擁抱動態定型。

擁抱動態定型的常見理由之一是,靜態定型時的型態宣告語法囉嗦、讓程式碼失去表達性。實際上就現今許多靜態定型語言來說,並不是隨處都要型態宣告,以靜態定型的Haskell為例,由於具備強大的型態推斷(Type inference)能力,幾乎可以不用宣告型態,然而為了增加程式的表達性,Haskell文化上反而建議在定義函式之類的場合時,加上型態宣告,Scala也具備強大的型態推斷,就連一向被認為笨重、囉嗦的Java,也不斷地改善型態推斷能力,朝提高程式碼表達性的方向努力中。

擁抱動態定型的常見理由之二是靜態定型在型態處理上缺乏彈性,最常被舉的例子就是無法支援Duck typing,也就是無法只考慮物件的公開操作協定(方法),而必須考慮型態。實際上靜態定型語言可以支援Duck typing,像是Scala的Structural typing,可以只宣告物件的公開操作協定而不用限定型態,有人說這只是靜態的Duck typing,事實上即便Java,也可以透過反射機制來實作執行時期的Duck typing。

擁抱動態定型的常見理由之三是,靜態定型語言沒有辦法抓出所有的型態錯誤,單元測試還是必要的。實際上單元測試在靜態與動態定型中都是必要的,問題在於開發者本身是否願意、是否有能力執行單元測試來取代靜態定型的型態檢查,我前一篇專欄〈靜態定型與單元測試之爭〉已經談論過這方面的議題,有人勇敢地發表了一篇〈Unit testing isn't enough. You need static typing too〉,談到自己的實驗中,找到具有單元測試的Python專案中仍有型態錯誤,姑且不論其實驗方法是否完備,但多少呈現出全面的單元測試有其困難度的事實。

靜態定型限制的意義

靜態定型時的型態宣告,基本上是要提供型態資訊,資訊的閱讀對象之一是開發者,這也是為何編譯器有辦法推斷而不用開發者宣告型態的Haskell,仍建議在定義函式時宣告型態之意義。Martin Fowler在〈DynamicTyping〉文中也提到,即使是在寫得很好的(well-written)Ruby程式碼中,因為沒有參數的型態資訊,他老是會問自己「現在到底是在哪了?」型態資訊的閱讀對象之二是開發工具,動態定型領域中,不乏有為語言在語法之外添加型態資訊的工具,像是Python中有python-rightarrow、曾經提出的Function annotation(PEP3107),JetBrains RubyMine中透過自定義的註解形式來添加型態資訊,更可獲得實質的方法智慧提示選單。

靜態定型語言中,變數本身帶有型態,確實是增加了實作Duck typing時的麻煩,不過倒也獲得了實作重載(Overloading)機制的方便性,特別是在定義函式時,對於型態不同而必須有不同流程實作的情況下,我在先前專欄〈介面一致、實作各異的特定多型〉提過,靜態定型的重載讓特定多型(Ad-hoc polymorphism)的實現更簡便且更有表達性。

Duck typing在動態定型中是很有用的特性,特別是在專案起步、需求變化多、物件公開操作難以決定下來的階段,只需在函式中增減物件操作,而不用修改類別或介面,確實可增加開發速度,然而在函式運用Duck typing,實際上就是在操作一組方法,開發者在後續的開發中,仍需思考該組方法是屬於某一類物件所擁有,還是可在不同種類物件之間共用的方法,靜態定型只是強迫開發者一開始就思考這個問題,並且強制要求專案成員都遵守規範,在成員眾多、水準不一、慣例之類的約束難以貫徹或確實傳達的環境中,靜態定型的這類限制,就會是必要的手段。

享用動態定型彈性優點之後

動態定型將選擇權交還開發者,開發者不需要思考如何讓變數型態與值型態符合、不用辛苦地迎合型態推斷規則以讓程式碼更為簡潔、不用事先將行為定義在某個類別或介面(模組)之上...在享用過動態定型的種種優點之後,不代表開發者後續什麼工作都不用做。讓變數型態與值型態符合本身就是開發者應盡的責任,許多情況下,開發者以為自己能掌握在操作的型態,事實上並不是這麼一回事,如果寫過Haskell語言,就更能體會這點,因為無法完全掌握型態的正確性,要通過Haskell編譯器並不容易,因而有時就會有種「It compiles, let's ship it」的感覺。

在動態定型中可以Duck typing,並不表示不需要型態檢查,只不過型態檢查多半發生在執行時期,如果必須在動態定型語言中實作函式重載,就有型態檢查的必要,為了讓程式可讀性更高,在確認實例的型態之後,不同型態的處理最好定義另一個專用函式並加以呼叫;既然採用Duck typing了,型態檢查實際上就不侷促在檢查型態,而可以改為檢查實例上是否具備某些特性,例如在Ruby中,可以是使用respond_to?檢查訊息的接受者有無方法可回應,而不侷促在使用is_a?方法。

Duck typing對於支援物件個體化(Object individuation),像是JavaScript、Ruby這類語言來說更為方便,以Ruby來說,物件隨時可以在添加方法之後作為引數傳遞,然而,如果經常在為物件添加方法後予以傳遞,就表示這些方法可能就是物件一開始就要有的特性,此時必須考量,這個方法是否必須重構,定義在生成實例的類別(雖然並不建議,Ruby甚至可以用Open class將方法定義在標準類別上),或是建立子類別,或者是定義為可共用的模組。

Joshua Bloch說「既然寫正確的程式那麼難,我們就應該盡力取得幫助。所以,能減少bug的所有東西都是好的。這就是我是靜態型別語言和靜態分析的信徒的原因」,如果型態資訊、型態檢查等確實是你需要的,那麼也應尋找相關的靜態分析或開發工具,用它們來補足動態定型本身為了交換彈性,而犧牲掉的靜態定型優勢。

能力越大、責任越重

在我的經驗中,往往在越瞭解靜態定型之後,更能進一步瞭解動態定型,越是知道靜態定型的優缺點之後,越能學會動態定型的強勢與弱勢,能夠瞭解靜態定型的限制,也才能知道在動態定型中,因為不再有編譯器之類來限制開發者,開發者應該負起哪些責任。

動態定型語言本身,確實因為了少許多限制,就陳述程式的自由度來說,比靜態定型語言來得有威力,這是一把雙面刃,當這樣的利器交到了開發者身上,開發者本身少了編譯器耳提面命,就需要更自發地思考型態的問題,採取必要的行動與措施,而不是只會說「We are all consenting adults」,而實際行為卻不是這麼一回事!