4

在我的应用程序中,我正在跟踪用户拥有的积分数。为了添加一些类型检查,我使用了一个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 中的案例类?
4

3 回答 3

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)
    }
  }

正如你所看到的,regex 是一个特殊的函数,它接受一个字符串并通过调用宏 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 是 Literal,包含常量 String ,则调用 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

于 2012-07-20T09:32:10.300 回答
4

基本上,您需要依赖类型。为什么 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)
于 2012-07-20T14:09:35.683 回答
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)在运行时导致匹配错误。

于 2012-07-20T09:15:25.987 回答