</>
CodeLearn Pro
Start Learning Free โ†’

Scala โ€” Complete Beginner Tutorial

6 modules ยท 18 examples ยท Click โ–ถ Run on any example to see the output

โœฆ Beginner
โ† Overview

๐Ÿ’ก How to use this page

Read the explanation, study the code, then hit โ–ถ Run to see the output.

๐Ÿ“ฆ
Module 01

Variables & Types

Scala has two kinds of values: val (immutable โ€” like a constant, cannot be reassigned) and var (mutable โ€” can be changed). Prefer val in functional Scala โ€” changing state leads to bugs. Scala infers types automatically so you rarely need to write them explicitly.

val and var โ€” Immutable vs Mutable

val is like a constant โ€” once set it never changes. var can be reassigned. In Scala you will use val for almost everything. The compiler will warn you if you use var when val would work. Scala also infers the type from the value, so String, Int, Double and Boolean are all detected automatically.

Main.scalaScala
// val โ€” immutable, cannot be reassigned
val name: String = "Alice"   // explicit type
val age  = 25                // type inferred as Int
val pi   = 3.14159           // type inferred as Double
val isStudent = true         // type inferred as Boolean

// var โ€” mutable, CAN be changed
var score = 80
var level = 1

println(s"Name: $name")
println(s"Age: $age")
println(s"Score: $score")

// Reassign the var
score = 95
level += 1
println(s"Updated score: $score")
println(s"Level: $level")

// Type annotations โ€” explicit
val city: String = "Cape Town"
var lives: Int   = 3

println(s"$name lives in $city")
println(s"Lives: $lives")
$ scala Main.scala
๐Ÿ’ก

Scala programmers prefer val over var as a discipline. Immutable values make programs easier to reason about โ€” if a value cannot change, you never have to wonder "what is this right now?" It is also safer in concurrent code.

Strings & String Interpolation

Scala has three string interpolators. The s"" interpolator lets you embed variables and expressions with $variable or ${expression}. The f"" interpolator adds printf-style formatting. The raw"" interpolator ignores escape sequences. All are far cleaner than Java's string concatenation.

Main.scalaScala
val name  = "Alice"
val score = 92.567
val grade = "A"

// s interpolator โ€” embed values with $ or ${}
println(s"Hello, $name!")
println(s"Score: $score, Grade: $grade")
println(s"Next year: ${2025 + 1}")
println(s"Passing: ${score >= 50}")
println(s"Uppercase: ${name.toUpperCase}")

// f interpolator โ€” formatted output
println(f"Score: $score%.2f")        // 2 decimal places
println(f"Score: $score%8.2f")       // right-aligned, width 8
println(f"Name: $name%-10s | OK")    // left-aligned, width 10

// Multi-line strings with triple quotes
val message =
  s"""
  |Hello, $name!
  |You scored ${score.toInt}%.
  |Grade: $grade
  """.stripMargin

println(message)

// Useful String methods
val text = "  Hello, Scala World!  "
println(text.trim)
println(text.trim.length)
println(text.trim.contains("Scala"))
println(text.trim.replace("Scala", "Functional"))
println(text.trim.split(", ").mkString(" | "))
$ scala Main.scala
๐Ÿ’ก

The stripMargin method removes the leading whitespace up to and including the | character from each line of a multi-line string. This keeps your code indented nicely without the pipe characters appearing in the output.

Types & Basic Operations

Scala's basic types are Int, Long, Double, Float, Boolean, Char, and String. Unlike Java, there are no "primitive" types โ€” everything is an object. Type conversion is explicit โ€” you must call toInt, toDouble etc. In expressions, Scala returns a value, meaning if/else and match are expressions not statements.

Main.scalaScala
// Numeric types
val anInt: Int     = 42
val aLong: Long    = 9_876_543_210L
val aDouble: Double = 3.14
val aFloat: Float  = 2.5f

