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 条评论
登录 后参与评论

相关文章

来自专栏章鱼的慢慢技术路

用Go语言实现一个简单的聊天机器人

Go语言基本构成要素:标识符、关键字、字面量、分隔符、操作符。它们可以组成各种表达式和语句,而后者都无需以分号结尾。

1052
来自专栏web前端-

JavaScript基础概述

    1.常量: 常量就是在程序运行过程中,不会发生变化的量,常量通常用来表示固定不变的量,比如圆周率,万有引力常量

632
来自专栏ml

C plus plus 控制格式

使用这些格式需要声明包含<iomainip> long flags( ) const 返回当前的格式标志。 long flays(long newflag) 设...

2134
来自专栏C/C++基础

C++编码格式建议

每个人都可能有自己的代码风格和格式,但如果一个项目中的所有人都遵循同一风格的话,这个项目就能更顺利地进行。每个人未必能同意下述的每一处格式规则,而且其中的不少规...

1002
来自专栏hbbliyong

12个JavaScript技巧

在这篇文章中将给大家分享12个有关于JavaScript的小技巧。这些小技巧可能在你的实际工作中或许能帮助你解决一些问题。 使用!!操作符转换布尔值 有时候我们...

28510
来自专栏Java帮帮-微信公众号-技术文章全总结

JavaWeb03-轻松理解JS(Java真正的全栈开发)

? 一.js常用对象 ljs中的常见对象有以下几个: Boolean Number String Array 数组 Date 日期 Math 数学 RegEx...

27612
来自专栏Nian糕的私人厨房

JavaScript 函数

通常来说,一个函数就是一个可以被外部代码调用(或者函数本身递归调用)的"子程序",和程序本身一样,一个函数的函数体是由一系列的语句组成的,函数可以接收传入参数,...

723
来自专栏web前端

JavaScript基础学习--07函数的传参、重用、价格计算

Demos:   https://github.com/jiangheyan/JavaScriptBase 一、函数传参      1、参数=js数据类型  ...

1888
来自专栏深度学习思考者

C语言标准工具库函数库:stdlib.h

  对于一些特殊的操作,C语言提供了标准工具库函数库,其中包括可以实现数值转换,内存分配,随机数操作以及字符串转换等函数。本篇博文一一来讲述这个函数库中的那些函...

2648
来自专栏转载gongluck的CSDN博客

结构体字节对齐

      在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。从理论上讲,对于任何变量...

3415

扫码关注云+社区