float除以int是什么型_怒了!TypeClass什么时候变得这么重要了?

作者 | vran

1fbb64f7191457967b4b8ca2b3edd873.png

Hello, 麻烦给我的代码来一杯 Mojito 

前言

作为一个 Java 开发者, class 的概念肯定是耳熟能详了,可是在山的另一边还有拥有别样风情的 type classes,但不翻过 Java 这座山,它就始终隔着一层纱。

一个经典的问题

在编程中,经常需要判断两个值是否相等,这就是判等问题, 然而在很长的一段时间内这个问题都没有一个标准的解决方案。

我这里统一使用  “值” 来代替对象、基本类型等等概念,以便于简化沟通

在 Java 中,我们可以用 == ,也可以用 equals 来判断值是否相等

public void test() {    boolean res = "hello" == "world";    boolean res2 = "hello".equals("hello");    boolean res3 = 3 == 3;    boolean res4 = 5 == 9;}

熟悉 Java 的同学都知道对于非基础类型, equals 方法的默认实现其实就是调用==操作符,而 == 操作比较的是对象的引用地址

public class Object {  // ......    public boolean equals(Object obj) {  	return (this == obj);  }    // ......}

所有类都会有 equals 方法,这是因为在 Java 中默认所有类型都是 Object 的子类。其实这也是 Java 语言处理判等问题的解决方案,即统一从 Object 中继承判等方法。

cb5620941207af0819e3c3068fddd1b1.png

 可是对于纯函数式的语言,比如 Haskell 来说,它没有 OOP 中的继承、类等概念,它又该如何优雅的解决判等的问题呢?

如果你对 Haskell 比较陌生,我们就换一种提问的方式:还有其它通用的设计方案可以解决这类判等问题吗?

当然有,Type classes 就是一种方案,不过要了解 Type classes, 还得先从多态开始。

Type classes 与多态

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 的开篇作。

Haskell 与 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 中的 List 中的 T。

由此看来,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

在调用 == 或 /= 函数时,编译器会根据参数类型自动找到类型类实例,然后调用类型类实例的函数执行调用。如果用户需要自定义判等函数,只需要实现自己的类型类实例即可。

此时你可能会不自觉的和最开始提到的继承方案做一个对比,我画了两个图,可以参考一下

  • 继承方案中,结构是一个层次型的

1e46e6dd23d33a078cb07cdb6182cb24.png

  • Type classes 方案,结构是线性的692f9c7babb892961664eff2697a9446.png

这样的差别就像是 Java 中ComparableComparator的区别一样。

Scala 与 Type classes Pattern

目前在 Java 中是无法实现 Type classes 的,但同为 JVM 的语言,多范式的 Scala 却可以实现。

但 Type classes 在 Scala 中其实也不是一等公民,也就是没有直接的语法支持,但借助于强大的隐式系统我们也能实现 Type classes,由于实现的步骤比较公式化,也就被称之为 Type classes Pattern (类型类模式)。

在 Scala 中实现 Type classes Pattern 大致分为 3 个步骤

  1. 定义 Type class
  2. 实现 Type class 实例
  3. 定义包含隐式参数的函数

还是以前面提到的判等问题为需求,按照前面总结的模式步骤来实现一个 Scala 版的 Type classes 解决方案。 

第一步定义 Type class,实际就是定义一个带泛型参数的trait

trait 也类似于 Java 的 interface,不过更加强大

trait Eq[T] {  def eq(a: T, b: T): Boolean}

接着我们针对 String、Int 来实现两个类型类实例

object EqInstances {  // 处理 Int 类型的类型类实例  implicit val intEq = new Eq[Int] {    override def eq(a: Int, b: Int) = a == b  }    // 处理 String 类型的类型类实例  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 采用了不同的构造方式

  • stringEq 实例我采用的是类似于 Java 的匿名类进行构造
  • 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 隐式查找的更多规则可以查看 https://docs.scala-lang.org/tutorials/FAQ/finding-implicits.html

到这儿其实就差不多了,但是这样的写法在 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

回望 Java

以判等问题引出 Type classes 有一些不足,我们只意识到了与 OOP 的继承是一个不一样的判等解决方案,不妨再回到 Java 做一些其他的比较。

Comparator[T]接口为例,在 Java 中我们经常在集合框架中这样使用

List 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 定义类型一样

// Java8Map map2 = new HashMap<>();// Java10var map = new HashMap();

如果类型系统能帮你完成的事情,就让它帮你做吧!

总结一下

看了 Haskell 和 Scala 的例子,还是得总结一下:Type classes 就是抽象了某一些类型的共同行为,当某个类型需要用到这些行为时,由类型系统去找到这些行为的具体实现。

在 Scala 中基于隐式系统实现 Type classes Pattern 并不是很直观,好在 Scala3 中, Type classes 得到了足够的重视,直接提供了语法层面的支持,再也不用写一大堆的模板代码, 从此可以叫做 Type classes without Pattern

不过为了避免“长篇大论”,相关的内容就留给下一篇文章了(学不动了 ing,但还得学)。

全文完


以下文章您可能也会感兴趣:

  • 简单说说spring的循环依赖

  • Mysql 的字符集以及带来的一点存储影响

  • 从 30 分钟到 1 分钟 - 一个 Scala 项目的编译速度优化

  • Hadoop 生态之 MapReduce 及 Hive 简介

  • 一篇文章带你搞懂 Swagger 与 SpringBoot 整合

  • 延时队列:基于 Redis 的实现

  • 你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式

  • 锁优化的简单思路

  • 小程序中 Redux 的使用

  • 中文房间之争:浅论到底什么是智能

  • 容器管理利器:Web Terminal 简介

  • 理解 RabbitMQ Exchange

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

9968723d322158503d452b650f2dffb8.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值