Software Engineering

Scala Notes from 2015, still valid today.

Table of content

Context and Motivation

Back in 2015, I began my journey into functional programming with Scala. Along the way, I documented key insights and lessons learned as I explored Scala’s unique approach to functional and object-oriented programming.

This post is a distilled collection of those notes, bringing together the foundational ideas I found most useful. Although these notes trace back to my early days with Scala, they remain a valuable resource for anyone looking to revisit Scala’s core concepts.

What this post is not: This is not a tutorial for learning programming or even a step-by-step guide to learning Scala as a second language.

The Cornerstones of Functional Programming

The fundamental pillars of functional programming (and by extension, Scala) are pure functions and immutable states.

  • Pure Functions: Pure functions are essential for simplifying development. They produce the same output every time for the same input and avoid side effects, making code predictable and easier to test. By keeping functions isolated from external state, they allow developers to compose complex operations from simpler, reliable building blocks.
  • Immutable States: Immutable state management is crucial for handling concurrency and parallelism. By ensuring that data remains unchanged once created, Scala minimizes the risks associated with global mutable states and interdependencies between different parts of code. This approach greatly simplifies concurrent programming models, reducing potential bugs and making the system more robust.
  • Functional Approaches and Styles: Functional programming emphasizes composition over inheritance and declarative over imperative styles, both of which enhance code structure, modularity, and even lead to cleaner code.
    • Composition promotes combining small, single-responsibility functions to form larger, flexible behaviors. Divide and conquer!
    • A declarative style lets developers describe what to do rather than how to do it, resulting in cleaner, more readable code that highlights the business logic rather than the mechanics.
  • Referential Transparency: An expression is referentially transparent if it can be replaced with its value without changing the behavior of the program. This property is essential for FP as it enables reasoning about and refactoring code confidently, especially when functions are pure and data is immutable.
  • Cathegory Theory: This is a vast topic in its own right. While a deep understanding of category theory isn’t necessary to work with Scala (or even any understanding at all), it’s helpful to know that many FP principles are rooted in these mathematical concepts.

Why Scala?

Of course, Scala isn’t the only functional programming language, but it’s the one I decided to learn. Remember, these notes are from 2015, so some perspectives are relative to the context of programming languages at that time.

My main reason for choosing Scala was its blend of object-oriented (OOP) and functional programming (FP) paradigms—taking the best of both worlds.

Here’s a breakdown of what drew me to Scala:

  • Concise Code: Scala’s syntax is significantly more concise compared to many other languages of that time, allowing for cleaner, more readable code.

  • JVM Compatibility: Scala compiles to JVM bytecode, bringing several benefits:

    • Access to Java libraries and seamless integration with Java’s ecosystem.
    • Compatibility with new Java features as they become available.
    • Ability to mix Java and Scala files within the same project.
    • Support for Java tooling, including profiling and debugging tools.
  • Pure Object-Oriented Language: Scala is a truly OOP language, where even operators are methods of classes.

    • Supports classes, traits, inheritance, composition, mixing, etc.
    • Statically typed.
    • Advanced type system features such as compound types, generics, variance annotations, and upper/lower type bounds.
  • Functional from the Ground Up:

    • Scala treats functions as first-class citizens, meaning they can be assigned to variables, passed as parameters, and returned from other functions.
    • Every function returns a value, so there’s no void equivalent as in Java or C++ - even Unit is an object.
    • Provides functional types for handling nullity and side effects, like Option, Either, and Try.
    • Higher-order functions allow methods to take or return other functions.
    • Support for Anonymous Functions (also known as Lambdas), making it easy to write unnamed functions.
    • Nested functions enable scoping functions within other functions.
  • Local Type Inference: Scala’s powerful type inference lets the compiler deduce types from expressions and functions, enabling even more concise code (an uncommon feature among languages in 2015).

  • Interoperability and Flexibility: Beyond JVM, Scala can compile to native binaries and has plugins to transpile to JavaScript, extending its applicability beyond the JVM.

But, as with any language, Scala has its drawbacks and a few aspects I don’t particularly like:

  • Backward Compatibility: Higher-level releases of Scala don’t always maintain backward compatibility.
  • Inherited JVM Limitations:
    • Type Erasure: Particularly frustrating for deserializing collections from JSON strings.
    • Lack of Unsigned Types: Java and Scala both lack unsigned integers.
    • Tuple Limitations: Scala simulates tuples using classes, capping tuples at 22 elements.
    • Startup Time: JVM and Scala compiler optimizations slightly increase startup time (by milliseconds) but generally improve runtime performance. Quick scripts, however, may feel this initial lag.

Object-Oriented Programming (OOP) and Functional Programming (FP) in Scala

When working with Scala, I recommend shifting your mindset toward functional programming rather than trying to directly translate imperative code into Scala. However, functional programming isn’t always the ideal approach for every scenario, so understanding Scala’s object-oriented capabilities is equally important.

If you’re coming from Java, you might be surprised to find that Scala offers even more advanced OOP features.

From here to the end of the post, I’ll explore concepts in Scala that relate to both functional programming and object-oriented programming, highlighting how Scala integrates and leverages both paradigms.

Global Classes Hierarchy

As mentioned earlier, Scala is a purely object-oriented language. In Scala, even elements that other languages treat as native primitives are implemented as classes. This unified approach underpins Scala’s OOP model and allows for a consistent object-oriented experience throughout the language.

Below, you’ll find a diagram illustrating Scala’s class hierarchy, showcasing how primitives and other core types are represented as classes.

Basic Class Diagram

So basically:

  • Any: This is the root type of the Scala type system. Every type in Scala inherits from Any, making it the ultimate superclass. The Any type has two main subclasses: AnyVal (for value types) and AnyRef (for reference types).

  • AnyVal: This is the root type for all types that represent values. It includes primitive-like types such as Byte, Short, Int, Long, Float, Double, Char, Boolean, and Unit. These types are optimized for performance and typically correspond to native types in the JVM.

  • AnyRef: This is the root type for reference types, covering all non-value types. In Scala, AnyRef is equivalent to Java’s Object class, providing a foundation for all reference-based objects.

Variables or Values

When defining a variable in Scala, you have two options:

  • val: Creates an immutable variable.
  • var: Creates a mutable variable.

In functional programming (FP), it’s generally recommended to favor immutability by using val whenever possible. In most cases (you will see than more than 90% of the time) you’ll find that using immutable values is feasible and leads to simpler, more predictable code.

Here’s a quick example:

val immutableOne: String = "This is immutable."
var mutableOne: String = "You can mutate it."

// immutableOne = "Try to change it!" // Error: reassignment to val
mutableOne = "This can be changed."

Using val keeps your variables constant, reducing side effects and potential bugs in your code.

Classes in Scala

Let’s have a look to this example:

class Person(val id: Int, private val name: String, var nickname: String ) {

  def this(nickname: String) = {
    this(0, "Incognito", nickname)
  }

  def printName() = {
    println(name)
  }
}

val person = new Person(1000, "Angel", "angelito")
println(person.id)
println(person.nickname)
person.nickname = "angel"
println(person.nickname)
// println(person.step) // Error
// person.id = 2000 // Error
// println(person.name) // Error
person.printName()

val incognito = new Person("Nobody")
println(incognito.nickname)

Here’s a quick overview of key points about class definitions in Scala:

  • Primary Constructor: The primary constructor is defined within the class signature itself, essentially through the parameter list.
  • Auxiliar Constructors: It’s possible to define extra constructors using the keyword this, and at the end of the execution, the primary constructor must be called.
  • Visibility of Parameters:
    • val creates only a getter (making it immutable).
    • var creates both getter and setter (making it mutable).
    • If neither val nor var is specified, no accessors are created, making the parameter private to the class.
    • Custom setters can be implemented with a special syntax, _=, to assign values.
  • Access Modifiers:
    • public: Accessible from everywhere (this is the default scope).
    • protected: Accessible from subclasses of the class where the member is defined.
    • private: Accessible only within the class or object containing the member.
    • Scope of Protection: You can specify the scope of an access modifier, such as private[X] or protected[X], which limits access to within X, where X is an enclosing package, class, or singleton object.
  • By default, class members are immutable and private.

Modeling via OOP

Let’s extend the previous example:

trait Talker {
  def sayHello: Unit
}

trait Flying {
  def startFlying: Unit
}

class Walker(id: Int, name: String, nickname: String, private var steps: Int = 0 )
        extends Person(id, name, nickname) with Talker with Flying {


  def goAhead() = {
    steps = steps + 1
  }

  def printSteps() = {
    println(steps)
  }

  def sayHello: Unit = println("Hello!!")
  def startFlying: Unit = println("Let's start flying")
}

val walker = new Walker(1000, "Angel", "angelito")

walker.goAhead()
walker.printSteps()

Class modeling

Compared to Java and many other languages, Scala offers a richer object-oriented programming model. Below is a summary of some of Scala’s key class modeling features:

  • Inheritance: A class can extend only one other class, trait, or abstract class, using the extends keyword.
  • Mixins and Compound Types: Scala allows multiple inheritance of types (traits, but not classes or abstract classes) through the with keyword. Each additional type is called a mixin. For example, class composition in Scala typically follows this format: class ClassName extends [Class|Abstract|Trait] with [Trait] with [Trait] ...
  • Traits Extending Abstract Classes: Traits in Scala can extend abstract classes.
  • Overriding Parameters: When overriding a parameter from the superclass, you only need to specify the access modifier (val or var) if you want the parameter to be a class member in the subclass.
  • Sealed Classes and Traits: The sealed keyword marks classes and traits to restrict all their subtypes to be defined in the same file. This is particularly useful for exhaustively defining a known set of types, such as messages for an Akka Actor.
  • override Keyword: The override keyword is required when redefining methods inherited from a superclass or trait.

Traits

  • Traits in Scala are similar to interfaces in Java but more powerful. They’re used to declare or implement fields and methods.
  • Methods in traits can either be implemented or declared without implementation. If a method is only declared, it must be implemented in any concrete subclass unless the subclass is abstract or extends another trait that provides an implementation.
  • Traits can extend other traits or abstract classes.

Abstract Classes

Using traits is generally preferred in Scala. However, there are a few cases where abstract classes are useful:

  • Java Interoperability: Abstract classes can have constructors that Java code can directly call, making them a good choice when interoperability with Java is required.
  • Constructor Arguments: Abstract classes allow you to define base classes with constructor parameters, providing more flexibility in cases where initialization with arguments is needed.

Advanced Class Relationships

  • Type Parameters (Generics):

    • Similar to Java, Scala allows generic classes to take types as parameters, defined with square brackets [].
    • Variance extends generics to define more complex relationships:
      • Covariance: For a type C that is a subtype of B, defining ClassZ[+A] means ClassZ[C] is also a subtype of ClassZ[B]. This is common in collections.
      • Contravariance: For a type C that is a subtype of B, defining ClassZ[-A] means ClassZ[B] is a subtype of ClassZ[C].
      • Invariance: The type is exactly as specified, without variance.
    • Type Bounds:
      • Upper Type Bounds: T <: Animal specifies that T must be a subtype of Animal.
      • Lower Type Bounds: T >: Animal specifies that T must be a supertype of Animal.
    • Abstract Type: Traits and abstract classes can define abstract types to be specified in concrete implementations.
      // Abstract type example
      
      trait TraitWithAbstractType {
        type T
        val value: T
        def length: Int
      }
      
      class ForIntImpl(val value: Int) extends TraitWithAbstractType {
        type T = Int
        def length: Int = value
      }
      
      class ForStringImpl(val value: String) extends TraitWithAbstractType {
        type T = String
        def length: Int = value.length
      }
      
      List[TraitWithAbstractType](new ForIntImpl(1), new ForStringImpl("Hello")).foreach(v => println(v.length))
      
  • Self-Type: A self-type allows a trait A to require another trait B without directly importing it. This ensures that A is only used in classes that also mix in B.

    // Self-type example
    
    trait A {
      val msg = "Hello"
    }
    
    trait B { 
      this: A => // Self-type definition
    
      def printMsg = println(msg)
    }
    
    // class NoImplementA extends B <- Compilation error: A is not available.
    
    class ImplementAAndB extends A with B
    
    val selfTypeExample = new ImplementAAndB
    selfTypeExample.printMsg
    

Inner Classes

Like in Java, Scala allows the definition of inner classes. However, in Scala, an inner class is tied to the instance of its enclosing class. This means that two instances of an inner class created from different instances of the enclosing class are considered different types.

To avoid this default behavior, you can declare parameters or members using the # syntax, which allows you to refer to the inner class type independently of the enclosing instance. For example, you can use Enclosing#Inner to refer to the Inner type within any instance of Enclosing.

class Outer {
  class Inner

  def acceptInner(inner: Inner): Unit = {
    println(s"Only accepts instances created from the Outer instance: $inner")
  }
}

val outer1 = new Outer
val outer2 = new Outer

val inner1: outer1.Inner = new outer1.Inner
val inner2: outer2.Inner = new outer2.Inner

// To accept the type from any instance
def acceptInner(inner: Outer#Inner): Unit = {
  println(s"Accepts instance of Inner from any Outer: $inner")
}

acceptInner(inner1) // Works
acceptInner(inner2) // Works


outer1.acceptInner(inner1) // Works
// outer1.acceptInner(inner2) // Compilation error

Singleton Objects

  • Singleton Objects in Scala are classes that follow the Singleton pattern, ensuring only one instance per process.
  • They are automatically and lazily instantiated the first time they are accessed.
  • Since Scala does not have a static keyword, singleton objects are where you declare all static-like members and methods.
  • Singleton objects are defined using the object keyword instead of class.

Case Classes

Let’s have a look at this example:

sealed trait Example
case class WithDefaultApplyUnapply(id: Int) extends Example
case class CustomApplyUnapply(val id: String) extends Example

object CustomApplyUnapply {
  def apply(id: Int, name: String) = new CustomApplyUnapply(s"${id}/${name}")

  def unapply(obj: CustomApplyUnapply): Option[(Int, String)] = {
    val s = obj.id.split("/")
    Some((s(0).toInt, s(1)))
  }
}

