Variables and Basic Types in Scala: val vs var and Expression-Oriented Syntax
val and var look like a cosmetic naming choice until you internalize what each one actually commits you to — and once you do, a second, bigger idea falls into place: in Scala, if, for, match, and even a bare { } block all evaluate to a value, the same way 2 + 2 does. This single fact — that control structures are expressions, not statements — reshapes how you write nearly every function, and it’s the piece most newcomers coming from Java or Python have to unlearn their old habits around first.
val vs var — Immutability by Default
val name = "Alice"name = "Bob" // Error: reassignment to val
var counter = 0counter = counter + 1 // fine — var allows reassignmentval binds a name to a value permanently — once assigned, that binding can never point to a different value. var allows reassignment, exactly like a normal variable in most other languages. Scala’s own style guidance, and the overwhelming convention in idiomatic Scala code, is to default to val and reach for var only when mutation is genuinely the clearest way to express something — a running total inside a tight, self-contained loop, for instance.
This isn’t stylistic pickiness — it’s a practical safety property. A val can never be reassigned somewhere unexpected deep in a large function, which means reasoning about what a val’s value is at any point in the code is simpler: it’s always whatever it was assigned, full stop. A var reintroduces the class of bug immutable-by-default languages are specifically designed to eliminate — a value changing out from under code that assumed it wouldn’t.
// val doesn't mean the referenced object can't change internally — only the binding is fixedval numbers = scala.collection.mutable.ArrayBuffer(1, 2, 3)numbers += 4 // fine — the ArrayBuffer itself is mutable, even though `numbers` is a val// numbers = ArrayBuffer(5, 6) // Error — reassigning the binding itself is still forbiddenThis distinction — val fixes the binding, not necessarily the contents — matters the moment you use a mutable collection type, and it’s exactly analogous to final in Java or const in JavaScript, both of which have the identical caveat.
Basic Types
val age: Int = 30val price: Double = 19.99val initial: Char = 'A'val isActive: Boolean = trueval bigNumber: Long = 9_000_000_000Lval precise: Float = 3.14fval nothing: Unit = ()| Type | Size / notes |
|---|---|
Int | 32-bit signed integer — the default for whole numbers |
Long | 64-bit signed integer — needs an L suffix on literals |
Double | 64-bit floating point — the default for decimals |
Float | 32-bit floating point — needs an f suffix |
Boolean | true / false |
Char | A single UTF-16 character |
String | Text — technically java.lang.String under the hood, since Scala runs on the JVM |
Unit | The type of “no meaningful value” — Scala’s equivalent of void, but genuinely a type with one value, () |
Numeric literals can use underscores as visual separators (9_000_000_000L) — purely for readability, with zero effect on the actual value, exactly mirroring the same feature in Java and JavaScript.
Unit — Scala’s honest version of “no return value”
def printMessage(msg: String): Unit = { println(msg)}
val result: Unit = printMessage("hi") // result is genuinely a value: ()Unlike Java’s void, which is a special non-type carve-out meaning “there is nothing here,” Scala’s Unit is a real type with exactly one possible value, written (). This consistency (every expression, even one with no meaningful result, still produces some value of some type) is what lets if/for/blocks all be expressions uniformly — there’s no special “these don’t return anything” exception anywhere in the language.
Type Inference — Explicit Types Are Usually Optional
val x = 42 // inferred: Intval y = 3.14 // inferred: Doubleval name = "Scala" // inferred: Stringval items = List(1, 2, 3) // inferred: List[Int]
def double(n: Int) = n * 2 // parameter must be annotated; return type (Int) is inferredScala’s compiler infers types from context aggressively enough that explicit annotations on local variables are the exception rather than the rule in idiomatic code — the opposite convention from Java, where every variable declaration states its type. Function parameters are the one place annotations remain mandatory (there’s no value at the definition site to infer from), and function return types are technically optional but commonly written explicitly on public methods anyway, for the same “acts as a contract, catches accidental signature drift” reason covered in typed-language guides generally.
// Recursive functions are the one case where return type annotation is NOT optional — requireddef factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)The compiler cannot infer a recursive function’s return type from its own body (it would need to already know the type to type-check the recursive call, a chicken-and-egg problem) — this is a real, non-stylistic case where the annotation is mandatory, not just good practice.
Control Structures Are Expressions, Not Statements
// This is the idea that reshapes everything else in the languageval status = if (age >= 18) "adult" else "minor"In Java, if is a statement — it controls which block of code runs, but the if construct itself has no value; you have to assign inside each branch separately. In Scala, if/else is an expression: it evaluates to whichever branch’s value actually ran, and that value can be assigned directly, exactly as shown above.
// A block { } is also an expression — it evaluates to its LAST lineval discount = { val base = 0.1 val bonus = 0.05 base + bonus // this is the block's value — the whole block evaluates to 0.15}A { } block’s value is whatever its final line evaluates to — every earlier line is executed for its side effects or to compute intermediate vals, but only the last expression’s value “escapes” the block. This is why Scala functions are so often written as a single expression rather than a sequence of statements with an explicit return — the function body’s last line is the return value, with no return keyword needed (and return is in fact rarely used in idiomatic Scala at all).
// for loops CAN be expressions too, using "yield" — this is a preview of for-comprehensionsval doubled = for (n <- List(1, 2, 3)) yield n * 2// doubled: List[Int] = List(2, 4, 6)
// Without yield, a for loop is used purely for its side effect and produces Unitfor (n <- List(1, 2, 3)) println(n)for...yield transforms each element and collects the results into a new collection — this is the gateway into for-comprehensions, covered in full depth in their own dedicated guide later in this series, since they’re genuinely one of Scala’s most distinctive and useful features.
// match is an expression too — arguably the most powerful one in the languageval description = age match { case a if a < 13 => "child" case a if a < 20 => "teenager" case _ => "adult"}match (covered in full in its own guide) is Scala’s pattern matching construct, and like if and blocks, it evaluates to a value based on whichever case matched — this uniform “everything produces a value” design is what makes Scala code read as a series of composed expressions rather than a sequence of imperative steps mutating shared state.
String Interpolation
val name = "Alice"val age = 30
println(s"$name is $age years old") // s-strings: simple variable interpolationprintln(s"Next year, $name will be ${age + 1}") // ${} needed for expressions, not just bare variablesprintln(f"Pi is approximately ${math.Pi}%.2f") // f-strings: printf-style formattingprintln(raw"Line1\nLine2") // raw strings: no escape sequence processing at allThe s"..." prefix enables interpolation — $name substitutes a simple identifier directly, while any expression more complex than a bare variable name (age + 1) needs curly braces (${age + 1}) to disambiguate where the expression ends. The f"..." interpolator adds printf-style format specifiers (%.2f for two decimal places), and raw"..." disables escape sequence processing entirely — useful for regex patterns or Windows-style file paths where you don’t want \n interpreted as a newline.
Tuples — Quick, Anonymous Groupings
val point = (3, 4) // Tuple2[Int, Int]val person = ("Alice", 30, true) // Tuple3[String, Int, Boolean]
println(point._1) // 3 — 1-indexed, not 0-indexedprintln(point._2) // 4
val (x, y) = point // destructuring — x = 3, y = 4Tuples group a fixed, small number of values of possibly different types without needing a named class — useful for quick, local groupings (like returning two values from a function) where defining a full case class would be unnecessary ceremony for something used in exactly one place. Note the _1, _2 accessors are 1-indexed, a genuine Scala quirk that trips up people coming from almost any other language’s 0-indexed conventions.
Frequently Asked Questions
Should I ever use var? Sparingly — the idiomatic default is val, with var reserved for cases where mutation is genuinely the clearest local implementation detail (an accumulator inside a tight, self-contained loop that isn’t shared or observed from outside). Most Scala style guides and most experienced Scala codebases treat a var as something worth a second look during code review, not a forbidden construct.
Why does Scala make if an expression instead of a statement? Because it composes better — an expression can be nested, assigned, passed as an argument, or returned directly, while a statement can only be executed for its side effect. This uniformity is a deliberate design choice supporting Scala’s functional programming half, where composing small expressions into larger ones is the primary way programs are built.
Do I need a return keyword? Rarely — since a function’s body is itself an expression whose last line is automatically its result, explicit return is mostly unnecessary and, in fact, somewhat discouraged in idiomatic Scala, partly because return inside a nested function/lambda has surprising non-local-exit semantics that differ from what most developers expect coming from other languages.
What’s the actual difference between Int and Integer in Scala? There isn’t a separate Integer type the way Java has both int (primitive) and Integer (boxed object) — Scala’s Int unifies both roles at the language level, with the compiler handling boxing/unboxing to the JVM’s actual primitive int transparently where possible for performance.
Why are tuple accessors 1-indexed (_1, _2) when everything else in Scala is 0-indexed? This is a genuine historical inconsistency in the language, inherited from tuple conventions in some earlier functional languages. It’s worth simply memorizing as an exception rather than expecting it to follow the 0-indexed convention every collection type in Scala otherwise uses.
What’s Next
You now understand Scala’s core values-and-types vocabulary, and — more importantly — the expression-oriented mindset that makes the rest of the language click. Next: Functions in Scala, where this expression-based thinking extends into higher-order functions, currying, and treating functions themselves as ordinary values you can pass around.