scala隐式转换及其DSL实现(二)

作者: 安静1337 | 来源:发表于2016-04-21 16:15 被阅读328次

在实现具体应用前,先介绍下隐式类的概念。

隐式类介绍

Scala 2.10引入了一种叫做隐式类的新特性。隐式类指的是用implicit关键字修饰的类。在对应的作用域内,带有这个关键字的类的主构造函数可用于隐式转换。

用法

创建隐式类时,只需要在对应的类前加上implicit关键字。比如:

object Helpers {
    implicit class IntWithTimes(x: Int) {
        def times[A](f: => A): Unit = {
            def loop(current: Int): Unit =
                if(current > 0) {
                    f
                    loop(current - 1)
                }
            loop(x)
        }
    }
}

这个例子创建了一个名为IntWithTimes的隐式类。这个类包含一个int值和一个名为times的方法。要使用这个类,只需将其导入作用域内并调用times方法。比如:

scala> import Helpers._
import Helpers._
scala> 5 times println("HI")
HI
HI
HI
HI
HI

使用隐式类时,类名必须在当前作用域内可见且无歧义,这一要求与隐式值等其他隐式类型转换方式类似。

限制条件

隐式类有以下限制条件:

  • 只能在别的trait/类/对象内部定义。
object Helpers {
   implicit class RichInt(x: Int) // 正确!
}
implicit class RichDouble(x: Double) // 错误!
  • 构造函数只能携带一个非隐式参数。
implicit class RichDate(date: java.util.Date) // 正确! 
implicit class Indexer[T](collecton: Seq[T], index: Int) // 错误! 
implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // 正确!

虽然我们可以创建带有多个非隐式参数的隐式类,但这些类无法用于隐式转换。

  • 在同一作用域内,不能有任何方法、成员或对象与隐式类同名。注意:这意味着隐式类不能是case class。
object Bar
implicit class Bar(x: Int) // 错误!
val x = 5
implicit class x(y: Int) // 错误!
implicit case class Baz(x: Int) // 错误!

现在,我们来实现一个简单的DSL。通常,我们在计算货币运算时,如果不涉及到币种的转换,通常可以简单的用数学表达式计算,比如23 + 12,在任何编程语言里都能做到。但是涉及到币种时,这种简单的表达式不满足要求了,需要做一下汇率转换才能进行计算,然而这样却不很直观。我们需要一种直观的运算,同时又要包含汇率转换的操作,比如像这样的表达式:23(USD) + 12(EUR),下面,我们就用scala的隐式转换实现这样的要求。

首先,我们定义一个关于货币的伴生类(class)和伴生对象(单例对象object)。定义它们的成员变量code,name以及object中对应的币种信息。关键代码如下:

object Currency {
    type Rate = Map[(Currency, Currency), BigDecimal] //first_currency / second_currency = rate: BigDecimal

    def apply (code: String, name: String) = new Currency(code, name)

    /*
    def apply (code: String) = code.toLowerCase match {
        case "USD" => USD
        case "EUR" => EUR
        case "GBP" => GBP
    }
    */

    lazy val USD: Currency = Currency("USD", "美元")
    lazy val EUR: Currency = Currency("EUR", "欧元")
    lazy val GBP: Currency = Currency("GBP", "英镑")

    def convert (from: Currency, to: Currency, rate: Rate): BigDecimal = {
        if (from.code.equalsIgnoreCase(to.code))
            1
        else
            rate.getOrElse((from, to), 1 / rate((to, from)))
    }

    implicit class BigDecimalExt (value: BigDecimal) {
        def apply(currency: Currency)(implicit rate: Rate): Money = Money(currency, value)
    }

    implicit class IntExt (value: Int) {
        def apply(currency: Currency)(implicit rate: Rate): Money = (value: BigDecimal).apply(currency)
    }

    implicit class CurrIntExt (c: Currency) {
        def apply(value: Int)(implicit rate: Rate): Money = Money(c, value)(rate)
    }

}


class Currency (val code: String, val name: String = "未知")

object代码中,我们定义了汇率的类型Rate,也就是汇率等于先后两个币种的比值。convert方法用于后面金钱的具体转换。

然后定义Money类:

case class Money (currency: Currency,amount: BigDecimal)(implicit rate: Rate) {
    def to (to: Currency): Money = {
        val rates = Currency.convert(currency, to, rate)
        Money(to, rates * amount)
    }

    def operation (that: Money, operate: (BigDecimal, BigDecimal) => BigDecimal): Money = {
        that match {
            case Money(curr, am) if (currency.code equalsIgnoreCase curr.code) => Money(currency, operate(amount, am))
            case Money(curr, am) => operation(that.to(currency), operate)
        }
    }

    def + (that: Money): Money = {
        operation(that, (a, b)=> a + b) //占位符写法 operation(that, _ + _)
    }
}

Money类中,方法to(to: Curreyct)用于币种转换,+用于币种计算,此处仅仅实现了+方法。
至此,我们实现了不同币种加法的运算。利用上面的代码,我们可以有三种不同的调用方法。

  • 传统的方法调用
    val rate_0: Rate = Map(
    (GBP, EUR) -> 1.39,
    (EUR, USD) -> 1.08,
    (GBP, USD) -> 1.5
    )

      val r0 = (Money(USD, 42)(rate_0) + Money(EUR, 35)(rate_0)) to GBP
      println("r0: " + r0.currency.name + " " + r0.currency.code + " " + r0.amount)
    
  • 两种DSL方式写法(涉及到Currency里的隐式类)
    implicit val rate_1: Rate = Map(
    (GBP, EUR) -> 1.39,
    (EUR, USD) -> 1.08,
    (GBP, USD) -> 1.5
    )//for r1 and r2

      import Currency.IntExt
      val r1 = (42(USD) + 35(EUR)) to GBP
      println("r1: " + r1.currency.name + " " + r1.currency.code + " " + r1.amount)
    
      import Currency.CurrIntExt
      val r2 = (USD(42) + EUR(35)) to GBP
      println("r2: " + r2.currency.name + " " + r2.currency.code + " " + r2.amount)
    

小记:虽然scala隐式转换是一个非常强大的功能,但是实现起来很困难,dsl虽然使用方便,但是这样的“魔幻”代码理解起来还是费劲。

相关文章

网友评论

    本文标题:scala隐式转换及其DSL实现(二)

    本文链接:https://www.haomeiwen.com/subject/dgptrttx.html