錄影檔 …
簡報檔 …
非常榮幸地,小弟有機會在這邊作為 Ruby Conference 2014 的第一場 Keynote 講者,題目是從語言的資料定型來更一步瞭解 Ruby 的運用。

這是我今天要談的範圍,扣除剛剛的第一頁,我還有 48 頁的投影片要進行。我會談到 …
什麼是定型(typing)?型態宣告(type declaration)的優缺點,像隻鴨子呱呱叫實際上代表了什麼?靜態定型與單元測試之爭,以及瞭解這一切對 Ruby 的使用有什麼幫助?
列點的順序呈現了我演溝的大致順序,由於我會從多角度來切入,因此順序上會稍微與這邊的列點有些不同…

這是我的 Linkedin 簡歷,這張大頭照是女兒五歲左右幫我拍的,我很喜歡這張照片。
我目前是個自由工作者(free lancer),最熟悉的語言生態系(eco-system)是 Java,不過平常就愛涉獵各種程式語言,從中瞭解什麼叫作程式設計,這也是為什麼我會選「從靜態定型瞭解 Ruby 這個動態定型語言」作為題目的原因之一。
我明明最熟悉的是 Java,那我有什麼資格在 Ruby Conference 2014 擔任第一場 Keynote 講者呢?理由其實很簡單 …
拳王爭霸賽前總是要有小弟出來先暖場,不是嗎?

我們從最簡單的開始:什麼是定型(Typing)?
其實我們都知道,電腦裏所有的一切都是位元,對於一組有用的位元資料,指定一種資料型態,也就是所謂的定型,這樣我們就可以用具體的概念來操作這一組位元,而不用直接面對 0101 的運算。
先用 Haskell 的 ghci 來作示範,2 在 Haskell 中的型態是 Num,“Justin” 的型態是字元序列(List),就算是 Lambda 函式也有型態,具有兩個參數(parameter) x、y 傳回 x > y 比較結果的 Lambda 函式,參數型態、順序與傳回型態組成了 Lambda 函式的型態。
來看各位比較熟悉的 Ruby 好了,大家都知道,Ruby 中每個東西都是物件(object),可以用 .class(dot class)取得物件是從哪個型態實例化(instantize)而來。舉例來說,2 的型態是 Fixnum,“Justin” 的型態是 String,Ruby 1.9 後新增的 Lambda 語法,建立的實例(instance)之型態其實是 Proc。

因為現在的程式語言,都內建了許多型態,因此初學者一開始都不太需要思考如何定型的問題,動態定型語言因為少了靜態定型時,隨時會耳題面命的編譯器,更是讓初學者少了許多機會在學習語言的一開始就思考:什麼是定型?
實際上,當你開始要自定義型態之時,就是在面對定型的問題,在開始定型之前,你必須思考什麼?
當一組值具有相關性時,你才會為他們定義型態,對吧!例如,Haskell 中的布林值(boolean)具有 True 與 False 的關係,你才會為他們定義一個 Bool 型態。
除此之外,面對定型問題時,你還會思考這組有相關性的值,有哪些相關操作可以對該型態進行運算,像是 Haskell 的 Bool 型態,會有 &&(and)、||(or)、not 等函式(function)可對其進行運算。
思考定型時,必須思考值的相關性與相關操作,對多數程式語言來說都是必要的,以函數式(Functional)主要典範(Paradigm)的 Haskell 是如此,以物件導向為主要典範的 Ruby 也是如此,定義一個類別時,必須思考的,不就是相關的值與操作嗎?操作在 Ruby 中稱之為方法(method),可以用 methods 取得,對吧!

動態定型的支持者都說他們討厭定型,這不太對,實際上他們還是得定型,他們討厭的東西之一,其實是型態檢查(Type checking),因為你得符合型態的約束(constraint),才能通過型態檢查。
舉靜態定型的 Scala 為例,isRuby 是 Boolean 型態,false 也是 Boolean 型態,year 是 Int 型態,2014 是 Int 型態,在這樣的情況下就相安無事。
如果 pi 是 Int 型態,3.14 是 Double 型態,這樣惱人的錯誤訊息就會出現,因為在 Scala 預設的定義上,Int 與 Double 之間並沒有可以直接轉換的關係。
如果 year 是 Int 型態,而 Int 定義有 toFloat 方法,那麼 year.toFloat 就不會有問題,然而如果 Int 沒有定義 toList 方法,那麼 year.toList 就會出現錯誤訊息。
你在這邊看到的示範,是在 Scala 的互動環境(Interactive shell)中進行,因為 Scala 是靜態定型語言,因此這些錯誤訊息,實際上都是在程式運行之前就被檢查出錯誤。

