Scala - Complete Tutorial for Beginners

• Scala combines object-oriented and functional programming paradigms on the JVM, offering Java interoperability while providing concise syntax and powerful type inference

Key Insights

• Scala combines object-oriented and functional programming paradigms on the JVM, offering Java interoperability while providing concise syntax and powerful type inference • Pattern matching and case classes form the backbone of idiomatic Scala code, enabling expressive data manipulation and algebraic data types • Scala’s collections library provides immutable data structures by default with rich transformation methods that encourage functional programming patterns

Setting Up Your Scala Environment

Install Scala using SDKMAN or download from scala-lang.org. Verify installation:

scala -version
# Scala code runner version 3.3.1

Create a simple Scala file Hello.scala:

@main def hello(): Unit =
  println("Hello, Scala!")

Run it directly:

scala Hello.scala

For larger projects, use sbt (Scala Build Tool). Create build.sbt:

scalaVersion := "3.3.1"
name := "scala-tutorial"

Basic Syntax and Variables

Scala uses val for immutable values and var for mutable variables:

val immutableValue = 42  // Type inferred as Int
var mutableVariable = "Hello"
mutableVariable = "World"  // OK

// Explicit type annotation
val explicitType: Double = 3.14

// Multiple values
val (x, y) = (10, 20)

Prefer val over var. Immutability prevents bugs and enables better reasoning about code.

Data Types and Type Inference

Scala’s type system is sophisticated yet practical:

// Basic types
val integer: Int = 42
val long: Long = 42L
val double: Double = 3.14
val float: Float = 3.14f
val boolean: Boolean = true
val char: Char = 'A'
val string: String = "Scala"

// Type inference works in most cases
val inferred = 100  // Int
val inferredDouble = 3.14  // Double

// String interpolation
val name = "Alice"
val age = 30
println(s"$name is $age years old")
println(s"Next year: ${age + 1}")

Functions and Methods

Functions are first-class citizens in Scala:

// Method definition
def add(x: Int, y: Int): Int = x + y

// Single expression - return type inferred
def multiply(x: Int, y: Int) = x * y

// Function with multiple lines
def greet(name: String): String = {
  val greeting = "Hello"
  s"$greeting, $name!"
}

// Anonymous functions (lambdas)
val square = (x: Int) => x * x
val sum = (a: Int, b: Int) => a + b

// Higher-order functions
def applyOperation(x: Int, y: Int, op: (Int, Int) => Int): Int =
  op(x, y)

println(applyOperation(5, 3, sum))      // 8
println(applyOperation(5, 3, _ * _))    // 15 (using placeholder syntax)

Control Structures

Scala’s control structures return values:

// If expressions
val max = if (a > b) a else b

// Pattern matching (replaces switch)
def describe(x: Any): String = x match {
  case 0 => "zero"
  case i: Int if i > 0 => "positive integer"
  case i: Int => "negative integer"
  case s: String => s"string: $s"
  case _ => "something else"
}

// For comprehensions
val numbers = List(1, 2, 3, 4, 5)
val doubled = for (n <- numbers) yield n * 2

// For with guards
val evens = for {
  n <- numbers
  if n % 2 == 0
} yield n

// Multiple generators
val pairs = for {
  x <- 1 to 3
  y <- 1 to 3
} yield (x, y)

Collections

Scala provides rich immutable collections:

// Lists (immutable, linked list)
val fruits = List("apple", "banana", "orange")
val numbers = List(1, 2, 3, 4, 5)

// Common operations
val doubled = numbers.map(_ * 2)
val evens = numbers.filter(_ % 2 == 0)
val sum = numbers.reduce(_ + _)
val folded = numbers.foldLeft(0)(_ + _)

// Vectors (better random access)
val vec = Vector(1, 2, 3, 4, 5)

// Sets
val uniqueNumbers = Set(1, 2, 2, 3, 3, 4)  // Set(1, 2, 3, 4)

