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.