根據型態檢查是發生在編譯時期或是在執行時期,可概略將程式語言分為兩類:靜態定型語言,像是 Haskell、Scala、Java;動態定型語言,像是 JavaScript、Python 與這兩日的主角 Ruby。
以 Scala 為例,如果你定義了一個 doubleMe 函式,接受 Int 引數(argument),你傳給他一個 10,就不會有事 …
你傳給他一個 3.14,那在 doubleMe 實際運行之前,編譯器就會跟你囉嗦型態不符(type mismatch)。
並不是動態定型語言就不會做型態檢查,以 Ruby 為例,雖然就傳遞引數給 doubleMe 而言,任何型態的值都可以,像是 10、3.14 等,只要它們有乘法操作,看 … 多方便 … 這確實是動態定型語言的優點之一,也就是 Duck typing,稍後我會再來討論它 …
但如果你傳給它一個沒定義乘法操作的物件,那運行時因為檢查到沒有乘法操作,還是丟出了 NoMethodError。
人們總是急躁的,在程式運行之前,就看到一堆型態檢查錯誤,總是令人感到不耐,只不過,動態定型將這種不耐,推遲到執行時期罷了 … XD

呃 … 如果我沒記錯,今天是 Ruby Conference 對吧!?為了避免被丟水瓶,還是來多講一些靜態定型的壞話好了 … XD
其實你們應該知道很多了 … 像是 …
最常見的壞話就是 … 型態宣告(Type declaration)很囉嗦、讓程式碼沒有表達性、煩死人了。
另一個常見的壞話就是 … 明明我看到有隻鳥走路像個鴨子,游泳像個鴨子,叫起來也像個鴨子,但我就是沒辦法把這隻鳥當作鴨子來呼叫 …
再來就是 … 靜態定型就算通過了型態檢查,其實還是沒辦法保證函式的功能是正確的,當然,這是單元測試的職責,只是既然型態檢查的目的之一是要確認程式正確性,單元測試也能確認型態的正確性,那乾脆就全部都寫單元測試,在運行時程式的正確性就好了,何必在運行前白費工夫在型態檢查呢?

呃 …你們會不會知道太多了 … XD

靜態定型中,型態宣告真的會造成囉嗦、沒有表達性而且煩人嗎?
其實,型態宣告並不一定要寫出來,例如這是 Haskell 版本的快速排序法,沒有元素的 List 不用排序就直接回傳空 List 就好,有元素的 List 拆為首元素 x 與尾 List xs,將清單過濾出小於 x 以及大於等於 x 兩個部份,然後分別對這兩個部份做快速排序。
你有看到型態宣告嗎?沒有吧!Haskell 是靜態定型語言,由於它有強大的型態推斷(Type inference)機制,在這邊能自動推斷出每個變數的型態,因此就不用自行撰寫型態推斷。
不過,為了讓程式碼的意圖更清楚,Haskell 的文化上,反而建議你要將型態宣告寫出來,讓閱讀程式碼的人一看就知道,qsort 接受一個 List,傳回一個 List。想想看,有多少次你看 Ruby 的 API 文件,只是為了確認一個函式,接受什麼型態的引數,傳回值又是什麼型態呢?

型態推斷,基本上就是編譯器可以從開發者如何使用變數的前後情境(Context)中,推斷出變數應該是何種型態,因此,就不用程式設計者自行宣告型態。
不只 Haskell,現代越來越多靜態定型語言中,都具備有型態推斷的能力,例如,這邊的 Scala 示範中,實際上編譯器可以從 1 推斷出,number 應該是 Int 型態,從 “Justin” 推論,name 應該是 String,從 (1, “Justin”, 38) 推斷,personalInfo 應該是 (Int, String, Int) 型態,就 Scala 的語意來說,是一個沒有名稱的型態。

