Skip to main content
Scala beginner Lesson 1 of 10

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.

Frequently Asked Questions

What is the difference between val and var in Scala?
`val` binds a name once and cannot be reassigned; `var` can be. Idiomatic Scala uses `val` almost everywhere, because immutable bindings remove a whole class of bugs and are safe to share between threads.
Do I need sbt to run Scala?
No. `scala-cli` runs a single `.scala` file, resolves dependencies from a directive at the top, and starts a REPL — no build file. sbt is worth adding when a project has many modules or a publishing pipeline.
Does Scala need semicolons and braces?
Semicolons are optional and idiomatically omitted. Scala 3 also allows significant indentation instead of braces, which is what this track uses; the brace syntax remains valid and you will meet it in older code.
Is everything an expression in Scala?
Almost. `if`, `match`, and blocks all evaluate to a value, so you write `val x = if cond then a else b` rather than assigning inside branches. Statements that produce nothing have type `Unit`.