Introduction to scala
Table of Contents
Variadic Methods in Scala
In Scala, a variadic method is one that accepts a variable number of arguments. These are defined using an asterisk (*) after the type. This is useful when you don't know how many parameters will be passed to a method in advance.
Below is an example using a custom recursive data structure to construct a list of numbers using a variadic method.
1object NumberList {
2
3 sealed trait NumList
4 case object End extends NumList
5 case class Node(value: Int, next: NumList) extends NumList
6
7 // Construct a NumList recursively using a variadic method
8 def apply(nums: Int*): NumList = {
9 if (nums.isEmpty) End
10 else Node(nums.head, apply(nums.tail*))
11 }
12
13 // Recursively print the NumList structure
14 def printList(n: NumList): Unit = n match {
15 case End => println("End")
16 case Node(value, next) =>
17 println(s"Node($value)")
18 printList(next)
19 }
20
21 def main(args: Array[String]): Unit = {
22 val myList = apply(1, 2, 3, 4)
23 printList(myList)
24 }
25}The apply method uses recursion to convert a variable number of Int values into a chain of Node instances ending in End. The special syntax : _* is used to expand a sequence (in this case, nums.tail) into individual arguments that can be passed recursively.
Apply and Unapply Methods in Scala
The apply Method
The apply method in Scala allows you to instantiate classes without explicitly using the new keyword. It is commonly defined inside a companion object and is treated like a factory method.
1class Person(val name: String, val age: Int)
2
3object Person {
4 def apply(name: String, age: Int): Person = new Person(name, age)
5}
6
7val p = Person("Alice", 25) // Equivalent to: new Person("Alice", 25)Here, Person("Alice", 25) looks like a function call but is actually invoking the apply method in the companion object. This style makes your code more concise and idiomatic.
The unapply Method
The unapply method is used for pattern matching. It extracts values from an object, acting as the inverse of apply. It is often used in match expressions and enables powerful destructuring syntax.
1class Person(val name: String, val age: Int)
2
3object Person {
4 def apply(name: String, age: Int): Person = new Person(name, age)
5
6 def unapply(p: Person): Option[(String, Int)] = Some((p.name, p.age))
7}
8
9val p = Person("Bob", 30)
10
11p match {
12 case Person(name, age) => println(s"Name: $name, Age: $age")
13}The unapply method returns an Option of a tuple that represents the object's components. In the example above, it enables the Person(name, age) pattern to match and extract fields from p.
Together, apply and unapply make your classes more expressive — letting you construct and deconstruct objects in a consistent, readable way.
ADTs in Scala: Sealed Traits, Case Classes, Case Objects and Companion Object
Understanding Sealed Traits in Scala
In Scala, a sealed trait (or class) is used to restrict where it can be extended. Specifically, a sealed trait can only be extended within the same file where it is defined. This design enables the compiler to know all possible subtypes, making pattern matching safer and exhaustive.
Why use sealed traits?
• Limit inheritance to a known set of types.
• Enable safer pattern matching by informing the compiler of all subtypes.
• Model algebraic data types (ADTs), a key concept in functional programming.
1sealed trait Animal
2
3case class Dog(name: String) extends Animal
4case class Cat(name: String) extends Animal
5
6def speak(animal: Animal): String = animal match {
7 case Dog(name) => s"$name says woof"
8 case Cat(name) => s"$name says meow"
9 // No 'case _ =>' needed — all cases are covered
10}In the example above, since Animal is sealed and its subtypes (Dog and Cat) are defined in the same file, the Scala compiler knows all possible cases. This ensures that your match expressions are exhaustive, helping catch errors at compile time.
Case Object in Scala
A case object in Scala is a special kind of object that acts as both a singleton and a case class without parameters. It's commonly used when you need a single, reusable instance and still want the benefits of pattern matching.
Key characteristics of a case object:
• Singleton: There is only one instance of the case object in the entire application.
• Pattern Matching: You can use case objects directly in pattern matching — since they have no parameters, the match is against the object itself.
• Equality and Hashing: Case objects get default implementations of equals, hashCode, and toString, just like case classes.
• Serializable: Case objects are implicitly serializable, making them suitable for use in distributed systems or Akka messages.
Typical use cases:
• Representing singleton states or events in a sealed trait hierarchy (e.g., for a state machine).
• Modeling values that don't need any associated data but still benefit from pattern matching.
• Representing “empty” or “no data” cases in an ADT (Algebraic Data Type).
1sealed trait State
2
3case object Idle extends State
4case object Working extends State
5case object Finished extends State
6
7val currentState: State = Working
8
9currentState match {
10 case Idle => println("The system is idle.")
11 case Working => println("The system is working.")
12 case Finished => println("The system is finished.")
13}In this example, Idle, Working, and Finished are case objects extending the sealed trait State. Because they are singleton values, the pattern matching is performed directly against the object reference. Scala automatically generates an unapply method for case objects, but since they have no parameters, matching is simplified and safe.
Case Class in Scala
A case class in Scala is a regular class enhanced with several useful features, most notably automatic support for pattern matching. Scala generates an unapply method for case classes, allowing their fields to be extracted directly in match expressions.
This makes case classes ideal for modeling immutable data and working with algebraic data types (ADTs).
1case class Person(name: String, age: Int)
2
3val person = Person("Alice", 25)
4
5person match {
6 case Person(name, age) => println(s"Name: $name, Age: $age")
7 case _ => println("Unknown person")
8}In this example, Person(name, age) is a pattern that extracts the name and age fields from the Person instance. Scala automatically provides the unapply method that enables this deconstruction.
This pattern matching mechanism makes it easy and concise to access individual fields of a case class and is one of the reasons case classes are widely used in functional programming for modeling structured data.
Companion Object in Scala
In Scala, a companion object is an object that shares the same name as a class and is defined in the same source file. It acts as a singleton object and serves as a place to define methods or values related to the class, much like static members in Java. One key benefit is that a companion object can access the private members of its corresponding class, and vice versa.
1class Person(val name: String) {
2 private val secret = "likes pizza"
3
4 def greet(): Unit = println(s"Hi, I'm $name")
5}
6
7object Person {
8 def apply(name: String): Person = new Person(name)
9
10 def revealSecret(person: Person): String = person.secret // can access private member
11}In the example above, the Person class defines an instance method greet and a private field secret. The companion object Person defines:
• An apply method, which provides a more concise way to create instances (i.e., Person("Alice") instead of new Person("Alice")).
• A revealSecret method that demonstrates how the object can access the class's private members — a feature unique to companion objects in Scala.
Companion objects are commonly used for factory methods, constants, and utility functions related to the class. They also enable more idiomatic Scala code by supporting object-oriented and functional programming paradigms simultaneously.
Ordering in Scala
In Scala, Ordering is a type class used to define how elements of a certain type should be compared. It provides functionality for sorting and comparison operations, and works seamlessly with collections and higher-order functions.
You can use predefined orderings for basic types, or define custom orderings for your own case classes. Ordering enables sorting, min/max computations, and other comparison-based operations.
Key features of Ordering:
• Works with sorted, sortBy, and sortWith.
• Can be imported or passed implicitly to sorting functions.
• Useful for customizing sort behavior for complex types.
1import scala.collection.mutable.PriorityQueue
2
3object Main extends App {
4
5 case class Person(name: String, age: Int)
6
7 val people = List(
8 Person("Alice", 32),
9 Person("Bob", 25),
10 Person("Charlie", 30)
11 )
12
13 implicit val ageOrdering: Ordering[Person] = Ordering.by(_.age)
14
15 val sortedPeople = people.sorted
16 sortedPeople.foreach(p => println(s"${p.name}, Age: ${p.age}"))
17
18 val queue = PriorityQueue.empty[Person](using ageOrdering.reverse)
19
20 queue.enqueue(people*)
21
22 println("PriorityQueue (min-heap by age):")
23 while (queue.nonEmpty) {
24 val p = queue.dequeue()
25 println(s"${p.name}, Age: ${p.age}")
26 }
27}In this example, we define an Ordering[Person] that sorts by age. The sorted method uses the implicit ordering to sort the list of Person instances.
You can also reverse an ordering using Ordering.by(...).reverse, or define more complex multi-level comparisons by chaining functions or using Ordering.comparator style logic.
We also use the same Ordering[Person] to create a PriorityQueue. To turn it into a min-heap (which gives us the person with the lowest age first), we simply reverse the ordering using ageOrdering.reverse. This ensures that elements with the lowest age are dequeued first.
Type Classes in Scala
Type classes in Scala are a versatile design pattern that enable ad-hoc polymorphism, allowing you to define generic functionality that can be applied to many types — even those whose source code you cannot modify. This makes it possible to add new behavior to existing types in a modular, non-intrusive way.
1// Step 1: Define the type class as a trait with a type parameter
2trait Printable[A] {
3 def format(value: A): String
4}
5
6// Step 2: Provide type class instances for types
7object PrintableInstances {
8 implicit val stringPrintable: Printable[String] = new Printable[String] { // anonymous class that extends Printable[String]
9 def format(value: String): String = value
10 }
11
12 implicit val intPrintable: Printable[Int] = new Printable[Int] {
13 def format(value: Int): String = value.toString
14 }
15}
16
17// Step 3: Provide interface methods that use the type class
18object Printable {
19 def format[A](value: A)(implicit p: Printable[A]): String = p.format(value)
20
21 // convenience method
22 def print[A](value: A)(implicit p: Printable[A]): Unit =
23 println(format(value))
24}
25
26// Step 4: Use it in your code
27import Printable._
28import PrintableInstances._
29
30Printable.print("Hello, world!")
31Printable.print(123)
32
33// You can also create your own types and provide instances
34case class Cat(name: String, age: Int)
35
36implicit val catPrintable: Printable[Cat] = new Printable[Cat] {
37 def format(cat: Cat): String = s"${cat.name} is ${cat.age} years old"
38}
39
40val myCat = Cat("Whiskers", 3)
41Printable.print(myCat) // prints: Whiskers is 3 years oldThe type class pattern in Scala generally consists of three parts:
1. The Type Class Trait: A generic trait that defines a contract for behavior. It typically has one or more type parameters representing the types for which the behavior is defined.
2. Type Class Instances: Concrete implementations of the type class trait for specific types. In Scala 2, these are usually implicit values; in Scala 3, they are typically defined as given instances.
3. The Interface: Methods or syntax that use the type class instances. This can be achieved via context bounds, implicit parameters, or using clauses in Scala 3. The Scala compiler automatically resolves the correct instance based on the type in scope.
This approach promotes extensibility and separation of concerns, letting you define operations for types without tightly coupling them to the type's definition.
Call by name functions in Scala
In Scala, a call-by-name function parameter is a mechanism where the argument expression is not evaluated before the function call. Instead, the expression is re-evaluated each time the parameter is referenced within the function body. This is in contrast to call-by-value, where the argument is evaluated once before the function is invoked and the resulting value is passed into the function.
A key property of call-by-name is its lazy evaluation. The argument expression is only executed when it is actually used inside the function. If the parameter is never referenced, the computation is skipped entirely. This makes it useful for avoiding unnecessary or expensive operations.
To declare a call-by-name parameter, you prepend => to its type in the function signature:
1def printTwice(x: => Int): Unit = {
2 println(s"First evaluation: $x")
3 println(s"Second evaluation: $x")
4}
5
6var counter = 0
7def generateValue(): Int = {
8 counter += 1
9 println(s"Generating value... counter is now $counter")
10 counter
11}
12
13printTwice(generateValue())In the above example, each time x is referenced insideprintTwice, the function generateValue() is executed again, demonstrating that call-by-name evaluates the expression multiple times.
Call-by-name is particularly useful for lazy computations, where you can defer expensive or optional operations until they are actually needed.
Partial Functions in Scala
In Scala, most functions are defined for all possible inputs of their domain type. However, there are situations where a function only makes sense for a subset of inputs. This is where thePartialFunction type comes in. A partial function allows you to explicitly define behavior for certain inputs while leaving others undefined.
1val divide: PartialFunction[Int, Int] = {
2 case x if x != 0 => 42 / x
3}
4
5println(divide.isDefinedAt(0)) // false
6println(divide.isDefinedAt(2)) // true
7println(divide(2)) // 21
8// println(divide(0)) // would throw MatchErrorIn the example above, the function divide is only defined for inputs where x != 0. Calling divide(2) works as expected, but divide(0) would cause a MatchError since the function is not defined for that input.
A PartialFunction[A, B] behaves like a normal A => B function, but it comes with an additional method:
1. isDefinedAt(a: A): Boolean – tells you whether the function is safe to call for a given input.
2. apply(a: A): B – executes the function, but should only be called if isDefinedAt returns true.
This makes partial functions particularly useful when you want to handle only certain cases explicitly, such as in pattern matching, custom error handling, or when chaining functions together using combinators like orElse to cover additional cases.
Operator Overloading
In Scala, operators like +, -, and * are not special syntax — they are simply method names. This means you can redefine or "overload" them by defining methods with symbolic names inside your own classes. Operator overloading allows custom types to behave like built-in numeric or logical types, making code more expressive and natural to read.
For instance, imagine you have a class representing an amount of money in Hong Kong Dollars (HKD). You can define a + method that adds two HKD values together, so that using a + b works just like adding two numbers.
1class HKD(val amount: Double) {
2 def +(that: HKD): HKD = new HKD(this.amount + that.amount)
3}
4
5object HKD {
6 def apply(amount: Double) = new HKD(amount)
7}
8
9object CompanionObject1 extends App {
10 val a = HKD(100)
11 val b = HKD(200)
12 val c = a + b
13 println(s"Total amount in HKD: ${c.amount}")
14}Operator overloading helps you design domain-specific abstractions that read naturally. For example, you can define custom behavior for subtraction, multiplication, or even string concatenation on your own types:
1def -(that: HKD): HKD = new HKD(this.amount - that.amount)
2def *(factor: Double): HKD = new HKD(this.amount * factor)While operator overloading can make code elegant and expressive, it should be used judiciously — clarity always takes precedence over clever syntax. Scala encourages this flexibility as part of its design philosophy: everything is an object, and every operation is a method call.
Defining Getters and Setters
You can define custom getters and setters to add specific logic, such as validation or side effects, when a field is accessed or modified. This involves defining a private backing field and then explicitly defining the getter and setter methods.
In the example below, the class Trade defines a private variable _price and exposes it through a getter price and a setter price_=. When you write trade.price = 20.0, Scala translates it into trade.price_=(20.0).
This approach allows encapsulation and validation logic — for example, the setter only updates _price if the new value is non-negative. This is a powerful feature of Scala's object-oriented design: you get the readability of field access with the safety of controlled mutation.
def price → defines the getter method.def price_=(value: Double) → defines the setter method.trade.price → calls trade.price() internally.trade.price = 20.0 → calls trade.price_=(20.0) internally.1class Trade(val id: String, val symbol: String, val quantity: Int, val initialPrice: Double) {
2 override def toString: String = s"Trade id: $id consist of symbol: $symbol, price: $price and quantity: $quantity"
3 private var _price = initialPrice
4 def price = _price // getter method
5 def price_=(value: Double) = { // setter method
6 if(value >= 0) {
7 _price = value
8 }
9 }
10 def value() = {
11 quantity * price
12 }
13}
14
15object Trade {
16 def apply(id: String, symbol: String, quantity: Int, initialPrice: Double): Trade
17 = new Trade(id, symbol, quantity, initialPrice)
18}
19
20object ObjectOrientedExample extends App {
21 val trade = Trade("id", "FB", 100, 10.0)
22 trade.price = 20.0
23 println(trade)
24 println(trade.value())
25}