Scala Programming Guidesscala-basicsscala-tutorial

Values, Types, and the Basics | Scala Programming Guide

By Dmitri Meshin
Picture of the author
Published on
Values, Types, and the Basics - Scala Programming Guide

val vs var: Immutability as a Default

In most programming languages, mutability is the default. You declare a variable, you change it, you move on. Scala flips this: immutability is the default, and you must explicitly opt into mutation. This is not a minor syntactic choice—it fundamentally changes how you reason about your code.

// CORRECT: Using val for immutable bindings
val playlistSize = 50
val artistNames = List("The Midnight", "Carpenter Brut", "M83")
val durationMinutes = 120.5

// Later in your code:
// playlistSize = 51  <- This WON'T COMPILE. val cannot be reassigned.

Why does this matter? Consider a multi-threaded scenario—something you encounter constantly in real applications:

// WRONG: Using var opens the door to bugs
var userScore = 0

// Thread A increments the score
userScore += 10  // Now it's 10

// Thread B reads and increments the score
userScore += 5   // Now it's... 10? 15? 5? Undefined behavior.

With multiple threads accessing the same mutable variable, race conditions emerge. Thread A reads the value, Thread B reads the same value before A writes, and both threads write back their incremented results—one of the increments is lost. This is a subtle but devastating category of bugs. Testing doesn't catch it reliably because race conditions are timing-dependent and non-deterministic.

With val, this problem vanishes:

// CORRECT: Using val with immutable data structures
var userScore = 0  // We only use var here as a placeholder

// Instead, we create *new* values:
val currentScore = 0
val updatedScore = currentScore + 10
val finalScore = updatedScore + 5

// Each thread sees consistent values. No corruption.

By using immutable values, each transformation produces a new value. There's no shared mutable state for threads to race over. This doesn't just prevent bugs—it makes code easier to reason about. When you see a val, you know it won't change. When you refactor that code, you know you can move it around and not worry about its value being different elsewhere.

In practice, you'll use val 95% of the time. var exists for state that genuinely must change (rare), and even then, you'll usually hide it behind a more sophisticated abstraction like a mutable collection or an actor. The discipline of reaching for val first forces you to think: "Do I actually need to mutate this?" Often, you don't.

Rule of thumb: Start with val. If the compiler complains that you need reassignment, then use var. Odds are, you won't need it. The fact that immutability is the default, and mutation is opt-in, aligns with modern best practices around state management.

Basic Types: The Foundation

Scala inherits all of Java's primitive types, plus a few refinements. Understanding these types is essential because the type system is your first line of defense against bugs—many errors that would manifest at runtime in Python or JavaScript are caught at compile time in Scala.

// Integer types (whole numbers)
val byteValue: Byte = 127          // 8-bit signed integer (-128 to 127)
val shortValue: Short = 32000      // 16-bit signed integer
val intValue: Int = 2_000_000      // 32-bit signed integer (default for whole numbers)
val longValue: Long = 9_000_000_000L  // 64-bit signed integer (suffix L required)

// Floating-point types
val floatValue: Float = 3.14f      // 32-bit floating point (suffix f required)
val doubleValue: Double = 3.14159  // 64-bit floating point (default for decimals)

// Boolean and character
val isActive: Boolean = true       // Either true or false
val firstLetter: Char = 'S'        // Single character (single quotes)

// Strings
val title: String = "Scala Essentials"  // Text (double quotes)
val singleQuoteString: String = "It's a 'great' language"

// When you don't specify a type, Scala infers it
val inferredInt = 42               // Scala knows this is Int
val inferredString = "hello"       // Scala knows this is String

Notice the numeric suffixes: L for Long, f for Float. These are necessary because 2_000_000_000L and 2_000_000_000 are different types, and Scala won't guess which you want. The underscore in 2_000_000 is just for readability—the compiler strips it.

Type annotations are optional but encouraged for public APIs and complex expressions. In local code, the compiler can usually figure it out, but being explicit about return types in function signatures is a best practice:

// These are equivalent, but the first is clearer for public methods
def calculateTotal(items: Int): Double = items * 19.99
def calculateTotal(items: Int) = items * 19.99  // Compiler infers Double

// For local variables, inference is fine
val result = 42 + 58  // Clearly an Int, no annotation needed

When you're reading someone else's code, knowing the type of a function's return value matters. Explicit annotations make that obvious. For local variables that are reassigned once and never passed elsewhere, type inference is readable enough.

Type Inference: Let the Compiler Work

Scala's type inference is among the best in the industry. The compiler has sophisticated algorithms for figure out types from context, letting you write clean code without drowning in type annotations. This isn't just convenience—it's a philosophy: express what you mean without bureaucracy.

// No type annotations needed here:
val numbers = List(1, 2, 3, 4, 5)  // Scala infers List[Int]
val names = List("Alice", "Bob", "Charlie")  // Scala infers List[String]

// This works with complex expressions too:
val doubled = numbers.map(_ * 2)  // Returns List[Int] without annotation

// Nested structures:
val data = Map(
  "count" -> 42,
  "active" -> true,
  "name" -> "Synthwave"
)
// Scala infers Map[String, Any] - it finds the common type for all values

The magic here is that the compiler isn't guessing. It's analyzing each expression and determining types compositionally. The map function has a signature that tells the compiler what type goes in and what type comes out. From that, everything flows.

Sometimes you need to guide the compiler. When a collection mixes types, Scala picks the common supertype:

// Ambiguous: is this a List of Int, a List of Double, or mixed?
val nums = List(1, 2.5, 3)  // Scala picks List[Double] (common supertype)

// Explicit when you mean something specific:
val onlyInts: List[Int] = List(1, 2, 3)

If you tried List(1, 2.5, 3) and wanted List[Double], you'd get it without annotation because Scala climbs to the common type. But if you wanted List[Number] or some other supertype, you'd need to specify it. The inference engine is smart but not telepathic.

String Interpolation: Three Flavors

Scala gives you three powerful ways to build strings, each with a purpose. String interpolation eliminates the need for + concatenation + or cumbersome printf formatting.

// 1. The 's' prefix: simple string interpolation
val artist = "M83"
val year = 2011
val message = s"$artist released their hit in $year"  // "M83 released their hit in 2011"

// You can embed expressions, not just variables:
val trackCount = 12
val totalSeconds = 2850
val formatted = s"Album has $trackCount tracks totaling ${totalSeconds / 60} minutes"
// "Album has 12 tracks totaling 47 minutes"

// 2. The 'f' prefix: formatted strings with printf-style specs
val price = 19.99
val quantity = 5
val formatted = f"Price: $$${price}%.2f, Qty: $quantity%d"
// "Price: $19.99, Qty: 5"

val temperature = 72.456
val tempString = f"Temperature: $temperature%.1f°C"  // "Temperature: 72.5°C"

// 3. The 'raw' prefix: no escape sequence processing
val filePath = "C:\\Users\\Alice\\Music"
val rawPath = raw"Path: $filePath"  // Backslashes are literal, no escaping needed
// "Path: C:\Users\Alice\Music"

Use s"" by default—it's the most readable. Use f"" when you need specific formatting like decimal places or field widths. raw"" is rare but handy for file paths, URLs, and regex patterns where backslashes shouldn't be interpreted as escape sequences.

These aren't just syntax sugar. They compile to efficient string building operations, and they're far more readable than the alternatives. A line like s"User $name requested $action at $time" immediately conveys meaning, whereas "User " + name + " requested " + action + " at " + time requires more visual parsing.

Tuples: Quick Multi-Value Containers

Sometimes you need to group a few values together without creating a full class. That's what tuples are for. They're lightweight, immutable containers that hold a fixed number of elements, each with its own type. Tuples are perfect for returning multiple values from a function or grouping related data temporarily.

// Create tuples with parentheses
val songMetadata = ("Midnight City", "M83", 244)  // (String, String, Int)
val coordinates = (40.7128, -74.0060)             // (Double, Double)
val mixed = ("Alice", 25, true)                   // (String, Int, Boolean)

// Access elements by position (1-indexed, not 0-indexed)
val title = songMetadata._1   // "Midnight City"
val artist = songMetadata._2  // "M83"
val duration = songMetadata._3  // 244

// Destructure tuples to extract values
val (songTitle, artistName, durationSeconds) = songMetadata
println(s"$songTitle by $artistName is $durationSeconds seconds")

// Use tuples as lightweight return values
def analyzeTrack(filename: String): (String, Int, Double) = {
  // Read file, process audio, extract metadata...
  ("Untitled", 180, 128.5)
}

val (name, duration, bpm) = analyzeTrack("song.mp3")

The tuple's type is the cartesian product of its elements' types. A (String, Int, Double) is a tuple with exactly three elements—first a String, then an Int, then a Double. If you try to access _4 on a three-element tuple, you get a compile error. This type safety is powerful: the compiler ensures you're not accessing elements that don't exist.

Note the 1-indexed access (_1, _2, _3). This comes from Scala's functional programming heritage, where tuples are based on mathematical tuples. It takes getting used to, but it's consistent across the language.

Named tuples (Scala 3) make tuples more readable:

// Old style (Scala 2 and 3)
val result = (100, "Success", 0.95)
val statusCode = result._1

// New style (Scala 3 with named fields)
val result = (code = 100, message = "Success", confidence = 0.95)
val statusCode = result.code   // Much clearer!

Named tuples make the intent obvious. When you see result.code, you know exactly what that value represents, whereas result._1 requires you to remember the tuple's structure.

The Type Hierarchy: Understanding Any, AnyVal, AnyRef, Nothing, and Null

Scala's type system has a hierarchy. Everything inherits from a single root type. Understanding this hierarchy is crucial because it explains how types relate to each other and why certain operations are or aren't valid.

                          Any
                      /         \
                 AnyVal          AnyRef
              /    |    \         /    \
            Int  Double Boolean String Class...
           Byte  Float   Char    List
          Short Long            Array ...

Any is the parent of all types. It's the root of the entire type hierarchy. You'll rarely reference it directly, but it's useful to know. When you have a collection that could contain anything, you might use List[Any]:

val values: List[Any] = List(42, "hello", 3.14, true)  // Heterogeneous list
// Each element is an Any, but we've lost type information about individual elements

This is powerful when you need to group disparate types together. The tradeoff is that you lose type information—later, you'd need to check the type of each element before using it. This is typically avoided in well-typed Scala code, but it's an option when necessary.

AnyVal is the parent of all "value" types—the primitives. When you use Int, Double, Boolean, they compile to JVM primitives with no object wrapper. This is crucial for performance: an array of 1 million Ints is 4 million bytes, not millions of pointer-to-object overhead. JVM primitives are fast because they're represented directly in memory.

AnyRef is the parent of all reference types (classes, arrays, etc.). This maps to Java's Object. Everything that isn't a primitive is an AnyRef—strings, lists, user-defined classes, all of it.

This matters because:

val num = 42
val text = "hello"

// Both are AnyVal (no wrapper objects, pure primitives)
val list: List[AnyVal] = List(num, text)

// Mixed types go through AnyRef
val mixed: List[AnyRef] = List("string", Array(1, 2, 3), new Object())

When you write List[Int], you get a list of primitives (represented efficiently). When you write List[String], you get a list of references to String objects. This distinction underlies a lot of Scala's performance characteristics.

Unit, Null, and Nothing are special:

// Unit is Scala's "void"—a value that carries no information
def greet(name: String): Unit = {
  println(s"Hello, $name")
  // No return statement needed; the last expression (println) is evaluated,
  // then Unit is returned implicitly
}

// Null is the type of the null reference (like Java)
val nothing: String = null  // Compiles, but not recommended

// Nothing is the type of expressions that never return
def crash(): Nothing = {
  throw new RuntimeException("Unrecoverable error")
}

def infiniteLoop(): Nothing = {
  while (true) {}
}

// Nothing is useful for type inference in edge cases
val emptyList: List[Nothing] = List()  // A list of no-type (truly empty)

Unit is Scala's equivalent to Java's void. It represents the absence of meaningful information. Functions that execute side effects (like printing) often return Unit. Unlike Java's void, Unit is an actual value (the single value ()), which enables interesting functional patterns.

Null is the type of the null reference. In Java, null is famously a source of errors—the "billion-dollar mistake," according to Tony Hoare who invented it. Scala discourages null in favor of Option[T] (which we'll explore later), but null exists for Java interop.

Nothing is the most interesting: it's the type of expressions that never return normally. If a function throws an exception, it returns Nothing. If it loops forever, it returns Nothing. In type theory, Nothing is the bottom type—it's a subtype of everything. This is useful: if you have List[Nothing], it can be assigned to List[String] because Nothing is a subtype of String. This is how empty lists work.

Type Conversions and Type Casting

Scala doesn't have implicit widening conversions like Java. You must be explicit:

val small: Int = 42
// val big: Long = small  <- This WON'T COMPILE
val big: Long = small.toLong  // Explicit conversion

val decimal: Double = small.toDouble  // 42.0
val text: String = small.toString  // "42"

// Narrowing conversions can lose data
val original: Double = 42.7
val truncated: Int = original.toInt  // 42 (fractional part lost)

This explicitness prevents silent bugs. In Java, int x = 5; long y = x; works silently, but long x = 5L; int y = x; doesn't compile. The first is widening (no data loss), the second is narrowing (potential data loss). Java allows the first implicitly but not the second. Scala requires both to be explicit, which is more consistent and less error-prone.

For pattern matching and instance checks:

def describe(value: Any): String = value match {
  case i: Int => s"Integer: $i"
  case s: String => s"String: $s"
  case d: Double => s"Double: $d"
  case _ => "Unknown type"
}

describe(42)        // "Integer: 42"
describe("hello")   // "String: hello"
describe(3.14)      // "Double: 3.14"

This is type casting, but safer. Instead of (int) obj, you use pattern matching to both check the type and extract the value. If the check fails, you simply don't enter that case—no exception.

Type Hierarchy Diagram

Here's a visual representation of Scala's type system:

graph TD
    Any["<b>Any</b><br/>Root type<br/>of all types"]

    AnyVal["<b>AnyVal</b><br/>Value types<br/>no object wrapper"]
    AnyRef["<b>AnyRef</b><br/>Reference types<br/>inherits from Object"]

    Int["Int<br/>32-bit signed"]
    Long["Long<br/>64-bit signed"]
    Double["Double<br/>64-bit float"]
    Float["Float<br/>32-bit float"]
    Boolean["Boolean<br/>true/false"]
    Char["Char<br/>Unicode char"]
    Byte["Byte<br/>8-bit signed"]
    Short["Short<br/>16-bit signed"]
    Unit["Unit<br/>void-like"]

    String["String<br/>text"]
    Array["Array[T]<br/>fixed size"]
    List["List[T]<br/>immutable list"]
    Null["Null<br/>null value"]
    Nothing["<b>Nothing</b><br/>Never returns"]

    Any --> AnyVal
    Any --> AnyRef
    Any --> Null
    Any --> Nothing

    AnyVal --> Int
    AnyVal --> Long
    AnyVal --> Double
    AnyVal --> Float
    AnyVal --> Boolean
    AnyVal --> Char
    AnyVal --> Byte
    AnyVal --> Short
    AnyVal --> Unit

    AnyRef --> String
    AnyRef --> Array
    AnyRef --> List

    style Any fill:#e1f5ff
    style AnyVal fill:#f3e5f5
    style AnyRef fill:#f3e5f5
    style Nothing fill:#ffebee
    style Null fill:#fce4ec