做为一个 Java 开发者, class
的概念确定是耳熟能详了,但是在山的另外一边还有拥有别样风情的 type classes
,但不翻过 Java 这座山,它就始终隔着一层纱。html
在编程中,常常须要判断两个值是否相等,而在很长的一段时间内这个问题都没有一个标准的解决方案,这就是经典的判等问题。java
我这里统一使用 “值” 来代替对象、基本类型等等概念,以便于简化沟通编程
在 Java 中,咱们能够用 ==
,也能够用 equals
来判断值是否相等markdown
public void test() {
boolean res = "hello" == "world";
boolean res2 = "hello".equals("hello");
boolean res3 = 3 == 3;
boolean res4 = 5 == 9;
}
复制代码
熟悉 Java 的同窗都知道对于非基础类型, equals
方法的默认实现其实就是调用 ==
操做符,而 ==
操做比较的是对象的引用地址app
public class Object {
// ......
public boolean equals(Object obj) {
return (this == obj);
}
// ......
}
复制代码
全部类都会有 equals
方法,这是由于在 Java 中默认全部类型都是 Object 的子类。框架
其实这也是 Java 语言处理判等问题的解决方案,即统一从 Object 中继承判等方法。less
但是对于纯函数式的语言,好比 Haskell 来讲,它没有 OOP 中的继承、类等概念,它又该如何优雅的解决判等的问题呢?ide
若是你以为 Haskell 比较陌生,咱们就换一种提问的方式:还有其它通用的设计方案能够解决这类判等问题吗?函数
固然有,而 Type classes 就是这个领域内最靓的那个仔,要了解 Type classes, 还得先从多态开始。oop
Type classes 结合了 ad-hoc polymorphism(特设多态)和 Parametric polymorphism (参数化多态),实现了一种更通用的重载。
问题来了,什么是特设多态、参数化多态呢?
关于多态的更多内容 ,还能够参考个人前一篇文章《多态都不知道,谈什么对象》
ad-hoc polymorphism
(特设多态) 指的是函数应用不一样类型的参数时,会有不一样的行为(或者说实现)
最典型的就是算术重载
3 * 3 // 表明两个整形的乘法
3.14 * 3.14 // 表明两个浮点数的乘法
复制代码
Parametric polymorphism
(参数化多态) 指的是函数被定义在某一些类型之上,对于这些类型来讲函数的实现都是同样的。
好比 List[T] 的 size()
函数,不管 T 的类型是 String、仍是 Int, size()
的实现都同样
List[String].size()
List[Int].size()
复制代码
虽然 Type classes 结合了两种多态类型,但它自己却被归到特设多态(ad-hoc polymorphism)这一分类下。
若是你想了解更多 type classes 的思想,很是推荐阅读 《How to make ad-hoc polymorphism less ad hoc》 这篇论文,它也算是 Type classes 的开篇做。
Type classes 通常译做类型类,最开始是由 haskell 引入并实现,因此咱们颇有必要先了解一下 haskell 中的 Type classes。
以最开始提到的判等问题为例,来看看在 Haskell 中怎么用 Type classes 去解决。
首先咱们得用关键字 class
定义一个 Type class,千万不要和 Java 的 class 混为一谈。
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
复制代码
/= 其实就是 !=
haskell 的 Type class 与 Java 的 Interface 相似,上面的 Eq 类型类就定义了 ==
和 /=
两个抽象函数,其中的 a 就是类型变量,与 Java 中的泛型相似。
由此看来,Type classes 只是抽象了一些共同的行为,而这些行为的具体实现会根据类型的不一样而不一样,具体的实现会由类型类实例来定义。
经过 instance
关键字能够建立类型类实例,下面展现了针对于于 Float 和 Int 的 Eq 类型类实例
instance Eq Int where
(==) = eqInt
(/=) = neInt
instance Eq Float where
(==) = eqFloat
(/=) = neFloat
复制代码
咱们假设 eqInt、neInt、eqFloat、neFloat 都已经由标准库实现了
这样就能够直接用 ==
和/=
函数对 Int 和 Float 进行判等了
-- 判断 Int 的相等性
== 1 2
/= 2 4
-- 判断 Float 的相等性
== 1.2 1.2
/= 2.4 2.1
复制代码
在调用 ==
或 /=
函数时,编译器会根据参数类型自动找到类型类实例,而后调用类型类实例的函数执行调用。
若是用户须要自定义判等函数,只须要实现本身的类型类实例便可。
此时你可能会不自觉的和最开始提到的继承方案作一个对比,我画了两个图,能够参考一下
若是仅仅从结构上来看的话,它们之间的差异就像 Comparable
和 Comparator
同样。
目前的 Java 是没法实现 Type classes 的,但同为 JVM 的语言,多范式的 Scala 却能够实现。
与 Haskell 不同, Type classes 在 Scala 中并非一等公民,也就是没有直接的语法支持,但借助于强大的隐式系统咱们也能实现 Type classes,因为实现的步骤比较公式化,也就被称之为 Type classes Pattern (类型类模式)。
在 Scala 中实现 Type classes Pattern 大体分为 3 个步骤
仍是之前面提到的判等问题为需求,按照前面总结的模式步骤来实现一个 Scala 版的 Type classes 解决方案。
第一步定义 Type class,实际就是定义一个带泛型参数的 trait
trait 也相似于 Java 的 interface,不过更增强大
trait Eq[T] {
def eq(a: T, b: T): Boolean
}
复制代码
接着咱们针对 String、Int 来实现两个类型类实例
object EqInstances {
implicit val intEq = new Eq[Int] {
override def eq(a: Int, b: Int) = a == b
}
implicit val stringEq = instance[String]((a, b) => a.equals(b))
def instance[T](func: (T, T) => Boolean): Eq[T] = new Eq[T] {
override def eq(a: T, b: T): Boolean = func(a, b)
}
}
复制代码
stringEq 和 intEq 采用了不一样的构造方式
两个实例都被 implicit
关键字修饰,通常称之为隐式值,做用会在后面讲到。
最后一步,来实现一个带隐式参数的 same
函数, 其实调用类型类实例来判断两个值是否相等
object Same {
def same[T](a: T, b: T)(implicit eq: Eq[T]): Boolean = eq.eq(a, b)
}
复制代码
implicit eq: Eq[T]
就是隐式参数, 调用方能够不用主动传入,编译器会在做用域内查找匹配的隐式值传入(这就是为何前面的实例须要被 implicit 修饰)最后来进行调用验证一下,在调用时咱们须要先在当前做用域内经过 import
关键字导入类型类实例(主要是为了让编译器能找到这些实例)
import EqInstances._
Same.same(1, 2)
Same.same("ok", "ok")
// 编译错误:no implicits found for parameter eq: Eq[Float]
Same.same(1.0F, 2.4F)
复制代码
能够看见,针对 Int 和 String 类型的 same
函数调用能经过编译, 而当参数是 Float 时调用就会提示编译错误,这就是由于编译器在做用域内没有找到能够处理 Float 类型的 Eq 实例。
关于 Scala 隐式查找的更多规则能够查看 docs.scala-lang.org/tutorials/F…
到这儿其实就差很少了,可是这样的写法在 Scala 里其实不是很优雅,咱们能够再经过一些小技巧优化一下
将 same
函数改成 apply
函数,能够简化调用
使用 context bound 优化隐式参数,别慌,context bound 实际就是个语法糖而已
object Same {
def apply[T: Eq](a: T, b: T): Boolean = implicitly[Eq[T]].eq(a, b)
}
// 使用 apply 做为函数, 调用时能够不用写函数名
Same(1, 1)
Same("hello", "world")
复制代码
简单说一下 context bund,首先泛型的定义 由 T
变成了 [T: Eq]
,这样就能够用 implicitly
[Eq[T]] 让编译器在做用域内找到一个 Eq[T] 的隐式实例,context bound 可让函数的签名更加简洁。
在 Scala 中,类型类的设计其实随处可见,典型的就有 Ordered
。
以判等问题引出 Type classes 有一些不足,咱们只意识到了与 OOP 的继承是一个不同的判等解决方案,不妨再回到 Java 作一些其余的比较。
以 Comparator[T]
接口为例,在 Java 中咱们常常在集合框架中这样使用
List<Integer> list = new ArrayList<>();
list.sort(Comparator.naturalOrder())
复制代码
若是将其改形成为 Type classes 的话
trait Comparator[T] {
def compare(o1: T, o2: T): Int
}
object Instances {
implicit val intComprator = new Comparator[T] {
def compare(o1: Int, o2: Int) = o1.compareTo(o2)
}
//... other instances
}
复制代码
List 的 sort 方法也须要改成带隐式参数的方法前面,这样咱们就不须要显示的传 Compartor 实例了
// 编译期会自动找到 Comparator[Integer] 实例
List[Integer] list = new ArrayList<>();
list.sort()
复制代码
能够认为上面的 Type classes 是基于 Scala 语法的伪代码
相信你也看出来了,与 Type classes 方案相比,最大的差异就是 Java 须要手动传入 Comparator 实例,也许你会疑惑:就这?
不要小看这二者的区别,这二者的区别就像用 var 定义类型同样
// Java8
Map<String, String> map2 = new HashMap<>();
// Java10
var map = new HashMap<String, String>();
复制代码
若是类型系统能帮你完成的事情,就让它帮你作吧!
看了 Haskell 和 Scala 的例子,最后仍是得总结一下:
Type classes 就是抽象了某一些类型的共同行为,当某个类型须要用到这些行为时,由类型系统去找到这些行为的具体实现。
最后仍是得再安利一下 Scala3,在 Scala3 中, Type classes 获得了足够的重视,直接提供了语法层面的支持,不再用写一大堆的模板代码, 今后能够叫作 Type classes without Pattern。
不过为了不“长篇大论”,相关的内容就留给下一篇文章了(点赞点赞点赞)。
弱弱的皮一下,还学得动吗?