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 Workbook

Workbook to the Scala 3 book.

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?

  1. 1n
  2. ~n
  3. this_is_a_number
  4. n1?
  5. A1
  6. match
  7. function()

Exercise

  1. Create a val called pi and assign it the value of 3.14159:
  2. Create a val called message and assign it a string value:
  3. Create a val called age and assign it an integer value:
  4. Create a val called isTall and assign it a boolean value:
  5. Create a val called name and assign it a string value, then print a message that includes the value of name:

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

Write a program that takes an integer input and prints whether it is even or odd.

Exercise 2: Larger

Write a program that takes two integers as input and prints the larger of the two.

Exercise 3: Maximum

Write a program that takes three integers as input and prints the largest of the three.

Exercise 4: Vowel or Consonant

Write a program that takes a character input and prints whether it is a vowel or a consonant.

In this example, we use the match expression to match the input character against a set of patterns to check if it is a vowel or consonant We use the match to return a 'Vowel' or a 'Consonant'.

Exercise 5: Weekend

Write a program that takes a day of the week as input and prints whether it is a weekday or a weekend day.

In this example, we use the match expression to match the input day against multiple patterns. If the input is "Saturday" or "Sunday", the code in the first block will execute and return "Weekend". Otherwise, the code in the last block will execute and return "Weekday".

Exercise 6: Number name

Write a program that takes an integer input and prints its English name. For example, if the input is 2, the program should print "two".

Solutions

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

Create a list of integers from 1 to 10, and use a for loop with yield to create a new list containing the squares of each integer.

Exercise 2 string lengths

Create a list of strings, and use a for loop with yield to create a new list containing the lengths of each string.

Exercise 3 evens

Create a list of integers, and use a for loop with yield to create a new list containing only the even integers.

Exercise 4 tuples

Create a list of tuples containing a name and an age, and use a for loop with yield to create a new list containing only the names of people who are under 30 years old.

Exercise 5 string contains

Create a list of strings, and use a for loop with yield to create a new list containing only the strings that contain the letter "a".

String

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

Write a function that takes a list of strings and returns a new list with only the strings that contain the letter 'a'.

Exercise 2 even numbers

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

Exercise 3 string lengths

Write a function that takes a list of strings and returns a new list with the length of each string.

Exercise 4: Default Parameters

Task: Define a function greet that takes two parameters: a name of type String and a greeting of type String with a default value of "Hello". The function should print a greeting message.

Exercise 5: Named Arguments

Task: Define a function describePerson that takes three parameters: name (String), age (Int), and country (String) with a default value of "unknown". Use named arguments to call this function, specifying age and name but not country.

Exercise 6: Varargs

Task: Write a function sum that accepts an arbitrary number of integer arguments and returns their sum.

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.

Solutions

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

Task: Create a tuple representing a book. The tuple should contain three elements: the title of the book as a String, the year of publication as an Int, and a Boolean indicating whether you have read it. Then, write code to print each element of the tuple separately.

Exercise 2: Tuple Destructuring

Task: Given a tuple representing a person (firstName, lastName, age), destructure it into separate variables and print a formatted string using these variables.

Exercise 3: Using Tuples in Functions

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

Spring Boot Tutorial

1. Create a Spring Project

Goto start.spring.io and choose:

  • maven
  • add dependencies:
      • web
      • data-jpa
      • h2

2. Project Structure

Your project structure will look similar to a typical Java project but will include Scala source files:

scala-java-spring
|-- src
    |-- main
        |-- scala
            |-- controllers
               |-- TodoController.scala
            |-- model
               |-- Todo.scala   
            |-- repository
               |-- TodoRepository.scala   
        |-- resources
            |-- application.properties 
    |-- test
        |-- scala
            |-- Your Scala test files here
|-- pom.xml

3. Update the pom.xml

You need to add dependencies and plugins to your pom.xml for Scala, Spring Boot, and to integrate Scala with Maven. Below is a simplified example of what the pom.xml might include:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala3-library_3</artifactId>
            <version>3.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>

            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>4.8.1</version>
                <executions>
                    <execution>
                        <id>scala-compile-first</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>scala-test-compile</id>
                        <phase>process-test-resources</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

4. Write Your Scala Application

Inside the src/main/scala directory, create your Scala application. For example, a simple Spring Boot application in Scala might look like this:

package com.example.demo

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class DemoApplication

object DemoApplication extends App: 
  SpringApplication.run(classOf[DemoApplication])
  

5. Todo class

package com.example.demo.model

import jakarta.persistence.{Entity, GeneratedValue, GenerationType, Id}

import scala.annotation.meta.companionObject
import scala.beans.BeanProperty

@Entity
class Todo:
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @BeanProperty
  var id: Long = _

  @BeanProperty
  var task: String = _
  

6. Todo Repository

package com.example.demo.repository

import com.example.demo.model.Todo
import org.springframework.data.jpa.repository.JpaRepository

trait TodoRepository extends JpaRepository[Todo, Long]

7. Todo Controller

package com.example.demo.controllers

import com.example.demo.model.Todo
import com.example.demo.repository.TodoRepository
import org.springframework.web.bind.annotation.{GetMapping, PostMapping, RequestBody, RequestMapping, RestController}

import scala.jdk.CollectionConverters.*

@RequestMapping(path= Array("/todo"))
@RestController
class TodoController(todoRepository: TodoRepository):

  @PostMapping
  def save(@RequestBody todo : Todo): Todo = 
    todoRepository.save(todo)
  
  import java.util
  @GetMapping
  def findAll(): util.List[Todo] = 
    todoRepository.findAll()
  

8. Running Your Application

You can run your Scala-Spring Boot application using Maven commands such as mvn spring-boot:run or by running the main method directly from your IDE.

9. Integratie with Java

|-- src
    |-- main
        |-- java
            |-- OtherController.java
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OtherController {

  @GetMapping("/hello")
  String sayHello() {
    return "hello";
  }
}

And it just works!
So you can integrate Java and Scala easily.
For example by writing your tests in Scala.

10. RestClient

Add a companion object to the Todo class
So we have a decent constructor


@companionObject
object Todo: 
  def apply(task: String): Todo = 
    val todo = new Todo()
    todo.task = task
    todo
    
package com.example.demo

import com.example.demo.model.Todo
import org.springframework.http.MediaType
import org.springframework.web.client.RestClient


@main
def main(): Unit = 

  val todo = Todo("driving")  // call the companion object
  val uriBase = "http://localhost:8080"

  val response: Todo =
    RestClient.create
      .post
      .uri(uriBase + "/todo")
      .contentType(MediaType.APPLICATION_JSON)
      .body(todo)
      .retrieve
      .body(classOf[Todo])

  println(response.task)
  

You could put it in: src --> test --> scala --> TodoRestClient.scala

Exercises

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

Exercise 1 Begin

Create new project.
Create a @main function
Add the code and run

Exercise 2 Welcome

Ask for a user name
Print a welcome message with the name: Hello, [name]

Exercise 3 Add

Ask the user for two numbers n1 and n2
Add and print the numbers
Hint: use readInt()
Next divide the numbers What do you see. Is this OK?
Read a Double Hint use toDouble()

Exercise 4 Menu

Create een choice menu with the next layout

Menu Choose

(1) - print hello
(2) - print bye

Your choice:    1
hello

Exercise 5 Calculator

Create a calculator that can add, subtract, multiply and divide
Use the next layout

Calculator

Give the first number: 3
Give the second number: 4
Give the operator: *
3 * 4 = 12

Exercise 6 Drawing

We will use nested for-loops to draw shapes.

for i <- 0 to 3 do
  for j <- 0 to 3 do
    print("* ")
    println()
println()

An example of a rectangle





Draw the next shapes

*
* *
* * *  
* * * * 

* * * * 
* * *  
* *
*

* * * * 
  * * *  
    * * 
      *  

*
  *
    *
      *

Exercise 7 List

Create list in the range from 0 to 10
Print de list with a for-loop

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

Reverse and print the list
Add all the values
Calculate the average value

Exercise 8 String

Ask the user to give a text string

Calculate the length of the text.
Split de text in words Hint use: split
How many words are in the tekst.

Exercise 9 Function

Write a function that add two numbers.
Write function log(text: String) print the date and the text.

Projects

Create a new scala project in IntelliJ

Add scala-swing to sbt

// build.sbt
ThisBuild / version := "0.1.0-SNAPSHOT"

ThisBuild / scalaVersion := "3.3.3"

lazy val root = (project in file("."))
  .settings(
    name := "untitled"
  )

libraryDependencies += "org.scala-lang.modules" %% "scala-swing" % "3.0.0"

Project 1: Celcius Fahrenheit Converter


import scala.swing._
import scala.swing.event.ButtonClicked

object CelsiusFahrenheitConverter extends SimpleSwingApplication {
  def top = new MainFrame {
    title = "Celsius to Fahrenheit Converter"

    // Components
    val textField = new TextField { columns = 10 }
    val convertButton = new Button { text = "Convert" }
    val resultLabel = new Label { text = "Result: " }

    // Layout
    contents = new FlowPanel {
      contents += new Label("Celsius: ")
      contents += textField
      contents += convertButton
      contents += resultLabel
    }

    // Logic for conversion
    listenTo(convertButton)
    reactions += {
      case ButtonClicked(_) =>
        val celsius = textField.text.toDouble
        val fahrenheit = celsius * 9/5 + 32
        resultLabel.text = f"Result: $fahrenheit%.2f Fahrenheit"
    }

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

Project 2a: Number guess

Write a number guess game on the command line

  • generate a random
  • ask the user to guess the number
  • tell the user if the number is smaller, larger or equal.

Project 2b: Number guess

Write a number guess game as a swing application

Project 3a: Todo List

Create a todo list on the command line

Project 3b: Todo List

Create a todo list as a swing application

Project 4: TicTacToe

Do this one after the Project: Calculator

Project: Calculator

Step 1 Project

Create a new scala project in IntelliJ

Add scala-swing to build.sbt

libraryDependencies += "org.scala-lang.modules" %% "scala-swing" % "3.0.0"

It will only work with Scala 2.13. So we'll have to use the Scala 2 syntax (e.g. braces, new) The compiler is backwards compatible, so it will work with Scala 3.

Step 2: MainFrame

import scala.swing.*

@main
def calculator(): Unit = {
  new MainFrame() {
    title = "Hello world"
    size = new Dimension(300, 300)
    centerOnScreen()
    open()
  }
}

Step 3: Add Components

val label = new Label("hello, world")
val button = new Button("OK")

contents = new BorderPanel {
  add(label, BorderPanel.Position.North)
  add(button, BorderPanel.Position.South)
}

Step 4: Event Handling

button.reactions += {
  case ButtonClicked(e) => click(e)
}

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

Step 5: More Buttons

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


val buttonGrid = new GridPanel(4, 4)

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

Step 6: State Machine

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")
var num1 = 0
var operator = ""

Step 7: Calculate

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
}

Step 8: Layout

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

Quiz

Question 1: Variable Declarations

Consider the following Scala code snippet:

val x: Int = 10
var y: Int = 20
y = x + 10
val z: Int = y + 10

What is the value of z at the end of this execution?

A) 20
B) 30
C) 40
D) 50

Question 2: Function Definition

Review the Scala function below:

def multiply(a: Int, b: Int): Int = a * b

What does this function do?

A) Adds two numbers
B) Subtracts two numbers
C) Multiplies two numbers
D) Divides two numbers

Question 3: Immutable List Operations

Given the Scala code:

val numbers = List(1, 2, 3, 4, 5)
val doubled = numbers.map(_ * 2)

What does the doubled list contain?

A) List(2, 4, 6, 8, 10)
B) List(1, 2, 3, 4, 5)
C) List(0, 2, 4, 6, 8)
D) List(3, 4, 5, 6, 7)

Question 4: Pattern Matching

Consider this Scala code snippet using pattern matching:

val greeting = "Hello"

greeting match {
  case "Hello" => "World"
  case "Goodbye" => "Everyone"
  case _ => "Unknown"
}

What is the result of the pattern matching?

A) World
B) Everyone
C) Unknown
D) Error

Question 5: Variable Mutability

Which keyword is used to declare a mutable variable in Scala?

A) let
B) var
C) val
D) const

Question 6: Scala's Type Inference

What does Scala's type inference feature allow you to do?

A) Automatically detect errors in your code
B) Infer the type of variables at runtime
C) Omit the type of a variable when it is declared
D) Change the type of a variable after it is initialized

Question 7: Function Declarations

How do you declare a function in Scala that takes two integers and returns their sum?

A) def sum(x: Int, y: Int): Int = { return x + y }
B) function sum(x: Int, y: Int): Int = x + y
C) def sum(x: Int, y: Int) => x + y
D) Both A and C are correct

Question 8: Using a while Loop

Consider the following Scala code snippet:

var sum = 0
var i = 1
while (i <= 5) {
  sum += i
  i += 1
}
println(sum)

What is the output of this code?

A) 10
B) 15
C) 5
D) 20

Question 9: Iterating with for Loop

Analyze the Scala code below:

var product = 1
for (i <- 1 to 4) {
  product *= i
}
println(product)

What does this code print?

A) 10
B) 24
C) 12
D) 4

Question 10: Nested for Loops

Consider the following Scala code snippet that uses nested for loops:

val rows = 3
val cols = 2
for (row <- 1 to rows) {
  for (col <- 1 to cols) {
    print(s"($row,$col) ")
  }
  println()
}

What does this code print?

A) (1,1) (1,2)
(2,1) (2,2)
(3,1) (3,2)
B) (1,1) (2,1) (3,1)
(1,2) (2,2) (3,2)
C) (1,1) (1,2) (2,1) (2,2) (3,1) (3,2)
D) None of the above

Question 11: Basic if-else Logic

Consider the Scala code snippet below:

val age = 20
val canVote = if (age >= 18) {
  println("You can vote.")
  true
} else {
  println("You cannot vote.")
  false
}
println(s"Can vote: $canVote")

What is the output of this code?

A) You can vote.
Can vote: true
B) You cannot vote.
Can vote: false
C) You can vote.
Can vote: false
D) Compilation error.

Question 12: Nested if-else with Logical Operators

Review the Scala code snippet involving nested if-else and logical operators:

val age = 25
val hasLicense = true
if (age >= 18) {
  if (hasLicense) {
    println("You can drive.")
  } else {
    println("You cannot drive without a license.")
  }
} else {
  println("You are too young to drive.")
}

What is the output of this program?

A) You can drive.
B) You cannot drive without a license.
C) You are too young to drive.
D) None of the above

Question 13: if-else with Compound Conditions

Consider the following Scala code snippet:

val temperature = 30
val isRaining = false
if (temperature > 25 && !isRaining) {
  println("It's a good day for a walk.")
} else if (temperature > 25 && isRaining) {
  println("It's warm, but it's raining.")
} else {
  println("Stay indoors.")
}

What does this code print?

A) It's a good day for a walk.
B) It's warm, but it's raining.
C) Stay indoors.
D) Compilation error.

Question 14: Complex if-else with Function Calls

def checkEligibility(age: Int, hasPermit: Boolean): String = {
  if (age >= 18) {
    if (hasPermit) "Eligible" else "Not eligible: No permit"
  } else {
    "Not eligible: Too young"
  }
}

val result = checkEligibility(17, hasPermit = true)
println(result)

What is the output of this code?

A) Eligible
B) Not eligible: No permit
C) Not eligible: Too young
D) Compilation error

Question 15: Basic Pattern Matching

Consider the following Scala code snippet:

val number = 3
val result = number match {
  case 1 => "One"
  case 2 => "Two"
  case 3 => "Three"
  case _ => "Other"
}
println(result)

What is the output of this code?

A) One
B) Two
C) Three
D) Other

Question 16: Defining and Calling a Simple Function

def greet(name: String): String = {
  return "Hello, " + name + "!"
}

val greeting = greet("Alice")
println(greeting)

What is the output of this code?

A) Hello, Alice
B) Hello, Alice!
C) Greet: Alice
D) None of the above

Object Oriented

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

Task: Define a case class named Book with two fields: title (String) and authors (List[String]). Create instances of this class for three different books.

Exercise 2: Pattern Matching on Case Classes

Task: Write a function describeBook that accepts a Book and returns a string describing the book. If the book has more than one author, the description should note that it's written by multiple authors.

Exercise 3: Copying and Modifying Case Classes

Task: Using the Book case class from Exercise 1, create a copy of book1 but with an additional author "Venners Bill". Use the copy method provided by case classes.

Exercise 4: Case Classes in Collections

Task: Given a list of books, write a function titlesByAuthor that takes an author's name and returns a list of titles by that author.

Pattern Matching

Exercise 1: Basic Case Class and Pattern Matching

Task: Define a case class for a Person with two fields: name (String) and age (Int). Write a function that accepts a Person and returns a greeting message that varies based on the person's age.

Exercise 2: Using Case Classes in Collections

Enum

Exercises

Scala 3 introduces enums as a first-class feature to define a group of named values. Enums are a significant improvement over Scala 2's sealed trait and case object pattern for enumerations, providing a cleaner syntax and more functionality. Here are some exercises to help you understand and practice using enums in Scala 3.

Exercise 1: Basic Enum

Task: Define an enum DayOfWeek representing the days of the week. Then, write a function isWeekend that takes a DayOfWeek and returns true if it's either Saturday or Sunday, and false otherwise.

Exercise 2: Enum with Parameters

Task: Define an enum TrafficLight where each value can have a color name as a String parameter. Implement a method color that returns the color of the traffic light.

Exercise 3: Enums in Collections

Task: Given a list of TrafficLight enums, write a function that counts how many of each light are in the list and prints the result.

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

Step 1 Paint Rectangle

import java.awt.{Color, Graphics2D}
import scala.swing.BorderPanel.Position.Center
import scala.swing.{BorderPanel, Dimension, MainFrame, Panel, SimpleSwingApplication}

object Scribble1 extends SimpleSwingApplication:
  def top = new MainFrame:
    title = "Scribble App"

    object canvas extends Panel:
      override def paintComponent(g: Graphics2D): Unit =
        super.paintComponent(g)
        g.setColor(Color.black)
        g.drawRect(20, 20, 20, 20)

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

step 2 Mouse Events

add the mouse events to the canvas

object canvas extends Panel:
  listenTo(mouse.clicks)
  var x1 = 0
  var y1 = 0
  var x2 = 0
  var y2 = 0

  reactions +=
    case e: MousePressed =>
      x1 = e.point.x
      y1 = e.point.y

    case e: MouseReleased =>
      x2 = e.point.x
      y2 = e.point.y
      repaint()

step 3: Rectangle class

class Rect(var x1: Int, var y1: Int, var x2: Int, var y2: Int):
  def draw(g: Graphics2D): Unit =
    g.drawRect(x1, y1, x2 - x1, y2 - y1)

step 4 Shape trait

trait Shape:
  var x1: Int
  var y1: Int
  var x2: Int
  var y2: Int
  def draw(g: Graphics2D): Unit

Also create an Oval class

step 5 More shapes

private val shapes: ListBuffer[Shape] = ListBuffer()
case e: MousePressed =>
  if e.triggersPopup then
    shp = Oval(e.point.x, e.point.y, 0, 0)
  else
    shp = Rect(e.point.x, e.point.y, 0, 0)

  shapes.append(shp)

step 6 Button Panel

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
  }
  // backticks: In pattern matching, it helps you to use variable as if its a concrete value.

Exercises

Exercise 1: Basic List Operations

Create a list of integers from 1 to 10. Then, perform the following tasks:

  1. Extract the first element.
  2. Extract the last element.
  3. Get all elements except the first.
  4. Get all elements except the last.
  5. Check if the list contains the number 5.

Exercise 2: Concatenation and Addition

  1. Create two lists: one with even numbers from 2 to 10, and another with odd numbers from 1 to 9.
  2. Concatenate these lists into a single list.
  3. Add the number 0 to the beginning of the concatenated list.

Exercise 3: Mapping and Filtering

Given a list of numbers from 1 to 10:

  1. Multiply each number by 2.
  2. Filter the resulting list to keep only numbers greater than 10.

Exercise 4: Folding and Reducing

  1. Create a list of integers from 1 to 5.
  2. Compute the sum of all elements using foldLeft.
  3. Compute the product of all elements using reduce.

Exercise 5: Using ListBuffer

  1. Create a mutable ListBuffer and add elements 1, 2, and 3 to it.
  2. Append the element 4 to the list buffer.
  3. Remove the element 2 from the list buffer.
  4. Convert the ListBuffer to an immutable list.

Exercise 6: Working with Arrays

  1. Create an array of integers from 1 to 5.
  2. Update the third element (index 2) to 10.
  3. Print the length of the array.
  4. Iterate over the array and print each element.

Exercise 7: Zipping and Unzipping

  1. Create two lists: one with numbers 1 to 3, and another with the strings "one", "two", and "three".
  2. Zip these lists together.
  3. Unzip the zipped list back into two separate lists.

Exercise 8: Using Range to Create Lists

  1. Create a list of numbers from 1 to 10 using Range.
  2. Create a list of even numbers from 2 to 20 using Range.
  3. Create a list of numbers from 10 to 1 (in reverse order) using Range.

Exercise 9: LazyList

  1. Create a LazyList of the first 10 positive integers.
  2. Define a LazyList that generates an infinite sequence of even numbers.
  3. Take the first 10 elements from the infinite LazyList and print them.

Exercise 10: Grouping and Partitioning

  1. Create a list of integers from 1 to 10.
  2. Group the integers by whether they are even or odd.
  3. Partition the list into two lists: one containing numbers less than or equal to 5, and another containing numbers greater than 5.

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

Objective: Find all books written by a specific author.

def findBooksByAuthor(library: List[Book], authorName: String): List[Book] = {
  // TODO
}

val booksByOrwell = findBooksByAuthor(library, "George Orwell")
println(s"Books by George Orwell: ${booksByOrwell.map(_.title)}")

Use Case 2: Filtering Books Based on Price

Objective: Filter books that are within a certain price range.

def filterBooksByPrice(library: List[Book], minPrice: Double, maxPrice: Double): List[Book] = {
  // TODO
}

val affordableBooks = filterBooksByPrice(library, 10.0, 15.0)
println(s"Affordable books: ${affordableBooks.map(_.title)}")

Use Case 3: Filtering Books by Category

Objective: Filter books belonging to a specific category.

def filterBooksByCategory(library: List[Book], category: String): List[Book] = {
  // TODO
}

val dystopianBooks = filterBooksByCategory(library, "Dystopian")
println(s"Dystopian books: ${dystopianBooks.map(_.title)}")

Use Case 4: Displaying Book Titles with Prices

Objective: Create a display list of book titles along with their prices.

def displayBookTitlesWithPrices(library: List[Book]): List[String] = { 
  // TODO
}

val bookTitlesWithPrices = displayBookTitlesWithPrices(library)
bookTitlesWithPrices.foreach(println)

Use Case 5: Finding Books Published After a Certain Year

Objective: Find all books published after a specified year.

def findBooksPublishedAfter(library: List[Book], year: Int): List[Book] = {
  // TODO
}

val modernBooks = findBooksPublishedAfter(library, 1950)
println(s"Books published after 1950: ${modernBooks.map(_.title)}")

Use Case 6: Creating a Summary Description for Books

Objective: Create a summary description for each book, combining title, author, and category.

def createBookSummaries(library: List[Book]): List[String] = {
  // TODO
}

val bookSummaries = createBookSummaries(library)
bookSummaries.foreach(println)

Use Case 7: Finding Discounted Books

Objective: Apply a discount to all books and return the new prices.

def applyDiscount(library: List[Book], discountRate: Double): List[(String, Double)] = {
  // TODO
}

val discountedBooks = applyDiscount(library, 0.10)
discountedBooks.foreach { case (title, price) => println(s"$title - $price") }

Exercises Bookstore collect

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: Collecting Books with Descriptions

Objective: Extract the titles and descriptions of books that have descriptions.

def collectBooksWithDescriptions(library: List[Book]): List[String] = {
// TODO
}

val booksWithDescriptions = collectBooksWithDescriptions(library)
booksWithDescriptions.foreach(println)

Use Case 2: Calculating Total Value of Books

Objective: Calculate the total value of all books in the library using foldLeft.

def calculateTotalValue(library: List[Book]): Double = {
  // TODO
}

val totalValue = calculateTotalValue(library)
println(s"Total value of books: $$${totalValue}")

Use Case 3: Finding the Oldest Book

Objective: Find the oldest book in the library using foldLeft.

def findOldestBook(library: List[Book]): Option[Book] = {
  // TODO
}

val oldestBook = findOldestBook(library)
oldestBook match {
  case Some(book) => println(s"Oldest book: ${book.title}, published in ${book.year}")
  case None => println("No books in the library")
}

Use Case 4: Grouping Books by Decade

Objective: Group books by the decade they were published using collect.

def groupBooksByDecade(library: List[Book]): Map[Int, List[Book]] = {
  // TODO
}

val booksByDecade = groupBooksByDecade(library)
booksByDecade.foreach { case (decade, books) =>
  println(s"$decade:")
  books.foreach(book => println(s"  ${book.title}"))
}

Use Case 5: Counting Books by Category

Objective: Count the number of books in each category using foldLeft.

def countBooksByCategory(library: List[Book]): Map[String, Int] = {
  // TODO
}

val booksCountByCategory = countBooksByCategory(library)
booksCountByCategory.foreach { case (category, count) =>
  println(s"$category: $count books")
}

Exercises Bookstore zip

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: Creating Pairs of Book Titles and Prices with zip

Objective: Pair each book title with its price.

def pairTitlesWithPrices(library: List[Book]): List[(String, Double)] = {
 // TODO
}

val titlesWithPrices = pairTitlesWithPrices(library)
titlesWithPrices.foreach { case (title, price) => println(s"$title: $$${price}") }

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

Objective: Pair each book title with its index in the list.

def pairTitlesWithIndexes(library: List[Book]): List[(String, Int)] = {
  // TODO
}

val titlesWithIndexes = pairTitlesWithIndexes(library)
titlesWithIndexes.foreach { case (title, index) => println(s"$index: $title") }

Use Case 3: Matching Authors to Their Books with zip

Objective: Pair each author with their respective book titles.

def matchAuthorsToBooks(library: List[Book]): List[(String, List[String])] = {
  // TODO
}

val authorsWithBooks = matchAuthorsToBooks(library)
authorsWithBooks.foreach { case (author, books) =>
  println(s"$author: ${books.mkString(", ")}")
}

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

Objective: Pair each book title with its publication year.

def pairTitlesWithYears(library: List[Book]): List[(String, Int)] = {
  // TODO
}

val titlesWithYears = pairTitlesWithYears(library)
titlesWithYears.foreach { case (title, year) => println(s"$title: $year") }

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

Objective: Create pairs of original and discounted prices for each book.

def pairOriginalWithDiscountedPrices(library: List[Book], discountRate: Double): List[(Double, Double)] = {
  // TODO
}

val originalWithDiscountedPrices = pairOriginalWithDiscountedPrices(library, 0.10)
originalWithDiscountedPrices.foreach { case (original, discounted) => println(f"Original: $$${original}%.2f, Discounted: $$${discounted}%.2f") }

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

Objective: Pair each book title with its index to create a display list.

def displayBooksWithIndexes(library: List[Book]): List[String] = {
  // TODO
}

val booksWithIndexes = displayBooksWithIndexes(library)
booksWithIndexes.foreach(println)

Exercises Bookstore GroupBy

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: Grouping Books by Category

Objective: Group all books by their category.

def groupBooksByCategory(library: List[Book]): Map[String, List[Book]] = {
  // TODO 
}

val booksByCategory = groupBooksByCategory(library)
booksByCategory.foreach { case (category, books) =>
  println(s"$category:")
  books.foreach(book => println(s"  ${book.title} by ${book.author.name}"))
}

Use Case 2: Grouping Books by Author

Objective: Group all books by their author.

def groupBooksByAuthor(library: List[Book]): Map[String, List[Book]] = {
  // TODO 
}

val booksByAuthor = groupBooksByAuthor(library)
booksByAuthor.foreach { case (author, books) =>
  println(s"$author:")
  books.foreach(book => println(s"  ${book.title} [${book.category}]"))
}

Use Case 3: Grouping Books by Decade

Objective: Group all books by the decade they were published.

def groupBooksByDecade(library: List[Book]): Map[Int, List[Book]] = {
  // TODO 
}

val booksByDecade = groupBooksByDecade(library)
booksByDecade.foreach { case (decade, books) =>
  println(s"$decade:")
  books.foreach(book => println(s"  ${book.title} by ${book.author.name}"))
}

Use Case 4: Grouping Books by Price Range

Objective: Group all books by price range (e.g., <$10, $10-$20, >$20).

def groupBooksByPriceRange(library: List[Book]): Map[String, List[Book]] = {
  // TODO 
}

val booksByPriceRange = groupBooksByPriceRange(library)
booksByPriceRange.foreach { case (priceRange, books) =>
  println(s"$priceRange:")
  books.foreach(book => println(s"  ${book.title} by ${book.author.name} - $$${book.price}"))
}

Use Case 5: Grouping Books by Availability of Description

Objective: Group all books by whether they have a description or not.

def groupBooksByDescription(library: List[Book]): Map[String, List[Book]] = {
  // TODO 
}

val booksByDescription = groupBooksByDescription(library)
booksByDescription.foreach { case (descriptionStatus, books) =>
  println(s"$descriptionStatus:")
  books.foreach(book => println(s"  ${book.title} by ${book.author.name}"))
}

Exercises Bookstore more

Use Case 1: Partition Books by Availability of Description

Objective: Partition books into two lists: those with a description and those without.

def partitionBooksByDescription(library: List[Book]): (List[Book], List[Book]) = {
  // TODO
}

val (booksWithDescription, booksWithoutDescription) = partitionBooksByDescription(library)
println("Books with Description:")
booksWithDescription.foreach(book => println(s"${book.title} by ${book.author.name}"))

println("\nBooks without Description:")
booksWithoutDescription.foreach(book => println(s"${book.title} by ${book.author.name}"))

Use Case 2: Find the Most Expensive Book

Objective: Find the most expensive book in the library.

def findMostExpensiveBook(library: List[Book]): Option[Book] = {
  // TODO
}

val mostExpensiveBook = findMostExpensiveBook(library)
mostExpensiveBook match {
  case Some(book) => println(s"Most expensive book: ${book.title} by ${book.author.name} - $$${book.price}")
  case None => println("No books in the library")
}

Use Case 3: Get Distinct Authors

Objective: Get a list of distinct authors in the library.

def distinctAuthors(library: List[Book]): List[Author] = {
  // TODO
}

val authorsList = distinctAuthors(library)
println("Distinct Authors:")
authorsList.foreach(author => println(s"${author.name} (${author.nationality})"))

Use Case 4: Flatten Nested List of Book Lists

Objective: Flatten a list of lists of books into a single list of books.

val moreBooks = List(
  List(Book("Sapiens", authors(6), 2011, "Non-Fiction", 19.99, Some("A brief history of humankind."))),
  List(Book("Homo Deus", authors(6), 2015, "Non-Fiction", 21.99, Some("A brief history of tomorrow."))),
  List(Book("Brave New World", authors(3), 1932, "Dystopian", 12.99, None))
)

def flattenBookLists(bookLists: List[List[Book]]): List[Book] = {
  // TODO
}

val allBooks = flattenBookLists(moreBooks)
println("All Books:")
allBooks.foreach(book => println(s"${book.title} by ${book.author.name}"))

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

Objective: Extract the titles of books written by a specific author using flatMap.

def titlesByAuthor(library: List[Book], authorName: String): List[String] = {
  // TODO
}

val orwellTitles = titlesByAuthor(library, "George Orwell")
println("Titles by George Orwell:")
orwellTitles.foreach(println)

Use Case 6: Calculate Total Price of Books Using foldLeft

Objective: Calculate the total price of all books in the library using foldLeft.

def totalPrice(library: List[Book]): Double = {
  // TODO
}

val totalCost = totalPrice(library)
println(f"Total cost of all books: $$${totalCost}%.2f")

Use Case 7: Combine Books and Authors into a Map

Objective: Create a map of authors to their books.

def mapAuthorsToBooks(library: List[Book]): Map[Author, List[Book]] = {
  // TODO
}

val authorBooksMap = mapAuthorsToBooks(library)
authorBooksMap.foreach { case (author, books) =>
  println(s"${author.name}'s books:")
  books.foreach(book => println(s"  ${book.title}"))
}

Project: Bookstore

Let's build a simple version with functionalities to

  • add books
  • list books
  • add customers
  • list customers
  • place orders

Define the Bookstore Structure

Create a Scala object to hold your application logic. Create a new file in the src/main/scala directory named BookstoreApp.scala.

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

object BookstoreApp {

  // TODO: case class Book, Customer, Order

  // TODO: Mutable lists of books, customers and orders

  
  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 = {
    // TODO
  }

  def listBooks(): Unit = {
     // TODO
  }

  def addCustomer(): Unit = {
    // TODO
  }

  def listCustomers(): Unit = {
    // TODO
  }

  def placeOrder(): Unit = {
    // TODO
  }

  def listOrders(): Unit = {
   // TODO
}

Structure

  1. Case Classes:
  • Book: Represents a book with id, title, author, and price.
  • Customer: Represents a customer with id, name, and email.
  • Order: Represents an order with id, customerId, and bookId.
  1. Lists:
  • books: A mutable list to hold books.
  • customers: A mutable list to hold customers.
  • orders: A mutable list to hold orders.
  1. Counters:
  • nextBookId: A counter to assign unique IDs to books.
  • nextCustomerId: A counter to assign unique IDs to customers.
  • nextOrderId: A counter to assign unique IDs to orders.
  1. Methods:
  • addBook(): Allows adding a new book.
  • listBooks(): Lists all books.
  • addCustomer(): Allows adding a new customer.
  • listCustomers(): Lists all customers.
  • placeOrder(): Allows placing an order.
  • listOrders(): Lists all orders.

Quiz

Question 1: Collection Filtering

Review the Scala code below:

val nums = List(-1, 2, -3, 4, 5)
val positiveNums = nums.filter(_ > 0)

What does positiveNums contain?

A) List(-1, -3)
B) List(2, 4, 5)
C) List(1, 2, 3, 4, 5)
D) List(-3, -1, 5, 4, 2)

Question 2: Tuples and Destructuring

Consider the Scala tuple:

val person = ("John", 30)

How do you access the age from the person tuple?

A) person(1)
B) person._2
C) person[1]
D) person.age

Question 3: Defining Classes

How do you correctly define a simple class in Scala that takes one parameter?

A) class MyClass(param: Type)
B) def MyClass(param: Type)
C) object MyClass { val param: Type }
D) new class MyClass(param: Type)

Question 4: Scala Collections

Which of the following collections is immutable?

A) ArrayBuffer
B) List
C) Array
D) ListBuffer

Question 5: Object-Oriented Programming

Which of the following statements about Scala is true?

A) Scala supports multiple inheritance through classes.
B) Scala does not allow the use of generic types.
C) Scala allows defining singleton objects using the object keyword.
D) Scala classes cannot have parameters.

Question 68: Defining and Instantiating a Simple Class

Consider the following Scala code snippet:

class Person(name: String, age: Int) {
  def greet(): String = s"Hello, my name is $name and I am $age years old."
}

val alice = new Person("Alice", 30)
println(alice.greet())

val bob = new Person("Bob", 25)
println(bob.greet())

What is the output of this code?

A) Hello, my name is Alice and I am 30 years old.
Hello, my name is Bob and I am 25 years old.
B) Compilation error
C) Hello, my name is Person and I am 30 years old.
Hello, my name is Person and I am 25 years old.
D) None of the above

Question 6: Class with Private Members

Review this Scala code snippet for a class with private members and methods:

class Counter {
  private var count = 0

  private def checkLimit(): Boolean = count < 10

  def increment(): Unit = {
    if (checkLimit()) count += 1
  }

  def getCount(): Int = count
}

val myCounter = new Counter()
for (_ <- 1 to 15) myCounter.increment()
println(myCounter.getCount())

What is the output of this program?

A) 10
B) 15
C) 0
D) Compilation error

Question 8: Overriding Methods in Subclasses

Consider the following Scala code snippet demonstrating method overriding:

class Animal {
  def sound(): String = "Some sound"
}

class Dog extends Animal {
  override def sound(): String = "Woof"
}

val myAnimal = new Animal()
println(myAnimal.sound())

val myDog = new Dog()
println(myDog.sound())

What does this code print?

A) Some sound
Woof
B) Woof
Woof
C) Some sound
Some sound
D) Compilation error

Question 9: Abstract Classes and Traits

Analyze the Scala code snippet involving an abstract class and its implementation:

abstract class Shape {
  def area: Double
}

class Circle(radius: Double) extends Shape {
  def area: Double = Math.PI * radius * radius
}

val circle = new Circle(5)
println(f"Circle area: ${circle.area}%.2f")

What is the output of this code?

A) Circle area: 78.54
B) Circle area: 25.00
C) Compilation error
D) None of the above

Question 10: Defining and Implementing Traits

Consider the following Scala code snippet demonstrating a trait and its implementation:

trait Greeter {
  def greet(name: String): Unit
}

class FormalGreeter extends Greeter {
  def greet(name: String): Unit = {
    println(s"Good day, $name.")
  }
}

val greeter = new FormalGreeter()
greeter.greet("Alice")

What is the output of this code?

A) Good day, Alice.
B) Hello, Alice.
C) Compilation error
D) None of the above

Question 11: Mixing in Multiple Traits

Analyze the Scala code snippet below, which mixes multiple traits into a class:

trait Walker {
  def walk(): Unit = println("Walking...")
}

trait Runner {
  def run(): Unit = println("Running...")
}

class Person extends Walker with Runner

val p = new Person()
p.walk()
p.run()

What does this code print?

A) Walking...
Running...
B) Compilation error
C) Walking...
D) Running...

Question 12: Overriding Trait Methods in Classes

Review the following Scala code snippet involving overriding trait methods in a class:

trait Animal {
  def makeSound(): Unit
}

class Dog extends Animal {
  override def makeSound(): Unit = {
    println("Woof")
  }
}

val myDog = new Dog()
myDog.makeSound()

What is the output of this program?

A) Woof
B) Compilation error
C) The program prints nothing
D) None of the above

Question 13: Abstract and Concrete Methods in Traits

Consider the Scala code snippet demonstrating a trait with both abstract and concrete methods:

trait Calculator {
  def add(a: Int, b: Int): Int
  def subtract(a: Int, b: Int): Int = a - b
}

class BasicCalculator extends Calculator {
  def add(a: Int, b: Int): Int = a + b
}

val calc = new BasicCalculator()
println(calc.add(5, 3))
println(calc.subtract(5, 3))

What does this code print?

A) 8
2
B) Compilation error
C) 8
D) 2

Question 14: Combining map and filter

Review the Scala code snippet involving a combination of map and filter:

val names = List("Alice", "Bob", "Charlie", "David")
val filteredNames = names.filter(_.length > 4).map(_.toUpperCase)

println(filteredNames)

What is the output of this program?

A) List("ALICE", "CHARLIE", "DAVID")
B) List("BOB", "DAVID")
C) List("ALICE", "CHARLIE")
D) List("CHARLIE", "DAVID")

Question 15: Matching Tuples

Review the Scala code snippet involving matching tuples:

val pair = (1, "apple")
val description = pair match {
  case (i, s) if i == 1 => s"One $s"
  case (i, s) if i > 1 => s"$i ${s}s"
  case _ => "Unknown"
}
println(description)

What is the output of this program?

A) One apple
B) 1 apples
C) Unknown
D) Compilation error

Question 16: Matching with Lists

Consider the following Scala code snippet that matches lists:

val myList = List(1, 2, 3)
val listDescription = myList match {
  case List(_, _, _) => "A list with three elements."
  case List(_, _) => "A list with two elements."
  case _ => "A list with a different number of elements."
}
println(listDescription)

What does this code print?

A) A list with three elements.
B) A list with two elements.
C) A list with a different number of elements.
D) None of the above

Question 17: Matching with Guards

Examine the Scala code snippet involving match expressions with guards:

val age = 25
val accessLevel = age match {
  case a if a >= 18 && a < 21 => "Partial access"
  case a if a >= 21 => "Full access"
  case _ => "No access"
}
println(accessLevel)

What is the output of this code?

A) Partial access
B) Full access
C) No access
D) Compilation error

Question 18: For-Comprehension with Multiple Generators

Review this Scala for-comprehension involving multiple generators:

val xValues = List(1, 2, 3)
val yValues = List(4, 5)

val pairs = for {
  x <- xValues
  y <- yValues
} yield (x, y)

println(pairs)

What does this code print?

A) List((1,4), (1,5), (2,4), (2,5), (3,4), (3,5))
B) List((1,4), (2,5), (3,4))
C) List([1,4], [1,5], [2,4], [2,5], [3,4], [3,5])
D) Compilation error

Function values

Exercise 1: Filter List with Function Parameter

Task: Implement a function filterList that takes a List[Int] and a predicate function from Int to Boolean. It should return a new list containing only the elements that satisfy the predicate.

Exercise 2: Implement a Custom map Function

Task: Define a function mapList that behaves like the map method for lists. It should take a List[A] and a function from A to B, and return a List[B].

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

Task: Write a higher-order function multiplier that takes an Int and returns a new function that takes an Int and multiplies it by the first number.

Exercise 4: Sorting with a Custom Comparator

Task: Write a function sortWithFunction that takes a List[A] and a comparison function (A, A) => Boolean which returns true if the first element should come before the second. It should return a list sorted according to this function.

Solutions

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 = 
  // TODO

// 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 = 
  // TODO

// 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 = {
	// TODO
}

// 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] =
	// TODO

// 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 =
  // TODO

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

tail-recursion

Option

Exercise 1

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

Exercise 2

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

Exercise 3

Write a vector with the number 1 to 10

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

Write a function indexOf that

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

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

indexOf(2, vector) match ???

Exercise 4

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

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

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.

Quiz

Question 1: Basic try-catch Usage

Consider the following Scala code snippet demonstrating basic usage of try-catch:

try {
  val result = 10 / 0
  println(result)
} catch {
  case e: ArithmeticException => println("Cannot divide by zero.")
} finally {
  println("Operation attempted.")
}

What is the output of this code?

A) Cannot divide by zero.
Operation attempted.
B) Operation attempted.
C) 10 / 0
Operation attempted.
D) Compilation error

Question 2: Catching Multiple Exceptions

Analyze the Scala code snippet below that catches multiple types of exceptions:

try {
  val arr = Array(1, 2, 3)
  println(arr(10))
} catch {
  case e: ArithmeticException => println("Arithmetic exception caught.")
  case e: ArrayIndexOutOfBoundsException => println("Array index out of bounds.")
} finally {
  println("Search attempted.")
}

What does this code print?

A) Arithmetic exception caught.
Search attempted.
B) Array index out of bounds.
Search attempted.
C) Search attempted.
D) Compilation error

Question 3: Using try-catch with a Return Value

Review the Scala code snippet involving try-catch with a return value:

def safeDivide(a: Int, b: Int): String = {
  try {
    val result = a / b
    s"Result: $result"
  } catch {
    case e: ArithmeticException => "Error: Division by zero."
  } finally {
    println("Division operation processed.")
  }
}

println(safeDivide(10, 0))
println(safeDivide(10, 5))

What is the output of this program?

A) Division operation processed.
Error: Division by zero.
Division operation processed.
Result: 2
B) Error: Division by zero.
Result: 2
C) Division operation processed.
Division operation processed.
D) Compilation error

Question 4: Using Option for Safe Value Access

Consider the following Scala code snippet demonstrating the use of Option to safely handle potentially missing values:

def safeFindFirstChar(str: String): Option[Char] = {
  if (str.isEmpty) None
  else Some(str.charAt(0))
}

val charA = safeFindFirstChar("apple")
println(charA)

val charB = safeFindFirstChar("")
println(charB)

What is the output of this code?

A) Some(a)
None
B) a
None
C) Some(a)
""
D) None of the above

Question 5: Handling Multiple Errors with Either

Analyze the Scala code snippet below that uses Either to handle multiple types of errors:

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

val result1 = divide(10, 2)
println(result1)

val result2 = divide(10, 0)
println(result2)

What does this code print?

A) Right(5)
Left("Cannot divide by zero.")
B) 5
Cannot divide by zero.
C) Right(5)
Right("Cannot divide by zero.")
D) None of the above

Question 6: Graceful Error Handling with Try

Review the Scala code snippet involving Try for graceful error handling:

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

def toInt(s: String): Try[Int] = Try(s.toInt)

val num1 = toInt("100")
println(num1)

val num2 = toInt("abc")
println(num2)

What is the output of this program?

A) Success(100)
Failure(java.lang.NumberFormatException)
B) 100
Error
C) Success(100)
Failure
D) None of the above

Question 7: Combining Option Values with flatMap

Consider the following Scala code snippet that demonstrates combining Option values using flatMap:

def parsePositiveInt(str: String): Option[Int] = {
  val i = Try(str.toInt).toOption
  i.flatMap { case x if x > 0 => Some(x) case _ => None }
}

val numA = parsePositiveInt("5")
println(numA)

val numB = parsePositiveInt("-5")
println(numB)

What does this code print?

A) Some(5)
None
B) 5
-5
C) Some(5)
Some(-5)
D) None of the above

Question 8: Using for-comprehension with Option

Analyze the Scala code snippet for using for-comprehension to chain operations on Option values:

def findPerson(id: Int): Option[String] = Some("John Doe")
def findAge(person: String): Option[Int] = Some(30)

val personAge = for {
  person <- findPerson(1)
  age <- findAge(person)
} yield age

println(personAge)

What is the output of this code?

A) Some(30)
B) 30
C) None
D) None of the above

Question 9: Safely Accessing Potentially Null Objects

Consider the following Scala code snippet demonstrating a safe way to access potentially null objects:

case class User(name: String, age: Option[Int])

def getUserAge(user: User): String = {
  user.age match {
    case Some(age) => s"User age: $age"
    case None => "Age not provided"
  }
}

val userWithAge = User("Alice", Some(30))
println(getUserAge(userWithAge))

val userWithoutAge = User("Bob", None)
println(getUserAge(userWithoutAge))

What is the output of this code?

A) User age: 30
Age not provided
B) User age: Some(30)
Age not provided
C) User age: 30
User age: None
D) None of the above

Question 10: Avoiding Nulls with Option in Method Returns

Analyze the Scala code snippet below that uses Option in method returns to avoid nulls:

def findUsername(userId: Int): Option[String] = {
  if (userId == 1) Some("JohnDoe")
  else None
}

val username1 = findUsername(1).getOrElse("Unknown")
println(username1)

val username2 = findUsername(2).getOrElse("Unknown")
println(username2)

What does this code print?

A) JohnDoe
Unknown
B) Some(JohnDoe)
Unknown
C) JohnDoe
None
D) None of the above

Question 11: Pattern Matching with Options to Handle Nulls

Consider the following Scala code snippet that uses pattern matching with options to handle potential null values:

def getDescription(word: Option[String]): String = {
  word match {
    case Some(w) => s"Found word: $w"
    case None => "No word found"
  }
}

val word1 = Some("Scala")
println(getDescription(word1))

val word2: Option[String] = None
println(getDescription(word2))

What does this code print?

A) Found word: Scala
No word found
B) Found word: Some(Scala)
No word found
C) Scala
None
D) None of the above

Question 12: Encapsulating Nullable References with Options

Analyze the Scala code snippet for encapsulating nullable references within Option to prevent NullPointerExceptions:

def getLength(s: Option[String]): Int = {
  s.getOrElse("").length
}

val str1: Option[String] = Some("Hello")
println(getLength(str1))

val str2: Option[String] = None
println(getLength(str2))

What is the output of this code?

A) 5
0
B) Some(5)
None
C) 5
Some(0)
D) None of the above

Question 13: Creating and Accessing a Map

Consider the following Scala code snippet demonstrating how to create and access elements of a Map:

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

println(capitals.get("France"))
println(capitals.getOrElse("Germany", "Not found"))

What is the output of this code?

A) Some(Paris)
Not found
B) Paris
Not found
C) Some(Paris)
Some(Not found)
D) None of the above

Question 14: Updating and Adding Elements to a Map

Analyze the Scala code snippet below that updates and adds elements to an immutable Map:

var numbers = Map(1 -> "one", 2 -> "two")
numbers = numbers + (3 -> "three")
numbers = numbers.updated(2, "TWO")

println(numbers)

What does this code print?

A) Map(1 -> one, 2 -> TWO, 3 -> three)
B) Map(1 -> "one", 2 -> "two", 3 -> "three")
C) Map(1 -> "one", 2 -> "TWO", 3 -> "three")
D) Compilation error

Question 15: Iterating Over a Map

Review the Scala code snippet involving iterating over a Map:

val ages = Map("Alice" -> 30, "Bob" -> 25, "Charlie" -> 28)

for ((name, age) <- ages) {
  println(s"$name is $age years old")
}

What is the output of this program?

A) Alice is 30 years old
Bob is 25 years old
Charlie is 28 years old
B) Alice: 30
Bob: 25
Charlie: 28
C) Compilation error
D) None of the above

Question 16: Using Options for Safe Access

Examine the Scala code snippet involving Option:

val maybeNum: Option[Int] = Some(5)
val result = maybeNum match {
  case Some(n) => n * 2
  case None => 0
}

println(result)

What is the output of this code?

A) 5
B) 10
C) 0
D) None

Question 17: Handling Exceptions with Try and Match

Review the following Scala code snippet for exception handling:

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

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

val result = divide(10, 0) match {
  case Success(value) => s"Result: $value"
  case Failure(exception) => s"Error: ${exception.getMessage}"
}

println(result)

What is the output of this code?

A) Result: 10
B) Error: / by zero
C) Error: ArithmeticException
D) Compilation error

Question 18: Using flatMap to Flatten and Transform

Consider the following Scala code snippet that demonstrates the use of the flatMap combinator:

val lists = List(List(1, 2, 3), List(4, 5), List(6))
val flattenedAndDoubled = lists.flatMap(numbers => numbers.map(_ * 2))

println(flattenedAndDoubled)

What does this code print?

A) List(2, 4, 6, 8, 10, 12)
B) List(List(2, 4, 6), List(8, 10), List(12))
C) List(1, 2, 3, 4, 5, 6)
D) None of the above

Question 19: Folding a List with foldLeft

Analyze the Scala code snippet for folding a list:

val numbers = List(1, 2, 3, 4, 5)
val sum = numbers.foldLeft(0)((acc, number) => acc + number)

println(sum)

What is the output of this code?

A) 15
B) 10
C) 5
D) None of the above

Question 20: Basic Usage of zip

Consider the following Scala code snippet demonstrating basic usage of the zip function:

val names = List("Alice", "Bob", "Charlie")
val ages = List(25, 30, 28)
val people = names.zip(ages)

println(people)

What is the output of this code?

A) List(("Alice", 25), ("Bob", 30), ("Charlie", 28))
B) List(Alice25, Bob30, Charlie28) C) [("Alice", 25), ("Bob", 30), ("Charlie", 28)] D) None of the above

Question 21: zip with Unequal Collection Sizes

Analyze the Scala code snippet below that uses the zip function with collections of unequal sizes:

val numbers = List(1, 2, 3, 4)
val letters = List('a', 'b', 'c')
val zippedList = numbers.zip(letters)

println(zippedList)

What does this code print?

A) List((1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'))
B) List((1, 'a'), (2, 'b'), (3, 'c'))
C) List((1, 'a'), (2, 'b'), (3, 'c'), (4, null))
D) Compilation error

Question 22: Using zipWithIndex

Review the Scala code snippet involving the zipWithIndex function:

val fruits = List("apple", "banana", "cherry")
val indexedFruits = fruits.zipWithIndex

println(indexedFruits)

What is the output of this program?

A) List(("apple", 0), ("banana", 1), ("cherry", 2)) B) List(("apple", 1), ("banana", 2), ("cherry", 3)) C) [("apple", 0), ("banana", 1), ("cherry", 2)]
D) None of the above

Question 23: Basic Usage of groupBy

Consider the following Scala code snippet demonstrating the basic usage of the groupBy function:

val words = List("apple", "banana", "pear", "apricot", "peach")
val groupedByFirstLetter = words.groupBy(word => word.head)

println(groupedByFirstLetter)

What is the output of this code?

A) Map('a' -> List("apple", "apricot"), 'b' -> List("banana"), 'p' -> List("pear", "peach"))
B) Map(a -> List("apple", "apricot"), b -> List("banana"), p -> List("pear", "peach"))
C) List("apple", "banana", "pear", "apricot", "peach")
D) None of the above

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

Question 24: Higher-Order Functions

Analyze this Scala code snippet that demonstrates a higher-order function:

def performOperation(x: Int, operation: Int => Int): Int = {
  operation(x)
}

val result = performOperation(5, y => y * y)
println(result)

What does this code print?

A) 10
B) 25
C) 5
D) None of the above

Question 25: Simple Recursive Function for Summation

Consider the Scala code snippet below for calculating the sum of numbers from 1 to N:

def sumToN(n: Int): Int = {
  if (n <= 0) 0
  else n + sumToN(n - 1)
}

val sum10 = sumToN(10)
println(sum10)
val sum5 = sumToN(5)
println(sum5)

What is the output of this code?

A) 55 and 15
B) 50 and 15
C) 55 and 10
D) 45 and 10

Exercises

Below are three exercises designed to help you practice writing tests using ScalaTest. These exercises cover different testing styles and use cases, including unit testing, property-based testing, and integration testing.

Exercise 1: Unit Testing with FunSuite

Objective: Write unit tests for a StringUtility object that provides a method to reverse a string and another method to check if a string is a palindrome.

  1. Implement the StringUtility object:
object StringUtility {
  def reverse(s: String): String = s.reverse
  def isPalindrome(s: String): Boolean = s == s.reverse
}
  1. Write tests using FunSuite: Create a test class StringUtilityTest using FunSuite to test both methods. Test normal strings, empty strings, and palindromes.

Exercise 2: Testing a Shopping Cart

Objective: Write tests for a ShoppingCart class that supports adding items, removing items, and calculating the total price. The cart should handle quantities for items and provide an option to apply a discount code.

  1. Implement the ShoppingCart and Item classes:
case class Item(id: String, name: String, price: Double, quantity: Int)

class ShoppingCart {
  private var items: Map[String, Item] = Map()

  def addItem(item: Item): Unit = {
    items = items.get(item.id) match {
      case Some(existingItem) => items.updated(item.id, existingItem.copy(quantity = existingItem.quantity + item.quantity))
      case None => items + (item.id -> item)
    }
  }

  def removeItem(itemId: String): Unit = {
    items -= itemId
  }

  def applyDiscount(code: String): Unit = {
    val discountRate = code match {
      case "DISCOUNT10" => 0.1
      case _ => 0.0
    }
    items = items.view.mapValues(item => item.copy(price = item.price * (1 - discountRate))).toMap
  }

  def total: Double = {
    items.values.map(item => item.price * item.quantity).sum
  }
}
  1. Write tests to verify:
  • Adding and removing items updates the cart correctly.
  • The total price is calculated correctly, with and without a discount code.

Exercise 3: Testing a Password Validator

Objective: Write tests for a PasswordValidator object that checks if a password meets certain criteria: it must be at least 8 characters long, contain both uppercase and lowercase letters, and at least one number.

  1. Implement the PasswordValidator:
object PasswordValidator {
  def isValid(password: String): Boolean = {
    password.length >= 8 &&
    password.exists(_.isUpper) &&
    password.exists(_.isLower) &&
    password.exists(_.isDigit)
  }
}
  1. Write tests to verify:
  • A password meeting all criteria is valid.
  • Passwords missing each specific criterion are invalid.
  • Edge cases, such as an empty string or a password with only spaces.

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

Objective: Use ScalaCheck to write property-based tests for a StringConcatenationUtility that concatenates two strings with a space between them.

  1. Implement the StringConcatenationUtility:
object StringConcatenationUtility {
  def concatenate(str1: String, str2: String): String = s"$str1 $str2"
}
  1. Write property-based tests to verify:
  • Concatenating two non-empty strings results in a string that contains both, separated by a space.
  • Concatenating an empty string with any string returns the other string with a leading or trailing space.
  • The length of the result is the sum of the lengths of the two strings plus one (for the space).

Exercise 5: Testing a Fibonacci Number Generator

Objective: Write tests for a FibonacciGenerator that generates the nth Fibonacci number. Consider both iterative and recursive implementations.

  1. Implement the FibonacciGenerator:
object FibonacciGenerator {
  def fibonacci(n: Int): Int = n match {
    case 0 | 1 => n
    case _ => fibonacci(n - 1) + fibonacci(n - 2)
  }
}
  1. Write tests to verify:
  • Correct Fibonacci numbers are generated for known values (e.g., 0, 1, 1, 2, 3, 5, 8, 13, ...).
  • Negative numbers return 0 or an appropriate error/exception.
  • Performance or stack overflow issues for larger n (e.g., n = 50), if applicable.

Exercise 6: Integration Testing for a File Processing Utility

Objective: Write integration tests for a FileProcessingUtility that reads a file, processes its content, and writes the result to another file. Assume the processing involves converting all text to uppercase.

  1. Implement the FileProcessingUtility (simplified for exercise):
import scala.io.Source
import java.nio.file.{Files, Paths}

object FileProcessingUtility {
  def processFile(inputPath: String, outputPath: String): Unit = {
    val source = Source.fromFile(inputPath)
    val content = source.getLines().mkString("\n").toUpperCase
    source.close()
    Files.write(Paths.get(outputPath), content.getBytes)
  }
}
  1. Write tests to verify:
  • Correct processing of an input file with known content.
  • Handling of non-existent input files.
  • The output file is created in the specified location with the expected processed content.

Project

Todo List

In this project we will add tests tp the todolist application

Create a class
TodoItem with a field

  • task
  • priority

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

Write test on all the functions.

Shopping basket

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

Article with the fields

  • name
  • price
  • amount

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

Add filter function on prices between an low and high value

Write tests on all the functions.

Exercises

  1. Hello World
  • Create a Twirl template that accepts a string parameter name and displays "Hello, name!" in an HTML paragraph tag.
  1. List Rendering
  • Make a Twirl template that takes a List of strings and renders each string in an unordered list (<ul>). Ensure to handle an empty list gracefully by displaying a friendly message.
  1. Form Submission Display
  • Develop a form in Twirl that posts a user's first and last name to a Play controller. Then, display the submitted names on a new page using another Twirl template.
  1. Conditional Content
  • Design a Twirl template that accepts a Boolean value. Depending on the value, it should display different messages or HTML elements.
  1. Nested Templates
  • Create a main layout template that includes a header, footer, and a placeholder for content. Then, create a child template that extends this layout and populates the content area with specific information.
  1. Loop with Conditional
  • Write a Twirl template that takes a list of integers. Display each number in a list item (<li>). Highlight numbers that are divisible by 3 by making them bold.

Tips for Getting Started

  • To add a Twirl template to your Play project, create a file with the .scala.html extension in the app/views directory.
  • Pass data to your Twirl templates by adding parameters to the template function declaration. For example, to pass a name to your template, start the file with @(name: String).
  • Use Scala control structures directly in your templates for loops and conditionals.

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

Working with untyped actors in Akka (a popular toolkit for building concurrent, distributed, and fault-tolerant systems on the JVM) is a great way to understand actor-based concurrency. Below are a few exercises that can help you practice working with untyped actors.

Exercise 1: Simple Ping-Pong

Objective: Implement two actors that send messages to each other.

  • Actor 1 (PingActor): Sends a "Ping" message to Actor 2.
  • Actor 2 (PongActor): Responds with a "Pong" message when it receives a "Ping".
  • PingActor: Upon receiving "Pong", sends another "Ping" after a short delay.

Task:

  • Implement the PingActor and PongActor classes.
  • Ensure the actors can send messages back and forth indefinitely.

Exercise 2: Counter Actor

Objective: Implement an actor that maintains a state.

  • CounterActor: This actor should maintain an integer counter. It should handle the following messages:
    • "Increment": Increments the counter by 1.
    • "Decrement": Decrements the counter by 1.
    • "Get": Replies with the current value of the counter.

Task:

  • Implement the CounterActor.
  • Create a main method where you create an instance of CounterActor, send several messages to it, and print out the final counter value.

Exercise 3: Actor Hierarchy and Supervision

Objective: Understand actor hierarchy and supervision.

  • ParentActor: This actor creates a child actor (ChildActor). It supervises the child and restarts it when it fails.
  • ChildActor: This actor might occasionally throw an exception when it receives a "DoWork" message.

Task:

  • Implement the ParentActor and ChildActor.
  • Set up supervision such that the ParentActor restarts the ChildActor upon failure.
  • Create a scenario where the ChildActor occasionally fails, and observe how supervision works.

Exercise 4: Bank Account Actor

Objective: Model a simple bank account system using actors.

  • BankAccountActor: This actor should handle the following messages:
    • "Deposit(amount)": Adds the specified amount to the account balance.
    • "Withdraw(amount)": Subtracts the specified amount if there are sufficient funds.
    • "GetBalance": Replies with the current balance.

Task:

  • Implement the BankAccountActor.
  • Create a main method where you create an instance of BankAccountActor, perform several deposits and withdrawals, and then print the final balance.

Exercise 5: Master-Worker Pattern

Objective: Implement a simple Master-Worker pattern using actors.

  • MasterActor: Distributes tasks among several worker actors and collects the results.
  • WorkerActor: Performs a computation and returns the result to the master.

Task:

  • Implement the MasterActor and WorkerActor.
  • The MasterActor should divide a list of integers into sublists and send each sublist to a worker actor to compute the sum.
  • Collect and print the final sum of all integers.

Todo App

Creating a simple Todo app using Akka's untyped actors can be an excellent way to demonstrate how to manage state, handle concurrency, and build a basic application using Akka's actor model. Below, I will walk you through the process of building a Todo app with the following functionalities:

  1. Add a new Todo item.
  2. Remove a Todo item.
  3. Mark a Todo item as completed.
  4. List all Todo items.

1. Setting Up the Project

Start by setting up your Scala project with the necessary Akka dependencies in your build.sbt:

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.x"

2. Defining the Messages

First, define the messages that actors will use to communicate. Messages are immutable case classes:

// Messages
case class AddTodo(id: Int, task: String)
case class RemoveTodo(id: Int)
case class CompleteTodo(id: Int)
case object GetTodos

3. Creating the Todo Actor

The TodoActor will manage the list of Todo items. It will handle messages to add, remove, complete, and retrieve Todo items:

import akka.actor.{Actor, ActorSystem, Props}

// Todo Item case class
case class TodoItem(id: Int, task: String, completed: Boolean = false)

class TodoActor extends Actor {
  var todos: Map[Int, TodoItem] = Map()

  def receive: Receive = {
    case AddTodo(id, task) =>
      todos += id -> TodoItem(id, task)
      println(s"Added Todo: $task")

    case RemoveTodo(id) =>
      todos.get(id) match {
        case Some(todo) =>
          todos -= id
          println(s"Removed Todo: ${todo.task}")
        case None =>
          println(s"Todo with id $id not found.")
      }

    case CompleteTodo(id) =>
      todos.get(id) match {
        case Some(todo) =>
          todos += id -> todo.copy(completed = true)
          println(s"Completed Todo: ${todo.task}")
        case None =>
          println(s"Todo with id $id not found.")
      }

    case GetTodos =>
      if (todos.isEmpty) {
        println("No Todos available.")
      } else {
        todos.values.foreach { todo =>
          val status = if (todo.completed) "Completed" else "Pending"
          println(s"${todo.id}: ${todo.task} [$status]")
        }
      }
  }
}

4. Creating the Main Application

Now, let's create a main application that will instantiate the ActorSystem, create the TodoActor, and interact with it:

object TodoApp extends App {
  // Create Actor System
  val system = ActorSystem("TodoSystem")

  // Create TodoActor
  val todoActor = system.actorOf(Props[TodoActor], "todoActor")

  // Interacting with the TodoActor
  todoActor ! AddTodo(1, "Buy milk")
  todoActor ! AddTodo(2, "Go to the gym")
  todoActor ! GetTodos

  todoActor ! CompleteTodo(1)
  todoActor ! GetTodos

  todoActor ! RemoveTodo(2)
  todoActor ! GetTodos

  // Shutdown the Actor System
  system.terminate()
}

5. Running the Application

To run the application, simply execute the TodoApp object in your Scala environment. The output will be:

Added Todo: Buy milk
Added Todo: Go to the gym
1: Buy milk [Pending]
2: Go to the gym [Pending]
Completed Todo: Buy milk
1: Buy milk [Completed]
2: Go to the gym [Pending]
Removed Todo: Go to the gym
1: Buy milk [Completed]
No Todos available.

Todo App

Creating a simple Todo application using Akka actors in Scala involves several steps, including defining the messages that actors will use for communication, creating the actor behaviors, and setting up the actor system. This application will consist of a main actor, TodoManager, which manages todo items, and a simplified interface for adding and listing todos.

Step 1: Define the Messages

First, define the messages that will be used for interaction. In a Todo app, we typically need to add todos and list all existing todos.

sealed trait Command
final case class AddTodo(description: String) extends Command
final case object ListTodos extends Command
final case class Todos(items: List[String])

Step 2: Define the TodoManager Actor

The TodoManager actor will handle AddTodo and ListTodos messages. It maintains a list of todo items in its state.

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

object TodoManager {
  def apply(): Behavior[Command] = manageTodos(Nil)

  private def manageTodos(todos: List[String]): Behavior[Command] =
    Behaviors.receive { (context, message) =>
      message match {
        case AddTodo(description) =>
          context.log.info(s"Adding todo: $description")
          manageTodos(todos :+ description)
        case ListTodos =>
          context.log.info(s"Current todos: $todos")
          Behaviors.same
      }
    }
}

Step 3: Set Up the Actor System and Interaction

Now, create the ActorSystem, and demonstrate adding some todos and listing them.

import akka.actor.typed.ActorSystem

object TodoApp extends App {
  val system: ActorSystem[Command] = ActorSystem(TodoManager(), "todoSystem")

  system ! AddTodo("Learn Akka Actors")
  system ! AddTodo("Build a TodoApp")
  system ! ListTodos

  // Shutdown the actor system after a delay to see the output
  import scala.concurrent.duration._
  import system.executionContext
  system.scheduler.scheduleOnce(2.seconds) {
    system.terminate()
  }
}

Running the Application

When you run this application, it will:

  • Create an ActorSystem with the TodoManager actor.
  • Send AddTodo messages to add new todos.
  • Send a ListTodos message to print out all current todos.
  • Finally, it schedules a system termination after a short delay so you can see the output before the application exits.

Shopping Basket

To convert the Shopping Basket system from Akka typed actors to untyped actors, we need to modify the code so that it aligns with the untyped actor model in Akka. Below is the step-by-step process to achieve this.

Step 1: Define Messages and Data Models

The message and data model definitions remain mostly the same, but we'll omit the sealed trait for commands, as untyped actors don't enforce the use of sealed trait in the same way.

// Messages
case class AddItem(item: Item)
case object Checkout

// Data Models
case class Item(name: String, price: BigDecimal)
case class Receipt(items: List[Item], total: BigDecimal)

Step 2: Define the BasketActor

Now, let's define the BasketActor. In untyped actors, we extend the Actor trait and implement the receive method. The actor's state can be managed as mutable variables inside the actor.

import akka.actor.{Actor, ActorLogging, Props}

class BasketActor extends Actor with ActorLogging {
  var items: List[Item] = List.empty

  override def receive: Receive = {
    case AddItem(item) =>
      log.info(s"Item added to basket: ${item.name}")
      items = items :+ item

    case Checkout =>
      val total = items.map(_.price).sum
      log.info(s"Checking out with total: $total")
      // In a real application, you might want to send the receipt to another actor or persist it.
      context.stop(self)
  }
}

object BasketActor {
  def props(): Props = Props(new BasketActor)
}

Step 3: Set Up the Actor System and Test

Finally, let's set up the ActorSystem, create the BasketActor, and send messages to test the functionality. The main difference in untyped actors is that we use system.actorOf to create actors and ! (tell) to send messages.

import akka.actor.{ActorSystem, Props}

object ShoppingApp extends App {
  // Create Actor System
  val system = ActorSystem("shoppingSystem")

  // Create BasketActor
  val basketActor = system.actorOf(BasketActor.props(), "basketActor")

  // Interact with the BasketActor
  basketActor ! AddItem(Item("Apple", BigDecimal("0.60")))
  basketActor ! AddItem(Item("Banana", BigDecimal("0.40")))
  basketActor ! Checkout

  // Shutdown the actor system after a short delay to see the output.
  Thread.sleep(1000) // Not ideal for real applications, just for demonstration
  system.terminate()
}

Explanation:

  • Actor Creation: In untyped actors, BasketActor is created using system.actorOf(BasketActor.props(), "basketActor").
  • Message Handling: The receive method handles incoming messages by pattern matching on the message type.
  • State Management: State (i.e., the list of items) is maintained as a mutable variable inside the actor.
  • Logging: The ActorLogging trait is mixed in to provide easy access to logging within the actor.

Shopping Basket App

Creating a shopping basket system using Akka actors in Scala involves several key steps, including defining messages for adding items to the basket and checking out, as well as creating actor behaviors to handle these actions. Below is a simplified example demonstrating these concepts.

Step 1: Define Messages and Data Models

First, define the messages that will be used to interact with the actors, and any data models needed for the shopping basket.

sealed trait BasketCommand
final case class AddItem(item: Item) extends BasketCommand
final case object Checkout extends BasketCommand
final case class Item(name: String, price: BigDecimal)
final case class Receipt(items: List[Item], total: BigDecimal)

Step 2: Define the BasketActor

The BasketActor will maintain a list of items as the state and handle AddItem and Checkout messages.

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

object BasketActor {
  def apply(): Behavior[BasketCommand] = basket(List.empty)

  private def basket(items: List[Item]): Behavior[BasketCommand] =
    Behaviors.receive { (context, message) =>
      message match {
        case AddItem(item) =>
          context.log.info(s"Item added to basket: ${item.name}")
          basket(items :+ item)

        case Checkout =>
          val total = items.map(_.price).sum
          context.log.info(s"Checking out with total: $total")
          // In a real application, you might want to send the receipt to another actor or persist it.
          Behaviors.stopped
      }
    }
}

Step 3: Set Up the Actor System and Test

Now, you can set up an ActorSystem, create a BasketActor, and send messages to test the functionality.

import akka.actor.typed.ActorSystem

object ShoppingApp extends App {
  val system: ActorSystem[BasketCommand] = ActorSystem(BasketActor(), "shoppingSystem")

  system ! AddItem(Item("Apple", BigDecimal("0.60")))
  system ! AddItem(Item("Banana", BigDecimal("0.40")))
  system ! Checkout

  // Shutdown the actor system after a short delay to see the output.
  Thread.sleep(1000) // Not ideal for real applications, just for demonstration
  system.terminate()
}

Considerations for a Real Application

  • Persistence: For a production system, you would want to persist the shopping basket's state using Akka Persistence to handle failures and system restarts without data loss.
  • Security: Ensure that each shopping basket is associated with a specific user session or account.
  • Concurrency Handling: Be mindful of concurrent modifications if your system allows for simultaneous updates to the basket from the same user.
  • Scalability: Consider how your actors will be distributed in a clustered environment to handle scaling requirements.
  • Integration: Think about how this system will integrate with other components, like inventory management, payment processing, and user authentication.

Exercises

Exercise 1: Hello Actor

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

Exercise 2: Counter Actor

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

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.

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

Todo App

Creating a Todo application that leverages Scala's Futures and Promises involves handling tasks asynchronously. This is particularly useful for operations that might take a long time, such as database interactions. In this simplified example, we'll implement a basic Todo app with asynchronous add and get functionalities.

Step 1: Setting Up the Environment

First, ensure you have the Scala execution context in scope for running your futures:

import scala.concurrent.{Future, ExecutionContext, Promise}
import scala.util.{Success, Failure}
import scala.concurrent.ExecutionContext.Implicits.global

Step 2: Define the Todo Model and Storage

Define a simple Todo model and a mock asynchronous storage for todos. In a real application, this storage might interact with a database.

case class Todo(id: Int, task: String, isCompleted: Boolean)

object TodoStorage {
  private var todos: List[Todo] = List()

  // Asynchronous add method
  def add(todo: Todo): Future[Unit] = Future {
    Thread.sleep(1000) // Simulate database delay
    todos = todo :: todos
  }

  // Asynchronous get method
  def get(id: Int): Future[Option[Todo]] = Future {
    Thread.sleep(1000) // Simulate database delay
    todos.find(_.id == id)
  }
}

Step 3: Implement the TodoService

The TodoService will interact with the TodoStorage. It includes error handling and demonstrates how to use a Promise to create a custom future that might be completed outside the usual flow.

object TodoService {
  def addTask(task: String): Future[Int] = {
    val todo = Todo(
      id = TodoStorage.todos.length + 1, // Simple ID generation strategy
      task = task,
      isCompleted = false
    )

    for {
      _ <- TodoStorage.add(todo)
    } yield todo.id
  }

  def completeTask(id: Int): Future[Boolean] = {
    val promise = Promise[Boolean]()

    TodoStorage.get(id).onComplete {
      case Success(Some(todo)) =>
        TodoStorage.add(todo.copy(isCompleted = true)).onComplete {
          case Success(_) => promise.success(true)
          case Failure(_) => promise.failure(new Exception("Failed to update the todo."))
        }
      case Success(None) => promise.failure(new NoSuchElementException("Todo not found."))
      case Failure(exception) => promise.failure(exception)
    }

    promise.future
  }
}

Step 4: Usage Example

Here’s how you might use TodoService to add and complete tasks:

val added = TodoService.addTask("Learn Scala Futures and Promises")

added.onComplete {
  case Success(id) =>
    println(s"Added todo with id: $id")
    TodoService.completeTask(id).onComplete {
      case Success(_) => println(s"Todo $id completed.")
      case Failure(exception) => println(s"Error completing todo: ${exception.getMessage}")
    }
  case Failure(exception) => println(s"Error adding todo: ${exception.getMessage}")
}

Shopping Basket

Creating a shopping basket application with Futures and Promises in Scala involves handling asynchronous operations, such as querying a database or making network requests, to add, remove, and fetch items in the shopping basket. Here's a simplified version of such an application.

Step 1: Setting Up the Environment

First, make sure to import necessary Scala concurrent execution contexts and other utilities:

import scala.concurrent.{Future, Promise, ExecutionContext}
import scala.util.{Success, Failure}
import scala.concurrent.ExecutionContext.Implicits.global

Step 2: Define the Models

Define a simple Item model and the ShoppingBasket class with asynchronous methods:

case class Item(id: String, name: String, price: Double, quantity: Int = 1)

class ShoppingBasket {
  private var items: Map[String, Item] = Map()

  def addItem(item: Item): Future[Unit] = Future {
    // Simulate a delay, e.g., database operation
    Thread.sleep(100)
    items.get(item.id) match {
      case Some(existingItem) =>
        items = items.updated(item.id, existingItem.copy(quantity = existingItem.quantity + item.quantity))
      case None =>
        items += (item.id -> item)
    }
  }

  def removeItem(itemId: String): Future[Unit] = Future {
    // Simulate a delay
    Thread.sleep(100)
    items -= itemId
  }

  def getTotal: Future[Double] = Future {
    // Simulate a computation delay
    Thread.sleep(100)
    items.values.map(item => item.price * item.quantity).sum
  }
}

Step 3: Implementing Asynchronous Operations

Here's how you might interact with the ShoppingBasket asynchronously, demonstrating adding items, removing an item, and calculating the total:

val basket = new ShoppingBasket()

val itemFutures = Future.sequence(Seq(
  basket.addItem(Item("1", "Apple", 0.60, 2)),
  basket.addItem(Item("2", "Banana", 0.40, 3)),
  basket.addItem(Item("3", "Carrot", 0.25, 4))
))

itemFutures.onComplete {
  case Success(_) =>
    println("Items added successfully.")
    basket.getTotal.onComplete {
      case Success(total) =>
        println(s"Total before removal: $total")
        basket.removeItem("2").onComplete {
          case Success(_) =>
            basket.getTotal.onComplete {
              case Success(newTotal) => println(s"Total after removal: $newTotal")
              case Failure(e) => println(s"Failed to get total after removal: ${e.getMessage}")
            }
          case Failure(e) => println(s"Failed to remove item: ${e.getMessage}")
        }
      case Failure(e) => println(s"Failed to get total: ${e.getMessage}")
  }
  case Failure(e) => println(s"Failed to add items: ${e.getMessage}")
}

Step 4: Error Handling and Promises

Using Promises is particularly useful when you need more control over the completion of a future, for example, in complex error handling or when integrating with callback-based APIs.

Let's say you have a method to apply a discount code, which is unpredictable and requires a Promise for better control:

def applyDiscount(code: String): Future[Double] = {
  val promise = Promise[Double]()
  
  // Simulate validating discount code asynchronously
  Future {
    Thread.sleep(100) // Simulate delay
    if (code == "DISCOUNT10") promise.success(0.1) // 10% discount
    else promise.failure(new IllegalArgumentException("Invalid discount code"))
  }
  
  promise.future.flatMap { discountRate =>
    getTotal.map(total => total * (1 - discountRate))
  }
}

In this example, applyDiscount uses a Promise to manually complete a future based on a discount code validation operation. It then uses the result to calculate the discounted total.

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.

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.

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.

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

Exercise 1

Extension Methods

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

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

A type class is the functional equivalent of polymorphism in the object-oriented world. And it is resolved in compile-time by importing context givens

The Object-Oriented Way

  trait Show:
    def show: String

  case class Person(name: String, age: Int) extends Show:
    override def show = s"$name with age $age"

  val john = Person("John", 28 )
  val showJohn = john.show
  • available only for the types we extend
  • provide only one override implementation

The Pattern Matching Way

def show(value: Any): String = value match 
  case Person(name, age) => s"$name with age $age"
  case _ => throw new IllegalArgumentException("not supported")
  • lose type safety
  • extends match for every pattern
  • has one case implementation

Type Classes

Create a type class has the following steps

  1. Type Class definition
  2. Type Class instances
  3. the API
  4. Extension methods

Type Class Definition

trait Shower[T] {
	def show(value: T): String
}

Type Class Instances

given userShower: Shower[Person] with
  override def show(value: Person) =
    val Person(name, age) = value
    s"$name with age $age"}

val john = Person("John", 28)
val showJohn = userShower.show(john)

API

object Shower {
  def show[T](value: T)(using shower: Shower[T]): String =
    shower.show(value)

  def apply[T](using shower: Shower[T]): Shower[T] = shower
}

val showJohn = Shower.show(john)

Extension method


object ShowSyntax:
  extension [T](value: T)
    def show(using shower: Shower[T]): String = shower.show(value)

import ShowSyntax.*
val showJohn = john.show
  • can define showers for other types
  • multiple shower for the same type

Example Dog ad Cat

In Scala 3, the mechanism for defining and using type classes has been significantly revamped with the introduction of given and using clauses, replacing the older implicit keyword. This change aims to make the definition and usage of type classes more explicit and readable, addressing some of the common criticisms of Scala's implicit system.

Defining Type Classes with given

To define a type class instance with Scala 3, you use the given keyword. This replaces the implicit val or implicit object definitions used in Scala 2.

Example: Defining a SoundMaker type class and its instances.

trait SoundMaker[T]:
  def makeSound(value: T): Unit

// Define instances of the SoundMaker type class
given SoundMaker[Dog] with
  def makeSound(dog: Dog): Unit = println("Woof")


given SoundMaker[Cat] with
  def makeSound(cat: Cat): Unit = println("Meow")

Here, given declarations define how Dog and Cat types fulfill the SoundMaker contract. These instances are globally available and automatically used wherever a SoundMaker[T] is required.

Using Type Classes with using

To use a type class instance, Scala 3 introduces the using clause. This explicitly specifies that a function requires a type class instance for its operation, making the function's dependencies clear.

Example: Using the SoundMaker type class to implement a polymorphic playSound function.

def playSound[T](value: T)(using maker: SoundMaker[T]): Unit = maker.makeSound(value)

The using clause in the function signature tells the compiler to search for an implicit SoundMaker[T] instance for whatever type T is passed into the function. This search is based on the instances defined with given.

Putting It All Together

class Dog(val name: String)
class Cat(val name: String)

// Assuming the given instances and the playSound method are defined as above

val myDog = Dog("Rex")
val myCat = Cat("Whiskers")

playSound(myDog) // Outputs: Woof
playSound(myCat) // Outputs: Meow

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]

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

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)

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.

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]

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]

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

Exercise

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

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

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

Step-by-Step

  1. Define a Book Case Class: Define a case class to represent a book with attributes like title, author, year of publication, and category.

  2. Create a List of Books: Create an initial list of books.

  3. Add a Book to the Library: Define a function to add a book to the library.

  4. Search for Books by Title or Author: Define a function to search for books by title or author.

  5. Filter Books by Category: Define a function to filter books by category.

  6. Calculate Statistics: Define functions to calculate statistics like the total number of books and the average publication year.

Enhanced Library

Step-by-Step Enhancements

  1. Sorting Books:
  • Sort books by title.
  • Sort books by year of publication.
  1. Grouping Books by Category:
  • Group books by their category.
  1. Partitioning Books by Year:
  • Partition books into those published before and after a certain year.
  1. Using Collect to Extract Specific Information:
  • Use the collect function to extract titles of books published after a certain year.
  1. Calculating Average Publication Year by Category:
  • Calculate the average publication year for each category.

Library with Shopping Basket

Step-by-Step Enhancements

  1. Extend the Book Case Class: Add a price attribute to the Book case class.

  2. Create a Shopping Basket: Define a class to manage the shopping basket, including adding books and calculating the total cost.

  3. Update the Library Management System: Update the main application to include the shopping basket functionality.

Library with Author

Step-by-Step Enhancements

  1. Define the Author Case Class: Add a case class to represent an author with attributes like name and nationality.

  2. Update the Book Case Class: Modify the Book case class to include an Author.

  3. Create a Shopping Basket Class: Define a class to manage the shopping basket, including adding books and calculating the total cost.

  4. Update the Library Management System: Update the main application to include the new Author case class and the shopping basket functionality.