在Scala中,我可以隐式地将某些文字转换为我的自定义类型吗?

时间:2012-07-20 08:43:45

标签: scala macros literals implicit-conversion

在我的应用中,我正在跟踪用户拥有的积分数量。要添加一些类型检查,我使用类似于这个的Credits类:

case class Credits(val numCredits: Int) extends Ordered[Credits] {
   ...
}

假设我有一个我想要调用的函数def accept(creds: Credits): Unit。有没有办法让我用

来调用它
process(Credits(100))
process(0)

但不是这个?

process(10)

即,我只想从文字0提供隐式转换,而不是其他。现在,我在伴侣对象中只有val Zero = Credits(0),我认为这是一个相当不错的练习,但无论如何我都会对答案感兴趣,包括其他评论,例如:

  • 这可以通过2.10中的宏隐式转换来完成吗?
  • 应该Credits而不是扩展AnyVal而不是2.10中的案例类?

3 个答案:

答案 0 :(得分:10)

这种编译时检查是使用宏的好地形,将在2.10中提供

一个名叫 Jason Zaugg 的聪明人已经实现了类似于你需要的东西,但它适用于正则表达式:正则表达式编译时检查。

您可能希望查看其Macrocosm以了解它是如何完成的,以及如何以相同的目的编写自己的宏。

https://github.com/retronym/macrocosm

如果你真的想了解更多有关宏的知识,首先我会说你需要勇敢,因为现在文档很少,而API很可能会改变。 Jason Zaugg的作品与2.10-M3合作很好,但我不确定它是否适用于较新的版本。

如果你想从一些读数开始:

现在,谈到主题,Scala宏是 CAT :“编译时AST转换”。 抽象语法树是编译器表示源代码的方式。编译器将后续转换应用于AST,并在最后一步实际生成java字节码。

现在让我们来看看Jason Zaugg的代码:

 def regex(s: String): scala.util.matching.Regex = macro regexImpl

  def regexImpl(c: Context)(s: c.Expr[String]): c.Expr[scala.util.matching.Regex] = {
    import c.universe._

    s.tree match {
      case Literal(Constant(string: String)) =>
        string.r // just to check
        c.reify(s.splice.r)
    }
  }

正如您所见,正则表达式是一个特殊的函数,它通过调用宏regexImpl接受一个字符串并返回一个正则表达式

宏函数在第一个参数列表中接收上下文,在第二个参数列表中以c.Expr [A]的形式列出宏的参数并返回c.Expr [B]。请注意,c.Expr是路径依赖类型,即它是在Context中定义的类,因此如果您有两个上下文,则以下内容是非法的

val c1: context1.Expr[String] = ...
val c2: context2.Expr[String] = ...
val c3: context1.Expr[String] = context2.Expr[String] // illegal , compile error

现在,如果你看一下代码中发生了什么:

  • 在s.tree
  • 上有匹配块
  • 如果s.tree是一个包含常量String的Literal,则称为string.r

这里发生的是从Predef.scala中定义的字符串到StringOps的隐式转换,它会在每个scala源的编译中自动导入

implicit def augmentString(x: String): StringOps = new StringOps(x)

StringOps扩展了scala.collection.immutable.StringLike,其中包含:

def r: Regex = new Regex(toString)

由于宏是在编译时执行的,所以这将在编译时执行,如果抛出异常,编译将失败(这是从无效的正则表达式字符串创建正则表达式的行为)


注意:不幸的是,API非常不稳定,如果查看http://scalamacros.org/documentation/reference.html,您会看到Context.scala链接断开。正确的链接是https://github.com/scala/scala/blob/2.10.x/src/reflect/scala/reflect/makro/Context.scala

答案 1 :(得分:3)

基本上,您需要dependent types。为什么Scala在路径依赖类型中支持有限形式的依赖类型,它无法满足您的要求。

Edmondo在建议宏方面有一个好主意,但它有一些局限性。由于它非常简单,我实现了它:

case class Credits(numCredits: Int)        
object Credits {
  implicit def toCredits(n: Int): Credits = macro toCreditsImpl

  import scala.reflect.makro.Context
  def toCreditsImpl(c: Context)(n: c.Expr[Int]): c.Expr[Credits] = {
    import c.universe._                                                                          

    n.tree match {                                                                               
      case arg @ Literal(Constant(0)) =>                                                         
        c.Expr(Apply(Select(Ident("Credits"), newTermName("apply")),           
          List(arg)))
      case _ => c.abort(c.enclosingPosition, "Expected Credits or 0")                            
    }                                                                                            
  }                                                                                              
}  

然后我启动了REPL,定义了accept,并进行了基本演示:

scala> def accept(creds: Credits) { println(creds) }
accept: (creds: Credits)Unit

scala> accept(Credits(100))
Credits(100)

scala> accept(0)
Credits(0)

scala> accept(1)
<console>:9: error: Expected Credits or 0
              accept(1)
                     ^

现在,问题是:

scala> val x = 0
x: Int = 0

scala> accept(x)
<console>:10: error: Expected Credits or 0
              accept(x)
                     ^

换句话说,我无法跟踪分配给标识符的值的属性,这是依赖类型允许我这样做的。

但是整个事情让我感到浪费。为什么要转换 0?您似乎想要一个默认值,在这种情况下,最简单的解决方案是使用默认值:

scala> def accept(creds: Credits = Credits(0)) { println(creds) }
accept: (creds: Credits)Unit

scala> accept(Credits(100))
Credits(100)

scala> accept()
Credits(0)

答案 2 :(得分:1)

使用可以使用隐式部分函数:

scala> case class Credits(val numCredits: Int)
defined class Credits

scala> def process(c: Credits) = {}
process: (c: Credits)Unit

scala> implicit def i2c:PartialFunction[Int, Credits] = { case 0 => Credits(0) }

i2c: PartialFunction[Int,Credits]

允许你

scala> process(Credits(12))

scala> process(0)

可是:

scala> process(12)
scala.MatchError: 12 (of class java.lang.Integer)
        at $anonfun$i2c$1.apply(<console>:9)
        at $anonfun$i2c$1.apply(<console>:9)
        at .<init>(<console>:12)
        at .<clinit>(<console>)
        at .<init>(<console>:11)
        at .<clinit>(<console>)
        at $print(<console>)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.lang.reflect.Method.invoke(Unknown Source)
        at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:704)

        at scala.tools.nsc.interpreter.IMain$Request$$anonfun$14.apply(IMain.sca
la:920)
        at scala.tools.nsc.interpreter.Line$$anonfun$1.apply$mcV$sp(Line.scala:4
3)
        at scala.tools.nsc.io.package$$anon$2.run(package.scala:25)
        at java.lang.Thread.run(Unknown Source)

编辑:但是,编译器仍会允许process(12)在运行时导致匹配错误。