2015年3月23日 星期一

Scala Duck typing - Structural type

不算前言
Duck typing在Dynamic Language很常見到, 儘管Scala是在JVM上運行的靜態語言, 但透過強大的Compiler 實現Duck typing, 也拜靜態語言之福, 還能保有Type safe的特色, 只是自然也有些限制...

Why ducking typing

Wiki Duck typing上的解釋算蠻簡單明瞭,
但若不是我自己真的遇到過需求, 還是很難真正體會
基本上我是把它Duck typing當作泛行Generic type的進階應用
在OOP世界中, 泛型的進階應用多半是透過繼承
但是要先定義好Super class 之後
才有辦法透過繼承開始寫下去

但在現實生活中, 很多原本想不到的軟體需求
日新月異的冒出來, 從客戶從PM等等
為了他們創造各種物件來Program
直到某一天發現很多code都重複了,
想寫一些Generic method來精簡化,
開始發現各種物件都有共通性進而開始定義Super class
要進行 Refactor, 就是頭大的時候

比如說
今天做一個報表A, 它有時間欄位, 然後又寫個Sort function來進行排序
 case class ReportA(time:Date, name:String, id:String)  
 val la = List( ReportA(new Date, "a1", "id1"), ReportA(new Date, "a2", "id2"))  
 def sort(aList:List[ ReportA]) = aList.sortBy(_.time)  
 sort(la)  

你自然會想到sort function 的參數都用List[A] 來當型態傳入
可是過兩週客戶跟你說他要報表B, 它跟A一樣有時間欄位也要排序, 但其它欄位都不一樣
 case class ReportB(time:Date, sister:String, max:Int)  
 val lb = List( ReportB(new Date, "b1", 1), ReportB(new Date, "b2", 2))  
 def sort2(bList:List[ ReportB]) = bList.sortBy(_.time)  
 sort(lb)  
然後你就得為了B在寫一個sort2 function?

接著就是發現A, B都有time欄位,
那乾脆創造Super class來做泛型
做 time的trait 然後讓ReportA, ReportB 去繼承,
然後sort function吃泛型T, T class是trait time後代
所以可以call time field來進行 sort
 trait time{ val time:Date }  
 case class ReportA(name:String, id:String) extends time { val time = new Date }  
 case class ReportB(sister:String, max:Int) extends time { val time = new Date }  
 val list = List( ReportB("b1",1), ReportB("b2",2))  
 def sort[ T <: trait ]( s:List[ T ] ) = s.sortBy( _.time )  
 sort(list)  
我得改所有ReportA, ReportB原本的Code
然後原本去創造ReportB(new Date, "b1", 1) 的Code得改成 ReportB("b1",1)
如果你今天已經做到ReportC....ReportZ之後才有時間整理重構
然後你還有其他上千行Code也有地方用到這些ReportX
與其改完一堆Compile error 
我看多半不如就放棄讓未來的人頭大吧XD

話不是這樣說的, 時代要進步, Coding也是
如果Code寫到最後都會變成這樣, 那乾脆別寫回老鄉耕田

多半會有人說SA(Software analytics)沒做好, 活該
但現在科技產業, 新創公司如雨後春筍冒出
不是強的打敗弱的, 是快的打敗慢的
兩三天就有新產品冒出來,
你閉關十年把SA做完, 人家早都改朝換代了

不是說不做SA, 是市場不不讓你過度設計,
更甚者你也不能預知未來,
此時一個靈活的語言, 能夠讓你無痛 Refactor
更甚者不斷把產品擴大, 把網路產業的Scalability展現出來
才是王道

What is ducking typing

在Scala 世界中是透過Structural type來實現Duck typing
就拿剛剛的例子來說
當 ReportB 出現時候, 不去改 ReportA 也不創Super class trait time,
直接改sort function吃的泛型T
 case class ReportA(time:Date, name:String, id:String)   
 case class ReportB(time:Date, sister:String, max:Int)   
 val list = List( ReportB("b1",1), ReportB("b2",2))  
 def sort[ T <: { val time:Date } ]( s:List[ T ] ) = s.sortBy( _.time )  
 sort(list)  

[ T <: { val time:Date } ]
這段就是在說, 傳進來的泛型T
我不管它是甚麼, 只要他有 field time:Date就允許
因為我sort function也只拿 time欄位來排序

Duck typing本質上就是, 我不管你傳進來是甚麼
只要它符合特定條件, 比如說它有鴨嘴, 蹼
那我就視它為鴨子
畢竟我也只要它的鴨嘴, 蹼來用而已
這樣而已

也有些duck typing 用在method
比如說它會游泳
 def swim( duck: { def swim():Unit } ) = duck.swim()  
傳進來的任何東西
只要有定義swim method會游泳
我就視為鴨子

當然有可能它傳進來魚
魚也會游泳, 這其實是只注重在你所在意的特定條件
所以也要謹慎命名, 使用免得Code 很難讀懂

在Scala 用Structural type來實現Duck typing的好處是

1.  Code簡單明瞭
[ T <: { val time:Date } ]就可以直接知道你在意的是有time field的物件
如果是繼承你還得去開其它檔案來找出Source trait 的 code

但如果今天你的Structural type比較複雜超過5行
Convention coding style 是要另外定義個type來命名
比如說
 type duck{  
 def swim(): Unit  
 val duckbill: String  
 val flippers: String  
 val skinColor: String  
 }  
 def sort( duck: duck ) = { ... }  
會比較乾淨明瞭
把 [ ] 裡面塞一堆code, 反而你要看function 本體很難看懂

2.  Compiler 層級就會知道你傳進來的Class有無正確
這是靜態語言的優勢, 很多Bug在, Compile時候就能幫挑出來
如果你傳入的物件並不符合Duck 定義, 在動態語言你得執行之後才能發現
最可怕的是, 是產品上線了都還沒測出來...

Scala Duck typing 限制

前幾天被這件事搞得很痛苦就是
Scala duck typing沒有辦法self reference
就是說你不能這樣寫
 def add[T <: { def +(d2:T):T }]( d1: T, d2: T ) = d1 + d2  

[T <: { def +(d2:T):T }] compile就會跟你說
error: Parameter type in structural refinement may not refer to an
abstract type defined outside that refinement

因為我的Duck type T 裡面定義的 + method 又用 Type T 來當作參數以及回傳型態
我上網研究一下Scala duck typing structural type self reference發現

Within a method declaration in a structural refinement, the type of any value parameter may only refer to type parameters or abstract types that are contained inside the refinement. That is, it must refer either to a type parameter of the method itself, or to a type definition within the refinement. This restriction does not apply to the function’s result type.

大概就是JVM 先天上的限制, 我覺得是跟Type eraser有關係
當然還是希望Scala 2.11之後有機會支援
或者說你有發現實作Structural type self reference 的方式
可以討論看看

我有想到Monoid或者implicit 等可以模擬出來
但就是很不乾脆得寫一堆 implicit
可惜了

語言天生能夠支援的話真的很強大


沒有留言:

張貼留言