Scalaz(2)- 基礎篇:隨意多態-typeclass, ad-hoc polymorphism


  scalaz功能基本上由以下三部分組成:

1、新的數據類型,如:Validation, NonEmptyList ...

2、標准scala類型的延伸類型,如:OptionOps, ListOps ...

3、通過typeclass的隨意多態(ad-hoc polymorphism)編程模式實現的大量概括性函數組件庫

我們在這篇重點討論多態(polymorphism),特別是隨意多態(ad-hoc polymorphism)。

多態,簡單來講就是一項操作(operation)可以對任意類型施用。在OOP的世界里,我們可以通過以下各種方式來實現多態:

1、重載 Overloading

2、繼承 Inheritance

3、模式匹配 Pattern-matching

4、特性 Traits/interfaces

5、類型參數 Type parameters

作為一種通用的組件庫,scalaz是通過任意多態typeclass模式來實現軟件模塊之間的松散耦合(decoupling).這樣scalaz的用戶就可以在不需要重新編譯scalaz源代碼的情況下對任何類型施用scalaz提供的函數功能了。

我們來分析一下各種實現多態的方式:

假如我們設計一個描述輸入參數的函數:tell(t: some type): String

如:tell(c: Color) >>> "I am color Red"

    tell(i: Int) >>> "I am Int 3"

    tell(p: Person) >>> "I am Peter"

如果用Overloading:

1 object overload { 2  case class Color(descript: String) 3  case class Person(name: String) 4 
5  def tell(c: Color) = "I am color "+ c.descript 6  def tell(p: Person) = "I am "+ p.name 7 }

我們看到用重載的話,除了相同的函數名稱tell之外這兩個函數沒有任何其它關系。我們必須對每一個不同的類型提供一個獨立的tell函數。這種方式沒什么用,我們需要的是一個函數施用在不同的類型上。

再試試用繼承Inheritance:

 1 trait Thing {  2  def tell: String  3 }  4 class Color(descript: String) extends Thing {  5   override def tell: String = "I am color " + descript  6 }  7 class Person(name: String) extends Thing {  8   override def tell: String = "I am " + name  9 } 10 
11 new Color("RED").tell                             //> res0: String = I am color RED
12 new Person("John").tell                           //> res1: String = I am John

這種方式更糟糕,tell和類有着更強的耦合。用戶必須擁有這些類的源代碼才能實現tell。試想如果這個類型是標准的Int怎么辦。

用模式匹配pattern-matching呢?

 1 case class Color(descript: String)  2 case class Person(name: String)  3 def tell(x: Any): String = x match {  4   case Color(descr) => "I am color " + descr  5   case Person(name) => "I am " + name  6   case i: Int => "I am Int "+i  7   case _ => "unknown"
 8 }                                                 //> tell: (x: Any)String
 9 
10 tell(23)                                          //> res0: String = I am Int 23
11 tell(Color("RED"))                                //> res1: String = I am color RED
12 tell(Person("Peter"))                             //> res2: String = I am Peter

Pattern-matching倒是可以把tell和類型分開。但是必須在tell里增加新的類型匹配,也就是說必須能控制tell的源代碼。

現在再嘗試用typeclass模式:typeclass模式是由trait加implicit組成。先看看trait: 

1 trait Tellable[T] { 2  def tell(t: T): String 3 }

這個trait Tellable代表的意思是把tell功能附加到任意類型T,但還未定義tell的具體功能。

如果用戶想把tell附加給Color類型:

1 trait Tellable[T] { 2  def tell(t: T): String 3 } 4 case class Color(descript: String) 5 case class Person(name: String) 6 object colorTeller extends Tellable[Color] { 7     def tell(t: Color): String = "I am color "+t.descript 8 }

針對Color我們在object colorTeller里實現了tell。現在更概括的tell變成這樣:

1 def tell[T](t: T)(M: Tellable[T]) = { 2  M.tell(t) 3 }                                                 //> tell: [T](t: T)(M: scalaz.learn.demo.Tellable[T])String
4 tell(Color("RED"))(colorTeller)                   //> res0: String = I am color RED