// Arithmetic
println(10 + 3)    // 13
println(10 - 3)    // 7
println(10 * 3)    // 30
println(10 / 3)    // 3  โ€” integer division!
println(10 % 3)    // 1  โ€” remainder
println(10.0 / 3)  // 3.3333...
println(Math.pow(2, 10).toInt)  // 1024

// Explicit type conversion
val x = "42"
val n = x.toInt
val d = x.toDouble
println(s"String: $x  Int: $n  Double: $d")

// In Scala, if/else is an EXPRESSION โ€” it returns a value
val status = if (n > 50) "big" else "small"
println(s"$n is $status")

// Everything in Scala is an expression
val result = {
  val a = 10
  val b = 20
  a + b    // last expression in a block is the return value
}
println(s"Block result: $result")
$ scala Main.scala
๐Ÿ’ก

In Scala, if/else is an expression that returns a value โ€” not just a statement that runs code. This means val status = if (condition) "yes" else "no" works perfectly. The same applies to match, try/catch, and code blocks {}.

โš–๏ธ
Module 02

Control Flow & Pattern Matching

Scala's match expression is one of its most powerful features โ€” far beyond a simple switch statement. It can match on values, types, ranges, case class structure, and even custom patterns. Guards (if conditions inside match cases) add even more precision. for comprehensions turn loops into elegant data transformations.

if Expressions & match

if/else works as you expect but always returns a value. match is Scala's pattern matching โ€” think of it as an extremely powerful switch. The _ wildcard is the catch-all. Guards (if conditions after a case) add extra filtering inside each case.

Main.scalaScala
val score = 78

// if/else as expression
val result = if (score >= 50) "passed" else "failed"
println(s"Result: $result")

// match โ€” Scala's powerful pattern matching
val grade = score match {
  case s if s >= 90 => "A โ€” Outstanding"
  case s if s >= 80 => "B โ€” Great"
  case s if s >= 70 => "C โ€” Good"
  case s if s >= 60 => "D โ€” Needs work"
  case _            => "F โ€” Please retry"
}
println(s"Grade: $grade")

// match on String
val day = "Saturday"
val dayType = day match {
  case "Monday" | "Tuesday" | "Wednesday" |
       "Thursday" | "Friday"  => "Weekday"
  case "Saturday" | "Sunday"  => "Weekend"
  case _                      => "Unknown"
}
println(s"$day is a $dayType")

// match returning different types of values
val number = 7
val description = number match {
  case 0            => "zero"
  case 1            => "one"
  case n if n < 0   => s"negative ($n)"
  case n if n % 2 == 0 => s"even ($n)"
  case n            => s"odd ($n)"
}
println(description)
$ scala Main.scala
๐Ÿ’ก

Scala's match must cover all possible inputs โ€” the compiler will warn you if it is not exhaustive. The _ wildcard ensures you always have a catch-all. Unlike Java switch, Scala match does not fall through โ€” each case is independent.

for Loops & for Comprehensions

Scala's for loop is called a "for comprehension" and is much more powerful than a simple loop. With yield it transforms a collection into a new one โ€” like map. You can add guards with if inside the for, and loop over multiple collections at once.

Main.scalaScala
// Basic for loop โ€” iterate over a range
for (i <- 1 to 5) {
  println(s"Count: $i")
}
println("---")

// 1 until 5 = 1, 2, 3, 4 (excludes 5)
for (i <- 1 until 5) print(s"$i ")
println()
println("---")

// Loop over a List
val fruits = List("apple", "banana", "cherry")
for (fruit <- fruits) {
  println(s"I like $fruit")
}
println("---")

// for comprehension with yield โ€” creates a new collection
val doubled = for (n <- 1 to 5) yield n * 2
println(s"Doubled: $doubled")

// for with a guard (filter)
val evens = for (n <- 1 to 10 if n % 2 == 0) yield n
println(s"Evens: $evens")

// Nested for โ€” like a nested loop
val pairs = for {
  x <- 1 to 3
  y <- 1 to 3
  if x != y
} yield (x, y)
println(s"Pairs: $pairs")

// while loop
var counter = 3
while (counter > 0) {
  print(s"$counter ")
  counter -= 1
}
println("Go!")
$ scala Main.scala
๐Ÿ’ก

for { ... } yield is a for comprehension โ€” it produces a new collection. Think of it as a readable version of map and filter combined. It is used extensively in Scala for data transformation. The multi-line form with curly braces allows guards and multiple generators.

โš™๏ธ
Module 03

Functions & Higher-Order Functions

Functions are first-class citizens in Scala โ€” you can pass them as arguments, return them from functions, and store them in variables. This is the core of functional programming. Higher-order functions like map, filter and reduce transform collections without manual loops.

Defining Functions with def

Use def to define a function. The return type comes after a colon following the parameters. If the body is a single expression, you can omit the curly braces. Scala functions always return the value of their last expression โ€” no return keyword needed (though it works).

Main.scalaScala
// Basic function โ€” single expression, no braces needed
def square(n: Int): Int = n * n

// Multi-line function
def greet(name: String, greeting: String = "Hello"): String = {
  val message = s"$greeting, $name!"
  message   // last expression is the return value
}

// Function with no return value โ€” Unit is like void
def printDivider(char: String = "-", length: Int = 20): Unit = {
  println(char * length)
}

println(square(5))
println(square(12))
println(greet("Alice"))
println(greet("Bob", "Good morning"))
printDivider()
printDivider("=", 30)

// Functions are expressions โ€” can be used inline
val results = List(1, 2, 3, 4, 5).map(n => square(n))
println(results)

// Recursive function
def factorial(n: Int): Int =
  if (n <= 1) 1
  else n * factorial(n - 1)

println(s"5! = ${factorial(5)}")
println(s"10! = ${factorial(10)}")
$ scala Main.scala
๐Ÿ’ก

Scala does not require a return statement โ€” the last expression in a function is automatically its return value. Using return explicitly is considered bad style in Scala. If the function body is one expression, drop the curly braces too for concise code.

Higher-Order Functions & Anonymous Functions

A higher-order function is one that takes another function as a parameter or returns a function. map, filter and reduce are the classic examples. Anonymous functions (lambdas) use the => arrow syntax. The shorthand _ represents a single parameter.

Main.scalaScala
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// map โ€” transform every element
val doubled  = numbers.map(n => n * 2)
val squared  = numbers.map(n => n * n)
val asString = numbers.map(n => s"item$n")
println(s"Doubled: $doubled")
println(s"Squared: $squared")

// Shorthand with _ (underscore = one parameter)
val tripled = numbers.map(_ * 3)
println(s"Tripled: $tripled")

// filter โ€” keep matching elements
val evens   = numbers.filter(_ % 2 == 0)
val overFive = numbers.filter(_ > 5)
println(s"Evens: $evens")
println(s"Over 5: $overFive")

// reduce / fold โ€” collapse to one value
val sum     = numbers.reduce(_ + _)
val product = numbers.reduce(_ * _)
val foldSum = numbers.foldLeft(0)(_ + _)  // with start value
println(s"Sum: $sum, Product: $product, FoldSum: $foldSum")

// Chain operations
val result = numbers
  .filter(_ % 2 == 0)   // keep evens: 2,4,6,8,10
  .map(_ * 10)           // multiply by 10
  .filter(_ > 40)        // keep > 40: 60,80,100
println(s"Chained: $result")

// Define your own higher-order function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ * 2, 3))   // 3 * 2 * 2 = 12
println(applyTwice(_ + 10, 5))  // 5 + 10 + 10 = 25
$ scala Main.scala
๐Ÿ’ก

The _ shorthand only works when the parameter appears exactly once. _ * 2 is fine (one param used once). If you need to use the parameter twice or name it for clarity, write n => n + n explicitly. When in doubt, write the full arrow syntax โ€” it is always correct.

๐Ÿ—๏ธ
Module 04

Classes & Case Classes

Scala has regular classes (like Java) and case classes โ€” a supercharged version designed for immutable data. Case classes automatically get equals, hashCode, toString, and copy methods for free. They are the foundation of functional data modelling in Scala. Traits are like interfaces that can also have method implementations.

Regular Classes & Companion Objects

A Scala class body is its constructor. Parameters with val/var become fields automatically. Methods go directly in the class body. A companion object (same name, object keyword) holds static-like methods โ€” including factory methods and constants.

Main.scalaScala
// Class โ€” constructor parameters are the fields
class Circle(val radius: Double) {
  val area: Double      = Math.PI * radius * radius
  val circumference: Double = 2 * Math.PI * radius

  def describe(): String =
    f"Circle(r=$radius%.1f, area=$area%.2f)"

  def scale(factor: Double): Circle =
    new Circle(radius * factor)
}

// Companion object โ€” same name, object keyword
object Circle {
  val unitCircle: Circle = new Circle(1.0)    // static-like

  def fromDiameter(d: Double): Circle =       // factory method
    new Circle(d / 2)
}

val c1 = new Circle(5.0)
val c2 = Circle.fromDiameter(10.0)
val c3 = Circle.unitCircle

println(c1.describe())
println(c2.describe())
println(c3.describe())
println(s"c1 area: ${c1.area.formatted("%.2f")}")

val bigger = c1.scale(2)
println(bigger.describe())
$ scala Main.scala
๐Ÿ’ก

Companion objects are Scala's answer to Java's static methods and fields. They live in the same file as the class with the same name. The class holds instance behaviour, the object holds shared/factory behaviour. Together they make a complete, well-organised type.

Case Classes & Traits

Case classes are perfect for modelling data. They are immutable by default, get free toString/equals/hashCode/copy, and work beautifully with pattern matching. Traits are Scala's interfaces โ€” they can have abstract AND concrete methods, and a class can extend multiple traits.

Main.scalaScala
// Trait โ€” like an interface with optional implementations
trait Describable {
  def describe(): String     // abstract โ€” must be implemented
  def shout(): String = describe().toUpperCase  // concrete
}

// Case class โ€” immutable data with free methods
case class Student(name: String, score: Int, city: String)
  extends Describable {

  def describe(): String = s"$name ($score%) from $city"

  def grade: String = score match {
    case s if s >= 90 => "A"
    case s if s >= 80 => "B"
    case s if s >= 70 => "C"
    case _            => "D"
  }

  // copy creates a modified clone without changing original
  def withScore(newScore: Int): Student = copy(score = newScore)
}

val alice = Student("Alice", 88, "Cape Town")
val bob   = Student("Bob",   72, "Durban")

println(alice.describe())
println(alice.shout())      // from Trait
println(alice.grade)

// copy โ€” create modified version
val aliceAfterBonus = alice.withScore(95)
println(aliceAfterBonus.describe())
println(alice.describe())   // original unchanged

// Case classes work with pattern matching
def assess(s: Student): String = s match {
  case Student(name, score, _) if score >= 90 => s"$name is excellent"
  case Student(name, score, _) if score >= 70 => s"$name is doing well"
  case Student(name, _, _)                    => s"$name needs support"
}

println(assess(alice))
println(assess(bob))
$ scala Main.scala
๐Ÿ’ก

Case class copy() is incredibly useful for immutable data โ€” it creates a new instance with all the same values except the ones you specify. alice.copy(score = 95) makes a new Student with score 95 but keeps everything else the same. The original is never modified.

๐Ÿ“‹
Module 05

Collections

Scala's collection library is one of the richest in any programming language. By default all collections are immutable โ€” operations return new collections rather than modifying the original. List is the go-to sequential collection, Map stores key-value pairs, and Option elegantly handles the absence of a value instead of null.

List, Map & Set

List is an immutable linked list โ€” the workhorse of Scala. Prepend with :: (cons operator) and combine with :::. Map stores key-value pairs with O(1) lookup. Set stores unique values. All three return new collections on modification โ€” the originals are untouched.

Main.scalaScala
// List โ€” immutable, ordered
val fruits = List("apple", "banana", "cherry")
val nums   = List(3, 1, 4, 1, 5, 9, 2, 6)

println(fruits)
println(s"Head: ${fruits.head}")   // first element
println(s"Tail: ${fruits.tail}")   // all but first
println(s"Size: ${fruits.size}")
println(s"Contains banana: ${fruits.contains("banana")}")

// Prepend with :: and combine with :::
val more = "mango" :: fruits
val combined = List("kiwi", "grape") ::: fruits
println(s"Prepended: $more")
println(s"Combined: $combined")

// Operations
println(nums.sorted)
println(nums.distinct)   // remove duplicates
println(nums.sum)
println(nums.max)

println("---")

// Map โ€” immutable key-value pairs
val scores = Map("Alice" -> 95, "Bob" -> 82, "Charlie" -> 71)

println(scores("Alice"))           // direct lookup (throws if missing)
println(scores.getOrElse("Dave", 0))  // safe lookup with default

// Add / update โ€” returns a NEW map
val updated = scores + ("Diana" -> 88) + ("Alice" -> 97)
println(updated)

// Iterate
scores.foreach { case (name, score) =>
  println(s"  $name: $score")
}

println("---")

// Set โ€” unique values, unordered
val set1 = Set(1, 2, 3, 4, 5)
val set2 = Set(4, 5, 6, 7, 8)
println(set1.union(set2))
println(set1.intersect(set2))
println(set1.diff(set2))
$ scala Main.scala
๐Ÿ’ก

The -> syntax creates a Tuple2 (pair). "Alice" -> 95 is the same as ("Alice", 95). Map stores these pairs. When you iterate a Map with foreach and pattern match with { case (k, v) => ... }, you unpack each pair into named variables automatically.

Option โ€” Safe Null Handling

Option is Scala's answer to null โ€” it is either Some(value) when a value exists, or None when it does not. This forces you to handle the "no value" case explicitly and eliminates NullPointerExceptions. Option works beautifully with map, flatMap and pattern matching.

Main.scalaScala
// Option is either Some(value) or None
val presentValue: Option[String] = Some("Alice")
val missingValue: Option[String] = None

// Pattern match to unwrap
presentValue match {
  case Some(name) => println(s"Found: $name")
  case None       => println("Not found")
}

missingValue match {
  case Some(name) => println(s"Found: $name")
  case None       => println("Not found")
}

// getOrElse โ€” provide a default
println(presentValue.getOrElse("Unknown"))
println(missingValue.getOrElse("Unknown"))

// map โ€” transform the value if present
val upper = presentValue.map(_.toUpperCase)
val noUpper = missingValue.map(_.toUpperCase)
println(upper)
println(noUpper)

// Real-world example โ€” Map lookup returns Option
val database = Map(
  1 -> "Alice",
  2 -> "Bob",
  3 -> "Charlie"
)

def findUser(id: Int): Option[String] = database.get(id)

findUser(2) match {
  case Some(user) => println(s"User found: $user")
  case None       => println("User not found")
}

findUser(99) match {
  case Some(user) => println(s"User found: $user")
  case None       => println("User not found")
}

// for comprehension with Option
val result = for {
  user  <- findUser(1)
  upper  = user.toUpperCase
} yield s"Hello, $upper!"

println(result.getOrElse("No result"))
$ scala Main.scala
๐Ÿ’ก

Option eliminates NullPointerException โ€” the most common runtime crash in Java programs. Map.get(key) returns Option[V] not V, forcing you to handle the missing-key case. Embrace Option and you will write code with far fewer runtime surprises.

๐Ÿ”€
Module 06

Functional Programming

Functional programming means writing programs as compositions of pure functions โ€” functions with no side effects, that always return the same output for the same input. Scala supports this style natively. flatMap and for-yield (for comprehensions with yield) are the core tools for chaining operations on collections and Options.

Immutability & Pure Functions

A pure function has no side effects and always returns the same result for the same inputs. Immutable data (val, immutable collections) combined with pure functions produces code that is easy to test, reason about and parallelise. In Scala, this style is the default.

Main.scalaScala
// Pure function โ€” no side effects, same input = same output
def add(a: Int, b: Int): Int = a + b
def multiply(a: Int, b: Int): Int = a * b

// Impure function โ€” reads external state, has side effects
var total = 0
def impureAdd(n: Int): Int = {
  total += n   // side effect: changes external state!
  total
}

// Function composition
def double(n: Int): Int = n * 2
def addOne(n: Int): Int = n + 1

// Compose: apply double, then addOne
val doubleAndAdd: Int => Int = double andThen addOne
val addAndDouble: Int => Int = addOne andThen double

println(doubleAndAdd(5))   // (5*2)+1 = 11
println(addAndDouble(5))   // (5+1)*2 = 12

// Partial application โ€” fix some arguments
def power(base: Int, exp: Int): Int =
  Math.pow(base, exp).toInt

val square  = (n: Int) => power(n, 2)
val cube    = (n: Int) => power(n, 3)
val powerOf2 = (n: Int) => power(2, n)

println(List(1,2,3,4,5).map(square))
println(List(1,2,3,4,5).map(cube))
println(List(1,2,3,4,5).map(powerOf2))

// Pipelines โ€” chain pure transformations
val data = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val answer = data
  .filter(_ % 2 == 0)      // [2,4,6,8,10]
  .map(_ * _ )              // [4,16,36,64,100]
  .filter(_ > 20)           // [36,64,100]
  .sum                      // 200
println(s"Pipeline result: $answer")
$ scala Main.scala
๐Ÿ’ก

andThen composes two functions left-to-right: f andThen g means "apply f first, then g". compose does the opposite: f compose g means "apply g first, then f". andThen reads more naturally for data pipelines and is used more often.

flatMap & for Comprehensions

flatMap is map followed by flatten โ€” it applies a function that returns a collection to each element, then flattens the results into one collection. For comprehensions with yield are syntactic sugar for flatMap and map chains, making complex transformations readable.

Main.scalaScala
// flatMap โ€” map then flatten
val words = List("hello world", "scala is fun", "code daily")

// map gives List[List[String]]
val nested = words.map(_.split(" ").toList)
println(s"Nested: $nested")

// flatMap gives List[String] โ€” flattened!
val flat = words.flatMap(_.split(" ").toList)
println(s"Flat: $flat")

// Count total characters across all words
val totalChars = flat.map(_.length).sum
println(s"Total characters: $totalChars")

println("---")

// for comprehension = flatMap + map + filter
val students = List("Alice", "Bob", "Charlie")
val subjects = List("Maths", "Science")

// Every student + every subject combination
val enrolments = for {
  student <- students
  subject <- subjects
} yield s"$student โ€” $subject"

enrolments.foreach(println)

println("---")

// flatMap with Option โ€” chain optional operations
def findScore(name: String): Option[Int] = name match {
  case "Alice" => Some(95)
  case "Bob"   => Some(72)
  case _       => None
}

def getGrade(score: Int): Option[String] = score match {
  case s if s >= 80 => Some("A")
  case s if s >= 70 => Some("B")
  case _            => None
}

// Chaining Options with flatMap
val aliceGrade = findScore("Alice").flatMap(getGrade)
val charlieGrade = findScore("Charlie").flatMap(getGrade)
println(s"Alice grade: $aliceGrade")
println(s"Charlie grade: $charlieGrade")

// Same thing, more readable with for-yield
val result = for {
  score <- findScore("Bob")
  grade <- getGrade(score)
} yield s"Bob scored $score โ€” Grade: $grade"
println(result.getOrElse("Not available"))
$ scala Main.scala
๐Ÿ’ก

The for/yield comprehension is pure syntax sugar โ€” the Scala compiler rewrites it into flatMap and map calls. A for comprehension with one generator is map. With two generators it is flatMap + map. With a guard it adds a filter. Understanding this connection helps you decide which style to use.

๐ŸŽ‰

You finished the Scala tutorial!

You now know variables, pattern matching, higher-order functions, case classes, collections, Option and functional programming fundamentals. You have the foundation to tackle Apache Spark, Akka and real Scala projects.