5

假设我们想require在 Scala 中构建一个类似 的功能,这在许多脚本语言中都是众所周知的。

如果我们需要这样的库...

require("some-library.jar")

...当执行 require 宏时,我们需要一种方法来将此 jar 添加到编译器的类路径中。如何才能做到这一点?

4

2 回答 2

5

另一个有趣的@kim-stebel 问题。

我的第一个想法(没有解决您的问题)是您的编译器可以使用 findMacroClassLoader 自定义宏类路径。由于@extempore,REPL 使用了它。

这对于像 requires("myfoo.jar") { mymacro } 这样的习语可能很有用。

您的问题是您是否可以更新编译器的类路径。这可以通过从您的上下文宇宙向下转换到编译器来实现,为此 isCompilerUniverse == true。然后你可以platform.updateClassPath。

用代码更新:

这类似于在所需宏的类路径中使用特殊占位符的想法。

下面提到的奇特方法是使用自定义类路径,该路径可以报告该位置的所有必需类。下面提到的旧代码用于类路径中的虚拟目录。

这种快速而俗气的方式使用文件系统来解压缩你的 jar,并要求编译器重新扫描它。

由于 invalidateClassPathEntries 的限制,占位符必须是实际目录,它想要检查规范文件路径是否在类路径上。

package alacs

import scala.language.experimental.macros
import scala.reflect.macros.Context

import scala.sys.process._
import java.io.File

/** A require macro to dynamically fudge the compilation classpath.  */
object PathMaker {
  // special place to unpack required libs, must be on the initial classpath
  val entry = "required"

  // whether to report updated syms without overly verbose -verbose
  val talky = true

  def require(c: Context)(name: c.Expr[String]): c.Expr[Unit] = {
    import c.universe._
    val st = c.universe.asInstanceOf[scala.reflect.internal.SymbolTable]
    if (st.isCompilerUniverse) {
      val Literal(Constant(what: String)) = name.tree
      if (update(what)) {
        val global = st.asInstanceOf[scala.tools.nsc.Global]
        val (updated, _) = global invalidateClassPathEntries entry
        c.info(c.enclosingPosition, s"Updated symbols $updated", force = talky)
      } else {
        c.abort(c.enclosingPosition, s"Couldn't unpack '$what' into '$entry'")
      }
    }
    reify { () }
  }

  // figure out where name is, and update the special class path entry
  def update(name: String): Boolean = {
    // Process doesn't parse the command, it just splits on space,
    // something about working on Windows
    //val status = s"sh -c \"mkdir $entry ; ( cd $entry ; jar xf ../$name )\"".!

    // but Process can set cwd for you
    val command = s"jar xf ../$name"
    val status = Process(command, new File(entry)).!
    (status == 0)
  }
}

所需的 API:

package alacs

import scala.language.experimental.macros

object Require {
  def require(name: String): Unit = macro PathMaker.require
}

用法:

package sample

import alacs.Require._

/** Sample app requiring something not on the class path. */
object Test extends App {
  require("special.jar")
  import special._
  Console println Special(7, "seven")
}

包装在 special.jar 中的东西

package special

case class Special(i: Int, s: String)

以这种方式测试:

rm -rf required
mkdir required
skalac pathmaker.scala
skalac -cp .:required require.scala sample.scala
skala -cp .:special.jar sample.Test

apm@mara:~/tmp/pathmaker$ . ./b
Unpack special.jar
sample.scala:8: Updated symbols List(package special)
  require("special.jar")
         ^
Special(7,seven)

宏不会将其潜入运行时路径,这是脚本要做的事情。

但我认为 require 宏可以做一些方便的事情,比如有条件地抓取不同版本的 jar,具有不同的编译时特性(常量等)。

更新,只是验证它很疯狂:

  require("fast.jar")
  import constants._
  Console println speed

  require("slow.jar")
  Console println speed

在哪里

package object constants {
  //final val speed = 55
  final val speed = 85
}

$ skalac -d fast.jar constants.scala

并使用内联常量运行它

85
55

新的警告:这是我的第一个宏,我正在为另一个应用程序寻找 invalidateClassPathEntries,所以我还没有探索限制。

更新:一个限制是控制宏何时展开。我想展示一些东西是如何针对旧 api 和新 api 进行编译的,并且必须将代码包装在块中以确保符号在需要之前可用:

require("oldfoo.jar")
locally {
  import foo._
  // something
  require("newfoo.jar")
  // try again
}

老警告:对不起,我知道你不遵守这个模糊的答案;我稍后会尝试,但与此同时,也许有人会清楚地向前迈进。

以前,我已经连接到编译器全局的“平台”实现,但对于这个用例来说,这可能是过度的。那时,您可以对类路径做任何您想做的事情,但我认为您想要一些开箱即用的东西。

于 2013-05-29T06:48:34.697 回答
0

正如 Eugene Burmanko 在这个问题中所说,目前(Scala 2.10)不能在宏之外添加/删除定义。如果你愿意用宏天堂,或许有办法。

于 2013-06-01T01:37:45.070 回答