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

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

隐式类介绍

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虽然使用方便,但是这样的“魔幻”代码理解起来还是费劲。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容