val example: Example = CustomApplyUnapply(1, "One")

example match {
  case WithDefaultApplyUnapply(1) => println("the example is a WithDefaultApplyUnapply with id equals 1")
  case CustomApplyUnapply(1, name) => println(s"The name of 1 is ${name}")
  case _ => println("No match")
}

Case Classes are a special kind of class, defined by the case class keyword, and are ideal for defining immutable data models.

They offer several advantages:

  • Immutable by Default: By default, case class parameters are immutable (val). However, you can make them mutable by using var in the parameter definition.
  • No new Keyword Needed: Case classes automatically define an apply method, allowing you to create instances without using new.
  • Pattern Matching: Case classes are automatically equipped with a companion object containing an unapply method, making them suitable for pattern matching. This makes them extractor objects, enabling you to deconstruct instances into their constituent parts.
  • Equality and Comparison: Case classes implement automatically equals and hashCode, so you can compare them with ==.
  • Copy Method: Case classes have a copy method, which allows you to create a new instance based on an existing one, useful for “updating” immutable objects.
  • Additional Methods: They automatically implement other methods like toString, canEqual, and productIterator.

They come with few limitations:

  • Single Parameter List: Case classes must have exactly one parameter list.
  • Case classes cannot extend other case classes.

Companion objects

Companion Objects are singleton objects associated with a class (or case class), having the same name and defined within the same file. They provide a place for defining all static content related to the class (or case class), such as factory methods or utilities:

  • Static Content: Use the companion object to implement apply methods, builder functions, and other static utilities.
  • Implicit Parameters and Conversions: The companion object is often used as a scope for defining implicit members that meet type requirements within the class.
  • Extractor Objects: Companion objects with an unapply or unapplySeq method are extractor objects. The unapply method reverses the apply method, taking an instance and returning an optional tuple or sequence representing its original parameters. This feature is used in pattern matching.

Functions

Earlier, I mentioned that Scala is a pure OOP language, with even native types implemented as classes, and showcased its strong OOP capabilities. However, the true power of Scala as a functional programming language lies in its treatment of functions.

Below, I’ll cover some of the most important concepts related to functions in Scala:

Methods vs. Functions

In Scala, the terms methods and functions are often used interchangeably, but there is a subtle difference.

Let’s have a look to this example:

def apply(f: Int => String, v: Int) = f(v)

// Method
def decoratorMethod(i: Int): String = s"[$i]"
println(apply(decoratorMethod, 10))

// Function (in this case, apply is a higher-order function)
val decoratorFunction = (i: Int) => s"[$i]"
println(apply(decoratorFunction, 10))
  • Functions: Functions are defined using val, var, or anonymously (also called Lambdas). This reflects Scala’s functions as first-class citizens: functions can be assigned to variables, passed as parameters, and returned as values.
  • Methods: Methods are defined with the def keyword.

Bath can include specified return types, multiple parameter lists, bound to an object or class, etc.

In this example:

  • decoratorMethod is a method, defined with def.
  • decoratorFunction is a function, assigned to a val variable. The apply method demonstrates a higher-order function, as it takes a function as an argument.

By-Value and By-Name Parameters

Scala provides two types of parameters for functions and methods:

  • By-Value Parameters: These are the most common type of parameters. They are evaluated once, before the function or method execution begins, and their value remains constant throughout the function’s execution.
  • By-Name Parameters: By-name parameters are re-evaluated every time they are referenced within the function or method. If they are not referenced, they are not evaluated at all.

It’s important not to confuse lazy values with by-name parameters. While both defer execution, lazy values are only evaluated once, the first time they are accessed, whereas by-name parameters are evaluated each time they are referenced.

Here’s an example to illustrate:

// Lazy value
lazy val lazyTxt = {
  println("Init lazy text")
  "Lazy text"
}

// by value and by name paramters
def print3Times(byValue: String, byName: => String) = {
  println(s"1 ${byValue} / ${byName}")
  println(s"2 ${byValue} / ${byName}")
  println(s"3 ${byValue} / ${byName}")
}

// Print a text every time that it is executed.
def byNameFunctionTxt = {
  println("Executing byNameFunctionTxt method")
  "by name text"
}

print3Times(lazyTxt, byNameFunctionTxt)

// Output
// Init lazy text                     <- One time, the first time that ot is referenced
// Executing byNameFunctionTxt method <- Every time that is referenced
// 1 Lazy text / by name text
// Executing byNameFunctionTxt method <- Every time that is referenced
// 2 Lazy text / by name text
// Executing byNameFunctionTxt method <- Every time that is referenced
// 3 Lazy text / by name text

In this example:

  • lazyTxt is a lazy value that initializes once, the first time it is accessed.
  • byNameFunctionTxt is passed as a by-name parameter to print3Times, causing it to be evaluated every time it’s referenced within the function.

