Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Learning Scala

Scala 3 is a multi-paradigm programming language that supports both object-oriented and functional programming.

Why Scala

  • Concise and expressive code: Scala's functional programming features allow developers to write code that is more concise and expressive. This is because functional programming encourages the use of higher-order functions, which can be used to abstract away common patterns in code.
  • Better code quality: Functional programming encourages immutability and pure functions, which can result in code that is easier to reason about and has fewer bugs.
  • Parallel and distributed computing: Functional programming allows for easy parallel and distributed computing, as pure functions can be executed in parallel without the need for locks or other synchronization mechanisms.
  • Type safety: Scala is a statically typed language, which means that type errors are caught at compile time. This can help catch errors early on in the development process and make code more reliable.
  • Easy integration with other systems: Scala is built on top of the JVM, which makes it easy to integrate with existing Java systems. Additionally, Scala is compatible with many other technologies, such as Akka and Apache Spark.
  • Overall, functional programming in Scala offers many benefits, including increased code quality, better parallel and distributed computing, and easy integration with existing systems.

Getting Started

Create a 'Hello, World' scala application

In a browser

On a computer

As result Java and Scala are installed on your computer

Scala REPL

The scala installation contains a Scala command-line playground, the REPL (Read Eval Print Loop)

On the command-line type

scala                                               

Otherwise: in intelliJ type Run -> Tools -> Scala REPL...

This will print a welcome message and give the scala prompt

Welcome to Scala 3.7.2 (24.0.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                  
scala> 

Hello, World

At the prompt we type the scala statements and end with Enter
The statement is evaluated and the result is printed

print ("Hello, World")
// Hello, World

IntelliJ

The other way is using an IntelliJ project

Prerequisites

  • Java (latest)
  • Scala 3
  • IntelliJ iDEA
  • Scala Plugin

New Project

In IntelliJ start a new project. File -> New -> Project...
Configure your project as below. If possible, select the latest JDK, SBT and Scala 3.

Select 'Use significant indentation syntax (Optional Braces)'

We will use the Scala 3 Pythonesque syntax.

//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
@main
def main(): Unit =
  //TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
  // to see how IntelliJ IDEA suggests fixing it.
  (1 to 5).map(println)

for (i <- 1 to 5) do
  //TIP Press <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
  // for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
  println(s"i = $i")


Run the application

Run the application to see if everything is working.
Click on the green arrow on line 2.
The application runs on the Terminal

Variable

This Scala code creates a value named "x" and assigns it the value of 5.
The "val" keyword is used to declare an immutable variable in Scala, which means that once the value is assigned, it cannot be changed. Here's an example of how you can use this value in Scala:

val x = 5
println(x) // Output: 5

In this example, the value of "x" is printed to the console using the "println" method. Since "x" is immutable, you cannot reassign it to a new value later in the code.

Inference

Scala is a statically-typed programming language, which means that every variable and expression must have a declared type. However, Scala also supports type inference, which allows the compiler to deduce the type of an expression automatically based on its context.

Type inference in Scala is performed by the compiler using a process called type unification. When a variable or expression is declared without an explicit type, the compiler attempts to infer the type by examining the types of other expressions in the same context. For example, consider the following code snippet:

val x = 5
val y = "Hello, world!"

val z = x + y

In this case, x is assigned an integer value, so its type is inferred to be Int. y is assigned a string value, so its type is inferred to be String. When the expression x + y is assigned to z, the compiler determines that the + operator is only defined for two arguments of numeric types, so it infers that x must also be a String and produces a compile-time error.

Type inference in Scala can greatly reduce the amount of boilerplate code required for variable and function declarations while still maintaining strong type safety. However, it's important to keep in mind that type inference is not always possible, particularly in complex or ambiguous situations, so it's still necessary to understand the type system and use explicit type annotations when appropriate.

Immutable

val

A val is an immutable variable. A constant.
It can not be reassigned and will give an error when you try.

scala> x = 4
-- `[E052]` Type Error: ----------------------------------------------------------
1 |x = 4
  |^^^^^
  |Reassignment to val x

Variable Names

The Scala variable names follow the Java conventions:

  • The name of a variable must begin with a letter, a dollar sign $ or a underscore _
  • The following characters can be letters or numbers. For example: number, A1, $value, _number
  • A letter is a Unicode character. This means that you can choose from 34,186 characters
  • White space is not allowed in variable name.
  • Uppercase, lowercase and accented letters are allowed.
  • The maximum length is unlimited
  • Scala distinguishes uppercase and lowercase letters, so KM, Km, kM, and km are different names.
  • Keywords are not allowed.

Valid names

number, thisIsANumber, this_is_a_number, n1, A1, $value, _number 

Invalid names

1value, a.b, a!, b?, type  

Number Types

Scala is a strong type language. Every variable must has a type.
We first take a look at the basic number types inherited from Java.

naamrolesize(bits)min to max value
Bytewhole8-128 to +127
Shortwhole1632.768 to 32.767
Intwhole32-2.147.483.648 to +2.147.483.747
Longwhole64-9.223.372.036.854.775.808 to 9.223.372.036.854.775.808
Floatreal323,40292347E+38 to +3,40292347E+38
Doublereal64-1,79769313486231570E+308 to +1,79769313486231570E+308
Charletter16Unicode '\u0000 to '/uFFFF
Booleantruth valuetrue or false

When you need a whole number take an Int and only when an Int is too small take a Long.
My advice is not to use a Byte and Short, because there are some corner cases.

When you work with texts you will use a String.
Most libraries will have functions on String and not on Char even if the text contains only one letter.

Variable Exercises

Examples

Create a val called pi and assign it the value of 3.14159:

val pi: Double = 3.14159

Create a val called message and assign it a string value:

val message: String = "Hello, world!"

Create a val called age and assign it an integer value:

val age: Int = 30

Create a val called isTall and assign it a boolean value:

val isTall: Boolean = true

Create a val called name and assign it a string value, then print a message that includes the value of name:

val name: String = "Alice"
println(s"My name is $name.")

In this example, the string interpolation syntax is used to include the value of name in the printed message. The dollar sign ($) followed by the variable name is replaced with the value of the variable. The s at the beginning of the string indicates that this is a string interpolation.

Operators

In a Computer Everything is a Number.
Strings, Images, Audio, Video are All Encoded as Numbers.

Scala does have some operators to do the basic calculations.
But with these basic thing you can do everything

  • toUpperCase on a text
  • darken an image
  • lower the volume on audio
  • calculate the average on a dataset

Arithmetic operators

val a = 5
val b = 2
val sum = a + b  // Addition operator
val difference = a - b  // Subtraction operator
val product = a * b  // Multiplication operator
val quotient = a / b  // Division operator
val remainder = a % b  // Modulus operator
operatorfunction
+add
-subtract
*multiply
/divide
%modulo

Examples

scala> 
val a: Int = 4
val b: Int = 3

scala> val add = a + b
val add: Int = 7
                                                                              
scala> val subtract = a - b
val subtract: Int = 1

scala> val multiply = a * b
val multiply: Int = 12
 
scala> val divide = a / b
val divide: Int = 1

This is an integer division. So it will give an integer value.

modulo

Modulo is the rest of an integer division

scala> val modulo = a % b
val modulo: Int = 1

divide floating point

If you want a floating point division then you have to cast an Int to a Double

scala> val d: Double = a / b
val d: Double = 1.0

To change to type to a Double is no enough, also a typecast is needed: toDouble

scala> val d: Double = a / b.toDouble
val d: Double = 1.3333333333333333

Comparison operators

operatorfunction
>greater then
<smaller then
==equals
>=greater and equals
<=smaller and equals
val x = 10
val y = 5
val isGreaterThan = x > y  // Greater than operator
val isLessThan = x < y  // Less than operator
val isEqualTo = x == y  // Equality operator
val isNotEqualTo = x != y  // Inequality operator
scala> b < 4
val res0: Boolean = true
                                                                                
scala> b > 4
val res1: Boolean = false
                                                                                
scala> b == 3
val res2: Boolean = true
                                                                                
scala> b >= 4
val res3: Boolean = false
                                                                                
scala> b <= 4
val res4: Boolean = true


Logical operators

operatorfunction
&&AND
||OR
!NOT
val p = true
val q = false
val andResult = p && q  // Logical AND operator
val orResult = p || q  // Logical OR operator
val notResult = !p  // Logical NOT operator
scala> b != 4
val res5: Boolean = true
                                                                                
scala> b > 2 && b < 4
val res6: Boolean = true
                                                                                
scala> b < 2 || b > 4
val res7: Boolean = false

Choices

If

The if statement is a fundamental control flow mechanism in Scala, as in many other programming languages. It allows you to execute a block of code conditionally, depending on whether a Boolean expression evaluates to true or false. Scala's if statement is similar to that in Java, C, and many other languages, but with some functional programming twists that enhance its utility and flexibility.

Basic if Statement

The simplest form of an if statement in Scala is:

if condition then 
  // Block of code to execute if the condition is true

For example:

val age = 18

if age >= 18 then
  println("You are an adult.")

This code checks if age is 18 or older and prints a message if the condition is true.

if-else Statement

The if-else statement extends the if statement by allowing you to execute an alternative block of code if the condition is false:

if condition then 
  // Block of code to execute if the condition is true
else 
  // Block of code to execute if the condition is false

For example:

if age >= 18 then
  println("You are an adult.")
else 
  println("You are a minor.")

if-else if Ladder

For multiple conditions, Scala supports the if-else if ladder, similar to other languages:

if condition1 then
  // Block of code for condition1
else if condition2 then 
  // Block of code for condition2
else 
  // Block of code if none of the above conditions are true

Example:

if age < 13 then
  println("You are a child.")
else if age < 18 then
  println("You are a teenager.")
else 
  println("You are an adult.")

if Expressions and Value Assignment

One of Scala's functional programming features is treating if-else as an expression that returns a value. This allows you to assign the result of an if-else directly to a variable:

val classification = if age < 13 then "child"
  else if age < 18 then "teenager"
  else "adult"

println(s"You are a $classification.")

This feature eliminates the need for the ternary operator (condition ? trueValue : falseValue) found in other languages, as if-else in Scala can be used in the same concise way.

Nested if Statements

if statements can be nested within each other to check multiple conditions:

val isSunny = true
val temperature = 25  // Celsius

if isSunny then 
  if temperature > 20 then
    println("It's a nice day for a walk.")
  else 
    println("It's cool outside, but sunny.")
else 
  println("It might rain today.")

Match

The match expression in Scala is a powerful feature that extends the concept of switch-case statements found in other languages. It allows you to match against a value, then execute a block of code depending on the match. Scala's match is more potent than a traditional switch-case because it can match types, values, patterns, and even guard expressions. It's a cornerstone of Scala's pattern matching capabilities, enabling concise and expressive code.

value match 
  case pattern1 => // Block of code for pattern1
  case pattern2 => // Block of code for pattern2
  // ...
  case _ => // Block of code if none of the patterns match (default case)

Examples

Matching Simple Values

val dayOfWeek = 3

val dayName = dayOfWeek match 
  case 1 => "Sunday"
  case 2 => "Monday"
  case 3 => "Tuesday"
  case 4 => "Wednesday"
  case 5 => "Thursday"
  case 6 => "Friday"
  case 7 => "Saturday"
  case _ => "Invalid day"

println(dayName) // Output: Tuesday

Multiple match

In Scala, the match expression can also be used to match multiple patterns and execute the same code for all matched patterns. This is done using the | operator.

val x: Int = 2

val result: String = x match 
  case 1 | 2 | 3 => "x is between 1 and 3"
  case 4 | 5 | 6 => "x is between 4 and 6"
  case _ => "x is outside the range"

println(result)

In this example, we use the | operator to match the value of x against multiple patterns. The code in the first block will execute if x is 1, 2, or 3. The code in the second block will execute if x is 4, 5, or 6. The code in the last block, which uses the underscore _ pattern, will execute if no other pattern matches.

Note that the | operator can be used with any pattern, including custom patterns.

Matching Types

def process(value: Any): Unit = value match 
  case s: String => println(s"String of length ${s.length}")
  case i: Int => println(s"Integer: $i")
  case _ => println("Unknown type!")


process("Hello")
process(123)
process(3.14)

Matching with Guards

You can add conditional expressions (guards) to your cases using if:

val number = 10

val parity = number match 
  case n if n % 2 == 0 => "even"
  case _ => "odd"
}

println(parity) // Output: even

Tips for Using Match Expressions

  • Exhaustiveness: Scala's compiler checks if the match cases are exhaustive, meaning all possible cases are covered. This helps prevent runtime errors.
  • Default Case: Always provide a default case (case _ =>) to handle unexpected values, unless you are sure the match cases are exhaustive.
  • Pattern Guards: Use guards to refine your match cases with additional conditions.

Loops

While

The while loop in Scala, as in many other programming languages, is a control flow statement that allows code to be executed repeatedly based on a given Boolean condition. The loop will continue to execute as long as the condition evaluates to true. It's a fundamental construct that's useful in scenarios where the number of iterations is not known before the loop starts.

while condition do 
  // Code block to be executed

Here, condition is a Boolean expression. If condition evaluates to true, the code block inside the while loop is executed. After each execution, condition is evaluated again, and if it's still true, the loop continues. This repeats until condition becomes false.

Example of while Loop

Let's look at a simple example where we use a while loop to iterate until a variable x becomes less than or equal to zero:

var x = 10
while x > 0 do 
  println(x)
  x -= 1 // Decrement x by 1

In this example, we start with x equal to 10, and in each iteration of the loop, we decrement x by 1 and print the current value of x. The loop terminates when x is no longer greater than 0.

Considerations When Using while Loops

  • Mutability: while loops often rely on mutable state (like the x variable in the examples) to control the loop's execution. This can be at odds with Scala's emphasis on immutability and functional programming principles.
  • Functional Alternatives: In many cases, especially when working with collections, functional alternatives such as foreach, for, map, filter, and fold can achieve the same results as a while loop but in a more concise and expressive way. These constructs also encourage immutability and side-effect-free programming.

Example of Replacing while with Functional Constructs

Consider a scenario where we want to sum all numbers from 1 to n. Instead of using a while loop with mutable state, we could use a functional and immutable approach:

val n = 10
val sum = (1 to n).sum
println(sum)

This code snippet uses a range (1 to n) and the sum method to calculate the total sum in a concise, readable, and immutable manner.

For

for item <- collection do 
  // Do something with item

Examples

Iterating Over a Range

for i <- 1 to 5 do
  println(i)

This will print numbers 1 through 5. Scala ranges are inclusive by default when you use to. If you want an exclusive range, you can use until:

for i <- 1 until 5 do
  println(i)

This prints numbers 1 through 4.

Iterating Over a Collection

val fruits = List("apple", "banana", "cherry")
for fruit <- fruits do
  println(fruit)

This iterates over each element in the fruits list and prints it.

For-Comprehensions

Scala's for loop can also be used to create new collections from existing ones, a feature known as for-comprehensions. When used in this way, you enclose the iteration logic in parentheses and use the yield keyword to produce values that form a new collection.

Example: Squaring Numbers

val numbers = List(1, 2, 3, 4, 5)
val squaredNumbers = for n <- numbers yield n * n
println(squaredNumbers)  // List(1, 4, 9, 16, 25)

Adding Filters

You can add filters within a for loop using an if clause to control which elements should be included in the iteration.

Example: Filtering Even Numbers

for 
  n <- 1 to 10 
  if n % 2 == 0 
do
  println(n)

This prints even numbers between 1 and 10.

Nested Loops

Scala allows nesting of for loops by adding multiple generators separated by semicolons.

Example: Generating Combinations

for 
  x <- 1 to 3 
  y <- 1 to 3 
do 
  println(s"($x, $y)")

This prints all combinations of (x, y) pairs where x and y range from 1 to 3.

For-Comprehensions with Guards

Just like simple for loops, for-comprehensions can also include guards.

Example: Filtering and Mapping

val evensSquared = 
  for 
    n <- 1 to 10
    if n % 2 == 0
  yield n * n

println(evensSquared)  // Vector(4, 16, 36, 64, 100)

This creates a collection of the squares of even numbers between 1 and 10.

String

Strings in Scala are sequences of characters. Scala strings are instances of the String class in Java, which means you can use any method from the Java String class in Scala. Additionally, Scala adds its own set of methods to strings through implicit conversions to StringOps to facilitate common operations in a more Scala-like way.

Creating Strings

Creating a string in Scala is straightforward and similar to other languages:

val greeting = "Hello, World!"

String Interpolation

Scala supports string interpolation, allowing you to embed variable references directly in string literals.

  • s-Interpolator: Prepend s to the string and use $ to insert variables.
  val name = "Scala"
  val message = s"Hello, $name!"
  println(message)  // Output: Hello, Scala!

You can also use ${} to insert more complex expressions:

  val temperature = 20.5
  val weatherMessage = s"The current temperature is ${temperature}°C."
  println(weatherMessage)  // Output: The current temperature is 20.5°C.
  • f-Interpolator: Similar to s, but allows formatting.
  val height = 1.9
  val formattedMessage = f"$name%s is $height%2.2f meters tall."
  println(formattedMessage)  // Output: Scala is 1.90 meters tall.
  • raw-Interpolator: Works like s but does not escape literals.
  println(raw"New\nLine")  // Output: New\nLine

Common Operations

  • Length: Get the number of characters.
  val length = greeting.length
  • Concatenation: Combine strings using +.
  val fullGreeting = greeting + " How are you?"
  • Substrings: Extract parts of a string.
  val hello = greeting.substring(0, 5)  // "Hello"
  • Comparisons: Compare two strings, optionally ignoring case.
  val isEqual = "Scala" == "scala"  // false
  val isEqualIgnoreCase = "Scala".equalsIgnoreCase("scala")  // true
  • Searching: Check if a string contains a sequence or matches a pattern.
  val containsWorld = greeting.contains("World")  // true
  • Splitting: Split a string into an array of substrings.
  val words = greeting.split(", ")  // Array("Hello", "World!")
  • Trimming: Remove leading and trailing whitespaces.
  val spaced = "  Hello, World!  "
  val trimmed = spaced.trim  // "Hello, World!"
  • Replacing: Replace parts of the string.
  val replaced = greeting.replace("World", "Scala")  // "Hello, Scala!"
  • Case Conversion: Convert to upper or lower case.
  val upper = greeting.toUpperCase  // "HELLO, WORLD!"
  val lower = greeting.toLowerCase  // "hello, world!"

Multiline Strings

Scala supports multiline strings using triple quotes, preserving line breaks and spaces:

val multilineString =
  """This is a
    |multiline string
    |in Scala."""
    .stripMargin

The .stripMargin method removes leading spaces up to and including the character |, making it easier to format multiline strings neatly.

Functions

A function is a number of instructions grouped together.
A function has a name, input parameters and a return value.
In scala there are two types of functions: funtion and method.

Method

an example of a simple function in Scala that takes two integers as parameters and returns their sum:


def add(x: Int, y: Int): Int = 
  x + y
  
// Example usage
val result = add(3, 5) // 8

In this example, the add function takes two parameters of type Int, named x and y. The function body simply adds these two parameters together and returns the result as an Int. The function definition ends with a colon followed by the return type of the function, which in this case is Int. Scala supports type inference, so in simple cases like this, you can often omit the return type and let the compiler figure it out.

Function

In Scala, a val can hold a function as a value. Here's an example of defining a function as a val:

val add: (Int, Int) => Int = (x, y) => x + y

// Example usage
val result = add(3, 5) // 8

In this example, we define a val named add which has the type (Int, Int) => Int. This means that add is a function that takes two parameters of type Int and returns a value of type Int.

The right-hand side of the = sign is a function literal, which is an anonymous function that takes two parameters x and y, adds them together, and returns the result.

Because the Scala compiler can usually infer the types of function parameters and return values, we can usually omit the type annotations and simply write:

val add = (x: Int, y: Int) => x + y

Default Parameters

Scala allows you to specify default values for function parameters. If the caller omits those parameters, Scala uses the default values.

def greet(name: String, greeting: String = "Hello"): String = s"$greeting, $name!"

println(greet("Alice")) // Output: Hello, Alice!
println(greet("Alice", "Hi")) // Output: Hi, Alice!

Named Arguments

When calling functions, you can specify arguments by name, rather than strictly by position. This is particularly useful when a function has multiple parameters with default values.

def describePerson(name: String, age: Int, country: String = "Unknown"): String =
  s"$name is $age years old from $country."

println(describePerson(age = 30, name = "Bob")) // Output: Bob is 30 years old from Unknown.

Variable Arguments (Varargs)

Scala allows you to define functions that take an arbitrary number of arguments of the same type. This is achieved using the * syntax.

def sum(nums: Int*): Int = nums.sum

println(sum(1, 2, 3, 4, 5)) // Output: 15

Anonymous Functions (Function Literals)

Scala allows the definition of anonymous functions, which are handy for short snippets of code that are passed to higher-order functions or used for creating quick and simple function objects.

val addOne = (x: Int) => x + 1
println(addOne(9)) // Output: 10

Tuples

Tuples can hold between two and twenty-two items. They are instantiated by enclosing the items in parentheses, separated by commas.

val myTuple = (1, "hello", true)

This creates a tuple containing an Int, a String, and a Boolean.

Accessing Tuple Elements

Elements of a tuple are accessed using the ._n syntax, where n is the 1-based index of the element.

val number = myTuple._1  // 1
val greeting = myTuple._2  // "hello"
val flag = myTuple._3  // true

Tuple Destructuring

Scala allows "destructuring" tuples into individual variables:

val (num, greet, flg) = myTuple

After this operation, num will be 1, greet will be "hello", and flg will be true.

Using Tuples in Functions

Tuples are particularly useful for functions that need to return multiple values. Here's an example:

def minMax(values: Array[Int]): (Int, Int) = {
  (values.min, values.max)
}

val limits = minMax(Array(1, 2, 3, 4, 5))
// limits is now (1, 5)

Tuples and Maps

Tuples are often used with Scala's Map collection since a map is essentially a collection of key-value pairs, and each key-value pair can be represented as a tuple:

val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")

// Adding a new key-value pair
val newCapitals = capitals + ("USA" -> "Washington D.C.")

Here, "France" -> "Paris" is syntactic sugar for creating a tuple of two elements.

Operations

OperationExampleResult
size(1, 2, 3).size3
head(3 *: 4 *: 5 *: EmptyTuple).head3
tail(3 *: 4 *: 5 *: EmptyTuple).tail(4, 5)
*:3 *: 4 *: 5 *: 6 *: EmptyTuple(3, 4, 5, 6)
drop(1, 2, 3).drop(2)(3)
take(1, 2, 3).take(2)(1, 2)
apply(1, 2, 3)(2)3
splitAt(1, 2, 3, 4, 5).splitAt(2)((1, 2), (3, 4, 5))
zip(1, 2, 3).zip(('a', 'b'))((1 'a'), (2, 'b'))
toList(1, 'a', 2).toListList(1, 'a', 2)
toArray(1, 'a', 2).toArrayArray(1, '1', 2)
toIArray(1, 'a', 2).toIArrayIArray(1, '1', 2)

Object Oriented

Scala runs on the JVM which based on Java.
So Scala can do everything Java can.
And has all the Java object-oriented features and more

Basic concepts:

  • class and object
  • inheritance
  • abstraction (abstract class, trait)
  • polymorphism
  • data hiding

Class

A class has members:

  • field
  • method

A class is instantiated to become an object.

Fields are variables in the object scope. Methods are functions in the object scope and can implicitly
access the fields and other methods.

class Greeter:
  val name = "John"                             // field
  def sayHello() =                              // method
    s"Hello, $name"                             // access the field

val greeter = new Greeter                       // instance
greeter.name = "Lucy"                           // dot operator
val greeting = greeter.sayHello()               // call the method
println(greeting) 

Constructor

It is a good practise to make classes immutable.
This means that fields are private and written in the constructor

class Greeter (name: String = "John"):          // constructor 
  def sayHello() =
    s"Hello, $name"

val greeter = new Greeter("Lucy")               // call the constructor
val greeting = greeter.sayHello()
println(greeting)

Constructor args

class Number(x: Int)                            // private
class Number(val x: Int)                        // getter
class Number(var x: Int)                        // getter and setter    

Object

In Scala an object is a singleton class.
An object only has one instance. So every object is the same instance.

It can be used as counterpart of static methods and fields in Java.

An object has a special method apply.
Apply is called with the object name and a parameter list.
In the example below the

Greeter("Lucy") calls Greeter.apply("Lucy")

object Greeter:
  private var name: String = "John"             // private field
  def apply(name: String) =                     // apply method
    this.name = name
  def sayHello() =
    s"Hello, $name"


Greeter("Lucy")                                 // call apply method
val greeting = Greeter.sayHello()               // only one instance
println(greeting)
  

Inheritance

We can extend a class with inheritance. You could see inheritance as a copy-and-paste without copying-and-pasting

All the member from the base class are available in the inherited class.

class Meeter(name: String = "John") extends Greeter(name):   // inheritance
  def greetAndMeet() =                          // extra method
    "Meet and " + sayHello()                    // from base class

val meeter = new Meeter("Lucy")                 // call the constructor
val greeting = meeter.greetAndMeet()
println(greeting)

Information hiding

You can restrict the access to class members. Scala has the next levels of access

syntaxlevelcan access
publiceverybody
protectedprotectedneighbours and children
private[pkg]package privatein package
privateprivateonly me and companion
private[this]strict privatenot the companion object

In Scala the default is public. This you do not have to write

class AccessLevels:
  def everybody() = {}
  protected def children() = {}
  private[util] def neighbours() = {}
  private def fromCompanion() = {}
  private[this] def onlyMe() = {}

Traits

abstract class Shape(w: Int, h: Int):           // abstract class
  def area: Double                              // abstract method

class Rect(w: Int, h: Int) extends Shape(w, h):
  override def area: Double = w * h             // implement method
trait Drawable:                                 // trait
  def draw: Unit                                // abstract method

abstract class Shape(w: Int, h: Int):           // abstract class
  def area: Double                              // abstract method

class Rect(w: Int, h: Int) extends Shape(w, h) with Drawable:
  override def area: Double = w * h             // from Shape
  override def draw: Unit = println("draw")     // from Drawable

Case Class

Case classes in Scala are a special type of class that is optimized for use in pattern matching and immutability by default. They come with several boilerplate features out of the box, such as sensible toString, equals, and hashCode implementations, as well as the ability to be deconstructed in pattern matching. Case classes are immensely useful for defining simple data-holding objects, making them a staple in functional programming and domain modeling in Scala.

Basic Case Class

Defining a case class is straightforward:

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

This declaration automatically provides:

  • Immutable fields: name and age are public val fields by default.
  • Sensible toString, equals, and hashCode methods based on the class's fields.
  • An apply method, allowing you to instantiate the class without the new keyword.
  • An unapply method, making it eligible for use in pattern matching.

Instantiating a Case Class

You can instantiate a case class without the new keyword, thanks to the automatically provided apply method:

val alice = Person("Alice", 30)

Copying

Case classes come with a copy method, which is useful for creating a new instance of the class with some changed attributes while keeping the rest unchanged:

val bob = alice.copy(name = "Bob")

This creates a new Person instance with the name "Bob" and the same age as alice.

Pattern Matching

Case classes shine when used in pattern matching, thanks to their unapply method:

alice match
  case Person(name, age) => println(s"Name: $name, Age: $age")
  case _ => println("Unknown person")

This pattern matching checks if alice is a Person instance and then extracts and prints the name and age.

Companion Objects

A companion object for the case class is automatically generated, containing the apply and unapply methods among others. This means you can add additional static utility methods or values in the companion object if needed:

object Person:
  def isAdult(person: Person): Boolean = person.age >= 18

val adultCheck = Person.isAdult(alice)  // true

Use Cases

Case classes are ideal for:

  • Domain models: Representing data entities in your domain.
  • Data transfer objects (DTOs): Encapsulating data sent between processes or systems.
  • Immutable data structures: Building complex data structures that benefit from immutability.

Pattern Matching

Pattern matching with case classes is a powerful feature in Scala that allows for concise and expressive handling of different data structures. Case classes are regular classes which are immutable by default and decomposable through pattern matching. They are especially useful in functional programming paradigms for matching complex data types. Here's a basic overview of how you can use pattern matching with case classes:

  1. Define Case Classes: These are special classes in Scala defined using the case class keyword. They automatically provide implementations of toString, equals, and hashCode methods, making them useful for pattern matching.

  2. Pattern Matching Syntax: You use the match keyword followed by cases. Each case can destructure an instance of a case class, extracting its parts for further processing.

Example

Let's define a simple example to demonstrate pattern matching with case classes. Suppose we are dealing with a system that models basic geometric shapes:

sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Square(side: Double) extends Shape

Here, Shape is a sealed trait, which means all implementations of Shape must be in the same file. This is useful for pattern matching because the compiler can warn about missing cases.

Next, let's define a function that uses pattern matching to compute the area of a shape:

def area(shape: Shape): Double = shape match 
  case Circle(radius) => Math.PI * radius * radius
  case Rectangle(width, height) => width * height
  case Square(side) => side * side

In this function, shape is matched against each case class (Circle, Rectangle, Square), and the appropriate formula is used to calculate the area based on the shape type.

Benefits of Using Case Classes with Pattern Matching

  • Readability: The intent of the code is clear and concise.
  • Safety: The compiler checks for exhaustiveness in match cases, ensuring that all possible cases are handled.
  • Decomposition: Pattern matching allows for easy decomposition of complex data types, making it straightforward to access and manipulate their components.

Class Hierarchy

The Scala class hierarchy has Any at the top, from which all other types derive. It splits into two main branches: AnyVal (value types) and AnyRef (reference types). Additionally, Nothing and Null are special types that serve specific purposes in the hierarchy.

Any
├── AnyVal
│    ├── Unit
│    ├── Boolean
│    ├── Char
│    ├── Byte
│    ├── Short
│    ├── Int
│    ├── Long
│    ├── Float
│    └── Double
├── AnyRef (alias: Object)
│    ├── String
│    ├── List
│    ├── Option
│    │    ├── Some
│    │    └── None
│    ├── Map
│    ├── Set
│    ├── ... (other classes and user-defined classes)
│    └── Null (subtype of every AnyRef type)
└── Nothing (subtype of every other type)

Key Points

Any

  • The root of the type hierarchy.
  • Defines common methods such as ==, !=, equals, hashCode, and toString.

AnyVal

  • The root class of value types, which includes Scala's primitive types.
  • Subclasses: Unit, Boolean, Char, Byte, Short, Int, Long, Float, Double.

AnyRef (alias Object)

  • The root class of all reference types.
  • Subclasses: String, List, Option, Map, Set, and user-defined classes.

Option

  • A container for an optional value.
  • Subclasses: Some (contains a value), None (represents the absence of a value).

Null

  • A subtype of every reference type (AnyRef).
  • Can be assigned to any reference type but not to value types.
  • The null literal is of type Null.

Nothing

  • A subtype of every other type (both AnyVal and AnyRef).
  • Represents the type of "no value" or "no normal return".
  • Useful for functions that never return normally (e.g., throw an exception).

Examples

Value Types (AnyVal)

val unitValue: Unit = ()
val booleanValue: Boolean = true
val charValue: Char = 'A'
val byteValue: Byte = 1
val shortValue: Short = 1
val intValue: Int = 1
val longValue: Long = 1L
val floatValue: Float = 1.0f
val doubleValue: Double = 1.0

Reference Types (AnyRef)

val stringValue: String = "Hello, Scala!"
val listValue: List[Int] = List(1, 2, 3)
val optionValue: Option[Int] = Some(1)
val noneValue: Option[Int] = None
val mapValue: Map[String, Int] = Map("one" -> 1)
val setValue: Set[Int] = Set(1, 2, 3)

class UserDefinedClass
val userDefinedInstance = new UserDefinedClass

val nullValue: String = null

Special Types

Nothing
def fail(message: String): Nothing = throw new RuntimeException(message)

// This function never returns normally, so it has type Nothing
val failed = fail("This is an error")
// This line is never reached
println("This will never be printed")
None
val maybeValue: Option[Int] = None

maybeValue match
  case Some(value) => println(s"Found value: $value")
  case None => println("No value found")
// Output: No value found

Mixins

In Scala, mixins are a powerful feature that allows a class to inherit functionality from multiple traits. This is similar to multiple inheritance but avoids the complexities and pitfalls associated with it. Mixins enable you to compose behaviors from different traits and integrate them into a single class.

Defining and Using Mixins

To define mixins, you use traits. Traits are similar to interfaces in Java but can also contain method implementations and fields.

Here's a step-by-step guide on how to define and use mixins in Scala:

  1. Define Traits: Traits can define methods and fields. You can then mix these traits into a class.

trait Logger: def log(message: String): Unit = println(s"Log: $message")

trait Greeter: def greet(name: String): Unit = println(s"Hello, $name!")


2. **Create a Class with Mixins:**
 You can mix traits into a class using the `extends` and `with` keywords.

 ```scala
class Person(val name: String) extends Logger with Greeter:
  def introduce(): Unit =
    log(s"Introducing $name")
    greet(name)
  1. Instantiate and Use the Class:

    val person = new Person("Alice")
    person.introduce()
    

    This will produce the following output:

    Log: Introducing Alice
    Hello, Alice!
    

Mixing Multiple Traits

You can mix multiple traits into a single class, and they can have their own method implementations and fields.

trait Runner:
  def run(): Unit =
    println("Running!")

class Athlete(name: String) extends Person(name) with Runner:
  def compete(): Unit =
    log(s"$name is competing")
    run()
    greet(name)

val athlete = new Athlete("Bob")
athlete.introduce()
athlete.compete()

This will produce the following output:

Log: Introducing Bob
Hello, Bob!
Log: Bob is competing
Running!
Hello, Bob!

Overriding Methods in Traits

You can override methods defined in traits in the class that mixes them in, or in the traits themselves if they extend other traits.

trait Speaker:
  def speak(): Unit =
    println("Speaking generically.")

trait Philosopher extends Speaker:
  override def speak(): Unit =
    println("Speaking philosophically.")

class Person(name: String) extends Philosopher:
  override def speak(): Unit =
    println(s"$name is speaking wisely.")

val person = new Person("Charlie")
person.speak()

This will produce the following output:

Charlie is speaking wisely.

Linearization of Traits

Scala uses a linearization technique to determine the order in which traits are initialized and their methods are called. This ensures a consistent and predictable method resolution order.

Consider the following example to understand linearization:

trait A:
  def message(): String = "A"

trait B extends A:
  override def message(): String = "B " + super.message()

trait C extends A:
  override def message(): String = "C " + super.message()

class D extends B with C:
  override def message(): String = "D " + super.message()

val d = new D()
println(d.message())

In this case, the output will be:

D C B A

In Scala, self-types are a way to express dependencies between traits. They allow you to declare that a trait can only be mixed into classes that also mix in another specified trait. This can be particularly useful for modularizing code and ensuring that certain traits are only used in the correct contexts.

Here’s a detailed guide on using self-types with mixins in Scala:

Defining Self-Types

A self-type is declared using the this: Type => syntax inside a trait. This specifies that the trait can only be mixed into classes or traits that conform to the specified type.

trait Logger:
  def log(message: String): Unit = println(s"Log: $message")

trait AuthService:
  this: Logger => // Self-type declaration
  def authenticate(user: String, password: String): Boolean =
    log(s"Authenticating user: $user")
    user == "admin" && password == "password"

class AuthApp extends AuthService with Logger

val app = new AuthApp
app.authenticate("admin", "password")

In this example:

  • The AuthService trait has a self-type Logger, meaning it requires an implementation of Logger to be mixed in.
  • The AuthApp class mixes in both AuthService and Logger, satisfying the self-type requirement.

Summary

  • Traits: Define reusable methods and fields.
  • Mixins: Use extends and with to mix traits into classes.
  • Method Override: Override trait methods in classes or other traits.
  • Linearization: Ensures a consistent method resolution order when mixing multiple traits.
  • Self-Types: Declare dependencies between traits using this: Type => syntax.

Design Patterns

Design patterns are common solutions to recurring problems in software design. In Scala, many design patterns can be implemented concisely and elegantly due to the language's rich features such as case classes, traits, and functional programming constructs. Here are examples of some common design patterns in Scala:

1. Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

object Singleton:
  def doSomething(): Unit =
    println("Doing something...")

// Usage
Singleton.doSomething()

2. Factory Pattern

The Factory pattern provides an interface for creating objects without specifying the exact class of the object that will be created.

sealed trait Animal:
  def makeSound(): String

case class Dog() extends Animal:
  def makeSound(): String = "Woof"

case class Cat() extends Animal:
  def makeSound(): String = "Meow"

object AnimalFactory:
  def createAnimal(animalType: String): Animal =
    animalType.toLowerCase match
      case "dog" => Dog()
      case "cat" => Cat()
      case _ => throw new IllegalArgumentException("Unknown animal type")

// Usage
val dog = AnimalFactory.createAnimal("dog")
println(dog.makeSound()) // Woof

val cat = AnimalFactory.createAnimal("cat")
println(cat.makeSound()) // Meow

3. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern lets the algorithm vary independently from the clients that use it.

trait PaymentStrategy:
  def pay(amount: Double): String

class CreditCardPayment extends PaymentStrategy:
  def pay(amount: Double): String = s"Paid $$amount using credit card."

class PayPalPayment extends PaymentStrategy:
  def pay(amount: Double): String = s"Paid $$amount using PayPal."

class PaymentContext(strategy: PaymentStrategy):
  def executeStrategy(amount: Double): String = strategy.pay(amount)

// Usage
val creditCardPayment = new PaymentContext(new CreditCardPayment)
println(creditCardPayment.executeStrategy(100)) // Paid $100.0 using credit card.

val payPalPayment = new PaymentContext(new PayPalPayment)
println(payPalPayment.executeStrategy(200)) // Paid $200.0 using PayPal.

4. Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

trait Observer:
  def update(message: String): Unit

class ConcreteObserver(name: String) extends Observer:
  def update(message: String): Unit =
    println(s"$name received message: $message")

trait Subject:
  private var observers: List[Observer] = List()

  def addObserver(observer: Observer): Unit =
    observers = observer :: observers

  def removeObserver(observer: Observer): Unit =
    observers = observers.filterNot(_ == observer)

  def notifyObservers(message: String): Unit =
    observers.foreach(_.update(message))

class ConcreteSubject extends Subject:
  def changeState(message: String): Unit =
    println(s"Subject state changed: $message")
    notifyObservers(message)

// Usage
val subject = new ConcreteSubject

val observer1 = new ConcreteObserver("Observer1")
val observer2 = new ConcreteObserver("Observer2")

subject.addObserver(observer1)
subject.addObserver(observer2)

subject.changeState("New state") // Both observers receive the message

5. Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

trait Coffee:
  def cost: Double
  def description: String

class SimpleCoffee extends Coffee:
  def cost: Double = 2.0
  def description: String = "Simple coffee"

class MilkDecorator(coffee: Coffee) extends Coffee:
  def cost: Double = coffee.cost + 0.5
  def description: String = coffee.description + ", milk"

class SugarDecorator(coffee: Coffee) extends Coffee:
  def cost: Double = coffee.cost + 0.2
  def description: String = coffee.description + ", sugar"

// Usage
val coffee = new SimpleCoffee
println(s"${coffee.description} costs ${coffee.cost}") // Simple coffee costs 2.0

val milkCoffee = new MilkDecorator(coffee)
println(s"${milkCoffee.description} costs ${milkCoffee.cost}") // Simple coffee, milk costs 2.5

val milkSugarCoffee = new SugarDecorator(milkCoffee)
println(s"${milkSugarCoffee.description} costs ${milkSugarCoffee.cost}") // Simple coffee, milk, sugar costs 2.7

6. Command Pattern

The Command pattern encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of requests.

trait Command:
  def execute(): Unit

class Light:
  def on(): Unit = println("The light is on")
  def off(): Unit = println("The light is off")

class LightOnCommand(light: Light) extends Command:
  def execute(): Unit = light.on()

class LightOffCommand(light: Light) extends Command:
  def execute(): Unit = light.off()

class RemoteControl:
  private var command: Option[Command] = None

  def setCommand(command: Command): Unit =
    this.command = Some(command)

  def pressButton(): Unit =
    command.foreach(_.execute())

// Usage
val light = new Light
val lightOn = new LightOnCommand(light)
val lightOff = new LightOffCommand(light)

val remote = new RemoteControl
remote.setCommand(lightOn)
remote.pressButton() // The light is on

remote.setCommand(lightOff)
remote.pressButton() // The light is off

Enum

Enums in Scala 3 provide a way to define a group of named values, known as enumeration constants. Scala 3's enum is a significant enhancement over the older Enumeration class in Scala 2, offering more capabilities and integrating more seamlessly with the type system. Enums are useful for representing a fixed set of constants, such as days of the week, states of a process, or categories of items, with the benefits of type safety and easy pattern matching.

Basic Enum Declaration

To declare an enum in Scala 3, use the enum keyword followed by the name of the enum and a block containing its cases:

enum Color:
  case Red, Green, Blue

Here, Color is an enum with three possible values: Red, Green, and Blue.

Using Enums

You can use enums in your Scala code like this:

val myColor: Color = Color.Red

myColor match 
  case Color.Red => println("Chosen color is Red")
  case Color.Green => println("Chosen color is Green")
  case Color.Blue => println("Chosen color is Blue")

This code demonstrates defining an enum variable and using pattern matching to perform different actions based on the enum's value.

Parameterized Enums

Enums in Scala 3 can also have parameters, allowing each case to carry additional information:

enum Planet(mass: Double, radius: Double) 
  case Earth extends Planet(5.972e24, 6.371e6)
  case Jupiter extends Planet(1.898e27, 6.9911e7)
  case Mars extends Planet(6.39e23, 3.3895e6)
  
  // A method that calculates the surface gravity of a planet
  def surfaceGravity: Double = Planet.G * mass / (radius * radius)
}

object Planet:
  private final val G = 6.67430e-11 // gravitational constant

In this example, each Planet case has its mass and radius. This makes enums very powerful, as they can encapsulate data and behavior.

Enum Methods and Values

Scala enums can have their methods, as shown with the surfaceGravity method in the Planet example. You can also list all enum values using the .values method:

val planets = Planet.values

planets.foreach(planet =>
  println(s"${planet}: Surface gravity = ${planet.surfaceGravity}")
)

This code prints the surface gravity for each planet defined in the Planet enum.

Enumerations with Sealed Trait Pattern (Scala 2 Approach)

Before Scala 3, a common way to simulate enums was using a sealed trait and case objects:

sealed trait Direction
case object North extends Direction
case object East extends Direction
case object South extends Direction
case object West extends Direction

While this pattern is still useful, especially for compatibility with Scala 2 codebases, Scala 3 enums offer a more integrated and straightforward way to define enumeration types.

List

1. Immutable List-Like Collections

1.1 List

  • Description: List is a linear, immutable collection that maintains order. It is the most commonly used collection in Scala for sequences of elements.
  • Characteristics:
    • Immutable: Once created, it cannot be modified.
    • Linked List: Internally implemented as a singly linked list.
    • Efficient prepends (::), but appends (:+) are O(n).
  • Use Case: General-purpose sequence where immutability is desired.
val list = List(1, 2, 3)

1.2 Vector

  • Description: Vector is an immutable indexed sequence that offers an alternative to List. It is designed to provide more efficient random access and updates.
  • Characteristics:
    • Immutable: Once created, it cannot be modified.
    • Indexed: Provides efficient random access (O(log32 n)).
    • Fast prepend, append, and random access.
  • Use Case: When you need fast random access and efficient append/prepend operations.
val vector = Vector(1, 2, 3)

1.3 Seq

  • Description: Seq is a general trait for ordered collections. List and Vector are common implementations of Seq.
  • Characteristics:
    • Immutable: Seq is immutable by default in the scala.collection.immutable package.
    • Maintains order.
  • Use Case: When you want to work with sequences generically without worrying about the specific implementation.
val seq: Seq[Int] = Seq(1, 2, 3)  // Immutable by default

1.4 Range

  • Description: Range is a special type of sequence representing an ordered sequence of integers. It is often used for looping or generating sequences of numbers.
  • Characteristics:
    • Immutable.
    • Efficient representation of ranges of integers.
  • Use Case: When you need to generate a sequence of numbers or iterate over a range of integers.
val range = 1 to 10  // Range from 1 to 10

1.5 Stream (deprecated, use LazyList instead)

  • Description: Stream was a lazily evaluated list, where elements are computed as needed. It has been replaced by LazyList in Scala 2.13.
  • Characteristics:
    • Immutable.
    • Lazy: Elements are computed only when accessed.
  • Use Case: When working with potentially infinite sequences or when you want to delay computation of elements.
val lazyList = LazyList(1, 2, 3, 4, 5)

1.6 LazyList

  • Description: LazyList is the replacement for Stream, offering lazy evaluation with more consistent performance.
  • Characteristics:
    • Immutable.
    • Lazy: Elements are computed as needed.
  • Use Case: For lazy evaluation of sequences, particularly useful for infinite sequences.
val lazyList = LazyList.from(1)  // Infinite lazy list starting from 1

2. Mutable List-Like Collections

2.1 ListBuffer

  • Description: ListBuffer is a mutable collection that is efficient for building lists incrementally. Once constructed, it can be converted to an immutable List.
  • Characteristics:
    • Mutable: Supports in-place modification.
    • Efficient append/prepend operations.
    • Converts easily to an immutable List.
  • Use Case: When you need to build a list incrementally before converting it to an immutable list.
import scala.collection.mutable.ListBuffer

val listBuffer = ListBuffer(1, 2, 3)
listBuffer += 4
val immutableList = listBuffer.toList

2.2 ArrayBuffer

  • Description: ArrayBuffer is a mutable, indexed sequence that provides fast random access and append/prepend operations.
  • Characteristics:
    • Mutable.
    • Indexed: Allows random access.
    • Efficient append/prepend operations.
  • Use Case: When you need a mutable, indexed sequence with efficient access and update operations.
import scala.collection.mutable.ArrayBuffer

val arrayBuffer = ArrayBuffer(1, 2, 3)
arrayBuffer += 4

2.3 Array

  • Description: Array is a mutable, fixed-size sequence of elements. It provides fast access and update operations.
  • Characteristics:
    • Mutable.
    • Indexed: Allows random access.
    • Fixed size: The length of an array cannot be changed after creation.
  • Use Case: When you need a fixed-size, mutable sequence with efficient access and update operations.
val array = Array(1, 2, 3)
array(0) = 0

2.4 MutableList (deprecated, use ListBuffer instead)

  • Description: MutableList is a mutable linked list. It has been largely replaced by ListBuffer in most use cases.
  • Characteristics:
    • Mutable.
    • Linked list structure.
  • Use Case: Use ListBuffer instead for most cases.

3. Other List-Like Collections

3.1 Queue

  • Description: Queue is a mutable collection that allows elements to be added at the end and removed from the front (FIFO - First In, First Out).
  • Characteristics:
    • Mutable.
    • FIFO: First In, First Out order.
  • Use Case: When you need a mutable, ordered collection with FIFO semantics.
import scala.collection.mutable.Queue

val queue = Queue(1, 2, 3)
queue.enqueue(4)
queue.dequeue()  // Removes 1

3.2 Stack (deprecated, use List or Vector instead)

  • Description: Stack is a mutable collection that allows elements to be added and removed in a LIFO (Last In, First Out) manner. It has been replaced by using List or Vector for stack-like behavior.
  • Characteristics:
    • Mutable.
    • LIFO: Last In, First Out order.
  • Use Case: Use List or Vector with operations like :: and tail instead of Stack.
import scala.collection.mutable.Stack

val stack = Stack(1, 2, 3)
stack.push(4)
stack.pop()  // Removes 4

3.3 Deque

  • Description: Deque (Double-ended Queue) allows elements to be added and removed from both ends. Available as a mutable collection.
  • Characteristics:
    • Mutable.
    • Allows operations on both ends (FIFO and LIFO).
  • Use Case: When you need a mutable sequence that supports adding/removing elements from both ends.
import scala.collection.mutable.ArrayDeque

val deque = ArrayDeque(1, 2, 3)
deque.append(4)
deque.prepend(0)

4. Summary

Scala provides a wide range of list-like collections, each suited for different purposes:

  • Immutable Collections:

    • List: The go-to immutable sequence.
    • Vector: Better for random access.
    • LazyList: For lazy evaluation.
    • Range: For generating sequences of numbers.
  • Mutable Collections:

    • ListBuffer: For building lists incrementally.
    • ArrayBuffer: For fast indexed access.
    • Array: For fixed-size, mutable sequences.
    • Queue: For FIFO operations.
    • Deque: For double-ended operations.

mkString

The mkString method is available for all collections in Scala, including lists, sets, arrays, etc. It can be used with or without delimiters.

Basic mkString without Delimiters

Concatenates all elements of the collection into a single string.

val numbers = List(1, 2, 3, 4, 5)
val result = numbers.mkString
println(result) // "12345"

mkString with a Separator

Concatenates all elements of the collection into a single string with a specified separator between elements.

val numbers = List(1, 2, 3, 4, 5)
val result = numbers.mkString(", ")
println(result) // "1, 2, 3, 4, 5"

mkString with Prefix, Separator, and Suffix

Concatenates all elements of the collection into a single string with a specified prefix, separator, and suffix.

val numbers = List(1, 2, 3, 4, 5)
val result = numbers.mkString("[", ", ", "]")
println(result) // "[1, 2, 3, 4, 5]"

Use Cases

Joining File Paths

val parts = List("home", "user", "documents", "file.txt")
val filePath = parts.mkString("/")
println(filePath) // "home/user/documents/file.txt"

Generating CSV Lines

val headers = List("Name", "Age", "City")
val row1 = List("Alice", "30", "New York")
val row2 = List("Bob", "25", "Los Angeles")

val csvHeaders = headers.mkString(", ")
val csvRow1 = row1.mkString(", ")
val csvRow2 = row2.mkString(", ")

println(csvHeaders) // "Name, Age, City"
println(csvRow1)    // "Alice, 30, New York"
println(csvRow2)    // "Bob, 25, Los Angeles"

Creating SQL Query IN Clause

val ids = List(1, 2, 3, 4, 5)
val sqlInClause = ids.mkString("(", ", ", ")")
println(sqlInClause) // "(1, 2, 3, 4, 5)"

Hierarchy

Traversable
  └── Iterable
        ├── Seq
        │     ├── LinearSeq
        │     │     ├── List
        │     │     ├── LazyList
        │     │     ├── Queue
        │     │     └── Stack
        │     ├── IndexedSeq
        │     │     ├── Vector
        │     │     ├── Range
        │     │     ├── String (Implicit)
        │     │     └── Array (Implicit)
        │     ├── Mutable
        │     │     ├── ArrayBuffer
        │     │     ├── ArraySeq
        │     │     └── StringBuilder
        ├── Set
        │     ├── SortedSet
        │     │     └── TreeSet
        │     ├── HashSet
        │     ├── BitSet
        │     ├── Mutable
        │     │     ├── HashSet
        │     │     ├── LinkedHashSet
        │     │     └── TreeSet
        ├── Map
        │     ├── SortedMap
        │     │     └── TreeMap
        │     ├── HashMap
        │     ├── ListMap
        │     ├── Mutable
        │     │     ├── HashMap
        │     │     ├── LinkedHashMap
        │     │     ├── TreeMap
        │     │     └── WeakHashMap

Key Points

  1. Traversable is the root of the collection hierarchy.
  2. Iterable extends Traversable and adds the iterator method.
  3. Seq, Set, and Map are the main collection types, each with mutable and immutable versions.
  4. LinearSeq and IndexedSeq are subtypes of Seq for specific sequence implementations.
  5. SortedSet and SortedMap are specialized collections for sorted elements.

List

A List in Scala is an immutable, ordered collection of elements. Once created, a List cannot be changed. It's a linked list, meaning that it consists of nodes, where each node contains a value and a reference to the next node.

Creating Lists

You can create a List using the List companion object.

val numbers = List(1, 2, 3, 4, 5)
val fruits = List("apple", "banana", "cherry")

Basic Operations on Lists

Accessing Elements

  • Head: The first element of the list.
  • Tail: A list consisting of all elements except the head.
  • IsEmpty: Checks if the list is empty.
val numbers = List(1, 2, 3, 4, 5)

val head = numbers.head       // 1
val tail = numbers.tail       // List(2, 3, 4, 5)
val isEmpty = numbers.isEmpty // false

List Concatenation

You can concatenate lists using the ::: operator or the ++ method.

val list1 = List(1, 2, 3)
val list2 = List(4, 5, 6)

val concatenated = list1 ::: list2 // List(1, 2, 3, 4, 5, 6)
val concatenated2 = list1 ++ list2 // List(1, 2, 3, 4, 5, 6)

Prepending and Appending Elements

  • Prepend: Add an element to the beginning of the list using the :: operator.
  • Append: Add an element to the end of the list using the :+ method.
val numbers = List(2, 3, 4)

val withPrepended = 1 :: numbers     // List(1, 2, 3, 4)
val withAppended = numbers :+ 5      // List(2, 3, 4, 5)

Common List Methods

Mapping

The map method transforms each element of the list using a given function.

val numbers = List(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map(n => n * n) // List(1, 4, 9, 16, 25)

Filtering

The filter method selects elements of the list that satisfy a predicate.

val numbers = List(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter(_ % 2 == 0) // List(2, 4)

Reducing

The reduce method combines the elements of the list using a binary operation.

val numbers = List(1, 2, 3, 4, 5)
val sum = numbers.reduce(_ + _)      // 15
val product = numbers.reduce(_ * _)  // 120

Sorting

The sorted method sorts a list based on the natural ordering of its elements or a provided comparator.

val numbers = List(5, 3, 1, 4, 2)
val sortedNumbers = numbers.sorted
println(sortedNumbers) // List(1, 2, 3, 4, 5)

val fruits = List("banana", "apple", "cherry")
val sortedFruits = fruits.sorted
println(sortedFruits) // List(apple, banana, cherry)

foreach

The foreach method applies a function to each element of the list.

val numbers = List(1, 2, 3, 4, 5)
numbers.foreach(println)

zip

The zip method combines two lists into a list of pairs.

val numbers = List(1, 2, 3)
val letters = List("a", "b", "c")
val zipped = numbers.zip(letters)
println(zipped) // List((1, "a"), (2, "b"), (3, "c"))

Immutable Nature of Lists

A key feature of List is its immutability. Once a List is created, it cannot be changed. Any operation that transforms a list will return a new list, leaving the original list unchanged.

val originalList = List(1, 2, 3)
val newList = originalList.map(_ * 2)

println(originalList) // List(1, 2, 3)
println(newList)     // List(2, 4, 6)

Map

A Map is a collection of key-value pairs, where each key is unique. Maps can be either mutable or immutable, but by default, Scala uses immutable maps.

Creating Maps

You can create a Map using the Map companion object.

val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)
val numbers = Map(1 -> "one", 2 -> "two", 3 -> "three")

Basic Operations on Maps

Accessing Elements

You can access the value associated with a key using the apply method, which is called using parentheses.

val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)

println(fruitPrices("apple"))  // 1.0
println(fruitPrices("banana")) // 0.5

If the key does not exist, apply throws an exception. To avoid this, you can use the get method, which returns an Option.

println(fruitPrices.get("apple"))  // Some(1.0)
println(fruitPrices.get("orange")) // None

Adding and Removing Elements

  • Add: Use the + operator to add a key-value pair.
  • Remove: Use the - operator to remove a key-value pair.
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5)
val newFruitPrices = fruitPrices + ("cherry" -> 2.0)
println(newFruitPrices) // Map(apple -> 1.0, banana -> 0.5, cherry -> 2.0)

val lessFruitPrices = newFruitPrices - "banana"
println(lessFruitPrices) // Map(apple -> 1.0, cherry -> 2.0)

For mutable maps, you can modify the map in place.

import scala.collection.mutable

val fruitPrices = mutable.Map("apple" -> 1.0, "banana" -> 0.5)
fruitPrices += ("cherry" -> 2.0)
println(fruitPrices) // Map(apple -> 1.0, banana -> 0.5, cherry -> 2.0)

fruitPrices -= "banana"
println(fruitPrices) // Map(apple -> 1.0, cherry -> 2.0)

Common Map Methods

Mapping

The map method transforms each key-value pair in the map.

val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)
val discountedPrices = fruitPrices.map { case (fruit, price) => (fruit, price * 0.9) }
println(discountedPrices) // Map(apple -> 0.9, banana -> 0.45, cherry -> 1.8)

Filtering

The filter method selects key-value pairs that satisfy a predicate.

val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)
val expensiveFruits = fruitPrices.filter { case (fruit, price) => price > 1.0 }
println(expensiveFruits) // Map(cherry -> 2.0)

Iterating

You can iterate over key-value pairs in a map using a for loop or the foreach method.

val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)

for ((fruit, price) <- fruitPrices) {
  println(s"The price of $fruit is $$ $price")
}

fruitPrices.foreach { case (fruit, price) => 
  println(s"The price of $fruit is $$ $price")
}

Immutable vs Mutable Maps

Immutable maps cannot be changed after they are created, while mutable maps can be updated in place.

Immutable Map

val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5)
val newFruitPrices = fruitPrices + ("cherry" -> 2.0)
println(fruitPrices)     // Map(apple -> 1.0, banana -> 0.5)
println(newFruitPrices)  // Map(apple -> 1.0, banana -> 0.5, cherry -> 2.0)

Mutable Map

import scala.collection.mutable

val fruitPrices = mutable.Map("apple" -> 1.0, "banana" -> 0.5)
fruitPrices += ("cherry" -> 2.0)
println(fruitPrices) // Map(apple -> 1.0, banana -> 0.5, cherry -> 2.0)

Advanced Map Operations

Merging Maps

You can merge two maps using the ++ operator or the ++= method for mutable maps.

val map1 = Map(1 -> "one", 2 -> "two")
val map2 = Map(2 -> "two revised", 3 -> "three")

val mergedMap = map1 ++ map2
println(mergedMap) // Map(1 -> one, 2 -> two revised, 3 -> three)

For mutable maps:

val map1 = mutable.Map(1 -> "one", 2 -> "two")
val map2 = Map(2 -> "two revised", 3 -> "three")

map1 ++= map2
println(map1) // Map(1 -> one, 2 -> two revised, 3 -> three)

Grouping

The groupBy method groups elements of a collection by a specified key function.

val words = List("apple", "banana", "cherry", "date", "apricot")
val groupedByFirstLetter = words.groupBy(_.head)
println(groupedByFirstLetter) 
// Map(a -> List(apple, apricot), b -> List(banana), c -> List(cherry), d -> List(date))

Transforming Keys and Values

You can transform the keys and values of a map using the mapKeys and mapValues methods (these methods are available in more recent versions of Scala).

val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5)

// Transform keys
val upperCaseKeys = fruitPrices.map { case (fruit, price) => (fruit.toUpperCase, price) }
println(upperCaseKeys) // Map(APPLE -> 1.0, BANANA -> 0.5)

// Transform values
val discountedPrices = fruitPrices.mapValues(price => price * 0.9)
println(discountedPrices) // Map(apple -> 0.9, banana -> 0.45)

Conversions

You can convert maps to other collection types, such as lists or arrays.

val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5)

val fruitList = fruitPrices.toList
println(fruitList) // List((apple,1.0), (banana,0.5))

val fruitArray = fruitPrices.toArray
println(fruitArray.mkString(", ")) // (apple,1.0), (banana,0.5)

Set

A Set is a collection of unique elements. The Set trait has two subtraits, scala.collection.immutable.Set and scala.collection.mutable.Set. By default, Scala uses immutable sets.

Creating Sets

You can create a Set using the Set companion object.

val fruit = Set("apple", "banana", "cherry")
val numbers = Set(1, 2, 3, 4, 5)

Basic Operations on Sets

Adding and Removing Elements

  • Add: Use the + operator to add an element.
  • Remove: Use the - operator to remove an element.
val fruit = Set("apple", "banana", "cherry")

val moreFruit = fruit + "date"
println(moreFruit) // Set(apple, banana, cherry, date)

val lessFruit = fruit - "banana"
println(lessFruit) // Set(apple, cherry)

For mutable sets, you can modify the set in place.

import scala.collection.mutable

val fruit = mutable.Set("apple", "banana", "cherry")

fruit += "date"
println(fruit) // Set(apple, banana, cherry, date)

fruit -= "banana"
println(fruit) // Set(apple, cherry, date)

Checking Membership

Use the contains method or the apply method (which is called using parentheses).

val fruit = Set("apple", "banana", "cherry")

println(fruit.contains("banana")) // true
println(fruit("banana"))          // true
println(fruit("date"))            // false

Common Set Methods

Union, Intersection, and Difference

  • Union: Combines two sets, including all elements from both sets.
  • Intersection: Returns elements that are present in both sets.
  • Difference: Returns elements that are present in the first set but not in the second.
val set1 = Set(1, 2, 3)
val set2 = Set(3, 4, 5)

val unionSet = set1 | set2           // Set(1, 2, 3, 4, 5)
val intersectionSet = set1 & set2    // Set(3)
val differenceSet = set1 &~ set2     // Set(1, 2)

println(unionSet)
println(intersectionSet)
println(differenceSet)

Subsets and Supersets

  • Subset: Checks if a set is a subset of another set.
  • Superset: Checks if a set is a superset of another set.
val set1 = Set(1, 2, 3)
val set2 = Set(1, 2)

println(set2.subsetOf(set1)) // true
println(set1.subsetOf(set2)) // false

Immutable vs Mutable Sets

Immutable sets cannot be changed after they are created, while mutable sets can be updated in place.

Immutable Set

val fruit = Set("apple", "banana", "cherry")
val moreFruit = fruit + "date"
println(fruit)     // Set(apple, banana, cherry)
println(moreFruit) // Set(apple, banana, cherry, date)

Mutable Set

import scala.collection.mutable

val fruit = mutable.Set("apple", "banana", "cherry")
fruit += "date"
println(fruit) // Set(apple, banana, cherry, date)

Conversions

You can convert sets to other collection types, such as lists or arrays.

val numbers = Set(1, 2, 3, 4, 5)

val numberList = numbers.toList
println(numberList) // List(1, 2, 3, 4, 5)

val numberArray = numbers.toArray
println(numberArray.mkString(", ")) // 1, 2, 3, 4, 5

Lazy List

A LazyList is a lazy, immutable collection that represents a sequence of elements. Elements of a LazyList are computed only when needed, which allows for efficient handling of potentially infinite sequences.

Creating LazyLists

You can create a LazyList using the LazyList companion object.

val lazyList = LazyList(1, 2, 3, 4, 5)

You can also create an infinite LazyList using the LazyList.cons method or the #:: operator.

// Infinite LazyList of natural numbers
def from(n: Int): LazyList[Int] = n #:: from(n + 1)
val naturals = from(1)

Accessing Elements

Head and Tail

  • Head: The first element of the LazyList.
  • Tail: The rest of the LazyList (which is lazily evaluated).
val lazyList = LazyList(1, 2, 3, 4, 5)

println(lazyList.head) // 1
println(lazyList.tail) // LazyList(<not computed>)

Taking Elements

You can take the first n elements of a LazyList using the take method.

val naturals = from(1)
val firstFive = naturals.take(5)
println(firstFive.toList) // List(1, 2, 3, 4, 5)

Basic Operations on LazyLists

Mapping

The map method transforms each element of the LazyList using a given function.

val lazyList = LazyList(1, 2, 3, 4, 5)
val squaredLazyList = lazyList.map(n => n * n)
println(squaredLazyList.take(5).toList) // List(1, 4, 9, 16, 25)

Filtering

The filter method selects elements of the LazyList that satisfy a predicate.

val lazyList = LazyList(1, 2, 3, 4, 5)
val evenLazyList = lazyList.filter(_ % 2 == 0)
println(evenLazyList.take(2).toList) // List(2, 4)

Reducing

The reduce method combines the elements of the LazyList using a binary operation.

val lazyList = LazyList(1, 2, 3, 4, 5)
val sum = lazyList.reduce(_ + _)
println(sum) // 15

Folding

The fold method combines elements of the LazyList using a binary operation and a starting value.

val lazyList = LazyList(1, 2, 3, 4, 5)
val sum = lazyList.fold(0)(_ + _)
println(sum) // 15

Infinite LazyLists

One of the key advantages of LazyLists is the ability to handle infinite sequences.

val naturals = from(1)
println(naturals.take(10).toList) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

You can create more complex infinite sequences.

val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map { case (a, b) => a + b }
println(fibs.take(10).toList) // List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)

Advanced Operations

Lazy Evaluation

LazyLists are lazily evaluated, meaning elements are computed only when accessed.

val lazyList = LazyList(1, 2, 3, 4, 5)
println(lazyList) // LazyList(<not computed>)
println(lazyList.force) // LazyList(1, 2, 3, 4, 5)

Pattern Matching

You can use pattern matching to deconstruct LazyLists.

val lazyList = LazyList(1, 2, 3, 4, 5)

lazyList match {
  case LazyList() => println("The LazyList is empty")
  case head #:: tail => println(s"The head is $head and the tail is $tail")
}
// Output: The head is 1 and the tail is LazyList(<not computed>)

Conversions

You can convert LazyLists to other collection types, such as lists or arrays.

val lazyList = LazyList(1, 2, 3, 4, 5)

val list = lazyList.toList
println(list) // List(1, 2, 3, 4, 5)

val array = lazyList.toArray
println(array.mkString(", ")) // 1, 2, 3, 4, 5

List all

Scala's List collection is a fundamental data structure in the Scala standard library, providing a wide range of functions for creating, transforming, querying, and manipulating lists. Here is a comprehensive overview of the most common functions available for List in Scala:

Creating Lists

  • apply: Creates a list from the given elements.

    val list = List(1, 2, 3)
    
  • fill: Creates a list filled with a specified value.

    val list = List.fill(3)("a") // List("a", "a", "a")
    
  • tabulate: Creates a list based on a tabulation function.

    val list = List.tabulate(5)(n => n * n) // List(0, 1, 4, 9, 16)
    
  • range: Creates a list containing a sequence of numbers.

    val list = List.range(1, 5) // List(1, 2, 3, 4)
    

Basic Operations

  • head: Returns the first element.

    val head = list.head
    
  • tail: Returns the list without the first element.

    val tail = list.tail
    
  • isEmpty: Checks if the list is empty.

    val empty = list.isEmpty
    
  • length: Returns the length of the list.

    val length = list.length
    

Adding Elements to a List

  1. Prepending Elements
  • ::: Prepend a single element to the beginning of the list.

    val list = List(1, 2, 3)
    val prependedList = 0 :: list
    println(prependedList) // List(0, 1, 2, 3)
    
  • :::: Prepend another list to the beginning of the list.

    val list1 = List(2, 3, 4)
    val list2 = List(0, 1)
    val combinedList = list2 ::: list1
    println(combinedList) // List(0, 1, 2, 3, 4)
    
  1. Appending Elements
  • :+: Append a single element to the end of the list.

    val list = List(1, 2, 3)
    val appendedList = list :+ 4
    println(appendedList) // List(1, 2, 3, 4)
    
  • ++: Append another list to the end of the list.

    val list1 = List(1, 2, 3)
    val list2 = List(4, 5, 6)
    val combinedList = list1 ++ list2
    println(combinedList) // List(1, 2, 3, 4, 5, 6)
    

ListBuffer Adding and Removing

Because a ListBuffer is mutable the adding and removing can be done on the same instance of the list. All the mutable collection do have these functions
They a located in the Growable trait

Apart from these functions List and ListBuffer do have the same functions This is also valid for: Array, Vector, Seq, Range

import scala.collection.mutable.ListBuffer

val buffer = ListBuffer(1, 2, 3)
  • +=: Appends an element to the buffer.

    buffer += 4 // ListBuffer(1, 2, 3, 4)
    
  • ++=: Appends multiple elements to the buffer.

    buffer ++= Seq(5, 6) // ListBuffer(1, 2, 3, 4, 5, 6)
    
  • -=: Removes an element from the buffer.

    buffer -= 3 // ListBuffer(1, 2, 4, 5, 6)
    
  • --=: Removes multiple elements from the buffer.

    buffer --= Seq(1, 4) // ListBuffer(2, 5, 6)
    
  • clear: Removes all elements from the buffer.

    buffer.clear() // ListBuffer()
    

Element Access

  • apply: Accesses the element at a specific index.

    val elem = list(1)
    
  • indexOf: Finds the index of the first occurrence of an element.

    val index = list.indexOf(2)
    
  • lastIndexOf: Finds the index of the last occurrence of an element.

    val lastIndex = list.lastIndexOf(2)
    

Transformations

  • map: Transforms each element by applying a function.

    val mapped = list.map(_ * 2)
    
  • flatMap: Transforms each element using a function and flattens the result.

    val flatMapped = list.flatMap(x => List(x, x * 2))
    
  • collect: Transforms elements using a partial function.

    val collected = list.collect { case x if x % 2 == 0 => x * 2 }
    

Filtering

  • filter: Selects elements that satisfy a predicate.

    val filtered = list.filter(_ % 2 == 0)
    
  • filterNot: Selects elements that do not satisfy a predicate.

    val filterNot = list.filterNot(_ % 2 == 0)
    
  • partition: Splits the list into two lists based on a predicate.

    val (evens, odds) = list.partition(_ % 2 == 0)
    

Aggregation

  • reduce: Combines elements using a binary function.

    val sum = list.reduce(_ + _)
    
  • fold: Combines elements using a binary function and an initial value.

    val sum = list.fold(0)(_ + _)
    
  • scan: Produces a list of intermediate results.

    val scanSum = list.scan(0)(_ + _)
    

Slicing and Dicing

  • slice: Extracts a sublist from a specified range.

    val sliced = list.slice(1, 3)
    
  • take: Takes the first n elements.

    val taken = list.take(2)
    
  • drop: Drops the first n elements.

    val dropped = list.drop(2)
    
  • splitAt: Splits the list at a specified position.

    val (left, right) = list.splitAt(2)
    
  • takeWhile: Takes elements while a predicate is true.

    val takenWhile = list.takeWhile(_ < 3)
    
  • dropWhile: Drops elements while a predicate is true.

    val droppedWhile = list.dropWhile(_ < 3)
    

Searching

  • exists: Checks if any element satisfies a predicate.

    val hasEven = list.exists(_ % 2 == 0)
    
  • forall: Checks if all elements satisfy a predicate.

    val allEven = list.forall(_ % 2 == 0)
    
  • find: Finds the first element that satisfies a predicate.

    val firstEven = list.find(_ % 2 == 0)
    

Zipping and Unzipping

  • zip: Combines two lists into a list of pairs.

    val zipped = list.zip(List("a", "b", "c"))
    
  • zipWithIndex: Pairs elements with their indices.

    val indexed = list.zipWithIndex
    
  • unzip: Splits a list of pairs into two lists.

    val (numbers, letters) = List((1, 'a'), (2, 'b')).unzip
    

Sorting

  • sorted: Sorts the list.

    val sorted = list.sorted
    
  • sortBy: Sorts the list by a specified function.

    val sortedBy = list.sortBy(-_)
    
  • sortWith: Sorts the list using a comparator function.

    val sortedWith = list.sortWith(_ > _)
    

Grouping

  • groupBy: Groups elements by a specified function.
    val grouped = list.groupBy(_ % 2)
    

Conversion

  • toArray: Converts the list to an array.

    val array = list.toArray
    
  • toSet: Converts the list to a set.

    val set = list.toSet
    
  • mkString: Converts the list to a string with a specified separator.

    val str = list.mkString(", ")
    

Advanced Functions

  • permutations: Returns an iterator of all permutations of the list.

    val perms = list.permutations.toList
    
  • combinations: Returns an iterator of all combinations of the list.

    val combs = list.combinations(2).toList
    
  • sliding: Returns an iterator over sliding windows.

    val windows = list.sliding(2).toList
    

Example Usage

Here are some example usages of the above functions:

val numbers = List(1, 2, 3, 4, 5)

// Basic Operations
val head = numbers.head // 1
val tail = numbers.tail // List(2, 3, 4, 5)
val isEmpty = numbers.isEmpty // false
val length = numbers.size // 5

// Element Access
val elem = numbers(1) // 2
val index = numbers.indexOf(2) // 1

// Transformations
val mapped = numbers.map(_ * 2) // List(2, 4, 6, 8, 10)
val flatMapped = numbers.flatMap(x => List(x, x * 2)) // List(1, 2, 2, 4, 3, 6, 4, 8, 5, 10)

// Filtering
val filtered = numbers.filter(_ % 2 == 0) // List(2, 4)

// Aggregation
val sum = numbers.reduce(_ + _) // 15

// Slicing and Dicing
val sliced = numbers.slice(1, 3) // List(2, 3)
val taken = numbers.take(2) // List(1, 2)
val dropped = numbers.drop(2) // List(3, 4, 5)

// Searching
val hasEven = numbers.exists(_ % 2 == 0) // true

// Zipping and Unzipping
val zipped = numbers.zip(List("a", "b", "c")) // List((1, "a"), (2, "b"), (3, "c"))

// Sorting
val sorted = numbers.sorted // List(1, 2, 3, 4, 5)

// Grouping
val grouped = numbers.groupBy(_ % 2) // Map(1 -> List(1, 3, 5), 0 -> List(2, 4))

// Conversion
val array = numbers.toArray // Array(1, 2, 3, 4, 5)
val set = numbers.toSet // Set(1, 2, 3, 4, 5)
val str = numbers.mkString(", ") // "1, 2, 3, 4, 5"

Map

In Scala, Map is a collection of key-value pairs where each key is unique. Scala provides a wide variety of methods to create, transform, query, and manipulate maps. Here's a comprehensive overview of the most common functions available for Map in Scala:

Creating Maps

  • apply: Creates a map from given key-value pairs.

    val map = Map(1 -> "one", 2 -> "two", 3 -> "three")
    
  • empty: Creates an empty map.

    val emptyMap = Map.empty[Int, String]
    

Basic Operations

  • get: Retrieves the value associated with a key, returning Option.

    val value = map.get(2) // Some("two")
    val missing = map.get(4) // None
    
  • apply: Retrieves the value associated with a key, throwing an exception if the key is not found.

    val value = map(2) // "two"
    
  • contains: Checks if the map contains a key.

    val exists = map.contains(2) // true
    val notExists = map.contains(4) // false
    
  • isEmpty: Checks if the map is empty.

    val empty = map.isEmpty // false
    
  • size: Returns the size of the map.

    val size = map.size // 3
    

Adding and Removing Elements

  • +: Adds a key-value pair to the map.

    val newMap = map + (4 -> "four")
    
  • -: Removes a key-value pair by key.

    val smallerMap = map - 2
    
  • ++: Adds multiple key-value pairs to the map.

    val morePairs = map ++ Map(4 -> "four", 5 -> "five")
    
  • --: Removes multiple keys from the map.

    val fewerPairs = map -- List(1, 2)
    

Transformations

  • map: Transforms each key-value pair using a function.

    val mapped = map.map { case (k, v) => (k, v.toUpperCase) }
    
  • flatMap: Transforms each key-value pair using a function that returns an iterable, and flattens the result.

    val flatMapped = map.flatMap { case (k, v) => List(k -> v, k -> v.toUpperCase) }
    
  • collect: Transforms key-value pairs using a partial function.

    val collected = map.collect { case (k, v) if k % 2 == 0 => (k, v.toUpperCase) }
    

Filtering

  • filter: Selects key-value pairs that satisfy a predicate.

    val filtered = map.filter { case (k, v) => k % 2 == 0 }
    
  • filterKeys: Selects key-value pairs whose keys satisfy a predicate.

    val filteredKeys = map.filterKeys(_ % 2 == 0)
    
  • filterNot: Selects key-value pairs that do not satisfy a predicate.

    val filterNot = map.filterNot { case (k, v) => k % 2 == 0 }
    

Aggregation

  • foldLeft: Aggregates values from left to right.

    val foldedLeft = map.foldLeft("") { case (acc, (k, v)) => acc + v }
    
  • foldRight: Aggregates values from right to left.

    val foldedRight = map.foldRight("") { case ((k, v), acc) => acc + v }
    

Merging Maps

  • ++: Merges two maps, with the second map overwriting keys in the first map.
    val merged = map ++ Map(2 -> "TWO", 4 -> "four")
    

Submaps and Key Sets

  • keys: Returns an iterable of all keys.

    val keys = map.keys // Iterable(1, 2, 3)
    
  • values: Returns an iterable of all values.

    val values = map.values // Iterable("one", "two", "three")
    
  • keySet: Returns a set of all keys.

    val keySet = map.keySet // Set(1, 2, 3)
    

Transformations by Keys and Values

  • mapValues: Transforms the values using a function.

    val mappedValues = map.mapValues(_.toUpperCase)
    
  • transform: Transforms both keys and values using a function.

    val transformed = map.transform { case (k, v) => v.toUpperCase }
    

Grouping

  • groupBy: Groups the map by a function applied to the keys.
    val groupedByMod = map.groupBy { case (k, v) => k % 2 }
    

Conversion

  • toList: Converts the map to a list of key-value pairs.

    val list = map.toList // List((1, "one"), (2, "two"), (3, "three"))
    
  • toArray: Converts the map to an array of key-value pairs.

    val array = map.toArray // Array((1, "one"), (2, "two"), (3, "three"))
    
  • toSeq: Converts the map to a sequence of key-value pairs.

    val seq = map.toSeq // Seq((1, "one"), (2, "two"), (3, "three"))
    

Example Usage

Here are some example usages of the above functions:

val map = Map(1 -> "one", 2 -> "two", 3 -> "three")

// Basic Operations
val value = map.get(2) // Some("two")
val missing = map.get(4) // None
val exists = map.contains(2) // true
val size = map.size // 3

// Adding and Removing Elements
val newMap = map + (4 -> "four") // Map(1 -> "one", 2 -> "two", 3 -> "three", 4 -> "four")
val smallerMap = map - 2 // Map(1 -> "one", 3 -> "three")
val morePairs = map ++ Map(4 -> "four", 5 -> "five") // Map(1 -> "one", 2 -> "two", 3 -> "three", 4 -> "four", 5 -> "five")
val fewerPairs = map -- List(1, 2) // Map(3 -> "three")

// Transformations
val mapped = map.map { case (k, v) => (k, v.toUpperCase) } // Map(1 -> "ONE", 2 -> "TWO", 3 -> "THREE")
val flatMapped = map.flatMap { case (k, v) => List(k -> v, k -> v.toUpperCase) } // Map(1 -> "one", 1 -> "ONE", 2 -> "two", 2 -> "TWO", 3 -> "three", 3 -> "THREE")
val collected = map.collect { case (k, v) if k % 2 == 0 => (k, v.toUpperCase) } // Map(2 -> "TWO")

// Filtering
val filtered = map.filter { case (k, v) => k % 2 == 0 } // Map(2 -> "two")
val filteredKeys = map.filterKeys(_ % 2 == 0) // Map(2 -> "two")
val filterNot = map.filterNot { case (k, v) => k % 2 == 0 } // Map(1 -> "one", 3 -> "three")

// Aggregation
val foldedLeft = map.foldLeft("") { case (acc, (k, v)) => acc + v } // "onetwothree"
val foldedRight = map.foldRight("") { case ((k, v), acc) => acc + v } // "threetwoone"

// Merging Maps
val merged = map ++ Map(2 -> "TWO", 4 -> "four") // Map(1 -> "one", 2 -> "TWO", 3 -> "three", 4 -> "four")

// Submaps and Key Sets
val keys = map.keys // Iterable(1, 2, 3)
val values = map.values // Iterable("one", "two", "three")
val keySet = map.keySet // Set(1, 2, 3)

// Transformations by Keys and Values
val mappedValues = map.mapValues(_.toUpperCase) // Map(1 -> "ONE", 2 -> "TWO", 3 -> "THREE")
val transformed = map.transform { case (k, v) => v.toUpperCase } // Map(1 -> "ONE", 2 -> "TWO", 3 -> “THREE”)

// Grouping
val groupedByMod = map.groupBy { case (k, v) => k % 2 } // Map(0 -> Map(2 -> “two”), 1 -> Map(1 -> “one”, 3 -> “three”))

// Conversion
val list = map.toList // List((1, “one”), (2, “two”), (3, “three”))
val array = map.toArray // Array((1, “one”), (2, “two”), (3, “three”))
val seq = map.toSeq // Seq((1, “one”), (2, “two”), (3, “three”))

Set

In Scala, Set is a collection that contains no duplicate elements and has no defined order. It provides various methods for creating, transforming, querying, and manipulating sets. Here’s a comprehensive overview of the most common functions available for Set in Scala:

Creating Sets

  • apply: Creates a set from the given elements.

    val set = Set(1, 2, 3)
    
  • empty: Creates an empty set.

    val emptySet = Set.empty[Int]
    

Basic Operations

  • contains: Checks if the set contains a specific element.

    val containsTwo = set.contains(2) // true
    
  • isEmpty: Checks if the set is empty.

    val empty = set.isEmpty // false
    
  • size: Returns the size of the set.

    val size = set.size // 3
    
  • add: Adds an element to the set (returns a new set, since sets are immutable).

    val newSet = set + 4 // Set(1, 2, 3, 4)
    
  • remove: Removes an element from the set (returns a new set).

    val smallerSet = set - 2 // Set(1, 3)
    

Transformations

  • map: Transforms each element by applying a function.

    val mapped = set.map(_ * 2) // Set(2, 4, 6)
    
  • flatMap: Transforms each element using a function and flattens the result.

    val flatMapped = set.flatMap(x => Set(x, x * 2)) // Set(1, 2, 3, 4, 6)
    
  • collect: Transforms elements using a partial function.

    val collected = set.collect { case x if x % 2 == 0 => x * 2 } // Set(4)
    

Filtering

  • filter: Selects elements that satisfy a predicate.

    val filtered = set.filter(_ % 2 == 0) // Set(2)
    
  • filterNot: Selects elements that do not satisfy a predicate.

    val filterNot = set.filterNot(_ % 2 == 0) // Set(1, 3)
    
  • partition: Splits the set into two sets based on a predicate.

    val (evens, odds) = set.partition(_ % 2 == 0)
    println(evens) // Set(2)
    println(odds)  // Set(1, 3)
    

Aggregation

  • reduce: Combines elements using a binary function.

    val sum = set.reduce(_ + _) // 6
    
  • fold: Combines elements using a binary function and an initial value.

    val sum = set.fold(0)(_ + _) // 6
    

Combining Sets

  • union: Returns the union of two sets.

    val otherSet = Set(3, 4, 5)
    val unionSet = set.union(otherSet) // Set(1, 2, 3, 4, 5)
    // or
    val unionSet2 = set | otherSet // Set(1, 2, 3, 4, 5)
    
  • intersect: Returns the intersection of two sets.

    val intersectSet = set.intersect(otherSet) // Set(3)
    // or
    val intersectSet2 = set & otherSet // Set(3)
    
  • diff: Returns the difference of two sets.

    val diffSet = set.diff(otherSet) // Set(1, 2)
    // or
    val diffSet2 = set &~ otherSet // Set(1, 2)
    

Subset and Superset

  • subsetOf: Checks if one set is a subset of another.

    val isSubset = Set(1, 2).subsetOf(set) // true
    
  • supersetOf: Checks if one set is a superset of another.

    val isSuperset = set.supersetOf(Set(1, 2)) // true
    

Conversion

  • toList: Converts the set to a list.

    val list = set.toList // List(1, 2, 3)
    
  • toArray: Converts the set to an array.

    val array = set.toArray // Array(1, 2, 3)
    
  • toSeq: Converts the set to a sequence.

    val seq = set.toSeq // Seq(1, 2, 3)
    
  • toMap: Converts a set of key-value pairs to a map.

    val pairSet = Set((1, "one"), (2, "two"))
    val map = pairSet.toMap // Map(1 -> "one", 2 -> "two")
    

Example Usage

Here are some example usages of the above functions:

val set = Set(1, 2, 3)

// Basic Operations
val containsTwo = set.contains(2) // true
val empty = set.isEmpty // false
val size = set.size // 3

// Transformations
val mapped = set.map(_ * 2) // Set(2, 4, 6)
val flatMapped = set.flatMap(x => Set(x, x * 2)) // Set(1, 2, 3, 4, 6)

// Filtering
val filtered = set.filter(_ % 2 == 0) // Set(2)

// Aggregation
val sum = set.reduce(_ + _) // 6

// Combining Sets
val otherSet = Set(3, 4, 5)
val unionSet = set.union(otherSet) // Set(1, 2, 3, 4, 5)
val intersectSet = set.intersect(otherSet) // Set(3)
val diffSet = set.diff(otherSet) // Set(1, 2)

// Subset and Superset
val isSubset = Set(1, 2).subsetOf(set) // true
val isSuperset = set.supersetOf(Set(1, 2)) // true

// Conversion
val list = set.toList // List(1, 2, 3)
val array = set.toArray // Array(1, 2, 3)
val seq = set.toSeq // Seq(1, 2, 3)
val pairSet = Set((1, "one"), (2, "two"))
val map = pairSet.toMap // Map(1 -> "one", 2 -> "two")

// Advanced Functions
val combs = set.subsets(2).toList // List(Set(1, 2), Set(1, 3), Set(2, 3))
val allSubsets = set.subsets().toList // List(Set(), Set(1), Set(2), Set(3), Set(1, 2), Set(1, 3), Set(2, 3), Set(1, 2, 3))

Generics

scala> List(1,2,3,4)
val res0: List[Int] = List(1, 2, 3, 4)
scala> val optX = Option(3)
val optX: Option[Int] = Some(3)

case class Pair[A](first: A, second: A)
// defined case class Pair
                                                                                
scala> val intPair = Pair[Int](3, 4)
val intPair: Pair[Int] = Pair(3,4)
                                                                                
scala> val doublePair = Pair[Double](1.1, 3.14)
val doublePair: Pair[Double] = Pair(1.1,3.14)

Defining Generic Classes and Traits

In Scala, you can define generic classes and traits using square brackets to specify type parameters.

class Box[T] {
  private var content: Option[T] = None
  
  def put(item: T): Unit = {
    content = Some(item)
  }

  def get(): Option[T] = content
}

trait Comparable[T] {
  def compareTo(other: T): Int
}

Using Generic Classes

val intBox = new Box[Int]
intBox.put(123)

val stringBox = new Box[String]
stringBox.put("Hello Generics")

println(intBox.get())  // Outputs: Some(123)
println(stringBox.get())  // Outputs: Some("Hello Generics")

Defining and Using Generic Methods

Scala allows defining methods with their own type parameters, which can be useful for utility methods.

object Utils {
  def getFirstElement[T](list: List[T]): Option[T] = list.headOption
}

val numbers = List(1, 2, 3)
val names = List("Alice", "Bob", "Charlie")

println(Utils.getFirstElement(numbers))  // Outputs: Some(1)
println(Utils.getFirstElement(names))  // Outputs: Some("Alice")

Function values

In Scala a function is a value like other values. It can be

  • assigned to another variable,
  • passed as a function parameter
  • returned as function value

Function as variable

The function value is the function name.
Calling the function is adding the parameter list between round brackets
This way we can convert a method into a function

def add(a: Int, b: Int): Int = a + b
 
val adder = add
val result = adder(3, 4)

In the example usage, we define a sequence of input numbers and pass it to transformNumbers, along with a function that squares its input. The result is a new sequence with the squares of the input values.

Function as parameter

In Scala, you can pass functions as parameters to other functions. This allows you to define higher-order functions, which are functions that take other functions as arguments or return functions as values.

Here's an example of defining a function that takes another function as a parameter:

def transformNumbers(numbers: List[Int], f: Int => Int): List[Int] =
  numbers.map(f)

// Example usage
val input = List(1, 2, 3, 4, 5)
val output = transformNumbers(input, x => x * x) // List(1, 4, 9, 16, 25)

In this example, we define a function named transformNumbers that takes two parameters: a sequence of Int values named numbers, and a function named f that takes an Int and returns an Int. The function body uses the map method to apply f to each element of numbers, returning a new sequence with the transformed values.

Functions as return values

In Scala, you can define functions that return other functions as their result. This allows you to create new functions with specialized behavior by partially applying parameters or selecting behavior based on runtime conditions.

Here's an example of defining a function that returns another function:


def createGreetingFunction(prefix: String): String => String = 
  (name: String) => s"$prefix, $name!"

// Example usage
val greetHello = createGreetingFunction("Hello")
val greetHi = createGreetingFunction("Hi")

val message1 = greetHello("Mary") // "Hello, Mary!"
val message2 = greetHi("John") // "Hi, John!"

In this example, we define a function named createGreetingFunction that takes a single parameter prefix of type String, and returns another function that takes a String parameter and returns a String. The returned function concatenates the prefix and the input name parameter with a comma and space in between.

In the example usage, we call createGreetingFunction twice to create two new functions, greetHello and greetHi, which prepend the strings "Hello" and "Hi" to their input arguments, respectively. We then call these functions with different input strings to produce two different output messages.

Recursion

Recursion is a technique used in functional programming where a function calls itself to solve a problem. Recursion is a powerful tool that can help simplify complex problems by breaking them down into smaller, simpler problems. Scala supports recursion, and here are some examples of recursive functions in Scala:

Factorial

The factorial of a non-negative integer n is the product of all positive integers less than or equal to n. The factorial function can be defined recursively as follows:

def factorial(n: Int): Int = 
  if n == 0 then 1 
  else n * factorial(n - 1)

// example usage
factorial(5) // returns 120

Fibonacci

The Fibonacci sequence is a series of numbers in which each number is the sum of the two preceding numbers. The sequence starts with 0 and 1, and the next number is always the sum of the previous two numbers. The Fibonacci sequence can be defined recursively as follows:

def fibonacci(n: Int): Int = 
  if n <= 1 then n 
  else fibonacci(n - 1) + fibonacci(n - 2)

// example usage
fibonacci(6) // returns 8

Binary search is a search algorithm that works by repeatedly dividing the search interval in half. The algorithm starts with an interval covering the whole array. If the value of the search key is less than the item in the middle of the interval, narrow the interval to the lower half. Otherwise, narrow it to the upper half. Binary search can be defined recursively as follows:

def binarySearch(arr: Array[Int], target: Int, low: Int, high: Int): Int = 
  if low > high then -1
  else 
    val mid = low + (high - low) / 2
    if arr(mid) == target then mid
    else if arr(mid) < target then
      binarySearch(arr, target, mid + 1, high)
    else 
      binarySearch(arr, target, low, mid - 1)

// example usage
val arr = Array(1, 3, 5, 7, 9)
binarySearch(arr, 5, 0, arr.length - 1) // returns 2

Recursion Drawbacks

Recursion is a powerful technique that can simplify complex problems in functional programming. However, there are some potential disadvantages to using recursion that should be considered when writing recursive functions:

  • Stack overflow: One of the main disadvantages of recursion is the possibility of stack overflow, especially if the recursive function is called with a large input. Each recursive call adds a new frame to the call stack, and if the stack becomes too deep, it can result in a stack overflow error. While this can be mitigated in some languages with tail recursion optimization, Scala does not support tail recursion optimization for all cases, so it's important to be careful when writing recursive functions.

  • Performance: Recursive functions can sometimes be slower than their iterative counterparts due to the overhead of creating new stack frames with each recursive call. This can be especially true in languages that don't support tail recursion optimization, like Scala. In some cases, an iterative approach may be more efficient.

  • Readability: Recursive functions can sometimes be more difficult to understand than their iterative counterparts, especially for programmers who are not familiar with recursion. Recursive functions can require some mental gymnastics to understand, as they depend on the function calling itself, which can be a tricky concept to grasp.

  • Debugging: Debugging recursive functions can be more challenging than debugging iterative functions, especially if the recursion is deep. It can be difficult to keep track of the sequence of function calls and the state of variables at each step.

Overall, recursion can be a powerful tool in functional programming, but it's important to be aware of its potential drawbacks and use it judiciously.

tail-recursion

Abstract Data Type

Option

With Option you can eliminate the use of null in Scala. An Option has two values Some or None.

  • None wraps the null pointer
  • Some wraps the value
scala> val optStr = Option("hello")
val optStr: Option[String] = Some(hello)
                                                                                
scala> optStr match 
     | case Some(value) => s"the result is: $value"
     | case None => "there is no result"
     | 
val res5: String = the result is: hello
                                                                                
scala> val optStr = Option(null)
val optStr: Option[Null] = None
                                                                                
scala> optStr match 
     | case Some(value) => s"the result is: $value"
     | case None => "there is no result"
     | 
val res6: String = there is no result
                                            
scala> val map = Map(1 -> "a", 2 -> "b", 3 -> "c")
val map: Map[Int, String] = Map(1 -> a, 2 -> b, 3 -> c)

scala> map(3)
val res0: String = c

scala> map(4)
java.util.NoSuchElementException: key not found: 4
at scala.collection.immutable.Map$Map3.apply(Map.scala:399)
... 40 elided

scala> map.get(3)
val res1: Option[String] = Some(c)

scala> map.get(4)
val res2: Option[String] = None

Try

`Try' wraps an exception.

An Option has two values Some or None.

  • Failure wraps the exception
  • Success wraps the value
 val x = 3 / 0
java.lang.ArithmeticException: / by zero
  ... 40 elided
scala> import scala.util.{Try, Success, Failure}

scala> val x = Try(3 / 0)
val x: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

scala> val x = Try(3 / 1)
val x: scala.util.Try[Int] = Success(3)
scala> x match
    | case Success(value) => s"calculation successful with the value:  $value"
    | case Failure(ex) => s"calculation failure with exception:  $ex"
    |
val res1: String = calculation successful with the value:  3

Either

An Either also wrap two value Right and Left. It is something between the Option and the Try and more general, but less used.

In this example an exception is wrapped inside an Either.
Try would be more natural.

def returnEither(value: String): Either[NumberFormatException, Int] =
    try 
        Right(value.toInt)
    catch 
        case ex: NumberFormatException => Left(ex)
    
def resultEither(value: String) =
  returnEither(value) match
    case Right(value) => s"Right value: $value"
    case Left(ex) => s"Left exception: $ex"
scala> resultEither("12")
val res2: String = Right value: 12

scala> resultEither("ab")
val res3: String = Left exception: java.lang.NumberFormatException: For input string: "ab"

Exercises

Exercise 1

  1. Write a function that return an Option[String]
  2. Call the function and pattern match on the possible results

Exercise 2

  1. Write a map with the days of the week 1 "monday", 2 :"tuesday", etc
  2. Write a function day that return the name of the day or an error message
def dayOfTheWeek(day: Int, map: [Int, String]): Option[String] = ???

Exercise 3

Write a vector with the number 1 to 10

val vector = Vector(1,2,3,4,5,6,7,8,9,10)

Write a function indexOf that

  • return the number at the index
  • or an error message when the index is out of bounds.
def indexOf(index: Int, vector: Vector[Int]): Try[Int] = ???

Call the indexOf function and print the result in a pattern match

indexOf(2, vector) match ???

Exercise 4

Do the same as in Exercise 3 but now use 'Either'

def indexOf(index: Int, vector: Vector[Int]): Either[Int] = ???

For Comprehension

Understanding flatMap

flatMap is used when you have a collection of elements, and you want to apply an operation to each element where that operation itself produces a collection. flatMap then merges all these collections into one. The signature of flatMap in the context of a collection looks something like this:

def flatMap[B](f: A => IterableOnce[B]): Iterable[B]
  • A: The type of elements in the original collection.
  • B: The type of elements in the returned collections and the final flattened collection.

Example Usage of flatMap:

val sentences = List("Hello World", "Scala is fun")
val words = sentences.flatMap(sentence => sentence.split(" "))
// words: List[String] = List("Hello", "World", "Scala", "is", "fun")

In this example, flatMap splits each string into words, producing a list of words for each sentence, and then flattens these lists into a single list.

For-Comprehensions

For-comprehensions provide a syntactic sugar for chaining multiple operations, including flatMap, map, and withFilter (a lazy version of filter). They allow for more readable code, especially when dealing with nested flatMap and map operations.

The general form of a for-comprehension is:

for 
  x <- xs
  y <- ys
  if condition 
yield expression

This is equivalent to:

xs.flatMap(x => ys.withFilter(y => condition).map(y => expression))

Example Using flatMap and for-Comprehension:

Imagine you want to find all pairs of numbers from two lists that sum up to a certain value.

Using flatMap and map:

val listA = List(1, 2, 3)
val listB = List(3, 4, 5)
val targetSum = 7

val pairs = listA.flatMap(a => listB.map(b => (a, b))).filter { case (a, b) => a + b == targetSum }
// pairs: List[(Int, Int)] = List((2,5), (3,4))

Using for-comprehension:

val pairsFor = for 
  a <- listA
  b <- listB
  if a + b == targetSum
yield (a, b)
// pairsFor: List[(Int, Int)] = List((2,5), (3,4))

Try

Handling exceptions is a crucial part of developing robust Scala applications. Scala provides a try-catch construct similar to other languages like Java, but with some functional twists that make it powerful and expressive. Scala's approach encourages the use of immutable values and provides mechanisms to deal with exceptions in a functional way.

Basic Try-Catch

In Scala, you use try-catch blocks to catch exceptions. The catch block uses pattern matching to handle different types of exceptions.

try 
  // Code that might throw an exception
  val result = 10 / 0
catch 
  case e: ArithmeticException => println("Arithmetic Exception caught: " + e.getMessage)
  case e: Exception => println("General exception caught: " + e.getMessage)
 finally 
  // Optional finally block executes regardless of whether an exception was caught
  println("Finally block executed")

The Try Type

Scala provides a Try type that represents a computation that may either result in an exception (Failure) or return a successfully computed value (Success). It is a better way to handle exceptions when working with functional programming paradigms.

To use Try, you need to import it from the Scala library:

import scala.util.{Try, Success, Failure}

You can wrap a computation in a Try, which will catch any non-fatal exceptions and return a Success with the value if the computation is successful, or a Failure with the exception if it is not.

val result: Try[Int] = Try(10 / 0)

You can then pattern match on the result:

result match 
  case Success(value) => println(s"Computation successful: $value")
  case Failure(exception) => println(s"Computation failed with exception: ${exception.getMessage}")

Chaining Operations with Try

One of the benefits of using Try is the ability to chain operations without having to explicitly check for exceptions at each step.

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

val result = divide(10, 0).map(_ * 2)

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

For-Comprehensions with Try

For-comprehensions can be used with Try to perform multiple operations that may fail, in a clean and readable way:

val forResult = 
  for 
    a <- Try(10 / 5)  // This succeeds
    b <- Try(a / 0)  // This fails
  yield b * 2

forResult match {
  case Success(value) => println(s"Result: $value")
  case Failure(exception) => println(s"Error: ${exception.getMessage}")
}

In the above example, the computation automatically stops at the first failure, and forResult becomes a Failure containing the exception.

Throw

In Scala, unlike Java, you're not required to declare checked exceptions using throws in the method signature. Scala doesn't distinguish between checked and unchecked exceptions; all exceptions are unchecked, meaning the compiler does not force you to catch or declare any exceptions. However, for documentation purposes or when interfacing with Java code, you might want to indicate that a method can throw an exception.

To annotate a method with the information that it might throw an exception, you can use the @throws annotation. This can improve readability and maintainability of your Scala code, especially for developers coming from a Java background or when Scala code is being called from Java.

Here’s how to use the @throws annotation in Scala:

def divide(a: Int, b: Int): Int = 
  if (b == 0) then
    throw new ArithmeticException("Division by zero.")
  else 
    a / b


// Annotating the method with @throws
@throws(classOf[ArithmeticException])
def divideWithAnnotation(a: Int, b: Int): Int = 
  if (b == 0) then
    throw new ArithmeticException("Division by zero.")
  else 
    a / b

In this example, the divideWithAnnotation method is explicitly annotated to indicate that it might throw an ArithmeticException. The @throws annotation takes the class of the exception you're warning about as a parameter.

This annotation is particularly useful when Scala methods are invoked from Java code, as it will inform Java developers about the potential exceptions, allowing them to handle these exceptions appropriately.

Remember, while the @throws annotation can be helpful for documentation and interoperability with Java, it does not change how Scala code behaves or is compiled. Scala treats all exceptions as unchecked, and the use of @throws is purely informational.

FP Design Patterns

Functional programming emphasizes the use of functions, immutability, and expressions over statements. Many traditional design patterns can be adapted to a functional style in Scala. Here are examples of functional programming design patterns in Scala:

1. Function Composition

Function composition is a fundamental concept in functional programming, where multiple functions are combined to produce a new function.

val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2

val addOneAndDouble: Int => Int = addOne.andThen(double)

// Usage
println(addOneAndDouble(3)) // 8 (3 + 1, then 4 * 2)

2. Higher-Order Functions

Higher-order functions are functions that take other functions as parameters or return functions as results.

def applyOperation(x: Int, y: Int, operation: (Int, Int) => Int): Int = operation(x, y)

val add: (Int, Int) => Int = _ + _
val multiply: (Int, Int) => Int = _ * _

// Usage
println(applyOperation(3, 4, add)) // 7
println(applyOperation(3, 4, multiply)) // 12

3. Currying

Currying transforms a function with multiple parameters into a series of functions, each with a single parameter.

def add(x: Int)(y: Int): Int = x + y

val addThree: Int => Int = add(3)

// Usage
println(addThree(4)) // 7
println(add(2)(3)) // 5

4. Partial Function Application

Partial function application involves fixing a few arguments of a function, producing another function of smaller arity.

def multiply(x: Int, y: Int, z: Int): Int = x * y * z

val multiplyBy2And3: Int => Int = multiply(2, 3, _)

// Usage
println(multiplyBy2And3(4)) // 24 (2 * 3 * 4)

5. Memoization

Memoization is an optimization technique to cache the results of expensive function calls and return the cached result when the same inputs occur again.

def memoize[I, O](f: I => O): I => O = {
  val cache = scala.collection.mutable.Map[I, O]()
  (input: I) =>
    cache.getOrElseUpdate(input, f(input))
}

val fib: Int => Int = {
  def fibHelper(n: Int): Int = n match {
    case 0 => 0
    case 1 => 1
    case _ => fib(n - 1) + fib(n - 2)
  }
  memoize(fibHelper)
}

// Usage
println(fib(10)) // 55

6. Functor

A functor is a structure that can be mapped over. In Scala, the map method on collections is an example.

val list = List(1, 2, 3)
val doubledList = list.map(_ * 2)

// Usage
println(doubledList) // List(2, 4, 6)

7. Monad

A monad is a design pattern used to handle program-wide concerns in a functional way. The flatMap method is used to chain operations.

val list = List(1, 2, 3)
val result = list.flatMap(x => List(x, x * 2))

// Usage
println(result) // List(1, 2, 2, 4, 3, 6)

8. Option Monad

The Option monad is used to represent optional values, providing a way to handle the absence of a value.

val someValue: Option[Int] = Some(5)
val noValue: Option[Int] = None

val result = someValue.map(_ * 2).getOrElse(0)
val result2 = noValue.map(_ * 2).getOrElse(0)

// Usage
println(result) // 10
println(result2) // 0

9. Either Monad

The Either monad is used to represent a value of one of two possible types (a disjoint union). It is often used for error handling.

def divide(x: Int, y: Int): Either[String, Int] = {
  if (y == 0) Left("Division by zero")
  else Right(x / y)
}

val result = divide(10, 2) match {
  case Right(value) => s"Result: $value"
  case Left(error) => s"Error: $error"
}

val result2 = divide(10, 0) match {
  case Right(value) => s"Result: $value"
  case Left(error) => s"Error: $error"
}

// Usage
println(result) // Result: 5
println(result2) // Error: Division by zero

10. Lazy Evaluation

Lazy evaluation is a technique where expressions are not evaluated until their results are needed.

lazy val expensiveComputation: Int = {
  println("Computing...")
  42
}

// Usage
println("Before accessing lazy value")
println(expensiveComputation) // "Computing..." followed by 42
println(expensiveComputation) // 42 (without recomputing)

11. Type Classes

Type classes enable ad-hoc polymorphism in a functional way.

trait Show[A] {
  def show(a: A): String
}

object ShowInstances {
  implicit val intShow: Show[Int] = (a: Int) => a.toString
  implicit val stringShow: Show[String] = (a: String) => a
}

object Show {
  def apply[A](implicit instance: Show[A]): Show[A] = instance

  def show[A: Show](a: A): String = Show[A].show(a)
}

// Usage
import ShowInstances._

println(Show.show(123)) // "123"
println(Show.show("Hello")) // "Hello"

Testing in Scala

Testing is a crucial part of software development that ensures your code behaves as expected. Scala, being a popular language on the JVM, has several testing frameworks that you can use to write unit tests, integration tests, and more. Here are some of the most widely used testing frameworks in the Scala ecosystem:

1. ScalaTest

ScalaTest is one of the most flexible and comprehensive testing libraries available for Scala. It supports different styles of testing, making it adaptable to various testing needs and preferences. ScalaTest can be used for unit testing, property-based testing, and even integration testing. It integrates seamlessly with other tools and libraries like SBT (Scala Build Tool), Maven, Jenkins, and IntelliJ IDEA.

Features:

  • Supports multiple testing styles (FlatSpec, FunSuite, WordSpec, etc.).
  • Easy integration with mocking frameworks.
  • Rich matchers for more readable assertions.

2. Specs2

Specs2 is a library for writing executable software specifications. With Specs2, you can write specifications for your Scala code using a very expressive Domain Specific Language (DSL). It's designed to produce well-formatted and detailed reports.

Features:

  • Supports Behavior-Driven Development (BDD) and Acceptance Test-Driven Development (ATDD).
  • Integrates with ScalaCheck for property-based testing.
  • Provides matchers for various types of assertions.
  • Supports mocking through integration with libraries like Mockito.

3. ScalaCheck

ScalaCheck is a library for property-based testing. It allows you to specify properties that your code should satisfy, and then automatically generates test data to verify those properties. It's inspired by Haskell's QuickCheck and can be used standalone or integrated with ScalaTest or Specs2.

Features:

  • Automatic test data generation.
  • Supports custom generators and shrinkers.
  • Can be integrated with ScalaTest and Specs2.

4. uTest

uTest is a simple testing framework with a minimalistic design. It aims to provide the essentials for testing Scala code without additional complexity. uTest's syntax is straightforward, making it easy to write and read tests.

Features:

  • Simple and concise syntax.
  • Supports asynchronous testing.
  • Provides detailed test reports.

5. minitest

minitest is a small, opinionated library for writing tests in Scala. It aims to be simple and fast, with a focus on being functional. minitest supports both synchronous and asynchronous tests, making it suitable for a wide range of testing scenarios.

Features:

  • Simple and lightweight.
  • Supports asynchronous testing out of the box.
  • Easy integration with SBT.

Choosing a Testing Framework

The choice of a testing framework depends on your project's specific needs, your team's preferences, and the kind of tests you plan to write. ScalaTest and Specs2 are more feature-rich and offer more flexibility, making them suitable for larger projects with diverse testing requirements. ScalaCheck is excellent for property-based testing, adding another layer of confidence in your code's correctness. For simpler projects, or if you prefer a more minimalistic approach, uTest or minitest might be more appropriate.

Regardless of the framework you choose, the important thing is to write tests. Testing helps catch bugs early, improves code quality, and can even guide your design, leading to more maintainable and robust Scala applications.

FlatSpec

ScalaTest is a versatile testing library in Scala that supports different testing styles, making it adaptable to various testing needs. Here's a basic tutorial on how to get started with ScalaTest, covering setup, writing tests, and running them.

Setting Up

To use ScalaTest, you need to add it as a dependency in your build tool configuration. If you're using sbt, add the following to your build.sbt:

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test

Ensure you're using the latest version of ScalaTest by checking the ScalaTest website.

Writing Tests

ScalaTest supports multiple testing styles. We'll use the FlatSpec style for this tutorial, which is a good choice for behavior-driven development (BDD).

Create a Scala class or object for the component you want to test. Here's a simple example:

object Calculator {
  def add(a: Int, b: Int): Int = a + b
}

Now, create a test class in your src/test/scala directory. Here's how you might test the Calculator object:

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class CalculatorSpec extends AnyFlatSpec with Matchers {

  "The Calculator" should "add two numbers correctly" in {
    Calculator.add(2, 3) should be (5)
  }

  it should "add negative numbers correctly" in {
    Calculator.add(-1, -1) should be (-2)
  }

  it should "add a number to zero correctly" in {
    Calculator.add(0, 5) should be (5)
  }
}

Running Tests

If you're using sbt, you can run your tests from the terminal with the following command:

sbt test

This command will compile your test classes and run all tests, reporting the results in the terminal.

Understanding the Test Code

  • AnyFlatSpec: This is a base class for writing tests in a "flat" specification style. You describe behaviors and write tests for each behavior.
  • Matchers: This trait provides a domain-specific language (DSL) for expressing assertions about values in your tests. For example, should be is part of this DSL.
  • "The Calculator" should "add two numbers correctly" in: This syntax describes a behavior and a test scenario. The block following in contains the test code for that scenario.

Further Testing Capabilities

ScalaTest offers much more than simple assertions:

  • ScalaTest with ScalaCheck: For property-based testing, ScalaTest can be integrated with ScalaCheck to automatically generate test cases.
  • Asynchronous Testing: ScalaTest supports testing asynchronous code, making it easier to test Futures and other async operations.
  • Before and After Hooks: ScalaTest provides BeforeAndAfter and BeforeAndAfterEach traits that you can mix into your test classes to set up preconditions or clean up after your tests.

FunSuite

Using AnyFunSuite with ScalaTest allows for writing tests in a very flexible and straightforward way, combining the benefits of specification and testing. Let's apply this to a Calculator example, focusing on creating a suite of tests to verify its functionality.

Step 1: Implement the Calculator

Create a simple Calculator object with basic arithmetic operations. Place this in your src/main/scala directory if you're following standard Scala project structure.

object Calculator {
  def add(a: Int, b: Int): Int = a + b
  def subtract(a: Int, b: Int): Int = a - b
  def multiply(a: Int, b: Int): Int = a * b
  def divide(a: Int, b: Int): Option[Int] = if (b == 0) None else Some(a / b)
}

Step 2: Set Up ScalaTest Dependency

Make sure ScalaTest is included in your build.sbt:

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test

Step 3: Write Tests Using AnyFunSuite

Now, create a test class for the Calculator using AnyFunSuite. This class should be in the src/test/scala directory.

import org.scalatest.funsuite.AnyFunSuite

class CalculatorTest extends AnyFunSuite {

  test("Calculator.add should return the sum of two numbers") {
    assert(Calculator.add(1, 2) === 3)
  }

  test("Calculator.subtract should return the difference of two numbers") {
    assert(Calculator.subtract(5, 3) === 2)
  }

  test("Calculator.multiply should return the product of two numbers") {
    assert(Calculator.multiply(4, 3) === 12)
  }

  test("Calculator.divide should return the quotient of two numbers") {
    assert(Calculator.divide(10, 2) === Some(5))
  }

  test("Calculator.divide should return None when dividing by zero") {
    assert(Calculator.divide(5, 0) === None)
  }
}

Step 4: Running Tests

To run your tests, use sbt from the command line in your project root:

sbt test

This command will execute all tests in your project, including the CalculatorTest suite you just defined.

Understanding the Test Code

  • AnyFunSuite: This is a base class for writing tests in a "fun" style, which is a simple way to write tests where you define tests as functions.
  • test Method: Each test is defined using the test method, where you provide a descriptive name for the test and a block of code that implements the test.
  • Assertions: The assert method is used to verify that the operation performed by the Calculator yields the expected result.

FlatSpec

Using FlatSpec in ScalaTest allows for behavior-driven development (BDD) style testing, which is very expressive and helps in writing readable tests. Let's apply FlatSpec to test the Calculator example, focusing on behavior descriptions and testing scenarios.

Step 1: Implement the Calculator

First, define a simple Calculator object with basic arithmetic operations:

object Calculator {
  def add(a: Int, b: Int): Int = a + b
  def subtract(a: Int, b: Int): Int = a - b
  def multiply(a: Int, b: Int): Int = a * b
  def divide(a: Int, b: Int): Either[String, Int] =
    if (b == 0) Left("Division by zero")
    else Right(a / b)
}

Notice the divide method returns an Either[String, Int] to handle division by zero gracefully.

Step 2: Set Up ScalaTest Dependency

Ensure ScalaTest is added to your build.sbt:

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test

Step 3: Write Tests Using FlatSpec

Create a test class for Calculator using FlatSpec. This style allows you to specify the behavior of an operation and then describe tests that confirm the behavior.

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class CalculatorSpec extends AnyFlatSpec with Matchers {

  "The Calculator add method" should "return the sum of two numbers" in {
    Calculator.add(1, 2) should be (3)
  }

  it should "return the correct sum when adding negative numbers" in {
    Calculator.add(-1, -2) should be (-3)
  }

  "The Calculator subtract method" should "return the difference of two numbers" in {
    Calculator.subtract(5, 3) should be (2)
  }

  "The Calculator multiply method" should "return the product of two numbers" in {
    Calculator.multiply(5, 4) should be (20)
  }

  "The Calculator divide method" should "return the quotient of two numbers" in {
    Calculator.divide(10, 2) should be (Right(5))
  }

  it should "return an error for division by zero" in {
    Calculator.divide(5, 0) should be (Left("Division by zero"))
  }
}

Step 4: Running Tests

To run your tests, use the sbt command line tool in your project root:

sbt test

This will compile and execute all test suites in your project, including the CalculatorSpec.

Understanding the Test Code

  • AnyFlatSpec with Matchers: This combination allows for a BDD style testing. Matchers provide a readable way to assert conditions.
  • Behavior Descriptions: Using should and strings, you can describe the behavior you're testing. It's a natural language-like approach that makes tests easier to read and understand.
  • Tests for Each Behavior: Under each behavior description, use it should to specify different scenarios or conditions you're testing for that behavior.

Async Test

Testing asynchronous code in Scala, especially when working with futures, requires a testing framework that can handle asynchronous results. ScalaTest provides excellent support for writing tests for asynchronous operations through its AsyncTestSuite traits. Let's demonstrate how to write an asynchronous test for a hypothetical asynchronous version of the Calculator that returns Future[Int] results.

Step 1: Implement the Async Calculator

First, we'll define an asynchronous Calculator object. For demonstration purposes, we'll make the add method asynchronous, returning a Future[Int].

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

object AsyncCalculator {
  def add(a: Int, b: Int): Future[Int] = Future {
    // Simulate a computation that takes time
    Thread.sleep(100)
    a + b
  }
}

Step 2: Set Up ScalaTest Dependency

Ensure you have ScalaTest added to your build.sbt file:

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test

Step 3: Write an Asynchronous Test

ScalaTest's AsyncFunSuite is designed for testing asynchronous code. It allows tests to return Future[Assertion]. Here's how you can test the asynchronous add method of our AsyncCalculator.

import org.scalatest.funsuite.AsyncFunSuite
import org.scalatest.matchers.should.Matchers

class AsyncCalculatorSpec extends AsyncFunSuite with Matchers {

  test("AsyncCalculator.add should correctly add two numbers") {
    val sumFuture = AsyncCalculator.add(1, 2) // This returns a Future[Int]
    sumFuture.map(sum => sum should be (3)) // The assertion is wrapped in a map and returns a Future[Assertion]
  }

}

Step 4: Running the Test

Execute your asynchronous tests by running:

sbt test

Understanding the Test Code

  • AsyncFunSuite: This suite is specifically designed for asynchronous code, allowing tests to return Future[Assertion] directly. ScalaTest handles the future and correctly reports the test result once the future completes.
  • Future.map: The assertion is made within the map of the Future, which transforms the result of the future (Int) into a test assertion. The entire expression returns a Future[Assertion], which AsyncFunSuite awaits.

Functional Testing

Functional test suites in Scala can be created using various testing frameworks like ScalaTest, Specs2, or ScalaCheck. These frameworks allow for testing Scala applications in a way that aligns with functional programming principles, ensuring that functions and methods behave as expected across a wide range of inputs and conditions. Functional testing typically involves testing the software against its functional requirements and specifications to ensure it performs its intended tasks correctly.

Using ScalaTest for Functional Testing

ScalaTest is a versatile testing framework that supports multiple styles of testing, making it well-suited for functional testing. Here's a brief guide on setting up functional test suites with ScalaTest.

Setup

Ensure ScalaTest is added to your build.sbt:

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test

Writing Functional Tests

Suppose you have a function that you want to test functionally. For example, a simple service that processes user data:

case class User(id: Int, name: String, email: String)

trait UserService {
  def findUserById(id: Int): Option[User]
  def updateUser(user: User): Option[User]
}

You can write functional tests to verify the behavior of these methods under various conditions:

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers

class UserServiceSpec extends AnyFunSpec with Matchers {

  describe("UserService") {
    
    val mockUsers = Map(
      1 -> User(1, "John Doe", "john@example.com"),
      2 -> User(2, "Jane Doe", "jane@example.com")
    )

    // Mock implementation for testing
    val userService: UserService = new UserService {
      def findUserById(id: Int): Option[User] = mockUsers.get(id)
      def updateUser(user: User): Option[User] = Some(user.copy(name = user.name.toUpperCase))
    }

    describe("findUserById") {
      it("should return a user for a valid id") {
        userService.findUserById(1) should be(Some(User(1, "John Doe", "john@example.com")))
      }

      it("should return None for an invalid id") {
        userService.findUserById(999) should be(None)
      }
    }

    describe("updateUser") {
      it("should update the user's name to upper case") {
        val originalUser = User(2, "Jane Doe", "jane@example.com")
        val updatedUser = userService.updateUser(originalUser)
        updatedUser should be(Some(User(2, "JANE DOE", "jane@example.com")))
      }
    }
  }
}

Running Tests

Execute your tests using sbt:

sbt test

Advantages of Functional Test Suites

  1. Specification Validation: Functional tests ensure that the application behaves according to its specifications, verifying each feature's correct implementation.
  2. Comprehensive Coverage: By focusing on the software's functionality, you can cover more cases and usage scenarios, which might be missed by unit tests alone.
  3. Refactoring Confidence: Functional tests provide a safety net that allows developers to refactor code with confidence, knowing that changes won't break the application's intended behavior.

Specs2

Specs2 is a powerful Scala library designed for writing software specifications and tests. It encourages behavior-driven development (BDD) and is well-suited for both unit and acceptance testing. Let's use Specs2 to test the Calculator example, focusing on behavior and readability.

Setting Up

First, ensure Specs2 is included in your build.sbt dependencies:

libraryDependencies ++= Seq(
  "org.specs2" %% "specs2-core" % "4.10.6" % Test,
  "org.specs2" %% "specs2-matcher-extra" % "4.10.6" % Test // For extra matchers
)

Check for the latest version of Specs2 to use in your project.

The Calculator Object

Here's a simple Calculator object to test:

object Calculator {
  def add(a: Int, b: Int): Int = a + b
  def subtract(a: Int, b: Int): Int = a - b
  def multiply(a: Int, b: Int): Int = a * b
  def divide(a: Int, b: Int): Option[Int] = if (b == 0) None else Some(a / b)
}

Writing Specifications with Specs2

Create a new specification in your src/test/scala directory. Specs2 supports a flexible way to write tests, including the should and must styles. Here, we'll use the should style for clarity:

import org.specs2.mutable.Specification

class CalculatorSpec extends Specification {
  "Calculator" should {
    "correctly add two numbers" in {
      Calculator.add(1, 2) must_== 3
    }

    "correctly subtract two numbers" in {
      Calculator.subtract(5, 3) must_== 2
    }

    "correctly multiply two numbers" in {
      Calculator.multiply(3, 4) must_== 12
    }

    "return None when dividing by zero" in {
      Calculator.divide(5, 0) must beNone
    }

    "correctly divide two numbers" in {
      Calculator.divide(10, 2) must beSome(5)
    }
  }
}

Running Tests

To run your Specs2 tests, use sbt:

sbt test

This command will compile and execute all test specifications.

Understanding the Specs2 Specification

  • Specification: This class is extended to define a Specs2 specification. Each specification contains examples of expected behavior.
  • must_==: This matcher tests for equality. It's part of Specs2's rich set of matchers that allow for expressive tests.
  • beNone and beSome: These option matchers are used for testing Scala Option values, making it easy to write clear and concise tests for optional values.

Parallel Programming

Let's break down what happens when the following code is run in parallel:

var x = 3
x = x + 1
x = x * 2
println(x)

Running in Parallel

When the code runs sequentially in a single thread, it executes predictably. However, if the operations are run in parallel, the behavior can become unpredictable due to race conditions. Let's examine this in the context of parallel execution.

Potential Parallel Execution:

Let's assume we try to run the increment and multiplication steps in parallel threads:

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._

var x = 3

val increment = Future {
  x = x + 1
}

val multiply = Future {
  x = x * 2
}

Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x)

Potential Issues in Parallel Execution:

  1. Race Condition:
  • A race condition occurs when two or more threads access shared data and try to change it simultaneously. The result depends on the order in which the threads execute.
  1. Indeterminate Results:
  • The final value of x depends on the timing of the increment and multiply operations.
  • If increment runs first and completes before multiply starts, x will be incremented to 4 first, then multiplied by 2 to get 8.
  • If multiply runs first and completes before increment starts, x will be multiplied by 2 to get 6 first, then incremented by 1 to get 7.
  • If both operations interfere with each other, intermediate states can result in completely unpredictable values.

Examples of Possible Outcomes:

  • Outcome 1:

    • increment reads x as 3.
    • multiply reads x as 3.
    • increment writes x as 4.
    • multiply writes x as 6.
    • Final value of x is 6.
  • Outcome 2:

    • multiply reads x as 3.
    • increment reads x as 3.
    • multiply writes x as 6.
    • increment writes x as 7.
    • Final value of x is 7.
  • Outcome 3:

    • increment completes entirely first, x becomes 4.
    • multiply completes next, x becomes 8.
    • Final value of x is 8.

Solutions to Ensure Correctness:

To ensure that the operations on x are performed correctly and predictably, you can use synchronization mechanisms:

  1. Synchronized Block:
  • Ensure that only one thread can modify x at a time.
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._

var x = 3
val lock = new AnyRef

val increment = Future {
  lock.synchronized {
    x = x + 1
  }
}

val multiply = Future {
  lock.synchronized {
    x = x * 2
  }
}

Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x)
  1. Atomic Variables:
  • Use atomic variables to ensure atomicity of operations.
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._

val x = new AtomicInteger(3)

val increment = Future {
  x.incrementAndGet()
}

val multiply = Future {
  x.updateAndGet(n => n * 2)
}

Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x.get())

Semaphore

Using locks in parallel programming is a common way to ensure that only one thread accesses a critical section of code at a time. This helps prevent race conditions and ensures data consistency. Scala relies on Java’s concurrency utilities for lock management. Here, we'll use ReentrantLock from the java.util.concurrent.locks package.

Using ReentrantLock in Scala

Importing the necessary classes:

import java.util.concurrent.locks.ReentrantLock
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
import scala.concurrent.duration._

Creating and using a ReentrantLock:

  1. Create the lock:

    val lock = new ReentrantLock()
    
  2. Acquire and release the lock:

    Use lock.lock() to acquire the lock and lock.unlock() to release it. It’s crucial to ensure that the lock is always released, typically using a try-finally block.

    def accessResource(threadName: String): Unit = {
      lock.lock()
      try {
        println(s"$threadName is accessing the resource.")
        Thread.sleep(1000) // Simulate some work with the resource
      } finally {
        println(s"$threadName is releasing the resource.")
        lock.unlock()
      }
    }
    
  3. Run tasks in parallel:

    Use Scala's Future to run tasks in parallel.

    val tasks = for (i <- 1 to 10) yield Future {
      accessResource(s"Thread-$i")
    }
    
    val aggregatedFuture = Future.sequence(tasks)
    
  4. Wait for all tasks to complete:

    Use Await.result to wait for all futures to complete.

    Await.result(aggregatedFuture, 10.seconds)
    

Complete Example

Here is the complete code example putting all the pieces together:

import java.util.concurrent.locks.ReentrantLock
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
import scala.concurrent.duration._

object LockExample extends App {
  // Create a reentrant lock
  val lock = new ReentrantLock()

  // Define a function to access the resource
  def accessResource(threadName: String): Unit = {
    lock.lock()
    try {
      println(s"$threadName is accessing the resource.")
      Thread.sleep(1000) // Simulate some work with the resource
    } finally {
      println(s"$threadName is releasing the resource.")
      lock.unlock()
    }
  }

  // Run tasks in parallel using Futures
  val tasks = for (i <- 1 to 10) yield Future {
    accessResource(s"Thread-$i")
  }

  // Aggregate all futures
  val aggregatedFuture = Future.sequence(tasks)

  // Wait for all tasks to complete
  Await.result(aggregatedFuture, 10.seconds)
}

LockExample.main(Array())

Explanation

  • ReentrantLock Creation: new ReentrantLock() creates a reentrant lock.
  • Access Resource Function: accessResource function locks the critical section, performs the operation (simulated by Thread.sleep), and then unlocks the critical section. The try-finally block ensures the lock is always released.
  • Parallel Tasks: A sequence of 10 futures is created, each calling accessResource with a different thread name.
  • Aggregated Future: Future.sequence combines the futures into a single future that completes when all the tasks are done.
  • Await Completion: Await.result waits for the aggregated future to complete, ensuring the main program does not exit prematurely.

Alternative: Using synchronized

In addition to using ReentrantLock, Scala provides a simpler way to achieve mutual exclusion using the synchronized keyword. This is a more concise way to protect critical sections of code.

Using synchronized:

object SynchronizedExample extends App {
  // Define a shared resource
  var sharedCounter = 0

  // Define a function to access the resource
  def incrementCounter(threadName: String): Unit = synchronized {
    println(s"$threadName is accessing the resource.")
    sharedCounter += 1
    println(s"$threadName incremented the counter to $sharedCounter.")
    Thread.sleep(1000) // Simulate some work with the resource
  }

  // Run tasks in parallel using Futures
  val tasks = for (i <- 1 to 10) yield Future {
    incrementCounter(s"Thread-$i")
  }

  // Aggregate all futures
  val aggregatedFuture = Future.sequence(tasks)

  // Wait for all tasks to complete
  Await.result(aggregatedFuture, 10.seconds)
}

SynchronizedExample.main(Array())

Explanation

  • Synchronized Block: The synchronized keyword is used to ensure that only one thread can execute the incrementCounter function at a time.
  • Shared Resource: A shared variable sharedCounter is incremented by each thread, ensuring mutual exclusion.
  • Parallel Tasks: A sequence of 10 futures is created, each calling incrementCounter with a different thread name.
  • Aggregated Future: Future.sequence combines the futures into a single future that completes when all the tasks are done.
  • Await Completion: Await.result waits for the aggregated future to complete, ensuring the main program does not exit prematurely.

Threads

Using threads in Scala is similar to using threads in Java since Scala runs on the JVM and can leverage the Java concurrency utilities. Below are examples demonstrating how to create and use threads in Scala.

1. Creating and Starting a Thread

You can create a thread by extending the Thread class and overriding its run method.

class MyThread extends Thread {
  override def run(): Unit = {
    println(s"Thread ${Thread.currentThread().getName} is running")
  }
}

// Usage
val thread1 = new MyThread()
val thread2 = new MyThread()

thread1.start()
thread2.start()

thread1.join() // Wait for thread1 to finish
thread2.join() // Wait for thread2 to finish

2. Using Runnable

Another common way is to implement the Runnable interface and pass an instance to a Thread object.

class MyRunnable extends Runnable {
  override def run(): Unit = {
    println(s"Runnable ${Thread.currentThread().getName} is running")
  }
}

// Usage
val runnable1 = new MyRunnable()
val runnable2 = new MyRunnable()

val thread1 = new Thread(runnable1)
val thread2 = new Thread(runnable2)

thread1.start()
thread2.start()

thread1.join() // Wait for thread1 to finish
thread2.join() // Wait for thread2 to finish

3. Using Anonymous Runnable

You can also use an anonymous Runnable instance.

val thread1 = new Thread(new Runnable {
  override def run(): Unit = {
    println(s"Anonymous Runnable ${Thread.currentThread().getName} is running")
  }
})

val thread2 = new Thread(new Runnable {
  override def run(): Unit = {
    println(s"Anonymous Runnable ${Thread.currentThread().getName} is running")
  }
})

thread1.start()
thread2.start()

thread1.join() // Wait for thread1 to finish
thread2.join() // Wait for thread2 to finish

4. Using Lambda Expressions

In Scala, you can use lambda expressions to create Runnable instances more concisely.

val thread1 = new Thread(() => println(s"Lambda Runnable ${Thread.currentThread().getName} is running"))
val thread2 = new Thread(() => println(s"Lambda Runnable ${Thread.currentThread().getName} is running"))

thread1.start()
thread2.start()

thread1.join() // Wait for thread1 to finish
thread2.join() // Wait for thread2 to finish

5. Synchronization

To avoid race conditions, you might need to synchronize critical sections of your code.

object Counter {
  private var count = 0

  def increment(): Unit = synchronized {
    count += 1
    println(s"Count: $count by ${Thread.currentThread().getName}")
  }

  def getCount: Int = count
}

class IncrementThread extends Thread {
  override def run(): Unit = {
    for (_ <- 1 to 1000) {
      Counter.increment()
    }
  }
}

// Usage
val threads = List.fill(10)(new IncrementThread())

threads.foreach(_.start())
threads.foreach(_.join())

println(s"Final count: ${Counter.getCount}")

6. Using Executors

For a higher-level approach, you can use the ExecutorService from the java.util.concurrent package to manage a pool of threads.

import java.util.concurrent.{Executors, ExecutorService}

val executor: ExecutorService = Executors.newFixedThreadPool(3)

val tasks = List(
  new Runnable {
    def run(): Unit = println(s"Task 1 running on ${Thread.currentThread().getName}")
  },
  new Runnable {
    def run(): Unit = println(s"Task 2 running on ${Thread.currentThread().getName}")
  },
  new Runnable {
    def run(): Unit = println(s"Task 3 running on ${Thread.currentThread().getName}")
  }
)

tasks.foreach(executor.submit)

executor.shutdown() // Prevent new tasks from being submitted
executor.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS) // Wait for all tasks to complete

7. Using Future and Promise

For more advanced and functional approaches to concurrency, you can use Future and Promise in Scala.

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.util.{Success, Failure}

def longRunningTask(x: Int): Int = {
  Thread.sleep(1000) // Simulate a long computation
  x * x
}

val future1 = Future {
  longRunningTask(10)
}
val future2 = Future {
  longRunningTask(20)
}

// Combining futures
val combinedFuture = for {
  result1 <- future1
  result2 <- future2
} yield result1 + result2

combinedFuture.onComplete {
  case Success(value) => println(s"Combined result: $value")
  case Failure(exception) => println(s"Failed: $exception")
}

// Waiting for the future to complete (for demonstration purposes only, typically avoid blocking)
Await.result(combinedFuture, 5.seconds)

Parallel Patterns

Parallel programming in Scala can be achieved using various libraries and constructs that leverage the power of modern multi-core processors. Below are some common parallel programming patterns and techniques in Scala, including examples using Scala's Future, parallel collections, and Akka.

1. Futures and Promises

Futures provide a way to handle asynchronous computations. They can be used to perform operations in parallel and handle the results once they are completed.

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.util.{Success, Failure}

def slowComputation(x: Int): Int = {
  Thread.sleep(1000) // Simulate a long computation
  x * x
}

// Creating a Future
val futureResult: Future[Int] = Future {
  slowComputation(10)
}

// Handling the result
futureResult.onComplete {
  case Success(value) => println(s"Result: $value")
  case Failure(exception) => println(s"Failed: $exception")
}

// Waiting for the future to complete (for demonstration purposes only, typically avoid blocking)
Await.result(futureResult, 2.seconds)

2. Parallel Collections

Parallel collections provide a simple way to parallelize operations on collections.

import scala.collection.parallel.CollectionConverters._

val list = List(1, 2, 3, 4, 5)

// Using parallel collections
val parallelList = list.par
val result = parallelList.map(_ * 2)

// Converting back to sequential collection
val sequentialResult = result.seq

// Usage
println(sequentialResult) // List(2, 4, 6, 8, 10)

3. Akka Actors

Akka provides a powerful model for building concurrent, distributed, and fault-tolerant applications using actors.

First, add the Akka dependencies to your build.sbt:

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.20"

Then, you can define and use actors:

import akka.actor._

case object Start
case class Compute(x: Int)

class Worker extends Actor {
  def receive: Receive = {
    case Compute(x) =>
      val result = x * x
      println(s"Computed $result for $x")
  }
}

class Master extends Actor {
  val worker = context.actorOf(Props[Worker], "worker")

  def receive: Receive = {
    case Start =>
      for (i <- 1 to 5) worker ! Compute(i)
  }
}

// Creating the actor system
val system = ActorSystem("MyActorSystem")
val master = system.actorOf(Props[Master], "master")

// Starting the computation
master ! Start

// Shutting down the system
system.terminate()

4. Parallel Execution with Future Combinators

You can also combine multiple futures to perform parallel operations and combine their results.

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.util.{Success, Failure}

val future1 = Future { slowComputation(10) }
val future2 = Future { slowComputation(20) }
val future3 = Future { slowComputation(30) }

// Combining futures
val combinedFuture = for {
  result1 <- future1
  result2 <- future2
  result3 <- future3
} yield result1 + result2 + result3

// Handling the result
combinedFuture.onComplete {
  case Success(value) => println(s"Combined result: $value")
  case Failure(exception) => println(s"Failed: $exception")
}

// Waiting for the combined future to complete (for demonstration purposes only, typically avoid blocking)
Await.result(combinedFuture, 5.seconds)

5. Parallel Streams with Akka Streams

Akka Streams provide a powerful way to handle streaming data and back-pressure. Add the Akka Streams dependency to your build.sbt:

libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.6.20"

Then, you can define and run a stream:

import akka.actor.ActorSystem
import akka.stream._
import akka.stream.scaladsl._

implicit val system = ActorSystem("MyActorSystem")
implicit val materializer = Materializer(system)

val source = Source(1 to 10)
val sink = Sink.foreach[Int](println)
val flow = Flow[Int].map(_ * 2)

// Connecting the source, flow, and sink
val runnableGraph = source.via(flow).to(sink)

// Running the stream
runnableGraph.run()

// Shutting down the system
system.terminate()

Structure

The Play Framework is known for its simplicity and scalability, largely due to its architecture, which is designed to make web application development more productive and performance-oriented. Here's an overview of the Play Framework's architecture, highlighting its main components and how they interact to serve web applications:

Core Components of Play Framework Architecture

  1. Web Server: Play includes a built-in web server (Akka HTTP or Netty in older versions) that eliminates the need for deploying your application on an external web server. This simplification speeds up development and testing processes.

  2. Request Handler: At its core, Play uses a lightweight, stateless model to handle requests. When a request comes in, it's handled by a routing mechanism that dispatches it to the appropriate controller action based on the defined routes.

  3. Router: The router is a key component that maps incoming HTTP requests to controller actions. Routes are defined in a simple and readable format within the conf/routes file, allowing for clean URL design and RESTful architectures.

  4. Controllers: Controllers are responsible for handling incoming requests and returning responses to the client. They act as the middleman between the user interface and the application's data logic, processing data, calling business logic, and preparing the response (e.g., rendering HTML, returning JSON).

  5. Actions: Actions are the actual methods in controllers that process the requests. Each action can perform operations like accessing databases, performing computations, or calling other services before generating a result to send back to the client.

  6. Results: Results are what actions return in response to HTTP requests. They include status codes, headers, and the body (which could be HTML, JSON, XML, etc.), telling the client what happened with their request.

  7. Models: In Play, models represent the application's data and are typically used in conjunction with a database. Play does not enforce a specific ORM (Object-Relational Mapping) tool, giving developers the freedom to choose the best tool for their application, such as Ebean, Slick, or JPA.

  8. Views: Play uses Twirl as its template engine for rendering HTML. Views in Play are essentially Scala functions that take model data and produce HTML output. This allows for dynamic content generation based on the application's state.

Asynchronous and Non-Blocking

One of the key architectural features of Play is its support for asynchronous and non-blocking operations. This is crucial for building scalable applications that can handle long-running operations or IO-bound tasks without blocking threads. Play achieves this by integrating with Scala's Futures and Akka, allowing developers to write non-blocking code that can scale across multiple cores and servers.

Modular Architecture

Play is modular, meaning that applications can include only the components they need. This keeps the application lightweight and efficient. Developers can add modules for various functionalities, including but not limited to database access, authentication, and caching.

Built on Akka

The framework is built on top of Akka, a toolkit for building highly concurrent, distributed, and resilient message-driven applications. This foundation provides Play applications with a robust concurrency model and lets them easily scale out across multiple servers.

Getting started

1. Install sbt (Scala Build Tool)

First, you need to have sbt, the interactive build tool for Scala, installed on your machine. You can download it from the official sbt website. Follow the installation instructions specific to your operating system.

2. Create a New Play Application

Open a terminal or command prompt and run the following command to create a new Play project:

sbt new playframework/play-scala-seed.g8

This command creates a new directory with a simple Play application template. Navigate into your project directory to start working on your application.

3. Explore the Project Structure

The generated project has several important directories and files:

  • app/: Contains your Scala application code, including controllers and models.
  • conf/: Contains configuration files, including routes which defines URL mappings to controllers.
  • public/: Contains static assets like images, JavaScript, and CSS files.
  • test/: Contains tests for your application.

4. Define Routes

Open the conf/routes file. This is where you define the URLs that your application responds to. Each line in this file consists of an HTTP method, a path, and an action. For example:

GET     /hello       controllers.HomeController.hello()

5. Implement Controller Actions

Controllers handle incoming requests and return responses. They are located in the app/controllers directory. To respond to our /hello route, you could create a HomeController like this:

package controllers

import play.api.mvc._

class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {

  def hello() = Action { implicit request: Request[AnyContent] =>
    Ok("Hello, Play Framework!")
  }
}

6. Run Your Application

To run your Play application, open a terminal in your project directory and execute:

sbt run

Your application will start on port 9000 by default. Open a web browser and go to http://localhost:9000/hello to see your application in action.

7. Learn More

This tutorial only scratches the surface of what you can do with the Play Framework. Here are some suggestions for next steps:

  • Explore Templates: Learn how to use Twirl, Play's templating engine, to create dynamic HTML content.
  • Database Access: Look into integrating a database using Play's built-in support for Slick or other ORMs.
  • Authentication: Implement user authentication and authorization for your application.
  • Asynchronous Programming: Utilize Play's support for asynchronous programming to handle long-running operations without blocking.

The official Play documentation is an excellent resource for in-depth exploration of these and other topics. The Scala community and Stack Overflow are also great places to find help and learn from others' experiences.

Twirl

Twirl is the templating engine used by the Play Framework for generating dynamic HTML content. It allows developers to write Scala code within HTML templates, which makes it possible to render dynamic data on web pages efficiently. Here's a basic tutorial to get you started with Twirl in a Play Framework application:

Step 1: Understanding Twirl Template Syntax

Twirl templates are basically HTML files with Scala code embedded in them. The files have a .scala.html extension and are located in the app/views directory of a Play project. The syntax for embedding Scala code is straightforward:

  • @expression to output the value of an expression.
  • @{code block} for more complex Scala code.
  • @for(item <- items) { ... } to iterate over collections.
  • @if(condition) { ... } for conditionals.

Step 2: Create a Twirl Template

  1. Navigate to the app/views directory.
  2. Create a new file named hello.scala.html.
  3. Add the following content to create a simple template that takes a String parameter and displays a greeting:
@(name: String)

<!DOCTYPE html>
<html>
<head>
    <title>Hello, @name!</title>
</head>
<body>
    <h1>Hello, @name!</h1>
</body>
</html>

This template expects a name parameter and uses it to display a greeting.

Step 3: Use the Template in a Controller

Now, let's use this template in a controller to render a dynamic web page:

  1. Open or create a controller in the app/controllers directory, for example, HomeController.scala.
  2. Add a new action method that renders the hello template with a name:
package controllers

import play.api.mvc._

class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {

  def hello(name: String) = Action { implicit request: Request[AnyContent] =>
    Ok(views.html.hello(name))
  }
}

This controller action calls the hello template, passing a name parameter to it, and returns the rendered HTML as an Ok HTTP response.

Step 4: Define a Route

To make the new action accessible via HTTP, you need to define a route:

  1. Open the conf/routes file.
  2. Add a new route definition for the hello action:
GET     /hello/:name      controllers.HomeController.hello(name)

This route maps GET requests to /hello/[name] to the hello action in the HomeController, passing the [name] part of the URL as a parameter.

Step 5: Test Your Application

  1. Start your Play application by running sbt run from the terminal in your project's root directory.
  2. Open a web browser and navigate to http://localhost:9000/hello/YourName, replacing YourName with any name you wish.
  3. You should see the greeting displayed on the page, dynamically generated by the Twirl template.

Twirl Flow

for

Twirl templates are located in the app/views folder and typically have the .scala.html extension. Let's create a simple template that demonstrates the use of variables, loops, and conditionals.

  1. Create a new file called list.scala.html in the app/views directory.
  2. Add the following content to list.scala.html:
@(items: List[String], title: String)

<!DOCTYPE html>
<html>
<head>
    <title>@title</title>
</head>
<body>
    <h1>@title</h1>
    @if(items.isEmpty) {
        <p>No items to display.</p>
    } else {
        <ul>
        @for(item <- items) {
            <li>@item</li>
        }
        </ul>
    }
</body>
</html>

This template accepts a list of strings (items) and a title (title), displaying each item in a list. It also demonstrates how to use conditionals to display a message if the list is empty.

Creating a Controller

Now, let's create a controller that uses our template to render a page.

  1. Create a new controller file named ListController.scala in the app/controllers directory.
  2. Add the following Scala code to the controller:
package controllers

import play.api.mvc._

class ListController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
  def showList = Action {
    val fruits = List("Apple", "Banana", "Cherry")
    Ok(views.html.list(fruits, "Fruit List"))
  }
}

This controller defines an action called showList that creates a list of fruits and passes it to the list template, along with a title.

Defining Routes

To make the showList action accessible via a web browser, you need to define a route in the conf/routes file.

  1. Open the conf/routes file.
  2. Add the following line to define a route for your action:
GET     /list     controllers.ListController.showList

This route maps GET requests to /list to the showList action in the ListController.

if

Imagine you have a web application where you need to display a welcome message to logged-in users and a different message to guests. You can use an if statement in a Twirl template to achieve this.

First, create a new Twirl template in the app/views directory of your Play application. Let's name it welcome.scala.html. In this template, you'll check whether the user is logged in by evaluating a boolean parameter isLoggedIn.

@(isLoggedIn: Boolean)

<!DOCTYPE html>
<html>
<head>
    <title>Welcome Page</title>
</head>
<body>
    @if(isLoggedIn) {
        <h1>Welcome back, user!</h1>
    } else {
        <h1>Welcome, guest!</h1>
        <p>Please log in to enjoy our services.</p>
    }
</body>
</html>

In this example, the @if(isLoggedIn) statement checks if isLoggedIn is true. If it is, the template renders a welcome back message for the user. If isLoggedIn is false, it displays a welcome message for guests and prompts them to log in.

if-else

You can also use if-else statements to handle multiple conditions. For example, you might want to display different messages based on the user's role:

@(userRole: String)

<!DOCTYPE html>
<html>
<head>
    <title>Role-Based Message</title>
</head>
<body>
    @if(userRole == "admin") {
        <h1>Welcome, Admin!</h1>
    } else if(userRole == "member") {
        <h1>Welcome, Member!</h1>
    } else {
        <h1>Welcome, Guest!</h1>
    }
</body>
</html>

In this template, userRole is checked to determine the user's role. Based on the role, a corresponding message is displayed. This example shows how you can use else if to add more conditions to your logic.

Integrating with Play Controllers

To render the template with dynamic data, you would pass the necessary information (e.g., isLoggedIn or userRole) from your Play controller when returning the template as a response. Here's an example controller method that renders the first template:

def welcomePage = Action {
  val userIsLoggedIn = checkIfUserIsLoggedIn() // Assume this is a method that checks the user's login status
  Ok(views.html.welcome(userIsLoggedIn))
}

Login Form

Step 1: Create the Login Form Model

Define a simple case class to represent the login form data. You can place this in a Scala file in the models package, for instance, LoginForm.scala:

package models

case class LoginForm(username: String, password: String)

Step 2: Create the Login Controller

Create a controller that will render the login form and handle form submission. If the username and password match the hardcoded credentials, it will redirect the user to a secure page; otherwise, it will reload the login page with an error.

In the controllers directory, create a file named LoginController.scala:

package controllers

import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import models.LoginForm
import javax.inject._

@Singleton
class LoginController @Inject()(cc: MessagesControllerComponents) extends MessagesAbstractController(cc) {

  val loginForm: Form[LoginForm] = Form(
    mapping(
      "username" -> text,
      "password" -> text
    )(LoginForm.apply)(LoginForm.unapply)
  )

  def login = Action { implicit request: MessagesRequest[AnyContent] =>
    Ok(views.html.login(loginForm))
  }

  def authenticate = Action { implicit request: MessagesRequest[AnyContent] =>
    loginForm.bindFromRequest.fold(
      formWithErrors => BadRequest(views.html.login(formWithErrors)),
      formData => {
        if (formData.username == "user" && formData.password == "password") {
          Redirect(routes.HomeController.index()).flashing("success" -> "You are logged in.")
        } else {
          Redirect(routes.LoginController.login()).flashing("error" -> "Invalid username or password.")
        }
      }
    )
  }
}

Step 3: Define Routes

In the conf/routes file, add routes for your login form and authentication process:

GET     /login                   controllers.LoginController.login
POST    /authenticate            controllers.LoginController.authenticate
GET     /                        controllers.HomeController.index

Ensure you have a route and controller action for /, which will serve as the landing page after a successful login.

Step 4: Create the Login Form View

Create a new Scala HTML file named login.scala.html in the app/views directory. This view will render the login form:

@import helper._
@(form: Form[LoginForm])(implicit request: MessagesRequestHeader)

@main("Login") {
    @form(routes.LoginController.authenticate()) {
        @CSRF.formField

        @inputText(form("username"), '_label -> "Username")
        @inputPassword(form("password"), '_label -> "Password")

        <button type="submit">Login</button>
    }

    @request.flash.get("error").map { error =>
        <div class="error">@error</div>
    }
}

Make sure you have a main template file (main.scala.html) that this login template references to structure the page.

Step 5: Running Your Application

Execute your application:

sbt run

Navigate to http://localhost:9000/login in your browser to see the login form. Try logging in with the username user and the password password.

Todo List

Creating a simple Todo List application with the Play Framework in Scala involves several steps, including setting up your Play project, defining the model, creating controllers and views, and setting up routes for handling HTTP requests. Here's a step-by-step guide to get you started:

1. Setting Up Your Play Project

First, create a new Play Framework project using sbt (Scala's build tool). Open a terminal and run:

sbt new playframework/play-scala-seed.g8

Follow the prompts to name your project. This creates a basic structure for your Play application.

2. Defining the Todo Model

Define a case class to represent a Todo item. You can create a Scala file Todo.scala in the models directory:

package models

case class Todo(id: Long, task: String, isDone: Boolean)

3. Creating the Application Controller

Create a controller to handle the logic for your Todo List application. You might name it TodoController.scala and place it under the controllers directory. Here's how you might start:

package controllers

import models.Todo
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import javax.inject._

@Singleton
class TodoController @Inject()(cc: MessagesControllerComponents) extends MessagesAbstractController(cc) {

  private val todos = scala.collection.mutable.ArrayBuffer(
    Todo(1, "Learn Play Framework", isDone = false),
    Todo(2, "Build a Todo List App", isDone = false)
  )

  private val todoForm: Form[Todo] = Form(
    mapping(
      "id" -> ignored(0L),
      "task" -> nonEmptyText,
      "isDone" -> boolean
    )(Todo.apply)(Todo.unapply)
  )

  def listTodos = Action { implicit request: MessagesRequest[AnyContent] =>
    Ok(views.html.listTodos(todos.toList, todoForm))
  }

  def addTodo = Action { implicit request: MessagesRequest[AnyContent] =>
    todoForm.bindFromRequest.fold(
      errorForm => BadRequest(views.html.listTodos(todos.toList, errorForm)),
      todo => {
        val newId = if (todos.isEmpty) 1L else todos.map(_.id).max + 1
        todos += todo.copy(id = newId)
        Redirect(routes.TodoController.listTodos)
      }
    )
  }
}

4. Defining Routes

In the conf/routes file, add routes for displaying the Todo List and adding a new Todo item:

GET     /todos              controllers.TodoController.listTodos
POST    /todos/add          controllers.TodoController.addTodo

5. Creating the Views

You need to create Scala HTML templates for displaying and adding Todo items. You can place these in the views directory. For instance, create listTodos.scala.html:

@(todos: List[Todo], todoForm: Form[Todo])(implicit request: MessagesRequestHeader)
@import helper._

@main("Todo List") {
    <h1>Todo List</h1>
    
    <ul>
    @for(todo <- todos) {
        <li>
            @if(todo.isDone) {
                <s>@todo.task</s>
            } else {
                @todo.task
            }
        </li>
    }
    </ul>
    
    <h2>Add a new task</h2>
    @form(routes.TodoController.addTodo) {
        @CSRF.formField
        
        @inputText(todoForm("task"))
        
        <button type="submit">Add</button>
    }
}

6. Running Your Application

Use sbt to run your Play application:

sbt run

After your application starts, you can access it by navigating to http://localhost:9000/todos in your web browser.

Notes:

  • This example uses an in-memory collection to store Todo items, which means your data won't persist across application restarts. For a real application, you would typically use a database.
  • Security measures like input validation and CSRF protection are included, but always consider additional security practices and validations based on your application's needs.
  • The UI is very basic. You can enhance it using CSS frameworks like Bootstrap for a better look and feel.

Anorm, Rest-API

Integrating Anorm with an MySQL database in a Play Framework project involves configuring your database connection, setting up the database evolutions, and adjusting your application code to interact with the database. Anorm is a simple data access layer that uses plain SQL to interact with the database, making it a flexible choice for Scala developers. Here’s how you can set it up:

Step 1: Add Dependencies

First, ensure you have the necessary dependencies in your build.sbt file. You'll need the Play Slick dependency and the H2 database driver for local development. If not already included, add them like so:

libraryDependencies ++= Seq(
  guice,
  jdbc,
  "mysql" % "mysql-connector-java" % "8.0.33",
  "org.playframework.anorm" %% "anorm" % "2.7.0",
  "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.1" % Test,
)

Replace version numbers with the latest versions suitable for your project setup.

Step 2: Configure the Database

In the conf/application.conf file, configure your MySQL database connection:

db.default.driver=com.mysql.cj.jdbc.Driver
db.default.url="jdbc:mysql://localhost:3306/todo"
db.default.user="root"
db.default.password= "mysql"

Setup the database connection pool: hikaricp

# Number of database connections
# See https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
fixedConnectionPool = 9

play.db {
  prototype {
    hikaricp.minimumIdle = ${fixedConnectionPool}
    hikaricp.maximumPoolSize = ${fixedConnectionPool}
  }
}

# Job queue sized to HikariCP connection pool
database.dispatcher {
  executor = "thread-pool-executor"
  throughput = 1
  thread-pool-executor {
    fixed-pool-size = ${fixedConnectionPool}
  }
}

Create the DatabaseExecutionContext

package models

import org.apache.pekko.actor.ActorSystem
import play.api.libs.concurrent.CustomExecutionContext
import javax.inject.*
/**
 * This class is a pointer to an execution context configured to point to "database.dispatcher"
 * in the "application.conf" file.
 */
@Singleton
class DatabaseExecutionContext @Inject()(system: ActorSystem) extends CustomExecutionContext(system, "database.dispatcher")

Step 3: Create database and table

It uses a todo database, which we'll have to create ourselves inside MySQL And a todo table has is created

create database todo;
use todo;       

create table todo ( 
    id bigint auto_increment primary key, 
    task varchar(255)  
);

Step 4: Interacting with the Database Using Anorm

Here we create a class Todo and the database service TodoRepository

package models

import javax.inject.Inject
import scala.util.{ Failure, Success }
import anorm._
import anorm.SqlParser.{ get, str }
import play.api.db.DBApi
import scala.concurrent.Future
case class Todo(id: Option[Long] = None, task: String)

object Todo {
  implicit def toParameters: ToParameterList[Todo] =
    Macro.toParameters[Todo]

  def unapply(c: Todo): Option[(Option[Long], String)]= Some((c.id, c.task))
}
@javax.inject.Singleton
class TodoRepository @Inject()(dbapi: DBApi)(implicit ec: DatabaseExecutionContext) {
  private val db = dbapi.database("default")
  
  private[models] val simple = {
    get[Option[Long]]("todo.id") ~ str("todo.task") map {
      case id ~ name => Todo(id, name)
    }
  }
  
  def list(): Future[List[Todo]] = Future {
    db.withConnection { implicit connection =>
      SQL"SELECT * FROM todo".as(simple.*)
    }
  }
  
  def create(todo: Todo): Future[Option[Long]] = Future {
    db.withConnection { implicit connection =>
      SQL("""insert into todo values ( {id}, {task})""")
        .bind(todo).executeInsert()
    }
  }(ec)
}

Step 5: HomeController Implementation

In your Play application under the controllers directory, create a file named HomeController.scala:

package controllers
import models.{Todo, TodoRepository}
import javax.inject.*
import play.api.*
import play.api.libs.json.{Json, OFormat}
import play.api.mvc.*
import scala.concurrent.ExecutionContext

@Singleton
class HomeController @Inject()(todoService: TodoRepository,
                               cc: MessagesControllerComponents)(implicit ec: ExecutionContext)
  extends MessagesAbstractController(cc) {
  
  implicit val toDoFormat: OFormat[Todo] = Json.format[Todo]

  def list: Action[AnyContent] = Action.async { implicit request =>
    todoService.list().map { todos =>
      Ok(Json.toJson(todos))
    }
  }

  def create: Action[AnyContent] = Action.async { implicit request =>
    val json = request.body.asJson.get
    val todoItem = json.as[Todo]
    todoService.create(todoItem).map { id =>
      Ok(Json.obj("id" -> id))
    }
  }
}

routes

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
GET     /todos                      controllers.HomeController.list()
POST     /todos                     controllers.HomeController.create()

Step 6: Running Your Application

Start your Play application using sbt:

sbt run

Step 7: Test with cURL

Test the todo app with cURL

POST

curl -X POST http://localhost:9000/todos \
-H "Content-Type: application/json" \
-d '{"task":"programming"}'

GET

curl -H "Accept: application/json" http://localhost:9000/todos

Result:

[
  {
    "task":"programming",
    "id":1
  }
]

With Akka

Integrating Akka actors in a Play Framework application allows you to take advantage of the actor model for handling concurrency, scalability, and fault tolerance. Actors are ideal for scenarios where you need to manage state in an asynchronous, non-blocking manner or when you need to perform long-running or compute-intensive tasks in response to HTTP requests. Here's a basic guide on how to use Akka actors within a Play Framework application:

Step 1: Define an Actor

First, define your actor class. Actors handle messages and can maintain state. Create a Scala class file in the app/actors directory of your Play project (you might need to create the directory first). Here's an example of a simple actor that echoes messages back:

package actors

import akka.actor.Actor

class EchoActor extends Actor {
  def receive = {
    case message: String => sender() ! message // Echoes the message back to the sender
  }
}

Step 2: Configure Actor System

Play Framework automatically provides an ActorSystem for you. You can access this system and use it to create actor instances. The ActorSystem is a heavyweight structure that controls the lifecycle of your actors and is capable of managing hundreds or thousands of actors.

Step 3: Create and Use an Actor in a Controller

Next, create a controller that sends a message to your actor and waits for a response. This example demonstrates how to do this asynchronously using ask pattern from Akka. You'll need to modify your controller to inject the ActorSystem and use it to create actor instances.

Add the following imports to your controller file:

import akka.actor.ActorSystem
import akka.util.Timeout
import actors.EchoActor
import akka.pattern.ask
import scala.concurrent.duration._
import scala.concurrent.Future
import play.api.mvc._
import javax.inject._

Then, define your controller and action method:

@Singleton
class EchoController @Inject()(val controllerComponents: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController {

  implicit val timeout: Timeout = 5.seconds

  def echo(message: String) = Action.async {
    val echoActor = actorSystem.actorOf(Props[EchoActor], "echo-actor")
    val futureResponse: Future[String] = (echoActor ? message).mapTo[String]
    futureResponse.map { response =>
      Ok(response)
    }
  }
}

In this controller:

  • An EchoActor instance is created using the ActorSystem.
  • The echo action method sends a message to the EchoActor using the ask pattern and waits for a response.
  • The response from the actor is sent back to the client as an HTTP response.

Step 4: Add Routes

Define a route for your action in the conf/routes file:

GET     /echo/:message        controllers.EchoController.echo(message)

Step 5: Test Your Actor

Run your Play application, and access the route /echo/Hello in your browser or via a tool like curl. You should receive the echoed message in response.

Note

Using actors for simple echo tasks might be overkill. The real power of actors comes into play when managing complex states, performing long-running tasks, or handling a high number of concurrent operations. Remember, actors are just one of many tools offered by Play and Akka for building reactive applications, and they're best used when their specific capabilities align with your application's requirements.

Actor Intro

The Actor Model provides a high-level abstraction for writing concurrent and distributed systems, addressing some of the challenges associated with traditional concurrent programming techniques. Below is an overview tutorial covering the need for actors, basic concepts, communication, and how these are implemented in Akka, a popular toolkit for Scala and Java.

The Need for Actors

Concurrent programming in traditional models (using threads and locks) is hard. It's prone to errors such as deadlocks, race conditions, and complexity in managing state. The Actor Model simplifies concurrency by encapsulating state, behavior, and communication in actors, reducing the cognitive load on developers and minimizing common concurrency issues.

The Actor Model

The Actor Model treats actors as the fundamental unit of computation. In this model, an actor can:

  • Process messages asynchronously.
  • Maintain private state.
  • Create more actors.
  • Send messages to other actors.

This model helps in building systems that are naturally concurrent and distributed, promoting message-passing over shared state for interactions.

Actor Concepts

Actors are objects that encapsulate state and behavior. They interact using asynchronous message passing, ensuring that each actor operates concurrently without interfering with the internal state of others.

State: Actors maintain their own state privately, making state changes in response to received messages.

Behavior: The logic that defines how an actor responds to messages.

Mailbox: Each actor has a mailbox where incoming messages are queued before being processed.

Supervision: Actors are arranged in a hierarchy. Parent actors supervise their child actors, deciding on the course of action (e.g., restart, stop) when a child encounters a failure.

Actor Communications

Actors communicate exclusively through asynchronous message passing. This mechanism ensures that actors remain loosely coupled, scalable, and isolated, making the system more resilient. Messages should be immutable to avoid shared state and ensure thread safety.

Akka Actors

Akka is a toolkit and runtime for building highly concurrent, distributed, and fault-tolerant applications on the JVM. It implements the Actor Model, providing a comprehensive set of features for managing actors, communication, and fault tolerance.

Key Features:

  • Location transparency: Actors can communicate regardless of their location in the cluster.
  • Lightweight: Millions of actors can be spawned on a single machine.
  • Fault tolerance: Supervision strategies help manage actor failures.

Akka Actor Application Structure and Naming

An Akka application is structured around actors and their interactions. Actors are organized hierarchically, forming a tree structure where each actor can have child actors.

Naming: Actors in Akka have a path representing their location in the hierarchy, akin to file paths in a filesystem. This path includes the actor system name and the names of parent actors, providing a way to uniquely identify and locate actors within the system.

Actor

Akka Actor Tutorial: Untyped Actors

Akka is a powerful toolkit and runtime for building concurrent, distributed, and fault-tolerant applications on the JVM. In this tutorial, we'll explore Akka's untyped actors, which were the default actors in Akka before the introduction of typed actors in Akka 2.6.

1. Setting Up the Project

First, create a new Scala project and add the Akka dependencies to your build.sbt:

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.x"

Replace x with the latest minor version of Akka.

2. Basic Concepts

  • Actor: A unit of computation that processes messages asynchronously.
  • Actor System: A hierarchical group of actors that share common configuration.
  • Message: Immutable data passed between actors.
  • Mailbox: Queue where incoming messages are stored until processed by the actor.

3. Creating an Actor System

Start by creating an ActorSystem, which will manage and supervise actors:

import akka.actor.{ActorSystem, Props, Actor, ActorRef}

// Create an Actor System
val system = ActorSystem("my-actor-system")

4. Creating a Simple Actor

To define an untyped actor, extend the Actor trait and implement the receive method, which handles incoming messages:

class SimpleActor extends Actor {
  def receive: Receive = {
    case "hello" => println("Hello, World!")
    case "goodbye" => println("Goodbye, World!")
    case msg: String => println(s"Received message: $msg")
  }
}

5. Instantiating and Sending Messages to an Actor

Actors are instantiated using the Props class, and messages are sent using the ! (tell) operator:

val simpleActor: ActorRef = system.actorOf(Props[SimpleActor], "simple-actor")

// Send messages to the actor
simpleActor ! "hello"
simpleActor ! "How are you?"
simpleActor ! "goodbye"

6. Actor Lifecycle

Actors have a lifecycle managed by the Akka framework:

  • Pre-start: Executed when the actor is created.
  • Post-stop: Executed when the actor is stopped.
  • Restart: Executed when the actor is restarted after a failure.

You can override lifecycle hooks in your actor:

class LifecycleActor extends Actor {
  override def preStart(): Unit = println("Actor is starting")
  
  override def postStop(): Unit = println("Actor has stopped")

  def receive: Receive = {
    case "stop" => context.stop(self)
    case msg => println(s"Received: $msg")
  }
}

val lifecycleActor: ActorRef = system.actorOf(Props[LifecycleActor], "lifecycle-actor")
lifecycleActor ! "test"
lifecycleActor ! "stop"

7. Actor Hierarchy and Supervision

Actors are organized hierarchically, with parent actors supervising child actors. Supervision strategies define how to handle child actor failures (e.g., restart, stop, resume).

Example of creating child actors:

class ParentActor extends Actor {
  override def preStart(): Unit = {
    val child = context.actorOf(Props[SimpleActor], "child-actor")
    child ! "hello"
  }

  def receive: Receive = {
    case _ =>
  }
}

val parentActor: ActorRef = system.actorOf(Props[ParentActor], "parent-actor")

8. Stopping Actors

Actors can be stopped using context.stop(actorRef) or PoisonPill:

simpleActor ! PoisonPill

Stopping an actor triggers its postStop lifecycle hook.

9. Handling Actor Failure

You can define a supervisor strategy to handle child actor failures:

import akka.actor.SupervisorStrategy._
import akka.actor.OneForOneStrategy

class SupervisorActor extends Actor {
  override val supervisorStrategy = OneForOneStrategy() {
    case _: Exception => Restart
  }

  def receive: Receive = {
    case _ =>
  }
}

10. Graceful Shutdown

To gracefully shut down an ActorSystem, use:

system.terminate()

11. Example: Simple Chat System

Let's create a simple chat system with actors representing users and a chat room.

User Actor

class UserActor(name: String) extends Actor {
  def receive: Receive = {
    case msg: String => println(s"$name received: $msg")
  }
}

Chat Room Actor

class ChatRoomActor extends Actor {
  var users: Set[ActorRef] = Set()

  def receive: Receive = {
    case Join(user) => users += user
    case Leave(user) => users -= user
    case Broadcast(msg) => users.foreach(_ ! msg)
  }
}

// Messages
case class Join(user: ActorRef)
case class Leave(user: ActorRef)
case class Broadcast(message: String)

Main

val chatRoom: ActorRef = system.actorOf(Props[ChatRoomActor], "chat-room")

val user1: ActorRef = system.actorOf(Props(new UserActor("Alice")), "user1")
val user2: ActorRef = system.actorOf(Props(new UserActor("Bob")), "user2")

chatRoom ! Join(user1)
chatRoom ! Join(user2)
chatRoom ! Broadcast("Hello, everyone!")
chatRoom ! Leave(user1)
chatRoom ! Broadcast("Goodbye, Alice!")

system.terminate()

Actor

Actors in Scala are a part of the Akka toolkit, a powerful concurrency and distributed computing library. Actors make it easier to write safe and efficient concurrent and parallel systems. The Actor Model provides a higher level of abstraction for writing concurrent and distributed systems, enabling you to think about your application in terms of actors sending messages to each other, rather than worrying about threads and locks.

Here's a basic tutorial on how to use actors in Scala with Akka.

Setting Up

First, you need to include Akka dependency in your project. If you're using sbt, add the following to your build.sbt file:

libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.8.5"

Basic Concepts

  • Actor System: The root level where actors live. It's a heavyweight structure that will allocate 1...N threads for your actors.
  • Actor: Encapsulates state and behavior. Actors communicate by exchanging messages.
  • Message: Immutable data that actors send to each other.

Creating Actors

Actors in Akka are created by defining actor behavior and spawning actors from an actor system.

Defining Actor Behavior

Actor behavior is defined by implementing the Behavior interface. Let's start with a simple example: an actor that says "Hello" when it receives a message.

First, define the messages your actor will handle. It's a good practice to define messages as case classes inside a companion object of your actor:

object Greeter {
  final case class SayHello(name: String)
}

Next, define the actor behavior:

import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import Greeter.SayHello

object Greeter {
  def apply(): Behavior[SayHello] = Behaviors.receive { (context, message) =>
    println(s"Hello, ${message.name}!")
    Behaviors.same
  }
}

Spawning Actors

To use the actor, you must create an actor system and spawn an instance of your actor:

import akka.actor.typed.ActorSystem

object HelloAkka {
  def main(args: Array[String]): Unit = {
    val greeter: ActorSystem[SayHello] = ActorSystem(Greeter(), "greeter")
    
    greeter ! SayHello("Akka")
  }
}

This program will print "Hello, Akka!" to the console.

Handling Different Messages

Actors can handle different types of messages. Here's an example of how to extend our Greeter to handle different kinds of greetings:

object Greeter {
  sealed trait Command
  final case class SayHello(name: String) extends Command
  final case class SayGoodbye(name: String) extends Command

object Greeter {
  def apply(): Behavior[Command] = Behaviors.receive { (context, message) =>
    message match {
      case SayHello(name) =>
        println(s"Hello, $name!")
        Behaviors.same
      case SayGoodbye(name) =>
        println(s"Goodbye, $name!")
        Behaviors.stopped // Stop this actor
    }
  }
}

More messages

Handling different messages is a fundamental aspect of working with actors in systems like Akka. Actors need to respond to various message types, each potentially requiring different handling logic. This capability allows actors to participate in complex workflows and maintain internal state accordingly.

Scala Example with Akka Typed

Akka Typed encourages more explicit and safer message handling by defining a protocol of messages an actor can handle. Let's look at a Scala example where an actor can handle different message types:

First, define a sealed trait (or an abstract class) that acts as a protocol for all messages the actor can receive. Then, implement case classes or case objects for each specific message:

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.Behavior

sealed trait Message
final case class Greet(name: String) extends Message
final case class Configure(setting: String) extends Message
case object Shutdown extends Message

Next, define the actor behavior that matches against these messages:

object MyActor {
  def apply(): Behavior[Message] = Behaviors.receive { (context, message) =>
    message match {
      case Greet(name) =>
        context.log.info(s"Hello, $name!")
        Behaviors.same
      case Configure(setting) =>
        context.log.info(s"Configuring with setting: $setting")
        Behaviors.same
      case Shutdown =>
        context.log.info("Shutting down.")
        Behaviors.stopped
    }
  }
}

Finally, create the actor system and send some messages to the actor:

object ActorSystemExample extends App {
  val system: ActorSystem[Message] = ActorSystem(MyActor(), "myActorSystem")

  system ! Greet("Akka")
  system ! Configure("verbose")
  system ! Shutdown
}

Supervisor

Handling errors and supervising child actors are core functionalities of the Akka Actor model. The supervision strategy allows a parent actor to decide how to handle errors thrown by its children, making the system more resilient. Let's dive into how you can implement a parent actor and manage error handling in an Akka system.

Understanding Supervision Strategies

In Akka, when an actor throws an exception, it's suspended and the supervision strategy of its parent actor is invoked. The parent can decide to:

  • Resume the child actor, keeping its internal state.
  • Restart the child actor, resetting its internal state.
  • Stop the child actor permanently.
  • Escalate the failure, causing the parent itself to stop and be supervised by its own parent.

Step 1: Setting Up Your Project

First, ensure your build.sbt file includes dependencies for Akka Typed:

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor-typed" % "2.6.15"
)

Step 2: Defining the Child Actor

Create a simple child actor that can throw an exception based on a received message. This example defines a Worker actor that supports DoWork and Fail messages:

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.Behavior

object Worker {
  sealed trait Command
  case object DoWork extends Command
  case object Fail extends Command

  def apply(): Behavior[Command] = Behaviors.receiveMessage {
    case DoWork =>
      println("Worker is doing work")
      Behaviors.same
    case Fail =>
      throw new RuntimeException("Worker failed")
  }
}

Step 3: Creating the Parent Actor with Supervision Strategy

Define a parent actor that creates the Worker actor and specifies a supervision strategy for it. This example uses a supervision strategy to restart the child actor upon failure:

import akka.actor.typed.ActorSystem
import akka.actor.typed.SupervisorStrategy
import akka.actor.typed.scaladsl.Behaviors

object Supervisor {
  def apply(): Behavior[Worker.Command] = Behaviors
    .supervise[Worker.Command] {
      Behaviors.setup { context =>
        val worker = context.spawn(Worker(), "workerActor")

        // Example: sending messages to the worker
        worker ! Worker.DoWork
        worker ! Worker.Fail // This will cause the worker to throw an exception

        Behaviors.receiveMessage { message =>
          worker ! message
          Behaviors.same
        }
      }
    }
    .onFailure[Exception](SupervisorStrategy.restart) // Supervision strategy to restart the child
}

Step 4: Running the System

Create and run an ActorSystem with the Supervisor actor:

object Main extends App {
  val system: ActorSystem[Worker.Command] = ActorSystem(Supervisor(), "supervisorSystem")

  // Optionally, interact with the system
  // e.g., system ! Worker.DoWork
}

Observing Behavior

When the Worker actor throws an exception due to the Fail message:

  • The parent's supervision strategy is invoked.
  • The strategy decides to restart the Worker actor.
  • The Worker actor's state is reset, and it's ready to receive messages again.

Conclusion

This tutorial introduced the basics of actor supervision in Akka. Supervision strategies empower you to build resilient systems by effectively managing failures. Experimenting with different strategies and understanding their impact on actor states is crucial for leveraging Akka's full potential for building fault-tolerant systems.

More Actors

To illustrate using two actors in an Akka system, let's create a simple Scala application where one actor sends a message to another actor. This example demonstrates basic actor interaction, message passing, and how actors can work together to achieve a task.

We'll create a scenario with two actors: Sender and Receiver. The Sender actor will send a greeting message to the Receiver actor, which will then log the greeting.

Step 1: Define Messages

First, define the messages that actors will use for communication. For simplicity, we'll have a single message type that includes a string payload.

sealed trait Message
final case class Greet(message: String) extends Message

Step 2: Define the Receiver Actor

The Receiver actor will handle the Greet message by printing it to the console.

import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.Behaviors

object Receiver {
  def apply(): Behavior[Message] = Behaviors.receive { (context, message) =>
    message match {
      case Greet(greeting) =>
        context.log.info(s"Received message: $greeting")
        Behaviors.same
    }
  }
}

Step 3: Define the Sender Actor

The Sender actor will send a Greet message to the Receiver. It needs a reference to the Receiver actor, which we'll pass as a constructor parameter.

object Sender {
  def apply(receiver: ActorRef[Message]): Behavior[Message] = Behaviors.setup { context =>
    // Sending the greeting message to the receiver
    receiver ! Greet("Hello, Akka Typed Actors!")
    Behaviors.same
  }
}

Step 4: Set Up the Actor System

Now, set up the ActorSystem, create both actors, and start the interaction.

import akka.actor.typed.ActorSystem

object AkkaQuickStart extends App {
  // Create the Actor System
  val actorSystem: ActorSystem[Message] = ActorSystem(Receiver(), "receiverSystem")

  // Create the Receiver Actor
  val receiver: ActorRef[Message] = actorSystem.systemActorOf(Receiver(), "receiver")

  // Create the Sender Actor and pass the Receiver's ActorRef
  actorSystem.systemActorOf(Sender(receiver), "sender")
}

In this example, when the AkkaQuickStart application runs, it sets up an ActorSystem and creates both Sender and Receiver actors. The Sender immediately sends a Greet message to the Receiver upon initialization, demonstrating basic actor communication. The Receiver actor logs the message to the console as its way of "handling" the message.

Todo App

Creating a simple Todo app using Akka's untyped actors can be an excellent way to demonstrate how to manage state, handle concurrency, and build a basic application using Akka's actor model. Below, I will walk you through the process of building a Todo app with the following functionalities:

  1. Add a new Todo item.
  2. Remove a Todo item.
  3. Mark a Todo item as completed.
  4. List all Todo items.

1. Setting Up the Project

Start by setting up your Scala project with the necessary Akka dependencies in your build.sbt:

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.x"

2. Defining the Messages

First, define the messages that actors will use to communicate. Messages are immutable case classes:

// Messages
case class AddTodo(id: Int, task: String)
case class RemoveTodo(id: Int)
case class CompleteTodo(id: Int)
case object GetTodos

3. Creating the Todo Actor

The TodoActor will manage the list of Todo items. It will handle messages to add, remove, complete, and retrieve Todo items:

import akka.actor.{Actor, ActorSystem, Props}

// Todo Item case class
case class TodoItem(id: Int, task: String, completed: Boolean = false)

class TodoActor extends Actor {
  var todos: Map[Int, TodoItem] = Map()

  def receive: Receive = {
    case AddTodo(id, task) =>
      todos += id -> TodoItem(id, task)
      println(s"Added Todo: $task")

    case RemoveTodo(id) =>
      todos.get(id) match {
        case Some(todo) =>
          todos -= id
          println(s"Removed Todo: ${todo.task}")
        case None =>
          println(s"Todo with id $id not found.")
      }

    case CompleteTodo(id) =>
      todos.get(id) match {
        case Some(todo) =>
          todos += id -> todo.copy(completed = true)
          println(s"Completed Todo: ${todo.task}")
        case None =>
          println(s"Todo with id $id not found.")
      }

    case GetTodos =>
      if (todos.isEmpty) {
        println("No Todos available.")
      } else {
        todos.values.foreach { todo =>
          val status = if (todo.completed) "Completed" else "Pending"
          println(s"${todo.id}: ${todo.task} [$status]")
        }
      }
  }
}

4. Creating the Main Application

Now, let's create a main application that will instantiate the ActorSystem, create the TodoActor, and interact with it:

object TodoApp extends App {
  // Create Actor System
  val system = ActorSystem("TodoSystem")

  // Create TodoActor
  val todoActor = system.actorOf(Props[TodoActor], "todoActor")

  // Interacting with the TodoActor
  todoActor ! AddTodo(1, "Buy milk")
  todoActor ! AddTodo(2, "Go to the gym")
  todoActor ! GetTodos

  todoActor ! CompleteTodo(1)
  todoActor ! GetTodos

  todoActor ! RemoveTodo(2)
  todoActor ! GetTodos

  // Shutdown the Actor System
  system.terminate()
}

5. Running the Application

To run the application, simply execute the TodoApp object in your Scala environment. The output will be:

Added Todo: Buy milk
Added Todo: Go to the gym
1: Buy milk [Pending]
2: Go to the gym [Pending]
Completed Todo: Buy milk
1: Buy milk [Completed]
2: Go to the gym [Pending]
Removed Todo: Go to the gym
1: Buy milk [Completed]
No Todos available.

Todo App

Creating a simple Todo application using Akka actors in Scala involves several steps, including defining the messages that actors will use for communication, creating the actor behaviors, and setting up the actor system. This application will consist of a main actor, TodoManager, which manages todo items, and a simplified interface for adding and listing todos.

Step 1: Define the Messages

First, define the messages that will be used for interaction. In a Todo app, we typically need to add todos and list all existing todos.

sealed trait Command
final case class AddTodo(description: String) extends Command
final case object ListTodos extends Command
final case class Todos(items: List[String])

Step 2: Define the TodoManager Actor

The TodoManager actor will handle AddTodo and ListTodos messages. It maintains a list of todo items in its state.

import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors

object TodoManager {
  def apply(): Behavior[Command] = manageTodos(Nil)

  private def manageTodos(todos: List[String]): Behavior[Command] =
    Behaviors.receive { (context, message) =>
      message match {
        case AddTodo(description) =>
          context.log.info(s"Adding todo: $description")
          manageTodos(todos :+ description)
        case ListTodos =>
          context.log.info(s"Current todos: $todos")
          Behaviors.same
      }
    }
}

Step 3: Set Up the Actor System and Interaction

Now, create the ActorSystem, and demonstrate adding some todos and listing them.

import akka.actor.typed.ActorSystem

object TodoApp extends App {
  val system: ActorSystem[Command] = ActorSystem(TodoManager(), "todoSystem")

  system ! AddTodo("Learn Akka Actors")
  system ! AddTodo("Build a TodoApp")
  system ! ListTodos

  // Shutdown the actor system after a delay to see the output
  import scala.concurrent.duration._
  import system.executionContext
  system.scheduler.scheduleOnce(2.seconds) {
    system.terminate()
  }
}

Running the Application

When you run this application, it will:

  • Create an ActorSystem with the TodoManager actor.
  • Send AddTodo messages to add new todos.
  • Send a ListTodos message to print out all current todos.
  • Finally, it schedules a system termination after a short delay so you can see the output before the application exits.

Shopping Basket

To convert the Shopping Basket system from Akka typed actors to untyped actors, we need to modify the code so that it aligns with the untyped actor model in Akka. Below is the step-by-step process to achieve this.

Step 1: Define Messages and Data Models

The message and data model definitions remain mostly the same, but we'll omit the sealed trait for commands, as untyped actors don't enforce the use of sealed trait in the same way.

// Messages
case class AddItem(item: Item)
case object Checkout

// Data Models
case class Item(name: String, price: BigDecimal)
case class Receipt(items: List[Item], total: BigDecimal)

Step 2: Define the BasketActor

Now, let's define the BasketActor. In untyped actors, we extend the Actor trait and implement the receive method. The actor's state can be managed as mutable variables inside the actor.

import akka.actor.{Actor, ActorLogging, Props}

class BasketActor extends Actor with ActorLogging {
  var items: List[Item] = List.empty

  override def receive: Receive = {
    case AddItem(item) =>
      log.info(s"Item added to basket: ${item.name}")
      items = items :+ item

    case Checkout =>
      val total = items.map(_.price).sum
      log.info(s"Checking out with total: $total")
      // In a real application, you might want to send the receipt to another actor or persist it.
      context.stop(self)
  }
}

object BasketActor {
  def props(): Props = Props(new BasketActor)
}

Step 3: Set Up the Actor System and Test

Finally, let's set up the ActorSystem, create the BasketActor, and send messages to test the functionality. The main difference in untyped actors is that we use system.actorOf to create actors and ! (tell) to send messages.

import akka.actor.{ActorSystem, Props}

object ShoppingApp extends App {
  // Create Actor System
  val system = ActorSystem("shoppingSystem")

  // Create BasketActor
  val basketActor = system.actorOf(BasketActor.props(), "basketActor")

  // Interact with the BasketActor
  basketActor ! AddItem(Item("Apple", BigDecimal("0.60")))
  basketActor ! AddItem(Item("Banana", BigDecimal("0.40")))
  basketActor ! Checkout

  // Shutdown the actor system after a short delay to see the output.
  Thread.sleep(1000) // Not ideal for real applications, just for demonstration
  system.terminate()
}

Explanation:

  • Actor Creation: In untyped actors, BasketActor is created using system.actorOf(BasketActor.props(), "basketActor").
  • Message Handling: The receive method handles incoming messages by pattern matching on the message type.
  • State Management: State (i.e., the list of items) is maintained as a mutable variable inside the actor.
  • Logging: The ActorLogging trait is mixed in to provide easy access to logging within the actor.

Shopping Basket App

Creating a shopping basket system using Akka actors in Scala involves several key steps, including defining messages for adding items to the basket and checking out, as well as creating actor behaviors to handle these actions. Below is a simplified example demonstrating these concepts.

Step 1: Define Messages and Data Models

First, define the messages that will be used to interact with the actors, and any data models needed for the shopping basket.

sealed trait BasketCommand
final case class AddItem(item: Item) extends BasketCommand
final case object Checkout extends BasketCommand
final case class Item(name: String, price: BigDecimal)
final case class Receipt(items: List[Item], total: BigDecimal)

Step 2: Define the BasketActor

The BasketActor will maintain a list of items as the state and handle AddItem and Checkout messages.

import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors

object BasketActor {
  def apply(): Behavior[BasketCommand] = basket(List.empty)

  private def basket(items: List[Item]): Behavior[BasketCommand] =
    Behaviors.receive { (context, message) =>
      message match {
        case AddItem(item) =>
          context.log.info(s"Item added to basket: ${item.name}")
          basket(items :+ item)

        case Checkout =>
          val total = items.map(_.price).sum
          context.log.info(s"Checking out with total: $total")
          // In a real application, you might want to send the receipt to another actor or persist it.
          Behaviors.stopped
      }
    }
}

Step 3: Set Up the Actor System and Test

Now, you can set up an ActorSystem, create a BasketActor, and send messages to test the functionality.

import akka.actor.typed.ActorSystem

object ShoppingApp extends App {
  val system: ActorSystem[BasketCommand] = ActorSystem(BasketActor(), "shoppingSystem")

  system ! AddItem(Item("Apple", BigDecimal("0.60")))
  system ! AddItem(Item("Banana", BigDecimal("0.40")))
  system ! Checkout

  // Shutdown the actor system after a short delay to see the output.
  Thread.sleep(1000) // Not ideal for real applications, just for demonstration
  system.terminate()
}

Considerations for a Real Application

  • Persistence: For a production system, you would want to persist the shopping basket's state using Akka Persistence to handle failures and system restarts without data loss.
  • Security: Ensure that each shopping basket is associated with a specific user session or account.
  • Concurrency Handling: Be mindful of concurrent modifications if your system allows for simultaneous updates to the basket from the same user.
  • Scalability: Consider how your actors will be distributed in a clustered environment to handle scaling requirements.
  • Integration: Think about how this system will integrate with other components, like inventory management, payment processing, and user authentication.

Akka Http

Akka HTTP is a suite of libraries for building and consuming HTTP-based services in Scala and Java. It's part of the Akka project, which provides tools for building concurrent, distributed, and resilient message-driven applications. Akka HTTP is designed around the actor model and asynchronous, non-blocking I/O operations, making it ideal for high-performance applications that need to handle many concurrent connections. Here’s a simple tutorial to get you started with Akka HTTP in Scala.

Setting Up the Project

First, make sure you have sbt (Scala Build Tool) installed. Create a new sbt project and add the following dependencies to your build.sbt file to include Akka HTTP and Akka Streams:

name := "akka-http-tutorial"

version := "0.1"

scalaVersion := "2.13.6"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-http" % "10.2.4",
  "com.typesafe.akka" %% "akka-actor-typed" % "2.6.14",
  "com.typesafe.akka" %% "akka-stream" % "2.6.14"
)

Creating a Simple HTTP Server

Next, let’s create a simple HTTP server that responds with "Hello, Akka HTTP!" when accessed. Create a Scala object in src/main/scala named WebServer.scala:

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import scala.io.StdIn

object WebServer {
  def main(args: Array[String]): Unit = {

    implicit val system = ActorSystem("mySystem")
    import system.dispatcher // needed for the future flatMap/onComplete in the end

    val route =
      pathEndOrSingleSlash { // matches the root path
        complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Hello, Akka HTTP!</h1>"))
      }

    val bindingFuture = Http().newServerAt("localhost", 8080).bind(route)

    println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
    StdIn.readLine() // let it run until user presses return
    bindingFuture
      .flatMap(_.unbind()) // trigger unbinding from the port
      .onComplete(_ => system.terminate()) // and shutdown when done
  }
}

This code does the following:

  • Initializes an ActorSystem, which is a heavyweight structure that will govern the lifecycle of our application.
  • Defines a simple route that matches the root URL path and responds with an HTML greeting.
  • Starts an HTTP server on localhost port 8080, binding it to the defined route.
  • Waits for the user to press RETURN before shutting down the server.

Running Your Server

To run your server, use sbt to compile and execute your application:

sbt run

Navigate to http://localhost:8080 in your web browser, and you should see the greeting "Hello, Akka HTTP!" displayed.

Expanding Your Application

Akka HTTP is not limited to simple text responses. You can serve JSON, handle form submissions, manage websockets, and much more. For a slightly more complex example, let's modify the route to handle different paths and HTTP methods:

val route =
  concat(
    pathEndOrSingleSlash {
      complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Welcome to Akka HTTP!</h1>"))
    },
    path("hello") {
      get {
        complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h2>Hello, GET request!</h2>"))
      } ~
      post {
        complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h2>Hello, POST request!</h2>"))
      }
    }
  )

This expanded route now also matches the /hello path and distinguishes between GET and POST requests, providing different responses for each.

Project: Bookstore

To create an Akka Actor-based version of the bookstore, we need to use Akka to manage concurrency and message passing between different components of the application. Below is an example implementation.

Step 1: Add Akka Dependencies

First, add Akka dependencies to your build.sbt file:

libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.6.18"

Step 2: Define the Project Structure

Create directories and files as follows:

src/main/scala/
├── models/
│   ├── Book.scala
│   ├── Customer.scala
│   └── Order.scala
├── actors/
│   ├── BookActor.scala
│   ├── CustomerActor.scala
│   └── OrderActor.scala
└── Main.scala

Step 3: Define Models

models/Book.scala

package models

case class Book(id: Int, title: String, author: String, price: Double, stock: Int)

models/Customer.scala

package models

case class Customer(id: Int, name: String, email: String, address: String)

models/Order.scala

package models

case class Order(id: Int, customerId: Int, bookId: Int, quantity: Int, status: String)

Step 4: Implement Actors

actors/BookActor.scala

package actors

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.Book

object BookActor {
  sealed trait Command
  case class AddBook(title: String, author: String, price: Double, stock: Int, replyTo: ActorRef[Response]) extends Command
  case class ListBooks(replyTo: ActorRef[Response]) extends Command
  case class FindBookById(id: Int, replyTo: ActorRef[Response]) extends Command
  case class UpdateStock(id: Int, newStock: Int, replyTo: ActorRef[Response]) extends Command

  sealed trait Response
  case class BookAdded(book: Book) extends Response
  case class BooksListed(books: List[Book]) extends Response
  case class BookFound(book: Option[Book]) extends Response
  case class StockUpdated(book: Option[Book]) extends Response

  def apply(): Behavior[Command] = {
    var books: Map[Int, Book] = Map.empty
    var nextBookId: Int = 1

    Behaviors.receiveMessage {
      case AddBook(title, author, price, stock, replyTo) =>
        val book = Book(nextBookId, title, author, price, stock)
        books += (nextBookId -> book)
        nextBookId += 1
        replyTo ! BookAdded(book)
        Behaviors.same

      case ListBooks(replyTo) =>
        replyTo ! BooksListed(books.values.toList)
        Behaviors.same

      case FindBookById(id, replyTo) =>
        replyTo ! BookFound(books.get(id))
        Behaviors.same

      case UpdateStock(id, newStock, replyTo) =>
        val updatedBook = books.get(id).map(book => book.copy(stock = newStock))
        updatedBook.foreach(book => books += (id -> book))
        replyTo ! StockUpdated(updatedBook)
        Behaviors.same
    }
  }
}

actors/CustomerActor.scala

package actors

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.Customer

object CustomerActor {
  sealed trait Command
  case class AddCustomer(name: String, email: String, address: String, replyTo: ActorRef[Response]) extends Command
  case class ListCustomers(replyTo: ActorRef[Response]) extends Command
  case class FindCustomerById(id: Int, replyTo: ActorRef[Response]) extends Command

  sealed trait Response
  case class CustomerAdded(customer: Customer) extends Response
  case class CustomersListed(customers: List[Customer]) extends Response
  case class CustomerFound(customer: Option[Customer]) extends Response

  def apply(): Behavior[Command] = {
    var customers: Map[Int, Customer] = Map.empty
    var nextCustomerId: Int = 1

    Behaviors.receiveMessage {
      case AddCustomer(name, email, address, replyTo) =>
        val customer = Customer(nextCustomerId, name, email, address)
        customers += (nextCustomerId -> customer)
        nextCustomerId += 1
        replyTo ! CustomerAdded(customer)
        Behaviors.same

      case ListCustomers(replyTo) =>
        replyTo ! CustomersListed(customers.values.toList)
        Behaviors.same

      case FindCustomerById(id, replyTo) =>
        replyTo ! CustomerFound(customers.get(id))
        Behaviors.same
    }
  }
}

actors/OrderActor.scala

package actors

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.{Order, Book, Customer}

object OrderActor {
  sealed trait Command
  case class PlaceOrder(customerId: Int, bookId: Int, quantity: Int, replyTo: ActorRef[Response]) extends Command
  case class ListOrders(replyTo: ActorRef[Response]) extends Command
  case class ListOrdersByCustomer(customerId: Int, replyTo: ActorRef[Response]) extends Command

  sealed trait Response
  case class OrderPlaced(order: Option[Order]) extends Response
  case class OrdersListed(orders: List[Order]) extends Response
  case class OrdersByCustomerListed(orders: List[Order]) extends Response

  def apply(bookActor: ActorRef[BookActor.Command], customerActor: ActorRef[CustomerActor.Command]): Behavior[Command] = {
    var orders: List[Order] = List.empty
    var nextOrderId: Int = 1

    Behaviors.receiveMessage {
      case PlaceOrder(customerId, bookId, quantity, replyTo) =>
        val order = Order(nextOrderId, customerId, bookId, quantity, "Placed")
        orders = orders :+ order
        nextOrderId += 1
        replyTo ! OrderPlaced(Some(order))
        Behaviors.same

      case ListOrders(replyTo) =>
        replyTo ! OrdersListed(orders)
        Behaviors.same

      case ListOrdersByCustomer(customerId, replyTo) =>
        replyTo ! OrdersByCustomerListed(orders.filter(_.customerId == customerId))
        Behaviors.same
    }
  }
}

Step 5: Create the Main Application

Main.scala

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}
import actors.{BookActor, CustomerActor, OrderActor}
import scala.util.{Try, Success, Failure}
import utils.Utils._

object Main extends App {
  sealed trait Command
  case class Run() extends Command

  def apply(): Behavior[Command] = Behaviors.setup { context =>
    val bookActor = context.spawn(BookActor(), "BookActor")
    val customerActor = context.spawn(CustomerActor(), "CustomerActor")
    val orderActor = context.spawn(OrderActor(bookActor, customerActor), "OrderActor")

    def run(): Unit = {
      var continue = true
      while (continue) {
        println("\nBookstore App")
        println("1. Add Book")
        println("2. List Books")
        println("3. Add Customer")
        println("4. List Customers")
        println("5. Place Order")
        println("6. List Orders")
        println("7. List Orders by Customer")
        println("8. Exit")
        print("Choose an option: ")

        scala.io.StdIn.readLine() match {
          case "1" => addBook(bookActor)
          case "2" => listBooks(bookActor)
          case "3" => addCustomer(customerActor)
          case "4" => listCustomers(customerActor)
          case "5" => placeOrder(orderActor)
          case "6" => listOrders(orderActor)
          case "7" => listOrdersByCustomer(orderActor)
          case "8" => continue = false
          case _ => println("Invalid option. Please try again.")
        }
      }
    }

    def addBook(bookActor: ActorRef[BookActor.Command]): Unit = {
      val title = readLineWithPrompt("Enter book title: ")
      val author = readLineWithPrompt("Enter book author: ")
      val priceTry = readDoubleWithPrompt("Enter book price: ")
      val stockTry = readIntWithPrompt("Enter book stock: ")

      (priceTry, stockTry) match {
        case (Success(price), Success(stock)) =>
          bookActor ! BookActor.AddBook(title, author, price, stock, context.self)
          println(s"Book added: $title by $author")
        case (Failure(priceEx), _) =>
          println(s"Invalid price input. Error: ${priceEx.getMessage}")
        case (_, Failure(stockEx)) =>
          println(s"Invalid stock input. Error: ${stockEx.getMessage}")
      }
    }

    def listBooks(bookActor: ActorRef[BookActor.Command]): Unit = {
      bookActor ! BookActor.ListBooks(context.self)
    }

    def addCustomer(customerActor: ActorRef[CustomerActor.Command]): Unit = {
      val name = readLineWithPrompt("Enter customer name: ")
      val email = readLineWithPrompt("Enter customer email: ")
      val address = readLineWithPrompt("Enter customer address: ")
      customerActor ! CustomerActor.AddCustomer(name, email, address, context.self)
      println(s"Customer added: $name")
    }

    def listCustomers(customerActor: ActorRef[CustomerActor.Command]): Unit = {
      customerActor ! CustomerActor.ListCustomers(context.self)
    }

    def placeOrder(orderActor: ActorRef[OrderActor.Command]): Unit = {
      val customerIdTry = readIntWithPrompt("Enter customer id: ")
      val bookIdTry = readIntWithPrompt("Enter book id: ")
      val quantityTry = readIntWithPrompt("Enter quantity: ")

      (customerIdTry, bookIdTry, quantityTry) match {
        case (Success(customerId), Success(bookId), Success(quantity)) =>
          orderActor ! OrderActor.PlaceOrder(customerId, bookId, quantity, context.self)
          println(s"Order placed: Customer $customerId ordered Book $bookId")
        case (Failure(customerIdEx), _, _) =>
          println(s"Invalid customer id input. Error: ${customerIdEx.getMessage}")
        case (_, Failure(bookIdEx), _) =>
          println(s"Invalid book id input. Error: ${bookIdEx.getMessage}")
        case (_, _, Failure(quantityEx)) =>
          println(s"Invalid quantity input. Error: ${quantityEx.getMessage}")
      }
    }

    def listOrders(orderActor: ActorRef[OrderActor.Command]): Unit = {
      orderActor ! OrderActor.ListOrders(context.self)
    }

    def listOrdersByCustomer(orderActor: ActorRef[OrderActor.Command]): Unit = {
      val customerIdTry = readIntWithPrompt("Enter customer id: ")
      customerIdTry match {
        case Success(customerId) =>
          orderActor ! OrderActor.ListOrdersByCustomer(customerId, context.self)
        case Failure(ex) =>
          println(s"Invalid customer id input. Error: ${ex.getMessage}")
      }
    }

    Behaviors.receiveMessage {
      case Run() =>
        run()
        Behaviors.same
    }
  }

  val system = ActorSystem(Main(), "BookstoreSystem")
  system ! Run()
}

Step 6: Implement Utility Functions

utils/Utils.scala

package utils

import scala.util.{Try, Success, Failure}

object Utils {
  def readLineWithPrompt(prompt: String): String = {
    print(prompt)
    scala.io.StdIn.readLine()
  }

  def readIntWithPrompt(prompt: String): Try[Int] = {
    print(prompt)
    Try(scala.io.StdIn.readInt())
  }

  def readDoubleWithPrompt(prompt: String): Try[Double] = {
    print(prompt)
    Try(scala.io.StdIn.readDouble())
  }
}

Running Your Akka Actor-based Application

To run your application, use SBT:

sbt run

Why Futures

Asynchronous programming is a crucial concept in modern software development, especially important in Scala when dealing with concurrent operations. Utilizing Scala's Futures is a common way to write non-blocking code that can perform long-running computations, IO operations, or network requests in parallel, improving the overall efficiency and responsiveness of applications. Here are key reasons why asynchronous programming is vital:

1. Improved Scalability

Asynchronous programming allows a system to handle more work concurrently without blocking for each operation to complete. For example, in a web server context, this means being able to handle more incoming requests without waiting for each request to be fully processed (including waiting for database queries or external API calls). This model significantly improves the ability to scale applications to support high loads.

2. Better Resource Utilization

By not blocking threads on long-running operations, you make better use of system resources. Threads, which are limited and expensive resources, can be reused for other tasks instead of idling and waiting for I/O operations or other blocking calls to complete. This efficient use of threads can lead to lower memory usage and less overhead for context switching.

3. Enhanced Responsiveness

Asynchronous operations can improve the responsiveness of applications, both from a user interface perspective and system architecture perspective. User interfaces remain responsive to user interactions even while the application is performing background tasks. Similarly, backend systems can continue to accept and process incoming requests while waiting for other operations to complete.

4. Simplified Concurrent Programming

Scala's Future and for-comprehension provide a powerful yet straightforward abstraction for handling asynchronous computations. They allow developers to write code that looks sequential but executes concurrently, simplifying error handling and the composition of asynchronous operations. This leads to more readable and maintainable code compared to traditional callback-based approaches.

5. Error Handling and Composition

Scala Futures make it easier to handle errors and compose multiple asynchronous operations. Using for-comprehensions and map/flatMap operations, you can chain Futures in a way that automatically handles threading and error propagation. This approach simplifies the management of complex asynchronous workflows, where operations depend on the result of previous ones.

Example: Using Scala Futures

Here’s a simple example demonstrating the use of Scala Future for asynchronous programming:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}

val futureResult: Future[Int] = Future {
  // Simulate a long-running computation
  Thread.sleep(1000)
  42
}

futureResult.onComplete {
  case Success(value) => println(s"The result is $value")
  case Failure(exception) => println(s"Failed with $exception")
}

Future

Scala's Future provides a powerful abstraction for working with asynchronous computations. It represents a value that may become available at some point, allowing your program to continue executing while waiting for the result. This is particularly useful for operations that involve IO, database queries, network requests, or any long-running computations.

Here's a simple example demonstrating how to use Scala Futures to perform asynchronous computations:

Setup

First, ensure you have the Scala execution context in scope, as it's needed to execute the futures:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}

Basic Future Usage

The following example shows how to create a future, perform an operation asynchronously, and handle the result once it's available:

// Define a future that simulates a long-running computation
val futureComputation: Future[Int] = Future {
  Thread.sleep(1000) // Simulating a long-running task
  42 // The answer to the ultimate question of life, the universe, and everything
}

// Handling the future's result
futureComputation.onComplete {
  case Success(result) => println(s"Result: $result")
  case Failure(e) => println(s"An error occurred: ${e.getMessage}")
}

Working with Results

Often, you'll want to chain operations on futures or combine them. Here's how to use map, flatMap, and for comprehensions to work with future results:

val futureSquare: Future[Int] = futureComputation.map(x => x * x)

futureSquare.onComplete {
  case Success(square) => println(s"Square of the result: $square")
  case Failure(e) => println(s"Failed to calculate the square: ${e.getMessage}")
}

// Combining futures with for-comprehension
val anotherFutureComputation: Future[Int] = Future {
  Thread.sleep(500) // Another simulated task
  10
}

val combinedResult: Future[Int] = for {
  result1 <- futureComputation
  result2 <- anotherFutureComputation
} yield result1 + result2

combinedResult.onComplete {
  case Success(sum) => println(s"Sum of results: $sum")
  case Failure(e) => println(s"Failed to combine results: ${e.getMessage}")
}

Error Handling

Handling errors with futures can be done using recover or recoverWith methods:

val riskyFuture: Future[Int] = Future {
  throw new RuntimeException("Something went wrong")
}

val recoveredFuture: Future[Int] = riskyFuture.recover {
  case _: RuntimeException => -1
}

recoveredFuture.onComplete {
  case Success(value) => println(s"Recovered value: $value")
  case Failure(e) => println("This should not be printed if recovered properly")
}

Promise

In Scala, a Promise is a writable, single-assignment container which completes a Future. Essentially, Promise and Future are two sides of the same coin: while Future provides a way to read a value that may not yet exist, Promise provides a way to write that value once it becomes available. This mechanism is particularly useful in scenarios where you need to manually complete a Future based on the outcome of some computation or asynchronous operation.

Basic Usage of Promise

Here's how you can create and use a Promise in Scala:

  1. Creating a Promise

First, you need to import the necessary classes:

import scala.concurrent.{Promise, Future}
import scala.concurrent.ExecutionContext.Implicits.global

Then, create a Promise:

val promise: Promise[Int] = Promise[Int]()
  1. Obtaining the Future from a Promise

You can obtain a Future associated with a Promise. This Future is completed when the Promise is fulfilled.

val future: Future[Int] = promise.future
  1. Completing a Promise

A Promise can be completed with a value or an exception. Completing the Promise also completes its associated Future.

// Completing the promise with a value
promise.success(42)

// Alternatively, completing the promise with a failure
// promise.failure(new RuntimeException("Failure"))
  1. Using the Future

You can work with the Future obtained from a Promise just like any other Future. For example, you can add callbacks to handle completion:

future.onComplete {
  case Success(value) => println(s"The result is $value")
  case Failure(exception) => println(s"Failed with $exception")
}

Example: Asynchronous Computation

Here's a more concrete example, demonstrating how a Promise can be used to wrap an asynchronous computation, such as a network request:

def asyncNetworkRequest(url: String): Future[String] = {
  val promise = Promise[String]()

  // Simulate an asynchronous network request
  global.execute(() => {
    try {
      // Simulating request delay
      Thread.sleep(1000)
      promise.success(s"Response from $url")
    } catch {
      case e: Exception => promise.failure(e)
    }
  })

  promise.future
}

val url = "http://example.com"
val responseFuture: Future[String] = asyncNetworkRequest(url)

responseFuture.onComplete {
  case Success(response) => println(response)
  case Failure(exception) => println(s"Request failed: $exception")
}

In this example, asyncNetworkRequest simulates making an asynchronous network request. It uses a Promise to represent the eventual result of the request. The Promise is completed with a success or failure once the simulated request finishes, and the associated Future is used to handle the response.

todoapp

shoppingbasket

async-test

More Functions

  • partial applied
  • currying
  • partial functions

Partial Applied Functions

Partial applied mean that not all the parameters are provided. Then the return type is a new function, which can be used with the rest of parameters

scala> val addFunction = (x: Int, y: Int) => x + y
val addFunction: (Int, Int) => Int = Lambda...

scala> addFunction(7, 8)
val res1: Int = 15

scala> val partialAdd1 = addFunction(7, _: Int)
val partialAdd1: Int => Int = Lambda...

scala> partialAdd1(8)
val res2: Int = 15

scala> val partialAdd2 = (x: Int) => addFunction(x, 7)
val partialAdd2: Int => Int = Lambda...

scala> partialAdd2(8)
val res3: Int = 15

scala> def partialAdd3(x: Int) = addFunction(x, 7)
def partialAdd3(x: Int): Int

scala> partialAdd3(8)
val res4: Int = 15

These are partial applied functions Also methods can be partial applied

scala> def addMethod(x: Int, y: Int): Int = x + y
def addMethod(x: Int, y: Int): Int

scala> addMethod(7, 8)
val res5: Int = 15

scala>  val partialAdd4 = addMethod(7, _: Int)
val partialAdd4: Int => Int = Lambda...

scala> partialAdd4(8)
val res6: Int = 15

Currying

scala> def curriedAdd(x: Int)(y: Int) = x + y
def curriedAdd(x: Int)(y: Int): Int

scala> curriedAdd(7)(8)
val res7: Int = 15

scala> val partialAdd5 = curriedAdd(7)
val partialAdd5: Int => Int = Lambda...

scala> partialAdd5(8)
val res8: Int = 15

scala> val partialAdd6 = curriedAdd(7)(_:Int)
val partialAdd6: Int => Int = Lambda...

scala> partialAdd6(8)
val res9: Int = 15

def toCurry(f: (a: Int, b: Int) => Int):  Int => Int => Int =
	x => y => f(x, y)


def fromCurry(f: Int => Int => Int): (Int, Int) => Int =
	(x, y) => f(x)(y)
println(toCurry(_ + _)(2)(3))
println(fromCurry(x => y => x + y)(2, 6))

Function composition

def andThen[A, B, C](f: A => B, g: B => C) =
   (a: A) => g(f(a))

def compose[A, B, C](f: B => C, g: A => B ) =
  (a: A) => f(g(a))

println(compose((z: Int) => z * 2, (y: Int) => y + 10)(3))
println(andThen[Int, Int, Int](z => z * 2, y => y + 10)(3))

Partial Functions

A partial function is a function applicable to a subset of the data it has been defined for.
For example, we could define a function on the Int domain that only works on positive numbers.

val squareRoot: PartialFunction[Double, Double] = 
	def apply(x: Double) = Math.sqrt(x)
	def isDefinedAt(x: Double) = x >= 0

val squareRoot: PartialFunction[Double, Double] = 
  case x if x >= 0 => Math.sqrt(x)

OrElse

val negativeToPositive: PartialFunction[Int, Int] = 
  case x if x <= 0 => Math.abs(x)

val positiveToNegative: PartialFunction[Int, Int] = 
  case x if x > 0 => -1 * x

val swapSign: PartialFunction[Int, Int] = 
	positiveToNegative orElse negativeToPositive

val partialPrint: PartialFunction[Int, Unit] = {
  case x if x > 0 => println(s"$x is greater then zero")
}

(swapSign andThen partialPrint)(-1)

Collect, map and filter

Collect , map and FIlter can be used with partial functions

val parseRange: PartialFunction[Int, Int] = {
  case x: Int if x > 5 => x + 1
}
List(15, 3, "hello") collect { parseRange }
scala> List(1, 2) collect { case i: Int => i > 10 }
val res11: List[Boolean] = List(false, false)

scala> List(1, 2) filter { case i: Int => i > 10 }
val res12: List[Int] = List()

scala> List(1, 2) map { case i: Int if i < 10 => i + 1  }
val res13: List[Int] = List(2, 3)

Here we defined the partial function as an anonymous function.

Call by Value, Name, Need

Call by

  • value
  • name
  • need

Call by Value

Evaluated on function call

def functionByValue(x: Long) = 
  println(x)
  println(x)

The param x is evaluated on the function call
From then on the value of x is fixed
So the two print statements will print the same value

Call by Name

Evaluated on usage of x

def functionByName(x: => Long) = 
  println(x)
  println(x)

The param x is evaluated on the moment of usage
So the two print statements will print the different values

Call by Need

Evaluated on the first time usage of x1

def functionByNeed(x: => Long) = {
  lazy val x1 = x
  println(x1)
  println(x1)

The param x is evaluated on the first println usage
From then on the value of lazy x1 is fixed
So the two print statements will print the same value

main

def main(args: Array[String]): Unit = 
  functionByValue(System.nanoTime())
  functionByName(System.nanoTime())
  functionByNeed(System.nanoTime())

// 118920267089041
// 118920267089041

// 118920327472000
// 118920327493583

// 118920327830750
// 118920327830750

Given and Using

Sorting

Sorted

@main
def main(): Unit =
  val list = List(1, 5, 2, 6, 4, 3)
  val sortedList = list.sorted
  println(sortedList)

// List(1,2,3,4,5,6)

Ordering

given ordering: Ordering[Int] with
  override implicit def compare(x: Int, y: Int): Int = x - y

This is the same as the default implements of the standard libraries

Reverse Ordering

given reversedOrdering: Ordering[Int] with
  override implicit def compare(x: Int, y: Int): Int = y - x

Using

def sorting(using order: Ordering[Int])(list: List[Int]): List[Int] =
  list.sorted

Here we have a function that is using a given as parameter. When he compiler sees a using will search for a `given' of that type.

@main
def main(): Unit =
  val list = List(1,5,2,6,4,3)
  println(sorting(list))

Case Classes

case class Todo (task: String, priority: Int)
given todoOrdering: Ordering[Todo] with
  override implicit def compare(a: Todo, b: Todo): Int = 
    a.priority - b.priority
@main
def main(): Unit =
  val todoList = List(Todo("walk", 3), Todo("talk", 5), Todo("sleep", 1))
  println(todoList.sorted)

// List(Todo(sleep,1), Todo(walk,3), Todo(talk,5))

Scopes

The compiler will look for available given in scopes in the following order

  1. imported givens
  2. current scope
  3. companion object

Companion Object

object Todo:
  given todoOrdering: Ordering[Todo] with
    override implicit def compare(a: Todo, b: Todo): Int = 
      a.priority - b.priority

The ordering is added to the campanion object.
If there is no given in the current scope. The compiler will look in the companion object.

@main
def main(): Unit =
  val todoList = List(Todo("walk", 3), Todo("talk", 5), Todo("sleep", 1))
  println(todoList.sorted)

// List(Todo(sleep,1), Todo(walk,3), Todo(talk,5))

The using is found in the companion object

import given

object TodoOrdering:
  given todoOrdering: Ordering[Todo] with
    override implicit def compare(a: Todo, b: Todo): Int = 
      a.priority - b.priority

The ordering is added to another scope

@main
def main(): Unit =
  val todoList = List(Todo("walk", 3), Todo("talk", 5), Todo("sleep", 1))
  
  import TodoOrdering.given
  println(todoList.sorted)

// List(Todo(sleep,1), Todo(walk,3), Todo(talk,5))

Now we have to import the given. We can do this in two ways:

  • import TodoOrdering.todoOrdering
  • import TodoOrdering.given
  • import TodoOrdering.*

The wildcard * does not work on givens.

Rule of thumb

  • Put the most frequently used ordering in the companion object
  • Put the other orderings in separate objects with a clear names

Given examples

In Scala 3 (formerly known as Dotty), the given and using clauses represent a sophisticated evolution of Scala's implicits feature, making it more explicit and easier to reason about. These features are part of Scala's type class support, allowing for more expressive and type-safe code. Let's dive into a few examples to understand how given and using work.

Example 1: Basic Type Class

A type class is a sort of interface that defines some behavior. Unlike traditional interfaces, type classes can be "attached" to classes after they've been defined. Let's define a simple JsonSerializer type class and then use given and using to serialize objects to JSON.

Define the Type Class

trait JsonSerializer[T] {
  def serialize(value: T): String
}

Implement the Type Class

given JsonSerializer[String] with {
  def serialize(value: String): String = s""""$value""""
}

given JsonSerializer[Int] with {
  def serialize(value: Int): String = value.toString
}

Use the Type Class

def toJson[T](value: T)(using serializer: JsonSerializer[T]): String = {
  serializer.serialize(value)
}

println(toJson("hello"))  // Output: "hello"
println(toJson(123))      // Output: 123

Example 2: Contextual Abstractions with using Parameters

Sometimes, you might want to pass additional parameters contextually without cluttering the method signature for every call. This is where using shines.

Define a Context

case class RequestContext(userId: String)

given RequestContext = RequestContext("user123")

A Method that Requires Context

def getUser(using ctx: RequestContext): String = {
  s"Fetching data for user: ${ctx.userId}"
}

println(getUser)  // Output: Fetching data for user: user123

Example 3: Generic Programming with given

You can also use given instances for generic programming, such as defining a generic sum function for numeric types.

Numeric Type Class

trait Numeric[T] {
  def plus(x: T, y: T): T
  def zero: T
}

given Numeric[Int] with {
  def plus(x: Int, y: Int): Int = x + y
  def zero: Int = 0
}

Generic Sum Function

def sum[T](items: List[T])(using numeric: Numeric[T]): T = {
  items.foldLeft(numeric.zero)(numeric.plus)
}

println(sum(List(1, 2, 3)))  // Output: 6

Extension Methods

extension

extension(i: Int)
  def print: Unit =
    println(s"some: $i")
@main
def main(): Unit =
  val x = 3
  x.print

generics

extension[A](a: A)
  def print: Unit =
    println(s"some: $a")
@main
def main(): Unit =
  val x = 3
  x.print
  
  val str = "hello"
  str.print

case class

case class Todo(task: String, priority: Int)
object Todo:
  extension(todo: Todo)
    def print: Unit =
      println(s"${todo.priority}. ${todo.task.capitalize}")
@main
def main(): Unit =
  val todoList = List(Todo("walking", 1),Todo("swimming", 2) )
  todoList.foreach(_.print)

// 1. Walking
// 2. Swimming

Scope

The scope rules of extension methods are the simular to the scope of given

  1. imported extension
  2. current scope
  3. companion object

imported extension

object TodoExtension:
  extension (todo: Todo)
    def print: Unit =
      println(s"${todo.priority}. ${todo.task.capitalize}")
@main
def main(): Unit =
  val todoList = List(Todo("walking", 1),Todo("swimming", 2) )
  import TodoExtension.*
  todoList.foreach(_.print)

For extensions the wildcard * import is used.
This is different from to the given import.

Rule of thumb

The rules are similar to the given:

  • Put the most frequently used extension in the companion object
  • Put the other extensions in separate objects with a clear names

Conversion

case class

case class Person(name: String)
  def greet(): String = s"Hello, $name"
given fromStringToPerson: Conversion[String, Person] = Person(_)
@main def run =
  println("John".greet())

Scope

The conversion rules are the same as the given rules

  1. imported conversion
  2. current scope
  3. companion object

Implicit

Scala's implicits are a powerful feature that allows the compiler to "fill in" values or methods automatically, enhancing the language's expressiveness and enabling more concise code. They are especially useful for type class patterns, extension methods, and implicit parameters, among others. Here, we'll explore an introductory tutorial covering several key aspects of Scala implicits.

Implicit Values

Implicit values are automatically used by the compiler to fill in parameters for methods or constructors.

implicit val defaultName: String = "John Doe"

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

greet  // Output: Hello, John Doe!

Implicit Conversions

Implicit conversions can automatically convert one type to another, allowing for cleaner code when working with different but related types.

implicit def intToString(value: Int): String = value.toString

val myString: String = 123  // Implicitly converts Int to String
println(myString)  // Output: 123

Implicit Classes

Implicit classes allow adding new methods to existing types without modifying their source code, similar to extension methods in other languages.

implicit class RichInt(val i: Int) {
  def squared: Int = i * i
}

println(5.squared)  // Output: 25

Implicit Parameters

Implicit parameters enable passing context or configuration to methods without cluttering their call sites.

case class Config(prefix: String)

implicit val myConfig: Config = Config("Hello, ")

def greet(name: String)(implicit config: Config): Unit = {
  println(config.prefix + name)
}

greet("Scala")  // Output: Hello, Scala

Best Practices

While powerful, implicits should be used judiciously:

  • Limit Scope: Only bring implicits into scope when necessary to avoid conflicts and confusion.
  • Naming Conventions: Use clear and descriptive names for implicit values and parameters.
  • Documentation: Document your implicits well to aid in understanding and maintenance.

Type Classes

A type class is the functional equivalent of polymorphism in the object-oriented world. And it is resolved in compile-time by importing context givens

The Object-Oriented Way

  trait Show:
    def show: String

  case class Person(name: String, age: Int) extends Show:
    override def show = s"$name with age $age"

  val john = Person("John", 28 )
  val showJohn = john.show
  • available only for the types we extend
  • provide only one override implementation

The Pattern Matching Way

def show(value: Any): String = value match 
  case Person(name, age) => s"$name with age $age"
  case _ => throw new IllegalArgumentException("not supported")
  • lose type safety
  • extends match for every pattern
  • has one case implementation

Type Classes

Create a type class has the following steps

  1. Type Class definition
  2. Type Class instances
  3. the API
  4. Extension methods

Type Class Definition

trait Shower[T] {
	def show(value: T): String
}

Type Class Instances

given userShower: Shower[Person] with
  override def show(value: Person) =
    val Person(name, age) = value
    s"$name with age $age"}

val john = Person("John", 28)
val showJohn = userShower.show(john)

API

object Shower {
  def show[T](value: T)(using shower: Shower[T]): String =
    shower.show(value)

  def apply[T](using shower: Shower[T]): Shower[T] = shower
}

val showJohn = Shower.show(john)

Extension method


object ShowSyntax:
  extension [T](value: T)
    def show(using shower: Shower[T]): String = shower.show(value)

import ShowSyntax.*
val showJohn = john.show
  • can define showers for other types
  • multiple shower for the same type

Example Dog ad Cat

In Scala 3, the mechanism for defining and using type classes has been significantly revamped with the introduction of given and using clauses, replacing the older implicit keyword. This change aims to make the definition and usage of type classes more explicit and readable, addressing some of the common criticisms of Scala's implicit system.

Defining Type Classes with given

To define a type class instance with Scala 3, you use the given keyword. This replaces the implicit val or implicit object definitions used in Scala 2.

Example: Defining a SoundMaker type class and its instances.

trait SoundMaker[T]:
  def makeSound(value: T): Unit

// Define instances of the SoundMaker type class
given SoundMaker[Dog] with
  def makeSound(dog: Dog): Unit = println("Woof")


given SoundMaker[Cat] with
  def makeSound(cat: Cat): Unit = println("Meow")

Here, given declarations define how Dog and Cat types fulfill the SoundMaker contract. These instances are globally available and automatically used wherever a SoundMaker[T] is required.

Using Type Classes with using

To use a type class instance, Scala 3 introduces the using clause. This explicitly specifies that a function requires a type class instance for its operation, making the function's dependencies clear.

Example: Using the SoundMaker type class to implement a polymorphic playSound function.

def playSound[T](value: T)(using maker: SoundMaker[T]): Unit = maker.makeSound(value)

The using clause in the function signature tells the compiler to search for an implicit SoundMaker[T] instance for whatever type T is passed into the function. This search is based on the instances defined with given.

Putting It All Together

class Dog(val name: String)
class Cat(val name: String)

// Assuming the given instances and the playSound method are defined as above

val myDog = Dog("Rex")
val myCat = Cat("Whiskers")

playSound(myDog) // Outputs: Woof
playSound(myCat) // Outputs: Meow

Json Converter

Creating a JSON converter using type classes in Scala allows for flexible, reusable serialization logic that can be applied to various types without requiring modifications to those types. This approach is particularly useful when working with third-party classes or when you want to keep serialization logic decoupled from domain logic. Below is a simplified example demonstrating how to implement a JSON converter using type classes in Scala 3, utilizing the given and using syntax for clarity and explicitness.

Step 1: Define the JSON Type Class

First, define a trait that represents the ability to convert a value of type T to JSON.

trait JsonConverter[T] {
  def toJson(value: T): String
}

Step 2: Create Instances of the Type Class

Next, provide given instances of the JsonConverter for the types you want to support. Let's start with a few basic types like String and Int, and then create a converter for a custom class.

given JsonConverter[String] with {
  def toJson(value: String): String = s""""$value""""
}

given JsonConverter[Int] with {
  def toJson(value: Int): String = value.toString
}

// A sample case class for demonstration
case class Person(name: String, age: Int)

// Creating a JsonConverter for the Person case class
given JsonConverter[Person] with {
  def toJson(person: Person): String =
    s"""{"name": "${person.name}", "age": ${person.age}}"""
}

Step 3: Implement a Generic toJSON Function

Now, define a generic function that uses the JsonConverter type class to convert any supported type to JSON. This function will use the using clause to specify that it requires a JsonConverter for the type T.

def toJson[T](value: T)(using converter: JsonConverter[T]): String = {
  converter.toJson(value)
}

Step 4: Using the JSON Converter

Finally, you can use the toJson function to serialize different types to JSON. The compiler will automatically use the appropriate given instance based on the type of the value passed to toJson.

val name = "John Doe"
val age = 30
val person = Person(name, age)

println(toJson(name))   // Outputs: "John Doe"
println(toJson(age))    // Outputs: 30
println(toJson(person)) // Outputs: {"name": "John Doe", "age": 30}

Extensibility

One of the strengths of this approach is its extensibility. You can easily add support for new types by defining new given instances of the JsonConverter type class. This does not require modifying existing code, adhering to the open/closed principle.

Monads

The railway metaphor is a popular way to explain monads in a more intuitive and less abstract manner. It helps visualize the flow of data through transformations, especially in a language like Scala, where monads play a crucial role in handling computations, side effects, and more.

Imagine a railway system where trains (data) travel from one station (function) to the next. Each station transforms the train in some way, and the tracks guide where the train goes. In a perfect world, the train goes from start to finish without any issues. However, real life (and code) involves complications like missing tracks (exceptions) or stations that can't handle the train (errors).

The Tracks: Happy Path and Error Path

The railway has two parallel tracks: the happy path and the error path.

  • Happy Path: This is where everything goes right. The train moves from one station to the next, getting transformed along the way without any issues. In Scala, this is akin to operations on monads (like Option, Try, or Future) that successfully transform data.

  • Error Path: Sometimes, a station encounters a problem it can't handle (e.g., an invalid operation). Instead of derailing the train, the railway switches it to the error path. The train bypasses the remaining stations, as it's no longer on the happy path. This represents error handling in monads, where once an error is encountered, further transformations are skipped, and the error is propagated instead.

Example with Option Monad

Consider the Option monad, which represents a computation that may or may not return a value:

  • Some(value) represents a train on the happy path; there's a value (train) to work with.
  • None represents a train that has been switched to the error path; there's no value due to some issue.

Imagine a simple operation like adding numbers, but the numbers are provided by stations along the way:

def addStation(a: Option[Int], b: Option[Int]): Option[Int] = 
  for 
    x <- a  // The train arrives at station a
    y <- b  // The train arrives at station b
 yield x + y  // The train is transformed by adding x and y
  • If both a and b are Some(value), the train successfully travels through both stations and arrives at its destination with the sum of x and y (Some(x+y)).
  • If either a or b is None, it's like one of the stations had an issue and couldn't process the train. The train is immediately switched to the error path, and the result is None, bypassing any further computation.

The Monad Laws: Ensuring Reliable Railway Operations

Monads follow certain laws that ensure the reliability and predictability of the railway:

  1. Left identity (Boarding the train): Putting a value directly onto the happy path should be the same as applying a function to that value. Like starting your journey directly from the station, without any need for an intermediate step.

  2. Right identity (Reaching the destination): Taking a train on the happy path and doing nothing else should leave the train unchanged. Like traveling from start to finish without any unnecessary detours.

  3. Associativity (Order of stations): The order in which you combine transformations (stations) doesn't matter; the final destination (result) remains the same. You can group stations without affecting the final outcome.

The railway metaphor provides a tangible way to grasp monads: They are like well-organized railway systems for our data, ensuring that even when things go wrong, there's a clear path forward, and the system behaves predictably.

Monad Usage

Monads are a fundamental concept in functional programming, providing a way to handle side effects, manage state, sequence computations, and much more. In Scala, monads are not just an abstract concept; they are a practical tool used extensively in the standard library and many third-party libraries. The most recognizable examples of monads in Scala are Option, List, and Future.

A monad, in a very simplified view, is a type constructor (a generic type) that implements two basic operations:

  1. flatMap (also known as bind in other languages): Allows chaining operations on monadic values.
  2. unit (often available as a constructor in Scala, such as Some, List(), or Future.apply): Wraps a value into the monad.

To qualify as a monad, these operations must satisfy three laws: left identity, right identity, and associativity.

Example with Option Monad

The Option type in Scala is a monad that represents a computation that might fail. It has two subtypes: Some(value) for successful computations, and None for failed ones.

flatMap and unit

Here’s how you might use Option to perform safe computations and chaining:

def divide(num: Int, denom: Int): Option[Int] =
  if denom != 0 then Some(num / denom) else None

val result = divide(10, 2)
  .flatMap(r1 => divide(r1, 2))
  .flatMap(r2 => divide(r2, 2))

println(result) // Outputs: Some(1)

In this example, flatMap is used to chain the divide operations safely. If any divide operation fails (i.e., attempts to divide by zero), the entire computation will result in None.

For-Comprehension

In Scala, for-comprehension provides a syntactic sugar for working with monads, making the chaining operations more readable. The previous example can be rewritten as:

val result = for 
  r1 <- divide(10, 2)
  r2 <- divide(r1, 2)
  r3 <- divide(r2, 2)
 yield r3

println(result) // Outputs: Some(1)

Example with Future Monad

Future is another monad that represents a computation that may take some time to complete. It's used for asynchronous programming in Scala.

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def asyncOperation(x: Int): Future[Int] = Future:
  Thread.sleep(1000) // Simulate a time-consuming computation
  x * 2


val futureResult = 
  for 
    r1 <- asyncOperation(10)
    r2 <- asyncOperation(r1)
    r3 <- asyncOperation(r2)
  yield r3

futureResult.onComplete(println) // Outputs: Success(80) after some delay

In this Future example, for-comprehension is used to chain asynchronous operations. The Future monad handles the sequencing of these operations, ensuring that r2 is computed after r1 is completed, and r3 after r2.

Advanced Pattern Matching

Advanced pattern matching in Scala extends beyond simple case class decomposition, offering powerful features that allow for more intricate and nuanced control flow based on the shape and characteristics of data. These features include nested patterns, pattern guards, type patterns, extractor objects, and more. Let's explore some of these advanced concepts:

1. Nested Patterns

Pattern matching can be nested to decompose complex data structures. This is particularly useful when working with nested case classes or tuples.

case class Person(name: String, address: Address)
case class Address(city: String, country: String)

def matchPerson(person: Person): String = person match {
  case Person(_, Address("New York", "USA")) => "Lives in New York, USA"
  case Person(name, Address(city, _)) => s"$name lives in $city"
}

2. Pattern Guards

Pattern guards provide additional filtering conditions for a match case, using an if clause. This can refine the selection criteria for a particular pattern.

def evaluateNumber(number: Int): String = number match {
  case x if x > 0 => "Positive number"
  case x if x == 0 => "Zero"
  case x if x < 0 => "Negative number"
}

3. Type Patterns

Type patterns allow you to match objects based on their type. This can be particularly useful for polymorphic behavior in pattern matching.

def printType(x: Any): String = x match {
  case _: Int => "This is an integer"
  case _: String => "This is a string"
  case _ => "Unknown type"
}

4. Extractor Objects

Extractor objects allow for custom pattern matching logic by defining an unapply method. This method enables an object to be deconstructed in a custom way for pattern matching.

object Even {
  def unapply(arg: Int): Option[Int] = if (arg % 2 == 0) Some(arg) else None
}

def checkEven(number: Int): String = number match {
  case Even(n) => s"$n is even"
  case _ => s"$n is odd"
}

5. Matching on Collections

Scala allows pattern matching on collections such as Lists, Arrays, and more, with patterns that can match specific collection characteristics.

def listMatcher(list: List[Int]): String = list match {
  case List(_, _, third) => s"The third element is $third"
  case head :: tail => s"The head is $head"
  case Nil => "The list is empty"
}

6. Case Class Sequence Patterns

You can match sequences of case classes, combining the power of case classes with collection pattern matching.

def processShapes(shapes: List[Shape]): String = shapes match {
  case Circle(_) :: Rectangle(_, _) :: Nil => "A circle followed by a rectangle"
  case _ => "Other shapes sequence"
}

These advanced features greatly enhance the expressiveness and flexibility of pattern matching in Scala, enabling concise and powerful data manipulation and control flow mechanisms.

Variance

Scala's type system includes variance annotations that influence how subtyping between more complex types works, such as between generic classes of those types.

  • Covariance (+T): If A is a subtype of B, then Box[A] is a subtype of Box[B].
  • Contravariance (-T): If A is a subtype of B, then Box[B] is a subtype of Box[A].
  • Invariance: By default, generic types in Scala are invariant. If A is a subtype of B, there is no relationship between Box[A] and Box[B].

Example of covariance:

class Container[+A]

val animalContainer: Container[Animal] = new Container[Cat]  // Cat is a subtype of Animal

Bounds

Scala allows you to restrict the types that can be used as type parameters through bounds.

  • Upper Bounds (<:): Specifies that a type parameter must be a subtype of a particular type.

    def printName[T <: Animal](animal: T): Unit = {
      println(animal.name)
    }
    
  • Lower Bounds (>:): Specifies that a type parameter must be a supertype of a particular type.

  • View Bounds (deprecated in Scala 2.11 and removed in Scala 3): Were used to demand that there exists an implicit conversion from a type T to another type.

  • Context Bounds ([T: Ordering]): Useful for requiring an implicit value of a certain type, such as an Ordering[T] for sorting.

Type Constraints

Scala also supports type constraints that allow more control over the relationships between type parameters.

  • <% (View Bound): Deprecated.
  • <:< (Upper Type Bound): Ensures one type is a subtype of another.
  • =:!= (Not Equal): Ensures two types are not the same.
def foo[A, B](a: A, b: B)(implicit ev: A <:< B): B = b

Types

Type Alias

A type alias in Scala provides a way to give a new name to an existing type. It's a feature that enhances code readability and maintainability by allowing you to use more descriptive names for types, especially when dealing with complex types like collections or function types. Type aliases do not create new types; they simply create a new way to refer to an existing type. This means that the alias and the original type are interchangeable.

Defining a Type Alias

You can define a type alias using the type keyword. Type aliases can be defined within an object, class, or trait.

type StringList = List[String]

This alias allows you to use StringList as a shorthand for List[String].

Example Usage

Here's a simple example that demonstrates how to define and use a type alias:

object Model:
  // Define a type alias for a Map that maps Strings to Ints
  type StringToIntMap = Map[String, Int]

  // Use the type alias in a function signature
  def process(map: StringToIntMap): Unit = 
    map.foreach { 
      case (key, value) => println(s"$key -> $value")
    }
      

// Creating an instance of the aliased type
val myMap: Model.StringToIntMap = Map("one" -> 1, "two" -> 2)

// Using the function that utilizes the type alias
Model.process(myMap)

Benefits of Using Type Aliases

  1. Clarity: Type aliases can make complex type signatures clearer and easier to understand.
  2. Maintainability: If the underlying type needs to change, you can update the type alias in one place, and all uses of the alias will automatically use the new type.
  3. Abstraction: They can help abstract away implementation details, making it easier to modify or refactor code in the future.

Type Aliases for Function Types

Type aliases are particularly useful for simplifying function type signatures:

type Callback = (Int, String) => Boolean

def registerCallback(cb: Callback): Unit = {
  // Register the callback
}

// Use the alias for a function parameter
registerCallback((code, msg) => code == 200 && msg.nonEmpty)

In this example, Callback is an alias for a function type that takes an Int and a String and returns a Boolean. This makes the registerCallback function's signature more readable.

Generic Type Aliases

Type aliases can also be generic, allowing them to be used with different types:

type Pair[A, B] = (A, B)

val intPair: Pair[Int, Int] = (1, 2)
val stringPair: Pair[String, String] = ("key", "value")

This defines a generic Pair type alias for a tuple of two elements, which can then be instantiated with specific types as needed.

Union Type

Union types, introduced in Scala 3 as part of its significant language overhaul, offer a more expressive type system by allowing a value to be of one type or another. Before Scala 3, achieving similar functionality required workarounds like using Either, sealed trait hierarchies, or other less straightforward methods. Union types simplify these use cases by providing a native, more readable, and concise syntax.

Understanding Union Types

A union type A | B represents a type that can be either A or B. It's a way to say that a value can be any one of multiple types. This is particularly useful in functions that need to accept or return values of different types without resorting to Any (which is too generic and loses type safety) or complex type hierarchies.

Syntax and Basic Usage

Here's a simple example demonstrating how to use union types:

def logMessage(message: String | Int): Unit = {
  message match {
    case s: String => println(s"String: $s")
    case i: Int => println(s"Int: $i")
  }
}

logMessage("Hello, Scala 3!")  // Outputs: String: Hello, Scala 3!
logMessage(123)                // Outputs: Int: 123

In this example, logMessage can accept either a String or an Int, showcasing how union types allow for more flexible function parameters.

Union Types with Methods

When you have a value of a union type, you can only call methods that are available on all types within the union. If you need to perform type-specific operations, you'll typically use pattern matching to handle each type separately, as shown in the example above.

Combining Union Types with Other Scala 3 Features

Scala 3's improved type system, including union types, intersection types (&), and match types, provides powerful tools for expressive type-level programming. Union types, in particular, can be combined with features like enum and opaque type aliases to create robust, type-safe abstractions.

Use Cases

Union types are useful in multiple scenarios, including:

  • Functions with flexible parameters: Functions that can naturally work with inputs of different types.
  • Return types that can vary: When a function might need to return different types based on its logic.
  • Interoperability with dynamic languages or APIs: When interacting with JSON data or external systems where a field might be of different types.

Conclusion

Union types in Scala 3 enhance the language's type system, making it more expressive and flexible. By allowing values to be of one type or another, they enable developers to write more concise and type-safe code, especially in scenarios where values might legitimately be of multiple types. Union types are a significant step forward in Scala's evolution, aligning it with other advanced type systems and making it an even more powerful tool for functional and object-oriented programming.

Opaque Type

Opaque types are a feature introduced in Scala 3 as part of its rich type system enhancements. They allow developers to create type aliases that are opaque from the outside, meaning the alias is treated as a distinct type from its underlying type outside the scope where it's defined. Inside its defining scope, however, the opaque type and its underlying type are considered the same. This feature is particularly useful for creating type-safe abstractions without incurring runtime overhead, as it's implemented entirely at compile time without using additional wrapper classes or objects.

Benefits of Opaque Types

  1. Type Safety: You can use opaque types to enforce strict type distinctions in your API, preventing mix-ups between types that are structurally the same but semantically different.
  2. No Runtime Overhead: Unlike wrapper classes, opaque types do not incur any runtime overhead because they are just aliases for existing types and do not introduce new classes or objects.
  3. Encapsulation: Opaque types allow you to hide implementation details and expose only the operations and constructors that make sense for a given abstraction.

Defining Opaque Types

Opaque types are defined within an object, trait, or class and are only visible within their defining scope. Here's an example of how to define and use an opaque type:

object Lengths: 
  opaque type Meter = Double
  opaque type Kilometer = Double

  // Constructors
  def Meter(value: Double): Meter = value
  def Kilometer(value: Double): Kilometer = value

  // Extension methods
  extension (m: Meter) 
    def toKilometers: Kilometer = m / 1000
  
  extension (km: Kilometer) 
    def toMeters: Meter = km * 1000
  


import Lengths._

val distanceInMeters: Meter = Meter(1500)
val distanceInKilometers: Kilometer = distanceInMeters.toKilometers

In this example, Meter and Kilometer are opaque types for Double. They are treated as distinct types outside of the Lengths object, thus providing type safety for operations dealing with lengths and distances. The extension methods allow you to define operations on these opaque types, making them more useful and expressive.

Comparing Opaque Types with Type Aliases

Scala already has type aliases, which let you give a new name to an existing type. However, type aliases are transparent, meaning the alias and the original type are interchangeable everywhere. Opaque types, on the other hand, provide a stronger separation between the alias and the underlying type, making them distinct outside their defining scope.

Usage Patterns

Opaque types are useful for a wide range of applications, including but not limited to:

  • Wrapping primitive types for additional type safety without the overhead of case classes.
  • Creating units of measure to prevent mixing up values with the same underlying type but different semantic meanings (like meters and kilometers).
  • Encapsulating implementation details of data structures while exposing a minimal, safe API to the users.

Conclusion

Opaque types in Scala 3 offer a powerful mechanism for improving type safety and encapsulation in your Scala applications without sacrificing performance. They provide a means to distinguish between types that are structurally the same but semantically different, allowing for safer and more expressive code.

Path Dependent Types

Path-dependent types are an advanced feature in Scala that ties types to the instances of the objects in which they are defined. This means the type is dependent not just on the class or trait, but also on the instance (or "path") of the object that contains it. Path-dependent types allow for more expressive type relationships in Scala's type system, enabling more precise control over how types are associated with specific instances of classes or traits.

Understanding Path-Dependent Types

To understand path-dependent types, consider a scenario where a class or trait defines a type or class inside of it. The type of the inner class or type is then dependent on the instance of the outer class or trait. This is particularly useful in scenarios where you want the types of certain components to be tightly coupled with the instances they belong to.

Example: Bank Accounts and Currencies

Consider an example of a banking application where you want each Bank to have its own Currency. The type of currency should be specific to the bank instance, preventing mix-ups between different banks' currencies.

class Bank 
  class Currency(val name: String)
  
  def createCurrency(name: String): Currency = new Currency(name)


val bank1 = new Bank
val bank2 = new Bank

// Create currencies specific to each bank
val dollar = bank1.createCurrency("Dollar")
val euro = bank2.createCurrency("Euro")

// dollar: bank1.Currency
// euro: bank2.Currency

In this example, the type of dollar is bank1.Currency, and the type of euro is bank2.Currency. Even though Dollar and Euro are both currencies, they are tied to their respective bank instances, making them distinct types. This prevents a function that is supposed to accept only bank1.Currency from accidentally accepting bank2.Currency.

Use Case: Safe Associations

Path-dependent types are especially useful for ensuring type-safe associations between objects. For example, you could have a method that only accepts currencies from the same bank, preventing errors at compile time:

def addAmount(amount1: bank1.Currency, amount2: bank1.Currency): Unit = 
  println(s"Adding ${amount1.name} and ${amount2.name}")

addAmount(dollar, dollar)  // Compiles fine
// addAmount(dollar, euro)  // This would not compile because types don't match

Advantages of Path-Dependent Types

  1. Type Safety: They ensure that only compatible types are used together, as determined by their enclosing instance.
  2. Encapsulation: They help in encapsulating types within instances, making your code more modular and expressive.
  3. Flexibility: Path-dependent types allow for more flexible and sophisticated type relationships than would be possible with only class-based types.

Higher Kinded Types

Higher-kinded types (HKTs) are a sophisticated feature of Scala's type system, allowing you to abstract over types that themselves take type parameters. They are akin to "generics for generics" and provide a powerful tool for creating highly reusable and generic code that can work with various kinds of containers or abstractions like collections, futures, options, and more.

Understanding Higher-Kinded Types

To grasp higher-kinded types, it's essential to understand that types in Scala can have different levels of abstraction:

  • First-order types are concrete types, like Int, String, or List[Int].
  • Type constructors take type parameters to produce first-order types. For example, List is a type constructor because you need to provide a type parameter to create a concrete type, like List[Int].
  • Higher-kinded types abstract over type constructors. They allow you to write code that works with any type constructor that fits the abstracted shape.

Why Use Higher-Kinded Types?

Higher-kinded types allow you to write highly generic and reusable code. For instance, you might want to implement a function that wraps a value into any container type, like Option, List, or a custom container. Without higher-kinded types, you'd have to write a separate function for each container type.

Syntax and Examples

Let's dive into how to declare and use higher-kinded types in Scala.

Declaration

You declare higher-kinded types using underscores to represent their type parameters. For example, a type constructor that takes a single type parameter can be represented as F[_].

trait Container[F[_]] {
  def put[A](value: A): F[A]
  def get[A](container: F[A]): A
}

This trait Container is generic in the type constructor F. It can abstract over any type constructor that takes a single type parameter.

Usage

Suppose you have the following Option and List implementations of Container:

object OptionContainer extends Container[Option] {
  def put[A](value: A): Option[A] = Some(value)
  def get[A](container: Option[A]): A = container.get
}

object ListContainer extends Container[List] {
  def put[A](value: A): List[A] = List(value)
  def get[A](container: List[A]): A = container.head
}

You can now write code that works with any Container, regardless of the specific type constructor (Option, List, etc.) it uses.

Working with Higher-Kinded Types

Higher-kinded types are especially useful in functional programming patterns found in libraries like Cats and Scalaz, where they enable a level of abstraction and code reuse that's difficult or impossible to achieve with only first-order types or simple generics.

For instance, the concept of a Monad can be abstractly defined using higher-kinded types, allowing you to write monadic operations and for-comprehensions that work with any monadic structure (like Option, List, Future, etc.).

MyList - step 1

Build a tiny, immutable linked list from scratch. If you come from an object‑oriented background and are new to FP, this guide will walk you through the core ideas step by step.

What we are building (and why)

We will create a minimal version of Scala's standard List, called MyList. It will be:

  • Immutable: once created, it never changes.
  • Singly linked: each element points to the rest of the list.
  • Built from two building blocks (an Algebraic Data Type):
    • Empty: represents “no elements.”
    • Cons: represents “one element followed by the rest.”

Think of a list as either:

  • nothing (Empty), or
  • a box with a head value and an arrow pointing to the tail (the rest of the list).
- Cons(1, Cons(2, Cons(3, Empty)))
  1 -> 2 -> 3 ->
- Empty
  []
- Cons(1, Cons(2, Empty))
  [1] -> [2] -> []

Step 1: the common interface (a trait)

We start with a trait that describes what any MyList can do, regardless of whether it’s Empty or Cons. If you’re new to Scala:

  • trait is like an interface with possible default methods.
  • [A] means the list is generic (it can hold Int, String, etc.).
trait MyList[A]:
  def isEmpty: Boolean
  def head: A
  def tail: MyList[A]

Notes:

  • head returns the first element.
  • tail returns “the rest of the list.”
  • On an empty list, head and tail don’t make sense; we’ll address that below.

Exercise

Implement the trait with two cases (this is a standard FP pattern called an Algebraic Data Type):

  • Empty: the empty list.
  • Cons: a node that stores a head element and a tail (another MyList).

Start from these class skeletons and fill them in:

case class Empty[A]() extends MyList[A]
case class Cons[A]() extends MyList[A]

Hints:

  • Empty.isEmpty should be true.
  • Cons.isEmpty should be false.
  • Empty.head and Empty.tail should signal an error (e.g., throw NoSuchElementException), because there is no first element or remainder.
  • Cons should store its head and tail in the constructor.

Solution (expand if you’re stuck)

Empty

The empty list has no head or tail, so trying to access them is an error. We’ll use NoSuchElementException to indicate that.

case class Empty[A]() extends MyList[A]:
  override def isEmpty: Boolean = true
  override def head: A = throw new NoSuchElementException("head of empty list")
  override def tail: MyList[A] = throw new NoSuchElementException("tail of empty list")

Cons

A non‑empty list stores its first element (head) and a reference to the rest (tail). We pass both via the constructor. Because we are using case class, Scala will generate useful methods like toString, equals, and copy for free.

case class Cons[A](override val head: A, override val tail: MyList[A]) extends MyList[A]:
  override def isEmpty: Boolean = false

Try it out

@main
def main(): Unit =
  val myList: MyList[Int] = Cons(1, Cons(2, Cons(3, Empty())))
  println(myList)        // thanks to case classes, prints the structure
  println(myList.head)   // 1
  println(myList.tail)   // Cons(2,Cons(3,Empty()))

What you should see (format may vary):

- Cons(1,Cons(2,Cons(3,Empty())))
- 1
- Cons(2,Cons(3,Empty()))

## Key FP ideas you just practiced
- Immutability: we build new lists by composing nodes; we never mutate an existing node.
- Recursion by structure: a list is defined in terms of a smaller list (its tail). This is the foundation for many list operations you’ll implement next (map, filter, toString, etc.).
- Algebraic Data Types: modeling data with a small set of precise cases (Empty or Cons) leads to simple, predictable code.

In the next step, we’ll add behavior (like a nicer toString) and build more operations on top of this structure.


MyList - step 2

ToString

Add the toString method to MyList

trait MyList[A]:
  ...
  def toString: String
  • Make a recursive version
  • A Tail recursive version
  • A pretty print version that prints MyList(3,2,1)

Exercise

Implement the add method in Cons and Empty And test it in main

Solution

Empty

override def toString: String = ""

Recursive

We have to walk through the linked list get the head and then jump to the tail recursively. The case is the empty node.

override def toString: String =
  def concat(remainder: MyList[A]): String =
    if !remainder.isEmpty then
      current.head.toString + " " + concat(remainder.tail)
    else 
      ""
    concat(this)

Tail Recursive

In the tail recursive version we add a accumulator to the parameters. In every iteration we add the head to the accumulator And the last step in returning the accumulator

override def toString: String =
  def concat(remainder: MyList[A], accumulator: String): String =
    if !remainder.isEmpty then
      concat(remainder.tail, accumulator + " " + remainder.head)
    else
      s"MyList($accumulator)"
  concat(this, "")

Pretty print

To make in pretty print version we add a comma in every iteration. But then we get as many comma's as there are elements and that is one comma too much.

Iterating one less is starting the recursive loop with the tail
And the accumulator start with the head

override def toString: String =
  def concat(remainder: MyList[A], accumulator: String): String =
    if !remainder.isEmpty then
      concat(remainder.tail, accumulator + ", " + remainder.head)
    else
      accumulator
      
  val elements = concat(tail, head.toString)    
  s"MyList($elements)"

main

@main
def main(): Unit =
  val myList: MyList[Int] = Empty() + 1 + 2 + 3
  println(myList)
  
  //  3 2 1
  //  MyList(3, 2, 1)

The order is reversed now.

MyList - step 3

Build up lists by adding elements and make construction ergonomic. In this step we will:

  • Add an add method that prepends an element.
  • Provide a + operator as a friendly alias.
  • Create a MyList companion object with an apply method so you can write MyList(1,2,3).

Why prepend? Our MyList is immutable and singly linked. Prepending is O(1) (make one new node that points to the rest), while appending would be O(n) because you’d have to walk to the end first.

Exercise 1: add

Goal: give all MyList instances a method to add a new head in front of the list.

trait MyList[A]:
  ...
  def add(element: A): MyList[A]
  • Implement add in Empty and Cons.
  • Try it: Empty().add(1).add(2).add(3)
  • Hint: adding to either Empty or Cons returns a new Cons with element as head and the current list as tail.

Exercise 2: + operator

Let’s make the same operation feel natural with an operator.

trait MyList[A]:
  ...
  def +(element: A): MyList[A]
  • Implement + as an alias for add.
  • Try it: Empty() + 1 + 2 + 3
  • Hint: in Scala, symbols like + are valid method names. Using the infix modifier lets you omit dots and parentheses when calling it.

Exercise 3: companion object apply

Build lists from a variable number of arguments.

object MyList:
  def apply[A](elements: A*): MyList[A]
  • Implement apply so that MyList(1,2,3) creates a list 1 -> 2 -> 3.
  • Hint: a small tail-recursive helper that accumulates using + works well. Remember that + prepends, so you’ll probably want to process elements in reverse order to preserve the original left-to-right order.

Solution: add

Empty

override def add(element: A): MyList[A] = Cons(element, this)

Adding to Empty yields a new Cons whose tail is this (the empty list).

Cons

override def add(element: A): MyList[A] = Cons(element, this)

Adding to a non-empty list simply creates a new head and points its tail at the current list.

Put it in the trait

The implementations are identical, so we can provide a default on the trait and remove the duplicates in the cases:

trait MyList[A]:
  ...
  def add(element: A): MyList[A] = Cons(element, this)

Try it

@main
def main(): Unit =
  val myList: MyList[Int] = Empty().add(1).add(2).add(3)
  println(myList) // MyList(3, 2, 1) once we add pretty printing in step 2

Note the reverse order: because we always prepend, the last added element ends up at the front.

Solution: + operator

trait MyList[A]:
  ...
  def add(element: A): MyList[A] = Cons(element, this)
  infix def +(element: A): MyList[A] = add(element)
    • is just a method name.
  • With infix, you can write Empty() + 1 instead of Empty().+(1).
@main
def main(): Unit =
  val myList: MyList[Int] = Empty() + 1 + 2 + 3
  println(myList) // MyList(3, 2, 1)

Solution: companion object apply

object MyList:
  def apply[A](elements: A*): MyList[A] =
    def build(rem: Seq[A], acc: MyList[A]): MyList[A] =
      if rem.isEmpty then acc
      else build(rem.tail, acc + rem.head)

    build(elements.reverse, Empty())
  • elements: A* declares a varargs parameter.
  • We accumulate using our + method.
  • Because + prepends, we reverse the incoming sequence first to preserve the original order.

Try it

@main
def main(): Unit =
  val myList: MyList[Int] = MyList(1,2,3)
  println(myList) // MyList(1, 2, 3)

Key ideas

  • Prepending to a singly linked immutable list is O(1) and natural.
  • Providing an operator and a companion object makes APIs ergonomic without changing the underlying model.

MyList - step 4

Add behavior: foreach, map, and filter. These are the bread-and-butter operations of functional collections. We’ll implement them recursively on our immutable, singly linked list.

  • foreach visits each element and runs a side-effecting function (like printing).
  • map transforms every element and returns a new list of the results.
  • filter keeps only the elements that satisfy a predicate.

Exercise: add methods to MyList

trait MyList[A]:
  ...
  def foreach(f: A => Unit): Unit
  def map[B](f: A => B): MyList[B]
  def filter(p: A => Boolean): MyList[A]

Hints:

  • Use structural recursion: handle Empty as the base case; for Cons, do work on head, then recurse on tail.
  • Keep everything immutable. Construct new Cons nodes rather than mutating.

Solution: foreach

Empty

override def foreach(f: A => Unit): Unit = ()

Doing nothing for the empty list makes sense and returns Unit (written as ()).

Cons

override def foreach(f: A => Unit): Unit =
  f(head)
  tail.foreach(f)

Apply f to head, then recurse into the tail.

Solution: map

Empty

override def map[B](f: A => B): MyList[B] = Empty[B]()

Mapping over an empty list yields an empty list of the new type B.

Note: If Empty were declared as case object Empty extends MyList[Nothing], we could avoid specifying [B] here thanks to variance. We keep it simple for now and stay with EmptyB.

Cons

override def map[B](f: A => B): MyList[B] =
  Cons[B](f(head), tail.map(f))

Transform the head, then recursively map the tail, and rebuild a new list. You can omit [B] in Cons because the type is inferred from the return type: Cons(f(head), tail.map(f))

Solution: filter

Empty

override def filter(p: A => Boolean): MyList[A] = this

Filtering an empty list is still empty.

Cons

override def filter(p: A => Boolean): MyList[A] =
  if p(head) then Cons(head, tail.filter(p))
  else tail.filter(p)

Keep head if it matches the predicate p; otherwise drop it and continue with the tail.

Try it out

@main
def main(): Unit =
  val myList: MyList[Int] = MyList(1, 2, 3)

  myList.foreach(x => println(x + 2))         // prints 3, 4, 5
  println(myList.map(x => x * 2))             // MyList(2, 4, 6)
  println(myList.filter(x => x < 2))          // MyList(1)

Key ideas

  • Recursion mirrors the list’s structure: Empty is the base case; Cons does work and recurses on tail.
  • Immutability makes it natural to “return a new list” rather than modify in place.

MyList - step 5

Combine and expand lists with concatenation and flatMap. These two operations unlock expressive transformations and composition.

  • ++ (concatenation) glues two lists together.
  • flatMap maps each element to a list and flattens the results.

Exercise: add ++ and flatMap

trait MyList[A]:
  ...
  infix def ++(other: MyList[A]): MyList[A]
  def flatMap[B](f: A => MyList[B]): MyList[B]

Hints:

  • For ++, think in terms of rebuilding the left list while pointing the last node to the right list.
  • For flatMap, use ++ to combine the list produced from the head with the recursively flatMapped tail.

Solution: concatenation (++)

Empty

override infix def ++(other: MyList[A]): MyList[A] = other

Appending anything to an empty list returns the other list unchanged.

Cons

override infix def ++(other: MyList[A]): MyList[A] =
  Cons(head, tail ++ other)

Rebuild the left list node by node, and when you reach the end (Empty), hook it up to other.

Walk-through:

[1,2,3] ++ [4,5,6]
Cons(1, [2,3] ++ [4,5,6])
Cons(1, Cons(2, [3] ++ [4,5,6]))
Cons(1, Cons(2, Cons(3, [] ++ [4,5,6])))
Cons(1, Cons(2, Cons(3, [4,5,6])))
[1,2,3,4,5,6]

Solution: flatMap

Empty

override def flatMap[B](f: A => MyList[B]): MyList[B] = Empty[B]()

Flat-mapping an empty list yields an empty list (regardless of f).

Cons

override def flatMap[B](f: A => MyList[B]): MyList[B] =
  f(head) ++ tail.flatMap(f)
  • Transform head into a list with f(head).
  • Concatenate it with the flat-mapped tail.

Walk-through:

[1,2,3].flatMap(a => [a, a + 1])
[1,2] ++ [2,3].flatMap(f)
[1,2] ++ [2,3] ++ [3].flatMap(f)
[1,2] ++ [2,3] ++ [3,4] ++ [].flatMap(f)
[1,2] ++ [2,3] ++ [3,4] ++ []
[1,2,2,3,3,4]

Try it out

@main
def main(): Unit =
  val myList: MyList[Int] = MyList(1, 2, 3)
  val otherList: MyList[Int] = MyList(4, 5)

  println(myList ++ otherList)                      // MyList(1, 2, 3, 4, 5)
  println(myList.flatMap(a => MyList(a, a + 1)))    // MyList(1, 2, 2, 3, 3, 4)

Key ideas

  • ++ structurally rebuilds the left list and attaches the right list at the end.
  • flatMap is “map then flatten,” and becomes simple once ++ is available.

MyList - step 6

Use Scala’s for-comprehension with MyList. Because we implemented map and flatMap, MyList already supports for-comprehension. We’ll also add withFilter to support if guards inside for.

For-comprehension basics

A for-comprehension like this:

val result = for
  a <- MyList(1, 2, 3)
  b <- MyList(a, a + 1)
yield b

Is desugared by the compiler roughly into:

MyList(1, 2, 3).flatMap(a => MyList(a, a + 1).map(b => b))

Since we already implemented map and flatMap, this just works.

Try it out

@main
def main(): Unit =
  val result = for
    a <- MyList(1, 2, 3)
    b <- MyList(a, a + 1)
  yield b

  println(result) // MyList(1, 2, 2, 3, 3, 4)

Filtering in for: withFilter

In for-comprehensions, if-guards (if a < 3) use withFilter under the hood (not filter). The standard library’s withFilter is lazy; we’ll keep it simple and make ours delegate to filter.

Add this to MyList:

trait MyList[A]:
  ...
  def withFilter(p: A => Boolean): MyList[A]

Implementation

override def withFilter(p: A => Boolean): MyList[A] = filter(p)

Example

@main
def main(): Unit =
  val result = for
    a <- MyList(1, 2, 3, 4, 5) if a < 3
  yield a

  println(result) // MyList(1, 2)

Key ideas

  • for-comprehension is syntax sugar that uses map, flatMap, and withFilter.
  • Providing withFilter allows if-guards in for without changing our existing filter behavior.

MyList - step 7

Introduce laziness: build an infinite list and only compute what you need. We’ll adapt our eager MyList into a lazy version (LzList) that can represent infinite sequences safely. Then we’ll implement take and a generator for infinite lists.

Why laziness? With an eager list, trying to build an infinite structure would never finish. With a lazy list, elements are computed on demand and cached (call-by-need).

Exercise 1: from MyList to LzList

We’ll keep the same API where possible, but change evaluation strategy.

  • Rename MyList to LzList (your IDE’s rename refactoring helps).
  • Replace Cons with a class that takes head and tail by-name (delayed) and stores them as lazy vals (computed at most once).
  • Empty remains a case class (or case object) with the same shape.

Lazy Cons

class Cons[A](hd: => A, tl: => LzList[A]) extends LzList[A]:
  def isEmpty: Boolean = false
  override lazy val head: A = hd
  override lazy val tail: LzList[A] = tl

Notes:

  • hd and tl are call-by-name parameters (=>). We wrap them into lazy vals so they are computed at most once (call-by-need).
  • We use class rather than case class because case class parameters are evaluated eagerly.
  • You’ll instantiate with new Cons(...).

After this change, re-run your small examples to confirm existing map/flatMap/filter semantics still hold, just lazily.

Exercise 2: take and an infinite generator

To see laziness in action, we’ll build an infinite list and take only the first n elements.

  • Add take to LzList:
def take(n: Int): LzList[A]
  • Add a generator on the companion object to build an infinite integer list by repeatedly applying a step function:
object LzList:
  def generate(start: Int)(next: Int => Int): LzList[Int] = ???

Solution: take

Empty

override def take(n: Int): LzList[A] = this

Cons

override def take(n: Int): LzList[A] =
  def loop(rem: LzList[A], count: Int): LzList[A] =
    if count <= 0 || rem.isEmpty then Empty()
    else new Cons(rem.head, loop(rem.tail, count - 1))
  loop(this, n)
  • We stop when count reaches 0 or the list is empty.
  • We preserve laziness by constructing Cons with delayed tail.

Solution: generate

object LzList:
  def generate(start: Int)(next: Int => Int): LzList[Int] =
    new Cons[Int](start, generate(next(start))(next))

This creates an infinite list: start, next(start), next(next(start)), and so on. Because Cons is lazy, values are produced only when needed.

Try it out

@main
def main(): Unit =
  val genList: LzList[Int] = LzList.generate(1)(_ + 1)  // 1,2,3,4,...
  val genMap: LzList[Int]  = genList.map(_ * 100)       // 100,200,300,... (lazily)

  println(genMap.take(10))     // first 10 multiples of 100
  println(genMap.take(100))    // first 100
  println(genMap.take(100000)) // still fine, computed on demand

Key ideas

  • call-by-name (=>) delays evaluation; lazy val caches the result the first time it’s needed.
  • Laziness allows infinite data structures when you only observe a finite prefix (via take).

Scala Intro

Introduction to Scala

  • Overview of Scala: A high-level programming language that integrates features of object-oriented and functional programming.
  • Purpose and Goals: Designed to be concise, elegant, and, most importantly, type-safe.
  • Creator: Martin Odersky and his team at EPFL (École Polytechnique Fédérale de Lausanne) in Switzerland.
  • Release Date: First released in 2003.







Development and Evolution

  • Early Development: The motivations behind Scala's creation, including addressing shortcomings of Java.
  • Major Releases and Features:
    • Scala 2.0 (March 2006): Introduction of generic classes, abstract type members, and the new collections library.
    • Scala 2.8 (July 2010): Major overhaul of the collections library, named and default parameters.
    • Scala 2.12 (November 2016): Full support for Java 8, lambda syntax for SAM types, and backend improvements.
    • Scala 3.0 (May 2021): Significant language redesign and simplification, introduction of new concepts like implicits redesign, top-level definitions, and enum classes.
  • Contributions to the JVM Ecosystem: Impact on the development of other JVM languages and tools.







Key Features and Concepts

  • Functional Programming: Immutability, higher-order functions, and pattern matching.
  • Object-Oriented Programming: Classes, traits, and mixin composition.
  • Type System: Explanation of Scala's powerful type inference and system, including abstract type members, variance annotations, and compound types.
  • Concurrency and Distribution: Scala's approach to concurrency, including Futures, Promises, and the Akka framework.







Scala Ecosystem and Community

  • Tooling: SBT (Scala Build Tool), Metals (language server), and IDE support (IntelliJ IDEA, Visual Studio Code).
  • Libraries and Frameworks: Play Framework (web applications), Akka (concurrency and distributed computing), and Spark (big data processing).
  • Community and Adoption: Overview of the Scala community, major companies using Scala, and its presence in open-source projects.







Challenges and Criticisms

  • Learning Curve: The complexity of some features and its functional programming aspects can be challenging for newcomers.
  • Compilation Speed: Scala's compilation time has been criticized, though efforts like the Dotty compiler aim to address this.
  • Market Position: Discussion of Scala's niche in the programming landscape, its competition with other languages, and future outlook.







Variable

Introduction to var and val

  • Definition and Usage:
    • val is used to declare an immutable variable. Once initialized, its value cannot be changed.
    • var is used for mutable variables. Its value can be reassigned after initialization.
  • Syntax:
    val immutableVariable: Type = initialValue
    var mutableVariable: Type = initialValue
    
  • Key Difference: Mutability vs. immutability.







val - Immutable Variables

  • Advantages:
    • Encourages functional programming style.
    • Safer to use in concurrent or multi-threaded environments.
    • Helps in avoiding side effects, making code more predictable.
  • Use Cases:
    • When the value does not need to change after initialization.
    • For constants or read-only properties.
    • In method parameters to ensure they remain unchanged.
  • Examples:
    val pi: Double = 3.14
    val greeting: String = "Hello, Scala!"
    







var - Mutable Variables

  • Advantages:
    • Provides flexibility when a variable’s value needs to change, e.g., counters, stateful objects.
  • Disadvantages:
    • Can lead to code that is harder to understand and maintain.
    • Increased risk of errors in concurrent or multi-threaded environments.
  • Use Cases:
    • When a variable’s value needs to be updated, such as in loops or when reacting to external inputs.
    • State management within an object.
  • Examples:
    var counter: Int = 0
    var message: String = "Initial message"
    







Best Practices

  • Prefer val over var:
    • Use val by default to make your code more functional, predictable, and thread-safe.
  • Limit the use of var:
    • Restrict var usage to local scopes or cases where mutability is necessary.
    • Consider using immutable collections or structures to manage state changes more safely.
  • Performance Considerations:
    • Immutable objects can sometimes lead to increased memory usage due to the need for creating new objects instead of updating existing ones. However, the benefits of immutability in terms of code clarity and safety often outweigh these concerns.







Transitioning from Mutable to Immutable Patterns

  • Strategies:
    • Use immutable collections and operations that return new collections instead of modifying the original.
    • Apply functional programming techniques, such as mapping and reducing over collections, instead of imperative loops with mutable state.
  • Case Study: Refactoring an imperative loop to a functional style.
  • Example: Converting a loop that sums numbers using var to a functional style using val and collection methods.







Basic Types







Introduction to Basic Types in Scala

  • Overview: Scala integrates object-oriented and functional programming in a statically typed language. Unlike Java, Scala treats all types as objects.
  • Type Hierarchy: Explanation of Scala's type hierarchy, with Any at the top, splitting into AnyVal (value types) and AnyRef (reference types).
  • Immutability: Emphasizes Scala's preference for immutable types to promote functional programming practices.







Numeric Types

Int

  • Range: -2^31 to 2^31-1
  • Use: Default integer type for general numerical computation.
  • Example: val i: Int = 123456

Long

  • Range: -2^63 to 2^63-1
  • Use: Storing large integer values.
  • Example: val l: Long = 12345678910L

Double

  • Precision: 64-bit IEEE 754 floating-point.
  • Use: Default type for floating-point numbers, for double-precision decimals.
  • Example: val d: Double = 123.456







Character and Boolean Types

  1. Char
  • Description: Represents a single 16-bit Unicode character.
  • Use: Storing characters or small character-based data.
  • Example: val c: Char = 'A'
  1. Boolean
  • Description: Represents a logical value, either true or false.
  • Use: Controlling flow with conditional statements, logical operations.
  • Example: val flag: Boolean = true







String Type

  • String
    • Description: A sequence of characters. In Scala, String is equivalent to java.lang.String, fully supported with additional Scala-specific methods.
    • Use: Storing and manipulating text data.
    • Example: val str: String = "Hello, Scala!"







Unit, Null, and Nothing Types

  1. Unit
  • Equivalent to void in Java. Represents a method that does not return a meaningful value.
  • Example: def myFunction(): Unit = { println("This function returns no meaningful value") }
  1. Null
  • Represents the absence of a value for reference types. It is a subtype of all reference types (AnyRef), not value types.
  • Example: var str: String = null
  1. Nothing
  • A subtype of every other type; used to indicate abnormal termination or a program location that must not be reached.







Operators







Basic Arithmetic Operations

  • Overview: Introduction to performing basic arithmetic operations like addition, subtraction, multiplication, and division in Scala.
  • Operations:
    • Addition (+): Adds two numbers.
    • Subtraction (-): Subtracts the second number from the first.
    • Multiplication (*): Multiplies two numbers.
    • Division (/): Divides the first number by the second. Note the behavior with integer division (result is truncated).
    • Modulus (%): Returns the remainder of the division of the first number by the second.







Examples:

val sum = 5 + 3     // 8
val diff = 5 - 3    // 2
val product = 5 * 3 // 15
val quotient = 5 / 3  // 1 (if integers), or 1.666... (if floats or doubles)
val remainder = 5 % 3 // 2







Comparison Operations

  • Overview: Describes how to compare two numbers in Scala using comparison operators.
  • Operations:
    • Equal to (==): Checks if two numbers are equal.
    • Not equal to (!=): Checks if two numbers are not equal.
    • Greater than (>): Checks if the first number is greater than the second.
    • Less than (<): Checks if the first number is less than the second.
    • Greater than or equal to (>=): Checks if the first number is greater than or equal to the second.
    • Less than or equal to (<=): Checks if the first number is less than or equal to the second.
  • Examples:
    val isEqual = 5 == 3      // false
    val isNotEqual = 5 != 3   // true
    val isGreaterThan = 5 > 3 // true
    val isLessThan = 5 < 3    // false
    







Advanced Mathematical Operations

  • Overview: Introduction to more complex mathematical operations available in Scala, including those in the scala.math package.
  • Key Functions:
    • Exponential (math.pow, math.exp): Raises a number to the power of another, calculates the exponential.
    • Logarithm (math.log, math.log10): Calculates the natural or base 10 logarithm of a number.
    • Square Root (math.sqrt): Calculates the square root of a number.
    • Trigonometry (math.sin, math.cos, math.tan): Performs trigonometric operations.
  • Examples:
    val power = scala.math.pow(2, 3)  // 8.0
    val squareRoot = scala.math.sqrt(16) // 4.0
    val sinValue = scala.math.sin(scala.math.Pi / 2) // 1.0
    







Numeric Type Conversions

  • Overview: Details on how to convert between different numeric types in Scala.
  • Conversions:
    • Implicit conversions happen automatically when required by the context.
    • Explicit conversions can be performed using methods like toInt, toDouble, etc.
  • Examples:
    val intVal: Int = 10
    val doubleVal: Double = intVal.toDouble // Explicit conversion to Double
    







Best Practices

  • Precision and Accuracy: Choose the appropriate numeric type (Float vs. Double, Int vs. Long) based on the required precision and magnitude.
  • Immutability: Favor immutable values (val) over mutable ones (var) for better predictability and safety in concurrent environments.
  • Numeric Literals: Use suffixes (L for Long, D for Double, F for Float) to clearly indicate the type of numeric literals.

If..else







Understanding if in Scala

  • Introduction: Scala's if tests a condition and executes a block of code if the condition is true.
  • Syntax:
    if (condition) {
      // block of code to execute if the condition is true
    }
    
  • Key Points:
    • condition must be a Boolean expression.





if-else Structure

  • Extended Syntax:
    if (condition) {
      // block of code if condition is true
    } else {
      // block of code if condition is false
    }
    
  • As an Expression: Scala's if-else can be used to directly assign values based on conditions.
    • Example:
      val result = if (condition) "Yes" else "No"
      







if-else if Ladder

  • Syntax for Multiple Conditions:
    if (condition1) {
      // block if condition1 is true
    } else if (condition2) {
      // block if condition2 is true
    } else {
      // block if none of the conditions are true
    }
    
  • Use Case: To test for multiple, mutually exclusive conditions.







if as an Expression

  • Functional Programming Feature: In Scala, if can return a value, making it more powerful and expressive.
  • Examples:
    • Value assignment:
      val max = if (a > b) a else b
      
    • Inlining in function calls:
      println(if (condition) "Condition is true" else "Condition is false")
      
    • Scala allows if without an accompanying else, but using if as an expression in such cases will yield a Unit type (()).







Best Practices

  • Using if as an Expression: Leverage Scala’s if as an expression for cleaner and more concise code, particularly in assignments and return statements.
  • Boolean Expressions: Ensure the condition in if is a Boolean expression to avoid compilation errors.
  • Nested if Statements: While Scala allows nesting if statements, for better readability and maintainability, consider using match expressions or combining conditions logically.
  • if and Performance: Using if expressions efficiently can lead to more performant Scala code. Avoid unnecessary complex conditions and deep nesting.







match







match Expressions

  • Overview: Scala's match expression is a concise way of selecting from multiple possible blocks of code based on the value of an expression.
  • Syntax:
    variable match {
      case pattern1 => // expression or block of code for pattern1
      case pattern2 => // expression or block of code for pattern2
      ...
      case _ => // default case if no patterns match
    }
    
  • Key Points:
    • A match expression is exhaustive; it must cover all possible values of the variable.
    • The _ symbol represents a wildcard pattern that matches any value.







Simple Pattern Matching

  • Use Case: Using match for simple value comparisons, similar to a switch-case statement in other languages.
  • Example:
    val day = 4
    val dayName = day match {
      case 1 => "Monday"
      case 2 => "Tuesday"
      case 3 => "Wednesday"
      case 4 => "Thursday"
      case 5 => "Friday"
      case 6 => "Saturday"
      case 7 => "Sunday"
      case _ => "Invalid day"
    }
    







Guarded Patterns

  • Use Case: Adding conditions to patterns using if-clauses to make matches more specific.
  • Example:
    val number = 10
    val numberMatch = number match {
      case x if x < 0 => "Negative number"
      case x if x % 2 == 0 => "Even number"
      case _ => "Odd number"
    }
    







: Best Practices and Tips

  • Exhaustiveness: Always ensure that match expressions are exhaustive to avoid MatchError at runtime.
  • Readability: Use match expressions to replace complex if-else chains for better readability.
  • Pattern Guards: Use guards to fine-tune pattern matching conditions, making patterns more flexible and expressive.







while







while Loops

  • Overview: The while loop in Scala is a control flow statement that allows code to be executed repeatedly based on a given Boolean condition. The loop might not execute at all if the condition is false at the beginning.
  • Syntax:
    while (condition) {
      // code block to be executed
    }
    
  • Key Points:
    • The condition is evaluated before each iteration. If the condition evaluates to true, the loop's body is executed.
    • The condition must be a Boolean expression.
    • It’s important to modify a variable within the loop to eventually make the condition false and exit the loop, avoiding an infinite loop.

Sheet 2: while Loop Examples

  • Simple Counter:
    var counter = 0
    while (counter < 5) {
      println(s"Counter is at: $counter")
      counter += 1 // Important to avoid an infinite loop
    }
    
  • Reading from a Buffer (Pseudo-Code):
    var bufferNotEmpty = checkBuffer() // Assume this checks if a buffer is not empty
    while (bufferNotEmpty) {
      processBuffer()
      bufferNotEmpty = checkBuffer() // Re-check to update the loop condition
    }
    

Sheet 3: Introduction to do-while Loops

  • Overview: The do-while loop is similar to the while loop but guarantees that the loop's body is executed at least once because the condition is evaluated after the loop's body.
  • Syntax:
    do {
      // code block to be executed
    } while (condition)
    
  • Key Points:
    • Ideal for scenarios where the loop must execute at least once, such as user input validation.

Sheet 4: do-while Loop Examples

  • User Input Validation (Pseudo-Code):
    var userInput: String = ""
    do {
      userInput = readInput() // Assume this reads user input
      processInput(userInput)
    } while (userInput != "exit")
    
  • Post-Condition Checks:
    var continueProcessing = true
    do {
      continueProcessing = processChunk() // Assume this processes a chunk of data
    } while (continueProcessing)
    

Sheet 5: Best Practices and Recommendations

  • Prefer for or foreach for Collections: Scala encourages using higher-level abstractions like for loops or foreach methods for iterating over collections for more concise and readable code.
  • Avoid Infinite Loops: Ensure that the loop's condition will eventually become false. It's easy to create an infinite loop if the condition is not properly updated.
  • Use While Loops Sparingly: Given Scala's emphasis on functional programming, consider whether a recursive function could be used instead of a loop for better clarity and immutability.
  • Side Effects: Be cautious of using while and do-while loops to modify mutable state or perform side effects. Scala encourages a functional programming style, favoring immutability and expressions over statements.

These sheets provide a fundamental understanding of using while and do-while loops in Scala, including syntax, practical examples, and best practices to ensure efficient and effective use of loops in various programming scenarios.

for







Introduction to for Loops

  • Overview: Scala's for loop is a control structure for iterating over elements in collections, such as lists, arrays, and more. It's more powerful and flexible than traditional for loops found in many other languages, supporting advanced iteration patterns and for-comprehensions.
  • Syntax:
    for (item <- collection) {
      // operation using item
    }
    
  • Key Points:
    • The loop variable item takes on each value in the collection one by one.
    • Scala's for can also iterate through multiple collections simultaneously or nest iterations within one another.
    • The arrow <- is used to denote extraction of each element from the collection.







Filtering with Guards

  • Overview: Guards are conditions within for loops that allow filtering elements of the collection being iterated over.
  • Syntax:
    for (item <- collection if condition) {
      // operation using item
    }
    
  • Example:
    for (num <- 1 to 10 if num % 2 == 0) {
      println(num) // Prints even numbers between 1 and 10
    }
    







for Comprehensions

  • Overview: for comprehensions are a powerful feature in Scala that allow for filtering, mapping, and flatMapping operations on collections, all within the for loop syntax. They are particularly useful for working with collections in a more functional style.
  • Syntax:
    val result = for (item <- collection if condition) yield {
      // transform item
    }
    
  • Example:
    val squares = for (num <- 1 to 5) yield num * num
    // squares: IndexedSeq[Int] = Vector(1, 4, 9, 16, 25)
    







Nested for Loops

  • Overview: Scala supports nesting for loops within each other to iterate over multiple dimensions or perform complex iteration patterns.
  • Syntax:
    for (item1 <- collection1; item2 <- collection2) {
      // operation using item1 and item2
    }
    
  • Example:
    for (x <- 1 to 3; y <- 1 to 3) {
      println(s"($x, $y)")
    }
    







Best Practices

  • Use Descriptive Names: Choose descriptive names for loop variables to make your code more readable and maintainable.
  • Prefer for Comprehensions for Complex Logic: Leverage for comprehensions to simplify complex collection manipulations, improving readability.
  • Immutable Collections: When generating new collections with for comprehensions, take advantage of Scala's immutable collections to promote functional programming principles.
  • Performance Considerations: While for loops are convenient and powerful, be mindful of their performance impact, especially with large collections or complex nested loops.

List







Scala Lists

  • Overview: Lists in Scala are ordered collections of elements of the same type. They are immutable, meaning that their elements cannot be changed after the list is created.
  • Creating Lists:
    val emptyList: List[Nothing] = List()
    val singleList: List[Int] = List(1)
    val multiList: List[Int] = List(1, 2, 3)
    
  • Key Properties:
    • Lists are immutable.
    • Lists are recursive structures (a head element and a tail list).







Basic Operations on Lists

  • Accessing Elements:
    • Access by index: val firstElement = multiList(0)
    • Head and tail: val head = multiList.head, val tail = multiList.tail
  • Appending and Prepending Elements:
    • Prepend with :: or +:: val newList = 0 :: multiList
    • Append with :+ (less efficient): val appendedList = multiList :+ 4
  • Concatenation:
    • Concatenate two lists: val combinedList = List(1, 2) ::: List(3, 4)







Functional Programming with Lists

  • Mapping (map): Applying a function to each element.
    val doubledList = multiList.map(_ * 2)
    
  • Filtering (filter): Selecting elements based on a condition.
    val evenList = multiList.filter(_ % 2 == 0)
    
  • Reducing (reduce): Combining elements using a binary operation.
    val sum = multiList.reduce(_ + _)
    







Common List Methods

  • flatMap: Applying a function that returns a sequence for each element, and flattening the results into a single list.
  • foreach: Executing a side-effecting function on each element.
  • foldLeft and foldRight: Like reduce, but with an initial accumulator value.
  • zip: Combining two lists into a list of pairs.
  • reverse: Reversing the list.
  • Examples for each method will illustrate their utility and usage.







Best Practices

  • Immutability and Performance: Understanding the implications of list immutability on performance, especially for operations like appending.
  • Choosing the Right Collection: When to use List vs. other collections (e.g., Vector, Array) based on performance characteristics and use cases.
  • Functional Transformations Over Mutations: Emphasizing the use of functional transformations (map, filter, etc.) over imperative modifications to embrace Scala's functional programming paradigm.

mutable lists







Scala Mutable Lists

  • Overview: Mutable lists in Scala are part of the scala.collection.mutable package. They allow for modification after creation, supporting operations like adding, removing, and updating elements.

  • Importing Mutable Lists: Before using mutable lists, you need to import the mutable package.

    import scala.collection.mutable.ListBuffer
    







Creating Mutable Lists

Mutable lists can be instantiated using ListBuffer, which is a resizable array buffer analogous to a list.

val mutableList: ListBuffer[Int] = ListBuffer(1, 2, 3)







Adding Elements

  • Append elements using +=:
    mutableList += 4  // ListBuffer(1, 2, 3, 4)
    
  • Prepend elements using +=::
    0 +=: mutableList  // ListBuffer(0, 1, 2, 3, 4)
    
  • Append collections using ++=:
    mutableList ++= List(5, 6)  // ListBuffer(0, 1, 2, 3, 4, 5, 6)
    







Removing Elements

  • Remove a single element by value:
    mutableList -= 3  // ListBuffer(0, 1, 2, 4, 5, 6)
    
  • Remove elements by index with remove:
    mutableList.remove(0)  // Removes the first element; resulting ListBuffer(1, 2, 4, 5, 6)
    







Best Practices

  • Use Cases: Opt for mutable lists when you have a collection that needs to change frequently, such as elements being added or removed based on program logic.
  • Performance: Mutable lists offer performance benefits for certain operations like appending, but be mindful of copying costs when converting to immutable collections.
  • Thread Safety: ListBuffer is not thread-safe. For concurrent programming, consider using thread-safe collections from scala.collection.concurrent or using synchronization mechanisms.







array







Scala Arrays

  • Overview: Arrays in Scala are a direct mapping to Java's arrays. They provide a sequence of elements that are indexed and have a fixed size. Scala arrays can store any type of elements, including AnyVal (value types) and AnyRef (reference types).
  • Creating Arrays:
    val nums = Array(1, 2, 3, 4)
    val names = Array("Alice", "Bob", "Charlie")
    
  • Accessing Elements: Arrays allow random access to their elements through an index.
    val firstNum = nums(0)  // Accessing the first element
    nums(2) = 10            // Updating the third element
    
  • Key Properties:
    • Fixed size but mutable in terms of element values.
    • Can be multidimensional.







Basic Operations

  • Length of an Array: Use the length property to get the size of an array.
    val size = nums.length
    
  • Iterating Over Arrays: You can use a for loop or higher-order functions like foreach.
    for (name <- names) println(name)
    names.foreach(println)
    
  • Modifying Elements: Elements can be updated by specifying their index.
    nums(0) = 100  // Update the first element to 100
    







Common Methods

  • Mapping and Filtering: Arrays support functional programming methods such as map and filter.
    val squaredNums = nums.map(x => x * x)
    val evenNums = nums.filter(_ % 2 == 0)
    
  • Sorting Arrays: Use the sorted method or sortWith for custom sorting.
    val sortedNames = names.sorted
    val descendingNums = nums.sortWith(_ > _)
    







Multidimensional Arrays

  • Creating and Using Multidimensional Arrays: Scala supports arrays of arrays, allowing for multidimensional structures.
    val matrix = Array.ofDim[Int](2, 2)  // 2x2 matrix
    matrix(0)(0) = 1
    matrix(0)(1) = 2
    matrix(1)(0) = 3
    matrix(1)(1) = 4
    







Converting Between Arrays and Other Collections

  • To and From Collections: Arrays can be converted to and from other Scala collections like List, Seq, etc.
    val numList = nums.toList
    val numSeq = nums.toSeq
    val arrayFromList = numList.toArray
    







Best Practices

  • When to Use Arrays:
    • Choose arrays for performance-sensitive, low-level programming tasks.
    • Ideal when working with a fixed-size collection of elements.
  • Interoperability with Java:
    • Arrays provide seamless interoperability with Java methods that require Java's native arrays.
  • Immutability vs. Mutability:
    • Consider using immutable collections like Vector or List for functional programming practices. Use arrays when mutability is a necessity or for performance reasons.

ArrayBuffer







Introduction to ArrayBuffer

  • Overview: An ArrayBuffer is a mutable indexed sequence that allows you to append, prepend, insert, and remove elements efficiently. It is part of the Scala Collections Library.
  • Importing ArrayBuffer:
    import scala.collection.mutable.ArrayBuffer
    
  • Creating an ArrayBuffer:
    val nums = ArrayBuffer[Int]()  // Or new ArrayBuffer[Int]
    val strings = ArrayBuffer("one", "two", "three")
    







Adding Elements

  • Appending Elements: Use += to append one or more elements at the end.
    nums += 4
    nums += (5, 6, 7)
    
  • Prepending Elements: Use +=: or prepend to add elements at the start.
    3 +=: nums
    nums.prepend(1, 2)
    
  • Inserting Elements: Use insert to add elements at a specific index.
    nums.insert(2, 8)  // Inserts 8 at index 2
    







Removing Elements

  • Removing by Index: Use remove to delete elements at a specific index.
    nums.remove(2)  // Removes the element at index 2
    
  • Removing Multiple Elements: Specify the index and the number of elements to remove.
    nums.remove(0, 3)  // Removes 3 elements starting from index 0
    
  • Clearing All Elements: Use clear to empty the entire buffer.
    nums.clear()
    







Accessing and Updating Elements

  • Accessing Elements: Use the apply method or parentheses.
    val firstElement = nums(0)
    
  • Updating Elements: Assign a new value to a specific index.
    nums(0) = 10
    







Common Operations

  • Iterating Over Elements: Use foreach, for loops, or higher-order functions.
    nums.foreach(println)
    
  • Transforming ArrayBuffer: Use map, filter, etc., to produce new collections.
    val doubledNums = nums.map(_ * 2)
    
  • Converting to Arrays: Use toArray for a fixed-size representation.
    val numsArray = nums.toArray
    







Performance Considerations

  • Amortized Costs: While append operations are generally efficient, be mindful of the amortized cost of resizing, especially for very large collections.
  • Memory Overhead: ArrayBuffer uses more memory than a plain array due to its resizable nature.







Use Cases for ArrayBuffer

  • When to Use: ArrayBuffer is ideal for building up sequences of elements when the number of elements is unknown upfront or when frequent updates to the sequence are required.
  • Comparisons: Prefer ArrayBuffer over immutable collections when mutability is necessary for performance reasons, but choose immutable collections when mutability is not a requirement to leverage functional programming benefits.







vector







Scala Vectors

  • Overview: Scala's Vector is an immutable collection that provides effective random access and updates. It's implemented as a trie (a tree-like data structure) with a branching factor of 32, balancing between quick access and functional updates.
  • Creating Vectors:
    val emptyVector: Vector[Int] = Vector()
    val vector: Vector[Int] = Vector(1, 2, 3, 4, 5)
    
  • Key Properties:
    • Immutable.
    • Efficient for large collections.
    • Provides fast random access and updates.







Basic Operations

  • Accessing Elements: Use the apply method or simply parentheses.
    val firstElement = vector(0)
    
  • Appending and Prepending Elements: Since Vectors are immutable, these operations return a new Vector.
    val appended = vector :+ 6
    val prepended = 0 +: vector
    
  • Concatenation: Combining two Vectors.
    val vector2 = Vector(6, 7, 8)
    val concatenated = vector ++ vector2
    







Transformations

  • Mapping: Apply a function to each element.
    val doubled = vector.map(_ * 2)
    
  • Filtering: Select elements based on a predicate.
    val evens = vector.filter(_ % 2 == 0)
    
  • Folding: Reduce elements using a binary operation.
    val sum = vector.foldLeft(0)(_ + _)
    







Advanced Features and Performance

  • Random Access Performance: Vectors offer excellent performance for both random reads and writes due to their trie-like structure, making them suitable for large collections.
  • Memory Footprint: While providing fast access, Vectors might have a larger memory footprint than simple sequences like Lists, due to their underlying structure.
  • Iteration Performance: Iterating over a Vector is efficient and comparable to iterating over an array.







Use Cases and Best Practices

  • When to Use Vectors:
    • Ideal for large collections requiring frequent reads and updates.
    • When you need a general-purpose, immutable collection with good performance characteristics.
  • Vector vs. List: Prefer Vectors over Lists for large collections or when you need fast access. Lists can be more efficient for operations that involve primarily head-based operations or when working with small collections.
  • Immutable Collections: Embrace the use of immutable collections like Vectors to leverage the benefits of functional programming, such as easier reasoning about code and inherent thread safety.







Converting Between Vectors and Other Collections

  • Conversion to Other Collections: Vectors can be easily converted to and from other collection types.
    val listFromVector = vector.toList
    val arrayFromVector = vector.toArray
    val seqFromVector = vector.toSeq
    







Java collections







Overview and Key Differences

  • Java Collections: Part of the Java Collections Framework (JCF), focusing on mutable collections with an imperative programming style. Key interfaces include List, Set, Map, and more specialized collections like Queue and Deque.
  • Scala Collections: Designed for both mutable and immutable collections, encouraging a functional programming approach. Scala collections are divided into scala.collection.immutable and scala.collection.mutable packages, providing a unified API for manipulating collections in a more expressive manner.
  • Key Differences:
    • Mutability: Java collections are mutable by default, whereas Scala offers both immutable and mutable collections.
    • Functional Operations: Scala collections support a rich set of functional operations like map, filter, reduce, which are not as seamlessly integrated into Java collections without Streams API.
    • Concurrency: Scala has a separate library (scala.collection.concurrent) for concurrent collection operations, while Java integrates concurrent collections within the JCF (java.util.concurrent).







: Mutable Collections Comparison

  • Lists:

    • Java: ArrayList, LinkedList
    • Scala: mutable.ListBuffer
  • Sets:

    • Java: HashSet, LinkedHashSet, TreeSet
    • Scala: mutable.HashSet, mutable.LinkedHashSet
  • Maps:

    • Java: HashMap, LinkedHashMap, TreeMap
    • Scala: mutable.HashMap, mutable.LinkedHashMap
  • Usage Example:

    • Java: Modifying a List requires direct manipulation.
    • Scala: ListBuffer allows for easy append/prepend operations, and can be converted to an immutable List for functional operations.







Immutable Collections Comparison

  • Lists:
    • Java: Immutable lists can be created with Collections.unmodifiableList or Java 9's List.of.
    • Scala: List is the default immutable sequence.
  • Sets:
    • Java: Collections.unmodifiableSet or Java 9's Set.of.
    • Scala: Set for immutable sets.
  • Maps:
    • Java: Collections.unmodifiableMap or Java 9's Map.of.
    • Scala: Map for immutable maps.
  • Usage Example:
    • Java: Immutability is often an afterthought, wrapped around mutable collections.
    • Scala: Immutability is the default, encouraging functional programming styles.







Functional Programming with Collections

  • Java Streams API (Java 8+): Introduced to support functional-style operations on collections, like filter, map, reduce.
  • Scala Collections API: Integrates functional operations directly into the collections, making them more concise and expressive for transformations.
  • Example Comparison:
    • Java:
      List<String> filtered = list.stream().filter(s -> s.startsWith("a")).collect(Collectors.toList());
      
    • Scala:
      val filtered = list.filter(_.startsWith("a"))
      







Interoperability and Conversions

  • Scala-Java Interoperability: Scala provides implicit conversions between Java and Scala collections using JavaConverters or JavaConversions (deprecated) to facilitate interoperability.
  • Conversion Examples:
    • Converting a Java List to a Scala List:
      import scala.collection.JavaConverters._
      val scalaList = javaList.asScala.toList
      
    • Converting a Scala Seq to a Java List:
      val javaList = scalaSeq.asJava
      







Choosing Between Java and Scala Collections

  • Considerations:
    • Performance: Java collections might offer better performance for certain operations, especially in low-latency environments.
    • Immutability: Scala's immutable collections are ideal for functional programming and concurrency.
    • Interoperability: Use Java collections when working closely with Java codebases; otherwise, prefer Scala collections for Scala projects.
  • Best Practice: Leverage the strengths of both ecosystems by choosing the right tool for the task, considering factors like mutability, performance, and the programming paradigm.







function







Functions in Scala

  • Overview: Functions in Scala are declared with the def keyword, followed by a name, parameter list, return type, and the function body.
  • Basic Syntax:
    def functionName(param1: Type1, param2: Type2): ReturnType = {
      // Function body
      // Last expression is the return value
    }
    
  • Example: A simple function to add two numbers.
    def add(x: Int, y: Int): Int = x + y
    
  • Key Concepts:
    • Functions can be assigned to variables and passed as arguments.
    • Scala supports both procedure (functions that return Unit) and functions that return a value.







Higher-Order Functions

  • Overview: Higher-order functions are functions that can take functions as parameters and/or return functions as results.

  • Examples:

    • Taking a function as a parameter:
      def applyOperation(a: Int, b: Int, operation: (Int, Int) => Int): Int = operation(a, b)
      
    • Returning a function:
      def multiplier(factor: Int): Int => Int = (x: Int) => x * factor
      
  • Use Cases: Higher-order functions are instrumental in enabling functional programming patterns like map, filter, and reduce.







Anonymous Functions and Lambdas

  • Overview: Scala allows the definition of anonymous functions (or lambdas), which are functions without a name.
  • Syntax:
    (param1: Type1, param2: Type2) => expression
    
  • Example:
    val add = (x: Int, y: Int) => x + y
    val numbers = List(1, 2, 3)
    val incremented = numbers.map(n => n + 1)
    







Functions vs Methods

  • Differences:
    • Definition: Methods in Scala are defined with the def keyword and are part of a class or object. Functions are first-class values that can exist independently.
    • Usage: Methods can be converted to functions if needed (method lifting).
  • Example of Method to Function Conversion:
    object MathOps {
      def addMethod(x: Int, y: Int): Int = x + y
    }
    val addFunction: (Int, Int) => Int = MathOps.addMethod _
    







Best Practices and Tips

  • Immutability: Favor immutable data structures and operations without side effects for easier reasoning and debugging.
  • Composition: Leverage function composition to build more complex operations from simpler ones.
  • Type Inference: Utilize Scala's type inference to simplify function declarations without losing clarity.







parameters







Basic Parameters

  • Overview: Fundamental to defining functions, basic parameters are specified in the function definition and are required when calling the function.
  • Syntax:
    def functionName(param1: Type1, param2: Type2): ReturnType = {
      // Function body
    }
    
  • Example:
    def add(x: Int, y: Int): Int = x + y
    







Default Parameters

  • Overview: Default parameters allow functions to be called with fewer arguments than defined, by specifying default values.
  • Syntax:
    def functionName(param1: Type1 = defaultValue1): ReturnType = {
      // Function body
    }
    
  • Example:
    def log(message: String, level: String = "INFO"): Unit = println(s"[$level] $message")
    log("System starting")  // Uses the default level of INFO
    







Named Arguments

  • Overview: Named arguments enable the specification of arguments out of order by naming them directly in the function call.
  • Syntax:
    functionName(param1 = value1, param2 = value2)
    
  • Example:
    def printDetails(name: String, age: Int, country: String): Unit = {
      println(s"Name: $name, Age: $age, Country: $country")
    }
    printDetails(age = 30, name = "Alice", country = "Canada")
    







Variable Arguments (Varargs)

  • Overview: Variable arguments (varargs) allow functions to accept an arbitrary number of arguments of the same type.
  • Syntax:
    def functionName(args: Type*): ReturnType = {
      // Function body
    }
    
  • Example:
    def sum(numbers: Int*): Int = numbers.sum
    println(sum(1, 2, 3, 4))  // Outputs 10
    







Higher-Order Functions as Parameters

  • Overview: Functions can take other functions as parameters, allowing for powerful abstractions and operations.
  • Syntax:
    def functionName(f: (Type1, Type2) => ReturnType): ResultType = {
      // Function body
    }
    
  • Example:
    def applyOperation(a: Int, b: Int, operation: (Int, Int) => Int): Int = operation(a, b)
    println(applyOperation(5, 3, _ + _))  // Outputs 8
    







Best Practices

  • Use Default Parameters to Avoid Overloading: Instead of creating multiple overloaded methods, use default parameters to simplify the API.
  • Leverage Named Arguments for Clarity: Especially in functions with many parameters or boolean flags, named arguments can enhance readability.
  • Prefer Varargs for Flexible Argument Lists: When the number of arguments is not fixed, use varargs to make your functions more adaptable.





function calls







Basic Function Calls

  • Overview: Calling a function in Scala involves specifying the function name followed by its arguments in parentheses, if any are required.
  • Syntax:
    functionName(arg1, arg2, ..., argN)
    
  • Example:
    def add(x: Int, y: Int): Int = x + y
    val result = add(5, 3)  // Calls the add function with 5 and 3 as arguments
    







Higher-Order Function Calls

  • Overview: Higher-order functions are functions that take other functions as parameters or return them as results. Calling them involves passing functions as arguments.
  • Example of Passing Functions as Arguments:
    def applyOperation(a: Int, b: Int, operation: (Int, Int) => Int): Int = operation(a, b)
    val sum = applyOperation(5, 3, (x, y) => x + y)  // Using a lambda expression as an argument
    
  • Example of Using Functions Returned by Other Functions:
    def greeting(language: String): String => String = {
      language match {
        case "English" => (name: String) => s"Hello, $name!"
        case "Spanish" => (name: String) => s"Hola, $name!"
      }
    }
    val englishGreeting = greeting("English")
    println(englishGreeting("Alice"))  // Outputs: Hello, Alice!
    







Calling Functions with Named Arguments

  • Overview: Scala allows calling functions with named arguments, letting you specify the name of the parameters explicitly, which can enhance readability and allow for arguments to be passed in any order.
  • Syntax:
    functionName(paramName1 = value1, paramName2 = value2)
    
  • Example:
    def printDetails(name: String, age: Int, country: String): Unit = {
      println(s"Name: $name, Age: $age, Country: $country")
    }
    printDetails(age = 30, name = "Alice", country = "Canada")  // Arguments are out of order but correctly named
    







Using Default and Optional Parameters

  • Overview: Functions in Scala can define default values for parameters, allowing those parameters to be omitted when the function is called.
  • Syntax:
    def functionName(param1: Type = defaultValue1, param2: Type = defaultValue2): ReturnType = { ... }
    
  • Example:
    def log(message: String, level: String = "INFO"): Unit = println(s"[$level] $message")
    log("System startup")  // Level parameter is optional due to a default value
    







Best Practices in Function Calls

  • Clarity and Readability: Prefer named arguments for functions with multiple parameters, especially boolean flags, to enhance code readability.
  • Use Default Parameters Wisely: Default parameters can simplify API usage but use them judiciously to avoid confusion about function behavior.
  • Leverage Higher-Order Functions: Embrace higher-order functions for more expressive and concise code, especially when working with collections or implementing patterns that benefit from function composition.







composition







Function Composition

  • Overview: Function composition is a technique where the result of one function is passed as the input to another function. In Scala, this can be achieved using the compose and andThen methods.
  • Key Concepts:
    • Compose: Given two functions, f and g, the compose method creates a new function where g is applied first and then f (f(g(x))).
    • AndThen: Similar to compose, but applies the first function followed by the second (g(f(x))).







: Composing Functions with compose

  • Syntax and Example:
    val f: Int => Double = _ * 1.5
    val g: Double => Double = _ + 10
    val h = f.compose(g)  // h(x) = f(g(x))
    // Example usage
    h(5)  // First applies g to 5, then applies f to the result of g(5)
    
  • Use Cases: compose is particularly useful when you want to reverse the order of function application, starting with the function passed as an argument to compose.







Composing Functions with andThen

  • Syntax and Example:
    val f: Int => Double = _ * 1.5
    val g: Double => Double = _ + 10
    val h = f.andThen(g)  // h(x) = g(f(x))
    // Example usage
    h(5)  // First applies f to 5, then applies g to the result of f(5)
    
  • Use Cases: andThen is useful for creating a chain of function applications where the output of one function is the input to the next in a straightforward, left-to-right sequence.







Function Composition Using Anonymous Functions

  • Overview: Anonymous functions (or lambda expressions) can be composed directly without the need for named functions.
  • Example:
    val multiplyBy2 = (x: Int) => x * 2
    val add3 = (y: Int) => y + 3
    val composed = multiplyBy2.andThen(add3)
    // Example usage
    composed(5)  // First multiplies 5 by 2, then adds 3 to the result
    







Practical Applications

  • Data Transformation Pipelines: Use function composition to create pipelines for processing data, where each function performs a transformation or filtering operation.
  • Modularizing Code: Break down complex operations into simpler functions that can be composed, making the code easier to understand and test.







Best Practices

  • Debuggability: While function composition can make code more concise, it can also make debugging more challenging. Consider readability and the ease of debugging when composing multiple functions.
  • Performance: Be mindful of the performance implications of creating deeply composed functions, especially if they are applied frequently or operate on large data sets.
  • Type Compatibility: Ensure that the output type of one function matches the input type of the next in the composition chain to avoid compile-time errors.





recursion







Introduction to Recursion

  • Overview: Recursion occurs when a function calls itself to solve a problem. It's particularly common in functional programming languages like Scala, where functions are first-class citizens.
  • Key Concepts:
    • Base Case: The condition under which the recursive function stops calling itself, preventing infinite recursion.
    • Recursive Case: The part of the function where the recursion occurs, typically altering the inputs with each recursive call to eventually reach the base case.







: Writing Recursive Functions

  • Syntax and Structure:
    def functionName(parameters): ReturnType = {
      if (baseCaseCondition) baseCaseSolution
      else functionName(modifiedParameters) // Recursive call
    }
    
  • Example - Factorial Function:
    def factorial(n: Int): Int = {
      if (n <= 1) 1
      else n * factorial(n - 1)
    }
    







Tail Recursion in Scala

  • Overview: Tail recursion is a special case of recursion where the recursive call is the last operation in the function. Scala can optimize tail-recursive functions to prevent stack overflow errors.
  • Key Concepts:
    • Tail Recursive Function: A recursive function where the compiler can optimize the recursive calls to avoid consuming additional stack frames.
    • Scala's @tailrec Annotation: Used to ensure a function is tail-recursive. If it's not, the compiler will throw an error.
  • Example - Tail-Recursive Factorial:
    import scala.annotation.tailrec
    
    def factorial(n: Int): Int = {
      @tailrec
      def loop(acc: Int, n: Int): Int = {
        if (n <= 1) acc
        else loop(acc * n, n - 1)
      }
      loop(1, n)
    }
    







Common Pitfalls in Recursive Functions

  • Stack Overflow Error: Occurs when too many recursive calls are made without reaching the base case, exceeding the stack size limit. This is particularly common in non-tail-recursive functions.
  • Incorrect Base Case: Leads to infinite recursion if the base case is not correctly defined or never reached.
  • Performance Considerations: While recursion can simplify code, it may lead to performance issues compared to iterative solutions, especially if not optimized for tail recursion.







Best Practices

  • Choosing Iteration vs. Recursion: Consider using iteration for simple loops to avoid stack overflow risks and potential performance issues, especially when tail recursion cannot be applied.
  • Optimizing Recursive Functions: Leverage tail recursion where possible to optimize recursive functions, and use Scala's @tailrec annotation to ensure the compiler can apply tail call optimization.
  • Debugging Recursive Functions: Break down the function calls and carefully trace the execution path, paying special attention to the base and recursive cases.







console







: Writing to the Console

  • Overview: Writing to the console in Scala is performed using the println, print, or printf methods, which are available by default as they are inherited from Scala’s Predef object.
  • Key Methods:
    • println: Outputs a line to the console with a newline character at the end.
    • print: Similar to println but does not append a newline at the end.
    • printf: Allows formatted output, similar to C's printf.
  • Example:
    println("Hello, World!") // Prints with a newline
    print("Hello, Scala ")   // Prints without a newline
    printf("Age: %d", 25)    // Prints formatted string
    







Reading from the Console

  • Overview: Reading input from the console allows interactive Scala applications to capture user input. This is typically done using scala.io.StdIn methods.
  • Importing StdIn: Before reading from the console, ensure you import StdIn methods:
    import scala.io.StdIn._
    
  • Key Methods:
    • readLine: Reads a line of text from the console.
    • readInt, readDouble, etc.: Reads and converts input to a specific type.
  • Example:
    println("Enter your name: ")
    val name = readLine()
    println(s"Hello, $name!")
    







Handling Input Errors

  • Overview: When reading input from the console, it’s important to handle potential errors such as format mismatches (e.g., expecting an integer but receiving text).
  • Try-Catch Blocks: Scala’s try-catch blocks can be used to handle errors gracefully when reading input.
  • Example:
    import scala.io.StdIn._
    import scala.util.Try
    
    println("Enter an integer: ")
    val input = readLine()
    val maybeInt = Try(input.toInt).toOption
    
    maybeInt match {
      case Some(i) => println(s"You entered: $i")
      case None => println("That's not an integer!")
    }
    







: Advanced Console I/O

  • Overview: Beyond basic text input and output, Scala can interact with the console in more sophisticated ways, including reading hidden input (like passwords) and redirecting output.
  • Reading Hidden Input: Scala does not provide a built-in method for reading passwords directly from the console without displaying them. However, Java’s System.console().readPassword() can be utilized within Scala for this purpose.
  • Redirecting Output: While Scala applications typically write to standard output (stdout), it’s possible to redirect output to files or other streams using Java's I/O capabilities.







Best Practices in Console I/O

  • User Experience: Provide clear prompts and feedback when expecting input and displaying output to ensure a good user experience.
  • Validation and Error Handling: Always validate and handle errors in user input to prevent crashes or unexpected behavior.
  • Avoiding Hardcoding: For more complex applications, consider using external configuration or argument parsing libraries instead of relying heavily on console I/O.





file







Overview of File I/O in Scala

  • Introduction: File I/O operations are essential for reading data from files and writing data to files, enabling persistence and data exchange.
  • Scala and Java I/O: Scala does not have its own file I/O library and typically uses Java's I/O classes found in java.io and java.nio packages.







Writing to Files

  • Using java.io.PrintWriter: Scala can utilize Java’s PrintWriter for simple file writing tasks.
    • Example:
      import java.io.PrintWriter
      
      val pw = new PrintWriter(new File("example.txt"))
      try {
        pw.write("Hello, Scala file I/O!")
      } finally {
        pw.close()
      }
      
  • Using java.nio.file (Java 7+): The newer Java NIO package provides a more modern and flexible approach to file I/O.
    • Example:
      import java.nio.file.{Paths, Files}
      import java.nio.charset.StandardCharsets
      
      val path = Paths.get("example_nio.txt")
      val lines = Seq("Hello", "Scala", "NIO").asJava
      Files.write(path, lines, StandardCharsets.UTF_8)
      







Reading from Files

  • Using scala.io.Source: Scala’s Source class offers a convenient way to read from files line by line.
    • Example:
      import scala.io.Source
      
      val filename = "example.txt"
      for (line <- Source.fromFile(filename).getLines()) {
        println(line)
      }
      
  • Using java.nio.file.Files: Java NIO also provides methods for reading files, such as reading all lines at once into a List.
    • Example:
      import java.nio.file.{Paths, Files}
      import java.util.stream.Collectors
      import scala.jdk.CollectionConverters._
      
      val path = Paths.get("example_nio.txt")
      val lines = Files.readAllLines(path).asScala
      lines.foreach(println)
      







Exception Handling in File I/O

  • Overview: Handling exceptions is critical in file I/O operations to deal with issues like missing files, access rights, or disk space.
  • Try-Catch-Finally: Using try-catch-finally blocks ensures that resources are closed properly, even when an error occurs.
    • Example:
      import scala.io.Source
      import scala.util.Using
      
      val filename = "example.txt"
      try {
        Using.resource(Source.fromFile(filename)) { source =>
          for (line <- source.getLines()) {
            println(line)
          }
        }
      } catch {
        case e: Exception => e.printStackTrace()
      }
      







Advanced File I/O Operations

  • Working with Large Files: For large files, consider using streams or readers that don't load the entire file into memory.
  • File Attributes and Operations: Java NIO provides tools to check file attributes, such as size or modification time, and to perform operations like moving or copying files.







Best Practices

  • Resource Management: Always ensure that file readers and writers are properly closed to prevent resource leaks.
  • File Paths: Be mindful of platform-specific path separators and use the Paths utility to construct paths that are portable across operating systems.
  • Error Handling: Robust error handling and input validation can prevent many common pitfalls in file I/O operations, such as handling non-existent files or permission issues.







internet







Reading Content from a URL

  1. Import Necessary Classes: Start by importing Java's URL class, which represents a Uniform Resource Locator, and other necessary classes for reading the input stream.

    import java.net.URL
    import java.io.{BufferedReader, InputStreamReader}
    import scala.util.Using
    
  2. Create a URL Instance: Create an instance of the URL class by passing the desired URL as a string to the constructor.

    val url = new URL("http://example.com")
    
  3. Open a Connection and Read: Use the openStream method to open a connection to the URL and fetch the content. It's recommended to use a BufferedReader for efficient reading of text data.

    Using.resource(new BufferedReader(new InputStreamReader(url.openStream()))) { reader =>
      var inputLine = reader.readLine()
      while (inputLine != null) {
        println(inputLine)
        inputLine = reader.readLine()
      }
    }
    

    The Using.resource block ensures that the reader is closed properly after use, which is a part of Scala's standard library to manage resources automatically and safely.







Handling Exceptions

When performing network operations, numerous exceptions can occur, such as java.net.MalformedURLException if the URL is not valid, or java.io.IOException if an I/O error occurs. It's crucial to handle these exceptions gracefully:

try {
  Using.resource(new BufferedReader(new InputStreamReader(url.openStream()))) { reader =>
    var inputLine = reader.readLine()
    while (inputLine != null) {
      println(inputLine)
      inputLine = reader.readLine()
    }
  }
} catch {
  case e: Exception => e.printStackTrace()
}







Asynchronous and Advanced HTTP Requests

For more complex use cases, such as asynchronous requests, handling HTTP methods other than GET, or managing HTTP headers and status codes, you might consider using dedicated Scala libraries like sttp, scalaj-http, or integrating with Java libraries like Apache HttpClient. These libraries offer more control and flexibility over HTTP requests and responses.

swing







Scala Swing

  • Overview: Scala Swing is a part of the Scala standard library that provides a high-level API for creating graphical user interfaces. It is built on top of Java Swing, aiming to be more idiomatic to Scala.
  • Key Components:
    • Simple Components: Labels, Buttons, Text Fields, and Text Areas.
    • Containers: Panels, Frames, and Windows.
    • Layout Managers: Box Layout, Grid Layout, Border Layout, and Flow Layout.
  • Event Handling: Scala Swing simplifies event handling through listeners and reactions, making GUI programming more straightforward and type-safe.







Creating Swing Application

  • Basic Structure: A simple Scala Swing application usually includes a main frame and some UI components.
  • Example: Creating a simple window with a button.
    import scala.swing._
    
    object SimpleApp extends SimpleSwingApplication {
      def top = new MainFrame {
        title = "Simple Scala Swing App"
        contents = new Button {
          text = "Click me"
          reactions += {
            case _: event.ButtonClicked => println("Button clicked!")
          }
        }
        size = new Dimension(300, 200)
      }
    }
    
  • Explanation: This sheet should explain the components used, such as MainFrame and Button, and how to set properties like title, contents, and size.







Handling Events

  • Overview: Event handling in Scala Swing uses a more declarative approach than Java Swing, utilizing partial functions and pattern matching.
  • Example: Adding a reaction to a button click.
    button.reactions += {
      case event.ButtonClicked(_) => println("Button was clicked!")
    }
    
  • Key Concepts:
    • Reactions: A collection where event handlers are registered.
    • Event Types: Different types of events that can be handled, such as ButtonClicked, WindowClosing, etc.







Layout Management

  • Overview: Scala Swing provides several layout managers to control the arrangement of components within containers.
  • Example: Using a BorderLayout.
    new BorderPanel {
      layout += new Button("North") -> BorderPanel.Position.North
      layout += new Button("South") -> BorderPanel.Position.South
    }
    
  • Explanation: Discuss how to use different layout managers and the importance of positioning components correctly within the GUI.







Swing Components

  • Overview: Beyond basic components, Scala Swing offers advanced components for more complex user interfaces, such as Tables, Trees, Menus, and Dialogs.
  • Example: Creating a menu bar.
    new MenuBar {
      contents += new Menu("File") {
        contents += new MenuItem(Action("Open") { println("Open clicked!") })
        contents += new MenuItem(Action("Exit") { sys.exit(0) })
      }
    }
    
  • Explanation: Introduce some advanced components and how they can be used to build more functional and interactive applications.







Best Practices and Tips

  • Design Considerations: Encourage thoughtful UI design, focusing on user experience, accessibility, and responsiveness.
  • Separation of Concerns: Advocate for separating the GUI layout and design from the application logic, possibly using the Model-View-Controller (MVC) pattern.
  • Performance Tips: Discuss the importance of minimizing unnecessary component updates and using listeners judiciously to maintain responsive and performant applications.







scala 3







Introduction to Indentation Syntax in Scala 3

  • Overview: Scala 3 introduces an indentation-based syntax as an alternative to the traditional braces-based syntax, influenced by languages like Python. This change aims to make code more concise and readable.
  • Key Concepts:
    • Significant Whitespace: Whitespace (indentation) is now used to denote block structure, eliminating the need for curly braces in many cases.
    • Compatibility: The indentation-based syntax is optional; Scala 3 codebases can use either or both styles, promoting gradual adoption and compatibility with Scala 2 code.







Basic Rules of Indentation Syntax

  • Indentation Blocks: An indentation block starts after a colon (:) that indicates a control structure (if, while, for), class, or method definition followed by a newline. The subsequent lines must be indented more than the line with the colon.

  • Example - Method Definition:

    def sum(a: Int, b: Int): Int =
      val total = a + b
      total
    
  • Example - Control Structures:

    if a > b then
      println("a is greater")
    else
      println("b is greater or equal")
    
  • Example - Loops:

    for 1 to 10 do
      println("a is greater")
    







Using Indentation for Classes and Objects

  • Classes and Objects: The indentation syntax also applies to class and object definitions, where members are defined within an indentation block.
  • Example:
    class Person(name: String, age: Int):
      def greet = println(s"Hi, my name is $name and I am $age years old.")
    
    object MyApp:
      def main(args: Array[String]): Unit =
        val person = new Person("Alice", 30)
        person.greet
    







Indentation with Match Expressions

  • Match Expressions: Scala 3 enhances match expressions with indentation, making them more readable and expressive.
  • Example:
    val result = x match
      case 1 => "one"
      case 2 => "two"
      case _ => "other"
    







Multiline Expressions and Indentation

  • Multiline Expressions: When writing expressions that span multiple lines, ensure that the continuation lines are indented to signal that the expression is not complete.
  • Example:
    val total = a +
      b +
      c
    







Best Practices

  • Indentation: The indentation is the preferred style
  • Consistency: Stick to one style (either indentation or braces) within a file to maintain readability.
  • Adoption: Start using indentation syntax in smaller, less complex code blocks to become familiar with the concept before applying it to larger codebases.
  • Tool Support: Use Scala 3 compatible tools and IDEs that understand and correctly format indentation-based syntax.







Transitioning from Scala 2 to Scala 3

  • Mixed Syntax: Scala 3 allows mixing both braces and indentation syntax, facilitating gradual migration from Scala 2 to Scala 3.
  • Migration Tools: Leverage Scala 3 migration tools and linters that can automatically convert braces to indentation, aiding in the transition process.







futures







Scala Futures

  • Overview: Scala Futures provide a way to perform asynchronous operations, enabling tasks to run concurrently without blocking the main execution flow. They represent a value that may become available at some point in the future.
  • Importing Futures:
    import scala.concurrent.Future
    import scala.concurrent.ExecutionContext.Implicits.global
    
  • Creating a Future: A Future is initialized by passing a block of code that executes asynchronously.
    val myFuture: Future[Int] = Future {
      // Code that returns an Int after some computation
      Thread.sleep(1000) // Simulating a long computation
      42
    }
    







Future Results

  • Callbacks: Futures provide methods like onComplete, onSuccess, and onFailure to handle their results asynchronously.
    myFuture.onComplete {
      case Success(value) => println(s"The result is $value")
      case Failure(exception) => println(s"Failed with $exception")
    }
    
  • Awaiting Results: While generally discouraged, Scala allows blocking until a Future is completed using Await.result.
    import scala.concurrent.Await
    import scala.concurrent.duration._
    
    val result = Await.result(myFuture, 10.seconds)
    







Composing Futures

  • Sequencing Operations: Use methods like map and flatMap to chain multiple asynchronous operations without blocking.
    val anotherFuture: Future[String] = myFuture.map(_.toString)
    
  • Parallel Execution: Use Future.sequence to transform a sequence of Futures into a Future that contains a sequence of results.
    val futureList: List[Future[Int]] = List(Future(1), Future(2), Future(3))
    val combinedFuture: Future[List[Int]] = Future.sequence(futureList)
    







Error Handling

  • Recovering from Failures: Futures provide recover and recoverWith methods to handle exceptions and recover from failures.
    val recoveredFuture: Future[Int] = myFuture.recover {
      case _: ArithmeticException => 0 // Return a default value in case of failure
    }
    







Advanced Techniques

  • For Comprehensions: Use for comprehensions for more readable code when working with multiple Futures.
    val futureA: Future[Int] = Future(10)
    val futureB: Future[Int] = Future(20)
    
    val resultFuture: Future[Int] = for {
      a <- futureA
      b <- futureB
    } yield a + b
    
  • Timeouts: Implementing timeouts for Futures can prevent indefinitely waiting for a result.
    import scala.concurrent.duration._
    import scala.concurrent.Future
    import scala.util.{Failure, Success}
    import scala.concurrent.ExecutionContext.Implicits.global
    
    val futureWithTimeout = Future.firstCompletedOf(Seq(myFuture, Future {
      Thread.sleep(5000) // 5 seconds timeout
      throw new TimeoutException("Operation timed out")
    }))
    
    futureWithTimeout.onComplete {
      case Success(value) => println(s"Completed with value: $value")
      case Failure(exception) => println(s"Completed with exception: $exception")
    }
    







Best Practices

  • ExecutionContext: Be conscious of the ExecutionContext used for executing futures. It's responsible for running the computations. The global context is a good default, but specific contexts might be needed for IO-intensive or CPU-bound tasks.
  • Avoid Blocking: Maximize the benefits of Futures by avoiding blocking operations. Use non-blocking techniques like callbacks and for comprehensions.
  • Error Handling: Utilize recover and recoverWith for robust error handling in asynchronous code paths.







Common Use Cases

  • Web Services Calls: Making non-blocking calls to web services, allowing other operations to continue while waiting for responses.
  • Database Operations: Performing database operations asynchronously to improve application responsiveness.
  • CPU-bound Tasks: Distributing computationally intensive tasks across multiple cores or nodes without blocking the main application flow.







repl







Scala REPL

  • Overview: The Scala REPL is an interactive command-line interface for Scala. It reads Scala expressions, evaluates them, and prints the results, making it a useful tool for experimentation and learning.
  • Starting the REPL: Typically, you start the REPL by typing scala in your command line after Scala has been installed on your system. This launches an interactive session where you can type and evaluate Scala expressions.
  • Basic Usage: In the REPL, you can type Scala expressions or definitions, and they will be immediately compiled and executed. The REPL provides feedback on the types and values of the expressions.







Basic Operations

  • Evaluating Expressions: Type any valid Scala expression, and the REPL will evaluate it, showing the result and type.
    scala> 1 + 1
    res0: Int = 2
    
  • Defining Variables and Functions: You can define variables and functions directly in the REPL.
    scala> val x = 5
    x: Int = 5
    
    scala> def addOne(a: Int) = a + 1
    addOne: (a: Int)Int
    
  • Loading External Libraries: Use :require or :load to include external Scala files or libraries in your REPL session for testing or exploration.





Advanced Features

  • Tab Completion: The REPL supports tab completion, which can help you find method names, variable names, and more.
  • Inspecting Types: Use :type to inspect the type of an expression without evaluating it.
    scala> :type addOne
    Int => Int
    
  • Viewing Imported Names: Use :imports to see a list of all imported names in the current session.







Customizing Scala REPL

  • Custom Initialization: You can customize the REPL environment through a .scala file in your home directory named .scalarc that contains Scala code to be executed at startup.
  • REPL Command History: The REPL keeps a history of commands. You can navigate through this history using the up and down arrow keys, and search through it using Ctrl+R.







Integrating with IDEs

  • IDE Support: Many Integrated Development Environments (IDEs) offer integrated REPL tools, providing a seamless development experience by combining the benefits of the REPL with the features of the IDE.
  • Ammonite REPL: An alternative to the Scala REPL, Ammonite offers additional features like better syntax highlighting, multi-line editing, and more powerful integration capabilities.

#





Best Practices

  • Experimentation and Learning: Use the REPL as a playground for learning new Scala features, experimenting with language constructs, or testing library functions.
  • Rapid Prototyping: Quickly prototype functions and algorithms in the REPL before integrating them into a larger Scala project.
  • Debugging and Testing: Test small pieces of code or expressions related to a larger problem you're debugging.







Limitations

  • Performance: The REPL is not optimized for performance testing. The evaluation time in the REPL may not accurately reflect the performance of compiled Scala code.
  • State Management: In complex sessions, managing the state and imports can become cumbersome. Restarting the REPL session can sometimes be necessary to clear the state.





tools







Build Tools

  • sbt (Scala Build Tool):

    • Overview: sbt is the de facto build tool for Scala projects, offering powerful features for compiling, running, and testing Scala code. It supports incremental compilation and integrates well with continuous integration systems.
    • Key Features: Dependency management, multi-project builds, interactive shell, custom tasks and plugins.
    • Website: https://www.scala-sbt.org/
  • Mill:

    • Overview: Mill is a newer build tool aimed at simplicity and performance, offering a more straightforward configuration than sbt.
    • Key Features: Simple configuration syntax, incremental compilation, and integration with common Scala tools and libraries.
    • Website: https://com-lihaoyi.github.io/mill/





Integrated Development Environments (IDEs)

  • IntelliJ IDEA:

    • Overview: IntelliJ IDEA by JetBrains offers robust Scala support through the Scala plugin, providing an integrated environment for Scala development, including sbt and Play Framework support.
    • Key Features: Intelligent code completion, on-the-fly code analysis, refactoring tools, and debugging support.
    • Website: https://www.jetbrains.com/idea/
  • Visual Studio Code with Metals:

    • Overview: Visual Studio Code (VS Code), combined with the Metals plugin, provides a lightweight yet powerful Scala development experience.
    • Key Features: Code completion, diagnostics, goto definition, code formatting, debugging, and integrated terminal.
    • Metals Website: https://scalameta.org/metals/







Testing Frameworks

  • ScalaTest:

    • Overview: ScalaTest is a flexible testing framework that supports different styles of testing, making it adaptable to various testing needs.
    • Key Features: Support for TDD, BDD, and integration testing; easily integrates with sbt and IDEs.
    • Website: https://www.scalatest.org/
  • Specs2:

    • Overview: Specs2 is a library for writing executable software specifications. With its BDD approach, it's particularly well-suited for unit and acceptance testing.
    • Key Features: Support for matchers, mocking, and specification structuring; integrates with sbt and IDEs.
    • Website: https://specs2.org/







Linters and Code Formatting Tools

  • Scalafmt:

    • Overview: Scalafmt is a code formatter for Scala, ensuring consistent code style across your project and team.
    • Key Features: Customizable formatting rules, IDE and build tool integration.
    • Website: https://scalameta.org/scalafmt/
  • Scalafix:

    • Overview: Scalafix is a refactoring and linting tool for Scala, helping to enforce best practices and migrate codebases to new Scala versions.
    • Key Features: Custom linting rules, automatic code fixes, and migration aids for upgrading Scala versions.
    • Website: https://scalacenter.github.io/scalafix/







Performance

  • Kamon:
    • Overview: Kamon provides instrumentation for monitoring applications built with Scala, Akka, and Play Framework, offering insights into application performance and bottlenecks.
    • Key Features: Metrics collection, distributed tracing, and context propagation.
    • Website: https://kamon.io/