Scala中的Implicit详解

Scala中的implicit关键字对于咱们初学者像是一个谜同样的存在,一边惊讶于代码的简洁,html

一边像在迷宫里打转同样地去找隐式的代码,所以咱们团队结合目前的开发工做,将implicit
做为一个专题进行研究,了一些心得。
前端

在研究的过程中,咱们注重三方面:java

  1. 为何须要implicit?
  2. implicit 包含什么,有什么内在规则?
  3. implicit 的应用模式有哪些?

为何须要Implicit?

Scala在面对编译出现类型错误时,提供了一个由编译器自我修复的机制,编译器试图去寻找
一个隐式implicit的转换方法,转换出正确的类型,完成编译。这就是implicit的意义。git

咱们正在作Guardian系统的升级,Guardian是公司内部的核心系统,提供统一权限管控、
操做审计、单点登陆等服务。系统已经有4年多的历史了,已经难以知足目前的须要,好比:
当时仅提供了RESTFul的服务接口,而随着性能需求的提升,有些服务使用Tcp消息完成远程
调用;另外,在RESTFull接口的协议方面,咱们也想作一些优化。github

而现状是公司内部系统已经所有接入Guardian,要接入新版,不可能一次所有迁移,甚至
要花很长一段时间才能完成迁移工做,所以新版接口必须同时支持新老两个版本接口协议。编程

所以咱们必须解决两个问题:并发

  1. 兼容老版本协议, 以便可以平滑升级
  2. 支持多种协议,以知足不一样业务系统的需求

咱们但愿对接口层提供一个稳定的Service接口,以避免业务的变更影响前端接口代码,常规
的作法是咱们在Service接口上定义多种版本的方法(重载),好比鉴权服务:app

trait AuthService { // 兼容老版本的鉴权业务方法 def auth(p: V1HttpAuthParam): Future[V1HttpAuthResult] // 新版本的鉴权业务方法 def auth(p: V2HttpAuthParam): Future[V2HttpAuthResult] // 新版本中支持的对Tcp消息鉴权的业务方法 def auth(p: V2TcpMsg): Future[V2TcpMsg] } 

这种作法的问题在于一旦业务发生变化,出现了新的参数,势必要修改AuthService接口,
添加新的接口方法,接口不稳定。框架

假若有一个通用的auth方法就行了:ide

trait AuthParam {} trait StableAuthService{ // 稳定的鉴权接口 def auth(p: AuthParam) } 

这样,咱们就能够按照下面的方式调用:

//在老版本的REST WS接口层: val result = authService auth V1HttpAuthParam response(result) //在新版本的REST WS接口层: val result = authService auth V2HttpAuthParam response(result) // .... 在更多的服务接口层,任意的传入参数,得到结果 

很明显,这样的代码编译出错。 由于在authService中没有这样的方法签名。

再举个简单的例子, 咱们想在打印字符串时,添加一些分隔符,下面是最天然的调用方式:

"hello,world" printWithSeperator "*" 

很明显,这样的代码编译出错。 由于String 没有这样的方法。

Scala在面对编译出现类型错误时,提供了一个由编译器自我修复的机制,编译器试图去寻找
一个隐式implicit的转换方法,转换出正确的类型,完成编译。这就是implicit 的意义。

Implicit包含什么,有什么内在规则?

Scala 中的implicit包含两个方面:

  1. 隐式参数(implicit parameters)
  2. 隐式转换(implicit conversion)

隐式参数(implicit parameters)

隐式参数一样是编译器在找不到函数须要某种类型的参数时的一种修复机制,咱们能够采用显式的柯里化式
的隐式参数申明,也能够进一步省略,采用implicitly方法来获取所须要的隐式变量。

隐式参数相对比较简单,Scala中的函数申明提供了隐式参数的语法,在函数的最后的柯里化参数
列表中能够添加隐式implicit关键字进行标记, 标记为implicit的参数在调用中能够省略,
Scala编译器会从当前做用域中寻找一个相同类型的隐式变量,做为调用参数。

在Scala的并发库中就大量使用了隐式参数,好比Future:

// Future 须要一个隐式的ExecutionContext // 引入一个默认的隐式ExecutionContext, 不然编译不经过 import scala.concurrent.ExecutionContext.Implicits.default Future { sleep(1000) println("I'm in future") } 

对于一些常量类的,可共用的一些对象,咱们能够用隐式参数来简化咱们的代码,好比,咱们的应用
通常都须要一个配置对象:

object SomeApp extends App { //这是咱们的全局配置类 class Setting(config: Config) { def host: String = config.getString("app.host") } // 申明一个隐式的配置对象 implicit val setting = new Setting(ConfigFactory.load) // 申明隐式参数 def startServer()(implicit setting: Setting): Unit = { val host = setting.host println(s"server listening on $host") } // 无需传入隐式参数 startServer() } 

甚至,Scala为了更进一步减小隐式参数的申明代码,咱们均可以不须要再函数参数上显示的申明,在scala.Predef包中,提供了一个implicitly的函数,帮助咱们找到当前上下文中所须要类型的
隐式变量:

@inline def implicitly[T](implicit e: T) = e // for summoning implicit values from the nether world 

所以上面的startServer函数咱们能够简化为:

// 省略隐式参数申明 def startServer(): Unit = { val host = implicitly[Setting].host println(s"server listening on $host") } 

须要注意的是,进一步简化以后,代码的可读性有所损失,调用方并不知道startServer须要一个隐式的
配置对象,要么增强文档说明,要么选用显式的申明,这种权衡须要团队达成一致。

隐式转换(implicit conversion)

回顾一下前面说到的小例子,让字符串可以带分隔符打印:

"hello,world" printWithSeperator "*" 

此时,Scala编译器尝试从当前的表达式做用域范围中寻找可以将String转换成一个具备printWithSeperator
函数的对象。

为此,咱们提供一个PrintOpstrait,有一个printWithSeperator函数:

trait PrintOps { val value: String def printWithSepeator(sep: String): Unit = { println(value.split("").mkString(sep)) } } 

此时,编译仍然不经过,由于Scala编译器并无找到一个能够将String转换为PrintOps的方法!那咱们申明一个:

def stringToPrintOps(str: String): PrintOps = new PrintOps { override val value: String = str } 

OK, 咱们能够显示地调用stringToPrintOps了:

stringToPrintOps("hello,world") printWithSepeator "*" 

离咱们的最终目标只有一步之遥了,只须要将stringToPrintOps方法标记为implicit便可,除了为String
添加stringToPrintOps的能力,还能够为其余类型添加,完整代码以下:

object StringOpsTest extends App { // 定义打印操做Trait trait PrintOps { val value: String def printWithSeperator(sep: String): Unit = { println(value.split("").mkString(sep)) } } // 定义针对String的隐式转换方法 implicit def stringToPrintOps(str: String): PrintOps = new PrintOps { override val value: String = str } // 定义针对Int的隐式转换方法 implicit def intToPrintOps(i: Int): PrintOps = new PrintOps { override val value: String = i.toString } // String 和 Int 都拥有 printWithSeperator 函数 "hello,world" printWithSeperator "*" 1234 printWithSeperator "*" } 

隐式转换的规则 -- 如何寻找隐式转换方法

Scala编译器是按照怎样的套路来寻找一个能够应用的隐式转换方法呢? 在Martin Odersky的Programming in Scala, First Edition中总结了如下几条原则:

  1. 标记规则:只会去寻找带有implicit标记的方法,这点很好理解,在上面的代码也有演示,若是不申明为implicit
    只能手工去调用。
  2. 做用域范围规则:
    1. 只会在当前表达式的做用范围以内查找,并且只会查找单一标识符的函数,上述代码中,
      若是stringToPrintOps方法封装在其余对象(加入叫Test)中,虽然Test对象也在做用域范围以内,但编译器不会尝试使用Test.stringToPrintOps进行转换,这就是单一标识符的概念。
    2. 单一标识符有一个例外,若是stringToPrintOps方法在PrintOps的伴生对象中申明也是有效的,Scala
      编译器也会在源类型或目标类型的伴生对象内查找隐式转换方法,本规则只会在转型有效。而通常的惯例,会将隐式转换方法封装在伴生对象中
    3. 当前做用域上下文的隐式转换方法优先级高于伴生对象内的隐式方法
  3. 不能有歧义原则:在相同优先级的位置只能有一个隐式的转型方法,不然Scala编译器没法选择适当的进行转型,编译出错。
  4. 只应用转型方法一次原则:Scala编译器不会进行屡次隐式方法的调用,好比须要C类型参数,而实际类型为A,做用域内
    存在A => B,B => C的隐式方法,Scala编译器不会尝试先调用A => B ,再调用B => C
  5. 显示方法优先原则:若是方法被重载,能够接受多种类型,而做用域中存在转型为另外一个可接受的参数类型的隐式方法,则不会
    被调用,Scala编译器优先选择无需转型的显式方法,例如:
    def m(a: A): Unit = ??? def m(b: B): Unit = ??? val b: B = new B //存在一个隐式的转换方法 B => A implicit def b2a(b: B): A = ??? m(b) //隐式方法不会被调用,优先使用显式的 m(b: B): Unit 

Implicit的应用模式有哪些?

隐式转换的核心在于将错误的类型经过查找隐式方法,转换为正确的类型。基于Scala编译器的这种隐式转换机制,一般有两种应用
模式:Magnet PatternMethod Injection

Magnet Pattern

Magnet Pattern模式暂且翻译为磁铁模式, 解决的是方法参数类型的不匹配问题,可以优雅地解决本文开头所提出的问题,
用一个通用的Service方法签名来屏蔽不一样版本、不一样类型服务的差别。

磁铁模式的核心在于,将函数的调用参数和返回结果封装为一个磁铁参数,这样方法的签名就统一为一个了,不须要函数重载;再
定义不一样参数到磁铁参数的隐式转换函数,利用Scala的隐式转换机制,达到相似于函数重载的效果。

磁铁模式普遍运用于Spray Http 框架,该框架已经迁移到Akka Http中。

下面,咱们一步步来实现一个磁铁模式,来解决本文开头提出的问题。