Multiple Parameter Lists, Currying, and Partially Applied Functions

Scala allows methods to be defined with multiple parameter lists. When such methods are called with fewer parameter lists than defined (a Partially Applied Function), the call returns a function that takes the remaining parameter lists as its arguments. This concept is known as Currying.

In simpler terms, if a function takes two list of parameters A and B and returns C, calling it with only the parameter list A will return another function that takes B and produces C.

Here’s an example that demonstrates these concepts:

def apply(f: Int => String, v: Int) = f(v)

// Method
def decoratorMethod(dec: String)(i: Int): String = s"$dec$i$dec" // Multiple parameter list function
println(apply(decoratorMethod("-"), 10))

// Currying: create a new function by partially applied function.  
val yieldedFunction: Int => String = decoratorMethod("-") // <- new function with missing parameters.
println(apply(yieldedFunction, 10))

In this example:

  • Multiple Parameter Lists: decoratorMethod is defined with two parameter lists, (dec: String) and (i: Int).
  • Currying: By partially applying decoratorMethod with only the dec as parameter, we create a new function ( yieldedFunction ) that only requires the i parameter.
  • Partially Applied Function: decoratorMethod("-") returns a partially applied function that can be passed to apply.

Named and Positional Arguments

In Scala, when calling a function, you can either pass parameters in the order they are defined or specify them by name. Named arguments allow you to assign values to parameters explicitly, making the code more readable and flexible.

Using named arguments can be especially helpful in functions with, let’s say, a large number of parameters, as it improves readability and reduces the chance of passing arguments in the wrong order.

def sayHello(name: String, prefix: String): Unit = {
  println(s"$prefix $name!!!!")
}

sayHello("Angel", "Hello") // Ordered arguments
sayHello(name = "Angel", prefix = "Hello") // Named arguments
sayHello(prefix = "Hello", name = "Angel") // Named arguments in any order

Default Parameter Values

In Scala, you can define default values for function parameters. This feature allows you to call a function without specifying all arguments, as the default values will be used for any omitted parameters.

Default parameter values simplify function calls when common values are frequently used.

Keep in mind that you cannot skip over parameters with default values when providing positional arguments immediately after a defaulted one. If you want to specify a later parameter while using defaults for intermediate ones, you’ll need to use named arguments.

def greet(name: String, greeting: String = "Hello"): Unit = {
  println(s"$greeting, $name!")
}

greet("Angel") // Uses the default greeting "Hello"
greet("Angel", "Welcome") // Uses the provided greeting "Welcome"

def greet2(first: String = "First", name: String, greeting: String = "Hello"): Unit = {
  println(s"$greeting, $name!")
}

// greet2("Angel") // This does not compile
greet2("First overrided value", "Angel") // Uses the provided greeting "Welcome"

Higher-Order Functions

Higher-Order functions are functions that either:

  1. Take other functions as parameters.
  2. Return a function as their result.

Here’s an example where we are passing a high-order function to the talk function.

def sayHi(name: String): String = s"Hi $name!"
def sayBye(name: String): String = s"Bye $name!"

def talk(name: String, talk: (String) => String) = println(talk(name))

talk("Angel", sayHi)
talk("Angel", sayBye)

Partial Functions

A Partial Function in Scala is a function that is only defined for certain input values. Unlike regular functions, partial functions may not handle all possible inputs. To manage this, they define two key methods:

  • isDefinedAt: Checks if the function is defined for a given input.
  • applyOrElse: Attempts to apply the function to an input or uses an alternative if it is undefined.

Partial functions are useful for scenarios where only certain cases need handling, allowing you to focus on specific conditions without managing undefined inputs.

Here’s an example:

val yourSurname: PartialFunction[String, String] = {
  case name if name == "Angel" => "Cervera"
  case name if name == "Silvia" => "Roldan"
}

println(Seq("Silvia", "Rodolfo", "Angel")
        .collect(yourSurname)
        .mkString(",")) // Output: Roldan,Cervera

In this example:

  • The yourSurname partial function only handles specific names ("Angel" and "Silvia").
  • Using collect on a sequence with yourSurname applies the function to matching elements, while elements not covered (like "Rodolfo") are ignored.

Polymorphic Methods

Polymorphic methods enable flexible, reusable operations that can work with any compatible data type.

Implementing polymorphic methods in Scala is similar to using generics. Type parameters are defined within square brackets, while method parameters are enclosed within parentheses, as usual.

Here’s an example of a polymorphic method:

// Example Polymorphic methods
def addIfEmpty[A](item: A, lst: List[A]) = if (lst.isEmpty) item :: lst else lst
println(addIfEmpty("XX", List[String]()).length) // Output: 1
println(addIfEmpty("XX", List[String]("AAA")).length) // Output: 1
println(addIfEmpty(3, List[Int](2)).length) // Output: 1

In this example:

  • addIfEmpty is a polymorphic method with a type parameter [A].
  • It takes an item of type A and a list of type List[A].
  • Because of its polymorphic nature, you can use addIfEmpty with any data type that matches the list type, making it both flexible and type-safe.

Compound Types and Mixins in Parameter Lists

In Scala, Compound Types and Mixins can also be used in parameter lists to enforce combinations of types as a single parameter. This allows you to specify that a parameter must implement multiple types simultaneously.

Compound types provide a powerful way to ensure that parameters meet multiple type requirements without requiring complex inheritance structures.

For example:

// Compound types example
def f(param: B with C): D

In this example, the param parameter must be an instance of both B and C.

Closure Functions

Closures are functions that capture values from variables declared outside their own scope. These external variables are known as free variables, while variables defined as function parameters are called bound variables.

In a closure, the function “closes over” the free variables, keeping their values accessible even when the function is executed in a different scope.

Next an example:

val freeVariable = "I'm free"
def closureFnt(boundedVariable: String) = {
  println(s"Content of the free variable: $freeVariable")
  println(s"Content of the bounded variable: $boundedVariable")
}

closureFnt("I'm not free")

// Output: Content of the free variable: I'm free
// Output: Content of the bounded variable: I'm not free

Function Composition

TODO://

Other Scala Features

The first two sections focused on Scala’s object-oriented and functional programming features. However, many of these concepts blur the lines between paradigms, so this classification is inherently subjective.

In the next section, I’ll cover a set of Scala’s features that don’t fit neatly into either programming paradigm, at least in my view.

Pattern Matching

This is one of my favourites features.

Pattern matching in Scala provides a powerful way to write concise, expressive code that can handle a wide range of patterns, making it especially useful in scenarios involving complex data structures.

Pattern matching in Scala is a powerful alternative to Java’s switch-case statement, allowing you to match complex patterns and use more precise control over conditions with pattern guards (using if statements within the match). Each case in a pattern match can return a value, following Scala’s philosophy of declarative programming.

The basic syntax is: ... match { case <pattern> [if <boolean expr>] => {<statement>} }

The behavior of pattern matching depends on the type of the class you’re matching against. You can match based on:

  • Type of Instance: It’s recommended to extend all possible instances from a sealed abstract class or trait, allowing exhaustive pattern matching and compiler checks.
  • Extractor Objects like Case Classes: You can:
    • Match based on field values.
    • Ignore field values.
    • Assign matched field values to variables.
    • Use @ to bind the entire matched pattern to a variable.
  • Wildcard Matching: Use _ to match anything.
  • Multiple Matches in a Single Case: Use the | operator to match multiple cases in one line, like in this example: case TileIndexActor.Done() | LookUpActor.Done() =>

Here’s an example of pattern matching using case classes:

// Example using case classes.
object InCaseClass {
  case class XClass(val name: String)

  val x = new XClass("x")
  val x2 = new XClass("x")

  def test = x match {
    case c if c == x2 => println("Matching because is a case class so equals implementation compare the content")
    case _ => println("No matching")
  }
}

object InNoCaseClass {
  class XClass(val name: String)

  val x = new XClass("x")
  val x2 = new XClass("x")

  def test = x match {
    case c if c == x2 => println("matching")
    case _ => println("No matching because is a class, so use default equals to compare")
  }
}

InCaseClass.test // <- Matching because is a case class so equals implementation compare the content
InNoCaseClass.test // <- No matching because is a class, so use default equals to compare


abstract class Animal(val legs: Int, var heads: Int)

case class Monster(val legs: Int, val hands: Int, var heads: Int)

case class HandsAndFeet(override val legs: Int, val hands: Int) extends Animal(legs, 1)

case class Dog() extends Animal(4, 1)

case class Cockroach() extends Animal(4, 1)

case class Snake() extends Animal(0, 1)

case class NoAnimalAtAll()

def descAnimal(animal: Any) = animal match {
  case Monster(_, _, heads) if heads > 1 => s"I don't care the number of legs or hands, with ${heads} heads, it is a monster for sure!!!!"
  case m@Monster(_, _, heads) if heads == 0 => s"I don't care the number of legs or hands, with no head and ${m.legs} legs, it is a monster for sure!!!!"
  case a: Dog => s"${a.legs} legs, but beautiful because it's a dog"
  case HandsAndFeet(legs, 2) => "Two hands is ok. More is strange."
  case a: Animal if a.legs == 4 => "I don't like animals with 4 legs"
  case a: Animal => s"Other animal with ${a.legs} legs"
  case _ => "What is this? An Alien?"
}

println(descAnimal(Dog())) // <-- 4 legs, but beautiful because it's a dog
println(descAnimal(Cockroach())) // <-- I don't like animals with 4 legs
println(descAnimal(HandsAndFeet(2, 2))) // <-- Two hands is ok. More is stage.
println(descAnimal(Snake())) // <-- Other animal with 0 legs
println(descAnimal(Monster(4, 2, 2))) // <-- I don't care the number of legs or hands, with 2 heads it is a monster for sure!!!!
println(descAnimal(Monster(4, 2, 0))) // <-- I don't care the number of legs or hands, with no head and 4 legs, it is a monster for sure!!!!
println(descAnimal(NoAnimalAtAll())) // <-- What is this? An Alien?

Implicits

Implicits in Scala are a broad topic deserving an article of their own. Here, I’ll summarize key points that cover the most important (if not all) use cases:

Implicit Parameters

Implicit Parameters are parameters marked with the implicit keyword. If provided directly, they behave like regular parameters; if omitted, Scala will attempt to supply them automatically. The search for implicit parameters follows this order:

  1. Accessible Scope: Scala first looks in the accessible scope for members or parameters declared with implicit that return a compatible type.
  2. Companion Objects: Next, Scala looks in the companion object of the type for members marked implicit that return a compatible type.

Implicit Conversions

Implicit Conversions allow Scala to automatically convert between types (for example, using JavaConverters). Scala applies an implicit conversion when:

  • An expression expects type B, but type A is provided. Scala searches for an implicit function that converts A to B and if found, it will be applied.
  • You attempt to call a method m on an instance e of type A, where m is not a member of A. Scala will search for an implicit conversion that transforms A to a type B where m is defined.

To define an implicit conversion from A to B, you can use:

  • A value marked as implicit that define a function with type A => B
  • An implicit method that returns a value of type B.
  • A class marked as implicit that has a constructor A and a method m

Implicit Classes

Implicit Classes are classes marked with the implicit keyword and must have a single constructor parameter. All methods defined within an implicit class become available as if they were methods of the type defined in the constructor parameter. This technique, known as Enriching Classes, is a way to add methods to existing types without subclassing them. Another way to achieve this is through the “Type classes” pattern.

implicitly

implicitly is not another form of implicit; instead, it’s a tool to verify at compile time if an implicit instance is available for a specified type. This can be useful to ensure dependencies are met without explicitly passing them.

Next, a bunch of examples:

// Example of implicit parameters.
object Decorator {
  implicit val decoraror = new Decorator("<<<", ">>>")
}

class Decorator(val start: String, val end: String) {
  def dec(txt: String) = s"${start}${txt}${end}"
}

def printDecorator(txt: String)(implicit dec: Decorator) = dec.dec(txt)

println(printDecorator("Specify the decorator as parameter")(new Decorator("[", "]"))) // <- [Specify the decorator as parameter]

println(printDecorator("Implicit using the decorator in the companion class")) // <- <<<Implicit using the decorator in the companion class>>>

object OtherScope {
  implicit val decoraror = new Decorator(">>>", "<<<")

  def print = println(printDecorator("Implicit using the decorator in the scope"))
}

OtherScope.print // <- >>>Implicit using the decorator in the scope<<<


// Example of implicit conversions.

class PairDecorator(val startAndEnd: String)

implicit def conversionFromPair(pair: PairDecorator): Decorator = new Decorator(pair.startAndEnd, pair.startAndEnd)

println(printDecorator("Implicit using implicit conversion")(new PairDecorator("---"))) // <- ---Implicit using implicit conversion---


implicit class OtherEnrichedDecorator(txt: String) {
  def decorWith(decor: Decorator) = decor.dec(txt)
}

println("Decor using implicit class conversion" decorWith new Decorator("\\\\", "//")) // <- \\Decor using implicit class conversion//

// Example of implicit classes

object ImplicitClassesExample {
  implicit class StringEnricher(str: String) {
    def addPrefixAndSuffix(xFix: String) = s"${xFix}${str}${xFix}"
  }
}

import ImplicitClassesExample.StringEnricher

println("Hello".addPrefixAndSuffix("XXXXX"))

Type aliases

type RowKey = (Long, String, Boolean)
type CountryID = String

Tuple Unpacking

Tuples in Scala resemble a list of function arguments, but they aren’t directly interchangeable with argument lists. However, it’s possible to work around this limitation by transforming the method to accept a tuple.

val tuple = ("A", "B")
def concat(a: String, b: String) = a + b
concat _ tupled (tuple)

The process involves two main steps:

  1. Eta Expansion: Using _ transforms the concat method into a function of type (String, String) => String. Documentation
  2. Tupled Conversion: The tupled method creates a version of this function that accepts a single tuple parameter. This allows us to pass a tuple directly to the function.

With these steps, it’s possible to “unpack” a tuple to match function parameters.

Operators

In Scala, there are three types of operators:

  • Prefix: The operator (the method executed) comes before the object (e.g., -7).
  • Postfix: The operator (the method executed) comes after the object (e.g., 7 toLong).
  • Infix: The operator (the method executed) sits between the object and the parameter (e.g., 7 + 1).

In the case of Prefix and Postfix operators, they are unary and take only one operand.

Prefix Operators

  • To define a prefix operator, the method name must begin with unary_ followed by the operator’s symbol. For example, to define the - operator, use def unary_- = v * -1.
  • Only four operators can be defined as prefix: +, -, !, and ~.

Postfix Operators

  • Postfix operators are methods without parameters.
  • To use postfix operators, you must import scala.language.postfixOps.

Infix Operators

  • Infix operators are methods that take a single parameter. This allows for a natural syntax without dots or parentheses, so a + b is equivalent to a.+(b).

  • By default, all operators are left-associative and bind to the expression on their left (like common functions). However, operators ending with a colon (:) are right-associative and bind to the expression on their right.

  • Infix operators have different precedence levels based on their first character, ordered as follows:

    1. (characters not shown below)
    2. * / %
    3. + -
    4. :
    5. = !
    6. < >
    7. &
    8. ^
    9. |
    10. (all letters)

Annotations

In my opinion, only two special annotations in Scala are worth mentioning:

  • @tailrec: This annotation ensures that a method is tail-recursive. The Scala compiler verifies this at compile time, allowing it to optimize the method into a loop, which significantly reduces memory usage for recursive calls.

  • @inline and @noinline:

    • @inline directs the compiler to replicate the function’s body at the call site rather than invoking it. This increases binary size but can improve performance by reducing function call overhead.
    • @noinline explicitly prevents inlining.

    If neither annotation is present, the compiler will attempt to choose the optimal strategy. To enable inlining, use the -opt:l:inline flag during compilation.

Design Patterns

A few design patterns are particularly worth mentioning in Scala:

Type Classes

Type Classes allow you to add functionality to types by associating them with traits, without relying on inheritance. This approach, inspired by Haskell, is a powerful composition technique in Scala that enhances flexibility and modularity.

Type classes are widely used in libraries like Scalaz, Cats, and Shapeless.

Here’s an example, were we extend Dog and Cat with the AnimalLang trait:

case class Dog(weight: Int)
case class Cat()

trait AnimalLang[C] {
  def hello: String
  def toneLevel(c: C): Int

  def greetings(c: C): String =
    s"$hello ${(0 to toneLevel(c)).map(_ => "!").mkString}"
}

// Type class instances for Dog
implicit val DogLang: AnimalLang[Dog] = new AnimalLang[Dog] {
  override def hello: String = "Guau"
  override def toneLevel(d: Dog): Int = if (d.weight > 10) 10 else 5
}

// Type class instances for Cat
implicit val CatLang: AnimalLang[Cat] = new AnimalLang[Cat] {
  override def hello: String = "Miau"
  override def toneLevel(c: Cat): Int = 2
}

// Generic method that requires an implicit AnimalLang instance for C
def sayHello[C: AnimalLang](c: C) =
  println(implicitly[AnimalLang[C]].greetings(c))

sayHello(Dog(10))  // Output: Guau !!!!!!
sayHello(Dog(3))   // Output: Guau !!!!!!
sayHello(Cat())    // Output: Miau !!!

In this example:

  • The AnimalLang trait defines common methods that can apply to any type.
  • DogLang and CatLang provide specific implementations for Dog and Cat respectively.
  • The sayHello method uses a type class context bound [C: AnimalLang] to require an implicit AnimalLang instance for any type C.

Cake Pattern

The Cake Pattern is a Dependency Injection (DI) technique that uses self-types to specify type dependencies without exposing them in the inheritance hierarchy, allowing dependencies to be injected while preserving type safety and reducing boilerplate.

trait Repository {
  def save(user: User): Unit
}

trait DatabaseRepository extends Repository {
  override def save(user: User): Unit = {
    println(s"Saving user: $user")
  }
}

trait UserService {
  self: Repository => // requires to be used along with a Repository instance
  def create(user: User): Unit = {
    // Use the save method from Repository
    save(user)
  }
}

// Create a UserService instance with a DatabaseRepository dependency injected
new UserService with DatabaseRepository

In this example:

  • UserService declares a self-type self: Repository =>, requiring a Repository to be mixed in without directly inheriting from it.
  • By mixing UserService with DatabaseRepository, you satisfy the dependency and enable UserService to use Repository methods.

Here’s your closing with a few minor tweaks for flow:

What’s Next?

This has been my collection of general notes on Scala, and I think it’s large enough to stop here. Time to relax and maybe grab a beer! I also have notes on functional programming concepts, Scala collections, Spark, Cats, Scalaz, and more, which I’ll organize into separate posts.

If you’ve made it to the end of this article, please feel free to ping me on LinkedIn or send me an email to let me know. It would truly make my day!

Useful references and links