scala的implicit和magnet模式

对初学scala的人,implicit像一个黑魔法,来无影去无踪,像它的名字一样非常“含蓄”。

从某种意义上讲,implicit是一个类型系统的游戏。scala是强类型系统,所有的参数都需要符合类型预期,如果需要一个Int类型,你传来一个String,编译器会报类型不符错误。implicit的引入,使在报错之前还有一次机会,即:如果编译器在当前作用域内找到一个从String转换到Int的implicit定义函数,编译器会用这个implicit把你传给它的String转换成它需要的Int,于是一切又愉快的发生下去了。

当然这只是implicit的一种使用场景,spray.io(已合并到akka-http)的magnet模式利用的就是这个特性。

magnet模式

magnet模式简单讲就是通过定义一个magnet类型作为统一的参数,然后针对需要重载的参数列表,类型等,在magnet类型的companion object中实现相应的转换为magnet类型的implitcit函数。

如:可以定义一个Magnet类型实现一个接受任意参数的add函数

def add(magnet: MyMagnet): magnet.Result = magnet()

sealed trait MyMagnet {
  type Result
  def apply(): Result
}

object MyMagnet {
  //一个整形参数到MyMagenet的转换
  implicit def fromInt(i: Int) =
    new MyMagnet {
      type Result = Int
      def apply(): Result = i + 1
    }
  //一个String参数到MyMagenet的转换
  implicit def fromString(s: String) =
    new MyMagnet {
      type Result = String
      def apply(): Result = "hello " + s
    }
  //一个String参数加一个整形参数到MyMagenet的转换
  implicit def fromStringAndInt(tuple: (String, Int)) =
    new MyMagnet {
      type Result = String
      def apply(): Result = tuple._1 + tuple._2.toString
    }
}

调用的时候可以:

scala> add(1)
res9: Int = 2

scala> add("world")
res10: String = hello world

scala> add("happy string ", 5)
res11: String = happy string 5

看到这里,你可能会说这不就是重载吗?java和scala原生就支持重载,但jvm对泛型(generics)的支持是通过类型擦除(type erasure)实现的,这意味着java常规的重载无法带类型参数,如:jvm无法区分下面这种类型不同的List参数。

scala> :paste
// Entering paste mode (ctrl-D to finish)

def add(a: List[Int]): Unit = {}
def add(a: List[String]): Unit = {}

// Exiting paste mode, now interpreting.

<console>:8: error: double definition:
def add(a: List[Int]): Unit at line 7 and
def add(a: List[String]): Unit at line 8
have same type after erasure: (a: List)Unit
def add(a: List[String]): Unit = {}

而magnet模式正好可以弥补这个缺憾,另外magnet模式相当于把重载的实现从语言层面拉到了自己的代码逻辑中,有利于针对性的引入一些新技巧减少冗余代码。当然,magnet的缺点也是很明显的:额外的一层增加了代码的复杂度。

隐含参数

implicit另外一个常用的场景是: 替代全局变量,作为某个执行上下文中的隐含参数。

如:scala中异步的一个重要方法是使用Future。Futrure语义清晰,使用优雅,比手动起线程不知道高到哪里去了;),但Future在后台其实还是通过线程来执行的,要用Future就需要一个指定的执行上下文环境(ExecutionContext ,一般是线程池)来跑Future。Future又是一个object(单例对象,不是普通类)没有地方放这个线程池的引用,解决方案只能是在所有Future的方法中加上ExecutionContext参数,方法很函数式,但接口略显冗余。好在scala有implicit,只要你调用Future时,上下文中有一个implicit的ExecutionContext变量,Future会自动在这个EC上跑代码。

所以scala的Future方法都有一个(implicit executor: ExecutionContext)参数

def onComplete(U  U)(implicit executor: ExecutionContext): Unit

不同于全局变量,你在调用Future方法时,想使用某个指定的ExecutionContext,还是可以把它作为参数显示的传递给Future方法,这个显示传递的参数会覆盖implicit的参数。

另:ExecutionContext的获取方法有

  1. 直接引用全局EC。import scala.concurrent.ExecutionContext.Implicits.global

  2. akka的actor中,引用当前actor系统的EC。import context.dispatcher

  3. 也可以手动创建一个独占使用,确保线程池里的线程不会被其他不相干任务耗尽。

    import java.util.concurrent.Executors
    import concurrent.ExecutionContext
    //创建一个4个线程的线程池
    val executorService = Executors.newFixedThreadPool(4)
    implicit val ec = ExecutionContext.fromExecutorService(executorService)
    

更多相关资料:

The magnet pattern

Design Patterns in Scala

revisiting implicits without import tax

Chapter 21 of Programming in Scala, First Edition

Type Classes as Objects and Implicits