就算是囉嗦的 Java,也一直朝更好的型態推斷努力中,雖然還不完美,不過仍有幫助。
舉例來說,如果 persons 是 List<Person> 型態,那麼在呼叫 filter、maptToInt 時,都不用宣告 person 的型態 …
因為編譯器可以從 persons 的型態 List<Person> 中推斷,person 應該是 Person 型態。
如果語言中具備的型態推斷能力越強大,是否做型態宣告就更有選擇性,當型態宣告越有選擇性時,就突顯了型態宣告的本質就是提供型態資訊 …

Java 界的名人 Martin Fowler 曾經在 2005 年發表過一篇〈DynamicTyping〉 的文章,裏面就提到,即使是在撰寫良好的 Ruby 程式碼中,如果缺少型態資訊的話,他就得經常問自己「現在到底是在哪了?」

如方才談到的,型態宣告的本質就是在提供型態資訊。別以為動態定型語言中不會有型態宣告的需求 …
為了提到型態資訊給某個對象,動態定型的領域中,還是隨處可見型態宣告,雖然語法上沒辦法直接宣告型態,但你可能透過各種方式來宣告型態,以提供型態資訊 …
像是 Python 中可透過 python-rightarrow 這個程式庫,提供函式的型態資訊 …
(Update:Python 3.5 以後,正式納入了型態提示,可對變數標註型態,透過 mypy 之類的工具進行型態檢查。)
有趣的是,我略為搜尋了一下 Ruby 領域中有沒有類似的東西,還真的有 … 你們應該很熟這種用法 … typesig 其實就是一個方法 …

型態資訊的提供也可能透過註解中,事先定義好的格式來標示,型態資訊的對象不一定是人,不一定只是單純地為了可讀性,也可能是提供某個工具,先前看到的 python-rightarrow,其型態資訊只要工具有支援,就可以如這邊看到的,可以提供實質的智慧提示選單。
別以為你沒看過型態宣告,動態定型語言的 API 文件中就有一大堆,即使那是透過命名慣例,但名稱上確實提供了型態資訊,從這邊你可以看到 fill 可以接受 object,傳回 array,也可以接受 range,傳回 array,start、index、length 等,都暗示了它們的型態是 Fixnum 整數型態。

對於靜態定型,變數上也會有型態資訊,這讓函式或方法重載(overloading)在實作時方便許多,也就是允許數個方法具有相同的名稱,但是方法的參數與傳回型態各有不同,當呼叫者以對應的引數型態呼叫時,編譯器可以藉由引數型態來確定呼叫者想呼叫的版本。
函式重載也可以根據引數的個數來區分,這麼一來數個方法也可以具備相同的名稱。

那麼,當你想要重載函式時,你心裡想要的是什麼?也許你想要的是 ad-hoc polymorphism,這種多型是指一個多型函式內可以依引數型態的不同,而有著特定或截然不同的流程處理, ad-hoc polymorphism 比較多人沒聽過,然而,大多數人知道的函式重載,就可以用來實現 ad-hoc polymorphism。
舉例來說,在這個 Java 程式範例中,方法名稱都是 valueOf,然而參數型態各不相同,這是一個 ad-hoc polymorphism 不錯的實現範例,因為 boolean、char、int 等不同型態,在 valueOf 中有著截然不同的處理方式。

再來看這個例子,雖然在這邊,表面上 show 方法重載了,然而進一步看看兩個 show 方法的實作內容,除了變數名稱與型態不同外,並沒有其他相異處,這種情況下,並不適合使用重載來解決這個問題。
你應該檢視一下目前的設計,看看 SwordsMan 與 Magician 的實作中,是不是適合將相同的實作提昇(Pull up)至一個父類別 Role,如果是的話,那麼就可以用一個 show 方法來重構上頭的兩個 show 方法 …
對於許多人來說,這種多型就很熟悉了,也就是使用次型態(Subtype)多型來解決這類需求,會是比較好的方式。

在運用函式重載時,有時可以思考一下,是不是想要的東西其實是預設值(default value)?例如,在 Java 這種沒有預設值語法的語言中,很多情況下,重載之目的其實是為了實作出預設值的概念,例如 TreeSet 的建構式(Constructor)實作中,可以接受 NavigableMap 實例,另一個建構式會自動建立 TreeMap,也就是具備 NavigableMap 行為的實例後,呼叫另一個建構式 …
也就是說,呼叫不需引數的 TreeSet 建構式時,實際上是在提供預設值 …

那麼動態定型語言中怎麼做函式重載呢?靜態定型語言的函式重載,其實是在編譯時期就決定了呼叫的方法版本,因而重載經常被說是靜態時期多型…
相對來說,因為動態定型變數上沒有型態,就沒辦法有靜態時期,也就是編譯時期檢查參數型態,因而只能在執行時期檢查型態或者是…呃…檢查特性(property)…
不少動態定型語言都有提供預設值的語法,不過其實沒有也沒關係,例如,JavaScript,其實也可以透過選項物件(option object)的方式來實現預設值語法 …
舉個例子來說,Ruby 的 Array 有個 fill 方法,就實現了重載 … 你知道它是怎麼實現的嗎?…

這是我找到的原始碼,可以看到參數的部份,有三個設定了 nil 做為預設值,藉由判斷參數值是否為 nil,就可以知道呼叫者有沒有提供對應的引數給 fill,這實現了依引數個數來重載 …
至於依參數型態來重載的部份,自然就得在執行時期進行型態檢查,不過,注意到,不一定要用 is_a? 方法,在這邊你可以看到,它用了 respond_to?,也就是說,不一定是 Range 實例,只要像 Range 的,有 begin 與 end 行為的物件,都可以用來呼叫 fill 方法 … Duck typing 對吧!既然動態定型可以 Duck typing,你當然也就得思考一下,什麼時候要用 is_a?,什麼時候要用 respond_to? … 而不是只會說 … 我要 Duck typing,而實際行動卻不完全是那麼一回事 …
如果你有寫過 JavaScript,應該很熟悉特性偵測這手法,其實在 Ruby 中許多場合也可以運用 …
當你檢查引數型態或特性時,就要思考一下,你是不是想要實現 ad-hoc polymorphism,如果是的話,記得再確認一下你的程式流程,有沒有依不同的引數型態或特性,而有著截然不同的實作?…

既然談到了 Duck typing,就多來談一點 Duck typing 好了 …
在這個 Ruby 的 drawQuack 方法中,只要 duck 參考(refer)的物件有 quack 方法,那麼就可以呱呱叫 … 對吧!
靜態定型語言沒辦法這麼做,對吧?…
在這個 Scala 範例中,Duck 與 Man 是沒有關係的兩個類別,然而都定義了 quack 方法,drawQuack 可以接受具有 quack 方法的物件,不管物件是何種型態,這是 Scala 用來支援 Duck typing 的語法,稱之為 Structural typing …
靜態定型語言沒辦法這麼做 … 真的嗎?

有人會說 … 喂 … 喂 .. 別騙人了 … 以為我不懂嗎?Structural typing 其實是靜態時期就決定好了吧!充其量只是在靜態時期模彷 Duck typing 吧!
嗯 … 你現在看到的 Java 程式碼,也可以做 Duck typing,只要傳給 doQuack 的物件有 quack 方法就可以了,這是利用到 Java 的反射(Reflection) API,可以在執行時期對任何具有 quack 方法的物件,呼叫其 quack …
當然啦!跟動態定型比起來,真的是囉嗦多了,畢竟 … 這才是 Java … XD

我說 …這才是 Java … 不只是在嘲笑 Java 真的比較囉嗦 … Java 本身就不是一個用來展現優雅的語言 … 在一些場合,囉嗦甚至反而會成為 Java 的優點,Java 在 Duck typing 的實現上讓開發者麻煩了許多,多半也表示這門語言,不太希望你使用 Duck typing …
那麼反過來看,為什麼你需要 Duck typing?彈性,特別是在一些需求變化快的 start-up 場合,在定義函式的實作內容時,如果不用特別去在型態上定義一些操作,儘管在函式中增加需要的操作,對開發初期而言,真的是非常非常地爽快 …
只是在爽快的感覺過後,專案已經渡過了 start-up 之後,大家的地位好像就拉近了 … 這時你該想的是,你現在的 Duck typing 運用中,實際上使用了一組操作…
那麼這組操作應該屬於誰?
某個類別的實例?
或者是不同類別的實例?

如果這組操作是屬於某個類別的實例呢?舉例來說,也許你會為了將實例 duck 傳給 drawQuack 方法執行,而臨時為實例定義 quack 單例方法(singleton method),實際上,你是在 duck 的匿名單例類別(anonymous singleton class)上定義了 quack 方法。
因而,上頭的單例方法定義,實際上也可以使用 Ruby 的開放類別語法,在開啟了 duck 的匿名單例類別之後,定義 quack 方法,也可以達到相同的效果。

所以,在享受過 Duck typing 與單例方法的方便性之後,你應該重構(refactor)程式碼 …
例如,如果在程式已經過 start-up 之後,你發現這樣的程式碼,但是你沒有權利或團隊慣例上不建議直接修改 Duck 類別所在的原始碼 …
可以考慮開啟 Duck 類別,在其中定義 quack 方法 …
如果你有權利直接定義 Duck 類別,慣例上也不違反,那就直接定義在 Duck 類別…

如果這組操作不屬於某個特定類別,而是可能在某幾種類別的實例之間共享呢?
在 Java 中,對於規範這組操作的公開協定(protocol),我們會使用 interface。
例如,我們會定義 DuckLike interface …
然後讓需要擁有這組操作的類別去擁有這個 interface 並實作操作細節 …
doQuack 方法接受擁有 DuckLike 行為的實例,也就是實作了 DuckLike interface 的實例,這麼一來,方法本體就簡潔多了 …
Java 囉嗦的好處就是,沒有擁有 DuckLike 行為的物件傳給 doQuack,編譯器絕對不會放過它,也就是說,執行時期的型態錯誤在這種情況下不會發生 … doQuack 的呼叫者,被強制規範了,至少,在開發成員眾多、水準不一、慣例之類的約束難以貫徹或確實傳達的環境中,這種強制規範算是種好處 …

所以了,再說一次,當你使用 Duck typing 時,實際上,你正在使用一組操作,Java 的 interface,只是迫使你思考,這組操作形成的集合,是不是要有個適當的名稱,也就是 interface 的名稱,然後要求實作類別都得實作出來 …
那麼,如果發現到,不同的類別實例,其實它們的一組方法實作有著類似的內容,你會怎麼做呢?

對於 Java 來說,JDK8之前可以透過設計模式(design pattern)來解決這類問題,而在 JDK8 中,在 interface 語法上多了個 default method 可以更漂亮地解決,你可以在介面讓方法有限度的實作了 …
例如,許多物件都會有比大比小的需求,實際上,像 lessThan、lessOrEquals 等方法,都可以基於 compareTo 來實作 …
因此,當你在 JDK8 如這邊 Comparable 實作之後,任何想要比大比小的實例,只要讓其類別實作 Comparable 的 compareTo 方法,就可以擁有 lessThan、lessOrEquals 等現成的實作了 …

在 Ruby 中,你會怎麼做?情勢應該很清楚了 … 用 module …
你可以定義一個 Comparable module,讓小於、小於等於等運算子的定義,基於 spaceship (<=>)這個運算子,然後讓想擁有小於、小於等於等運算子的類別,include Comparable module,並實作 spaceship 運算子,就可以小於、小於等於等現成的實作了 …
Duck typing 很有彈性,不過不表示,你在享用過彈性之後,任何進一步的思考都不用做 …

靜態定型如果遇上物件導向,就會引發更囉嗦的事情,來看看 Java 的 Generics 好了,在 Java 還沒有 Generics 之前,如果你想要定義一個可收集各種容器的容器,必須用 Object …
問題來了,因為編譯器看你 get 回來的是 Object 型態,不能指定給 String 的 text,本來會抱怨些什麼 … 不過 …
你用了 CAST 語法讓編譯器住嘴了,因為你認為自己知道在做什麼 … 很多人覺得 CAST 語法好像很玄 … 那沒什麼 … 多數情況下,大家只是拿它來封住編譯器的嘴罷了 …

只是,既然你要編譯器住嘴了,那麼在指鹿為馬的時候,編譯器就算千百個不願意,也只能忍氣吞聲 …
要是發生了 ClassCastException 的轉型錯誤,那一定是你的錯 … 跟編譯器沒關係了 …
Java 提出了 Generics 來拯救這個世界了 … 嗎?
Generics 可以讓你指定型態參數(type parameter),例如,在這邊你指定 ArrayList 裏都是 String,那麼,你就只能在裏頭放 String … 如果你斗膽在裏頭放別的,像是整數 …
那編譯器就會馬上告訴你,你正在做一些無腦的事情,而且,你沒辦法叫它住嘴 … YA!

事實上,Generics 可以摧毀程式碼,看著這堆角括號,我頭都痛了 … XD
如果你可以指定型態參數,其實就是對型態的一種擴充,ArrayList<String> 會是一種型態,ArrayList<Integer> 會是一種型態 …
如果型態參數再遇上物件導向,那無疑就是對型態的一種超巨大的擴充,你得考慮繼承的情況,得考慮正變性(Covariance)、逆變性(Contravariance)…
每次看到這種 Generics 應用,連我都很想跳到 Ruby 的世界了 … XD

Generics 其實是一種參數多型(Parametric polymorphism)的實現,只不過,在物件導向的世界中實現本來就很複雜,如果遇上如 Java 這種在 Generics 設計上不太優良的情況,就會更加複雜 …
複雜度不會憑空消失,被隱藏起來的複雜度,一定會在某處兌現,反過來說,如果有苦命的 API 設計者幫你兌現了複雜度,身為 API 的客戶端,就能得到方便性,像是在這邊的範例,其實就獲益於 Generics 與 Lambda 的型態推斷能力,而能讓程式碼簡潔許多 …
而且,就如剛剛所看到的,Generics 將型態檢查的工作,從執行時期推向了編譯時期,這麼一來,就可以避免執行時期錯誤 ClassNotFoundException 的發生…

談 Generics 做什麼?我相信各位當中,有些人是因為看到 Java 的 Generics 就沒力,因而放棄了 Java 而進入 Ruby 的世界 … 只是我想藉由剛剛簡短的 Generics 介紹,讓各位想想,誰應該為型態檢查負出責任?
靜態定型語言的支持者認為,編譯器能在編譯時期執行全面的型態檢查,確保某些因錯誤型態而引起的錯誤不致於發生 …
動態定型語言的支持者認為,呼叫函式時就算確定傳遞的型態是正確的,也不保證函式的結果會是正確的,只有單元測試才能保證型態與行為都是正確的 …

這下子就導到了靜態定型與單元測試之爭了 … 如果從這個角度來看 … 編譯器就像是在作型態的斷言(assertion)測試,而且,如果語言本身擁有很好的型態推論能力的話,這個型態斷言測試幾乎可以不付出太代價,甚至免費的擁有。
實際上,編譯器提供的型態錯誤訊息越多,表示你越無法通過型態的斷言測試,表示你沒有仔細思考型態,為了通過編譯器這邊,你需要更仔細地思考有關型態的問題。
有時開發者以為自己對型態思考的夠多了,應該沒什麼問題了,實際上並不是如此,這種情況在 Haskell 這個特別重視型態正確的語言中,更容易發生,要通過 Haskell 的編譯如此之難 …
甚至還有這種玩笑話,嘿!編譯通過,可以出貨了 … XD

在使用動態定型語言時,開發者免去了型態宣告的負擔,不過 … 變成得負擔型態檢查的工作 …
那麼,你知道程式碼中實際上要進行哪些型態檢查嗎?這個語言中採用了哪些型態?針對型態檢查,你知道如何做單元測試嗎?
你會想要,或者你能不能寫更多的單元測試,用以確保程式中的型態與行為都是正確的?
請問在場的各位,你有沒有寫單元測試的習慣?

型態檢查該檢查什麼呢?回顧一下剛剛看過的 fill 方法 …
為什麼 fill 方法中,用 respond_to? 來檢查,而不是用 is_a? 來檢查?

既然談到了單元測試,那就來看看怎麼測試這個 doubleMe 方法好了,到底要怎麼測試 … 施主 … 這個問題得問你自己 …
你有真的想過 doubeMe 到底要 double 的對象是誰嗎?Fixnum、String、Array 的實例嗎?Range 可不可以?喔!應該不行吧!Range 沒有定義 + 這個方法。
那麼,如果有個物件沒有定義 + 這個方法,你的 doubleMe 裏要不要做這項檢查?檢查出不具 + 這個方法時要不要拋出例外?還是 … 應該在被傳入的物件上定義 method_missing 方法呢?
在動態定型語言中,在單元測試時,可以多問問自己有關型態的問題,畢竟沒有編譯器在一旁嘮嘮叨叨了 …

Joshua Bloch說「既然寫正確的程式那麼難,我們就應該盡力取得幫助。所以,能減少bug的所有東西都是好的。這就是我是靜態型別語言和靜態分析的信徒的原因」…
也就是說 … 在動態定型語言中 …

要找出自己對型態檢查到底有哪些實際需求,而且不單只是依靠單元測試,在必要的時候,不要只會ㄍ一ㄥ …
找看看 Ruby 的生態系中,有沒有像 python-rightarrow、PEP3107 – Function Annotations 這類的工具 …

如果真的需要,強者也是可以借用一下 IDE 的啦!… XD

甚至是 … 找些像是 TypeScript 這類的語言來作協助 … 這個語言有點意思,它基於 JavaScript 的語法直接作擴充,有個 tsc 編譯器用來將程式碼編譯為 .js 檔案…裏頭當然是 JavaScript 的原始碼 …
TypeScript 其實是動態定型語言,不過你可以在必要的時候加註型態,對於有加註型態的程式碼,TypeScript 可以做型態推斷,也就是你可以有編譯時期檢查,它甚至也有 Generics 之類的東西 …
如果不想加註型態也沒關係,就當作動態語言來用罷了 … 你擁有選擇的權利 …
選擇的權利?對!選擇的權利!動態定型語言與靜態定型語言相比較,最重要的是前者擁有型態檢查的時機、工具等選擇的權利 … 而不像靜態定型語言那樣,被強制的情況比較多 …
既然你擁有選擇的權利,就別輕易放棄 … 多做些型態上的思考總是好的 …

看看時間,差不多是該做結論了 … 不過有個小小的問題留給各位 …
有機會的話,請各位想想,在 Java 中,checked exception 與 unchecked exception 的差別在哪?對!全世界只有 Java 特別將例外區分為這兩大類 … 也造成了不少麻煩 …
只不過,當編譯器介入例外的檢查之後,其實倒是有機會讓我們想想,哪些例外是客戶端可處理的,哪些例外是程式的 bug …
如果能從 Java 的 checked exception、unchecked exception 對例外多一些思考,那麼回到 Ruby 之後,想必對各位在處理錯誤的時候也會更有所幫助!

這是我的經驗 … 我愛研究各種語言生態,就是因為這些語言之間總能交相刺激我的思考 …
在定型這塊,我往往會發現到,想要更好的運用動態定型,就需要瞭解靜態定型 … 實際上在準備這場演講的過程中,我不斷地思考靜態定型裏的東西,在 Ruby 中要怎麼做,然後我又更深入認識了 Ruby …
只有在知道靜態定型的優點之後,也才能進一步學習到動態定型的優缺點 …
能理解靜態定型為什麼要有那些限制,也能發掘出更多你在使用動態定型語言時,應當負有的責任 …

Ruby 有著這麼多具強大威力的特性 … 動態定型、物件個體化、開放類別、執行時期自省等 …
手上握有 Ruby,你就握有這麼大的能力,那麼 … 有想過你應該負起的責任是什麼呢?

“We‘re all consenting adults here.” 對吧!我們都知道自己在作什麼,對吧!
不過,這可不只是一個口號而已!

記得!能力越大,責任越重!

如果你想更深入地瞭解 Ruby,記得,有時間的話,多進一步認識靜態定型!
很感謝各位聽完我今天的分享!謝謝大家!