Li Haoyi’s Ammonite is the most user-friendly Scala REPL out there.
It has a more than decent syntax highlight, you can edit multi-line snippets, search the command
history, auto-complete expressions and much more!
It’s remarkable how you can fiddle with a library without the hassle of creating a SBT project
thanks to the magic $ivy imports. Let’s say you want to try out some scalaz 7.2.23 for some
reason:
Importing $ivy.`org.scalaz::scalaz-core:7.2.23` is equivalent to adding
"org.scalaz" %% "scalaz-core" % "7.2.23" to SBT’s libraryDependencies.
Ammonite can be embedded in other applications and I find it useful to so when
developing Finatra services.
There are two interesting ways of doing the integration: as a local development console for
debugging and exploration or as a mechanism to inspect or operate against remote services.
Give your Finatra service a development console
If you have ever used Rails and enjoyed it, you will be offended when facing development
environments in which you cannot start a console and tinker with domain services, the database,
etc. The first step to get Ammonite integrated into Finatra a la Rails is to depend on
Ammonite with test scope so add to your build.sbt this:
where versions.ammonite is a recent version (e.g. 1.4.0).
The idea is to have an alternative main class under src/test that will start the service and the
console. To make it easier, you might create an alias in your build.sbt:
As you can see, we are using the ammonite.integration package. This is important because we need
to use some definitions that are package-private and only available from such package.
Then, you should create the launcher class. It should similar to your actual entry point (usually a
server class extending from com.twitter.finatra.http.HttpServer and a handful of Guice modules)
but instantiating Ammonite.
Let’s take a look at ConsoleLauncher bit by bit.
Nothing special up to here.
We can add a custom prelude to Ammonite. You can add common imports or even functions and implicit
conversions here. This prelude allows me to easily use services returning Future in the console.
Finally, we instantiate the console. Note that we are exposing the Guice dependency injector so
we can lookup any component of our service in the console. For common things we can directly bind
then to a name, exactly like the sample FooService is bound to foo.
Et voilà, no need to miss the Rails console anymore.
Remote Ammonite powers
The other interesting way to integrate Ammonite into your service is as a remote console. That way
you can connect to a remote server in the stating or even production machines and fiddle with them.
Maybe not a very good idea… but power to the people!
I’m going to demonstrate this approach by introducing a pluggable Guice module that you can include
or not in your server to enable or disable the server.
We will need an additional dependency but this time will have compile scope instead of test scope.
And a module named AmmoniteServerModule under src/main/scala:
We have some elements in common: same prelude and the class is in the ammonite.integration
package. However, this is an independent Guice module.
The SSH server is defined as an application singleton (@Provides @Singleton) and configured to
listen in localhost on the port 22222 honoring ~/.ssh/authorized_keys as system’s SSH server.
Note also, how the injector and some services can be also bound for the session.
We need also to start/stop the server using some lifecycle callback methods.
With this in place, we can SSH and enjoy of the same interactive experience as before but
potentially from the other side of the world: