Scala 中 Type Class 实现的套路

typeclass.png

Scala 中 Type Class 实现的套路

什么是Type Class

A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass,
that means that it supports and implements the behavior the typeclass describes. A lot of people
coming from OOP get confused by typeclasses because they think they are like classes in object
oriented languages. Well, they're not. You can think of them kind of as Java interfaces, only better.

TypeClassHaskell 语言里的概念,根据《Learn you a haskell》中的解释, TypeClass 是定义一些公共行
为的接口,=Java= 语言中最为接近的概念是 interface, Scala 语言中最为接近的概念是 trait.

Haskell 中,可以给任意类型实现任意 TypeClass, 而在 Java 中只能通过实现某个接口才能让 Class 具有接口定义
的行为, 如果要给某个类库的类型添加额外的行为,几乎是不可能的事情(通过代码生成技术可以).

为什么需要Type Class

TypeClass行为定义具有行为的对象 分离,更容易实现 DuckType; 同时, 在函数式编程中,通常将数据与行为
相分离,甚至是数据与行为按需绑定,已达到更为高级的组合特性.

Scala 中对 TypeClass的实现<a id="sec-1-3" name="sec-1-3"></a>

Scala 是基于JVM平台的语言,受JVM的限制,不具备像 Haskell 那样语言原生对 TypeClass 的支持, Scala 语言使用了
隐式转换的魔法,使之能够不那么完美地支持 TypeClass.

Scala 中实现 TypeClass 可总结为三板斧.

1. TypeClass Trait 定义

TypeClass定义即使用 trait 来定义相关行为接口,比如 半群 :

trait Semigroup[A] {
  def combine(a1: A, a2: A): A
}

2. 定义 TypeClass 隐式实例

针对需要实现 TypeClass 的隐式实例,比如我们实现 Int 加法半群,根据 Scalaimplicit 隐式的实现,会在伴生对象中
自动查找隐式实例,所以一般讲实例定义在 TypeClass 的伴生对象中,这样只要 import TypeClass 就可以获得隐式实例:

object Semigroup {
  implicit val intPlusInstance = new Semigroup[Int] {
    def combine(a1: Int, a2: Int): Int = a1 + a2
  }
}

一般地,还会在伴生对象中增加 apply 只能构造器来生成 TypeClass 实例,而 apply 方法可以省略,代码看起来会更加简洁:

object Semigroup {

  def apply[A](implicit instance: Semigroup[A]) = instance

  implicit val intPlusInstance = new Semigroup[Int] {
    def combine(a1: Int, a2: Int): Int = a1 + a2
  }
}

这样,我们就可以通过伴生对象,获取 TypeClass 实例,从而调用 TypeClass 的行为方法:

import Semigroup._

Semigroup[Int].combine(1, 2) // 3

3. 定义 TypeClass 语法结构

为了进一步简化代码,我们可以定义一些语法结构,来隐藏 TypeClass 的样板式的代码,如:
Semigroup[Int].combine(1, 2) , 我们希望通过简单的表达式来表示: 1 |+| 2. 如何来
实现呢?

object Semigroup {

  def apply[A](implicit instance: Semigroup[A]) = instance

  implicit val intPlusInstance = new Semigroup[Int] {
    def combine(a1: Int, a2: Int): Int = a1 + a2
  }

  // 增加语法结构
  abstract class Syntax[A](implicit I: Semigroup[A]) {
    def a1: A
    def |+|(a2: A): A = I.combine(a1, a2)
  }

  // 定义隐式转换方法,将
  implicit def to[A](target: A)(implicit I: Semigroup[A]): Syntax[A] = new Syntax[A] {
    val a1 = target
  }

}

定义完成后,我们就可以使用新的语法 |+| 来调用 Semigroup 这个 TypeClass 的方法了:

import Semigroup._

Semigroup[Int].combine(1, 2) // 3

// 使用语法操作符
1 |+| 2 // 3

改进,更简洁

将上述实现综合一下,实现一个 Semigroup TypeClass 需要以下代码:

trait Semigroup[A] {
  def combine(a1: A, a2: A): A
}

object Semigroup {

  def apply[A](implicit instance: Semigroup[A]) = instance

  implicit val intPlusInstance = new Semigroup[Int] {
    def combine(a1: Int, a2: Int): Int = a1 + a2
  }

  // 增加语法结构
  abstract class Syntax[A](implicit I: Semigroup[A]) {
    def a1: A
    def |+|(a2: A): A = I.combine(a1, a2)
  }

  // 定义隐式转换方法,将
  implicit def to[A](target: A)(implicit I: Semigroup[A]): Syntax[A] = 
    new Syntax[A] {
      val a1 = target
    }

}

仔细分析,这里面有一些样板代码,比如伴生对象的 apply 方法、语法结构的类和隐式转换方法,
这对于所有的类型类都一样,都要写这些重复的代码。去掉这些样板代码,我们真正需要的是:

  1. TypeClass 的定义
  2. TypeClass 的类型实例
  3. TypeClass 的语法操作符

得益于 Scala 元编程的强大,我们可以通过 Macro 来自动生成这些样板代码。 Simulacrum
项目就是专注于此。

利用 Simulacrum, 我们可以将我们的代码改进为

import simulacrum._

@typeclass trait Semigroup[A] {
  @op("|+|") def combine(a1: A, a2: A): A
}

// 定义隐式实例
implicit val intPlusInstance = new Semigroup[Int] {
  def combine(a1: Int, a2: Int): Int = a1 + a2
}

// 使用
import Semigroup.ops._
1 |+| 2 // 3

参考资料

  1. Simulacrum: https://github.com/mpilquist/simulacrum
  2. TypeClass in Cats: http://typelevel.org/cats/typeclasses.html
  3. About Scala Macro: http://docs.scala-lang.org/overviews/macros/overview.html
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Scala中的implicit关键字对于我们初学者像是一个谜一样的存在,一边惊讶于代码的简洁,一边像在迷宫里打转一...
    大刀阅读 18,461评论 10 18
  • 这篇讲义只讲scala的简单使用,目的是使各位新来的同事能够首先看懂程序,因为 scala 有的语法对于之前使用习...
    MrRobot阅读 2,890评论 0 10
  • 除了在 Predef 对象中自动加载的那些隐式对象外,其他在源码中出现的隐式对象均不是本地对象。[P112] 隐式...
    云之外阅读 629评论 0 0
  • 戏路如流水,从始至终,点滴不漏。一路百折千回,本性未变,终归大海。一步一戏,一转身一变脸,扑朔迷离。真心自然流露,...
    刘光聪阅读 2,012评论 3 13
  • Adora Cheung, Homejoy的创始人你该成为你所在行业的行家找到真正的痛点,一句话描述它,并让自己的...
    LeaChau阅读 351评论 0 2