Chapter 1. Command-Line Tasks

Most likely, one of the first steps on your Scala 3 journey will involve working at the command line. For instance, after you install Scala as shown in “Installing Scala”, you might want to start the REPL—Scala’s Read/Eval/Print/Loop—by typing scala at your operating system command line. Or you may want to create a little one-file “Hello, world” project and then compile and run it. Because these command-line tasks are where many people will start working with Scala, they’re covered here first.

The REPL is a command-line shell. It’s a playground area where you can run small tests to see how Scala and its third-party libraries work. If you’re familiar with Java’s JShell, Ruby’s irb, the Python shell or IPython, or Haskell’s ghci, Scala’s REPL is similar to all of these. As shown in Figure 1-1, just start the REPL by typing scala at your operating system command line, then type in your Scala expressions, and they’ll be evaluated in the shell.

Any time you want to test some Scala code, the REPL is a terrific playground environment. There’s no need to create a full-blown project—just put your test code in the REPL and experiment with it until you know it works. Because the REPL is such an important tool, its most important features are demonstrated in the first two recipes of this chapter.

Figure 1-1. The Scala 3 REPL running in a macOS Terminal window

While the REPL is terrific, it’s not the only game in town. The Ammonite REPL was originally created for Scala 2, and it had many more features than the Scala 2 REPL, including:

  • The ability to import code from GitHub and Maven repositories

  • The ability to save and restore sessions

  • Pretty-printed output

  • Multiline editing

At the time of this writing Ammonite is still being ported to Scala 3, but many important features already work. See Recipe 1.3 for examples of how to use those features.

Finally, when you need to build Scala projects, you’ll typically use a build tool like sbt, which is demonstrated in Chapter 17. But if you ever want to compile and run a small Scala application, such as one that has just one or two files, you can compile your code with the scalac command and run it with scala, just like you do in Java with the javac and java commands. This process is demonstrated in Recipe 1.4, and after that, Recipe 1.6 shows how you can run applications that you package as a JAR file with the java or scala commands.

1.1 Getting Started with the Scala REPL

Problem

You want to get started using the Scala REPL, and start taking advantage of some of its basic features.

Solution

If you’ve used REPL environments in languages like Java, Python, Ruby, and Haskell, you’ll find the Scala REPL to be very familiar. To start the REPL, type scala at your operating system command line. When the REPL starts up you may see an initial message, followed by a scala> prompt:

$ scala
Welcome to Scala 3.0
Type in expressions for evaluation. Or try :help.

scala> _

The prompt indicates that you’re now using the Scala REPL. Inside the REPL environment you can try all sorts of different experiments and expressions:

scala> val x = 1
x: Int = 1

scala> val y = 2
y: Int = 2

scala> x + y
res0: Int = 3

scala> val x = List(1, 2, 3)
x: List[Int] = List(1, 2, 3)

scala> x.sum
res1: Int = 6

As shown in these examples:

  • After you enter your command, the REPL output shows the result of your expression, including data type information.

  • If you don’t assign a variable name, as in the third example, the REPL creates its own variable, beginning with res0, then res1, etc. You can use these variable names just as though you had created them yourself:

scala> res1.getClass
res2: Class[Int] = int

scala> res1 + 2
res3: Int = 8

Both beginning and experienced developers write code in the REPL every day to quickly see how Scala features and their own algorithms work.

Tab completion

There are a few simple tricks that can make using the REPL more effective. One trick is to use tab completion to see the methods that are available on an object. To see how tab completion works, type the number 1, then a decimal, and then press the Tab key. The REPL responds by showing the dozens of methods that are available on an Int instance:

scala> 1.
!=                         finalize                   round
##                         floatValue                 self
%                          floor                      shortValue
&                          formatted                  sign
*                          getClass                   signum
many more here ...

You can also limit the list of methods that are displayed by typing the first part of a method name and then pressing the Tab key. For instance, if you want to see all the methods available on a List, type List(1). followed by the Tab key, and you’ll see over two hundred methods. But if you’re only interested in methods on a List that begin with the characters to, type List(1).to and then press Tab, and that output will be reduced to these methods:

scala> List(1).to
to              toIndexedSeq    toList          toSet           toTraversable
toArray         toIterable      toMap           toStream        toVector
toBuffer        toIterator      toSeq           toString

Discussion

I use the REPL to create many small experiments, and it also helps me understand some type conversions that Scala performs automatically. For instance, when I first started working with Scala and typed the following code into the REPL, I didn’t know what type the variable x was:

scala> val x = (3, "Three", 3.0)
val x: (Int, String, Double) = (3,Three,3.0)

With the REPL, it’s easy to run tests like this and then call getClass on a variable to see its type:

scala> x.getClass
val res0: Class[? <: (Int, String, Double)] = class scala.Tuple3

Although some of that result line is hard to read when you first start working with Scala, the text on the right side of the = lets you know that the type is a Tuple3 class.

You can also use the REPL’s :type command to see similar information, though it currently doesn’t show the Tuple3 name:

scala> :type x
(Int, String, Double)

However, it’s generally helpful in many other instances:

scala> :type 1 + 1.1
Double

scala> :type List(1,2,3).map(_ * 2.5)
List[Double]

Though these are simple examples, you’ll find that the REPL is extremely helpful when you’re working with more complicated code and libraries you’re not familiar with.

Starting the REPL Inside sbt

You can also start a Scala REPL session from inside the sbt shell. As shown in Recipe 17.5, “Understanding Other sbt Commands”, just start the sbt shell inside an sbt project:

$ sbt
MyProject> _

Then use either the console or the consoleQuick command from there:

MyProject> console
scala> _

The console command compiles the source code files in the project, puts them on the classpath, and starts the REPL. The consoleQuick command starts the REPL with the project dependencies on the classpath, but without compiling project source code files. This second option is useful for times when your code isn’t compiling, or when you want to try some test code with your dependencies (libraries).

See Also

If you like the idea of a REPL environment but want to try alternatives to the default REPL, there are several great free alternatives:

  • The Ammonite REPL has more features than the REPL, and it’s demonstrated in Recipe 1.3.

  • Scastie is a web-based alternative to the REPL that supports sbt options and lets you add external libraries into your environment.

  • ScalaFiddle is also a web-based alternative.

  • The IntelliJ IDEA and Visual Studio Code (VS Code) IDEs both have worksheets, which are similar to the REPL.

1.2 Loading Source Code and JAR Files into the REPL

Problem

You have Scala code in a source code file and want to use that code in the REPL.

Solution

Use the :load command to read source code files into the REPL environment. For example, given this code in a file named Person.scala, in a subdirectory named models:

class Person(val name: String):
    override def toString = name

you can load that source code into the running REPL environment like this:

scala> :load models/Person.scala
// defined class Person

After the code is loaded into the REPL, you can create a new Person instance:

scala> val p = Person("Kenny")
val p: Person = Kenny

Note, however, that if your source code has a package declaration:

// Dog.scala file
package animals
class Dog(val name: String)

the :load command will fail:

scala> :load Dog.scala
1 |package foo
  |^^^
  |Illegal start of statement

Source code files can’t use packages in the REPL, so for situations like this you’ll need to compile them into a JAR file, and then include them in the classpath when you start the REPL. For instance, this is how I use version 0.2.0 of my Simple Test library with the Scala 3 REPL:

// start the repl like this
$ scala -cp simpletest_3.0.0-0.2.0.jar

scala> import com.alvinalexander.simpletest.SimpleTest.* 

scala> isTrue(1 == 1)
true

At the time of this writing you can’t add a JAR to an already running REPL session, but that feature may be added in the future.

Discussion

Another good thing to know is that compiled class files in the current directory are automatically loaded into the REPL. For example, if you put this code in a file named Cat.scala and then compile it with scalac, that creates a Cat.class file:

case class Cat(name: String)

If you start the REPL in the same directory as that class file, you can create a new Cat:

scala> Cat("Morris")
val res0: Cat = Cat(Morris)

On Unix systems you can use this technique to customize your REPL environment. To do so, follow these steps:

  1. Create a subdirectory in your home directory named repl. In my case, I create this directory as /Users/al/repl. (Use any name for this directory that you prefer.)

  2. Put any *.class files you want in that directory.

  3. Create an alias or shell script you can use to start the REPL in that directory.

