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

Scala 3 Solutions

Solutions to the exercises in the Workbook

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

Which of the following variable names are valid?

  • 3 this_is_a_number
  • 5 A1
  • 7 function

Exercise

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

    val pi: Double = 3.14159
    
  2. Create a val called message and assign it a string value:

    val message: String = "Hello, world!"
    
  3. Create a val called age and assign it an integer value:

    val age: Int = 30
    
  4. Create a val called isTall and assign it a boolean value:

    val isTall: Boolean = true
    
  5. 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

Exercise 1

  1. Create number a = 10
  2. Print the number.

Exercise 2

  1. Multiply the number by 2.
  2. Print the result.

Exercise 3

  1. Create a second number b = 5
  2. Multiply a and b
  3. Print the result.

Exercise 4

The Celsius Fahrenheit converter.

  1. Create a variable temperature as Double for the temperature in Celsius.
  2. And print the temperature in Fahrenheit.
  3. Look on the internet for the formula.
  4. Use 0.0, 37.0 -40.0 as test examples

Exercise 5

Create a 'kilometre to miles' converter.

Choices

Exercise 1: Even Odd

val x: Int = 10
val result = 
if x % 2 == 0 then "Even"
else "Odd"

println(result)

Exercise 2: Larger

val x: Int = 10
val y: Int = 20

val larger: Int = if x > y then x else y
println(s"The larger number is $larger.")

Exercise 3: Maximum

val x: Int = 10
val y: Int = 20
val z: Int = 15

val largest: Int = if x > y && x > z  then
  x
else if y > z then 
  y
else 
  z

println(s"The largest number is $largest.")

Exercise 4: Vowel or Consonant

val ch: Char = 'a'

val result: String = ch match 
  case 'a' | 'e' | 'i' | 'o' | 'u' => "Vowel"
  case _ => "Consonant"

println(result)

Exercise 5: Weekend


val day: String = "Saturday"

val result: String = day match 
  case "Saturday" | "Sunday" => "Weekend"
  case _ => "Weekday"

println(result)

Exercise 6: Number name

val x: Int = 4

val result: String = x match 
  case 1 => "one"
  case 2 => "two"
  case 3 => "three"
  case 4 => "four"
  case 5 => "five"
  case 6 => "six"
  case 7 => "seven"
  case 8 => "eight"
  case 9 => "nine"
  case _ => "invalid"

println(result)

Loops

Exercise 1 integers

val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val squares = for n <- numbers yield n * n
println(squares)

Exercise 2 string lengths


val strings = List("apple", "banana", "cherry", "date", "elderberry")
val lengths = for s <- strings yield s.length
println(lengths)

Exercise 3 evens



val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evens = for n <- numbers if n % 2 == 0 yield n
println(evens)

Exercise 4 tuples

val people = List(("Alice", 25), ("Bob", 32), ("Charlie", 19), ("David", 42))
val youngNames = for 
    (name, age) <- people if age < 30 yield name
println(youngNames)

Exercise 5 string contains

val strings = List("apple", "banana", "cherry", "date", "elderberry")
val withA = for s <- strings if s.contains("a") yield s
println(withA)

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.

Exercises

Exercise 1 length

  1. Create a String "Hello, world"
  2. Print the length of the text.
  3. Split the text in words.
  4. Count the number of words.

Exercise 2 concat

  1. Create two Strings "Hello, " and "World"
  2. Concatenate the two Strings
  3. Merge the two String with s-interpolated String

Functions

Exercise 1 contains

def findStringsContainingA(strings: List[String]): List[String] = 
  strings.filter(str => str.contains('a'))

// example usage
findStringsContainingA(List("apple", "banana", "orange", "pear")) // returns List("apple", "banana", "orange")

Exercise 2 even numbers

def filterEvenNumbers(numbers: List[Int]): List[Int] = 
  numbers.filter(num => num % 2 == 0)

// example usage
filterEvenNumbers(List(1, 2, 3, 4, 5, 6)) //  List(2, 4, 6)

Exercise 3 string lengths

def stringLengths(strings: List[String]): List[Int] = 
  strings.map(str => str.length)

// example usage
stringLengths(List("apple", "banana", "orange", "pear")) //  List(5, 6, 6, 4)

Exercise 4: Default Parameters

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

greet("Scala") // Should print "Hello, Scala!"
greet("World", "Hi") // Should print "Hi, World!"

Exercise 5: Named Arguments

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

describePerson(age = 25, name = "Alice") // Should print "Alice is 25 years old from unknown."

Exercise 6: Varargs

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

println(sum(1, 2, 3, 4)) // Should print 10
println(sum()) // Should print 0

Exercise 7: Anonymous Functions and Map

Task: Given a list of integers, use an anonymous function to increment each element by 1, using the map method.

val numbers = List(1, 2, 3, 4)
val incrementedNumbers = numbers.map(n => n + 1)

println(incrementedNumbers) // Should print List(2, 3, 4, 5)

Tuples

Exercise 1: Creating and Accessing Tuples

// Create the tuple
val book = ("The Hobbit", 1937, false)

// Access and print the elements
println(s"Title: ${book._1}")
println(s"Year: ${book._2}")
println(s"Read: ${book._3}")

Exercise 2: Tuple Destructuring

val person = ("John", "Doe", 30)

// Destructure the tuple
val (firstName, lastName, age) = person

// Print formatted string
println(s"$firstName $lastName is $age years old.")

Task: Write a function that takes two numbers as parameters, returns a tuple containing the sum and product of the two numbers.

def calculateSumAndProduct(a: Int, b: Int): (Int, Int) = {
  (a + b, a * b)
}

// Test the function
val result = calculateSumAndProduct(5, 10)
println(s"Sum: ${result._1}, Product: ${result._2}")

Task: Given a list of tuples where each tuple contains the name of a fruit and its quantity, write a code snippet that prints the name of each fruit and its quantity in a formatted string.

val fruits = List(("Apple", 10), ("Banana", 5), ("Cherry", 20))

// Iterate and print
fruits.foreach {
  case (fruit, quantity) =>
    println(s"There are $quantity $fruit(s).")
}

spring-boot

Solutions

Solution 1 Begin

@main
def read(): Unit = 
  val scanner = new Scanner(System.in)
  print("read: ")
  val s = scanner.nextLine
  println("write: " + s)

Solution 2 Welcome

@main
def read(): Unit =
  val scanner = new Scanner(System.in)
  print("your name: ")
  val s = scanner.nextLine
  println("Welcome " + s)

Solution 3 Add

@main
def sum(): Unit =
  val scanner = new Scanner(System.in)
  
  print("number1: ")
  val number1 = scanner.nextInt
  
  print("number2: ")
  val number2 = scanner.nextInt

  val sum = number1 + number2
  println("sum: " + sum)

Solution 4 Menu

@main
def menu(): Unit =
  println("Menu choices")
  println("1 - hello")
  println("2 - bye")
  print("Your choice: ")
  
  val scanner = new Scanner(System.in)
  val menu = scanner.nextInt
  
  if menu == 1 then
    println("hello")
  else if menu == 2 then
    println("bye")
  else println("wrong number")

Solution 5 Calculator

@main
def calculator(): Unit = 
  val scanner = new Scanner(System.in)

  print("number1: ")
  val number1 = scanner.nextInt
  scanner.nextLine

  print("number2: ")
  val number2 = scanner.nextInt
  scanner.nextLine

  print("operator: ")
  val operator = scanner.nextLine

  val result: Int = operator match {
    case "+" => number1 + number2
    case "-" => number1 - number2
    case "*" => number1 * number2
    case "/" => number1 / number2
  }
  println("result: " + result)

Solution 6 Drawing

@main
def drawing(): Unit = 
  //  1 to 4 foreach { x =>
  //    println(x)
  //  }
  //
  //  for (x <- 1 to 4) {
  //    println(x)
  //  }

  for x <- 1 to 4 do
    for y <- 1 to x do
      print(" *")
    println

  for x <- 1 to 4 do
    for y <- 1 until x do
      print("  ")
      println(" *")

Solution 7 List

@main
def lists(): Unit = 
  val array = Array(1, 2, 3, 4, 5, 6, 7, 8)

  for x <- array do
    println(x)

  for x <- array.reverse do
    println(x)

  println(array.sum)

  println(array.sum.toDouble / array.length)

Solution 8 String


Solution 9 Function

@main
def functions(): Unit = 

  def add(a: Int, b: Int) = a + b
  println(add(3, 4))

  def log(text: String) = 
    println(s"${new Date()} : $text")

  println(log("the message"))

Project: Numberguess

with if..else

@main
def numberguessnew(): Unit =
  val random = Random.nextInt(100)
  val scanner = new Scanner(System.in)

  var next = true
  while next do
    print("guess the number: ")
    val number = scanner.nextInt
  
    if number < random then
      println("greater")
    else if number > random then
      println("smaller")
    else if number == random then
      println("found")
      next = false

with match

@main
def numberguessnewer(): Unit = 
  val random = Random.nextInt(100)
  val scanner = new Scanner(System.in)
  
  var next = true
  while next do
    print("guess the number: ")
    val number = scanner.nextInt

    val result = number match
      case gt if gt < random => "greater"
      case lt if lt > random => "smaller"
      case eq if eq == random => "found"

    println(result)
    if result == "found" then 
      next = false

project

Project: Calculator


import scala.swing.*
import scala.swing.event.*

@main
def calculator(): Unit = {
  var display = ""

  new MainFrame() {
    title = "Calculator"

    val label = new Label()
    label.preferredSize = new Dimension(200, 30)
    label.xAlignment = Alignment.Left
    label.font = Font("Arial", Font.Plain, 18)
    
    val buttonGrid = new GridPanel(4, 4)

    val labels = List(
      "7", "8", "9", "/",
      "4", "5", "6", "*",
      "1", "2", "3", "+",
      "C", "0", "=", "-")

    val buttons = labels.foreach(l => {
      val b = new Button(l)
      b.reactions.+= {
        case ButtonClicked(e) => click(e)
      }
      buttonGrid.contents += b
    }
    )
    contents = new BorderPanel {
      add(label, BorderPanel.Position.North)
      add(buttonGrid, BorderPanel.Position.Center)
    }

    var num1 = 0
    var operator = ""

    def click(but: AbstractButton): Unit = {
      println(but.text)

      but.text match {
        case "7" | "8" | "9" | "4" | "5" | "6" | "1" | "2" | "3" | "0" =>
          println("number")
          label.text += but.text

        case "*" | "/" | "+" | "-" =>
          println("operator")
          operator = but.text
          num1 = label.text.toInt
          label.text = ""

        case "=" => calculate()

        case _ => println("error")
      }
    }

    def calculate(): Unit = {
      val num2 = label.text.toInt
      val result = operator match {
        case "+" => num1 + num2
        case "-" => num1 - num2
        case "*" => num1 * num2
        case "/" => num1 / num2
      }
      label.text = result.toString
    }

    size = new Dimension(300, 300)
    centerOnScreen()
    open()
  }
}

Answers

Question 1: Variable Declarations

Correct Answer: C) 40

Question 2: Function Definition

Correct Answer: C) Multiplies two numbers

Question 3: Immutable List Operations

Correct Answer: A) List(2, 4, 6, 8, 10)

Question 4: Pattern Matching

Correct Answer: A) World

Question 5: Variable Mutability

Correct Answer: B) var

Question 6: Scala's Type Inference

Correct Answer: C) Omit the type of a variable when it is declared

Question 7: Function Declarations

Correct Answer: A) def sum(x: Int, y: Int): Int = { return x + y }

Question 8: Using a while Loop

Correct Answer: B) 15

Question 9: Iterating with for Loop

Correct Answer: B) 24

Question 10: Nested for Loops

Correct Answer: A) (1,1) (1,2)
(2,1) (2,2)
(3,1) (3,2)

Question 11: Basic if-else Logic

Correct Answer: A) You can vote.
Can vote: true

Question 12: Nested if-else with Logical Operators

Correct Answer: A) You can drive.

Question 13: if-else with Compound Conditions

Correct Answer: A) It's a good day for a walk.

Question 14: Complex if-else with Function Calls

Correct Answer: C) Not eligible: Too young

Question 15: Basic Pattern Matching

Correct Answer: C) Three

Question 16: Defining and Calling a Simple Function

Correct Answer: B) Hello, Alice!

Object Oriented

Exercises

Class

  1. Write a class Person with a constructor fields name and a method sayHello
  2. Instantiate the Person and call the sayHello method

Companion Object

  1. Write an object Person with a method apply
  2. Instantiate a Person with the companion object

Case Class

  1. Write a case class Person with a constructor field name and a method sayHello
  2. Instantiate the Person (with the generated companion object)

Inheritance

  1. Write a class Customer with a constructor field name that extends Person
  2. Instantiate the Customer and call the sayHello method

Trait

  1. Write a trait Greeter with a sayHello method
  2. Use the Greeter on the Customer and call the sayHello method

Case Class

Exercise 1: Defining and Instantiating Case Classes

case class Book(title: String, authors: List[String])

val book1 = Book("Scala Programming", List("Martin Odersky"))
val book2 = Book("Programming in Scala", List("Martin Odersky", "Lex Spoon", "Bill Venners"))
val book3 = Book("Functional Programming in Scala", List("Paul Chiusano", "Rúnar Bjarnason"))

Exercise 2: Pattern Matching on Case Classes

def describeBook(book: Book): String = book match {
  case Book(title, authors) if authors.length > 1 => s"$title, written by multiple authors."
  case Book(title, authors) => s"$title, written by ${authors.head}."
}

println(describeBook(book1)) // Output: Scala Programming, written by Martin Odersky.
println(describeBook(book2)) // Output: Programming in Scala, written by multiple authors.

Exercise 3: Copying and Modifying Case Classes

val book1Updated = book1.copy(authors = book1.authors :+ "Venners Bill")
println(book1Updated)

Exercise 4: Case Classes in Collections

val books = List(book1, book2, book3)

def titlesByAuthor(author: String, books: List[Book]): List[String] =
  books.filter(_.authors.contains(author)).map(_.title)

println(titlesByAuthor("Martin Odersky", books)) // Output: List(Scala Programming, Programming in Scala)

Pattern Matching

Solutions

Exercise 1: Basic Case Class and Pattern Matching

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

def greet(person: Person): String =
  person match
    case Person(_, age) if age < 18 => "Hello, young one!"
    case Person(name, _) => s"Hello, $name!"

// Test your function
val child = Person("Tim", 10)
val adult = Person("John", 30)

println(greet(child))  // Output: Hello, young one!
println(greet(adult))  // Output: Hello, John!

Exercise 2: Using Case Classes in Collections

def minors(people: List[Person]): List[String] =
  people.collect { case Person(name, age) if age < 18 => name }

// Test your function
val people = List(Person("Alice", 17), Person("Bob", 20), Person("Charlie", 15))
println(minors(people))  // Output: List(Alice, Charlie)

Enum

Exercise 1: Basic Enum

enum DayOfWeek:
  case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday

def isWeekend(day: DayOfWeek): Boolean = day match
  case DayOfWeek.Saturday | DayOfWeek.Sunday => true
  case _ => false

// Test your function
println(isWeekend(DayOfWeek.Saturday)) // Output: true
println(isWeekend(DayOfWeek.Wednesday)) // Output: false

Exercise 2: Enum with Parameters

enum TrafficLight(val color: String):
  case Red extends TrafficLight("red")
  case Yellow extends TrafficLight("yellow")
  case Green extends TrafficLight("green")

// Test
println(TrafficLight.Red.color) // Output: red

Exercise 3: Enums in Collections

def countLights(lights: List[TrafficLight]): Unit =
  val counts = lights.groupBy(identity).view.mapValues(_.size).toMap
  println(counts)

// Test your function
val lights = List(TrafficLight.Red, TrafficLight.Yellow, TrafficLight.Red, TrafficLight.Green, TrafficLight.Yellow)
countLights(lights) // Output: Map(Red -> 2, Yellow -> 2, Green -> 1)

Exercises

Exercise 1A

Create a Car class with the fields: mark and color. Give the Car the behavoir drive() and brake() with the state speed. Create a @main function Instantiate a Auto as a red Tesla as myCar. Let myCar drive and brake and print the speed.

class Car:
  var mark: String = ""
  var color: String = ""
  var speed = 0

  def drive() = speed += 10
  def brake() = speed -= 10
@main
def main(): Unit =
  val myCar = new Car()
  myCar.mark = "Tesla"
  myCar.color = "red"
  myCar.drive()
  println(s"speed: ${myCar.speed}")

Exercise 1B

Create hisCar a gray Suzuki.
Let it drive three times and print the speed

Exercise 2

Create a constructor on the fields: mark and color
Change the main function to use the constructor

Exercise 3

Make the Car immutable by changing var by val
Change de main function if needed.

exercise 4

Make the speed private. Generate the toString method Change de main function .

exercise 5

Create a new class Truck, inherited from the Car class.
Give the Truck an extra field freight.
Give the Truck a constructor met de fields mark and color
In de main function create a blue Volvo with the variable name: truck1 En give truck1 a freight of 1000.

Exercise 6

Override the function drive() and brake() with a slower drive and brake speed

Exercise 7

Create RaceAuto inherited from Car. Give it a field topSpeed
Instantiate a red Ferrari as secondCar

exercise 8

Create a trait Vehicle met de functions: drive() and brake() make the Car clas implementing this trait.

Exercise 9

Create a Bicycle class from the Vehicle trait.
Instantiate a gray VanMook as myBike

Project: Todo App

Step 1: Set Up Your Scala Project

First, ensure you have Scala and SBT (Scala Build Tool) installed on your machine. You can check by running the following commands:

scala -version
sbt -version

If you don't have them installed, follow the installation instructions from the official Scala website.

Next, in IntelliJ create a new SBT project

Step 2: Define the To-Do App Structure

Create a Scala object to hold your application logic. You can do this by creating a new file in the src/main/scala directory. Let's call it TodoApp.scala.

import scala.io.StdIn.readLine
import scala.collection.mutable.ListBuffer

object TodoApp:

  case class Task(id: Int, description: String)

  val tasks: ListBuffer[Task] = ListBuffer.empty
  var nextId: Int = 1

  def main(args: Array[String]): Unit = 
    var continue = true

    while (continue) 
      println("\nTODO App")
      println("1. Add Task")
      println("2. List Tasks")
      println("3. Delete Task")
      println("4. Exit")
      print("Choose an option: ")

      readLine() match {
        case "1" => addTask()
        case "2" => listTasks()
        case "3" => deleteTask()
        case "4" => continue = false
        case _ => println("Invalid option. Please try again.")


  def addTask(): Unit = 
    print("Enter task description: ")
    val description = readLine()
    tasks += Task(nextId, description)
    nextId += 1
    println(s"Task added with id $nextId")
  

  def listTasks(): Unit = 
    if tasks.isEmpty {
      println("No tasks available.")
    } else {
      tasks.foreach(task => println(s"${task.id}. ${task.description}"))
    }

  def deleteTask(): Unit = 
    print("Enter task id to delete: ")
    val id = readLine().toInt
    val taskIndex = tasks.indexWhere(_.id == id)
    if taskIndex != -1 {
      tasks.remove(taskIndex)
      println(s"Task with id $id deleted.")
    } else {
      println(s"Task with id $id not found.")
    }
  
}

Step 3: Running Your Application

To run your application, use SBT:

sbt run

This command compiles and runs your Scala application. You should see the menu and be able to interact with your to-do list by adding, listing, and deleting tasks.

Explanation

  1. Task Case Class: This defines a simple structure to hold task data.
  2. tasks ListBuffer: A mutable list to hold the tasks.
  3. nextId: A counter to assign unique IDs to tasks.
  4. main Method: This is the entry point of the application. It shows the menu and reads user input.
  5. addTask, listTasks, deleteTask Methods: These methods handle adding, listing, and deleting tasks respectively.

Add Priority

To add a priority to each task and sort the list based on priority, we need to modify the Task case class and the listTasks method. We will also adjust the addTask method to accept priority input from the user.

Here's the updated code for the TodoApp:

import scala.io.StdIn.readLine
import scala.collection.mutable.ListBuffer

object TodoApp:

  case class Task(id: Int, description: String, priority: Int)

  val tasks: ListBuffer[Task] = ListBuffer.empty
  var nextId: Int = 1

  def main(args: Array[String]): Unit = 
    var continue = true

    while (continue) 
      println("\nTODO App")
      println("1. Add Task")
      println("2. List Tasks")
      println("3. Delete Task")
      println("4. Exit")
      print("Choose an option: ")

      readLine() match 
        case "1" => addTask()
        case "2" => listTasks()
        case "3" => deleteTask()
        case "4" => continue = false
        case _ => println("Invalid option. Please try again.")

  def addTask(): Unit = 
    print("Enter task description: ")
    val description = readLine()
    print("Enter task priority (1=High, 2=Medium, 3=Low): ")
    val priority = readLine().toInt

    tasks += Task(nextId, description, priority)
    nextId += 1
    println(s"Task added with id $nextId and priority $priority")
  

  def listTasks(): Unit = 
    if tasks.isEmpty {
      println("No tasks available.")
    } else {
      println("Tasks (sorted by priority):")
      val sortedTasks = tasks.sortBy(_.priority)
      sortedTasks.foreach(task => println(s"${task.id}. [Priority: ${task.priority}] ${task.description}"))
    }

  def deleteTask(): Unit = 
    print("Enter task id to delete: ")
    val id = readLine().toInt
    val taskIndex = tasks.indexWhere(_.id == id)
    if taskIndex != -1 {
      tasks.remove(taskIndex)
      println(s"Task with id $id deleted.")
    } else {
      println(s"Task with id $id not found.")
    }

Explanation of Changes

  1. Task Case Class: Added a new field priority to the Task case class.
  2. addTask Method: Now asks the user to input a priority level for the task.
  3. listTasks Method: Sorts tasks by their priority before printing. Lower numbers represent higher priority (1=High, 2=Medium, 3=Low).

Add User

To add a User case class and allow associating tasks with specific users, we need to make some adjustments to our application. We'll introduce a user management system where users can be added, and tasks can be associated with users. This will include methods to add users, list users, and associate tasks with users.

Here is the updated code:

import scala.io.StdIn.readLine
import scala.collection.mutable.{ListBuffer, Map}

object TodoApp:

  case class Task(id: Int, description: String, priority: Int, userId: Int)
  case class User(id: Int, name: String)
  
  val tasks: ListBuffer[Task] = ListBuffer.empty
  val users: ListBuffer[User] = ListBuffer.empty
  var nextTaskId: Int = 1
  var nextUserId: Int = 1

  def main(args: Array[String]): Unit =
    var continue = true
    
    while (continue)
      println("\nTODO App")
      println("1. Add User")
      println("2. List Users")
      println("3. Add Task")
      println("4. List Tasks")
      println("5. Delete Task")
      println("6. Exit")
      print("Choose an option: ")
    
    readLine() match
      case "1" => addUser()
      case "2" => listUsers()
      case "3" => addTask()
      case "4" => listTasks()
      case "5" => deleteTask()
      case "6" => continue = false
      case _ => println("Invalid option. Please try again.")
  
  def addUser(): Unit =
    print("Enter user name: ")
    val name = readLine()
    users += User(nextUserId, name)
    println(s"User added with id $nextUserId and name $name")
    nextUserId += 1
  
  
  def listUsers(): Unit =
    if users.isEmpty then
      println("No users available.")
    else
      println("Users:")
    users.foreach(user => println(s"${user.id}. ${user.name}"))
  
  def addTask(): Unit =
    print("Enter task description: ")
    val description = readLine()
    print("Enter task priority (1=High, 2=Medium, 3=Low): ")
    val priority = readLine().toInt
    print("Enter user id: ")
    val userId = readLine().toInt
    
    if users.exists(_.id == userId) then
      tasks += Task(nextTaskId, description, priority, userId)
      println(s"Task added with id $nextTaskId, priority $priority, assigned to user $userId")
      nextTaskId += 1
    else
      println(s"User with id $userId does not exist.")
  
  
  def listTasks(): Unit =
    if tasks.isEmpty then
      println("No tasks available.")
    else
      println("Tasks (sorted by priority):")
    val sortedTasks = tasks.sortBy(_.priority)
    sortedTasks.foreach(task => {
      val user = users.find(_.id == task.userId).map(_.name).getOrElse("Unknown User")
      println(s"${task.id}. [Priority: ${task.priority}] ${task.description} (Assigned to: $user)")
    })
  
  
  def deleteTask(): Unit =
    print("Enter task id to delete: ")
    val id = readLine().toInt
    val taskIndex = tasks.indexWhere(_.id == id)
    if taskIndex != -1 then
      tasks.remove(taskIndex)
      println(s"Task with id $id deleted.")
    else
      println(s"Task with id $id not found.")

Explanation of Changes

  1. User Case Class: Added a User case class with id and name fields.
  2. Task Case Class: Added a userId field to associate tasks with a specific user.
  3. users ListBuffer: A mutable list to hold the users.
  4. nextUserId: A counter to assign unique IDs to users.
  5. addUser Method: Allows adding a new user.
  6. listUsers Method: Lists all users.
  7. addTask Method: Now asks for a userId to assign the task to a specific user.
  8. listTasks Method: Displays tasks with associated user names.

Project: Scribble

Complete Solution

import scala.swing.*
import scala.swing.event.*
import java.awt.{Color, Graphics2D, Point, Rectangle, Shape}
import scala.collection.mutable.ListBuffer
import scala.swing.BorderPanel.Position.{Center, North}

object ScribbleApp extends SimpleSwingApplication:
  def top = new MainFrame:
    title = "Rectangle and Oval Scribble App"

    // Modes for drawing shapes
    sealed trait DrawMode
    case object Rectangle extends DrawMode
    case object Oval extends DrawMode

    var currentMode: DrawMode = Rectangle // Default mode

    // Canvas is a Panel where shapes are drawn
    object canvas extends Panel:
      background = Color.white
      preferredSize = new Dimension(400, 400)
      focusable = true
      listenTo(mouse.clicks, mouse.moves)

      private var startPoint: Option[Point] = None
      private val shapes: ListBuffer[Shape] = ListBuffer()

      // React to mouse events to draw shapes
      reactions += {
        case e: MousePressed =>
          startPoint = Some(e.point)

        case e: MouseReleased =>
          startPoint match
        case Some(start) =>
          val shape = currentMode match
        case Rectangle => new Rectangle(start.x, start.y, e.point.x - start.x, e.point.y - start.y)
        case Oval => new java.awt.geom.Ellipse2D.Double(start.x, start.y, e.point.x - start.x, e.point.y - start.y)
          shapes += shape
          repaint()
        case None =>
          startPoint = None
      }

      override def paintComponent(g: Graphics2D): Unit =
        super.paintComponent(g)
        g.setColor(Color.black)
        shapes.foreach:
          case rect: Rectangle => g.draw(rect)
          case oval: java.awt.geom.Ellipse2D.Double => g.draw(oval)
          case _ =>

    // Mode selection buttons
    val modePanel = new FlowPanel:
      val rectangleButton = new Button("Rectangle")
      val ovalButton = new Button("Oval")

      contents += rectangleButton
      contents += ovalButton

      listenTo(rectangleButton, ovalButton)

      reactions +=
        case ButtonClicked(`rectangleButton`) => currentMode = Rectangle
        case ButtonClicked(`ovalButton`) => currentMode = Oval

    contents = new BorderPanel:
      layout(modePanel) = North
      layout(canvas) = Center
    size = new Dimension(500, 500)

Exercises

Exercise 1: Basic List Operations

val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val firstElement = list.head          // Output: 1
val lastElement = list.last           // Output: 10
val allButFirst = list.tail           // Output: List(2, 3, 4, 5, 6, 7, 8, 9, 10)
val allButLast = list.init            // Output: List(1, 2, 3, 4, 5, 6, 7, 8, 9)
val containsFive = list.contains(5)   // Output: true

Exercise 2: Concatenation and Addition

val evenList = List(2, 4, 6, 8, 10)
val oddList = List(1, 3, 5, 7, 9)

val concatenatedList = evenList ++ oddList  // Output: List(2, 4, 6, 8, 10, 1, 3, 5, 7, 9)
val finalList = 0 :: concatenatedList       // Output: List(0, 2, 4, 6, 8, 10, 1, 3, 5, 7, 9)

Exercise 3: Mapping and Filtering

val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val multipliedList = list.map(_ * 2)        // Output: List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
val filteredList = multipliedList.filter(_ > 10)  // Output: List(12, 14, 16, 18, 20)

Exercise 4: Folding and Reducing

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

val sum = list.foldLeft(0)(_ + _)   // Output: 15
val product = list.reduce(_ * _)    // Output: 120

Exercise 5: Using ListBuffer

import scala.collection.mutable.ListBuffer

val buffer = ListBuffer(1, 2, 3)

buffer += 4         // ListBuffer(1, 2, 3, 4)
buffer -= 2         // ListBuffer(1, 3, 4)

val immutableList = buffer.toList  // Output: List(1, 3, 4)

Exercise 6: Working with Arrays

val array = Array(1, 2, 3, 4, 5)

array(2) = 10         // Array(1, 2, 10, 4, 5)

val length = array.length  // Output: 5

array.foreach(println)  // Output: 1 2 10 4 5

Exercise 7: Zipping and Unzipping

val numbers = List(1, 2, 3)
val words = List("one", "two", "three")

val zipped = numbers.zip(words)  // Output: List((1,"one"), (2,"two"), (3,"three"))

val (unzippedNumbers, unzippedWords) = zipped.unzip
// unzippedNumbers: List(1, 2, 3)
// unzippedWords: List("one", "two", "three")

Exercise 8: Using Range to Create Lists

val rangeList = Range(1, 11).toList            // Output: List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evenRangeList = Range(2, 21, 2).toList     // Output: List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
val reverseRangeList = Range(10, 0, -1).toList // Output: List(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

Exercise 9: LazyList

val lazyList = LazyList.from(1).take(10)  // Output: LazyList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val evenNumbers = LazyList.from(2, 2)  // Infinite LazyList of even numbers

val firstTenEvens = evenNumbers.take(10)
firstTenEvens.foreach(println)  // Output: 2 4 6 8 10 12 14 16 18 20

Exercise 10: Grouping and Partitioning

val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val grouped = list.groupBy(_ % 2)  // Output: Map(0 -> List(2, 4, 6, 8, 10), 1 -> List(1, 3, 5, 7, 9))

val (lessThanOrEqualFive, greaterThanFive) = list.partition(_ <= 5)
// lessThanOrEqualFive: List(1, 2, 3, 4, 5)
// greaterThanFive: List(6, 7, 8, 9, 10)

Exercises Bookstore filter map

The bookstore

case class Author(name: String, nationality: String)
case class Book(title: String, author: Author, year: Int, category: String, price: Double, description: Option[String])

val authors = List(
  Author("George Orwell", "British"),
  Author("Harper Lee", "American"),
  Author("F. Scott Fitzgerald", "American"),
  Author("Aldous Huxley", "British"),
  Author("Herman Melville", "American"),
  Author("J.D. Salinger", "American"),
  Author("Yuval Noah Harari", "Israeli")
)

val library = List(
  Book("1984", authors(0), 1949, "Dystopian", 15.99, Some("A dystopian social science fiction novel and cautionary tale.")),
  Book("To Kill a Mockingbird", authors(1), 1960, "Fiction", 10.99, Some("A novel about the serious issues of rape and racial inequality.")),
  Book("The Great Gatsby", authors(2), 1925, "Classic", 8.99, Some("A story of the mysteriously wealthy Jay Gatsby and his love for Daisy Buchanan.")),
  Book("Brave New World", authors(3), 1932, "Dystopian", 12.99, None),
  Book("Moby Dick", authors(4), 1851, "Classic", 9.99, Some("The narrative of Captain Ahab's obsessive quest to kill the giant white sperm whale Moby Dick.")),
  Book("The Catcher in the Rye", authors(5), 1951, "Fiction", 14.99, Some("A novel about teenage rebellion and alienation."))
)

Use Case 1: Finding Books by a Specific Author

library.filter(_.author.name == authorName)

Use Case 2: Filtering Books Based on Price

library.filter(book => book.price >= minPrice && book.price <= maxPrice)

Use Case 3: Filtering Books by Category

library.filter(_.category == category)

Use Case 4: Displaying Book Titles with Prices

  library.map(book => s"${book.title} - $${book.price}")

Use Case 5: Finding Books Published After a Certain Year

  library.filter(_.year > year)

Use Case 6: Creating a Summary Description for Books

  library.map(book => s"${book.title} by ${book.author.name} [${book.category}]")

Use Case 7: Finding Discounted Books

 library.map(book => (book.title, book.price * (1 - discountRate)))

Exercises Bookstore collect

Use Case 1: Collecting Books with Descriptions

 library.collect {
    case Book(title, _, _, _, _, Some(description)) => s"$title: $description"
  }

Use Case 2: Calculating Total Value of Books

  library.foldLeft(0.0)((total, book) => total + book.price)

Use Case 3: Finding the Oldest Book

  library.foldLeft(Option.empty[Book]) {
    case (None, book) => Some(book)
    case (Some(oldest), book) if book.year < oldest.year => Some(book)
    case (oldest, _) => oldest
  }

Use Case 4: Grouping Books by Decade

  library.groupBy(book => (book.year / 10) * 10)

Use Case 5: Counting Books by Category

  library.foldLeft(Map.empty[String, Int]) { (counts, book) =>
    counts.updated(book.category, counts.getOrElse(book.category, 0) + 1)
  }

Exercises Bookstore zip

Use Case 1: Creating Pairs of Book Titles and Prices with zip

val titles = library.map(_.title)
val prices = library.map(_.price)
titles.zip(prices)

Use Case 2: Creating Pairs of Book Titles and Their Indexes with zipWithIndex

  library.map(_.title).zipWithIndex

Use Case 3: Matching Authors to Their Books with zip

  val authors = library.map(_.author.name).distinct
  val booksByAuthor = authors.map { author =>
    val books = library.filter(_.author.name == author).map(_.title)
    (author, books)
  }
  booksByAuthor

Use Case 4: Pairing Book Titles with Publication Years with zip

  val titles = library.map(_.title)
  val years = library.map(_.year)
  titles.zip(years)

Use Case 5: Creating Pairs of Original and Discounted Prices with zip

  val originalPrices = library.map(_.price)
  val discountedPrices = originalPrices.map(price => price * (1 - discountRate))
  originalPrices.zip(discountedPrices)

Use Case 6: Creating a List of Book Titles with Their Indexes for Display Purposes with zipWithIndex

  library.map(_.title).zipWithIndex.map { case (title, index) => s"$index: $title" }

Exercises Bookstore GroupBy

Use Case 1: Grouping Books by Category

  library.groupBy(_.category)

Use Case 2: Grouping Books by Author

  library.groupBy(_.author.name)

Use Case 3: Grouping Books by Decade

  library.groupBy(book => (book.year / 10) * 10)

Use Case 4: Grouping Books by Price Range

  library.groupBy { book =>
    if (book.price < 10) "<$10"
    else if (book.price <= 20) "$10-$20"
    else ">$20"
  }

Use Case 5: Grouping Books by Availability of Description

  library.groupBy { book =>
    if (book.description.isDefined) "With Description"
    else "Without Description"
  }

Exercises Bookstore more

Use Case 1: Partition Books by Availability of Description

  library.partition(_.description.isDefined)

Use Case 2: Find the Most Expensive Book

  library.sortBy(-_.price).headOption

Use Case 3: Get Distinct Authors

  library.map(_.author).distinct

Use Case 4: Flatten Nested List of Book Lists

  bookLists.flatten

Use Case 5: Extract Titles of Books by a Specific Author Using flatMap

  library.flatMap(book => if (book.author.name == authorName) Some(book.title) else None)

Use Case 6: Calculate Total Price of Books Using foldLeft

  library.foldLeft(0.0)((total, book) => total + book.price)

Use Case 7: Combine Books and Authors into a Map

  library.groupBy(_.author)

Project: Bookstore

import scala.io.StdIn.readLine
import scala.collection.mutable.ListBuffer

object BookstoreApp {

  case class Book(id: Int, title: String, author: String, price: Double)
  case class Customer(id: Int, name: String, email: String)
  case class Order(id: Int, customerId: Int, bookId: Int)

  val books: ListBuffer[Book] = ListBuffer.empty
  val customers: ListBuffer[Customer] = ListBuffer.empty
  val orders: ListBuffer[Order] = ListBuffer.empty

  var nextBookId: Int = 1
  var nextCustomerId: Int = 1
  var nextOrderId: Int = 1

  def main(args: Array[String]): 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. Exit")
      print("Choose an option: ")

      readLine() match {
        case "1" => addBook()
        case "2" => listBooks()
        case "3" => addCustomer()
        case "4" => listCustomers()
        case "5" => placeOrder()
        case "6" => listOrders()
        case "7" => continue = false
        case _ => println("Invalid option. Please try again.")
      }
    }
  }

  def addBook(): Unit = {
    print("Enter book title: ")
    val title = readLine()
    print("Enter book author: ")
    val author = readLine()
    print("Enter book price: ")
    val price = readLine().toDouble

    books += Book(nextBookId, title, author, price)
    println(s"Book added with id $nextBookId: $title by $author at $$${price}")
    nextBookId += 1
  }

  def listBooks(): Unit = {
    if (books.isEmpty) {
      println("No books available.")
    } else {
      println("Books:")
      books.foreach(book => println(s"${book.id}. ${book.title} by ${book.author} - $$${book.price}"))
    }
  }

  def addCustomer(): Unit = {
    print("Enter customer name: ")
    val name = readLine()
    print("Enter customer email: ")
    val email = readLine()

    customers += Customer(nextCustomerId, name, email)
    println(s"Customer added with id $nextCustomerId: $name, $email")
    nextCustomerId += 1
  }

  def listCustomers(): Unit = {
    if (customers.isEmpty) {
      println("No customers available.")
    } else {
      println("Customers:")
      customers.foreach(customer => println(s"${customer.id}. ${customer.name} (${customer.email})"))
    }
  }

  def placeOrder(): Unit = {
    print("Enter customer id: ")
    val customerId = readLine().toInt
    print("Enter book id: ")
    val bookId = readLine().toInt

    if (customers.exists(_.id == customerId) && books.exists(_.id == bookId)) {
      orders += Order(nextOrderId, customerId, bookId)
      println(s"Order placed with id $nextOrderId: Customer $customerId ordered Book $bookId")
      nextOrderId += 1
    } else {
      println("Invalid customer id or book id.")
    }
  }

  def listOrders(): Unit = {
    if (orders.isEmpty) {
      println("No orders placed.")
    } else {
      println("Orders:")
      orders.foreach(order => {
        val customer = customers.find(_.id == order.customerId).map(_.name).getOrElse("Unknown Customer")
        val book = books.find(_.id == order.bookId).map(_.title).getOrElse("Unknown Book")
        println(s"Order ${order.id}: Customer ${order.customerId} (${customer}) ordered Book ${order.bookId} (${book})")
      })
    }
  }
}

Answers

Question 1: Collection Filtering

Correct Answer: B) List(2, 4, 5)

Question 2: Tuples and Destructuring

Correct Answer: B) person._2

Question 3: Defining Classes

Correct Answer: A) class MyClass(param: Type)

Question 4: Scala Collections

Correct Answer: B) List

Question 5: Object-Oriented Programming

Correct Answer: C) Scala allows defining singleton objects using the object keyword.

Question 68: Defining and Instantiating a Simple Class

Correct Answer: A) Hello, my name is Alice and I am 30 years old.
Hello, my name is Bob and I am 25 years old.

Question 6: Class with Private Members

Correct Answer: A) 10

Question 8: Overriding Methods in Subclasses

Correct Answer: A) Some sound
Woof

Question 9: Abstract Classes and Traits

Correct Answer: A) Circle area: 78.54

Question 10: Defining and Implementing Traits

Correct Answer: A) Good day, Alice.

Question 11: Mixing in Multiple Traits

Correct Answer: A) Walking...
Running...

Question 12: Overriding Trait Methods in Classes

Correct Answer: A) Woof

Question 13: Abstract and Concrete Methods in Traits

Correct Answer: A) 8
2

Question 14: Combining map and filter

Correct Answer: D) List("CHARLIE", "DAVID")

Question 15: Matching Tuples

Correct Answer: A) One apple

Question 16: Matching with Lists

Correct Answer: A) A list with three elements.

Question 17: Matching with Guards

Correct Answer: B) Full access

Question 18: For-Comprehension with Multiple Generators

Correct Answer: A) List((1,4), (1,5), (2,4), (2,5), (3,4), (3,5))

Function values

Exercise 1: Filter List with Function Parameter

def filterList(lst: List[Int], predicate: Int => Boolean): List[Int] = lst.filter(predicate)

// Test
println(filterList(List(1, 2, 3, 4, 5), _ % 2 == 0)) // Should print: List(2, 4)

Exercise 2: Implement a Custom map Function

def mapList[A, B](lst: List[A], func: A => B): List[B] = lst.map(func)

// Test
println(mapList(List("1", "2", "3"), _.toInt)) // Should print: List(1, 2, 3)

Exercise 3: A Higher-order Function that Returns a Function

def multiplier(factor: Int): Int => Int = number => number * factor

// Test
val triple = multiplier(3)
println(triple(5)) // Should print: 15

Exercise 4: Sorting with a Custom Comparator

def sortWithFunction[A](lst: List[A], comparator: (A, A) => Boolean): List[A] = lst.sortWith(comparator)

// Test
println(sortWithFunction(List(3, 1, 4, 2), (x: Int, y: Int) => x < y)) // Should print: List(1, 2, 3, 4)

Recursion

Exercise 1 sum

Write a function that takes a positive integer n and returns the sum of all the integers from 1 to n.

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

// example usage
sum(5) // returns 15

Exercise 2 sumBetween

Write a function that takes two integers and returns the sum of all the integers between them, including the endpoints.

def sumBetween(x: Int, y: Int): Int = 
  if x == y then x 
  else x + sumBetween(x + 1, y)

// example usage
sumBetween(1, 5) // returns 15

Exercise 3 sumList

Write a function that takes a list of integers and returns the sum of all the integers in the list.

def sumList(list: List[Int]): Int = {
	if list.isEmpty then 0
	else list.head + sumList(list.tail)
}

// example usage
sumList(List(1, 2, 3, 4, 5)) // returns 15

Exercise 4 filter even

Write a function that takes a list of integers and returns a new list with all the even numbers.

def filterEven(list: List[Int]): List[Int] =
	if list.isEmpty then List()
	else if list.head % 2 == 0 then
		list.head :: filterEven(list.tail)
	else
		filterEven(list.tail)

// example usage
filterEven(List(1, 2, 3, 4, 5, 6)) // returns List(2, 4, 6)

Exercise 5 longest string

Write a function that takes a list of strings and returns the length of the longest string in the list.

def longestString(list: List[String]): Int =
  if list.isEmpty then 0
  else
    list.head.length.max(longestString(list.tail))

// example usage
longestString(List("apple", "banana", "orange", "pear")) // returns 6

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 returns 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 returns 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] = ???

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.

Exercises

Exercise 1: Add two Option number

def addOptions(optA: Option[Int], optB: Option[Int]): Option[Int] 

Use pattern matching and for comprehension

(Extra) Use map and flatMap

Exercise 2: Read a file

Read from file

with try-catch

def readFileWithTryCatch(filePath: String): String 

with Try, Success and Failure

def readFileWithTry(filePath: String): Try[String] 

Exercise 3: Login with password check

Implement a simple login function using Either.

  • Left will return an error message if the login fails,
  • Right will return a welcome message upon successful login.
  def login(username: String, password: String): Either[String, String] 

Exercise 2: Option for Handling Nulls

Task: Given a method that might return null, adapt it to return an Option of its result instead. Assume the method signature is def getUser(id: Int): User, where User is a class and getUser might return null.

def getUserById(id: Int): Option[User]

Write a main function that uses this function

Exercise 5: Using Option with Collections

Task: Write a function that receives a list of Option[Int] and returns a new list with all None values removed and doubles each Some value.

def processOptions(options: List[Option[Int]]): List[Int] = options.flatten.map(_ * 2)

// Test cases
val optionsList = List(Some(1), None, Some(2), None, Some(3))
println(processOptions(optionsList))  // Should print: List(2, 4, 6)

Projects

Todo List

In this project we will create a todolist application

Create a class
TodoItem with a field

  • task

TodoList with methods

  • add - give an error if an item already exists
  • list - give an error if the list has more then 10 items
  • delete - give an error if an item does not exist

Add a priority to the TodoItem

if the same item is added keep the one with the highest priority and sort the list on priority

Create a main method that tests the todo list.

Shopping basket

In this project we will create a shoppingbasket application Create the classes

Article with the fields

  • name
  • price

ShoppingBasket with the methods

  • add - give an error if an article already exists

  • list - give an error if the list has no items

  • delete - give an error if an item does not exist

  • filter on prices between an low and high value

Add an amount field to the article Sort the list alphabetically Sort the list on alphabet and amount

Create a main method that tests the shopping basket.

Aswers

Question 1: Basic try-catch Usage

Correct Answer: A) Cannot divide by zero.
Operation attempted.

Question 2: Catching Multiple Exceptions

Correct Answer: B) Array index out of bounds.
Search attempted.

Question 3: Using try-catch with a Return Value

Correct Answer: A) Division operation processed.
Error: Division by zero.
Division operation processed.
Result: 2

Question 4: Using Option for Safe Value Access

Correct Answer: A) Some(a)
None

Question 5: Handling Multiple Errors with Either

Correct Answer: A) Right(5)
Left("Cannot divide by zero.")

Question 6: Graceful Error Handling with Try

Correct Answer: A) Success(100)
Failure(java.lang.NumberFormatException)

Question 7: Combining Option Values with flatMap

Correct Answer: A) Some(5)
None

Question 8: Using for-comprehension with Option

Correct Answer: A) Some(30)

Question 9: Safely Accessing Potentially Null Objects

Correct Answer: A) JohnDoe
Unknown

Question 11: Pattern Matching with Options to Handle Nulls

Correct Answer: A) Found word: Scala
No word found

Question 12: Encapsulating Nullable References with Options

Correct Answer: A) 5
0

Question 13: Creating and Accessing a Map

Correct Answer: A) Some(Paris)
Not found

Question 14: Updating and Adding Elements to a Map

Correct Answer: C) Map(1 -> "one", 2 -> "TWO", 3 -> "three")

Question 15: Iterating Over a Map

Correct Answer: A) Alice is 30 years old
Bob is 25 years old
Charlie is 28 years old

Question 16: Using Options for Safe Access

Correct Answer: B) 10

Question 17: Handling Exceptions with Try and Match

Correct Answer: B) Error: / by zero

Question 18: Using flatMap to Flatten and Transform

Correct Answer: A) List(2, 4, 6, 8, 10, 12)

Question 19: Folding a List with foldLeft

Correct Answer: A) 15

Question 20: Basic Usage of zip

Correct Answer: A) List(("Alice", 25), ("Bob", 30), ("Charlie", 28))

Question 21: zip with Unequal Collection Sizes

Correct Answer: B) List((1, 'a'), (2, 'b'), (3, 'c'))

Question 22: Using zipWithIndex

Correct Answer: A) List(("apple", 0), ("banana", 1), ("cherry", 2))

Question 23: Basic Usage of groupBy

Correct Answer: B) Map(a -> List("apple", "apricot"), b -> List("banana"), p -> List("pear", "peach"))

Question 24: Higher-Order Functions

Correct Answer: B) 25

Question 25: Simple Recursive Function for Summation

Correct Answer: A) 55 and 15

Solutions

Solution 1: Unit Testing with FunSuite

import org.scalatest.funsuite.AnyFunSuite

class StringUtilityTest extends AnyFunSuite {
  test("reverse should reverse a string") {
    assert(StringUtility.reverse("hello") === "olleh")
  }

  test("reverse should handle empty string") {
    assert(StringUtility.reverse("") === "")
  }

  test("isPalindrome should return true for a palindrome") {
    assert(StringUtility.isPalindrome("madam"))
  }

  test("isPalindrome should return false for a non-palindrome") {
    assert(!StringUtility.isPalindrome("hello"))
  }
}

For the ShoppingCart exercise, let's first define the solution for the ShoppingCart class and then proceed with writing a test suite for it using ScalaTest.

Implementing the ShoppingCart and Item

Here's a basic implementation of the ShoppingCart class and Item case class:

Writing Tests for the ShoppingCart

Now, let's write tests for this ShoppingCart implementation. We'll test adding items, removing items, applying discounts, and calculating the total.

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

class ShoppingCartTest extends AnyFunSuite with Matchers {

  test("Adding items to the shopping cart should increase total accordingly") {
    val cart = new ShoppingCart
    cart.addItem(Item("1", "Apple", 0.60, 1))
    cart.addItem(Item("2", "Banana", 0.40, 2))

    cart.total shouldEqual 1.40
  }

  test("Removing items from the shopping cart should decrease total accordingly") {
    val cart = new ShoppingCart
    cart.addItem(Item("1", "Apple", 0.60, 1))
    cart.addItem(Item("2", "Banana", 0.40, 2))
    cart.removeItem("2")

    cart.total shouldEqual 0.60
  }

  test("Applying a discount should reduce the total price") {
    val cart = new ShoppingCart
    cart.addItem(Item("1", "Apple", 1.00, 2)) // Total before discount: 2.00
    cart.applyDiscount("DISCOUNT10") // 10% discount

    cart.total shouldEqual 1.80 // 10% off of 2.00
  }

  test("Adding multiple quantities of an item should be reflected in the total") {
    val cart = new ShoppingCart
    cart.addItem(Item("1", "Apple", 0.50, 2)) // 2 Apples

    cart.total shouldEqual 1.00
  }

  test("Adding the same item again increases its quantity") {
    val cart = new ShoppingCart
    cart.addItem(Item("1", "Apple", 0.50, 1))
    cart.addItem(Item("1", "Apple", 0.50, 1)) // Adding again

    cart.total shouldEqual 1.00 // Reflects total for 2 Apples
  }
}

Solution 3: Testing a Password Validator

Tests for the PasswordValidator might look like this:

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

class PasswordValidatorTest extends AnyFunSuite with Matchers {
  test("A valid password") {
    assert(PasswordValidator.isValid("ValidPass123"))
  }

  test("Password is too short") {
    PasswordValidator.isValid("Short1") should be(false)
  }

  test("Password lacks a digit") {
    PasswordValidator.isValid("NoDigitsHere!") should be(false)
  }

  test("Password lacks an uppercase letter") {
    PasswordValidator.isValid("alllowercase1") should be(false)
  }

  test("Password lacks a lowercase letter") {
    PasswordValidator.isValid("ALLUPPERCASE1") should be(false)
  }
}

Solution 4: Property-Based Testing for a String Concatenation Utility

For property-based tests of StringConcatenationUtility, using ScalaTest with ScalaCheck:

import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalatest.matchers.should.Matchers
import org.scalacheck.Prop.forAll

class StringConcatenationUtilityTest extends AnyFunSuite with ScalaCheckPropertyChecks with Matchers {
  test("Concatenating two strings should include both with a space in between") {
    forAll { (a: String, b: String) =>
      StringConcatenationUtility.concatenate(a, b) should be (s"$a $b")
    }
  }
  
  test("Concatenating an empty string with a non-empty string results in the latter with an extra space") {
    forAll { (a: String) =>
      StringConcatenationUtility.concatenate("", a) should startWith (" ")
      StringConcatenationUtility.concatenate(a, "") should endWith (" ")
    }
  }
}

Solution 5: Testing a Fibonacci Number Generator

Tests for FibonacciGenerator could be:

class FibonacciGeneratorTest extends AnyFunSuite with Matchers {
  test("Fibonacci numbers for known values") {
    val knownValues = Seq((0, 0), (1, 1), (2, 1), (3, 2), (4, 3), (5, 5), (6, 8), (7, 13))
    knownValues.foreach { case (n, expected) =>
      FibonacciGenerator.fibonacci(n) should be(expected)
    }
  }

  test("Fibonacci number for negative input") {
    intercept[IllegalArgumentException] {
      FibonacciGenerator.fibonacci(-1)
    }
  }
}

Note: The performance or stack overflow test is not included as it's more about optimization and implementation strategy rather than simple functional testing.

Solution 6: Integration Testing for a File Processing Utility

An integration test for FileProcessingUtility involves file operations:

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.BeforeAndAfter
import java.nio.file.{Files, Paths}

class FileProcessingUtilityTest extends AnyFunSuite with BeforeAndAfter with Matchers {
  val inputPath = "testInput.txt"
  val outputPath = "testOutput.txt"

  before {
    val content = "This is a test."
    Files.write(Paths.get(inputPath), content.getBytes)
  }

  after {
    Files.deleteIfExists(Paths.get(inputPath))
    Files.deleteIfExists(Paths.get(outputPath))
  }

  test("File processing converts text to uppercase") {
    FileProcessingUtility.processFile(inputPath, outputPath)
    val result = Files.readAllLines(Paths.get(outputPath)).get(0)
    result should be("THIS IS A TEST.")
  }

  test("Handling of non-existent input files") {
    intercept[Exception] {
      FileProcessingUtility.processFile("nonExistentFile.txt", outputPath)
    }
  }
}

project

Solutions

1. Hello World

Create a file named hello.scala.html in your app/views directory. This template should accept a single string parameter and display it in an HTML paragraph.

@(name: String)

<!DOCTYPE html>
<html>
<head>
    <title>Hello World Example</title>
</head>
<body>
    <p>Hello, @name!</p>
</body>
</html>

2. List Rendering

Create a file named listRendering.scala.html in your app/views directory. This template will accept a List of strings and render them in an unordered list. It will display a message if the list is empty.

@(items: List[String])

<!DOCTYPE html>
<html>
<head>
    <title>List Rendering Example</title>
</head>
<body>
    @if(items.isEmpty) {
        <p>No items to display.</p>
    } else {
        <ul>
            @for(item <- items) {
                <li>@item</li>
            }
        </ul>
    }
</body>
</html>

3. Form Submission Display

For this exercise, you'll need a controller to handle the form submission and two Twirl templates: one for the form and another to display the submitted data.

Form Template (userForm.scala.html):

@(formAction: Call)

<!DOCTYPE html>
<html>
<head>
    <title>User Form</title>
</head>
<body>
    <form action="@formAction" method="POST">
        <div>
            <label for="firstName">First Name:</label>
            <input type="text" id="firstName" name="firstName">
        </div>
        <div>
            <label for="lastName">Last Name:</label>
            <input type="text" id="lastName" name="lastName">
        </div>
        <button type="submit">Submit</button>
    </form>
</body>
</html>

Display Template (displayUser.scala.html):

@(firstName: String, lastName: String)

<!DOCTYPE html>
<html>
<head>
    <title>Display User</title>
</head>
<body>
    <p>First Name: @firstName</p>
    <p>Last Name: @lastName</p>
</body>
</html>

Controller Methods (Scala):

In your controller, you will need to add methods to render the form and handle the submission. This example assumes you are using a Scala controller.

import play.api.mvc._
import javax.inject._

class UserController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
  
  def showForm = Action { implicit request: Request[AnyContent] =>
    Ok(views.html.userForm(routes.UserController.handleSubmit))
  }

  def handleSubmit = Action { implicit request: Request[AnyContent] =>
    val postVals = request.body.asFormUrlEncoded
    val firstName = postVals.get("firstName").flatMap(_.headOption).getOrElse("")
    val lastName = postVals.get("lastName").flatMap(_.headOption).getOrElse("")
    Ok(views.html.displayUser(firstName, lastName))
  }
}

Ensure your routes file (conf/routes) includes the necessary routes for these actions:

GET     /form                       controllers.UserController.showForm
POST    /submit-form                controllers.UserController.handleSubmit

4. Conditional Content

Create a file named conditionalContent.scala.html in your app/views directory. This template will display different content based on the Boolean value passed to it.

@(showMessage: Boolean)

<!DOCTYPE html>
<html>
<head>
    <title>Conditional Content Example</title>
</head>
<body>
    @if(showMessage) {
        <p>This is a conditional message shown only when 'showMessage' is true.</p>
    } else {
        <p>This message is shown when 'showMessage' is false.</p>
    }
</body>
</html>

5. Nested Templates

For nested templates, you typically have a main layout template and one or more child templates. Let's create a simple layout template and a child template that uses it.

Main Layout Template (mainLayout.scala.html):

@(title: String)(content: Html)

<!DOCTYPE html>
<html>
<head>
    <title>@title</title>
</head>
<body>
    <header>
        <h1>Site Header</h1>
    </header>

    @content

    <footer>
        <p>Site Footer</p>
    </footer>
</body>
</html>

Child Template (childPage.scala.html):

@()

@mainLayout("Child Page") {
    <p>This is the content of the child page.</p>
}

6. Loop with Conditional

Create a file named loopWithConditional.scala.html in your app/views directory. This template will iterate over a list of integers, displaying each in a list item, and highlight numbers divisible by 3.

@(numbers: List[Int])

<!DOCTYPE html>
<html>
<head>
    <title>Loop with Conditional Example</title>
</head>
<body>
    <ul>
        @for(number <- numbers) {
            <li class="@if(number % 3 == 0) {highlight}">
                @number
            </li>
        }
    </ul>
</body>
</html>

You might also include some CSS within the <head> section or an external stylesheet to style the .highlight class for numbers divisible by 3, like so:

<style>
    .highlight {
        font-weight: bold;
    }
</style>

Project: Shopping Basket

Implement the ShoppingBasket from previous chapters in Play

Step1: With database

Step2: With Anorm

Step3: With Slick

Step4: Rest API

Step5: Error Handling

Step6: Add Bootstrap

Step7: More and more

Exercises

Exercise 1: Simple Ping-Pong

import akka.actor._

class PingActor(pongActor: ActorRef) extends Actor {
  def receive: Receive = {
    case "Start" =>
      println("Ping")
      pongActor ! "Ping"
      
    case "Pong" =>
      println("Ping")
      Thread.sleep(500)  // Short delay
      pongActor ! "Ping"
  }
}

class PongActor extends Actor {
  def receive: Receive = {
    case "Ping" =>
      println("Pong")
      sender() ! "Pong"
  }
}

object PingPongApp extends App {
  val system = ActorSystem("PingPongSystem")
  
  val pongActor = system.actorOf(Props[PongActor], "pongActor")
  val pingActor = system.actorOf(Props(new PingActor(pongActor)), "pingActor")
  
  pingActor ! "Start"
}

Exercise 2: Counter Actor

import akka.actor._

class CounterActor extends Actor {
  private var counter = 0
  
  def receive: Receive = {
    case "Increment" =>
      counter += 1
      
    case "Decrement" =>
      counter -= 1
      
    case "Get" =>
      sender() ! counter
  }
}

object CounterApp extends App {
  val system = ActorSystem("CounterSystem")
  
  val counterActor = system.actorOf(Props[CounterActor], "counterActor")
  
  counterActor ! "Increment"
  counterActor ! "Increment"
  counterActor ! "Decrement"
  counterActor ! "Get"
  
  import akka.pattern.ask
  import akka.util.Timeout
  import scala.concurrent.duration._
  import scala.concurrent.ExecutionContext.Implicits.global
  
  implicit val timeout: Timeout = 5.seconds
  
  val future = counterActor ? "Get"
  
  future.map(result => println(s"Final counter value: $result"))
}

Exercise 3: Actor Hierarchy and Supervision

import akka.actor._

class ChildActor extends Actor {
  def receive: Receive = {
    case "DoWork" =>
      if (scala.util.Random.nextBoolean()) throw new RuntimeException("Failure!")
      println("Work done!")
  }
}

class ParentActor extends Actor {
  val childActor = context.actorOf(Props[ChildActor], "childActor")
  
  override val supervisorStrategy: SupervisorStrategy =
    OneForOneStrategy() {
      case _: RuntimeException => SupervisorStrategy.Restart
    }
  
  def receive: Receive = {
    case msg => childActor forward msg
  }
}

object SupervisionApp extends App {
  val system = ActorSystem("SupervisionSystem")
  
  val parentActor = system.actorOf(Props[ParentActor], "parentActor")
  
  parentActor ! "DoWork"
  parentActor ! "DoWork"
  parentActor ! "DoWork"
}

Exercise 4: Bank Account Actor

import akka.actor._

case class Deposit(amount: Double)
case class Withdraw(amount: Double)
case object GetBalance

class BankAccountActor extends Actor {
  private var balance = 0.0
  
  def receive: Receive = {
    case Deposit(amount) =>
      balance += amount
      
    case Withdraw(amount) =>
      if (balance >= amount) balance -= amount
      else println("Insufficient funds")
      
    case GetBalance =>
      sender() ! balance
  }
}

