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)才能发现错误。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏desperate633

设计模式之适配器模式(Adapter Pattern)适配器模式的定义

适配器模式(Adapter Pattern)在生活中的应用随处可见。最常见的,我们使用的转接头就是利用了适配器模式的思想,我们可能用type-c接口的手机,但现...

902
来自专栏一直在跳坑然后爬坑

Flutter常用widget 'Row、Column'

用于水平显示子项 A widget that displays its children in a horizontal array. 注:这个控件本身不可...

752
来自专栏懒人开发

鸿洋AutoLayout代码分析(五):Attr相关类

上一节,我们分析了很多类, 其中,最重要的是 AutoLayoutHelper 类的2个方法 adjustChildren() 和 getAutoLayou...

823
来自专栏Android知识点总结

来谈谈Java的深浅拷贝吧

834
来自专栏WebHub

DOM中历史遗留的那些天坑 ...

即时到了DOM3.0时代, 为了同时满足浏览器的向下兼容和ES6的最新街口, DOM还是保留了很多古老的,极易和新类型引起混淆的类比如HTMLCollectio...

905
来自专栏用户画像

计算两个经纬度的距离

682
来自专栏前端儿

深入理解JavaScript的事件循环(Event Loop)

在两个环境下的Event Loop实现是不一样的,在浏览器中基于 规范 来实现,不同浏览器可能有小小区别。在Node中基于 libuv 这个库来实现

532
来自专栏美团技术团队

红黑树深入剖析及Java实现

红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。 BST 二叉查找树(Binary Search Tree,简称BST)是一棵二...

2783
来自专栏deepcc

javaScript创建无边框iframe兼容ie

3215
来自专栏Java3y

JSP第七篇【简单标签、应用、DynamicAttribute接口】

为什么要用到简单标签? 上一篇博客中我已经讲解了传统标签,想要开发自定义标签,大多数情况下都要重写doStartTag(),doAfterBody()和doEn...

2894

扫码关注云+社区