Beginner

Variables and Types in Scala

Learn about variables, immutability, type inference, and the Scala type system with practical Docker-ready examples

Scala’s type system is one of its most distinctive features: it is statically and strongly typed, yet rarely forces you to write type annotations by hand. The compiler infers types throughout your code, eliminating boilerplate while catching errors at compile time. This combination—safety without ceremony—sits at the heart of the Scala philosophy.

Beyond inference, Scala makes a meaningful distinction that many languages skip: val for immutable bindings and var for mutable variables. Functional programming in Scala favors val heavily, and you will find that the majority of well-written Scala code reaches for val by default and only introduces var when mutation is genuinely necessary.

In this tutorial you will explore Scala’s core types, the val/var split, type inference and explicit annotations, type conversions, string interpolation, and Option as a safe replacement for null.

val vs var: Immutability First

Scala provides two keywords for naming values:

  • val — an immutable binding. Once assigned, the name cannot be rebound to a different value. This is the default choice in idiomatic Scala.
  • var — a mutable variable. It can be reassigned after its initial assignment.

Create a file named variables.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@main def variables(): Unit =
  // val: immutable — preferred in Scala
  val language = "Scala"
  val birthYear = 2004
  val versionNumber = 3.7
  val isStaticallyTyped = true

  println(s"Language: $language")
  println(s"Born: $birthYear")
  println(s"Version: $versionNumber")
  println(s"Statically typed: $isStaticallyTyped")

  // Explicit type annotations (optional when the compiler can infer)
  val count: Int = 42
  val pi: Double = 3.14159265
  val initial: Char = 'S'
  val message: String = "Hello, Scala!"

  println(s"Count: $count, Pi: $pi, Initial: $initial")
  println(s"Message: $message")

  // var: mutable — use only when mutation is necessary
  var counter = 0
  counter += 1
  counter += 1
  println(s"Counter: $counter")

The @main annotation marks variables as the program entry point — the Scala 3 equivalent of wrapping everything in object Main { def main(...) }. Scala CLI discovers and runs it automatically.

Scala’s Core Types

Scala’s primitive-equivalent types map directly to JVM primitives at runtime, so there is no boxing overhead for simple numeric operations:

TypeDescriptionExample literal
Int32-bit signed integer42
Long64-bit signed integer9876543210L
Double64-bit floating point3.14159
Float32-bit floating point3.14f
Booleantrue or falsetrue
CharSingle Unicode character'S'
StringSequence of characters (JVM)"Scala"
UnitNo meaningful return value (void)()

Unlike Java, Scala has no distinction between primitives and wrapper types in source code. You always write Int, never int or Integer. The compiler decides the JVM representation.

Type Conversions and String Interpolation

Scala does not perform implicit numeric widening — converting between numeric types always requires an explicit call. This prevents accidental precision loss. String interpolation, on the other hand, is built into the language with three distinct modes.

Create a file named type_conversions.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@main def typeConversions(): Unit =
  // Explicit conversions between numeric types
  val intValue = 42
  val asDouble = intValue.toDouble
  val asLong   = intValue.toLong
  val asString = intValue.toString

  println(s"Int:    $intValue")
  println(s"Double: $asDouble")
  println(s"Long:   $asLong")
  println(s"String: \"$asString\"")

  // Parsing strings into numbers
  val parsed      = "100".toInt
  val parsedFloat = "2.718".toDouble
  println(s"Parsed Int:    $parsed")
  println(s"Parsed Double: $parsedFloat")

  // s"..." — evaluates expressions inline
  val name    = "Scala"
  val version = 3
  println(s"$name version $version")
  println(s"${name.toUpperCase} was released in ${2004 + (version - 1) * 6} (approx)")

  // f"..." — printf-style formatting
  val pi = 3.14159
  println(f"Pi to 2 places: $pi%.2f")
  println(f"Padded integer: $version%5d")

  // raw"..." — no escape processing
  println(raw"Newline literal: \n stays as-is")

