2015年4月18日 星期六

Scala knowledge - Type members, Path-dependent type and Dependent method types 的意義與由來

前言
從OO出身而言, 大家比較有概念的是class or object, 但到了FP世界, 就有type的概念, 看起來很相似又好像有點混淆, 但本質上應該都是一樣的...



Type

簡單來說不管你是Class or Object什麼其它的, 
在Compiler裡面, 都是用type角度去看待這些information
比如說String Class or String Object, 在Compiler層級看到就是一個 String type

在Scala中你定義的
  1. class, trait, object 都是type
  2. 或者透過type keyword 來定義 type
就是說當你寫一個Class, Trait, Object 就會自動相對應創造該 Class的 type 

Class, Trait 你可以透過本身的name 來取得type
Object 你得透過 name.type 來取得 type
比如
 scala> class ClassName  
 defined class ClassName  
 scala> trait TraitName  
 defined trait TraitName  
 scala> object ObjectName  
 defined module ObjectName  
 scala> def foo(x: ClassName) = x  
 foo: (x: ClassName)ClassName                 //ClassName取得該Class的 type
 scala> def bar(x: TraitName) = x  
  bar: (x: TraitName)TraitName                //TraitName取得該Trait的 type   
 scala> def baz(x: ObjectName.type) = x       //ObjectName.type取得Object的type      
  baz: (x: ObjectName.type)object ObjectName  

Type member

Keyword: type
Type member就是你的Class, Trait, Object裡面
透過keyword  type所創造出來的一個屬性
我們都知道Class裡面可以定義屬性和功能
傳統Java世界屬性包含變數和物件
現在多了個type member

比如
 scala> trait T{  
    | type X = Int  
    | type Y  
    | }  
 defined trait T  

我定義一個trait T, T 有兩個type member
一個是 X , 我assign Int type 給X , 所以X 算是Int type的alias name
另一個是 Y type member, Y 是abstract type, 因為我沒有定義這個type是什麼
當我implement trait T時候, 就得一起implement type y 是什麼 

type member的 select 方式是透過path & project
所以有相對應的東西出現就是path-dependent type

Path-dependent type

簡單來說要存取type member可以透過逗號 . 或者井字號 #
比如說 Trait T 中要提取 type member X 就是 T.X
就跟你提取物件裡面的變數或者method一樣
所以也是透過一個path來提取type member
這個path就是type member的外層Class, Trait or Object
更全面的例子
 class Outer {  
  trait Inner  
   def y = new Inner {}  
  def foo(x : this.Inner) = null  
  def bar(x : X#Inner) = null  
 }  
 scala> val x = new Outer  
 x: Outer = Outer@58804a77  
 scala> val y = new Outer  
 y: Outer = Outer@20e1ed5b  
 scala> x.y  
 res0: java.lang.Object with x.Inner = Outer$$anon$1@5faecf45  
 scala> x.foo(x.y)  
  res1: Null = null  
 scala> x.foo(y.y)  
 <console>:9: error: type mismatch;  
  found  : java.lang.Object with y.Inner  
  required: x.Inner  
 x.foo(y.y)  
 scala> x.bar(y.y)  
  res2: Null = null  

在我前一篇文章有提到nested巢狀class存取時候會有path dependent types 效果
是說不同的Instance 所提取出來的class 都是屬於不同type
type member也是(Class 就是 type)

Class Outer的兩個Instance 提取出來的Inner
是不一樣的, 儘管你認為他們都是 Inner class
因為path-dependent types 就是依賴在他的path上,
path就是Instance, 既然兩個Instance 視為不同
因此此時Path也就不同, 所以直白上
x.Inner 與 y.Inner 的語意就是
Outer@58804a77.Inner 與 Outer@20e1ed5b.Inner
兩者不一樣

另一個就Type projection
意思上就是把nested 的type member or class 映射出來使用
不需要透過Outer path, 
既然映射出來沒有path
所以不會有Instance path的dependent關係
不同的Instance 應射出來的 Inner 會是一樣的type(path不見了)

講到這裡只知道 what is path-dependent type
但是 why is path-dependent type?
最近拜讀了這篇文章大概有體會到 path-dependent type潛在的應用

我這邊簡單講下該篇文章的情境和應用(建議看原文更全面明白)
主要是在講美國同人小說,
今天有個 Franchies object and class
我不太清楚該怎麼翻譯, 大概就是小說製作商
裡面定義class Character 表示同人角色
然後透過createFanFiction來創作同人小說
 object Franchise {  
   case class Character(name: String)  
  }  
 class Franchise(name: String) {  
  import Franchise.Character  
  def createFanFiction(  
   lovestruck: Character,  
   objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire)  
 }  


然後可以開始應用
 val starTrek = new Franchise("Star Trek")  
 val starWars = new Franchise("Star Wars")  
 val quark = Franchise.Character("Quark")  
 val jadzia = Franchise.Character("Jadzia Dax")  
 val luke = Franchise.Character("Luke Skywalker")  
 val yoda = Franchise.Character("Yoda")  

先後創造
星際大戰starWar 同人製作商
星際爭霸戰starTrek 同人製作商

接著
創造各自的同人角色
星際大戰starWar
luke(天行者路克), yoda(尤達大師)
星際爭霸戰starTrek 的角色:
querk, jadzia (不知道是誰==)

接著就用
createFanFiction來創作同人小說
createFanFiction接受一對角色參數
(lovestruck: Character,objectOfDesire: Character)來讓你建立小說

悲劇的地方來了
 starTrek.createFanFiction(lovestruck = jadzia, objectOfDesire = luke)  

竟然在星際爭霸戰starTrek 製作商給予
星際大戰starWar的角色
luke(天行者路克), yoda(尤達大師)
這完全不合理, 不合原作的設定

簡單來說就好比
倚天屠龍記裡面你放入楊過一樣奇怪

但是這樣的Code還是可以執行不會有任何程式錯誤
但這樣下去你的Product這會潛藏各種Business knowledge error
而且你很難發現

Path-dependent type就是用來避免這種錯誤
在compiler階段可以確保code中隱含的商業邏輯正常
這種概念進一步讓你可以把商業邏輯對應到Code的物件邏輯
然後透過Compiler來確保你的商業邏輯正確正常

所以剛剛的
星際爭霸戰starTrek 與星際大戰starWar的角色邏輯錯誤問題
可以透過code這樣設計來避免
 class Franchise(name: String) {  
  case class Character(name: String)  
  def createFanFictionWith(  
   lovestruck: Character,  
   objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire)  
 }  

class Character定義在Franchies class裡面
而此時透過Franchies class創造出來的class Character
就附有path-dependent type 特質

 val starTrek = new Franchise("Star Trek")  
 val starWars = new Franchise("Star Wars")  
 val quark = starTrek.Character("Quark")  
 val jadzia = starTrek.Character("Jadzia Dax")  
 val luke = starWars.Character("Luke Skywalker")  
 val yoda = starWars.Character("Yoda")  

接著用個別的
星際大戰starWar Franchies instance來創造
luke(天行者路克), yoda(尤達大師)

再試著星際爭霸戰starTrek 製作商給予
星際大戰starWar的角色
luke(天行者路克), yoda(尤達大師)
 starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke)  


就會發生compile error
 found  : starWars.Character  
 required: starTrek.Character  
         starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke)  
                                           ^  


compiler 還直接跟你說
星際爭霸戰starTrek createFanFictionWith不接受
星際大戰的角色starWar.Character luke(天行者路克), yoda(尤達大師)
他只接受星際爭霸戰starTrek.Character 角色

這是一個很有意思的例子,
要怎樣活用Path-dependent type
端看你的設計和需求,

尤其在抽象化層級與generic programming
Path-dependent type加上
abstract type member是很有用的
未來有例子再介紹

Dependent method types

Dependent method types算是Path-dependent type的進階應用
也算是function polymorphism or parametric polymorphism的一種
function  polymorphism簡單來說就是function的return type 會多型態改變
parametric polymorphism簡單來說就是function的return type
會依據你給什麼參數型態來改變
以後會有文章再詳細介紹

dependent method type亦即method的type (return type)
會根據(依賴)在你傳給它單個或者多個的參數來決定
聽起來有點類似我上面講的parametric polymorphism

parametric polymorphism基本上就是generic泛型function
比如
 scala> def parametricPoly[T](t:T):T = t  
 parametricPoly: [T](t: T)T  
 scala> parametricPoly("string")  
 res0: String = string  
 scala> parametricPoly(0)  
 res1: Int = 0  
 scala>  

定義個parametricPoly function, 接受各種type
給String就回傳String
給Int就會傳Int
是依賴在類型參數parameter type T 上
這個概念

而Dependent method types就好比
 scala> def identity(x:AnyRef):x.type = x  
 identity: (x: AnyRef)x.type  
 scala> val y = "foo"  
 y: String = foo  
 scala> val y2:y.type = identity(y)  
 y2: y.type = foo  
 scala>  

Dependent method types是沒有泛型generic
單純依賴變數參數x 的type
不是依賴在parameter type T 上(根本沒有generic T宣告)

所以上面範例
function identity, 給它 y就是 y.type 也就是String.type

Type members, Path-dependent type and Dependent method types 的由來

我不確定因果關係, 但在Scala作者的這篇paper有提到
Haskell 的type system有進一步的
Associated type,
就好比
 class Collects c where  
 type Elem c  
 empty :: c  
 insert :: c → Elem c → c  
 toList :: c → [Elem c]  

Collects是個interface
吃一個類型參數type class c
然後type class c associate 一個type Elem c
然後整個Collects 運作的type都是依賴在associated type Elem c

因此在Scala中這樣的實作就是透過
Type members, Path-dependent type and Dependent method types
這三者合力交錯完成的


以上

沒有留言:

張貼留言