On my system I put a file named Repl.scala in my ~/repl directory, with these contents:

import sys.process.*

def clear = "clear".!
def cmd(cmd: String) = cmd.!!
def ls(dir: String) = println(cmd(s"ls -al $dir"))
def help =
    println("\n=== MY CONFIG ===")
    "cat /Users/Al/repl/Repl.scala".!

case class Person(name: String)
val nums = List(1, 2, 3)

I then compile that code with scalac to create its class files in that directory. Then I create and use this alias to start the REPL:

alias repl="cd ~/repl; scala; cd -"

That alias moves me to the ~/repl directory, starts the REPL, and then returns me to my current directory when I exit the REPL.

As another approach, you can create a shell script named repl, make it executable, and place it in your ~/bin directory (or anywhere else on your PATH):

#!/bin/sh

cd ~/repl
scala

Because a shell script is run in a subprocess, you’ll be returned to your original directory when you exit the REPL.

By using this approach, your custom methods will be loaded into the REPL when it starts up, so you can use them inside the scala shell:

clear       // clear the screen
cmd("ps")   // run the 'ps' command
ls(".")     // run 'ls' in the current directory
help        // displays my Repl.scala file as a form of help

Use this technique to preload any other custom definitions you’d like to use in the REPL.

1.3 Getting Started with the Ammonite REPL

Problem

You want to get started using the Ammonite REPL, including understanding some of its basic features.

Solution

The Ammonite REPL works just like the Scala REPL: just download and install it, then start it with its amm command. As with the default Scala REPL, it evaluates Scala expressions and assigns a variable name if you don’t provide one:

@ val x = 1 + 1
x: Int = 2

@ 2 + 2
res0: Int = 4

But Ammonite has many additional features. You can change the shell prompt with this command:

@ repl.prompt() = "yo: "

yo: _

Next, if you have these Scala expressions in a file named Repl.scala, in a subdirectory named foo:

import sys.process.*

def clear = "clear".!
def cmd(cmd: String) = cmd.!!
def ls(dir: String) = println(cmd(s"ls -al $dir"))

you can import them into your Ammonite REPL with this command:

@ import $file.foo.Repl, Repl.* 

Then you can use those methods inside Ammonite:

clear        // clear the screen
cmd("ps")    // run the 'ps' command
ls("/tmp")   // use 'ls' to list files in /tmp

Similarly, you can import a JAR file named simpletest_3.0.0-0.2.0.jar in a subdirectory named foo into your amm REPL session using the Ammonite $cp variable:

// import the jar file
import $cp.foo.`simpletest_3.0.0-0.2.0.jar`

// use the library you imported
import com.alvinalexander.simpletest.SimpleTest.*
isTrue(1 == 1)

The import ivy command lets you import dependencies from Maven Central (and other repositories) and use them in your current shell:

yo: import $ivy.`org.jsoup:jsoup:1.13.1`
import $ivy.$

yo: import org.jsoup.Jsoup, org.jsoup.nodes.{Document, Element}
import org.jsoup.Jsoup

yo: val html = "<p>Hi!</p>"
html: String = "<p>Hi!</p>"

yo: val doc: Document = Jsoup.parse(html)
doc: Document = <html> ...

yo: doc.body.text
res2: String = "Hi!"

Ammonite’s built-in time command lets you time how long it takes to run your code:

@ time(Thread.sleep(1_000))
res2: (Unit, FiniteDuration) = ((), 1003788992 nanoseconds)

Ammonite’s auto-complete ability is impressive. Just type an expression like this, then press Tab after the decimal:

@ Seq("a").map(x => x.

When you do so, Ammonite displays a long list of methods that are available on x—which is a String—beginning with these methods:

def intern(): String
def charAt(x$0: Int): Char
def concat(x$0: String): String
much more output here ...

This is nice because it shows you not only the method names but also their input parameters and return type.

Discussion

Ammonite’s list of features is long. Another great one is that you can use a startup configuration file, just like using a Unix .bashrc or .bash_profile startup file. Just put some expressions in a ~/.ammonite/predef.sc file:

import sys.process.*

repl.prompt() = "yo: "
def clear = "clear".!
def cmd(cmd: String) = cmd.!!
def ls(dir: String) = println(cmd(s"ls -al $dir"))
def reset = repl.sess.load()  // similar to the scala repl ':reset' command

Then, when you start the Ammonite REPL, your prompt will be changed to yo:, and those other methods will be available to you.

One more great feature is that you can save a REPL session, and it will save everything you’ve done to this point. To test this, create a variable in the REPL, and then save your session:

val remember = 42
repl.sess.save()

Then create another variable:

val forget = 0

Now reload the session, and you’ll see that the remember variable is still available, but the forget variable has been forgotten, as desired:

@ repl.sess.load()
res3: SessionChanged = SessionChanged(removedImports = Set('forget),
addedImports = Set(), removedJars = Set(), addedJars = Set())

@ remember
res4: Int = 42

@ forget
   |val res5 = forget
   |           ^^
   |           Not found: forget

You can also save and load multiple sessions by giving them different names, like this:

// do some work
val x = 1
repl.sess.save("step 1")

// do some more work
val y = 2
repl.sess.save("step 2")

// reload the first session
repl.sess.load("step 1")

x   // this will be found
y   // this will not be found

See the Ammonite documentation for details on more features.

1.4 Compiling with scalac and Running with scala

Problem

Though you’ll typically use a build tool like sbt or Mill to build Scala applications, occasionally you may want to use more basic tools to compile and run small test programs, in the same way you might use javac and java with small Java applications.

Solution

Compile small programs with scalac, and run them with scala. For example, given this Scala source code file named Hello.scala:

@main def hello = println("Hello, world")

compile it at the command line with scalac:

$ scalac Hello.scala

Then run it with scala, giving the scala command the name of the @main method you created:

$ scala hello
Hello, world

Discussion

Compiling and running classes is the same as Java, including concepts like the classpath. For instance, imagine that you have a class named Pizza in a file named Pizza.scala, and that it depends on a Topping type:

class Pizza(val toppings: Topping*):
    override def toString = toppings.toString

Assuming that Topping is defined like this:

enum Topping:
    case Cheese, Mushrooms

and that it’s in a file named Topping.scala, and has been compiled to Topping.class in a subdirectory named classes, compile Pizza.scala like this:

$ scalac -classpath classes Pizza.scala

Note that the scalac command has many additional options you can use. For instance, if you add the -verbose option to the previous command, you’ll see hundreds of lines of additional output that show how scalac is working. These options may change over time, so use the -help option to see additional information:

$ scalac -help

Usage: scalac <options> <source files>
where possible standard options include:
-P               Pass an option to a plugin, e.g. -P:<plugin>:<opt>
-X               Print a synopsis of advanced options.
-Y               Print a synopsis of private options.
-bootclasspath   Override location of bootstrap class files.
-classpath       Specify where to find user class files.

much more output here ...

Main methods

While we’re talking about compiling main methods, it helps to know that they can be declared in two ways with Scala 3:

  • Using the @main annotation on a method

  • Declaring a main method with the proper signature in an object

As shown in the Solution, a simple @main method that takes no input parameters can be declared like this:

@main def hello = println("Hello, world")

You can also declare an @main method to take whatever parameters you want on the command line, such as taking a String and Int in this example:

@main def hello(name: String, age: Int): Unit =
    println(s"Hello, $name, I think you are $age years old.")

After that code is compiled with scalac, it can be run like this:

$ scala hello "Lori" 44
Hello, Lori, I think you are 44 years old.

For the second approach, declaring a main method inside an object is just like declaring a main method in Java, and the signature for the Scala main method must look like this:

object YourObjectName:
    // the method must take `Array[String]` and return `Unit`
    def main(args: Array[String]): Unit =
        // your code here

If you’re familiar with Java, that Scala code is analogous to this Java code:

public class YourObjectName {
    public static void main(String[] args) {
        // your code here
    }
}

1.5 Disassembling and Decompiling Scala Code

Problem

In the process of learning how Scala code is compiled into class files, or trying to understand a particular problem, you want to examine the bytecode the Scala compiler generates from your source code.

Solution

The main way to disassemble Scala code is with the javap command. You may also be able to use a decompiler to convert your class files back to Java source code, and this option is shown in the Discussion.

Using javap

Because your Scala source code files are compiled into regular JVM class files, you can use the javap command to disassemble them. For example, assume that you’ve created a file named Person.scala that contains this source code:

class Person(var name: String, var age: Int)

Next, compile that file with scalac:

$ scalac Person.scala

Now you can disassemble the resulting Person.class file into its signature using javap, like this:

$ javap -public Person
Compiled from "Person.scala"
public class Person {
  public Person(java.lang.String, int);
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public int age();
  public void age_$eq(int);
}

This shows the public signature of the Person class, which is its public API, or interface. Even in a simple example like this you can see the Scala compiler doing its work for you, creating methods like name(), name_$eq, age(), and age_$eq. The Discussion shows more detailed examples.

If you want, you can see additional information with the javap -private option:

$ javap -private Person
Compiled from "Person.scala"
public class Person {
  private java.lang.String name;   // new
  private int age;                 // new
  public Person(java.lang.String, int);
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public int age();
  public void age_$eq(int);
}

The javap has several more options that are useful. Use the -c option to see the actual commands that comprise the Java bytecode, and add the -verbose option to that to see many more details. Run javap -help for details on all options.

Discussion

Disassembling class files with javap can be a helpful way to understand how Scala works. As you saw in the first example with the Person class, defining the constructor parameters name and age as var fields generates quite a few methods for you.

As a second example, take the var attribute off both of those fields, so you have this class definition:

class Person(name: String, age: Int)

Compile this class with scalac, and then run javap on the resulting class file. You’ll see that this results in a much shorter class signature:

$ javap -public Person
Compiled from "Person.scala"
public class Person {
  public Person(java.lang.String, int);
}

Conversely, leaving var on both fields and turning the class into a case class significantly expands the amount of code Scala generates on your behalf. To see this, change the code in Person.scala so you have this case class:

case class Person(var name: String, var age: Int)

When you compile this code, it creates two output files, Person.class and Person$.class. Disassemble those two files using javap:

$ javap -public Person
Compiled from "Person.scala"
public class Person implements scala.Product,java.io.Serializable {
  public static Person apply(java.lang.String, int);
  public static Person fromProduct(scala.Product);
  public static Person unapply(Person);
  public Person(java.lang.String, int);
  public scala.collection.Iterator productIterator();
  public scala.collection.Iterator productElementNames();
  public int hashCode();
  public boolean equals(java.lang.Object);
  public java.lang.String toString();
  public boolean canEqual(java.lang.Object);
  public int productArity();
  public java.lang.String productPrefix();
  public java.lang.Object productElement(int);
  public java.lang.String productElementName(int);
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public int age();
  public void age_$eq(int);
  public Person copy(java.lang.String, int);
  public java.lang.String copy$default$1();
  public int copy$default$2();
  public java.lang.String _1();
  public int _2();
}

$ javap -public Person$
Compiled from "Person.scala"
public final class Person$ implements scala.deriving.Mirror$Product,
java.io.Serializable {
  public static final Person$ MODULE$;
  public static {};
  public Person apply(java.lang.String, int);
  public Person unapply(Person);
  public java.lang.String toString();
  public Person fromProduct(scala.Product);
  public java.lang.Object fromProduct(scala.Product);
}

As shown, when you define a class as a case class, Scala generates a lot of code for you, and this output shows the public signature for that code. See Recipe 5.14, “Generating Boilerplate Code with Case Classes”, for a detailed discussion of this code.

About Those .tasty Files

You may have noticed that in addition to .class files, Scala 3 also generates .tasty files during the compilation process. These files are generated in what’s known as a TASTy format, where the acronym TASTy comes from the term typed abstract syntax trees.

Regarding what these files are, the TASTy Inspection documentation states, “TASTy files contain the full typed tree of a class including source positions and documentation. This is ideal for tools that analyze or extract semantic information from the code.”

One of their uses is for integration between Scala 3 and Scala 2.13+. As this Scala forward compatibility page states, “Scala 2.13 can read these (TASTy) files to learn, for example, which terms, types and implicits are defined in a given dependency, and what code needs to be generated to use them correctly. The part of the compiler that manages this is known as the Tasty Reader.”

See Also

  • In my “How to Create Inline Methods in Scala 3” blog post, I show how to use this technique to understand inline methods.

  • You may also be able to use decompilers to convert .class files into Java code. I occasionally use a tool named JAD, which was discontinued in 2001, but amazingly it’s still able to at least partially decompile class files twenty years later. A much more modern decompiler named CFR was also mentioned on the Scala Gitter channel.

For more information on TASTy and .tasty files, see these resources:

1.6 Running JAR Files with Scala and Java

Problem

You’ve created a JAR file from a Scala application and want to run it using the scala or java commands.

Solution

First, create a basic sbt project, as shown in Recipe 17.1, “Creating a Project Directory Structure for sbt”. Then add sbt-assembly into the project configuration by adding this line to the project/plugins.sbt file:

// note: this version number changes several times a year
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")

Then put this Hello.scala source code file in the root directory of that project:

@main def hello = println("Hello, world")

Next, create a JAR file with either the assembly or show assembly command in the sbt shell:

// option 1
sbt:RunJarFile> assembly

// option 2: shows the output file location
sbt:RunJarFile> show assembly
[info] target/scala-3.0.0/RunJarFile-assembly-0.1.0.jar

As shown, the output of the show assembly command prints the location where the output JAR file is written. This file begins with the name RunJarFile because that’s the value of the name field in my build.sbt file. Similarly, the 0.1.0 portion of the filename comes from the version field in that file:

lazy val root = project
   .in(file("."))
   .settings(
      name := "RunJarFile",
      version := "0.1.0",
      scalaVersion := "3.0.0"
  )

Next, create an Example subdirectory, move into that directory, and copy the JAR file into that directory:

$ mkdir Example
$ cd Example
$ cp ../target/scala-3.0.0/RunJarFile-assembly-0.1.0.jar .

Because the sbt-assembly plugin packages everything you need into the JAR file, you can run the hello main method with this scala command:

$ scala -cp "RunJarFile-assembly-0.1.0.jar" hello
Hello, world

Note that if your JAR file contains multiple @main methods in packages, you can run them with similar commands, specifying the full path to the methods at the end of the command:

scala -cp "RunJarFile-assembly-0.1.0.jar" com.alvinalexander.foo.mainMethod1
scala -cp "RunJarFile-assembly-0.1.0.jar" com.alvinalexander.bar.mainMethod2

Discussion

If you (a) attempt to run your JAR file with the java command, or (b) create the JAR file with sbt package instead of sbt assembly, you’ll need to manually add your JAR file dependencies to your classpath. For example, when running a JAR file with the java command, you’ll need to use a command like this:

$ java -cp "~/bin/scala3/lib/scala-library.jar:my-packaged-jar-file.jar"foo.bar.Hello
Hello, world

Note that the entire java command should be on one line, including the foo.bar.Hello portion at the end of the line.

For this approach you need to find the scala-library.jar file. In my case, because I manage the Scala 3 distribution manually, I found it in the directory shown. If you’re using a tool like Coursier to manage your Scala installation, the files it downloads can be found under these directories:

  • On macOS: ~/Library/Caches/Coursier/v1

  • On Linux: ~/.cache/coursier/v1

  • On Windows: %LOCALAPPDATA%\Coursier\Cache\v1, which, for a user named Alvin, typically corresponds to C:\Users\Alvin\AppData\Local\Coursier\Cache\v1

See the Coursier Cache page for up-to-date details on these directory locations.

Why use sbt-assembly?

Note that if your application uses managed or unmanaged dependencies and you use sbt package instead of sbt assembly, you’ll have to understand all of those dependencies and their transitive dependencies, find those JAR files, and then include them in the classpath setting. For that reason, the use of sbt assembly or a similar tool is strongly recommended.

See Also

Get Scala Cookbook, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.