Scala - Как скомпилировать код из внешнего файла во время выполнения?

Я хочу разработать программу Scala, которая принимает файлы Scala в качестве параметров, которые могут настраивать выполнение программы. В частности, я хочу предоставить во время выполнения файлы, которые содержат реализации методов, которые будут вызываться программой. Как я могу должным образом зависеть от внешних файлов и динамически вызывать их методы во время выполнения? В идеале мне бы хотелось, чтобы эти файлы могли зависеть от методов и классов в моей программе.

Пример сценария: у меня есть функция, которая содержит строку val p: Plant = Greenhouse.getPlant()и Greenhouse класс с getPlant Метод определен в одном из файлов, которые будут предоставлены во время выполнения. В этом файле метод getPlant возвращает Rose, где Rose <: Plant а также Plant определяется в оригинальной программе. Как мне добиться (или приблизить) этой взаимозависимости, предполагая, что файлы объединяются только во время выполнения, а не во время компиляции?

2 ответа

Решение

Вот как это сделать, используя только стандартную Scala. Неочевидные вещи все в GreenhouseFactory:

package customizable

abstract class Plant

case class Rose() extends Plant

abstract class Greenhouse {
  def getPlant(): Plant
}

case class GreenhouseFactory(implFilename: String) {
  import reflect.runtime.currentMirror
  import tools.reflect.ToolBox
  val toolbox = currentMirror.mkToolBox()
  import toolbox.u._
  import io.Source

  val fileContents = Source.fromFile(implFilename).getLines.mkString("\n")
  val tree = toolbox.parse("import customizable._; " + fileContents)
  val compiledCode = toolbox.compile(tree)

  def make(): Greenhouse = compiledCode().asInstanceOf[Greenhouse]
}

object Main {
  def main(args: Array[String]) {
    val greenhouseFactory = GreenhouseFactory("external.scala")
    val greenhouse = greenhouseFactory.make()
    val p = greenhouse.getPlant()

    println(p)
  }
}

Поместите ваше выражение переопределения в external.scala:

new Greenhouse {
  override def getPlant() = new Rose()
}

Выход:

Rose()

Единственная сложность в том, что GreenhouseFactory необходимо предвидеть, что import оператор для предоставления доступа ко всем типам и символам, необходимым для внешних файлов. Чтобы сделать это легко, сделайте единый пакет со всеми этими вещами.

Компилятор ToolBox вроде как задокументировано здесь. Единственное, что вам действительно нужно знать, кроме странного импорта, это то, что toolbox.parse преобразует строку (исходный код Scala) в абстрактное синтаксическое дерево и toolbox.compile преобразует абстрактное синтаксическое дерево в функцию с сигнатурой () => Any, Поскольку это динамически скомпилированный код, вы должны жить с приведением Any к типу, который вы ожидаете.

Scala изначально не предоставляет такую ​​функциональность. Самый простой способ сделать это - использовать библиотеку Twitter "util-eval". Эта библиотека объединяет необходимые вызовы компилятора Scala и различные ритуалы загрузки классов, экономя вам огромное количество усилий. Последовательность вызовов для выполнения того, что вы описываете, будет выглядеть примерно так

val eval = new Eval()
val greenhouse = eval.apply[Greenhouse](new File("path/to/MyGreenhouse.scala"))
val plant = greenhouse.getPlant()

Ваш динамически загружаемый файл Scala должен содержать выражение, а не класс как таковой, но это довольно легко сделать, в основном так.

new Greenhouse{
    def getPlant() = //thing to return the plant 
}

Насколько я понимаю, Twitter использует (или, по крайней мере, использовал) эту функциональность, чтобы его конфигурационные файлы были в виде Scala, а не properties/json/xml.

Другие вопросы по тегам