object BankAccountApp extends App {
  val system = ActorSystem("BankAccountSystem")
  
  val bankAccountActor = system.actorOf(Props[BankAccountActor], "bankAccountActor")
  
  bankAccountActor ! Deposit(100)
  bankAccountActor ! Withdraw(50)
  bankAccountActor ! GetBalance
  
  import akka.pattern.ask
  import akka.util.Timeout
  import scala.concurrent.duration._
  import scala.concurrent.ExecutionContext.Implicits.global
  
  implicit val timeout: Timeout = 5.seconds
  
  val future = bankAccountActor ? GetBalance
  
  future.map(balance => println(s"Final balance: $balance"))
}

Exercise 5: Master-Worker Pattern

import akka.actor._

case class Work(nums: List[Int])
case object GetSum

class WorkerActor extends Actor {
  def receive: Receive = {
    case Work(nums) =>
      val sum = nums.sum
      sender() ! sum
  }
}

class MasterActor(workerCount: Int) extends Actor {
  private var sum = 0
  private var receivedResponses = 0
  private val workers = (1 to workerCount).map(_ => context.actorOf(Props[WorkerActor]))
  
  def receive: Receive = {
    case Work(nums) =>
      val chunks = nums.grouped(nums.size / workerCount).toList
      chunks.zip(workers).foreach {
        case (chunk, worker) => worker ! Work(chunk)
      }
      
    case result: Int =>
      sum += result
      receivedResponses += 1
      if (receivedResponses == workerCount) context.parent ! sum
  }
}

object MasterWorkerApp extends App {
  val system = ActorSystem("MasterWorkerSystem")
  
  val masterActor = system.actorOf(Props(new MasterActor(3)), "masterActor")
  
  masterActor ! Work(List(1, 2, 3, 4, 5, 6, 7, 8, 9))
  
  masterActor ! GetSum
  
  import akka.pattern.ask
  import akka.util.Timeout
  import scala.concurrent.duration._
  import scala.concurrent.ExecutionContext.Implicits.global
  
  implicit val timeout: Timeout = 5.seconds
  
  val future = masterActor ? GetSum
  
  future.map(result => println(s"Final sum: $result"))
}

todo-untyped

todoapp

shopping-basket untyped

shoppingbasket-typed

Exercises

Exercise 1: Hello Actor

Task: Create an actor named HelloActor that receives a String message and prints out Hello, [message]!.

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.Behavior

object HelloActor {
  final case class SayHello(name: String)

  def apply(): Behavior[SayHello] = Behaviors.receive { (context, message) =>
    println(s"Hello, ${message.name}!")
    Behaviors.same
  }
}

// App to test HelloActor
object HelloApp extends App {
  val system: ActorSystem[HelloActor.SayHello] = ActorSystem(HelloActor(), "helloSystem")

  system ! HelloActor.SayHello("Akka")
}

Exercise 2: Counter Actor

Task: Implement a CounterActor that can receive messages to increment, decrement, and print its internal count.

object CounterActor {
  sealed trait Command
  case object Increment extends Command
  case object Decrement extends Command
  case object Print extends Command

  def apply(): Behavior[Command] = counterBehavior(0)

  private def counterBehavior(count: Int): Behavior[Command] =
    Behaviors.receive { (context, message) =>
      message match {
        case Increment => counterBehavior(count + 1)
        case Decrement => counterBehavior(count - 1)
        case Print =>
          println(s"Current count is $count")
          Behaviors.same
      }
    }
}

// App to test CounterActor
object CounterApp extends App {
  val system: ActorSystem[CounterActor.Command] = ActorSystem(CounterActor(), "counterSystem")

  system ! CounterActor.Increment
  system ! CounterActor.Increment
  system ! CounterActor.Decrement
  system ! CounterActor.Print // Should print "Current count is 1"
}

Exercise 3: Ping-Pong Actors

Task: Create two actors, PingActor and PongActor. PingActor should send a ping message to PongActor, and PongActor should respond with a pong message.

object PingPongActor {
  sealed trait Command
  case class Ping(replyTo: ActorRef[Command]) extends Command
  case class Pong(replyTo: ActorRef[Command]) extends Command

  def apply(): Behavior[Command] = Behaviors.receive { (context, message) =>
    message match {
      case Ping(replyTo) =>
        println("Ping received")
        replyTo ! Pong(context.self)
        Behaviors.same
      case Pong(replyTo) =>
        println("Pong received")
        replyTo ! Ping(context.self)
        Behaviors.same
    }
  }
}

// App to test PingPongActor
object PingPongApp extends App {
  val system: ActorSystem[PingPongActor.Command] = ActorSystem(PingPongActor(), "pingPongSystem")
  
  val pongActor = system
  val pingActor = system

  pingActor ! PingPongActor.Ping(pongActor)
}

Projects

Todo List

In this project we will create a todolist application with Akka Actors

Creata a class
TodoItem with a field

  • task

TodoList with methods

  • add
  • list
  • delete

Create a main method that tests the todo list.

Shopping basket

In this project we will create a shoppingbasket application with Akka Actors Create the classes

Article with the fields

  • name
  • price

ShoppingBasket with the methods

  • add
  • list
  • calcTotal

Create a main method that tests the shopping basket.

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

todoapp

shoppingbasket

Exercises

Here are three exercises focusing on Scala Futures to practice asynchronous programming. These exercises cover different aspects of working with Futures, including basic usage, composition, error handling, and integration with external services or databases.

Exercise 1: Asynchronous Data Processing

Objective: Implement an asynchronous method that processes a list of integers. The processing should square each number and then return the sum of all squared numbers. Use Futures to perform the squaring operations in parallel.

Task:

  1. Create a method squareNumber that takes an Int and returns a Future[Int] representing the square of the number.
  2. Implement a method sumOfSquares that accepts a List[Int] and returns a Future[Int] with the sum of the squares of the list elements, computed asynchronously.
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def squareNumber(number: Int): Future[Int] = Future {
  number * number
}

def sumOfSquares(numbers: List[Int]): Future[Int] = {
  Future.sequence(numbers.map(squareNumber)).map(_.sum)
}

Exercise 2: Combining Futures with for-comprehension

Objective: Write a function that asynchronously fetches the current temperatures (mocked as random values) for two cities and then computes the average temperature. Each city's temperature should be fetched in parallel, and the averaging should be done once both temperatures are available.

Task:

  1. Implement two functions, getTemperature(city: String): Future[Double], that simulate fetching temperature data for each city.
  2. Write a function averageTemperature(city1: String, city2: String): Future[Double] that uses for-comprehension to wait for both temperatures and then computes the average.
import scala.concurrent.Future
import scala.util.Random
import scala.concurrent.ExecutionContext.Implicits.global

def getTemperature(city: String): Future[Double] = Future {
  Thread.sleep(Random.nextInt(500)) // Simulate network delay
  20 + Random.nextInt(15) // Random temperature
}

def averageTemperature(city1: String, city2: String): Future[Double] = {
  for {
    temp1 <- getTemperature(city1)
    temp2 <- getTemperature(city2)
  } yield (temp1 + temp2) / 2
}

Exercise 3: Error Handling in Futures

Objective: Implement a function that tries to parse a list of strings to integers and computes their sum. The function should handle any parsing errors by treating unparsable strings as zeros.

Task:

  1. Write a method parseToInt that converts a String to an Int and returns a Future[Int]. If parsing fails, it should return Future.successful(0).
  2. Implement a method sumOfStrings(numbers: List[String]): Future[Int] that uses the parseToInt method to sum the list of strings treated as integers.
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.Try

def parseToInt(s: String): Future[Int] = Future {
  Try(s.toInt).getOrElse(0)
}

def sumOfStrings(numbers: List[String]): Future[Int] = {
  Future.sequence(numbers.map(parseToInt)).map(_.sum)
}

project

More Functions

Exercise 1

def sum(a: Int, b: Int) = a + b

Create a partial applied function for sum Create also a function sum and a partial Create a curried version of sum

Exercise 2

Try to write a fromCurry and toCurry yourself without looking at example above

Exercise 3

Create a partial function on Int's the works on odd numbers and returns a multiplication by 10

Exercise 4

Create a partial function on Int's the works on odd numbers and returns a multiplication by 10 Use it on a list List(1,2,3,4,5,6,7,8,9)

Use collect, filter and map

Call by Value, Name, Need

Exercise 1

  1. Make it a call by name
  2. Make it a call by need by adding a lazy val
def exercise(str: String) = 
  
  Thread.sleep(2000)
  println(s"first: $str at ${System.nanoTime()}")

  Thread.sleep(2000)
  println(s"second: $str at ${System.nanoTime()}")

exercise("hello")

Exercise 2

Create a function def currentTime(time: Long)

  1. Make it a call by value
  2. Make it a call by name
  3. Make it a call by need by adding a lazy val

Given and Using

Exercise 1

val cities = List("London", "Paris", "Lisbon", "Berlin")

Create a list of cities:

  1. sort the list of cities in ascending order
  2. sort in list of cities in descending order
  3. Put the two ordering function in their own scope
  4. Choice which order you use with the import

Create a case class CapitalCity with a city and a country Create a list of capital cities

  1. sort the capital cities in ascending order on the country name
  2. sort the capital cities in descending order in the city name
  3. Add the first ordering in the companion object and the second a separate object
  4. Choice which order you use with the import

Exercise 2

val names = List("John", "Alice", "Jane", "Edward")

Create a list of names:

  1. sort the names in ascending order
  2. sort in names in descending order
  3. Put the two ordering function in their own scope
  4. Choice which order you use with the import

Create a case class Person with a name and age Create a list of persons

  1. sort the persons in ascending order on the age
  2. sort the persons in descending order on the age
  3. Add the ascending ordering in the companion object and the descending a separate scope
  4. Choice which order you use with the import

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

Exercises

Exercise 1

val cities = List("London", "Paris", "Lisbon", "Berlin")

Create a list of cities:

  1. In an object create an extension method print to the String class.
  2. In another object create a second extension method print to the String that prints in ALL-CAPS
  3. Choice which print method you will use with an import

Create a case class CapitalCity with a city and a country
Create a list of capital cities

  1. Create an extension method print to the CapitalCity companion object
  2. Print the list of capital cities

Exercise 2

Create a case class Person with a name and age
Create a list of persons

  1. Create an extension method print to the Person companion object.
  2. In another object create an extension method print to the Persion that prints in ALL-CAPS
  3. Choice which print method you will use with an import

Conversion

Exercises

Exercise 1

case class Person(name: String)
  def greet(): String = s"Hello, $name"
  1. Write a conversion function from Person to Int that calculate the length of the name
  2. Put it in a separate object
  3. Use it with an import

Exercise 2

case class User(name: String)
  def login(): String = s"Logged in: $name"
  1. Write a conversion from Person to User
  2. Write a conversion from User to Person
  3. Test both

Type Classes

Type Classes Exercises

Exercise: Json Converter

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.

Exercises

Here are three exercises on monads in Scala, designed to help reinforce your understanding of how monads work and how to use them in different contexts. These exercises cover Option, List, and Future, three commonly used monads in Scala.

Exercise 1: Option Monad

Task: Write a function that takes two parameters: a list of strings and a map from strings to integers. The function should return the total length of all strings in the list that are keys in the map. Use Option to handle the case where a key is not present in the map.

def totalLengthOfMappedStrings(strings: List[String], map: Map[String, Int]): Int = 
  strings.flatMap(map.get).sum

Test Case:

val strings = List("apple", "banana", "cherry", "date")
val map = Map("apple" -> 5, "cherry" -> 6, "date" -> 4)
println(totalLengthOfMappedStrings(strings, map))  // Should output 15

Exercise 2: List Monad

Task: Implement a function that receives three lists of integers. The function should return a list of all possible combinations of triples (a, b, c) where a is from the first list, b is from the second list, and c is from the third list, such that a + b + c is divisible by 3.

def triplesDivisibleByThree(list1: List[Int], list2: List[Int], list3: List[Int]): List[(Int, Int, Int)] = {
  for 
    a <- list1
    b <- list2
    c <- list3
    if (a + b + c) % 3 == 0
  yield (a, b, c)
}

Test Case:

val list1 = List(1, 2, 3)
val list2 = List(4, 5, 6)
val list3 = List(7, 8, 9)
println(triplesDivisibleByThree(list1, list2, list3))
// Should output a list of triples (e.g., (1, 5, 7), (2, 4, 8), ...) where the sum of each triple is divisible by 3

Exercise 3: Future Monad

Task: Write a function that performs three asynchronous operations in sequence, where each operation multiplies its input by 2. Use Future to represent the asynchronous operations. The function should take an integer as input and return a Future of the result.

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def asyncTripleMultiplier(initialValue: Int): Future[Int] = 
  val operation1 = Future(initialValue * 2)
  operation1.flatMap { result1 =>
    val operation2 = Future(result1 * 2)
    operation2.flatMap { result2 =>
      Future(result2 * 2)
    }
  }

Or, using for-comprehension for cleaner syntax:

def asyncTripleMultiplierFor(initialValue: Int): Future[Int] = 
  for 
    result1 <- Future(initialValue * 2)
    result2 <- Future(result1 * 2)
    result3 <- Future(result2 * 2)
  yield result3

Test Case:

asyncTripleMultiplierFor(1).onComplete(println)  // Should output Success(8) after completing the asynchronous computations

Remember, when testing Future-based code, you may need to wait for the future to complete to see the output. In a real application, this would typically be handled by the main thread of the application or a framework managing the lifecycle of the program.

These exercises should give you a practical understanding of working with monads in Scala, demonstrating how they can encapsulate various kinds of computations and control flows in a type-safe and expressive manner.

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.

Exercises

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

Exercises

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.

Exercises

MyList - step 1

MyList

We will create a MyList similar to the List in the standard libraries

We start with a trait

trait MyList[A]:
  def isEmpty: Boolean
  def head: A
  def tail: MyList[A]
  

Exercise

Implement the trait in a Cons node and an Empty node And then create a MyList with them.

case class Empty[A]() extends MyList[A]
case class Cons[A]() extends MyList[A]

Solution

Empty

case class Empty[A]() extends MyList[A]:
  override def isEmpty: Boolean = true
  override def head: A = throw new NoSuchElementException()
  override def tail: MyList[A] = throw new NoSuchElementException()

Cons

We put the head and tail in the constructor

case class Cons[A](override val head: A, override val tail: MyList[A]) extends MyList[A]:
  override def isEmpty: Boolean = false

main

@main
def main(): Unit =
  val myList: MyList[Int] = Cons(1, Cons(2, Cons(3, Empty())))
  println(myList)

//  Cons(1,Cons(2,Cons(3,Empty())))

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

Add method

  • Add the add method
  • Add the + method
  • Create a companion object

Exercise 1

trait MyList[A]:
  ...
  def add(element: A): MyList[A]
  • Give the MyList trait an add method
  • Implement it in Cons and Empty
  • And test it in main with: Empty().add(1).add(2).add(3)

Exercise 2

trait MyList[A]:
  ...
  def +(element: A): MyList[A]
  • Implement + operator as alias for add method
  • And test it in main with: Empty() + 1 + 2 + 3

Exercise 3

object MyList:
  def apply[A](elements: A*): MyList[A]
  • Implement the companion object
  • And test it in main with: MyList(1,2,3)

Add method

Empty

override def add(element: A): MyList[A] = Cons(element, this)

Adding an element to Empty means that Empty is not empty anymore and becomes a Cons with the tail Empty (this)

Cons

override def add(element: A): MyList[A] = Cons(element, this)

Adding an element to Cons means adding new head and the tail becomes the current Cons (this)

trait

trait MyList[A]:
  ...
  def add(element: A): MyList[A] = Cons(element, this)

The implementation in Empty and Cons are the same.
So we can move it up to the MyList trait (and remove them from Empty and Cons).
Via inheritance, they are available in Empty and Cons again

main

@main
def main(): Unit =
  val myList: MyList[Int] = Empty().add(1).add(2).add(3)
  println(myList)
  
//  MyList(3, 2, 1)

The order is reversed now.

Solution 2

trait MyList[A]:
  ...
  def add(element: A): MyList[A] = Cons(element, this)
  infix def + (element: A): MyList[A] = add(element)

In Scala + is a valid function name
With the infix modifier we do not have to use the braces in the function call.

@main
def main(): Unit =
  val myList: MyList[Int] = Empty() + 1 + 2 + 3
  println(myList)

//  MyList(3, 2, 1)

Solution 3

companion object

object MyList:
  def apply[A](elements: A*): MyList[A] =
    def build(elements: Seq[A], acc: MyList[A]): MyList[A] =
      if elements.isEmpty then acc
      else build(elements.tail, acc + elements.head)

    build(elements.reverse, Empty())

In the companion object we add the apply method with a varargs param list.
In a tail recursive build function we add the elements to MyList with our own infix +
Because the recursive call will build the MyList in reversed order, we start with reversing the elements

main

@main
def main(): Unit =
  val myList: MyList[Int] = MyList(1,2,3)
  println(myList)

//  MyList(1, 2, 3)

MyList - step 4

Foreach, Map and Filter

Exercise

Add the following methods to MyList

  • foreach
  • map
  • filter
trait MyList[A]:
  ...
  def foreach(f: A => B): Unit
  def map[B](f: A => B): MyList[B]
  def filter(f: A => Boolean): MyList[A]

Solution Foreach

Empty

override def foreach(f: A => Unit): Unit = ()

Should return Unit. The implementation of Unit is ()

Cons

override def foreach(f: A => Unit): Unit =
  f(head)
  tail.foreach(f)

We have to walk through the linked list get the call the function on the head
and then jump to the tail recursively.

Solution Map

Empty

override def map[B](f: A => B): MyList[B] = Empty[B]()

Transforming an empty list of type A gives us an empty list of type B. This feels a weird. That because of our definition Empty If we had it defined as: case object Empty extends MyList[Nothing]

But then we have implement a covariant/contravariant version op the type A. That is something for the advanced course.

You could leave out the type on the Cons because the compiler know the type from the return type MyList[B] Empty()

Cons

override def map[B](f: A => B): MyList[B] = 
  Cons[B](f(head), tail.map(f))

We have to walk through the linked list of type A
call the function on the head
and walk the tail recursively. And wrap inside a new Cons of type B

You could leave out the type on the Cons because the compiler know the type from the return type MyList[B] Cons(f(head), tail.map(f))

Solution Filter

Empty

override def filter[B](f: A => Boolean): MyList[A] = this

Filtering an empty list gives an empty list.

Cons

override def filter[B](f: A => Boolean): MyList[A] =
  if !f(head) then 
    tail.filter(f)
  else 
    Cons(head, tail.filter(f))

We check if the predicate f on the head is false then we go further on filtering the tail If the predicate f is true then the head is added to new Cons, and then we go further on the filtering the tail

main

@main
def main(): Unit =
  val myList: MyList[Int] = MyList(1,2,3)

  myList.foreach(x => println(x + 2))
  println(myList.map(x => x * 2))
  println(myList.filter(x => x < 2))

// 3
// 4
// 5
// MyList(2, 4, 6)
// MyList(1)

MyList - step 5

Concat and Flatmap

We will add two new methods to MyList

  • ++ (concatenation)
  • flatMap

Exercise

trait MyList[A]:
  ...
  infix def ++(other: MyList[A]): MyList[A]
  def flatMap[B](f: A => MyList[B]): MyList[B]

Solution Concatenation

Empty

override infix def ++(other: MyList[A]): MyList[A] = other

Another list added to an empty list gives the other list

Cons

override infix def ++(other: MyList[A]): MyList[A] = 
  tail ++ other + head

We could also write this as

Cons(head, tail ++ other)

A recursion example flow

[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 => B): MyList[B] = Empty[B]()

Like the map method flatMap returns an Empty list

Cons

override def flatMap[B](f: A => MyList[B]): MyList[B] = 
  f(head) ++ tail.flatMap(f)

With the implementation of concatenation (++) flatMap becomes simple
The function call f(head) returns a MyList
which is concatenated with the recursive call of flatMap on the tail

A recursion example flow

[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]

main

@main
def main(): Unit =
  val myList: MyList[Int] = MyList(1,2,3)
  val otherList = MyList(4, 5)

  println( myList ++ otherList )

  println( myList.flatMap(a => MyList(a, a + 1)) )

// MyList(1, 2, 3, 4, 5)
// MyList(1, 2, 2, 3, 3, 4)

MyList - step 6

For Comprehension

The MyList has a map and flatMap function
So we can use in a for comprehension

Exercise

@main
def main(): Unit =
  val result = for
      a <- MyList(1, 2, 3)
      b <- MyList(a, a + 1)
    yield
      b
  println(result)

This is the same as the example in the for

@main
def main(): Unit =
  val result = for
      a <- MyList(1, 2, 3)
      b <- MyList(a, a + 1)
    yield
      b
  println(result)

WithFilter

  def withFilter[A](f: A => MyList[B]): MyList[B]

In the scala for comprehension we can filter elements.
This is not the filter method but the withFilter method. In the standard libraries the withFilter is lazy, but that is for the advanced course
So for now we use our filter method as implementation

  def withFilter[A](f: A => MyList[B]): MyList[B] = filter(f)
@main
def main(): Unit =
  val result = for
    a <- MyList(1, 2, 3, 4, 5) if a < 3
  yield
    a
  println(result)

MyList - step 7

The lazy list can handle an infinite list.
In the end we take some elements that are calculated

Lazy List Exercises

Exercise 1

We will use MyList as a reference. Refactor the MyList name to a LzList (shift-F6) Nearly all the methods are the same.

Cons

class Cons[A](hd: => A, tl: => LazyList[A]) extends LazyList[A] {
	def isEmpty: Boolean = false
	override lazy val head: A = hd
	override lazy val tail: LazyList[A] = tl

Here we do a call by need on the head and the tail And we need a class because call by name are not allow on case classes.

And new Cons is used in instantiate a Cons

Run the LzList to see if everything still works

Exercise 2

To use the laziness of the list we will create an inifite list and take method

  • take
  • infinite list
def take(n :Int): LzList[A]

Implement those in Empty and Cons

def generate(start: Int)(next: Int => Int): LzList[Int] = ???

In the companion object a generate method is added of type Int

Solution 2

Empty

override def take(n: Int): LzList[A] = this

Cons

override def take(n: Int): LzList[A] =
  def loop(remainder: LzList[A], count: Int): LzList[A] =
    if count == 0 then Empty()
    else new Cons(remainder.head, loop (remainder.tail, count - 1))
  loop(this, n)

object LzList

def generate(start: Int)(next: Int => Int): LzList[Int] =
  new Cons[Int](start, generate(next(start))(next))

main

val genList: LzList[Int] = LzList.generate(1)( _ + 1)
val genMap = genList.map(_ * 100)

println(genMap.take(10))
println(genMap.take(100))
println(genMap.take(100000))

Library

case class Book(title: String, author: String, year: Int, category: String)

object LibraryManagement extends App {
  var library = List(
    Book("1984", "George Orwell", 1949, "Dystopian"),
    Book("To Kill a Mockingbird", "Harper Lee", 1960, "Fiction"),
    Book("The Great Gatsby", "F. Scott Fitzgerald", 1925, "Classic"),
    Book("Brave New World", "Aldous Huxley", 1932, "Dystopian"),
    Book("Moby Dick", "Herman Melville", 1851, "Classic"),
    Book("The Catcher in the Rye", "J.D. Salinger", 1951, "Fiction")
  )

  def addBook(library: List[Book], book: Book): List[Book] = {
    book :: library
  }

  def searchBooks(library: List[Book], query: String): List[Book] = {
    library.filter(book => book.title.contains(query) || book.author.contains(query))
  }

  def filterByCategory(library: List[Book], category: String): List[Book] = {
    library.filter(_.category == category)
  }

  def totalBooks(library: List[Book]): Int = {
    library.length
  }

  def averagePublicationYear(library: List[Book]): Double = {
    if (library.isEmpty) 0.0
    else library.map(_.year).sum.toDouble / library.length
  }

  println("Initial Library:")
  library.foreach(println)

  // Add a new book
  val newBook = Book("Sapiens", "Yuval Noah Harari", 2011, "Non-Fiction")
  library = addBook(library, newBook)

  println("\nLibrary after adding a new book:")
  library.foreach(println)

  // Search for books
  val searchQuery = "George Orwell"
  val searchResults = searchBooks(library, searchQuery)

  println(s"\nSearch results for '$searchQuery':")
  searchResults.foreach(println)

  // Filter by category
  val category = "Dystopian"
  val dystopianBooks = filterByCategory(library, category)

  println(s"\nBooks in the '$category' category:")
  dystopianBooks.foreach(println)

  // Calculate statistics
  val total = totalBooks(library)
  val averageYear = averagePublicationYear(library)

  println(s"\nTotal number of books: $total")
  println(f"Average publication year: $averageYear%.2f")
}

Enhanced Library

case class Book(title: String, author: String, year: Int, category: String)

object EnhancedLibraryManagement extends App {
  var library = List(
    Book("1984", "George Orwell", 1949, "Dystopian"),
    Book("To Kill a Mockingbird", "Harper Lee", 1960, "Fiction"),
    Book("The Great Gatsby", "F. Scott Fitzgerald", 1925, "Classic"),
    Book("Brave New World", "Aldous Huxley", 1932, "Dystopian"),
    Book("Moby Dick", "Herman Melville", 1851, "Classic"),
    Book("The Catcher in the Rye", "J.D. Salinger", 1951, "Fiction")
  )

  def addBook(library: List[Book], book: Book): List[Book] = {
    book :: library
  }

  def searchBooks(library: List[Book], query: String): List[Book] = {
    library.filter(book => book.title.contains(query) || book.author.contains(query))
  }

  def filterByCategory(library: List[Book], category: String): List[Book] = {
    library.filter(_.category == category)
  }

  def totalBooks(library: List[Book]): Int = {
    library.length
  }

  def averagePublicationYear(library: List[Book]): Double = {
    if (library.isEmpty) 0.0
    else library.map(_.year).sum.toDouble / library.length
  }

  def sortBooksByTitle(library: List[Book]): List[Book] = {
    library.sortBy(_.title)
  }

  def sortBooksByYear(library: List[Book]): List[Book] = {
    library.sortBy(_.year)
  }

  def groupBooksByCategory(library: List[Book]): Map[String, List[Book]] = {
    library.groupBy(_.category)
  }

  def partitionBooksByYear(library: List[Book], year: Int): (List[Book], List[Book]) = {
    library.partition(_.year < year)
  }

  def collectTitlesAfterYear(library: List[Book], year: Int): List[String] = {
    library.collect {
      case Book(title, _, y, _) if y > year => title
    }
  }
}

Library with Shopping Basket

Step-by-Step Enhancements

case class Book(title: String, author: String, year: Int, category: String, price: Double)

class ShoppingBasket {
  private var items: List[Book] = List()

  def addBook(book: Book): Unit = {
    items = book :: items
  }

  def totalCost: Double = {
    items.map(_.price).sum
  }

  def showBasket(): Unit = {
    println("Shopping Basket:")
    items.foreach(book => println(s"${book.title} - ${book.price}"))
  }
}

object EnhancedLibraryManagement extends App {
  var library = List(
    Book("1984", "George Orwell", 1949, "Dystopian", 15.99),
    Book("To Kill a Mockingbird", "Harper Lee", 1960, "Fiction", 10.99),
    Book("The Great Gatsby", "F. Scott Fitzgerald", 1925, "Classic", 8.99),
    Book("Brave New World", "Aldous Huxley", 1932, "Dystopian", 12.99),
    Book("Moby Dick", "Herman Melville", 1851, "Classic", 9.99),
    Book("The Catcher in the Rye", "J.D. Salinger", 1951, "Fiction", 14.99)
  )

  def addBook(library: List[Book], book: Book): List[Book] = {
    book :: library
  }

  def searchBooks(library: List[Book], query: String): List[Book] = {
    library.filter(book => book.title.contains(query) || book.author.contains(query))
  }

  def filterByCategory(library: List[Book], category: String): List[Book] = {
    library.filter(_.category == category)
  }

  def totalBooks(library: List[Book]): Int = {
    library.length
  }

  def averagePublicationYear(library: List[Book]): Double = {
    if (library.isEmpty) 0.0
    else library.map(_.year).sum.toDouble / library.length
  }

  def sortBooksByTitle(library: List[Book]): List[Book] = {
    library.sortBy(_.title)
  }

  def sortBooksByYear(library: List[Book]): List[Book] = {
    library.sortBy(_.year)
  }

  def groupBooksByCategory(library: List[Book]): Map[String, List[Book]] = {
    library.groupBy(_.category)
  }

  def partitionBooksByYear(library: List[Book], year: Int): (List[Book], List[Book]) = {
    library.partition(_.year < year)
  }

  def collectTitlesAfterYear(library: List[Book], year: Int): List[String] = {
    library.collect {
      case Book(title, _, y, _, _) if y > year => title
    }
  }

  def averagePublicationYearByCategory(library: List[Book]): Map[String, Double] = {
    val groupedByCategory = library.groupBy(_.category)
    groupedByCategory.mapValues(books => books.map(_.year).sum.toDouble / books.length)
  }

  println("Initial Library:")
  library.foreach(println)

  // Add a new book
  val newBook = Book("Sapiens", "Yuval Noah Harari", 2011, "Non-Fiction", 19.99)
  library = addBook(library, newBook)

  println("\nLibrary after adding a new book:")
  library.foreach(println)

  // Search for books
  val searchQuery = "George Orwell"
  val searchResults = searchBooks(library, searchQuery)

  println(s"\nSearch results for '$searchQuery':")
  searchResults.foreach(println)

  // Filter by category
  val category = "Dystopian"
  val dystopianBooks = filterByCategory(library, category)

  println(s"\nBooks in the '$category' category:")
  dystopianBooks.foreach(println)

  // Calculate statistics
  val total = totalBooks(library)
  val averageYear = averagePublicationYear(library)

  println(s"\nTotal number of books: $total")
  println(f"Average publication year: $averageYear%.2f")

  // Sort books by title
  val sortedByTitle = sortBooksByTitle(library)
  println("\nBooks sorted by title:")
  sortedByTitle.foreach(println)

  // Sort books by year
  val sortedByYear = sortBooksByYear(library)
  println("\nBooks sorted by year:")
  sortedByYear.foreach(println)

  // Group books by category
  val groupedByCategory = groupBooksByCategory(library)
  println("\nBooks grouped by category:")
  groupedByCategory.foreach { case (category, books) =>
    println(s"$category:")
    books.foreach(println)
  }

  // Partition books by year
  val (before2000, after2000) = partitionBooksByYear(library, 2000)
  println("\nBooks published before 2000:")
  before2000.foreach(println)
  println("\nBooks published after 2000:")
  after2000.foreach(println)

  // Collect titles of books published after 1950
  val titlesAfter1950 = collectTitlesAfterYear(library, 1950)
  println("\nTitles of books published after 1950:")
  titlesAfter1950.foreach(println)

  // Calculate average publication year by category
  val avgYearByCategory = averagePublicationYearByCategory(library)
  println("\nAverage publication year by category:")
  avgYearByCategory.foreach { case (category, avgYear) =>
    println(s"$category: $avgYear%.2f")
  }

  // Shopping Basket functionality
  val basket = new ShoppingBasket()
  basket.addBook(library.head)
  basket.addBook(library(1))
  basket.addBook(library(2))
  basket.showBasket()
  println(s"Total cost: ${basket.totalCost}")
}

Library with Author

case class Author(name: String, nationality: String)

case class Book(title: String, author: Author, year: Int, category: String, price: Double)

class ShoppingBasket {
  private var items: List[Book] = List()

  def addBook(book: Book): Unit = {
    items = book :: items
  }

  def totalCost: Double = {
    items.map(_.price).sum
  }

  def showBasket(): Unit = {
    println("Shopping Basket:")
    items.foreach(book => println(s"${book.title} by ${book.author.name} - ${book.price}"))
  }
}

object EnhancedLibraryManagement extends App {
  val authors = List(
    Author("George Orwell", "British"),
    Author("Harper Lee", "American"),
    Author("F. Scott Fitzgerald", "American"),
    Author("Aldous Huxley", "British"),
    Author("Herman Melville", "American"),
    Author("J.D. Salinger", "American"),
    Author("Yuval Noah Harari", "Israeli")
  )

  var library = List(
    Book("1984", authors(0), 1949, "Dystopian", 15.99),
    Book("To Kill a Mockingbird", authors(1), 1960, "Fiction", 10.99),
    Book("The Great Gatsby", authors(2), 1925, "Classic", 8.99),
    Book("Brave New World", authors(3), 1932, "Dystopian", 12.99),
    Book("Moby Dick", authors(4), 1851, "Classic", 9.99),
    Book("The Catcher in the Rye", authors(5), 1951, "Fiction", 14.99)
  )

  def addBook(library: List[Book], book: Book): List[Book] = {
    book :: library
  }

  def searchBooks(library: List[Book], query: String): List[Book] = {
    library.filter(book => book.title.contains(query) || book.author.name.contains(query))
  }

  def filterByCategory(library: List[Book], category: String): List[Book] = {
    library.filter(_.category == category)
  }

  def totalBooks(library: List[Book]): Int = {
    library.length
  }

  def averagePublicationYear(library: List[Book]): Double = {
    if (library.isEmpty) 0.0
    else library.map(_.year).sum.toDouble / library.length
  }

  def sortBooksByTitle(library: List[Book]): List[Book] = {
    library.sortBy(_.title)
  }

  def sortBooksByYear(library: List[Book]): List[Book] = {
    library.sortBy(_.year)
  }

  def groupBooksByCategory(library: List[Book]): Map[String, List[Book]] = {
    library.groupBy(_.category)
  }

  def partitionBooksByYear(library: List[Book], year: Int): (List[Book], List[Book]) = {
    library.partition(_.year < year)
  }

  def collectTitlesAfterYear(library: List[Book], year: Int): List[String] = {
    library.collect {
      case Book(title, _, y, _, _) if y > year => title
    }
  }

  def averagePublicationYearByCategory(library: List[Book]): Map[String, Double] = {
    val groupedByCategory = library.groupBy(_.category)
    groupedByCategory.mapValues(books => books.map(_.year).sum.toDouble / books.length)
  }

  println("Initial Library:")
  library.foreach(println)

  // Add a new book
  val newBook = Book("Sapiens", authors(6), 2011, "Non-Fiction", 19.99)
  library = addBook(library, newBook)

  println("\nLibrary after adding a new book:")
  library.foreach(println)

  // Search for books
  val searchQuery = "George Orwell"
  val searchResults = searchBooks(library, searchQuery)

  println(s"\nSearch results for '$searchQuery':")
  searchResults.foreach(println)

  // Filter by category
  val category = "Dystopian"
  val dystopianBooks = filterByCategory(library, category)

  println(s"\nBooks in the '$category' category:")
  dystopianBooks.foreach(println)

  // Calculate statistics
  val total = totalBooks(library)
  val averageYear = averagePublicationYear(library)

  println(s"\nTotal number of books: $total")
  println(f"Average publication year: $averageYear%.2f")

  // Sort books by title
  val sortedByTitle = sortBooksByTitle(library)
  println("\nBooks sorted by title:")
  sortedByTitle.foreach(println)

  // Sort books by year
  val sortedByYear = sortBooksByYear(library)
  println("\nBooks sorted by year:")
  sortedByYear.foreach(println)

  // Group books by category
  val groupedByCategory = groupBooksByCategory(library)
  println("\nBooks grouped by category:")
  groupedByCategory.foreach { case (category, books) =>
    println(s"$category:")
    books.foreach(println)
  }

  // Partition books by year
  val (before2000, after2000) = partitionBooksByYear(library, 2000)
  println("\nBooks published before 2000:")
  before2000.foreach(println)
  println("\nBooks published after 2000:")
  after2000.foreach(println)

  // Collect titles of books published after 1950
  val titlesAfter1950 = collectTitlesAfterYear(library, 1950)
  println("\nTitles of books published after 1950:")
  titlesAfter1950.foreach(println)

  // Calculate average publication year by category
  val avgYearByCategory = averagePublicationYearByCategory(library)
  println("\nAverage publication year by category:")
  avgYearByCategory.foreach { case (category, avgYear) =>
    println(s"$category: $avgYear%.2f")
  }

  // Shopping Basket functionality
  val basket = new ShoppingBasket()
  basket.addBook(library.head)
  basket.addBook(library(1))
  basket.addBook(library(2))
  basket.showBasket()
  println(s"Total cost: ${basket.totalCost}")
}