Hire Me: (+353) 86 074 8999 or via LinkedIn
Scala Notes from 2015, still valid today.
Table of content
- Context and Motivation
- The Cornerstones of Functional Programming
- Why Scala?
- Object-Oriented Programming (OOP) and Functional Programming (FP) in Scala
- Global Classes Hierarchy
- Variables or Values
- Classes in Scala
- Modeling via OOP
- Functions
- Methods vs. Functions
- By-Value and By-Name Parameters
- Multiple Parameter Lists, Currying, and Partially Applied Functions
- Named and Positional Arguments
- Default Parameter Values
- Higher-Order Functions
- Partial Functions
- Polymorphic Methods
- Compound Types and Mixins in Parameter Lists
- Closure Functions
- Function Composition
- Other Scala Features
- Design Patterns
- What’s Next?
- Useful references and links
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++ - evenUnit
is an object. - Provides functional types for handling nullity and side effects, like
Option
,Either
, andTry
. - 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.
So basically:
-
Any
: This is the root type of the Scala type system. Every type in Scala inherits fromAny
, making it the ultimate superclass. TheAny
type has two main subclasses:AnyVal
(for value types) andAnyRef
(for reference types). -
AnyVal
: This is the root type for all types that represent values. It includes primitive-like types such asByte
,Short
,Int
,Long
,Float
,Double
,Char
,Boolean
, andUnit
. 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’sObject
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
norvar
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]
orprotected[X]
, which limits access to withinX
, whereX
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
orvar
) 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: Theoverride
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 ofB
, definingClassZ[+A]
meansClassZ[C]
is also a subtype ofClassZ[B]
. This is common in collections. - Contravariance: For a type
C
that is a subtype ofB
, definingClassZ[-A]
meansClassZ[B]
is a subtype ofClassZ[C]
. - Invariance: The type is exactly as specified, without variance.
- Covariance: For a type
- Type Bounds:
- Upper Type Bounds:
T <: Animal
specifies thatT
must be a subtype ofAnimal
. - Lower Type Bounds:
T >: Animal
specifies thatT
must be a supertype ofAnimal
.
- Upper Type Bounds:
- 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))
- Similar to Java, Scala allows generic classes to take types as parameters, defined with square brackets
-
Self-Type: A self-type allows a trait
A
to require another traitB
without directly importing it. This ensures thatA
is only used in classes that also mix inB
.// 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 ofclass
.
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 usingvar
in the parameter definition. - No
new
Keyword Needed: Case classes automatically define anapply
method, allowing you to create instances without usingnew
. - 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
andhashCode
, 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
, andproductIterator
.
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
orunapplySeq
method are extractor objects. Theunapply
method reverses theapply
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 withdef
.decoratorFunction
is a function, assigned to aval
variable. Theapply
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 toprint3Times
, 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 thedec
as parameter, we create a new function (yieldedFunction
) that only requires thei
parameter. - Partially Applied Function:
decoratorMethod("-")
returns a partially applied function that can be passed toapply
.
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:
- Take other functions as parameters.
- 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 withyourSurname
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 typeList[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:
- Accessible Scope: Scala first looks in the accessible scope for members or parameters declared with
implicit
that return a compatible type. - 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 typeA
is provided. Scala searches for an implicit function that convertsA
toB
and if found, it will be applied. - You attempt to call a method
m
on an instancee
of typeA
, wherem
is not a member ofA
. Scala will search for an implicit conversion that transformsA
to a typeB
wherem
is defined.
To define an implicit conversion from A
to B
, you can use:
- A value marked as
implicit
that define a function with typeA => B
- An implicit method that returns a value of type
B
. - A class marked as
implicit
that has a constructorA
and a methodm
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:
- Eta Expansion: Using
_
transforms theconcat
method into a function of type(String, String) => String
. Documentation - 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, usedef 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 toa.+(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:
- (characters not shown below)
*
/
%
+
-
:
=
!
<
>
&
^
|
- (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
andCatLang
provide specific implementations forDog
andCat
respectively.- The
sayHello
method uses a type class context bound[C: AnimalLang]
to require an implicitAnimalLang
instance for any typeC
.
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-typeself: Repository =>
, requiring aRepository
to be mixed in without directly inheriting from it.- By mixing
UserService
withDatabaseRepository
, you satisfy the dependency and enableUserService
to useRepository
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
- Official Scala tour
- Scala Center
- Scala online playground
- Resources about patterns and design
- About collections
- Github Scala awesome:
- Puzzles:
- Best practices and coursers:
- Type Erasure
- Functional streams in Scala