The three interpolator styles:

  • s"..." evaluates $name and ${expression} — the workhorse of Scala string building.
  • f"..." adds C-style format specifiers after the variable (%.2f, %5d).
  • raw"..." treats backslashes literally — useful for regex patterns and Windows paths.

Option: Null Safety Built In

Scala does not encourage using null. Instead, values that might be absent are wrapped in Option[T], a type with exactly two possible values: Some(value) when something is present and None when it is absent. This forces callers to handle the missing case explicitly, eliminating an entire class of null-pointer errors at compile time.

Create a file named option_types.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@main def optionTypes(): Unit =
  val found: Option[String]    = Some("Scala")
  val missing: Option[String]  = None

  // Pattern matching is the idiomatic way to unpack an Option
  found match
    case Some(v) => println(s"Found: $v")
    case None    => println("Not found")

  missing match
    case Some(v) => println(s"Found: $v")
    case None    => println("Value is absent")

  // getOrElse provides a fallback for the None case
  println(s"Present with default: ${found.getOrElse("default")}")
  println(s"Absent with default:  ${missing.getOrElse("default")}")

  // map transforms the value only when it is present
  val upper = found.map(_.toUpperCase)
  val skip  = missing.map(_.toUpperCase)
  println(s"Mapped present: $upper")
  println(s"Mapped absent:  $skip")

  // Option integrates with type inference
  val inferred = Some(2004)       // Option[Int]
  println(s"Inferred type holds: $inferred")

Option participates fully in Scala’s type system: map, flatMap, filter, and foreach all work on it, making it a natural fit for for-comprehensions and functional pipelines.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the Scala CLI image
docker pull virtuslab/scala-cli:latest

# Run the variables example
docker run --rm -v $(pwd):/app -w /app virtuslab/scala-cli:latest run variables.scala

# Run the type conversions example
docker run --rm -v $(pwd):/app -w /app virtuslab/scala-cli:latest run type_conversions.scala

# Run the Option example
docker run --rm -v $(pwd):/app -w /app virtuslab/scala-cli:latest run option_types.scala

Scala CLI compiles each file on first run and caches the result, so subsequent runs of the same file are significantly faster.

Expected Output

Running variables.scala:

Language: Scala
Born: 2004
Version: 3.7
Statically typed: true
Count: 42, Pi: 3.14159265, Initial: S
Message: Hello, Scala!
Counter: 2

Running type_conversions.scala:

Int:    42
Double: 42.0
Long:   42
String: "42"
Parsed Int:    100
Parsed Double: 2.718
Scala version 3
SCALA was released in 2016 (approx)
Pi to 2 places: 3.14
Padded integer:     3
Newline literal: \n stays as-is

Running option_types.scala:

Found: Scala
Value is absent
Present with default: Scala
Absent with default:  default
Mapped present: Some(SCALA)
Mapped absent:  None
Inferred type holds: Some(2004)

Key Concepts

  • Prefer val over var — immutable bindings are the default in idiomatic Scala; reach for var only when you have a clear reason to mutate.
  • Type inference is pervasive — you rarely need to write type annotations; the compiler deduces them from the right-hand side expression.
  • Explicit type annotations clarify intent — annotating public APIs and complex expressions improves readability even when the compiler does not require it.
  • No implicit numeric conversions — Scala requires explicit .toDouble, .toInt, etc., preventing silent precision loss across numeric boundaries.
  • Three string interpolatorss"..." for general use, f"..." for formatted output, raw"..." for literal backslash handling.
  • Option[T] replaces null — the type system encodes the possibility of absence, forcing safe handling at compile time rather than crashing at runtime.
  • All types are objectsInt, Double, and Boolean have methods (.toDouble, .toString, .abs) because Scala unifies primitive and reference types in its type hierarchy.
  • Unit is a real type — functions that perform side effects (like println) return Unit, making effect-ful code visible in the type signature.

Running Today

All examples can be run using Docker:

docker pull virtuslab/scala-cli:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining