scala学习笔记 - 隐式参数

25 篇文章 0 订阅
15 篇文章 2 订阅

隐式参数

函数或方法可以带有一个标记为implicit的参数列表。在这种情况下,编译器将会查找默认值,提供给本次函数调用,以下是一个简单的示例:

case class Delimiters(left: String, right: String) 
def quote(what: String)(implicit delimiters: Delimiters): String = 
  delimiters.left + what + delimiters.right

可以用一个显式的Delimiters对象来调用quote方法,就像这样:

val str = quote("Bonjour le monde")(Delimiters("<<", ">>")) 
println(str) // 输出:<<Bonjour le monde>>

注意这里有两个参数列表,这个函数是“柯里化的”。你也可以略去隐式参数列表:

quote("Bonjour le monde")

在这种情况下,编译器将会查找一个类型为Delimiters的隐式值,这必须是一个被声明为implicit的值。编译器将会在如下两个地方查找一个这样的对象:

  • 在当前作用域所有可以用单个标识符指代的满足类型要求的val和def。
  • 与所要求类型相关联的类型的伴生对象,相关联的类型包括所要求类型本身,以及它的类型参数(如果它是一个参数化的类型的话)。
    在我们的示例当中,可以做一个对象,比如:
object FunctionTest {
  implicit val quoteDelimits: Delimiters = Delimiters("<<", ">>")
  def main(args: Array[String]): Unit = {
    val str = quote("Bonjour le monde")
    println(str) // 输出:<<Bonjour le monde>>
  }
}

这样我们就可以从这个对象引人所有的值:

import operator._
// 或特定的值:
import operator.quoteDelimiters

如此一来,法语标点符号中的定界符(<<和>>)就被隐式地提供给了quote函数。
说明:对于给定的数据类型,只能有一个隐式的值。因此,使用常用类型的隐式参数并不是一个好主意。例如:

def quote(what: String)(implicit left: String, right: String// 别这样做
// 上述代码行不通, 因为调用者无法提供两个不同的字符串。

利用隐式参数进行隐式转换

隐式的函数参数也可以被用作隐式转换,为了明白它为什么重要,首先考虑如下这个泛型函数:

def smaller[T] (a: T, b: T) = if (a < b) a else b // 不太对劲

这实际上行不通,编译器不会接受这个函数,因为它并不知道a和b属于一个带有<操作符的类型,我们可以提供一个转换函数来达到目的:

def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if(order(a) < b) a else b

由于Ordered[T]特质有一个接受T作为参数的<操作符,因此这个版本是正确的。也许是巧合,这种情况十分常见,Predef对象对大量已知类型都定义了T => Ordered[T],包括所有已经实现了Order[T]或Comparable[T]的类型,正因为如此,才可以调用。

smaller(40 , 2)
// 以及
smaller("Hello", "World")
// 如果你想要调用
smaller(Fraction(1, 7), Fraction(2, 9))

就需要定义一个Fraction => Ordered[Fraction]的函数,要么在调用的时候显式写出,要么把它做成一个implicit val。
再次检查:

def smaller[T] (a: T, b: T) (implicit order: T => Ordered[T])

注意order是一个被打上了implicit标签的函数,并且在作用域内。因此,它不仅是一个隐式参数,它还是一个隐式转换,正因为这样,我们才可以在函数体中略去对order的显式调用:

def smaller[T](a: T, b: T) (implicit order: T => Ordered[T]) = if(a < b) a else b 
// 将调用 order(a) < b,如果a没有带<操作符的话

上下文界定

类型参数可以有一个形式为T : M的上下文界定(context bound),其中M是另一个泛型类型。 它要求作用域中存在一个类型为M[T]的隐式值。例如:class Pair[T: Ordering]
要求存在一个类型为Ordering[T]的隐式值,该隐式值可以被用在该类的方法当中,考虑如下示例:

class Pair[T: Ordering](val first: T, val second: T) { 
  def smaller(implicit ord: Ordering[T]) = if(ord.compare(first, second) < 0) first else second
}

如果new一个Pair(40, 2),编译器将推断出我们需要一个Pair[Int],由于Ordering伴生对象中有一个类型为Ordering[Int]的隐式值,因此Int满足上下文界定。这个Ordering[Int]就成为该类的一个字段,其被传入需要该值的方法当中。
如果你愿意,也可以用Predef类的implicitly方法获取该值:

class Pair[T: Ordering] (val first: T, val second: T) { 
  def smaller = if(implicitly[Ordering[T]].compare(first, second) < 0) first else second
}

implicitly函数在Predef.scala中定义如下:

def implicitly[T](implicit e: T) = e // 用于从冥界召唤隐式值
// 说明:上述注释的表述很贴切, 隐式值生活在"冥界", 并以一种不可见的方式被加入到方法中。

或者,你也可以利用Ordered特质中定义的从Ordering到Ordered的隐式转换。一旦引入了这个转换,你就可以使用关系操作符:

class Pair[T: Ordering](val first: T, val second: T){
  def smaller = {
    import Ordered._
    if(first < second) first else second
  }
}

这些只是细微的变化;重要的是你可以随时实例化Pair[T],只要满足存在类型为Ordering[T]的隐式值的条件即可。举例来说,如果你想要Pair[Point],则可以组织一个隐式的Ordering[Point]值:

implicit object PointOrdering extends Ordering[Point] { 
  def compare(a: Point , b: Point) = ...
}

类型类

上面的Ordering特质,我们有一个要求参数带有排序规则的算法,通常,在面向对象编程中,我们会要求参数类型要扩展自某个特质,不过这里并没有这个要求,为了让某个类可以用于这个算法,我们完全无须修改相应的类,我们只要提供一个隐式转换即可,跟面向对象的方案相比,这种做法要灵活得多。
像Ordering这样的特质被称为“类型类(type class)”,类型类定义了某种行为,任何类型都可以通过提供相应的行为来加入这个类。
要搞清楚某个类型是如何加人类型类的,看一个简单的例子,计算平均值,(x1 +…+ xn)/n,为此,我们需要能将两个值相加然后除以一个整数,Scala类库中有一个类型类叫作Numeric,它要求相应的值可以相加、相乘、相比较,不过它并没有要求相应的值可以被整数除,既然这样,那我们就自己来定义:

trait NumberLike[T] { 
  def plus(x: T, y: T): T 
  def divideBy(x: T, n: Int): T
}

接下来 ,为了确保类型类出厂以后是有用的,我们添加一些常用的类型作为它的
成员,通过在伴生对象中提供隐式对象,这并不难:

object NumberLike { 
  implicit object NumberLikeDouble extends NumberLike[Double] { 
    def plus(x: Double, y : Double) = x + y 
    def divideBy(x: Double , n: Int) = x / n
  }
  implicit object NumberLikeBigDecimal extends NumberLike[BigDecimal] { 
    def plus(x: BigDecimal , y : BigDecimal) = x + y 
    def divideBy(x: BigDecimal, n: Int) = x / n
  }
}

接下来,我们就可以开始用这个类型类了。在average方法中,我们需要该类型类的一个实例,这样我们就可以调用plus和divideBy。(注意:这些是类型类的方法,而不是成员类型的方法)
有两种方式可以提供类型类的实例:作为隐式参数,或使用上下文界定。第一种方式如下:

def average[T](x: T, y: T)(implicit ev: NumberLike[T]) = ev.divideBy(ev.plus(x, y), 2 )

而使用上下文界定时,我们是从“冥界”中获取相应的隐式对象:

def average[T: NumberLike] (x: T, y: T) = { 
  val ev = implicitly[NumberLike[T]] 
  ev.divideBy(ev.plus(x, y), 2)
}

如果某个类型要加入NumberLike类型类需要做些什么,首先,它必须提供一个隐式的对象,就像我们出厂时提供的NumberLikeDouble和NumberLikeBigDecimal对象那样。以下是将Point类型加入NumberLike类型类所需要做的:

class Point(val x: Double , val y: Double) {
  ...
}
object Point { 
  def apply(x: Double, y: Double) = new Point(x, y) 
  implicit object NumberLikePoint extends NumberLike[Point] { 
    def plus(p: Point, q : Point) = Point(p.x + q.x, p.y + q.y) 
    def divideBy(p: Point, n: Int) = Point(p.x * 1.0 / n, p.y * 1.0 / n)
  }
}

这里我们将隐式对象添加到了Point的伴生对象中,如果你不能修改Point类,可以将这个隐式对象放在别的地方,然后按需引人就好。
Scala标准类库提供了很多有用的类型类,比如Equiv、Ordering、Numeric、Fractional、Hashing、IsTraverableOnce、IsTraverableLike等。正如你看到的那样,提供自定义的类型类也是很容易的。
关于类型类,最为重要的一点是它们提供了一种“特设(ad hoc)”的多态机制,这跟继承比起来,更为宽松。

类型证明

下面这样的类型约束:

T =:= U 
T <:< U 
T => U

这些约束将校验T是否等于U,是否是U的子类型,或者是否可以被转换为U,要使用这样的类型约束,做法是提供一个隐式参数,比如:

def firstLast[A, C](it: C)(implicit ev: C <:< Iterable[A]) = (it.head, it.last) 

=:=和<:<带有隐式值的类,其定义在Predef对象当中。例如,<:<从本质上讲就是:

abstract class <:<[-From, +To] extends Function1[From, To] 
object <:< { 
  implicit def conforms[A] = new (A <:< A) { def apply(x: A) = x } 
}

假定编辑器需要处理约束implicit ev: String <:< AnyRef。它会在伴生对象中查找类型 String <:< AnyRef 的隐式对象。注意<:<相对于From是逆变的,而相对于To是协变的,因此如下对象:

<:<.conforms[String]

可以被当作String <:< AnyRef的实例使用,(<:<.conforms[AnyRef]对象也是可以用的,但它相对而言更笼统,因而不会被考虑)。
我们ev称作“类型证明对象(evidence object)”,它的存在证明了如下事实:以本例来说,String是AnyRef的子类型。
这里的类型证明对象是恒等函数(即永远返回参数原值的函数)。要弄明白为什么这个恒等函数是必需的,请仔细看如下代码:

def firstLast[A, C](it: C) (implicit ev: C <:< Iterable[A]) = (it.head, it.last) 

编译器实际上并不知道C是一个Iterable[A],<:<并不是语言特性,而只是一个类。因此,像it.head和it.last这样的调用并不合法,但ev是一个带有单个参数的函数,因此也是也个从C到Iterable[A]的隐式转换 ,编译器将会应用这个隐式转换,计算ev(it).head和ev(it).last。
提示:为了检查一个泛型的隐式对象是否存在,你可以在REPL中调用implicitly函数。举例来说,REPL 中键入implicitly[String <:< AnyRef],你将会得到一个结果(碰巧是一个函数)。但implicitly[AnyRef <:< String]会失败,并给出错误提示。

@implicitNotFound注解

@implicitNotFound注解告诉编译器在不能构造出带有该注解的类型的参数时给出错误提示,这样做的目的是给程序员有意义的错误提。举例来说,类被注解为:

@implicitNotFound (msg = "Cannot prove that ${From} <:< ${To}. ") 
abstract class <:<[-From, +To] extends Function1[From, To]
// 例如,如果你调用
firstLast[String, List[Int]] (List(1, 2, 3))
// 则错误提示为
Cannot prove that List[Int] <:< Iterable[String]

这比起如下默认的错误提示更有可能给程序员提供有价值的信息:

could not find implicit value for parameter ev: <:<[List[Int], Iterable[String]]

注意错误提示中的${From}和${To}将被替换成被注解类的类型参数From和To。

CanBuildFrom解读

以map方法为例,稍微简化一些来说,map是一个Iterable[A, Repr]的方法,实现如下:

def map[B, That] (f: (A) => B) (implicit bf: CanBuildFrom[Repr, B, That]): That= { 
  val builder= bf() 
  val iter = iterator () 
  while(iter.hasNext) builder += f(iter.next())
  builder.result 
}

这里Repr 的意思是“展现类型(representation type)”。该参数将让我们可以选择合适的构建器工厂来构建诸如Range或String这样的非常规集合。
说明:在Scala类库中,map实际上被定义在TraversableLike [A , Repr]特质中。 这样,更常用的 Iterable特质就无须背着Repr这个类型参数的包袱。
CanBuildFrom[From, E, To]特质将提供类型证明:可以创建一个类型为To的集合,握有类型为E的值,并且和类型From兼容。在讨论这些类型证明对象是如何生成的之前,让我们先来看看它们是做什么用的。
CanBuildFrom特质带有一个apply方法,其交出类型为Builder[E, To]的对象。Builder类型带有一个+=方法用来将元素添加到一个内部的缓冲,还有一个result法用来产出所要求的集合。

trait Builder[-E, +To] { 
  def +=(e: E): Unit 
  def result(): To
}
trait CanBuildFrom[-From, -E, +To] { 
  def apply(): Builder[E, To]
}

因此,map方法只是构造出一个目标类型的构建器,为构建器填充函数f的值,然后产出结果的集合。
每个集合都在其伴生对象中提供了一个隐式的CanBuildFrom对象。考虑如下简版的ArrayBuffer类:

class Buffer[E: ClassTag] extends Iterable[E, Buffer[E]] with Builder[E, Buffer[E]] { 
  private var elems = new Array[E](10) 
  ...
  def iterator() = ...
    private var i = 0 
    def hasNext = i < length 
    def next() = { i += l ; elems(i - 1) }
  }
  def +=(e: E) { ... } 
  def result() = this
}
object Buffer { 
  implicit def canBuildFrom[E: ClassTag] = new CanBuildFrom[Buffer[_], E, Buffer[E]] { 
    def apply() = new Buffer[E]
  }
}

我们来看看如果调用buffer.map(f)会发生什么,其中f是一个类型为A=>B的函数。首先,通过调用Buffer伴生对象中的canBuildFrom[B]方法,我们可以得到隐式的bf参数。它的apply方法返回了构建器,拿本例来说就是Buffer[E]。
由于Buffer类碰巧已经有一个+=方法,而它的result方法也被定义为返回它自己。因此,Buffer就是它自己的构建器。
然而,Range类的构建器并不返回一个Range,而且它显然也不能返回Range。举例来说,(1 to 10).map(x => x * x )的结果并不是一个Range,在实际的Scala类库中,Range扩展自IndexedSeq[Int],而 IndexedSeq的伴生对象定义了一个构建Vector的构建器。
以下是一个简化版的Range类,提供了一个Buffer作为其构建器:

class Range(val low: Int, val high: Int) extends Iterable[Int, Range] { 
  def iterator() = ...
}
object Range { 
  implicit def canBuildFrom[E: ClassTag] = new CanBuildFrom[Range, E, Buffer[E]] { 
    def apply() = new Buffer[E]
  }
}

现在再来考虑如下调用:Rang(1, 10).map(f)。这个方法需要一个implicit bf: CanBuildFrom[Repr, B, That]。由于Repr就是Range,因此相关联的类型有CanBuildFrom、Range、B和未知的That,其中, Range对象可以通过调用其canBuildFrom[B]方法交出一个匹配项,该方法返回一个CanBuildFrom [Range, B, Buffer [B]]。这个匹配项就成为bf ;其apply方法将交出Buffer[B],用于构建结果。
正如你刚才看到的那样,隐式参数CanBuildFrom[Repr, B, That]将会定位到一个可以产出目标集合的构建器的工厂对象,这个构建器工厂是定义在Repr伴生对象中的一个隐式值。
参考:快学scala(第二版)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值