  1. 定义Magnet参数和使用Magnet参数的通用鉴权服务方法

    // Auth Magnet参数 trait AuthMagnet { type Result def apply(): Result } // Auth Service 方法 trait AuthService { def auth(am: AuthMagnet): am.Result = am() } 
  2. 实现不一样版本的AuthService

    //v1 auth service trait V1AuthService extends AuthService //v2 auth service trait V2AuthService extends AuthService 
  3. 实现不一样版本AuthService的伴生对象,添加适当的隐式转换方法

    //V1 版本的服务实现 object V1AuthService { case class V1AuthRequest() case class V1AuthResponse() implicit def toAuthMagnet(p: V1AuthRequest): AuthMagnet {type Result = V1AuthResponse} = new AuthMagnet { override def apply(): Result = { // v1 版本的auth 业务委托到magnet的apply中实现 println("这是V1 Auth Service") V1AuthResponse() } override type Result = V1AuthResponse } } //V2 版本的服务实现 object V2AuthService { case class V2AuthRequest() case class V2AuthResponse() implicit def toAuthMagnet(p: V2AuthRequest): AuthMagnet {type Result = V2AuthResponse} = new AuthMagnet { override def apply(): Result = { // v2 版本的auth 业务委托到magnet的apply中实现 println("这是V2 Auth Service") V2AuthResponse() } override type Result = V2AuthResponse } } 
  4. 编写两个版本的资源接口(demo)

    trait V1Resource extends V1AuthService { def serv(): Unit = { val p = V1AuthRequest() val response = auth(p) println(s"v1 resource response: $response") } } trait V2Resource extends V2AuthService { def serv(): Unit = { val p = V2AuthRequest() val response = auth(p) println(s"v2 resource response: $response") } } val res1 = new V1Resource {} val res2 = new V2Resource {} res1.serv() res2.serv() 

    控制台输出结果为:

    这是V1 Auth Service
    v1 resource response: V1AuthResponse()
    这是V2 Auth Service
    v2 resource response: V2AuthResponse()

Method Injection

Method Injection 暂且翻译为方法注入,意思是给一个类型添加没有定义的方法,实际上也是经过隐式转换来实现的,
这种技术在Scalaz中普遍使用,Scalaz为咱们提供了和Haskell相似的函数式编程库。

本文中的关于printWithSeperator方法的例子其实就是Method Injection的应用,从表面上看,便是给String
Int类型添加了printWithSeperator方法。

Magnet Pattern不一样的是转型所针对的对象,Magnet Pattern是针对方法参数进行转型,
Method Injection是针对调用对象进行转型。

举个简单的例子,Scala中的集合都是一个Functor,均可以进行map操做,可是Java的集合框架却没有,
若是须要对java.util.ArrayList等进行map操做则须要先转换为Scala对应的类型,很是麻烦,借助Method Injection,咱们能够提供这样的辅助工具,让Java的集合框架也成为一种Functor,具有map能力:

  1. 首先定义一个Functor
    trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A ⇒ B): F[B] } 
  2. 再定义一个FunctorOps
    final class FunctorOps[F[_], A](l: F[A])(implicit functor: Functor[F]) { def map[A, B](f: A ⇒ B): F[B] = functor.map(l)(f) } 
  3. 在FunctorOps的伴生对象中定义针对java.util.List[E]的隐式Funcotr实例和针对java.util.List[E]到
    FunctorOps的隐式转换方法
    object FunctorOps { // 针对List[E]的functor implicit val jlistFunctor: Functor[JList] = new Functor[JList] { override def map[A, B](fa: JList[A])(f: (A) => B): JList[B] = { val fb = new JLinkList[B]() val it = fa.iterator() while(it.hasNext) fb.add(f(it.next)) fb } } // 将List[E]转换为FunctorOps的隐式转换方法 implicit def jlistToFunctorOps[E](jl: JList[E]): FunctorOps[JList, E] = new FunctorOps[JList, E](jl) } 
  4. 愉快滴使用map啦
    val jlist = new util.ArrayList[Int]() jlist.add(1) jlist.add(2) jlist.add(3) jlist.add(4) import FunctorOps._ val jlist2 = jlist map (_ * 3) println(jlist2) // [3, 6, 9, 12] 

总结

Implicit 是Scala语言中处理编译类型错误的一种修复机制,利用该机制,咱们能够编写出任意参数和返回值的多态方法(这种多
态也被称为Ad-hoc polymorphism -- 任意多态),实现任意多态,咱们一般使用Magnet Pattern磁铁模式;同时还能够
给其余类库的类型添加方法来对其余类库进行扩展,一般将这种技术称之为Method Injection

参考资料

  1. 《Programming in Scala》中关于隐式转换和隐式参数章节: http://www.artima.com/pins1ed/implicit-conversions-and-parameters.html
  2. 《The Magnet Pattern》http://spray.io/blog/2012-12-13-the-magnet-pattern/
相关文章
相关标签/搜索