Scala 3 Module 1 – Start here: Scala 3.9 LTS on the JVM
First programs: scala run, scripts and the REPL
Write an @main program with typed arguments, run it and a .sc script with scala run, try code in the REPL, and see what the compile server does.
What you will learn
- Write an @main program and pass it typed arguments with scala run . -- and the arguments
- Predict what an @main program does with an argument that does not parse, a missing one and an extra one
- Run a .sc script that reads its arguments from args, and decide when code belongs in .scala files
- Use the REPL to try expressions and read their types with :type
- Explain what the compile server does and when to run with --server=false
Before you start
What you’ll learn · Before you start
What you will learn
- Write an @main program and pass it typed arguments with scala run . -- and the arguments
- Predict what an @main program does with an argument that does not parse, a missing one and an extra one
- Run a .sc script that reads its arguments from args, and decide when code belongs in .scala files
- Use the REPL to try expressions and read their types with :type
- Explain what the compile server does and when to run with --server=false
Before you start
On this page
A Scala program needs one method marked @main and nothing else. The tip calculator below is seven lines, and
scala run . -- 4250 15 compiles it and prints a tip of 638 cents on a bill of 4,250 cents. The same command runs
scripts, and scala repl answers one line of code at a time.
This lesson writes that first program, shows what it does when its arguments are wrong, runs a script, explores types
in the REPL, and explains the compile server that keeps scala run quick on your computer, and why the runs in this
track switch it off.
@main turns a method into a program with typed arguments
Here is the whole program. The arithmetic is an ordinary function, and the method marked @main reads the bill and
the percentage from the command line:
// A tip calculator: the arithmetic is a plain function, and @main turns the rest into a program.
def tipCents(billCents: Long, percent: Int): Long =
(billCents * percent + 50) / 100
@main def tip(billCents: Long, percent: Int): Unit =
val t = tipCents(billCents, percent)
println(s"Tip: $t cents. Total: ${billCents + t} cents.") scala run . compiles every Scala file in the current folder and starts the program, and everything after -- goes
to the program as its arguments. The session runs it four times, with good arguments, with a word where a number
belongs, with one argument missing and with one too many:
from session import run
# The tip program with good arguments, with one that does not parse, with one missing and with one too many.
run("""
cd tip
scala run . --server=false -- 4250 15
scala run . --server=false -- 4250 fifteen
scala run . --server=false -- 4250
scala run . --server=false -- 4250 15 99
""") Output
$ cd tip $ scala run . --server=false -- 4250 15 Tip: 638 cents. Total: 4888 cents. $ scala run . --server=false -- 4250 fifteen Illegal command line after first argument: java.lang.NumberFormatException: For input string: "fifteen" $ scala run . --server=false -- 4250 Illegal command line after first argument: more arguments expected $ scala run . --server=false -- 4250 15 99 Tip: 638 cents. Total: 4888 cents.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tip_session.py
The first run worked: (4250 * 15 + 50) / 100 is 638 in whole-number arithmetic, so the tip rounds to the nearest
cent. The arguments arrive as text, and the @main method: A method marked @main, which the compiler turns into a program: a class with the method's name and a static main method that converts each command-line argument to its parameter's type. converts each one to its
parameter’s type before your code runs: "4250" to a Long and "15" to an Int. When a conversion fails, the
program prints Illegal command line with the reason, here a NumberFormatException for "fifteen", and when an
argument is missing it says more arguments expected. The fourth run shows a quieter case: the extra 99 was ignored.
Step through what happened inside the first run:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Compile the folder. Scala CLI compiles tip.scala. Because def tip is marked @main, the compiler also writes a class named tip with the static main method that the JVM starts.
- Start the program. Scala CLI starts a JVM on the class tip and hands it the words after -- as an array of two strings.
- Convert the first word. The generated main converts "4250" to the type of the first parameter, Long, with the FromString instance that Scala provides for Long.
- Convert the second word. "15" becomes an Int for the parameter percent. Both conversions worked, so your method can run.
- Call your method. The generated main calls tip with the converted values. From here on it is your code.
- Print the tip. tipCents gives (4250 * 15 + 50) / 100 = 638, and the program prints Tip, 638 cents, and a total of 4888 cents.
- A word that is not a number. In the second run the second word is "fifteen". Its conversion to Int fails, so the generated main prints Illegal command line with the NumberFormatException, and your method never runs.
- Status 0 all the same. After printing the message the generated main returns normally, so the program ends with exit status 0, as if it had worked.
Trusting the exit status after bad arguments
None of the four runs printed [exit status 1]: on Scala 3.9.0 the generated main reports a bad or missing
argument and then ends normally, with status 0, so a script that tests the exit status sees success. When a caller
must be able to tell, read the arguments as text and check them yourself, then end with sys.exit(1) on a mistake.
The conversion works for every parameter type that has a FromString instance in scala.util.CommandLineParser:
String, Boolean and the number types come with one, and your own types can get one too.
Key idea
The parameters of an @main method are the program’s command-line arguments, converted to their types; a bad or
missing argument prints Illegal command line, an extra one is ignored, and the exit status stays 0.
Checkpoint
Check: @main programs and their arguments
scala run compiles the whole folder, not one file
scala run . takes the folder, not a file, so it compiles every .scala and .sc file in it. Put the tip program
and a hello program in one folder, and the command no longer knows which program to start:
from session import run
# One folder with two @main methods: scala run . must be told which program to start.
run("""
cd two-mains
!scala run . --server=false
scala run . --server=false --main-class hello
""") Output
$ cd two-mains $ scala run . --server=false [error] Found several main classes: hello, tip You can run one of them by passing it with the --main-class option, e.g. scala run . --server=false --main-class hello You can pick the main class interactively by passing the --interactive option. scala run . --server=false --interactive [exit status 1] $ scala run . --server=false --main-class hello Hello from Scala 3.9.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 mains_session.py
Each @main method made a program named after the method, hello and tip, and Scala CLI stops rather than guess.
--main-class hello chooses one; --interactive, which the message also offers, asks you to pick from a list, which
works at a keyboard but not in a script or CI. A .sc script left in the same folder is compiled along with the rest,
so a forgotten file with a mistake in it stops the run too. While you learn, keep one program per folder, or name the
file, as in scala run tip.scala, which compiles that file alone.
Checkpoint
Check: What scala run . compiles
A script runs from top to bottom
A script: A .sc file whose top-level statements run from top to bottom, with the command-line arguments in args. Scala CLI wraps it in a class with a main method before it runs. is a .sc file with statements at the top level and no @main method. Scala CLI
wraps the statements in a class with a main method of its own, runs them in order, and passes the words after --
in a value named args:
// A script: its statements run from top to bottom, and args holds the words after -- on the command line.
val expenses = List("train" -> 4250L, "hostel" -> 9800L, "food" -> 3675L)
for (name, cents) <- expenses do println(f"$name%-7s $cents%6d")
val total = expenses.map(_._2).sum
println(f"${"total"}%-7s $total%6d")
if args.nonEmpty then
println(s"Each of ${args.mkString(", ")} pays ${total / args.length} cents.") from session import run
# A script runs from top to bottom; the words after -- reach it as args.
run("""
cd script
scala run expenses.sc --server=false
scala run expenses.sc --server=false -- Asha Ben
""") Output
$ cd script $ scala run expenses.sc --server=false train 4250 hostel 9800 food 3675 total 17725 $ scala run expenses.sc --server=false -- Asha Ben train 4250 hostel 9800 food 3675 total 17725 Each of Asha, Ben pays 8862 cents.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 script_session.py
Without arguments the if at the end does nothing; with two names it splits the total. Look at the share: 17,725
cents between two people is 8,862.5, and dividing two whole numbers drops the half, so one cent is lost. Money in this
track is counted in whole cents, so that rounding is always visible.
Scripts suit quick jobs, such as adding up a list, renaming files or trying an idea. Move the code into .scala files
as soon as it needs tests, a second file that uses its functions, or a name of its own as a program: definitions in a
.scala file can be imported and tested, while a script runs once, from top to bottom.
Checkpoint
Check: Scripts
The REPL answers one line at a time
The REPL: Read-eval-print loop: a prompt that compiles and runs each line you type, then prints the value and its type, so you can try code without writing a program., short for read-eval-print loop, compiles and runs each line you type and prints what
it got. scala repl opens it, and :quit closes it. The session typed eight lines; each answer comes right after the
line it answers:
from session import repl
# The REPL in a terminal of its own: each line is typed after the scala> prompt, and the REPL answers it.
repl([
"val bill = 4250L",
"bill * 15 / 100",
":type bill * 15 / 100",
'val code: Int | String = "A1"',
"def tipCents(billCents: Long, percent: Int): Long = (billCents * percent + 50) / 100",
"tipCents(bill, 15)",
'List("train" -> 4250L, "food" -> 3675L)',
":quit",
]) Output
$ scala repl --server=false
Welcome to Scala 3.9.0 (25.0.4.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> val bill = 4250L
val bill: Long = 4250L
scala> bill * 15 / 100
val res0: Long = 637L
scala> :type bill * 15 / 100
Long
scala> val code: Int | String = "A1"
val code: Int | String = "A1"
scala> def tipCents(billCents: Long, percent: Int): Long = (billCents * percent + 50) / 100
def tipCents(billCents: Long, percent: Int): Long
scala> tipCents(bill, 15)
val res1: Long = 638L
scala> List("train" -> 4250L, "food" -> 3675L)
val res2: List[(String, Long)] = List(("train", 4250L), ("food", 3675L))
scala> :quit
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 repl_session.py
Read the answers as declarations. val bill: Long = 4250L says that bill is a Long, inferred from the L at the
end of 4250L. An expression that you do not name gets one: res0, then res1 and so on, which you can use in later
lines. :type prints only the type of an expression, without running it. Int | String is a union type, a value that
is either an Int or a String; the REPL is a quick way to see such types. A def answers with its signature, and
since Scala 3.8 the REPL pretty-prints results, which is why strings appear in quotes and Long values with their L.
The REPL forgets everything when you quit. For experiments you want to keep, an editor offers worksheets: a file whose
name ends in .worksheet.sc. Metals in VS Code evaluates it each time you save it, IntelliJ IDEA when you press its
run arrow, and both show each line’s result beside it.
Try it in your own REPL
Open scala repl and type bill * 15 / 100 with val bill = 4250, without the L. The type changes from Long to
Int. Then type :type 4250L * 1.5 and see which type a Long times a Double gives.
Checkpoint
Check: The REPL
The compile server is fast once it is running, and needs a download first
On your computer, scala run compiles through a compile server: A background JVM that keeps a warm Scala compiler between builds. Scala CLI uses Bloop by default and downloads it on first use; --server=false compiles inside the scala command instead. called Bloop: a
background JVM that keeps a warm compiler between runs, so a second run after a small change compiles quickly. Scala
CLI downloads Bloop the first time it needs it, starts it, and leaves it running after the command ends;
scala --power bloop exit stops it. --server=false skips the server: the scala command starts a fresh compiler
for that one run, which starts cold every time, ends with the command and prints the compiler’s own messages
unchanged. The Scala CLI documentation suggests it for setups with limited web access, and that is the case the
session below shows: a computer that has never downloaded Bloop, with no network:
from session import run
# Without network access: the first run with the compile server needs Bloop, which has never been downloaded here.
run("""
cd hello
!scala run .
scala run . --server=false
""", downloads="first") Output
$ cd hello $ scala run . Downloading https://repo1.maven.org/maven2/ch/epfl/scala/bloop-frontend_2.12/2.1.1/bloop-frontend_2.12-2.1.1.pom Failed to download https://repo1.maven.org/maven2/ch/epfl/scala/bloop-frontend_2.12/2.1.1/bloop-frontend_2.12-2.1.1.pom [10 more lines of download attempts not shown] Error: java.lang.Exception: Error getting Bloop class path For more details, please see '.scala-build/stacktraces/<n>.log' [exit status 1] $ scala run . --server=false Hello from Scala 3.9.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 server_session.py
The first command tried to download Bloop from Maven Central, every attempt failed, and it stopped with
Error getting Bloop class path. The second compiled and ran the same program with --server=false, from what was
already on the computer. Step through both kinds of run:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- First run, online. The first run that needs the server downloads Bloop from Maven Central, once, and starts it as a JVM of its own in the background.
- Compile in the server. Scala CLI asks Bloop to compile the folder. The compiler now stays loaded in the server between runs.
- Run the program. With the classes compiled, Scala CLI starts your program in a JVM of its own, as in every run.
- The server stays. The scala command ends, but Bloop keeps running, ready for the next run. scala --power bloop exit stops it.
- The next run. A second run finds the server warm, so it compiles only what changed since the last run, and the program starts sooner.
- Offline, never downloaded. On a computer that has never downloaded Bloop and has no network, every download fails, and scala run stops with Error getting Bloop class path.
- --server=false. With --server=false the scala command starts a fresh compiler for this one run, cold each time but with nothing to download, and gone when the run ends. The program runs as before.
So leave the server on for your own work: after its first download it makes every later run in a session faster. Turn
it off with --server=false on a computer without network access, in a CI job that should leave no background process
behind, and whenever you want the compiler’s messages exactly as it prints them, which is why every run in this track
uses it.
Checkpoint
Check: The compile server
Practise: split a bill
The program below shares a bill equally among some people and says how many cents are left over. Both functions
return 0 for now, and a second @main method, checks, tests them on four cases. Copy the folder, write the two
functions, and run scala run . --main-class checks until all four pass; then try scala run . --main-class split -- 17725 3 on your own numbers:
// Practise: share a bill equally. Write the two functions so that every check passes, then run the checks with
// scala run . --main-class checks
def shareCents(totalCents: Long, people: Int): Long = 0
def leftoverCents(totalCents: Long, people: Int): Long = 0
@main def split(totalCents: Long, people: Int): Unit =
val share = shareCents(totalCents, people)
val left = leftoverCents(totalCents, people)
println(s"Each of $people people pays $share cents, and $left cents are left over.")
@main def checks(): Unit =
val cases = List((17725L, 2, 8862L, 1L), (17725L, 3, 5908L, 1L), (900L, 4, 225L, 0L), (5L, 7, 0L, 5L))
val results =
for (total, people, share, left) <- cases yield
val ok = shareCents(total, people) == share && leftoverCents(total, people) == left
println(s"${if ok then "ok " else "FAIL"} $total cents among $people: share $share, left over $left")
ok
println(s"${results.count(ok => ok)} of ${results.size} checks pass")
if results.contains(false) then sys.exit(1) As given, every check fails, the checks end with status 1, and the program itself prints nonsense:
from session import run
# The practice program as given: both functions return 0, so the checks fail and the program reports nonsense.
run("""
cd split
!scala run . --server=false --main-class checks
scala run . --server=false --main-class split -- 17725 3
""") Output
$ cd split $ scala run . --server=false --main-class checks FAIL 17725 cents among 2: share 8862, left over 1 FAIL 17725 cents among 3: share 5908, left over 1 FAIL 900 cents among 4: share 225, left over 0 FAIL 5 cents among 7: share 0, left over 5 0 of 4 checks pass [exit status 1] $ scala run . --server=false --main-class split -- 17725 3 Each of 3 people pays 0 cents, and 0 cents are left over.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 split_session.py
First hint: whole-number division / drops the remainder, and % gives exactly what it dropped, so 17,725 among 3 is
17725 / 3 cents each and 17725 % 3 cents left over.
Then put the five ideas of this lesson to work in thirteen short questions, from a program’s arguments to the compile server; each one shows why its answer is right as soon as you check it:
Practice that adapts to you
Practise this lesson
13 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What does @main do in Scala 3, and how is it different from writing def main(args: Array[String]) yourself?
@main on a method tells the compiler to generate a program: a class named after the method, with the standard
public static void main(String[]) that the JVM starts. The generated main converts each command-line argument to
the type of the matching parameter, using a FromString instance from scala.util.CommandLineParser, and calls your
method. A bad or missing argument makes it print Illegal command line with the reason, and it ignores extra
arguments. With def main(args: Array[String]) inside an object you get the raw strings and do every conversion and
check yourself, which gives you full control over messages and exit status, and it is the form that also compiles with
Scala 2. @main is the shorter and recommended way for Scala 3 programs; the hand-written form suits tools that need
their own argument handling.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your CI runs
scala testandscala runin containers without internet access. After the image was rebuilt, the jobs fail withError getting Bloop class path. Explain the cause and give two fixes, with their costs. -
Fresher to mid
You are writing a small command-line tool in Scala 3 that other scripts call, for example
split 17725 3. Design its entry point: an @main method with typed parameters, or your own argument handling? Cover error messages, exit status and testing.
Key takeaways
- An
@mainmethod is a program:scala run . -- 4250 15compiles the folder and converts each argument to the type of its parameter. - A bad or missing argument prints
Illegal command line, an extra one is ignored, and the exit status stays 0, so check arguments yourself when callers rely on the status. scala run .compiles every file in the folder and refuses to guess between two@mainmethods; choose with--main-class, or keep one program per folder.- A
.scscript runs its statements from top to bottom with its arguments inargs; code that needs tests belongs in.scalafiles. - The REPL prints the name, type and value of every line,
:typeshows a type without running anything, and worksheets keep experiments in a file. - The compile server, Bloop, keeps a warm compiler after a first download;
--server=falsestarts a fresh compiler for each run, leaves nothing running and needs no download of Bloop.
Terms in this lesson
- @main method
- A method marked @main, which the compiler turns into a program: a class with the method's name and a static main method that converts each command-line argument to its parameter's type.
- compile server
- A background JVM that keeps a warm Scala compiler between builds. Scala CLI uses Bloop by default and downloads it on first use; --server=false compiles inside the scala command instead.
- REPL
- Read-eval-print loop: a prompt that compiles and runs each line you type, then prints the value and its type, so you can try code without writing a program.
- script
- A .sc file whose top-level statements run from top to bottom, with the command-line arguments in args. Scala CLI wraps it in a class with a main method before it runs.
References
- Scala 3 Book: main methods (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Book: the REPL (Scala Center and EPFL (docs.scala-lang.org))
- Scala 3 Book: worksheets (Scala Center and EPFL (docs.scala-lang.org))
- Scala CLI run command (VirtusLab (Scala CLI))
- Scala CLI scripts (VirtusLab (Scala CLI))
- Scala CLI compile command (Bloop and --server=false) (VirtusLab (Scala CLI))
- Scala CLI and Bloop (VirtusLab (Scala CLI))
- Scala 3.8 announcement (the REPL as a separate artifact, pretty-printed results) (The Scala Programming Language (scala-lang.org))
Report a problem with this lesson
Kept only in this browser. Your Learn progress