Scala的Higher-Kinded类型

简介: Scala的Higher-Kinded类型

Scala的Higher-Kinded类型


Higher-Kinded从字面意思上看是更高级的分类,也就是更高一级的抽象。我们先看个例子。


如果我们要在scala中实现一个对Seq[Int]的sum方法,应该怎么做呢?


def sum(seq: Seq[Int]): Int = seq reduce (_ + _)
sum(Vector(1,2,3,4,5)) // 结果值: 15


看起来很简单,刚刚我们实现了Seq[Int]的sum操作,那么如果我们想更进一步,我们想同时实现Seq[Int]和Seq[(Int,Int)]的操作该怎么处理呢?


不同的Seq需要不同的add实现,我们先抽象一个trait:


trait Add[T] { 
def add(t1: T, T2: T): T
}


接下来我们在Add的伴生类中定义两个隐式实例,一个Add[Int], 一个Add[(Int,Int)]。


object Add { 
implicit val addInt = new Add[Int] {
def add(i1: Int, i2: Int): Int = i1 + i2
}
implicit val addIntIntPair = new Add[(Int,Int)] {
def add(p1: (Int,Int), p2: (Int,Int)): (Int,Int) =
(p1._1 + p2._1, p1._2 + p2._2)
}
}


这两个隐式实例分别为Add[Int], 一个Add[(Int,Int)]实现了add方法。


最后我们可以定义sumseq方法了:


def sumSeq[T : Add](seq: Seq[T]): T = 
seq reduce (implicitly[Add[T]].add(_,_))


T : Add 被称为 上下文定界( context bound), 它暗指隐式参数列表将接受Add[T] 实例。


我们看下怎么调用:


sumSeq(Vector(1 -> 10, 2 -> 20, 3 -> 30)) // 结果值: (6,60)
sumSeq(1 to 10) // 结果值: 55
sumSeq(Option(2)) // 出错!


sumSeq可以接受Seq[Int]和Seq[(Int,Int)]类型,但是无法接收Option。


对于任何一种序列,只要我们为它定义了隐式的Add 实例,那么sumSeq 方法便能计算出该序列的总和。


不过,sumSeq 仍然只支持Seq 子类型。假如容器类型并不是Seq 子类型,但是却实现了reduce 方法,我们该如何对该容器进行处理呢?我们会使用更加泛化的求和操作。这时候就需要使用到higher-kinded 类型了。


我们用M替代Seq,则可以得到M[T],M[T]就是本文介绍的Higher-Kinded类型。


trait Reduce[T, -M[T]] { 
def reduce(m: M[T])(f: (T, T) => T): T
}
object Reduce { 
implicit def seqReduce[T] = new Reduce[T, Seq] {
def reduce(seq: Seq[T])(f: (T, T) => T): T = seq reduce f
}
implicit def optionReduce[T] = new Reduce[T, Option] {
def reduce(opt: Option[T])(f: (T, T) => T): T = opt reduce f
}
}


为了能对Seq 和Option 值执行reduce操作,我们分别为这两类类型提供了隐式实例。为了简化起见,我们将直接使用类型中已经提供的reduce 方法执行reduce操作。


注意这里-M[T]是逆变类型,还记得我们之前的结论吗?函数的参数一定是逆变类型的。 因为M[T]是reduce(m: M[T])的参数,所以我们需要定义它为逆变类型-M[T]。


我们看一下sum方法该怎么定义:


def sum[T : Add, M[T]](container: M[T])( 
implicit red: Reduce[T,M]): T =
red.reduce(container)(implicitly[Add[T]].add(_,_))


调用结果如下:


sum(Vector(1 -> 10, 2 -> 20, 3 -> 30)) // 结果值: (6,60)
sum(1 to 10) // 结果值: 55
sum(Option(2)) // 结果值: 2
sum[Int,Option](None) // 错误!


最后一个调用,我们为sum 方法添加的类型签名[Int, Opton] 会要求编译器将None 解释成Option[Int] 类型。假如不添加该类型签名,我们将得到编译错误:无法判断Option[T] 类型中的类型参数T 到底应该对应addInt 方法还是addIntIntPair 方法。


通过显式地指定类型,我们能够得到真正希望捕获的运行错误:我们不能对None 值调用reduce 方法。


在上面的sum方法中,sum[T : Add, M[T]], T: Add是上下文边界,我们也想定义M[T] 的上下文边界,比如M[T] : Reduce。


因为上下文边界只适用于包含单参数的场景,而Reduce 特征包


含两个类型参数,所以我们需要对Reduce进行改造:


trait Reduce1[-M[_]] { 
def reduce[T](m: M[T])(f: (T, T) => T): T
}
object Reduce1 { 
implicit val seqReduce = new Reduce1[Seq] {
def reduce[T](seq: Seq[T])(f: (T, T) => T): T = seq reduce f
}
implicit val optionReduce = new Reduce1[Option] {
def reduce[T](opt: Option[T])(f: (T, T) => T): T = opt reduce f
}
}


在新的reduce1中,只包含一个类型参数且属于higher-kinded 类型。


M[_]是上篇文章我们讲到的存在类型。T 参数被移至reduce 方法。


修改后的sum方法如下:


def sum[T : Add, M[_] : Reduce1](container: M[T]): T =
implicitly[Reduce1[M]].reduce(container)(implicitly[Add[T]].add(_,_))


我们定义了两个上下文边界,它们分别作用于Reduce1 和Add。而使用implicity 修饰的类型参数则能够区分出这两种不同的隐式值。


M[_]就是我们经常会看到的higher-kinded, higher-kinded虽然带给我们额外的抽象,但是使代码变得更加复杂。大家可以酌情考虑是否需要使用。


相关文章
|
7月前
|
IDE Java 编译器
scala的两种变量类型 var 和 val
scala的两种变量类型 var 和 val
158 2
scala的两种变量类型 var 和 val
|
安全 Java 大数据
大数据开发基础的编程语言的Scala的类型系统
Scala是一种强类型的编程语言,它具有一套完善的类型系统。本文将介绍Scala的类型系统,帮助开发者了解这门语言的类型安全性和灵活性。
100 0
|
大数据 Java 编译器
Scala 字符类型|学习笔记
快速学习 Scala 字符类型。
181 0
|
Java Scala
Scala的存在类型
Scala的存在类型
|
编译器 Scala
Scala教程之:静态类型(三)
Scala教程之:静态类型(三)
|
存储 Java Scala
Scala教程之:静态类型(二)
Scala教程之:静态类型(二)
|
安全 Java Scala
Scala教程之:静态类型 (一)
Scala教程之:静态类型 (一)
|
安全 Java Scala
Scala入门到精通——第二十一节 类型参数(三)-协变与逆变
作者:摇摆少年梦 视频地址:http://www.xuetuwuyou.com/course/12 本节主要内容 协变 逆变 类型通匹符 1. 协变 协变定义形式如:trait List[+T] {} 。当类型S是类型A的子类型时,则List[S]也可以认为是List[A}的子类型,即List[S]可以泛化为List[A]。也就是被参数化类型的泛化方向与参数类
2667 0
|
Scala
Scala入门到精通——第十七节 类型参数(一)
本节主要内容 类型变量界定(Type Variable Bound) 视图界定(View Bound) 上界(Upper Bound)与下界(Lower Bound) 1. 类型变量界定(Type Variable Bound) 类型变量界定是指在泛型的基础上,对泛型的范围进行进一步的界定,从而缩下泛型的具体范围,例如: //下面的类编译通不过 //因为泛型T
3417 0