The main class that the REPL uses is ILoop, which I imagine stands for "interpreter loop" or something to that effect. It is quite simple to embed the normal REPL into our own program, like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object ILoopExample extends App { | |
val settings = new Settings(println(_)) | |
settings.usejavacp.value = true | |
val iloop = new ILoop | |
iloop.process(settings) | |
} |
If you run this code, you'll be presented with the standard prompt that you see when you launch the REPL (note that without the jline library you won't get command history, tab completion, etc, but the compile-and-run functionality is there). It is running with the same classpath as your program, so any classes you have defined are available in the interpreter. Let's do something a bit more complex; what if we want to programmtically initialize the interpreter environment, e.g. to have certain variables pre-defined or classes pre-imported? We can extend the ILoop class and override the postInitialization() method where we can handle the newly created IMain object, which is the actual interpreter:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Foo { | |
def hi = println("Hello, World!") | |
} | |
class MyILoop extends ILoop(None, new PrintWriter(System.out)) { | |
override def postInitialization() { | |
super.postInitialization() | |
intp.bindValue("SomeConstant", 42) | |
intp.addImports(classOf[Foo].getCanonicalName) | |
intp.definedSymbols.foreach(out.println(_)) | |
out.flush() | |
} | |
} |
Here, we bind the variable SomeConstant to have value 42, pre-import the Foo class, and print out the list of symbols defined by the interpreter. Because we have access to the interpreter object, it is easy to see how you could set up the proper environment with the interpreter and make Domain-Specific Languages (DSLs) even more natural. When you start the custom ILoop from above, we see the following output:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
SomeConstant: Int = 42 | |
import blog.scala.repl.Foo | |
value $intp | |
value SomeConstant |
So our constant is defined, the Foo class is imported, and we see that the two defined symbols in the interpreter environment are SomeConstant and this special $intp variable. It turns out that this is a reference to the actual interpreter object that we had earlier, which lets us do crazy meta things like interpret a line of code which interprets a line of code. Here's a transcript from the interpreter environment:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scala> SomeConstant + 1 | |
res0: Int = 43 | |
scala> (new Foo).hi | |
Hello, World! | |
scala> $intp.interpret("val x = SomeConstant * 2") | |
x: Int = 84 | |
res2: scala.tools.nsc.interpreter.IR.Result = Success | |
scala> x | |
res3: Int = 84 |
So we see that the implementation of the Scala REPL is quite extensible in terms of altering the environment from code. I think there are a lot of applications of this, especially when using Scala outside of the traditional software engineering environment.