scala-repl-pp
Scala REPL PlusPlus - a (slightly) better Scala 3 / dotty REPL. Note: this currently depends on a slightly patched version of dotty. I'll try to get those merged upstream.
Motivation: scala-repl-pp fills a gap between the standard Scala3 REPL, Ammonite and scala-cli.
Note: this currently depends on a dotty fork, which has since been merged into dotty upstream, i.e. we'll be able to depend on the regular dotty release from 3.2.2 on
TOC
- Benefits over / comparison with
- Build it locally
- REPL
- Scripting
- Server mode
- Embed into your own project
- Limitations
Benefits over / comparison with
Regular Scala REPL
- add runtime dependencies on startup with maven coordinates - automatically handles all downstream dependencies via coursier
- pretty printing via pprint
- customize greeting, prompt and shutdown code
- multiple @main with named arguments (regular Scala REPL only allows an argument list)
- predef code - i.e. run custom code before starting the REPL - via string and scripts
- server mode: REPL runs embedded
- easily embeddable into your own build
Ammonite
- Ammonite's Scala 3 support is far from complete - e.g. autocompletion for extension methods has many shortcomings. In comparison: scala-repl-pp uses the regular Scala3/dotty ReplDriver.
- Ammonite has some Scala2 dependencies intermixed, leading to downstream build problems like this. It's no longer easy to embed Ammonite into your own build.
- Note: Ammonite allows to add dependencies dynamically even in the middle of the REPL session - that's not supported by scala-repl-pp yet. You need to know which dependencies you want on startup.
scala-cli
scala-cli is mostly a wrapper around the regular Scala REPL and Ammonite, so depending on which one you choose, you essentially end up with the same differences as above.
Build it locally
Prerequisite for all of the below:
sbt stage
Generally speaking, --help
is your friend!
./scala-repl-pp --help
REPL
# run with defaults
./scala-repl-pp
# customize prompt, greeting and exit code
./scala-repl-pp --prompt=myprompt --greeting='hey there!' --onExitCode='println("see ya!")'
# pass some predef code
./scala-repl-pp --predefCode='def foo = 42'
scala> foo
val res0: Int = 42
Add dependencies with maven coordinates
Note: the dependency must be known at startup time, either via --dependency
parameter...
./scala-repl-pp --dependency com.michaelpollmeier:versionsort:1.0.7
scala> versionsort.VersionHelper.compare("1.0", "0.9")
val res0: Int = 1
... or using lib
directive in predef code or predef files...
echo '//> using lib com.michaelpollmeier:versionsort:1.0.7' > predef.sc
./scala-repl-pp --predefFiles=predef.sc
scala> versionsort.VersionHelper.compare("1.0", "0.9")
val res0: Int = 1
Importing additional script files interactively
echo 'val bar = foo' > myScript.sc
./scala-repl-pp
val foo = 1
//> using file myScript.sc
println(bar) //1
Scripting
See ScriptRunnerTest for a more complete and in-depth overview.
Simple "Hello world" script
test-simple.sc
println("Hello!")
./scala-repl-pp --script test-simple.sc
Predef code for script
test-predef.sc
println(foo)
./scala-repl-pp --script test-predef.sc --predefCode 'val foo = "Hello, predef!"'
Predef code via environment variable
test-predef.sc
println(foo)
export SCALA_REPL_PP_PREDEF_CODE='val foo = "Hello, predef!"'
./scala-repl-pp --script test-predef.sc
Predef file(s)
test-predef.sc
println(foo)
test-predef-file.sc
val foo = "Hello, predef file"
./scala-repl-pp --script test-predef.sc --timesiles test-predef-file.sc
Importing files / scripts
foo.sc:
val foo = 42
test.sc:
//> using file foo.sc
println(foo)
./scala-repl-pp --script test.sc
Dependencies
Dependencies can be added via //> using lib
syntax (like in scala-cli).
test-dependencies.sc:
//> using lib com.michaelpollmeier:versionsort:1.0.7
val compareResult = versionsort.VersionHelper.compare("1.0", "0.9")
assert(compareResult == 1,
s"result of comparison should be `1`, but was `$compareResult`")
./scala-repl-pp --script test-dependencies.sc
Note: this also works with using
directives in your predef code - for script and REPL mode.
@main entrypoints
test-main.sc
@main def main() = println("Hello, world!")
./scala-repl-pp --script test-main.sc
multiple @main entrypoints: test-main-multiple.sc
@main def foo() = println("foo!")
@main def bar() = println("bar!")
./scala-repl-pp --script test-main-multiple.sc --command=foo
named parameters
test-main-withargs.sc
@main def main(name: String) = {
println(s"Hello, $name!")
}
./scala-repl-pp --script test-main-withargs.sc --params name=Michael
Server mode
./scala-repl-pp --server
curl http://localhost:8080/query-sync -X POST -d '{"query": "val foo = 42"}'
curl http://localhost:8080/query-sync -X POST -d '{"query": "val bar = foo + 1"}'
Embed into your own project
Try out the working string calculator example in this repo:
cd src/test/resources/demo-project
sbt stage
target/universal/stage/bin/stringcalc
Welcome to the magical world of string calculation!
Type `help` for help
stringcalc> add(One, Two)
val res0: stringcalc.Number = Number(3)
Predef code and scripts
There's a variety of ways to define predef code, i.e. code that is being run before any given script:
echo 'def bar = 90' > ~/.scala-repl-pp.sc
echo 'def baz = 91' > script1.sc
echo 'def bam = 92' > script2.sc
export SCALA_REPL_PP_PREDEF_CODE='def bax = 93'
./scala-repl-pp --predefCode='def foo = 42' --predefFiles=script1.sc,script2.sc
scala> foo
val res0: Int = 42
scala> bar
val res1: Int = 90
scala> baz
val res2: Int = 91
scala> bam
val res3: Int = 92
scala> bax
val res4: Int = 93
Limitations
Why are script line numbers incorrect?
Scala-REPL-PP currently uses a simplistic model for predef code|files and additionally imported files, and just copies everything into one large script. That simplicity naturally comes with a few limitations, e.g. line numbers may be different from the input script(s).
A better approach would be to work with a separate compiler phase, similar to what Ammonite does. That way, we could inject all previously defined values|imports|... into the compiler, and extract all results from the compiler context. That's a goal for the future.
If there's a compilation issue, the temporary script file will not be deleted and the error output will tell you it's path, in order to help with debugging.