The Scala REPL is a great tool. We can use it to run random code, test out theories, and other quick Scala tasks. We can even run scripts in the REPL to set up more complex code. The REPL can also be used as the command line user interface for applications. Anyone who has used sbt has used a customized Scala REPL. Wouldn't it be nice if we could leverage this powerful tool and the powerful Scala language for our user interfaces?
Using the Scala REPL in this way is not well documented, but it is able to be incorporated into your Scala code. This library, rebel, is intended to expose parts of the REPL in an easy to access place so that you too can utilize the REPL for powerful user interfaces.
Add the dependency to your product:
- sbt
libraryDependencies += "com.potenciasoftware" %% "rebel" % "[[version]]"
- Maven
<dependency> <groupId>com.potenciasoftware</groupId> <artifactId>rebel_2.13</artifactId> <version>[[version]]</version> </dependency>
Extend BaseRepl
and override its protected members to customize the REPL
according to your needs. Call run()
on an instance of your customized REPL
class to start the REPL.
import com.potenciasoftware.rebel.BaseRepl
object Main {
class MyRepl extends BaseRepl {
// Customize the REPL here.
}
def main(args: Array[String]): Unit = {
new MyRepl().run()
}
}
This allows full access to the compiler settings prior to instantiation of the
REPL. See scalac -help
and its more specific variants (e.g. scalac -X
) for
details. All of the options described in the help output are available from the
Settings
instance that is passed to updateSettings()
.
Also note, the Settings
instance is mutable, just set the desired values on
the properties you want to change.
As an example, the below code will cause the REPL to use VIM keybindings rather than Emacs keybindings:
class MyRepl extends BaseRepl {
import scala.tools.nsc.Settings
override protected def updateSettings(settings: Settings): Unit = {
// This corresponds to the -Xjline command line option of the scala command.
settings.Xjline.value = "vi"
}
}
Please note that if you leave Settings.classpath
set to the default value,
BaseRepl
will automatically include the full classpath of your running
application as the classpath of the REPL. This is usually what you want.
By default when the REPL starts up, it prints out a standard banner including
information about the version of Scala and the JVM running the REPL. Override
banner
to customize this startup banner. Including the value of
super.banner
will cause the inclusion of the default information.
You may want to set the prompt to something other than the default (scala>
).
You may also want to change the prompt to indicate some change in state within
your application. The prompt
getter and setter members (def prompt: String
and def prompt_=(String): Unit
may be used for these purposes.
You may want to bind objects from within your application to values that are
accessable from the REPL. Override boundValues
to provide a list of the
values to bind.
Example:
class ApplicationSettings {
var prettyPrintResults: Boolean = false
}
class MyRepl extends BaseRepl {
import com.potenciasoftware.rebel.BaseRepl.Parameter
val settings = new ApplicationSettings
override protected val boundValues: Seq[Parameter] =
Seq(Parameter[ApplicationSettings](
name = "settings",
value = settings))
}
Often you want to run a set of commands whenever your REPL starts. For instance you may want to automatically run imports so that the contents of your packages will be immediately available to the REPL user.
Example:
class MyRepl extends BaseRepl {
override protected val startupScript: String =
"""
|import com.example.Utils._
|import com.example.Implicits._
|""".stripMargin
}
Every command entered into the REPL is turned into a larger piece of Scala code
that is then compiled and executed. An execution wrapper is a method that takes
=> Any
and returns String
which gives you the ability to participate in the
generation of that code.
When an execution wrapper method is set on the REPL interpreter, each command
entered into the REPL will be wrapped with a call to that method effectively
passing each command to the method's => Any
parameter and returning the
String
that the method returns.
The execution wrapper method must be able to be statically referenced (i.e. from a static global value or a public object) because the fully qualified path to the execution wrapper method is provided as part of the code that the REPL compiles and executes.
The trait ExecutionWrapper
provides a place to define this fully qualified
path, def code: String
. A typical use of the trait would be to:
- Extend
ExecutionWrapper
on a public object. - Define the execution wrapper method as part of that object.
- Override
def code: String
on the object with the fully qualified path to the method from step 2. - Override
val executionWrapper: ExecutionWrapper
on your REPL class to return the object.
package com.example
import com.potenciasoftware.rebel.executionWrapper.ExecutionWrapper
object Main {
object MyExecutionWrapper extends ExecutionWrapper {
def execute(a: => Any): String = {
// do something before each command
val result = a
// do something after each command
result.toString
}
override def code: String =
"com.example.Main.MyExecutionWrapper.execute"
}
class MyRepl extends BaseRepl {
override val executionWrapper: ExecutionWrapper = MyExecutionWrapper
}
def main(args: Array[String]): Unit = {
new MyRepl().run()
}
}
Included in this library is an ExecutionWrapper
implementation,
ExecuteInFiber
, which executes each command in a ZIO fiber which can be
interrupted with the INT signal (Ctrl-C). When this ExecutionWrapper
is
installed, the user may cancel a long running (or hung) command with Ctrl-C
without causing the REPL to exit as it normally would.
import com.potenciasoftware.rebel.executionWrapper.ExecuteInFiber
object MyExecutionWrapper extends ExecuteInFiber
object Main {
class MyRepl extends BaseRepl {
override val executionWrapper: ExecutionWrapper = MyExecutionWrapper
}
def main(args: Array[String]): Unit = {
new MyRepl().run()
}
}
Note: If you need to debug your execution wrapper method, one thing that can be
helpful is to tell the REPL to output the code that will be compiled by
appending // show
at the end of your command like this:
scala> 1 + 1 // show
The REPL provides commands like :load
and :type
out of the box. This method
may be used to provide additional commands.
Example:
class MyRepl extends BaseRepl {
import com.potenciasoftware.rebel.BaseRepl.LoopCommand
override protected val customCommands: Seq[LoopCommand] =
Seq(LoopCommand(
name = "answer",
usage = "[question]",
help = "Gives the answer to any question you have regarding " +
"Life, the Universe, and Everything.",
f = input => {
println("42")
LoopCommand.Result(true, None)
}))
}
When a user enters the command :quit
into the REPL it will exit. Your
application may want to do some quick clean up before the REPL exits. Override
the onQuit()
method for this.
Note: Since users of the REPL expect it to close pretty quickly, BaseRepl
will automatically call sys.exit()
after 5 seconds has elapsed. Your cleanup
code needs to be fast enough to finish in that time.
class MyRepl extends BaseRepl {
override protected def onQuit(): Unit = {
println("Goodbye")
}
}