// Maps
val capitals = Map(
  "France" -> "Paris",
  "Japan" -> "Tokyo",
  "USA" -> "Washington"
)

println(capitals("France"))  // Paris
println(capitals.getOrElse("UK", "Unknown"))  // Unknown

// Chaining operations
val result = numbers
  .filter(_ > 2)
  .map(_ * 2)
  .sum  // 24

Case Classes and Pattern Matching

Case classes are Scala’s primary tool for modeling data:

case class Person(name: String, age: Int)

// Automatic companion object with apply
val alice = Person("Alice", 30)  // No 'new' needed

// Pattern matching on case classes
def categorize(person: Person): String = person match {
  case Person(name, age) if age < 18 => s"$name is a minor"
  case Person(name, age) if age < 65 => s"$name is an adult"
  case Person(name, _) => s"$name is a senior"
}

// Copy method for immutable updates
val olderAlice = alice.copy(age = 31)

// Sealed traits for algebraic data types
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape

def area(shape: Shape): Double = shape match {
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h
  case Triangle(b, h) => 0.5 * b * h
}

Classes and Objects

Scala supports full object-oriented programming:

// Class with primary constructor
class BankAccount(private var balance: Double) {
  def deposit(amount: Double): Unit = {
    require(amount > 0, "Amount must be positive")
    balance += amount
  }
  
  def withdraw(amount: Double): Boolean = {
    if (amount <= balance) {
      balance -= amount
      true
    } else false
  }
  
  def getBalance: Double = balance
}

// Companion object (singleton)
object BankAccount {
  def apply(initialBalance: Double): BankAccount =
    new BankAccount(initialBalance)
}

// Usage
val account = BankAccount(1000.0)
account.deposit(500.0)
account.withdraw(200.0)
println(account.getBalance)  // 1300.0

Options and Error Handling

Scala uses Option to handle null values safely:

def findPerson(id: Int): Option[Person] = {
  if (id > 0) Some(Person("John", 25))
  else None
}

// Pattern matching on Option
findPerson(1) match {
  case Some(person) => println(s"Found: ${person.name}")
  case None => println("Not found")
}

// Functional operations
val name = findPerson(1).map(_.name).getOrElse("Unknown")

// Try for exception handling
import scala.util.{Try, Success, Failure}

def divide(a: Int, b: Int): Try[Int] = Try(a / b)

divide(10, 2) match {
  case Success(result) => println(s"Result: $result")
  case Failure(exception) => println(s"Error: ${exception.getMessage}")
}

Traits and Mixins

Traits enable multiple inheritance of behavior:

trait Logging {
  def log(message: String): Unit = println(s"[LOG] $message")
}

trait Timestamped {
  def timestamp: Long = System.currentTimeMillis()
}

class Service extends Logging with Timestamped {
  def process(): Unit = {
    log(s"Processing at ${timestamp}")
  }
}

val service = new Service
service.process()

Practical Example: Building a Task Manager

case class Task(id: Int, description: String, completed: Boolean = false)

class TaskManager {
  private var tasks: List[Task] = List.empty
  private var nextId: Int = 1
  
  def addTask(description: String): Task = {
    val task = Task(nextId, description)
    tasks = tasks :+ task
    nextId += 1
    task
  }
  
  def completeTask(id: Int): Option[Task] = {
    tasks.find(_.id == id).map { task =>
      val completed = task.copy(completed = true)
      tasks = tasks.map(t => if (t.id == id) completed else t)
      completed
    }
  }
  
  def listTasks: List[Task] = tasks
  
  def pendingTasks: List[Task] = tasks.filter(!_.completed)
}

// Usage
val manager = new TaskManager
manager.addTask("Learn Scala")
manager.addTask("Build a project")
manager.completeTask(1)
manager.pendingTasks.foreach(t => println(s"- ${t.description}"))

Scala’s combination of functional and object-oriented features makes it powerful for building scalable applications. Start with immutable data structures, embrace pattern matching, and leverage the rich collections library. The type system catches errors at compile time while type inference keeps code concise.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.