Scala environment recommendations? ("Programming in Scala")

I’ve started working through “Programming in Scala,” and found that setting up my Scala environment isn’t as straightforward an exercise as I thought it’d be. Chapter 2 of the book, where the tutorial really starts, points the reader to the Scala download page and tells them to follow the instructions there. I’ve found, though, that installing Scala through SBT leads to issues with the code in the book running as written (scripts don’t run as written and I can’t start the Scala REPL with $ scala). I expect that I’ll find the same thing installing through an IDE (IntelliJ or VS Code).

On the other hand, installing Scala through SDKMAN works; everything runs as written in the book. But now I have a system-wide Scala installation (as opposed to local), and I still don’t know how to pull down additional libraries/modules/etc. This whole exercise has led me to wonder whether there’s a “best practices” or “most common” or “recommended” tooling environment. I come from a Python-centric world where pipenv install (plus a Pipfile) and pipenv shell are useful and easily understandable tools. And in my brief time in the Rust world, I appreciated cargo which like pipenv allows for local installations/configurations.

Is SBT anything like pipenv or cargo? What’s the “recommended” way to set up one’s Scala environment?

If it’s important, I’m working in an Ubuntu 20.04 WSL2 installation. I’m very comfortable with the command line (iPython), similarly comfortable in a browser (Jupyter), and less comfortable with a full IDE (though I’ve set up X11 forwarding).

PS - looks like the Scala 2.13.5 REPL does some nice things with multi-line editing, reverse search [Ctrl-R], and tab completion.

1 Like

Hi, welcome to the community and sorry for how difficult is the starting experience, especially related to tooling. But, everything from here is just way better, I promise!

Let me try to help you.

  1. Maybe the gitter channel is better for a newcomer, especially for this kind of question which are more interactive.
  2. Scala tooling can be divided into two groups: system-wide tools and project-specific tools; whenever to install system wide things depend on your preferences.
  • System-wide: I (and I would guess most people) have the following tools installed system-wide.
    • coursier: It is a small binary that helps to install and keep updated all the other tools in this list; check their home page and this video for details.
    • jdk: Since Scala main target is the JVM you need a jre to run most of your code and almost all tools in this list are actually jars; also I believe (but not sure) that the Scala compile requires a jdk.
    • sbtn (note the n at the end): A native launcher of sbt which is the default build tool in the Scala ecosystem, note this launcher will be used to download and execute the specific version of sbt that each of your project needs.
    • scala: (optional) I like to have a global Scala REPL and compiler for small snippets and experiments, but many people save this step due Ammonite (mentioned later).
    • ammonite: A more powerful REPL, which has things like a configurable set of default imports as well as the ability to import external libraries.
  • Project-wide: You only need one tool and it is your build tool, which will be in charge of downloading all your libraries / plugins including the Scala version, managing the classpath, compiling, testing and running your code; etc. The most common option is sbt which has a big plus over many other build-tools which is that your project can define the version of sbt it uses without needing additional extra wrappers or run scripts. You configure your project through a set of files:
    • build.sbt which contains things like your project name, organization and version; the Scala version (or versions if cross-compiling, but that is an advanced topic) to use, and your dependencies.
    • project/build.properties which is mainly used just to define your sbt version.
    • project/plugins.sbt (optional) which is used to add plugins to your build.

For example, a full hello world project that produces a fat jar that could be run by any JRE would be:

// file: build.sbt
name := "hello-world"
version := "1.0.0"
scalaVersion := "2.13.5"
assembly / assemblyJarName := "app.jar"

// file: src/main/scala/simple/Main.scala
package simple
object Main {
  def main(args: Array[String]): Unit = {
    println("Hello, World! - From: Scala")
  }
}

// file: project/build.properties
sbt.version=1.5.0

// file: project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")

And then run:

sbtn assembly
java -jar targe/scala-2.13/app.jar
  1. About IDE, you can choose between IntelliJ and metals, metals supports any text editor with supports the LSP; for example VSCode. I also use WSL 2 and since VSCode has the external plugin, I can just launch code . in my terminal and it works out of the box. However, IntelliJ has more features than metals and it should provide a more out-of-the-box experience; however, if you launch VSCode in the directory with the files I mentioned before it should prompt you to import the project and that would be it.

Hope this helps!
Let me know if you have more questions!

3 Likes

Strong :+1: here – I’ve taken to always recommending coursier as the way to get set up. It is much easier than older mechanisms. (The coursier approach is still quite new, so most books don’t know about it.)

2 Likes

No need to apologize. I’ve also read somewhere else that getting one’s environment set up can be a strong initial hurdle, but that things get much smoother after that. That’s one of the reasons I decided to ask here.

Maybe. Can gitter be searched? Do gitter messages show up in Google results? I’m guessing the answer is “no.” If so, I’m posting here because I’m guessing I’m not the first person to have these sorts of questions. It’d be useful for others to be able to find answers.

Wow, yeah, that’s a pretty nice tool. Took me all of about 10 minutes to blow away my other stuff (local SBT, SDKMAN Scala, apt-get OpenJDK) and reinstall it all via coursier:

$ cs list
amm
cs
sbt
sbtn
scala
scalac
scalafmt

$ java --version
openjdk 11.0.9 2020-10-20
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.9+11)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.9+11, mixed mode)

I don’t know why, but in my first attempt at installing ammonite, I think something went wrong. I did it in an SBT project (using the instructions on ammonite’s website), but when I’d use arrow keys to backtrack through multi-line code, characters disappeared. It was quite disorienting. But the coursier install doesn’t appear to have that problem.

I’ll have to go read more about sbt before I start really using it.

Thanks. I’ll probably try both VS Code and IntelliJ when I get to that point, and see which I like better.

2 Likes

Or maybe I’ll ask you folks to point me to good reading material.

Question: What’s going on “behind the scenes” when I execute a Scala script from scala, amm, and sbt run? I get very different behavior from each one, and I’m trying to make sense of it.

I first create a simple Scala script:

$ cat printArgs.scala
//Print args passed on commandline
args.foreach(println)

I then execute that script using scala and it runs as I expect it to:

$ scala printArgs.scala one two
one
two

However, when I execute it using amm I get an error:

$ amm printArgs.scala one two
Compiling <path>/printArgs.scala
printArgs.scala:2: not found: value args
val res = args.foreach(println)
          ^
Compilation Failed

I then create an sbt project:

$ sbt new scala/scala-seed.g8
<snip>
[info] resolving Giter8 0.13.1...

A minimal Scala project.

name [Scala Seed Project]: printArgs

Template applied in <path>/./printargs
$ cat src/main/scala/example/Hello.scala
<comment out default code>
args.foreach(println)

…and I get a different error when I try to run it:

$ sbt run
[info] welcome to sbt 1.5.0 (AdoptOpenJDK Java 11.0.9)
<snip>
[info]   Compilation completed in 10.073s.
[error] src/main/scala/example/Hello.scala:12:1: expected class or object definition
[error] args.foreach(println)
[error] ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed
[error] Total time: 12 s, completed Apr 22, 2021, 4:54:14 PM

Why do these 3 methods of executing code produce such different results?

One quick follow-up about coursier: I hit a snag installing on a different WSL image, one that didn’t have Java already installed:

$ curl -L https:/git.io/coursier-cli -o cs
<snip>
$ chmod +x cs
$ ./cs install cs
./cs: 25: exec: java: not found

Installing a Java JRE package solved the issue, but it seems counter-intuitive that Java would have to be on the machine before installing Java, Scala, and related tools.

Wow, that feels like a bug that you may report in the coursier channel, cs is supposed to be a native executable with no dependency at all in anything. I am pretty sure I used to install a full env on a clean WSL 2 machine image a couple of months ago and you can see Vlad doing that in the video I shared. So, I suspect you downloaded a wrong executable, but it seems you used the correct URL so that is probably a problem on their side.

About your question about running the program, the problem is that your code is only valid for a Scala script that receives special treatment, it is not valid code in general. And, that is why it fails in sbt. And, Ammonite being a different version of the REPL has its own way of managing entry points and arguments.

The valid sbt version of the code would be:

package example

object Main {
  def main(args: Array[String]): Unit = {
    args.foreach(println)
  }
}

A bit more verbose, but I prefer to do all this in general even for scripts; it makes it clear which is the entry point.

1 Like

Okay. So the three are different. I’m guessing scala wraps each line into an object / entry point behind the scenes before compiling and executing???

Yeah, except even that code doesn’t run anywhere except in sbt:

$ cat printargs.scala
package example

object Main {
    def main(args: Array[String]): Unit = {
        args.foreach( (arg: String) => println(arg) )
    }
}
$ scala printargs.scala a b v
printargs.scala:8: error: illegal start of definition
package example
^
$ amm printargs.scala one two
Compiling /home/phendric/Software/scalaPlayground/printargs.scala
Script printargs.scala does not take arguments: "one" "two"

(Note: I don’t necessarily have a problem with any of this. I’m just needing to make a mental note that, depending on how I want to execute a script, I’m going to need to alter it to be compatible.)

It’s worth noting that, while the Scala community takes a lot of pride in being able to use the language in many different ways, I believe that most Scala code is in larger-scale sbt-ish (whether built with sbt or other build tools) pre-compiled applications. Certainly the vast majority of the Scala programming I’ve done over the past dozen years has been sbt-built, both server-side and browser-based.

Scripting with it is a bit less common, which I think is why you find a couple of different idioms for doing it – there isn’t one true way. (Personally, I find Ammonite the best way to do Scala scripting, but there are folks fond of other approaches.)

3 Likes

Yeah, sorry for not being clear. I meant more about the Main object and the main method.
package is an “instruction” that doesn’t work in any scripting environment, mostly because it doesn’t make much sense.
Also, for Ammonite it actually has to be a little bit more different since as I said, they have their own @main annotation and all that.

So yeah, as @jducoeur said, while technically Scala can be used for big programs as well as scripts (and this also applies to running in the JVM and in JS) there are some details specific to each environment.

In Scala ecosystem sbt is similar to pipenv and build.sbt is similar to Pipfile. Note pipenv shell is implicit in executing sbt command, that is, by default sbt will launch project-specific environment. Equivalent to pipenv install is to specify dependencies in build.sbt via libraryDependiencies setting and then launching sbt which will automatically fetch (install) the dependencies. Note sbt does not require lock file.

My Scala onboarding suggestion is to first install system-wide tooling with coursier

then install Visual Studio Code with Metals extension

and make sure code command is on the path, for example in Mac it is added with

Next create a simple hello world Scala app from a template

sbt new scala/scala-seed.g8

Now import the hello world project into VSC with

code .

Now there are multiple facilities easily accessible for experimentation and learning

  1. Code navigation via control+click on symbols
  2. Quick running of tests and app via VSC code lenses
  3. Quick experimentation with VSC Worksheets
  4. Quick access to Scala REPL proper

For example, say you want to load the scripts from the book

  1. You could just copy paste them into VSC worksheets which are evaluated upon save, or
  2. open VSC terminal
  3. execute sbt console
  4. This will open Scala REPL proper with all the dependencies specified in build.sbt available
  5. Now load the scripts by executing :load path/to/your/script
3 Likes

Yeah, I’d expect books wouldn’t be updated with new/recent information. I’d hope that the Scala website, which has installation instructions that Martin Odersky’s book refers users to, would be reasonably up-to-date.

Or rather, given the link in @mario-galic’s post, I’d hope that at some point the Scala blog post about coursier would get linked to onto the download/install page.

And…the book I’m reading says that yes, that’s basically what happens:

“The actual mechanism that the scala program uses to ‘interpret’ a Scala source file is that it compiles the Scala source code to Java bytecodes, loads them immediately via a class loader, and executes them” (pg 111, footnote).

It also outlines the difference between a Scala script and a non-script:

“Neither ChecksumAccumulator.scala nor Summer.scala are scripts, because they end in a definition. A script, by contrast, must end in a result expression” (pg 110).

@mario-galic - thanks for the explicit relating of things in the Scala world to the Python / pipenv world.