Your First Scala Program
Run Scala with one command, see why val beats var, and read the type the compiler inferred when you did not write one.
Scala runs on the JVM, is statically typed, and lets you write in a functional or an
object-oriented style. This track uses Scala 3 and scala-cli, which runs a file
directly — no build tool, no project scaffolding.
Setting up
brew install Virtuslab/scala-cli/scala-cli # or see scala-cli.virtuslab.org
scala-cli version
Scala CLI version: 1.5.4
Scala version (default): 3.6.2
The first program
//> using scala 3.6.2
@main def run(): Unit =
val name = "Scala 3"
println(s"Hello, $name")
println(s"2 + 2 = ${2 + 2}")
scala-cli run hello.scala
Compiling project (Scala 3.6.2, JVM (21))
Compiled project (Scala 3.6.2, JVM (21))
Hello, Scala 3
2 + 2 = 4
Four things in six lines. @main marks the entry point — no object Main extends App
ceremony. s"..." is an interpolated string, and ${...} takes any expression. The
//> using comment is a scala-cli directive; it pins the Scala version inside the source
file, which is why this file is self-contained.
The compile step happens once; a second run is instant.
val, var, and why it matters
@main def run(): Unit =
val greeting = "hello"
var counter = 0
counter += 1
println(s"$greeting, counter is $counter")
greeting = "goodbye"
-- [E052] Type Error: reassignment.scala:9:2 -------------------------
9 | greeting = "goodbye"
| ^^^^^^^^^^^^^^^^^^^^
| Reassignment to val greeting
1 error found
Caught at compile time. val is the default in idiomatic Scala and var is the exception —
not for purity’s sake, but because an immutable binding is safe to pass anywhere, including
to another thread, without wondering who else can change it.
Types you do not have to write
@main def run(): Unit =
val n = 42
val pi = 3.14159
val name = "Ada"
val flag = true
val nothing = ()
println(s"$n is an Int, $pi is a Double, $name is a String, $flag is a Boolean")
val total: Long = 9_000_000_000L
val price: BigDecimal = BigDecimal("25.50")
println(s"$total, $price")
42 is an Int, 3.14159 is a Double, Ada is a String, true is a Boolean
9000000000, 25.50
The compiler infers each type from the right-hand side. Inference is for local values — annotate anything public, because the inferred type of a method is part of its API and you do not want it changing when you edit the body.
BigDecimal for money, as everywhere else: 0.1 + 0.2 is not 0.3 in binary floating
point on any platform.
Everything is an expression
@main def run(): Unit =
val hour = 14
val part = if hour < 12 then "morning" else if hour < 18 then "afternoon" else "evening"
println(part)
val label = hour match
case h if h < 6 => "night"
case h if h < 12 => "morning"
case h if h < 18 => "afternoon"
case _ => "evening"
println(label)
val computed =
val a = 3
val b = 4
math.sqrt(a * a + b * b)
println(computed)
afternoon
afternoon
5.0
if returns a value, so there is no need to declare a variable and assign into both
branches — and no way to forget one. A block evaluates to its last expression, which is why
computed is 5.0 and the intermediate a and b stay scoped inside it.
Note the indentation: Scala 3 accepts significant indentation with then and =, no braces
required. Braces still work, and older code uses them.
Methods
def shippingCost(weightKg: Double, express: Boolean = false): Double =
val base = if weightKg < 2 then 3.99 else 6.99
val adjusted = if weightKg >= 10 && !express then 0.0 else base
BigDecimal(adjusted * (if express then 2.5 else 1.0))
.setScale(2, BigDecimal.RoundingMode.HALF_UP)
.toDouble
@main def run(): Unit =
println(shippingCost(1.5))
println(shippingCost(12))
println(shippingCost(1.5, express = true))
println(shippingCost(weightKg = 12, express = true))
3.99
0.0
9.98
17.48
The return type is annotated (: Double) and there is no return keyword — the last
expression is the result. Default parameters and named arguments both work, and naming the
argument at the call site is worth doing for a bare true that would otherwise mean nothing
to a reader.
The REPL
scala-cli repl
Welcome to Scala 3.6.2 (21.0.5, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> val xs = List(1, 2, 3, 4, 5)
val xs: List[Int] = List(1, 2, 3, 4, 5)
scala> xs.map(_ * 2)
val res0: List[Int] = List(2, 4, 6, 8, 10)
scala> xs.filter(_ % 2 == 0).sum
val res1: Int = 6
scala> :type xs.map(_ * 2)
List[Int]
The REPL prints the type of everything, which makes it the fastest way to answer “what does
this expression actually return”. :type asks without evaluating.
Adding a dependency
//> using scala 3.6.2
//> using dep com.lihaoyi::upickle::4.0.2
import upickle.default.*
case class Order(orderId: Int, status: String, amount: Double) derives ReadWriter
@main def run(): Unit =
val order = Order(1001, "completed", 25.50)
val json = write(order)
println(json)
println(read[Order](json))
Compiling project (Scala 3.6.2, JVM (21))
Compiled project (Scala 3.6.2, JVM (21))
{"orderId":1001,"status":"completed","amount":25.5}
Order(1001,completed,25.5)
One comment line pulls a library from Maven Central. :: between the organisation and the
artifact means “use the Scala version I am compiling with” — Scala libraries are published
per compiler version.
Practice
1. Write a method that returns a discount band and call it with a named argument.
def band(total: Double, isMember: Boolean = false): String =
if total >= 100 && isMember then "gold"
else if total >= 100 then "silver"
else "none"
@main def run(): Unit =
println(band(120))
println(band(120, isMember = true))
println(band(total = 40, isMember = true))
silver
gold
none
The whole body is one if expression, so there is no intermediate variable and no branch
that can forget to assign one.
2. Try to reassign a val.
-- [E052] Type Error: --------------------------------------------
| Reassignment to val greeting
1 error found
A compile error, not a runtime one. Changing it to var makes it compile — and is worth
resisting, because a var in a method signature’s scope is something every later reader has
to track.
3. Use the REPL to find the type of an expression.
scala> :type List(1, 2, 3).map(_.toString)
List[String]
scala> :type List(1, 2, 3).sum
Int
scala> :type List(1, 2, 3).headOption
Option[Int]
headOption returning Option[Int] rather than Int is the language telling you the list
might be empty — the subject of lesson 4.
4. Compare a Double and a BigDecimal sum.
@main def run(): Unit =
println(0.1 + 0.2)
println(BigDecimal("0.1") + BigDecimal("0.2"))
println(0.1 + 0.2 == 0.3)
0.30000000000000004
0.3
false
The same binary floating-point behaviour as every other language. Use BigDecimal for money
and a tolerance for comparing doubles.
Next: collections — the part of the standard library you will use every day.