這個版本的tell增加了類型變量T、輸入參數M,意思是對任何類型T,因為M可以對任何類型T施用tell,所以這個版本的tell可以在任何類型上施用。上面的例子調用了針對Color類型的tell。那么針對Person的tell我們再實現一個Tellable[Person]實例就行了吧:

 1 val personTeller = new Tellable[Person] {  2     def tell(t: Person): String = "I am "+ t.name  3 }                                                 //> personTeller : scalaz.learn.demo.Tellable[scalaz.learn.demo.Person] = scala  4                                                   //| z.learn.demo$$anonfun$main$1$$anon$1@13969fbe
 5 tell(Person("John"))(personTeller)                //> res1: String = I am John
 6 val intTeller = new Tellable[Int] {  7     def tell(t: Int): String = "I am Int "+ t.toString  8 }                                                 //> intTeller : scalaz.learn.demo.Tellable[Int] = scalaz.learn.demo$$anonfun$ma  9                                                   //| in$1$$anon$2@6aaa5eb0
10 tell(43)(intTeller)                               //> res2: String = I am Int 43

如上,即使針對Int類型我們一樣可以調用這個tell[T]。也既是說如果這個概括性的tell[T]是由其他人開發的某些組件庫提供的,那么用戶只要針對他所需要處理的類型提供一個tell實現實例,然后調用這個共享的tell[T],就可以得到隨意多態效果了。至於這個類型的實現細節或者源代碼則不在考慮之列。

好了,現在我們可以用implicit來精簡tell[T]的表達形式: 

1 def tell[T](t: T)(implicit M: Tellable[T]) = { 2  M.tell(t) 3 }                                                 //> tell: [T](t: T)(implicit M: scalaz.learn.demo.Tellable[T])String

 也可以這樣寫:

1 def tell[T : Tellable](t: T) = { 2  implicitly[Tellable[T]].tell(t) 3 }                                                 //> tell: [T](t: T)(implicit evidence$1: scalaz.learn.demo.Tellable[T])String

 現在看看如何調用tell:

 

 1 implicit object colorTeller extends Tellable[Color] {  2     def tell(t: Color): String = "I am color "+t.descript  3 }  4 
 5 tell(Color("RED"))                                //> res0: String = I am color RED
 6 
 7 implicit val personTeller = new Tellable[Person] {  8     def tell(t: Person): String = "I am "+ t.name  9 }                                                 //> personTeller : scalaz.learn.demo.Tellable[scalaz.learn.demo.Person] = scala 10                                                   //| z.learn.demo$$anonfun$main$1$$anon$1@3498ed
11 tell(Person("John"))                              //> res1: String = I am John
12 
13 implicit val intTeller = new Tellable[Int] { 14     def tell(t: Int): String = "I am Int "+ t.toString 15 }                                                 //> intTeller : scalaz.learn.demo.Tellable[Int] = scalaz.learn.demo$$anonfun$ma 16                                                   //| in$1$$anon$2@1a407d53
17 tell(43)                                          //> res2: String = I am Int 43

 

假如我忽然需要針對新的類型List[Color], 我肯定無須理會tell[T],只要調用它就行了:

 

1 implicit object listTeller extends Tellable[List[Color]] { 2     def tell(t: List[Color]): String = { 3         (t.map(c => c.descript)).mkString("I am list of color [",",","]") 4  } 5 } 6 
7 tell[List[Color]](List(Color("RED"),Color("BLACK"),Color("YELLOW"),Color("BLUE"))) 8                                                   //> res3: String = I am list of color [RED,BLACK,YELLOW,BLUE]

 

這才是真正的隨意多態。

值得注意的是implicit是scala compiler的一項功能。在編譯時compiler發現類型不對稱就會進行隱式轉換解析(implicit resolution)。如果解析失敗則程序無法通過編譯。如果我這樣寫: tell(4.5),compiler會提示語法錯誤。而上面其它多態方式則必須在運算時(runtime)才能發現錯誤。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM