Scala 3 Solutions
Solutions to the exercises in the Workbook
Getting Started
Create a 'Hello, World' scala application
In a browser
- Try Scala in a browser with https://scastie.scala-lang.org/
On a computer
- Or install Scala on your computer
- Goto Getting Started on the scala-lang.org website and follow the instructions.
As result Java and Scala are installed on your computer
Scala REPL
The scala installation contains a Scala command-line playground, the REPL (Read Eval Print Loop)
On the command-line type
scala
Otherwise: in intelliJ type Run -> Tools -> Scala REPL...
This will print a welcome message and give the scala prompt
Welcome to Scala 3.7.2 (24.0.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala>
Hello, World
At the prompt we type the scala statements and end with Enter
The statement is evaluated and the result is printed
print ("Hello, World")
// Hello, World
IntelliJ
The other way is using an IntelliJ project
Prerequisites
- Java (latest)
- Scala 3
- IntelliJ iDEA
- Scala Plugin
New Project
In IntelliJ start a new project. File -> New -> Project...
Configure your project as below.
If possible, select the latest JDK, SBT and Scala 3.
Select 'Use significant indentation syntax (Optional Braces)'
We will use the Scala 3 Pythonesque syntax.
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
@main
def main(): Unit =
//TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
// to see how IntelliJ IDEA suggests fixing it.
(1 to 5).map(println)
for (i <- 1 to 5) do
//TIP Press <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
println(s"i = $i")
Run the application
Run the application to see if everything is working.
Click on the green arrow on line 2.
The application runs on the Terminal
Variable
Which of the following variable names are valid?
- 3 this_is_a_number
- 5 A1
- 7 function
Exercise
-
Create a val called pi and assign it the value of 3.14159:
val pi: Double = 3.14159 -
Create a val called message and assign it a string value:
val message: String = "Hello, world!" -
Create a val called age and assign it an integer value:
val age: Int = 30 -
Create a val called isTall and assign it a boolean value:
val isTall: Boolean = true -
Create a val called name and assign it a string value, then print a message that includes the value of name:
val name: String = "Alice"
println(s"My name is $name.")
In this example, the string interpolation syntax is used to include the value of name in the printed message. The dollar sign ($) followed by the variable name is replaced with the value of the variable. The s at the beginning of the string indicates that this is a string interpolation.
Operators
Exercise 1
- Create number a = 10
- Print the number.
Exercise 2
- Multiply the number by 2.
- Print the result.
Exercise 3
- Create a second number b = 5
- Multiply a and b
- Print the result.
Exercise 4
The Celsius Fahrenheit converter.
- Create a variable
temperatureas Double for the temperature in Celsius. - And print the temperature in Fahrenheit.
- Look on the internet for the formula.
- Use 0.0, 37.0 -40.0 as test examples
Exercise 5
Create a 'kilometre to miles' converter.
Choices
Exercise 1: Even Odd
val x: Int = 10
val result =
if x % 2 == 0 then "Even"
else "Odd"
println(result)
Exercise 2: Larger
val x: Int = 10
val y: Int = 20
val larger: Int = if x > y then x else y
println(s"The larger number is $larger.")
Exercise 3: Maximum
val x: Int = 10
val y: Int = 20
val z: Int = 15
val largest: Int = if x > y && x > z then
x
else if y > z then
y
else
z
println(s"The largest number is $largest.")
Exercise 4: Vowel or Consonant
val ch: Char = 'a'
val result: String = ch match
case 'a' | 'e' | 'i' | 'o' | 'u' => "Vowel"
case _ => "Consonant"
println(result)
Exercise 5: Weekend
val day: String = "Saturday"
val result: String = day match
case "Saturday" | "Sunday" => "Weekend"
case _ => "Weekday"
println(result)
Exercise 6: Number name
val x: Int = 4
val result: String = x match
case 1 => "one"
case 2 => "two"
case 3 => "three"
case 4 => "four"
case 5 => "five"
case 6 => "six"
case 7 => "seven"
case 8 => "eight"
case 9 => "nine"
case _ => "invalid"
println(result)
Loops
Exercise 1 integers
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val squares = for n <- numbers yield n * n
println(squares)
Exercise 2 string lengths
val strings = List("apple", "banana", "cherry", "date", "elderberry")
val lengths = for s <- strings yield s.length
println(lengths)
Exercise 3 evens
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evens = for n <- numbers if n % 2 == 0 yield n
println(evens)
Exercise 4 tuples
val people = List(("Alice", 25), ("Bob", 32), ("Charlie", 19), ("David", 42))
val youngNames = for
(name, age) <- people if age < 30 yield name
println(youngNames)
Exercise 5 string contains
val strings = List("apple", "banana", "cherry", "date", "elderberry")
val withA = for s <- strings if s.contains("a") yield s
println(withA)
String
Strings in Scala are sequences of characters. Scala strings are instances of the String class in Java, which means you
can use any method from the Java String class in Scala. Additionally, Scala adds its own set of methods to strings
through implicit conversions to StringOps to facilitate common operations in a more Scala-like way.
Creating Strings
Creating a string in Scala is straightforward and similar to other languages:
val greeting = "Hello, World!"
String Interpolation
Scala supports string interpolation, allowing you to embed variable references directly in string literals.
- s-Interpolator: Prepend
sto the string and use$to insert variables.
val name = "Scala"
val message = s"Hello, $name!"
println(message) // Output: Hello, Scala!
You can also use ${} to insert more complex expressions:
val temperature = 20.5
val weatherMessage = s"The current temperature is ${temperature}°C."
println(weatherMessage) // Output: The current temperature is 20.5°C.
- f-Interpolator: Similar to
s, but allows formatting.
val height = 1.9
val formattedMessage = f"$name%s is $height%2.2f meters tall."
println(formattedMessage) // Output: Scala is 1.90 meters tall.
- raw-Interpolator: Works like
sbut does not escape literals.
println(raw"New\nLine") // Output: New\nLine
Common Operations
- Length: Get the number of characters.
val length = greeting.length
- Concatenation: Combine strings using
+.
val fullGreeting = greeting + " How are you?"
- Substrings: Extract parts of a string.
val hello = greeting.substring(0, 5) // "Hello"
- Comparisons: Compare two strings, optionally ignoring case.
val isEqual = "Scala" == "scala" // false
val isEqualIgnoreCase = "Scala".equalsIgnoreCase("scala") // true
- Searching: Check if a string contains a sequence or matches a pattern.
val containsWorld = greeting.contains("World") // true
- Splitting: Split a string into an array of substrings.
val words = greeting.split(", ") // Array("Hello", "World!")
- Trimming: Remove leading and trailing whitespaces.
val spaced = " Hello, World! "
val trimmed = spaced.trim // "Hello, World!"
- Replacing: Replace parts of the string.
val replaced = greeting.replace("World", "Scala") // "Hello, Scala!"
- Case Conversion: Convert to upper or lower case.
val upper = greeting.toUpperCase // "HELLO, WORLD!"
val lower = greeting.toLowerCase // "hello, world!"
Multiline Strings
Scala supports multiline strings using triple quotes, preserving line breaks and spaces:
val multilineString =
"""This is a
|multiline string
|in Scala."""
.stripMargin
The .stripMargin method removes leading spaces up to and including the character |, making it easier to format
multiline strings neatly.
Exercises
Exercise 1 length
- Create a String "Hello, world"
- Print the length of the text.
- Split the text in words.
- Count the number of words.
Exercise 2 concat
- Create two Strings "Hello, " and "World"
- Concatenate the two Strings
- Merge the two String with s-interpolated String
Functions
Exercise 1 contains
def findStringsContainingA(strings: List[String]): List[String] =
strings.filter(str => str.contains('a'))
// example usage
findStringsContainingA(List("apple", "banana", "orange", "pear")) // returns List("apple", "banana", "orange")
Exercise 2 even numbers
def filterEvenNumbers(numbers: List[Int]): List[Int] =
numbers.filter(num => num % 2 == 0)
// example usage
filterEvenNumbers(List(1, 2, 3, 4, 5, 6)) // List(2, 4, 6)
Exercise 3 string lengths
def stringLengths(strings: List[String]): List[Int] =
strings.map(str => str.length)
// example usage
stringLengths(List("apple", "banana", "orange", "pear")) // List(5, 6, 6, 4)
Exercise 4: Default Parameters
def greet(name: String, greeting: String = "Hello"): Unit =
println(s"$greeting, $name!")
greet("Scala") // Should print "Hello, Scala!"
greet("World", "Hi") // Should print "Hi, World!"
Exercise 5: Named Arguments
def describePerson(name: String, age: Int, country: String = "unknown"): Unit =
println(s"$name is $age years old from $country.")
describePerson(age = 25, name = "Alice") // Should print "Alice is 25 years old from unknown."
Exercise 6: Varargs
def sum(numbers: Int*): Int = numbers.sum
println(sum(1, 2, 3, 4)) // Should print 10
println(sum()) // Should print 0
Exercise 7: Anonymous Functions and Map
Task: Given a list of integers, use an anonymous function to increment each element by 1, using the map method.
val numbers = List(1, 2, 3, 4)
val incrementedNumbers = numbers.map(n => n + 1)
println(incrementedNumbers) // Should print List(2, 3, 4, 5)
Tuples
Exercise 1: Creating and Accessing Tuples
// Create the tuple
val book = ("The Hobbit", 1937, false)
// Access and print the elements
println(s"Title: ${book._1}")
println(s"Year: ${book._2}")
println(s"Read: ${book._3}")
Exercise 2: Tuple Destructuring
val person = ("John", "Doe", 30)
// Destructure the tuple
val (firstName, lastName, age) = person
// Print formatted string
println(s"$firstName $lastName is $age years old.")
Task: Write a function that takes two numbers as parameters, returns a tuple containing the sum and product of the two numbers.
def calculateSumAndProduct(a: Int, b: Int): (Int, Int) = {
(a + b, a * b)
}
// Test the function
val result = calculateSumAndProduct(5, 10)
println(s"Sum: ${result._1}, Product: ${result._2}")
Task: Given a list of tuples where each tuple contains the name of a fruit and its quantity, write a code snippet that prints the name of each fruit and its quantity in a formatted string.
val fruits = List(("Apple", 10), ("Banana", 5), ("Cherry", 20))
// Iterate and print
fruits.foreach {
case (fruit, quantity) =>
println(s"There are $quantity $fruit(s).")
}
spring-boot
Solutions
Solution 1 Begin
@main
def read(): Unit =
val scanner = new Scanner(System.in)
print("read: ")
val s = scanner.nextLine
println("write: " + s)
Solution 2 Welcome
@main
def read(): Unit =
val scanner = new Scanner(System.in)
print("your name: ")
val s = scanner.nextLine
println("Welcome " + s)
Solution 3 Add
@main
def sum(): Unit =
val scanner = new Scanner(System.in)
print("number1: ")
val number1 = scanner.nextInt
print("number2: ")
val number2 = scanner.nextInt
val sum = number1 + number2
println("sum: " + sum)
Solution 4 Menu
@main
def menu(): Unit =
println("Menu choices")
println("1 - hello")
println("2 - bye")
print("Your choice: ")
val scanner = new Scanner(System.in)
val menu = scanner.nextInt
if menu == 1 then
println("hello")
else if menu == 2 then
println("bye")
else println("wrong number")
Solution 5 Calculator
@main
def calculator(): Unit =
val scanner = new Scanner(System.in)
print("number1: ")
val number1 = scanner.nextInt
scanner.nextLine
print("number2: ")
val number2 = scanner.nextInt
scanner.nextLine
print("operator: ")
val operator = scanner.nextLine
val result: Int = operator match {
case "+" => number1 + number2
case "-" => number1 - number2
case "*" => number1 * number2
case "/" => number1 / number2
}
println("result: " + result)
Solution 6 Drawing
@main
def drawing(): Unit =
// 1 to 4 foreach { x =>
// println(x)
// }
//
// for (x <- 1 to 4) {
// println(x)
// }
for x <- 1 to 4 do
for y <- 1 to x do
print(" *")
println
for x <- 1 to 4 do
for y <- 1 until x do
print(" ")
println(" *")
Solution 7 List
@main
def lists(): Unit =
val array = Array(1, 2, 3, 4, 5, 6, 7, 8)
for x <- array do
println(x)
for x <- array.reverse do
println(x)
println(array.sum)
println(array.sum.toDouble / array.length)
Solution 8 String
Solution 9 Function
@main
def functions(): Unit =
def add(a: Int, b: Int) = a + b
println(add(3, 4))
def log(text: String) =
println(s"${new Date()} : $text")
println(log("the message"))
Project: Numberguess
with if..else
@main
def numberguessnew(): Unit =
val random = Random.nextInt(100)
val scanner = new Scanner(System.in)
var next = true
while next do
print("guess the number: ")
val number = scanner.nextInt
if number < random then
println("greater")
else if number > random then
println("smaller")
else if number == random then
println("found")
next = false
with match
@main
def numberguessnewer(): Unit =
val random = Random.nextInt(100)
val scanner = new Scanner(System.in)
var next = true
while next do
print("guess the number: ")
val number = scanner.nextInt
val result = number match
case gt if gt < random => "greater"
case lt if lt > random => "smaller"
case eq if eq == random => "found"
println(result)
if result == "found" then
next = false
project
Project: Calculator
import scala.swing.*
import scala.swing.event.*
@main
def calculator(): Unit = {
var display = ""
new MainFrame() {
title = "Calculator"
val label = new Label()
label.preferredSize = new Dimension(200, 30)
label.xAlignment = Alignment.Left
label.font = Font("Arial", Font.Plain, 18)
val buttonGrid = new GridPanel(4, 4)
val labels = List(
"7", "8", "9", "/",
"4", "5", "6", "*",
"1", "2", "3", "+",
"C", "0", "=", "-")
val buttons = labels.foreach(l => {
val b = new Button(l)
b.reactions.+= {
case ButtonClicked(e) => click(e)
}
buttonGrid.contents += b
}
)
contents = new BorderPanel {
add(label, BorderPanel.Position.North)
add(buttonGrid, BorderPanel.Position.Center)
}
var num1 = 0
var operator = ""
def click(but: AbstractButton): Unit = {
println(but.text)
but.text match {
case "7" | "8" | "9" | "4" | "5" | "6" | "1" | "2" | "3" | "0" =>
println("number")
label.text += but.text
case "*" | "/" | "+" | "-" =>
println("operator")
operator = but.text
num1 = label.text.toInt
label.text = ""
case "=" => calculate()
case _ => println("error")
}
}
def calculate(): Unit = {
val num2 = label.text.toInt
val result = operator match {
case "+" => num1 + num2
case "-" => num1 - num2
case "*" => num1 * num2
case "/" => num1 / num2
}
label.text = result.toString
}
size = new Dimension(300, 300)
centerOnScreen()
open()
}
}
Answers
Question 1: Variable Declarations
Correct Answer: C) 40
Question 2: Function Definition
Correct Answer: C) Multiplies two numbers
Question 3: Immutable List Operations
Correct Answer: A) List(2, 4, 6, 8, 10)
Question 4: Pattern Matching
Correct Answer: A) World
Question 5: Variable Mutability
Correct Answer: B) var
Question 6: Scala's Type Inference
Correct Answer: C) Omit the type of a variable when it is declared
Question 7: Function Declarations
Correct Answer: A) def sum(x: Int, y: Int): Int = { return x + y }
Question 8: Using a while Loop
Correct Answer: B) 15
Question 9: Iterating with for Loop
Correct Answer: B) 24
Question 10: Nested for Loops
Correct Answer: A) (1,1) (1,2)
(2,1) (2,2)
(3,1) (3,2)
Question 11: Basic if-else Logic
Correct Answer: A) You can vote.
Can vote: true
Question 12: Nested if-else with Logical Operators
Correct Answer: A) You can drive.
Question 13: if-else with Compound Conditions
Correct Answer: A) It's a good day for a walk.
Question 14: Complex if-else with Function Calls
Correct Answer: C) Not eligible: Too young
Question 15: Basic Pattern Matching
Correct Answer: C) Three
Question 16: Defining and Calling a Simple Function
Correct Answer: B) Hello, Alice!
Object Oriented
Exercises
Class
- Write a class Person with a constructor fields
nameand a methodsayHello - Instantiate the Person and call the sayHello method
Companion Object
- Write an object Person with a method apply
- Instantiate a Person with the companion object
Case Class
- Write a case class
Personwith a constructor fieldnameand a methodsayHello - Instantiate the Person (with the generated companion object)
Inheritance
- Write a class Customer with a constructor field
namethat extends Person - Instantiate the Customer and call the
sayHellomethod
Trait
- Write a trait Greeter with a
sayHellomethod - Use the Greeter on the Customer and call the sayHello method
Case Class
Exercise 1: Defining and Instantiating Case Classes
case class Book(title: String, authors: List[String])
val book1 = Book("Scala Programming", List("Martin Odersky"))
val book2 = Book("Programming in Scala", List("Martin Odersky", "Lex Spoon", "Bill Venners"))
val book3 = Book("Functional Programming in Scala", List("Paul Chiusano", "Rúnar Bjarnason"))
Exercise 2: Pattern Matching on Case Classes
def describeBook(book: Book): String = book match {
case Book(title, authors) if authors.length > 1 => s"$title, written by multiple authors."
case Book(title, authors) => s"$title, written by ${authors.head}."
}
println(describeBook(book1)) // Output: Scala Programming, written by Martin Odersky.
println(describeBook(book2)) // Output: Programming in Scala, written by multiple authors.
Exercise 3: Copying and Modifying Case Classes
val book1Updated = book1.copy(authors = book1.authors :+ "Venners Bill")
println(book1Updated)
Exercise 4: Case Classes in Collections
val books = List(book1, book2, book3)
def titlesByAuthor(author: String, books: List[Book]): List[String] =
books.filter(_.authors.contains(author)).map(_.title)
println(titlesByAuthor("Martin Odersky", books)) // Output: List(Scala Programming, Programming in Scala)
Pattern Matching
Solutions
Exercise 1: Basic Case Class and Pattern Matching
case class Person(name: String, age: Int)
def greet(person: Person): String =
person match
case Person(_, age) if age < 18 => "Hello, young one!"
case Person(name, _) => s"Hello, $name!"
// Test your function
val child = Person("Tim", 10)
val adult = Person("John", 30)
println(greet(child)) // Output: Hello, young one!
println(greet(adult)) // Output: Hello, John!
Exercise 2: Using Case Classes in Collections
def minors(people: List[Person]): List[String] =
people.collect { case Person(name, age) if age < 18 => name }
// Test your function
val people = List(Person("Alice", 17), Person("Bob", 20), Person("Charlie", 15))
println(minors(people)) // Output: List(Alice, Charlie)
Enum
Exercise 1: Basic Enum
enum DayOfWeek:
case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
def isWeekend(day: DayOfWeek): Boolean = day match
case DayOfWeek.Saturday | DayOfWeek.Sunday => true
case _ => false
// Test your function
println(isWeekend(DayOfWeek.Saturday)) // Output: true
println(isWeekend(DayOfWeek.Wednesday)) // Output: false
Exercise 2: Enum with Parameters
enum TrafficLight(val color: String):
case Red extends TrafficLight("red")
case Yellow extends TrafficLight("yellow")
case Green extends TrafficLight("green")
// Test
println(TrafficLight.Red.color) // Output: red
Exercise 3: Enums in Collections
def countLights(lights: List[TrafficLight]): Unit =
val counts = lights.groupBy(identity).view.mapValues(_.size).toMap
println(counts)
// Test your function
val lights = List(TrafficLight.Red, TrafficLight.Yellow, TrafficLight.Red, TrafficLight.Green, TrafficLight.Yellow)
countLights(lights) // Output: Map(Red -> 2, Yellow -> 2, Green -> 1)
Exercises
Exercise 1A
Create a Car class with the fields: mark and color. Give the Car the behavoir drive() and brake() with the state speed. Create a @main function Instantiate a Auto as a red Tesla as myCar. Let myCar drive and brake and print the speed.
class Car:
var mark: String = ""
var color: String = ""
var speed = 0
def drive() = speed += 10
def brake() = speed -= 10
@main
def main(): Unit =
val myCar = new Car()
myCar.mark = "Tesla"
myCar.color = "red"
myCar.drive()
println(s"speed: ${myCar.speed}")
Exercise 1B
Create hisCar a gray Suzuki.
Let it drive three times and print the speed
Exercise 2
Create a constructor on the fields: mark and color
Change the main function to use the constructor
Exercise 3
Make the Car immutable by changing var by val
Change de main function if needed.
exercise 4
Make the speed private. Generate the toString method Change de main function .
exercise 5
Create a new class Truck, inherited from the Car class.
Give the Truck an extra field freight.
Give the Truck a constructor met de fields mark and color
In de main function create a blue Volvo with the variable name: truck1 En give truck1 a freight of 1000.
Exercise 6
Override the function drive() and brake() with a slower drive and brake speed
Exercise 7
Create RaceAuto inherited from Car. Give it a field topSpeed
Instantiate a red Ferrari as secondCar
exercise 8
Create a trait Vehicle met de functions: drive() and brake() make the Car clas implementing this trait.
Exercise 9
Create a Bicycle class from the Vehicle trait.
Instantiate a gray VanMook as myBike
Project: Todo App
Step 1: Set Up Your Scala Project
First, ensure you have Scala and SBT (Scala Build Tool) installed on your machine. You can check by running the following commands:
scala -version
sbt -version
If you don't have them installed, follow the installation instructions from the official Scala website.
Next, in IntelliJ create a new SBT project
Step 2: Define the To-Do App Structure
Create a Scala object to hold your application logic. You can do this by creating a new file in the src/main/scala directory. Let's call it TodoApp.scala.
import scala.io.StdIn.readLine
import scala.collection.mutable.ListBuffer
object TodoApp:
case class Task(id: Int, description: String)
val tasks: ListBuffer[Task] = ListBuffer.empty
var nextId: Int = 1
def main(args: Array[String]): Unit =
var continue = true
while (continue)
println("\nTODO App")
println("1. Add Task")
println("2. List Tasks")
println("3. Delete Task")
println("4. Exit")
print("Choose an option: ")
readLine() match {
case "1" => addTask()
case "2" => listTasks()
case "3" => deleteTask()
case "4" => continue = false
case _ => println("Invalid option. Please try again.")
def addTask(): Unit =
print("Enter task description: ")
val description = readLine()
tasks += Task(nextId, description)
nextId += 1
println(s"Task added with id $nextId")
def listTasks(): Unit =
if tasks.isEmpty {
println("No tasks available.")
} else {
tasks.foreach(task => println(s"${task.id}. ${task.description}"))
}
def deleteTask(): Unit =
print("Enter task id to delete: ")
val id = readLine().toInt
val taskIndex = tasks.indexWhere(_.id == id)
if taskIndex != -1 {
tasks.remove(taskIndex)
println(s"Task with id $id deleted.")
} else {
println(s"Task with id $id not found.")
}
}
Step 3: Running Your Application
To run your application, use SBT:
sbt run
This command compiles and runs your Scala application. You should see the menu and be able to interact with your to-do list by adding, listing, and deleting tasks.
Explanation
- Task Case Class: This defines a simple structure to hold task data.
- tasks ListBuffer: A mutable list to hold the tasks.
- nextId: A counter to assign unique IDs to tasks.
- main Method: This is the entry point of the application. It shows the menu and reads user input.
- 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
- Task Case Class: Added a new field
priorityto theTaskcase class. - addTask Method: Now asks the user to input a priority level for the task.
- 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
- User Case Class: Added a
Usercase class withidandnamefields. - Task Case Class: Added a
userIdfield to associate tasks with a specific user. - users ListBuffer: A mutable list to hold the users.
- nextUserId: A counter to assign unique IDs to users.
- addUser Method: Allows adding a new user.
- listUsers Method: Lists all users.
- addTask Method: Now asks for a
userIdto assign the task to a specific user. - listTasks Method: Displays tasks with associated user names.
Project: Scribble
Complete Solution
import scala.swing.*
import scala.swing.event.*
import java.awt.{Color, Graphics2D, Point, Rectangle, Shape}
import scala.collection.mutable.ListBuffer
import scala.swing.BorderPanel.Position.{Center, North}
object ScribbleApp extends SimpleSwingApplication:
def top = new MainFrame:
title = "Rectangle and Oval Scribble App"
// Modes for drawing shapes
sealed trait DrawMode
case object Rectangle extends DrawMode
case object Oval extends DrawMode
var currentMode: DrawMode = Rectangle // Default mode
// Canvas is a Panel where shapes are drawn
object canvas extends Panel:
background = Color.white
preferredSize = new Dimension(400, 400)
focusable = true
listenTo(mouse.clicks, mouse.moves)
private var startPoint: Option[Point] = None
private val shapes: ListBuffer[Shape] = ListBuffer()
// React to mouse events to draw shapes
reactions += {
case e: MousePressed =>
startPoint = Some(e.point)
case e: MouseReleased =>
startPoint match
case Some(start) =>
val shape = currentMode match
case Rectangle => new Rectangle(start.x, start.y, e.point.x - start.x, e.point.y - start.y)
case Oval => new java.awt.geom.Ellipse2D.Double(start.x, start.y, e.point.x - start.x, e.point.y - start.y)
shapes += shape
repaint()
case None =>
startPoint = None
}
override def paintComponent(g: Graphics2D): Unit =
super.paintComponent(g)
g.setColor(Color.black)
shapes.foreach:
case rect: Rectangle => g.draw(rect)
case oval: java.awt.geom.Ellipse2D.Double => g.draw(oval)
case _ =>
// Mode selection buttons
val modePanel = new FlowPanel:
val rectangleButton = new Button("Rectangle")
val ovalButton = new Button("Oval")
contents += rectangleButton
contents += ovalButton
listenTo(rectangleButton, ovalButton)
reactions +=
case ButtonClicked(`rectangleButton`) => currentMode = Rectangle
case ButtonClicked(`ovalButton`) => currentMode = Oval
contents = new BorderPanel:
layout(modePanel) = North
layout(canvas) = Center
size = new Dimension(500, 500)
Exercises
Exercise 1: Basic List Operations
val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val firstElement = list.head // Output: 1
val lastElement = list.last // Output: 10
val allButFirst = list.tail // Output: List(2, 3, 4, 5, 6, 7, 8, 9, 10)
val allButLast = list.init // Output: List(1, 2, 3, 4, 5, 6, 7, 8, 9)
val containsFive = list.contains(5) // Output: true
Exercise 2: Concatenation and Addition
val evenList = List(2, 4, 6, 8, 10)
val oddList = List(1, 3, 5, 7, 9)
val concatenatedList = evenList ++ oddList // Output: List(2, 4, 6, 8, 10, 1, 3, 5, 7, 9)
val finalList = 0 :: concatenatedList // Output: List(0, 2, 4, 6, 8, 10, 1, 3, 5, 7, 9)
Exercise 3: Mapping and Filtering
val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val multipliedList = list.map(_ * 2) // Output: List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
val filteredList = multipliedList.filter(_ > 10) // Output: List(12, 14, 16, 18, 20)
Exercise 4: Folding and Reducing
val list = List(1, 2, 3, 4, 5)
val sum = list.foldLeft(0)(_ + _) // Output: 15
val product = list.reduce(_ * _) // Output: 120
Exercise 5: Using ListBuffer
import scala.collection.mutable.ListBuffer
val buffer = ListBuffer(1, 2, 3)
buffer += 4 // ListBuffer(1, 2, 3, 4)
buffer -= 2 // ListBuffer(1, 3, 4)
val immutableList = buffer.toList // Output: List(1, 3, 4)
Exercise 6: Working with Arrays
val array = Array(1, 2, 3, 4, 5)
array(2) = 10 // Array(1, 2, 10, 4, 5)
val length = array.length // Output: 5
array.foreach(println) // Output: 1 2 10 4 5
Exercise 7: Zipping and Unzipping
val numbers = List(1, 2, 3)
val words = List("one", "two", "three")
val zipped = numbers.zip(words) // Output: List((1,"one"), (2,"two"), (3,"three"))
val (unzippedNumbers, unzippedWords) = zipped.unzip
// unzippedNumbers: List(1, 2, 3)
// unzippedWords: List("one", "two", "three")
Exercise 8: Using Range to Create Lists
val rangeList = Range(1, 11).toList // Output: List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evenRangeList = Range(2, 21, 2).toList // Output: List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
val reverseRangeList = Range(10, 0, -1).toList // Output: List(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
Exercise 9: LazyList
val lazyList = LazyList.from(1).take(10) // Output: LazyList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evenNumbers = LazyList.from(2, 2) // Infinite LazyList of even numbers
val firstTenEvens = evenNumbers.take(10)
firstTenEvens.foreach(println) // Output: 2 4 6 8 10 12 14 16 18 20
Exercise 10: Grouping and Partitioning
val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val grouped = list.groupBy(_ % 2) // Output: Map(0 -> List(2, 4, 6, 8, 10), 1 -> List(1, 3, 5, 7, 9))
val (lessThanOrEqualFive, greaterThanFive) = list.partition(_ <= 5)
// lessThanOrEqualFive: List(1, 2, 3, 4, 5)
// greaterThanFive: List(6, 7, 8, 9, 10)
Exercises Bookstore filter map
The bookstore
case class Author(name: String, nationality: String)
case class Book(title: String, author: Author, year: Int, category: String, price: Double, description: Option[String])
val authors = List(
Author("George Orwell", "British"),
Author("Harper Lee", "American"),
Author("F. Scott Fitzgerald", "American"),
Author("Aldous Huxley", "British"),
Author("Herman Melville", "American"),
Author("J.D. Salinger", "American"),
Author("Yuval Noah Harari", "Israeli")
)
val library = List(
Book("1984", authors(0), 1949, "Dystopian", 15.99, Some("A dystopian social science fiction novel and cautionary tale.")),
Book("To Kill a Mockingbird", authors(1), 1960, "Fiction", 10.99, Some("A novel about the serious issues of rape and racial inequality.")),
Book("The Great Gatsby", authors(2), 1925, "Classic", 8.99, Some("A story of the mysteriously wealthy Jay Gatsby and his love for Daisy Buchanan.")),
Book("Brave New World", authors(3), 1932, "Dystopian", 12.99, None),
Book("Moby Dick", authors(4), 1851, "Classic", 9.99, Some("The narrative of Captain Ahab's obsessive quest to kill the giant white sperm whale Moby Dick.")),
Book("The Catcher in the Rye", authors(5), 1951, "Fiction", 14.99, Some("A novel about teenage rebellion and alienation."))
)
Use Case 1: Finding Books by a Specific Author
library.filter(_.author.name == authorName)
Use Case 2: Filtering Books Based on Price
library.filter(book => book.price >= minPrice && book.price <= maxPrice)
Use Case 3: Filtering Books by Category
library.filter(_.category == category)
Use Case 4: Displaying Book Titles with Prices
library.map(book => s"${book.title} - $${book.price}")
Use Case 5: Finding Books Published After a Certain Year
library.filter(_.year > year)
Use Case 6: Creating a Summary Description for Books
library.map(book => s"${book.title} by ${book.author.name} [${book.category}]")
Use Case 7: Finding Discounted Books
library.map(book => (book.title, book.price * (1 - discountRate)))
Exercises Bookstore collect
Use Case 1: Collecting Books with Descriptions
library.collect {
case Book(title, _, _, _, _, Some(description)) => s"$title: $description"
}
Use Case 2: Calculating Total Value of Books
library.foldLeft(0.0)((total, book) => total + book.price)
Use Case 3: Finding the Oldest Book
library.foldLeft(Option.empty[Book]) {
case (None, book) => Some(book)
case (Some(oldest), book) if book.year < oldest.year => Some(book)
case (oldest, _) => oldest
}
Use Case 4: Grouping Books by Decade
library.groupBy(book => (book.year / 10) * 10)
Use Case 5: Counting Books by Category
library.foldLeft(Map.empty[String, Int]) { (counts, book) =>
counts.updated(book.category, counts.getOrElse(book.category, 0) + 1)
}
Exercises Bookstore zip
Use Case 1: Creating Pairs of Book Titles and Prices with zip
val titles = library.map(_.title)
val prices = library.map(_.price)
titles.zip(prices)
Use Case 2: Creating Pairs of Book Titles and Their Indexes with zipWithIndex
library.map(_.title).zipWithIndex
Use Case 3: Matching Authors to Their Books with zip
val authors = library.map(_.author.name).distinct
val booksByAuthor = authors.map { author =>
val books = library.filter(_.author.name == author).map(_.title)
(author, books)
}
booksByAuthor
Use Case 4: Pairing Book Titles with Publication Years with zip
val titles = library.map(_.title)
val years = library.map(_.year)
titles.zip(years)
Use Case 5: Creating Pairs of Original and Discounted Prices with zip
val originalPrices = library.map(_.price)
val discountedPrices = originalPrices.map(price => price * (1 - discountRate))
originalPrices.zip(discountedPrices)
Use Case 6: Creating a List of Book Titles with Their Indexes for Display Purposes with zipWithIndex
library.map(_.title).zipWithIndex.map { case (title, index) => s"$index: $title" }
Exercises Bookstore GroupBy
Use Case 1: Grouping Books by Category
library.groupBy(_.category)
Use Case 2: Grouping Books by Author
library.groupBy(_.author.name)
Use Case 3: Grouping Books by Decade
library.groupBy(book => (book.year / 10) * 10)
Use Case 4: Grouping Books by Price Range
library.groupBy { book =>
if (book.price < 10) "<$10"
else if (book.price <= 20) "$10-$20"
else ">$20"
}
Use Case 5: Grouping Books by Availability of Description
library.groupBy { book =>
if (book.description.isDefined) "With Description"
else "Without Description"
}
Exercises Bookstore more
Use Case 1: Partition Books by Availability of Description
library.partition(_.description.isDefined)
Use Case 2: Find the Most Expensive Book
library.sortBy(-_.price).headOption
Use Case 3: Get Distinct Authors
library.map(_.author).distinct
Use Case 4: Flatten Nested List of Book Lists
bookLists.flatten
Use Case 5: Extract Titles of Books by a Specific Author Using flatMap
library.flatMap(book => if (book.author.name == authorName) Some(book.title) else None)
Use Case 6: Calculate Total Price of Books Using foldLeft
library.foldLeft(0.0)((total, book) => total + book.price)
Use Case 7: Combine Books and Authors into a Map
library.groupBy(_.author)
Project: Bookstore
import scala.io.StdIn.readLine
import scala.collection.mutable.ListBuffer
object BookstoreApp {
case class Book(id: Int, title: String, author: String, price: Double)
case class Customer(id: Int, name: String, email: String)
case class Order(id: Int, customerId: Int, bookId: Int)
val books: ListBuffer[Book] = ListBuffer.empty
val customers: ListBuffer[Customer] = ListBuffer.empty
val orders: ListBuffer[Order] = ListBuffer.empty
var nextBookId: Int = 1
var nextCustomerId: Int = 1
var nextOrderId: Int = 1
def main(args: Array[String]): Unit = {
var continue = true
while (continue) {
println("\nBookstore App")
println("1. Add Book")
println("2. List Books")
println("3. Add Customer")
println("4. List Customers")
println("5. Place Order")
println("6. List Orders")
println("7. Exit")
print("Choose an option: ")
readLine() match {
case "1" => addBook()
case "2" => listBooks()
case "3" => addCustomer()
case "4" => listCustomers()
case "5" => placeOrder()
case "6" => listOrders()
case "7" => continue = false
case _ => println("Invalid option. Please try again.")
}
}
}
def addBook(): Unit = {
print("Enter book title: ")
val title = readLine()
print("Enter book author: ")
val author = readLine()
print("Enter book price: ")
val price = readLine().toDouble
books += Book(nextBookId, title, author, price)
println(s"Book added with id $nextBookId: $title by $author at $$${price}")
nextBookId += 1
}
def listBooks(): Unit = {
if (books.isEmpty) {
println("No books available.")
} else {
println("Books:")
books.foreach(book => println(s"${book.id}. ${book.title} by ${book.author} - $$${book.price}"))
}
}
def addCustomer(): Unit = {
print("Enter customer name: ")
val name = readLine()
print("Enter customer email: ")
val email = readLine()
customers += Customer(nextCustomerId, name, email)
println(s"Customer added with id $nextCustomerId: $name, $email")
nextCustomerId += 1
}
def listCustomers(): Unit = {
if (customers.isEmpty) {
println("No customers available.")
} else {
println("Customers:")
customers.foreach(customer => println(s"${customer.id}. ${customer.name} (${customer.email})"))
}
}
def placeOrder(): Unit = {
print("Enter customer id: ")
val customerId = readLine().toInt
print("Enter book id: ")
val bookId = readLine().toInt
if (customers.exists(_.id == customerId) && books.exists(_.id == bookId)) {
orders += Order(nextOrderId, customerId, bookId)
println(s"Order placed with id $nextOrderId: Customer $customerId ordered Book $bookId")
nextOrderId += 1
} else {
println("Invalid customer id or book id.")
}
}
def listOrders(): Unit = {
if (orders.isEmpty) {
println("No orders placed.")
} else {
println("Orders:")
orders.foreach(order => {
val customer = customers.find(_.id == order.customerId).map(_.name).getOrElse("Unknown Customer")
val book = books.find(_.id == order.bookId).map(_.title).getOrElse("Unknown Book")
println(s"Order ${order.id}: Customer ${order.customerId} (${customer}) ordered Book ${order.bookId} (${book})")
})
}
}
}
Answers
Question 1: Collection Filtering
Correct Answer: B) List(2, 4, 5)
Question 2: Tuples and Destructuring
Correct Answer: B) person._2
Question 3: Defining Classes
Correct Answer: A) class MyClass(param: Type)
Question 4: Scala Collections
Correct Answer: B) List
Question 5: Object-Oriented Programming
Correct Answer: C) Scala allows defining singleton objects using the object keyword.
Question 68: Defining and Instantiating a Simple Class
Correct Answer: A) Hello, my name is Alice and I am 30 years old.
Hello, my name is Bob and I am 25 years old.
Question 6: Class with Private Members
Correct Answer: A) 10
Question 8: Overriding Methods in Subclasses
Correct Answer: A) Some sound
Woof
Question 9: Abstract Classes and Traits
Correct Answer: A) Circle area: 78.54
Question 10: Defining and Implementing Traits
Correct Answer: A) Good day, Alice.
Question 11: Mixing in Multiple Traits
Correct Answer: A) Walking...
Running...
Question 12: Overriding Trait Methods in Classes
Correct Answer: A) Woof
Question 13: Abstract and Concrete Methods in Traits
Correct Answer: A) 8
2
Question 14: Combining map and filter
Correct Answer: D) List("CHARLIE", "DAVID")
Question 15: Matching Tuples
Correct Answer: A) One apple
Question 16: Matching with Lists
Correct Answer: A) A list with three elements.
Question 17: Matching with Guards
Correct Answer: B) Full access
Question 18: For-Comprehension with Multiple Generators
Correct Answer: A) List((1,4), (1,5), (2,4), (2,5), (3,4), (3,5))
Function values
Exercise 1: Filter List with Function Parameter
def filterList(lst: List[Int], predicate: Int => Boolean): List[Int] = lst.filter(predicate)
// Test
println(filterList(List(1, 2, 3, 4, 5), _ % 2 == 0)) // Should print: List(2, 4)
Exercise 2: Implement a Custom map Function
def mapList[A, B](lst: List[A], func: A => B): List[B] = lst.map(func)
// Test
println(mapList(List("1", "2", "3"), _.toInt)) // Should print: List(1, 2, 3)
Exercise 3: A Higher-order Function that Returns a Function
def multiplier(factor: Int): Int => Int = number => number * factor
// Test
val triple = multiplier(3)
println(triple(5)) // Should print: 15
Exercise 4: Sorting with a Custom Comparator
def sortWithFunction[A](lst: List[A], comparator: (A, A) => Boolean): List[A] = lst.sortWith(comparator)
// Test
println(sortWithFunction(List(3, 1, 4, 2), (x: Int, y: Int) => x < y)) // Should print: List(1, 2, 3, 4)
Recursion
Exercise 1 sum
Write a function that takes a positive integer n and returns the sum of all the integers from 1 to n.
def sum(n: Int): Int =
if n <= 1 then n
else n + sum(n - 1)
// example usage
sum(5) // returns 15
Exercise 2 sumBetween
Write a function that takes two integers and returns the sum of all the integers between them, including the endpoints.
def sumBetween(x: Int, y: Int): Int =
if x == y then x
else x + sumBetween(x + 1, y)
// example usage
sumBetween(1, 5) // returns 15
Exercise 3 sumList
Write a function that takes a list of integers and returns the sum of all the integers in the list.
def sumList(list: List[Int]): Int = {
if list.isEmpty then 0
else list.head + sumList(list.tail)
}
// example usage
sumList(List(1, 2, 3, 4, 5)) // returns 15
Exercise 4 filter even
Write a function that takes a list of integers and returns a new list with all the even numbers.
def filterEven(list: List[Int]): List[Int] =
if list.isEmpty then List()
else if list.head % 2 == 0 then
list.head :: filterEven(list.tail)
else
filterEven(list.tail)
// example usage
filterEven(List(1, 2, 3, 4, 5, 6)) // returns List(2, 4, 6)
Exercise 5 longest string
Write a function that takes a list of strings and returns the length of the longest string in the list.
def longestString(list: List[String]): Int =
if list.isEmpty then 0
else
list.head.length.max(longestString(list.tail))
// example usage
longestString(List("apple", "banana", "orange", "pear")) // returns 6
tail-recursion
Abstract Data Type
Option
With Option you can eliminate the use of null in Scala.
An Option has two values Some or None.
- None wraps the null pointer
- Some wraps the value
scala> val optStr = Option("hello")
val optStr: Option[String] = Some(hello)
scala> optStr match
| case Some(value) => s"the result is: $value"
| case None => "there is no result"
|
val res5: String = the result is: hello
scala> val optStr = Option(null)
val optStr: Option[Null] = None
scala> optStr match
| case Some(value) => s"the result is: $value"
| case None => "there is no result"
|
val res6: String = there is no result
scala> val map = Map(1 -> "a", 2 -> "b", 3 -> "c")
val map: Map[Int, String] = Map(1 -> a, 2 -> b, 3 -> c)
scala> map(3)
val res0: String = c
scala> map(4)
java.util.NoSuchElementException: key not found: 4
at scala.collection.immutable.Map$Map3.apply(Map.scala:399)
... 40 elided
scala> map.get(3)
val res1: Option[String] = Some(c)
scala> map.get(4)
val res2: Option[String] = None
Try
`Try' wraps an exception.
An Option has two values Some or None.
- Failure wraps the exception
- Success wraps the value
val x = 3 / 0
java.lang.ArithmeticException: / by zero
... 40 elided
scala> import scala.util.{Try, Success, Failure}
scala> val x = Try(3 / 0)
val x: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)
scala> val x = Try(3 / 1)
val x: scala.util.Try[Int] = Success(3)
scala> x match
| case Success(value) => s"calculation successful with the value: $value"
| case Failure(ex) => s"calculation failure with exception: $ex"
|
val res1: String = calculation successful with the value: 3
Either
An Either also wrap two value Right and Left.
It is something between the Option and the Try and more general, but less used.
In this example an exception is wrapped inside an Either.
Try would be more natural.
def returnEither(value: String): Either[NumberFormatException, Int] =
try
Right(value.toInt)
catch
case ex: NumberFormatException => Left(ex)
def resultEither(value: String) =
returnEither(value) match
case Right(value) => s"Right value: $value"
case Left(ex) => s"Left exception: $ex"
scala> resultEither("12")
val res2: String = Right value: 12
scala> resultEither("ab")
val res3: String = Left exception: java.lang.NumberFormatException: For input string: "ab"
Exercises
Exercise 1
- Write a function that returns an Option[String]
- Call the function and pattern match on the possible results
Exercise 2
- Write a map with the days of the week 1 "monday", 2 :"tuesday", etc
- Write a function day that returns the name of the day or an error message
def dayOfTheWeek(day: Int, map: [Int, String]): Option[String] = ???
Exercise 3
Write a vector with the number 1 to 10
val vector = Vector(1,2,3,4,5,6,7,8,9,10)
Write a function indexOf that
- return the number at the index
- or an error message when the index is out of bounds.
def indexOf(index: Int, vector: Vector[Int]): Try[Int] = ???
Call the indexOf function and print the result in a pattern match
indexOf(2, vector) match ???
Exercise 4
Do the same as in Exercise 3 but now use 'Either'
def indexOf(index: Int, vector: Vector[Int]): Either[Int] = ???
Try
Handling exceptions is a crucial part of developing robust Scala applications. Scala provides a try-catch construct similar to other languages like Java, but with some functional twists that make it powerful and expressive. Scala's approach encourages the use of immutable values and provides mechanisms to deal with exceptions in a functional way.
Basic Try-Catch
In Scala, you use try-catch blocks to catch exceptions. The catch block uses pattern matching to handle different types of exceptions.
try
// Code that might throw an exception
val result = 10 / 0
catch
case e: ArithmeticException => println("Arithmetic Exception caught: " + e.getMessage)
case e: Exception => println("General exception caught: " + e.getMessage)
finally
// Optional finally block executes regardless of whether an exception was caught
println("Finally block executed")
The Try Type
Scala provides a Try type that represents a computation that may either result in an exception (Failure) or return a successfully computed value (Success). It is a better way to handle exceptions when working with functional programming paradigms.
To use Try, you need to import it from the Scala library:
import scala.util.{Try, Success, Failure}
You can wrap a computation in a Try, which will catch any non-fatal exceptions and return a Success with the value if the computation is successful, or a Failure with the exception if it is not.
val result: Try[Int] = Try(10 / 0)
You can then pattern match on the result:
result match
case Success(value) => println(s"Computation successful: $value")
case Failure(exception) => println(s"Computation failed with exception: ${exception.getMessage}")
Chaining Operations with Try
One of the benefits of using Try is the ability to chain operations without having to explicitly check for exceptions at each step.
def divide(a: Int, b: Int): Try[Int] = Try(a / b)
val result = divide(10, 0).map(_ * 2)
result match
case Success(value) => println(s"Result: $value")
case Failure(exception) => println(s"Error: ${exception.getMessage}")
For-Comprehensions with Try
For-comprehensions can be used with Try to perform multiple operations that may fail, in a clean and readable way:
val forResult =
for
a <- Try(10 / 5) // This succeeds
b <- Try(a / 0) // This fails
yield b * 2
forResult match {
case Success(value) => println(s"Result: $value")
case Failure(exception) => println(s"Error: ${exception.getMessage}")
}
In the above example, the computation automatically stops at the first failure, and forResult becomes a Failure containing the exception.
Throw
In Scala, unlike Java, you're not required to declare checked exceptions using throws in the method signature. Scala doesn't distinguish between checked and unchecked exceptions; all exceptions are unchecked, meaning the compiler does not force you to catch or declare any exceptions. However, for documentation purposes or when interfacing with Java code, you might want to indicate that a method can throw an exception.
To annotate a method with the information that it might throw an exception, you can use the @throws annotation. This can improve readability and maintainability of your Scala code, especially for developers coming from a Java background or when Scala code is being called from Java.
Here’s how to use the @throws annotation in Scala:
def divide(a: Int, b: Int): Int =
if (b == 0) then
throw new ArithmeticException("Division by zero.")
else
a / b
// Annotating the method with @throws
@throws(classOf[ArithmeticException])
def divideWithAnnotation(a: Int, b: Int): Int =
if (b == 0) then
throw new ArithmeticException("Division by zero.")
else
a / b
In this example, the divideWithAnnotation method is explicitly annotated to indicate that it might throw an ArithmeticException. The @throws annotation takes the class of the exception you're warning about as a parameter.
This annotation is particularly useful when Scala methods are invoked from Java code, as it will inform Java developers about the potential exceptions, allowing them to handle these exceptions appropriately.
Remember, while the @throws annotation can be helpful for documentation and interoperability with Java, it does not change how Scala code behaves or is compiled. Scala treats all exceptions as unchecked, and the use of @throws is purely informational.
Exercises
Exercise 1: Add two Option number
def addOptions(optA: Option[Int], optB: Option[Int]): Option[Int]
Use pattern matching and for comprehension
(Extra) Use map and flatMap
Exercise 2: Read a file
Read from file
with try-catch
def readFileWithTryCatch(filePath: String): String
with Try, Success and Failure
def readFileWithTry(filePath: String): Try[String]
Exercise 3: Login with password check
Implement a simple login function using Either.
Leftwill return an error message if the login fails,Rightwill 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 existslist- give an error if the list has more then 10 itemsdelete- 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
nameprice
ShoppingBasket with the methods
-
add- give an error if an article already exists -
list- give an error if the list has no items -
delete- give an error if an item does not exist -
filter on prices between an low and high value
Add an amount field to the article Sort the list alphabetically Sort the list on alphabet and amount
Create a main method that tests the shopping basket.
Aswers
Question 1: Basic try-catch Usage
Correct Answer: A) Cannot divide by zero.
Operation attempted.
Question 2: Catching Multiple Exceptions
Correct Answer: B) Array index out of bounds.
Search attempted.
Question 3: Using try-catch with a Return Value
Correct Answer: A) Division operation processed.
Error: Division by zero.
Division operation processed.
Result: 2
Question 4: Using Option for Safe Value Access
Correct Answer: A) Some(a)
None
Question 5: Handling Multiple Errors with Either
Correct Answer: A) Right(5)
Left("Cannot divide by zero.")
Question 6: Graceful Error Handling with Try
Correct Answer: A) Success(100)
Failure(java.lang.NumberFormatException)
Question 7: Combining Option Values with flatMap
Correct Answer: A) Some(5)
None
Question 8: Using for-comprehension with Option
Correct Answer: A) Some(30)
Question 9: Safely Accessing Potentially Null Objects
Correct Answer: A) JohnDoe
Unknown
Question 11: Pattern Matching with Options to Handle Nulls
Correct Answer: A) Found word: Scala
No word found
Question 12: Encapsulating Nullable References with Options
Correct Answer: A) 5
0
Question 13: Creating and Accessing a Map
Correct Answer: A) Some(Paris)
Not found
Question 14: Updating and Adding Elements to a Map
Correct Answer: C) Map(1 -> "one", 2 -> "TWO", 3 -> "three")
Question 15: Iterating Over a Map
Correct Answer: A) Alice is 30 years old
Bob is 25 years old
Charlie is 28 years old
Question 16: Using Options for Safe Access
Correct Answer: B) 10
Question 17: Handling Exceptions with Try and Match
Correct Answer: B) Error: / by zero
Question 18: Using flatMap to Flatten and Transform
Correct Answer: A) List(2, 4, 6, 8, 10, 12)
Question 19: Folding a List with foldLeft
Correct Answer: A) 15
Question 20: Basic Usage of zip
Correct Answer: A) List(("Alice", 25), ("Bob", 30), ("Charlie", 28))
Question 21: zip with Unequal Collection Sizes
Correct Answer: B) List((1, 'a'), (2, 'b'), (3, 'c'))
Question 22: Using zipWithIndex
Correct Answer: A) List(("apple", 0), ("banana", 1), ("cherry", 2))
Question 23: Basic Usage of groupBy
Correct Answer: B) Map(a -> List("apple", "apricot"), b -> List("banana"), p -> List("pear", "peach"))
Question 24: Higher-Order Functions
Correct Answer: B) 25
Question 25: Simple Recursive Function for Summation
Correct Answer: A) 55 and 15
Solutions
Solution 1: Unit Testing with FunSuite
import org.scalatest.funsuite.AnyFunSuite
class StringUtilityTest extends AnyFunSuite {
test("reverse should reverse a string") {
assert(StringUtility.reverse("hello") === "olleh")
}
test("reverse should handle empty string") {
assert(StringUtility.reverse("") === "")
}
test("isPalindrome should return true for a palindrome") {
assert(StringUtility.isPalindrome("madam"))
}
test("isPalindrome should return false for a non-palindrome") {
assert(!StringUtility.isPalindrome("hello"))
}
}
For the ShoppingCart exercise, let's first define the solution for the ShoppingCart class and then proceed with writing a test suite for it using ScalaTest.
Implementing the ShoppingCart and Item
Here's a basic implementation of the ShoppingCart class and Item case class:
Writing Tests for the ShoppingCart
Now, let's write tests for this ShoppingCart implementation. We'll test adding items, removing items, applying discounts, and calculating the total.
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class ShoppingCartTest extends AnyFunSuite with Matchers {
test("Adding items to the shopping cart should increase total accordingly") {
val cart = new ShoppingCart
cart.addItem(Item("1", "Apple", 0.60, 1))
cart.addItem(Item("2", "Banana", 0.40, 2))
cart.total shouldEqual 1.40
}
test("Removing items from the shopping cart should decrease total accordingly") {
val cart = new ShoppingCart
cart.addItem(Item("1", "Apple", 0.60, 1))
cart.addItem(Item("2", "Banana", 0.40, 2))
cart.removeItem("2")
cart.total shouldEqual 0.60
}
test("Applying a discount should reduce the total price") {
val cart = new ShoppingCart
cart.addItem(Item("1", "Apple", 1.00, 2)) // Total before discount: 2.00
cart.applyDiscount("DISCOUNT10") // 10% discount
cart.total shouldEqual 1.80 // 10% off of 2.00
}
test("Adding multiple quantities of an item should be reflected in the total") {
val cart = new ShoppingCart
cart.addItem(Item("1", "Apple", 0.50, 2)) // 2 Apples
cart.total shouldEqual 1.00
}
test("Adding the same item again increases its quantity") {
val cart = new ShoppingCart
cart.addItem(Item("1", "Apple", 0.50, 1))
cart.addItem(Item("1", "Apple", 0.50, 1)) // Adding again
cart.total shouldEqual 1.00 // Reflects total for 2 Apples
}
}
Solution 3: Testing a Password Validator
Tests for the PasswordValidator might look like this:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class PasswordValidatorTest extends AnyFunSuite with Matchers {
test("A valid password") {
assert(PasswordValidator.isValid("ValidPass123"))
}
test("Password is too short") {
PasswordValidator.isValid("Short1") should be(false)
}
test("Password lacks a digit") {
PasswordValidator.isValid("NoDigitsHere!") should be(false)
}
test("Password lacks an uppercase letter") {
PasswordValidator.isValid("alllowercase1") should be(false)
}
test("Password lacks a lowercase letter") {
PasswordValidator.isValid("ALLUPPERCASE1") should be(false)
}
}
Solution 4: Property-Based Testing for a String Concatenation Utility
For property-based tests of StringConcatenationUtility, using ScalaTest with ScalaCheck:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalatest.matchers.should.Matchers
import org.scalacheck.Prop.forAll
class StringConcatenationUtilityTest extends AnyFunSuite with ScalaCheckPropertyChecks with Matchers {
test("Concatenating two strings should include both with a space in between") {
forAll { (a: String, b: String) =>
StringConcatenationUtility.concatenate(a, b) should be (s"$a $b")
}
}
test("Concatenating an empty string with a non-empty string results in the latter with an extra space") {
forAll { (a: String) =>
StringConcatenationUtility.concatenate("", a) should startWith (" ")
StringConcatenationUtility.concatenate(a, "") should endWith (" ")
}
}
}
Solution 5: Testing a Fibonacci Number Generator
Tests for FibonacciGenerator could be:
class FibonacciGeneratorTest extends AnyFunSuite with Matchers {
test("Fibonacci numbers for known values") {
val knownValues = Seq((0, 0), (1, 1), (2, 1), (3, 2), (4, 3), (5, 5), (6, 8), (7, 13))
knownValues.foreach { case (n, expected) =>
FibonacciGenerator.fibonacci(n) should be(expected)
}
}
test("Fibonacci number for negative input") {
intercept[IllegalArgumentException] {
FibonacciGenerator.fibonacci(-1)
}
}
}
Note: The performance or stack overflow test is not included as it's more about optimization and implementation strategy rather than simple functional testing.
Solution 6: Integration Testing for a File Processing Utility
An integration test for FileProcessingUtility involves file operations:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.BeforeAndAfter
import java.nio.file.{Files, Paths}
class FileProcessingUtilityTest extends AnyFunSuite with BeforeAndAfter with Matchers {
val inputPath = "testInput.txt"
val outputPath = "testOutput.txt"
before {
val content = "This is a test."
Files.write(Paths.get(inputPath), content.getBytes)
}
after {
Files.deleteIfExists(Paths.get(inputPath))
Files.deleteIfExists(Paths.get(outputPath))
}
test("File processing converts text to uppercase") {
FileProcessingUtility.processFile(inputPath, outputPath)
val result = Files.readAllLines(Paths.get(outputPath)).get(0)
result should be("THIS IS A TEST.")
}
test("Handling of non-existent input files") {
intercept[Exception] {
FileProcessingUtility.processFile("nonExistentFile.txt", outputPath)
}
}
}
project
Solutions
1. Hello World
Create a file named hello.scala.html in your app/views directory. This template should accept a single string parameter and display it in an HTML paragraph.
@(name: String)
<!DOCTYPE html>
<html>
<head>
<title>Hello World Example</title>
</head>
<body>
<p>Hello, @name!</p>
</body>
</html>
2. List Rendering
Create a file named listRendering.scala.html in your app/views directory. This template will accept a List of strings and render them in an unordered list. It will display a message if the list is empty.
@(items: List[String])
<!DOCTYPE html>
<html>
<head>
<title>List Rendering Example</title>
</head>
<body>
@if(items.isEmpty) {
<p>No items to display.</p>
} else {
<ul>
@for(item <- items) {
<li>@item</li>
}
</ul>
}
</body>
</html>
3. Form Submission Display
For this exercise, you'll need a controller to handle the form submission and two Twirl templates: one for the form and another to display the submitted data.
Form Template (userForm.scala.html):
@(formAction: Call)
<!DOCTYPE html>
<html>
<head>
<title>User Form</title>
</head>
<body>
<form action="@formAction" method="POST">
<div>
<label for="firstName">First Name:</label>
<input type="text" id="firstName" name="firstName">
</div>
<div>
<label for="lastName">Last Name:</label>
<input type="text" id="lastName" name="lastName">
</div>
<button type="submit">Submit</button>
</form>
</body>
</html>
Display Template (displayUser.scala.html):
@(firstName: String, lastName: String)
<!DOCTYPE html>
<html>
<head>
<title>Display User</title>
</head>
<body>
<p>First Name: @firstName</p>
<p>Last Name: @lastName</p>
</body>
</html>
Controller Methods (Scala):
In your controller, you will need to add methods to render the form and handle the submission. This example assumes you are using a Scala controller.
import play.api.mvc._
import javax.inject._
class UserController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
def showForm = Action { implicit request: Request[AnyContent] =>
Ok(views.html.userForm(routes.UserController.handleSubmit))
}
def handleSubmit = Action { implicit request: Request[AnyContent] =>
val postVals = request.body.asFormUrlEncoded
val firstName = postVals.get("firstName").flatMap(_.headOption).getOrElse("")
val lastName = postVals.get("lastName").flatMap(_.headOption).getOrElse("")
Ok(views.html.displayUser(firstName, lastName))
}
}
Ensure your routes file (conf/routes) includes the necessary routes for these actions:
GET /form controllers.UserController.showForm
POST /submit-form controllers.UserController.handleSubmit
4. Conditional Content
Create a file named conditionalContent.scala.html in your app/views directory. This template will display different content based on the Boolean value passed to it.
@(showMessage: Boolean)
<!DOCTYPE html>
<html>
<head>
<title>Conditional Content Example</title>
</head>
<body>
@if(showMessage) {
<p>This is a conditional message shown only when 'showMessage' is true.</p>
} else {
<p>This message is shown when 'showMessage' is false.</p>
}
</body>
</html>
5. Nested Templates
For nested templates, you typically have a main layout template and one or more child templates. Let's create a simple layout template and a child template that uses it.
Main Layout Template (mainLayout.scala.html):
@(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
<header>
<h1>Site Header</h1>
</header>
@content
<footer>
<p>Site Footer</p>
</footer>
</body>
</html>
Child Template (childPage.scala.html):
@()
@mainLayout("Child Page") {
<p>This is the content of the child page.</p>
}
6. Loop with Conditional
Create a file named loopWithConditional.scala.html in your app/views directory. This template will iterate over a list of integers, displaying each in a list item, and highlight numbers divisible by 3.
@(numbers: List[Int])
<!DOCTYPE html>
<html>
<head>
<title>Loop with Conditional Example</title>
</head>
<body>
<ul>
@for(number <- numbers) {
<li class="@if(number % 3 == 0) {highlight}">
@number
</li>
}
</ul>
</body>
</html>
You might also include some CSS within the <head> section or an external stylesheet to style the .highlight class for numbers divisible by 3, like so:
<style>
.highlight {
font-weight: bold;
}
</style>
Project: Shopping Basket
Implement the ShoppingBasket from previous chapters in Play
Step1: With database
Step2: With Anorm
Step3: With Slick
Step4: Rest API
Step5: Error Handling
Step6: Add Bootstrap
Step7: More and more
Exercises
Exercise 1: Simple Ping-Pong
import akka.actor._
class PingActor(pongActor: ActorRef) extends Actor {
def receive: Receive = {
case "Start" =>
println("Ping")
pongActor ! "Ping"
case "Pong" =>
println("Ping")
Thread.sleep(500) // Short delay
pongActor ! "Ping"
}
}
class PongActor extends Actor {
def receive: Receive = {
case "Ping" =>
println("Pong")
sender() ! "Pong"
}
}
object PingPongApp extends App {
val system = ActorSystem("PingPongSystem")
val pongActor = system.actorOf(Props[PongActor], "pongActor")
val pingActor = system.actorOf(Props(new PingActor(pongActor)), "pingActor")
pingActor ! "Start"
}
Exercise 2: Counter Actor
import akka.actor._
class CounterActor extends Actor {
private var counter = 0
def receive: Receive = {
case "Increment" =>
counter += 1
case "Decrement" =>
counter -= 1
case "Get" =>
sender() ! counter
}
}
object CounterApp extends App {
val system = ActorSystem("CounterSystem")
val counterActor = system.actorOf(Props[CounterActor], "counterActor")
counterActor ! "Increment"
counterActor ! "Increment"
counterActor ! "Decrement"
counterActor ! "Get"
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
implicit val timeout: Timeout = 5.seconds
val future = counterActor ? "Get"
future.map(result => println(s"Final counter value: $result"))
}
Exercise 3: Actor Hierarchy and Supervision
import akka.actor._
class ChildActor extends Actor {
def receive: Receive = {
case "DoWork" =>
if (scala.util.Random.nextBoolean()) throw new RuntimeException("Failure!")
println("Work done!")
}
}
class ParentActor extends Actor {
val childActor = context.actorOf(Props[ChildActor], "childActor")
override val supervisorStrategy: SupervisorStrategy =
OneForOneStrategy() {
case _: RuntimeException => SupervisorStrategy.Restart
}
def receive: Receive = {
case msg => childActor forward msg
}
}
object SupervisionApp extends App {
val system = ActorSystem("SupervisionSystem")
val parentActor = system.actorOf(Props[ParentActor], "parentActor")
parentActor ! "DoWork"
parentActor ! "DoWork"
parentActor ! "DoWork"
}
Exercise 4: Bank Account Actor
import akka.actor._
case class Deposit(amount: Double)
case class Withdraw(amount: Double)
case object GetBalance
class BankAccountActor extends Actor {
private var balance = 0.0
def receive: Receive = {
case Deposit(amount) =>
balance += amount
case Withdraw(amount) =>
if (balance >= amount) balance -= amount
else println("Insufficient funds")
case GetBalance =>
sender() ! balance
}
}
object BankAccountApp extends App {
val system = ActorSystem("BankAccountSystem")
val bankAccountActor = system.actorOf(Props[BankAccountActor], "bankAccountActor")
bankAccountActor ! Deposit(100)
bankAccountActor ! Withdraw(50)
bankAccountActor ! GetBalance
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
implicit val timeout: Timeout = 5.seconds
val future = bankAccountActor ? GetBalance
future.map(balance => println(s"Final balance: $balance"))
}
Exercise 5: Master-Worker Pattern
import akka.actor._
case class Work(nums: List[Int])
case object GetSum
class WorkerActor extends Actor {
def receive: Receive = {
case Work(nums) =>
val sum = nums.sum
sender() ! sum
}
}
class MasterActor(workerCount: Int) extends Actor {
private var sum = 0
private var receivedResponses = 0
private val workers = (1 to workerCount).map(_ => context.actorOf(Props[WorkerActor]))
def receive: Receive = {
case Work(nums) =>
val chunks = nums.grouped(nums.size / workerCount).toList
chunks.zip(workers).foreach {
case (chunk, worker) => worker ! Work(chunk)
}
case result: Int =>
sum += result
receivedResponses += 1
if (receivedResponses == workerCount) context.parent ! sum
}
}
object MasterWorkerApp extends App {
val system = ActorSystem("MasterWorkerSystem")
val masterActor = system.actorOf(Props(new MasterActor(3)), "masterActor")
masterActor ! Work(List(1, 2, 3, 4, 5, 6, 7, 8, 9))
masterActor ! GetSum
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
implicit val timeout: Timeout = 5.seconds
val future = masterActor ? GetSum
future.map(result => println(s"Final sum: $result"))
}
todo-untyped
todoapp
shopping-basket untyped
shoppingbasket-typed
Exercises
Exercise 1: Hello Actor
Task: Create an actor named HelloActor that receives a String message and prints out Hello, [message]!.
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.Behavior
object HelloActor {
final case class SayHello(name: String)
def apply(): Behavior[SayHello] = Behaviors.receive { (context, message) =>
println(s"Hello, ${message.name}!")
Behaviors.same
}
}
// App to test HelloActor
object HelloApp extends App {
val system: ActorSystem[HelloActor.SayHello] = ActorSystem(HelloActor(), "helloSystem")
system ! HelloActor.SayHello("Akka")
}
Exercise 2: Counter Actor
Task: Implement a CounterActor that can receive messages to increment, decrement, and print its internal count.
object CounterActor {
sealed trait Command
case object Increment extends Command
case object Decrement extends Command
case object Print extends Command
def apply(): Behavior[Command] = counterBehavior(0)
private def counterBehavior(count: Int): Behavior[Command] =
Behaviors.receive { (context, message) =>
message match {
case Increment => counterBehavior(count + 1)
case Decrement => counterBehavior(count - 1)
case Print =>
println(s"Current count is $count")
Behaviors.same
}
}
}
// App to test CounterActor
object CounterApp extends App {
val system: ActorSystem[CounterActor.Command] = ActorSystem(CounterActor(), "counterSystem")
system ! CounterActor.Increment
system ! CounterActor.Increment
system ! CounterActor.Decrement
system ! CounterActor.Print // Should print "Current count is 1"
}
Exercise 3: Ping-Pong Actors
Task: Create two actors, PingActor and PongActor. PingActor should send a ping message to PongActor, and PongActor should respond with a pong message.
object PingPongActor {
sealed trait Command
case class Ping(replyTo: ActorRef[Command]) extends Command
case class Pong(replyTo: ActorRef[Command]) extends Command
def apply(): Behavior[Command] = Behaviors.receive { (context, message) =>
message match {
case Ping(replyTo) =>
println("Ping received")
replyTo ! Pong(context.self)
Behaviors.same
case Pong(replyTo) =>
println("Pong received")
replyTo ! Ping(context.self)
Behaviors.same
}
}
}
// App to test PingPongActor
object PingPongApp extends App {
val system: ActorSystem[PingPongActor.Command] = ActorSystem(PingPongActor(), "pingPongSystem")
val pongActor = system
val pingActor = system
pingActor ! PingPongActor.Ping(pongActor)
}
Projects
Todo List
In this project we will create a todolist application with Akka Actors
Creata a class
TodoItem with a field
task
TodoList with methods
addlistdelete
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
nameprice
ShoppingBasket with the methods
addlistcalcTotal
Create a main method that tests the shopping basket.
Project: Bookstore
To create an Akka Actor-based version of the bookstore, we need to use Akka to manage concurrency and message passing between different components of the application. Below is an example implementation.
Step 1: Add Akka Dependencies
First, add Akka dependencies to your build.sbt file:
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.6.18"
Step 2: Define the Project Structure
Create directories and files as follows:
src/main/scala/
├── models/
│ ├── Book.scala
│ ├── Customer.scala
│ └── Order.scala
├── actors/
│ ├── BookActor.scala
│ ├── CustomerActor.scala
│ └── OrderActor.scala
└── Main.scala
Step 3: Define Models
models/Book.scala
package models
case class Book(id: Int, title: String, author: String, price: Double, stock: Int)
models/Customer.scala
package models
case class Customer(id: Int, name: String, email: String, address: String)
models/Order.scala
package models
case class Order(id: Int, customerId: Int, bookId: Int, quantity: Int, status: String)
Step 4: Implement Actors
actors/BookActor.scala
package actors
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.Book
object BookActor {
sealed trait Command
case class AddBook(title: String, author: String, price: Double, stock: Int, replyTo: ActorRef[Response]) extends Command
case class ListBooks(replyTo: ActorRef[Response]) extends Command
case class FindBookById(id: Int, replyTo: ActorRef[Response]) extends Command
case class UpdateStock(id: Int, newStock: Int, replyTo: ActorRef[Response]) extends Command
sealed trait Response
case class BookAdded(book: Book) extends Response
case class BooksListed(books: List[Book]) extends Response
case class BookFound(book: Option[Book]) extends Response
case class StockUpdated(book: Option[Book]) extends Response
def apply(): Behavior[Command] = {
var books: Map[Int, Book] = Map.empty
var nextBookId: Int = 1
Behaviors.receiveMessage {
case AddBook(title, author, price, stock, replyTo) =>
val book = Book(nextBookId, title, author, price, stock)
books += (nextBookId -> book)
nextBookId += 1
replyTo ! BookAdded(book)
Behaviors.same
case ListBooks(replyTo) =>
replyTo ! BooksListed(books.values.toList)
Behaviors.same
case FindBookById(id, replyTo) =>
replyTo ! BookFound(books.get(id))
Behaviors.same
case UpdateStock(id, newStock, replyTo) =>
val updatedBook = books.get(id).map(book => book.copy(stock = newStock))
updatedBook.foreach(book => books += (id -> book))
replyTo ! StockUpdated(updatedBook)
Behaviors.same
}
}
}
actors/CustomerActor.scala
package actors
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.Customer
object CustomerActor {
sealed trait Command
case class AddCustomer(name: String, email: String, address: String, replyTo: ActorRef[Response]) extends Command
case class ListCustomers(replyTo: ActorRef[Response]) extends Command
case class FindCustomerById(id: Int, replyTo: ActorRef[Response]) extends Command
sealed trait Response
case class CustomerAdded(customer: Customer) extends Response
case class CustomersListed(customers: List[Customer]) extends Response
case class CustomerFound(customer: Option[Customer]) extends Response
def apply(): Behavior[Command] = {
var customers: Map[Int, Customer] = Map.empty
var nextCustomerId: Int = 1
Behaviors.receiveMessage {
case AddCustomer(name, email, address, replyTo) =>
val customer = Customer(nextCustomerId, name, email, address)
customers += (nextCustomerId -> customer)
nextCustomerId += 1
replyTo ! CustomerAdded(customer)
Behaviors.same
case ListCustomers(replyTo) =>
replyTo ! CustomersListed(customers.values.toList)
Behaviors.same
case FindCustomerById(id, replyTo) =>
replyTo ! CustomerFound(customers.get(id))
Behaviors.same
}
}
}
actors/OrderActor.scala
package actors
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.{Order, Book, Customer}
object OrderActor {
sealed trait Command
case class PlaceOrder(customerId: Int, bookId: Int, quantity: Int, replyTo: ActorRef[Response]) extends Command
case class ListOrders(replyTo: ActorRef[Response]) extends Command
case class ListOrdersByCustomer(customerId: Int, replyTo: ActorRef[Response]) extends Command
sealed trait Response
case class OrderPlaced(order: Option[Order]) extends Response
case class OrdersListed(orders: List[Order]) extends Response
case class OrdersByCustomerListed(orders: List[Order]) extends Response
def apply(bookActor: ActorRef[BookActor.Command], customerActor: ActorRef[CustomerActor.Command]): Behavior[Command] = {
var orders: List[Order] = List.empty
var nextOrderId: Int = 1
Behaviors.receiveMessage {
case PlaceOrder(customerId, bookId, quantity, replyTo) =>
val order = Order(nextOrderId, customerId, bookId, quantity, "Placed")
orders = orders :+ order
nextOrderId += 1
replyTo ! OrderPlaced(Some(order))
Behaviors.same
case ListOrders(replyTo) =>
replyTo ! OrdersListed(orders)
Behaviors.same
case ListOrdersByCustomer(customerId, replyTo) =>
replyTo ! OrdersByCustomerListed(orders.filter(_.customerId == customerId))
Behaviors.same
}
}
}
Step 5: Create the Main Application
Main.scala
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}
import actors.{BookActor, CustomerActor, OrderActor}
import scala.util.{Try, Success, Failure}
import utils.Utils._
object Main extends App {
sealed trait Command
case class Run() extends Command
def apply(): Behavior[Command] = Behaviors.setup { context =>
val bookActor = context.spawn(BookActor(), "BookActor")
val customerActor = context.spawn(CustomerActor(), "CustomerActor")
val orderActor = context.spawn(OrderActor(bookActor, customerActor), "OrderActor")
def run(): Unit = {
var continue = true
while (continue) {
println("\nBookstore App")
println("1. Add Book")
println("2. List Books")
println("3. Add Customer")
println("4. List Customers")
println("5. Place Order")
println("6. List Orders")
println("7. List Orders by Customer")
println("8. Exit")
print("Choose an option: ")
scala.io.StdIn.readLine() match {
case "1" => addBook(bookActor)
case "2" => listBooks(bookActor)
case "3" => addCustomer(customerActor)
case "4" => listCustomers(customerActor)
case "5" => placeOrder(orderActor)
case "6" => listOrders(orderActor)
case "7" => listOrdersByCustomer(orderActor)
case "8" => continue = false
case _ => println("Invalid option. Please try again.")
}
}
}
def addBook(bookActor: ActorRef[BookActor.Command]): Unit = {
val title = readLineWithPrompt("Enter book title: ")
val author = readLineWithPrompt("Enter book author: ")
val priceTry = readDoubleWithPrompt("Enter book price: ")
val stockTry = readIntWithPrompt("Enter book stock: ")
(priceTry, stockTry) match {
case (Success(price), Success(stock)) =>
bookActor ! BookActor.AddBook(title, author, price, stock, context.self)
println(s"Book added: $title by $author")
case (Failure(priceEx), _) =>
println(s"Invalid price input. Error: ${priceEx.getMessage}")
case (_, Failure(stockEx)) =>
println(s"Invalid stock input. Error: ${stockEx.getMessage}")
}
}
def listBooks(bookActor: ActorRef[BookActor.Command]): Unit = {
bookActor ! BookActor.ListBooks(context.self)
}
def addCustomer(customerActor: ActorRef[CustomerActor.Command]): Unit = {
val name = readLineWithPrompt("Enter customer name: ")
val email = readLineWithPrompt("Enter customer email: ")
val address = readLineWithPrompt("Enter customer address: ")
customerActor ! CustomerActor.AddCustomer(name, email, address, context.self)
println(s"Customer added: $name")
}
def listCustomers(customerActor: ActorRef[CustomerActor.Command]): Unit = {
customerActor ! CustomerActor.ListCustomers(context.self)
}
def placeOrder(orderActor: ActorRef[OrderActor.Command]): Unit = {
val customerIdTry = readIntWithPrompt("Enter customer id: ")
val bookIdTry = readIntWithPrompt("Enter book id: ")
val quantityTry = readIntWithPrompt("Enter quantity: ")
(customerIdTry, bookIdTry, quantityTry) match {
case (Success(customerId), Success(bookId), Success(quantity)) =>
orderActor ! OrderActor.PlaceOrder(customerId, bookId, quantity, context.self)
println(s"Order placed: Customer $customerId ordered Book $bookId")
case (Failure(customerIdEx), _, _) =>
println(s"Invalid customer id input. Error: ${customerIdEx.getMessage}")
case (_, Failure(bookIdEx), _) =>
println(s"Invalid book id input. Error: ${bookIdEx.getMessage}")
case (_, _, Failure(quantityEx)) =>
println(s"Invalid quantity input. Error: ${quantityEx.getMessage}")
}
}
def listOrders(orderActor: ActorRef[OrderActor.Command]): Unit = {
orderActor ! OrderActor.ListOrders(context.self)
}
def listOrdersByCustomer(orderActor: ActorRef[OrderActor.Command]): Unit = {
val customerIdTry = readIntWithPrompt("Enter customer id: ")
customerIdTry match {
case Success(customerId) =>
orderActor ! OrderActor.ListOrdersByCustomer(customerId, context.self)
case Failure(ex) =>
println(s"Invalid customer id input. Error: ${ex.getMessage}")
}
}
Behaviors.receiveMessage {
case Run() =>
run()
Behaviors.same
}
}
val system = ActorSystem(Main(), "BookstoreSystem")
system ! Run()
}
Step 6: Implement Utility Functions
utils/Utils.scala
package utils
import scala.util.{Try, Success, Failure}
object Utils {
def readLineWithPrompt(prompt: String): String = {
print(prompt)
scala.io.StdIn.readLine()
}
def readIntWithPrompt(prompt: String): Try[Int] = {
print(prompt)
Try(scala.io.StdIn.readInt())
}
def readDoubleWithPrompt(prompt: String): Try[Double] = {
print(prompt)
Try(scala.io.StdIn.readDouble())
}
}
Running Your Akka Actor-based Application
To run your application, use SBT:
sbt run
todoapp
shoppingbasket
Exercises
Here are three exercises focusing on Scala Futures to practice asynchronous programming. These exercises cover different aspects of working with Futures, including basic usage, composition, error handling, and integration with external services or databases.
Exercise 1: Asynchronous Data Processing
Objective: Implement an asynchronous method that processes a list of integers. The processing should square each number and then return the sum of all squared numbers. Use Futures to perform the squaring operations in parallel.
Task:
- Create a method
squareNumberthat takes anIntand returns aFuture[Int]representing the square of the number. - Implement a method
sumOfSquaresthat accepts aList[Int]and returns aFuture[Int]with the sum of the squares of the list elements, computed asynchronously.
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def squareNumber(number: Int): Future[Int] = Future {
number * number
}
def sumOfSquares(numbers: List[Int]): Future[Int] = {
Future.sequence(numbers.map(squareNumber)).map(_.sum)
}
Exercise 2: Combining Futures with for-comprehension
Objective: Write a function that asynchronously fetches the current temperatures (mocked as random values) for two cities and then computes the average temperature. Each city's temperature should be fetched in parallel, and the averaging should be done once both temperatures are available.
Task:
- Implement two functions,
getTemperature(city: String): Future[Double], that simulate fetching temperature data for each city. - Write a function
averageTemperature(city1: String, city2: String): Future[Double]that uses for-comprehension to wait for both temperatures and then computes the average.
import scala.concurrent.Future
import scala.util.Random
import scala.concurrent.ExecutionContext.Implicits.global
def getTemperature(city: String): Future[Double] = Future {
Thread.sleep(Random.nextInt(500)) // Simulate network delay
20 + Random.nextInt(15) // Random temperature
}
def averageTemperature(city1: String, city2: String): Future[Double] = {
for {
temp1 <- getTemperature(city1)
temp2 <- getTemperature(city2)
} yield (temp1 + temp2) / 2
}
Exercise 3: Error Handling in Futures
Objective: Implement a function that tries to parse a list of strings to integers and computes their sum. The function should handle any parsing errors by treating unparsable strings as zeros.
Task:
- Write a method
parseToIntthat converts aStringto anIntand returns aFuture[Int]. If parsing fails, it should returnFuture.successful(0). - Implement a method
sumOfStrings(numbers: List[String]): Future[Int]that uses theparseToIntmethod to sum the list of strings treated as integers.
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.Try
def parseToInt(s: String): Future[Int] = Future {
Try(s.toInt).getOrElse(0)
}
def sumOfStrings(numbers: List[String]): Future[Int] = {
Future.sequence(numbers.map(parseToInt)).map(_.sum)
}
project
More Functions
Exercise 1
def sum(a: Int, b: Int) = a + b
Create a partial applied function for sum
Create also a function sum and a partial
Create a curried version of sum
Exercise 2
Try to write a fromCurry and toCurry yourself without looking at example above
Exercise 3
Create a partial function on Int's the works on odd numbers and returns a multiplication by 10
Exercise 4
Create a partial function on Int's the works on odd numbers and returns a multiplication by 10
Use it on a list List(1,2,3,4,5,6,7,8,9)
Use collect, filter and map
Call by Value, Name, Need
Exercise 1
- Make it a
call by name - Make it a
call by needby adding alazy 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)
- Make it a
call by value - Make it a
call by name - Make it a
call by needby adding alazy val
Given and Using
Exercise 1
val cities = List("London", "Paris", "Lisbon", "Berlin")
Create a list of cities:
- sort the list of cities in ascending order
- sort in list of cities in descending order
- Put the two ordering function in their own scope
- 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
- sort the capital cities in ascending order on the country name
- sort the capital cities in descending order in the city name
- Add the first ordering in the companion object and the second a separate object
- Choice which order you use with the
import
Exercise 2
val names = List("John", "Alice", "Jane", "Edward")
Create a list of names:
- sort the names in ascending order
- sort in names in descending order
- Put the two ordering function in their own scope
- Choice which order you use with the
import
Create a case class Person with a name and age Create a list of persons
- sort the persons in ascending order on the age
- sort the persons in descending order on the age
- Add the ascending ordering in the companion object and the descending a separate scope
- Choice which order you use with the
import
Given examples
In Scala 3 (formerly known as Dotty), the given and using clauses represent a sophisticated evolution of Scala's implicits feature, making it more explicit and easier to reason about. These features are part of Scala's type class support, allowing for more expressive and type-safe code. Let's dive into a few examples to understand how given and using work.
Example 1: Basic Type Class
A type class is a sort of interface that defines some behavior. Unlike traditional interfaces, type classes can be "attached" to classes after they've been defined. Let's define a simple JsonSerializer type class and then use given and using to serialize objects to JSON.
Define the Type Class
trait JsonSerializer[T] {
def serialize(value: T): String
}
Implement the Type Class
given JsonSerializer[String] with {
def serialize(value: String): String = s""""$value""""
}
given JsonSerializer[Int] with {
def serialize(value: Int): String = value.toString
}
Use the Type Class
def toJson[T](value: T)(using serializer: JsonSerializer[T]): String = {
serializer.serialize(value)
}
println(toJson("hello")) // Output: "hello"
println(toJson(123)) // Output: 123
Example 2: Contextual Abstractions with using Parameters
Sometimes, you might want to pass additional parameters contextually without cluttering the method signature for every call. This is where using shines.
Define a Context
case class RequestContext(userId: String)
given RequestContext = RequestContext("user123")
A Method that Requires Context
def getUser(using ctx: RequestContext): String = {
s"Fetching data for user: ${ctx.userId}"
}
println(getUser) // Output: Fetching data for user: user123
Example 3: Generic Programming with given
You can also use given instances for generic programming, such as defining a generic sum function for numeric types.
Numeric Type Class
trait Numeric[T] {
def plus(x: T, y: T): T
def zero: T
}
given Numeric[Int] with {
def plus(x: Int, y: Int): Int = x + y
def zero: Int = 0
}
Generic Sum Function
def sum[T](items: List[T])(using numeric: Numeric[T]): T = {
items.foldLeft(numeric.zero)(numeric.plus)
}
println(sum(List(1, 2, 3))) // Output: 6
Extension Methods
extension
extension(i: Int)
def print: Unit =
println(s"some: $i")
@main
def main(): Unit =
val x = 3
x.print
generics
extension[A](a: A)
def print: Unit =
println(s"some: $a")
@main
def main(): Unit =
val x = 3
x.print
val str = "hello"
str.print
case class
case class Todo(task: String, priority: Int)
object Todo:
extension(todo: Todo)
def print: Unit =
println(s"${todo.priority}. ${todo.task.capitalize}")
@main
def main(): Unit =
val todoList = List(Todo("walking", 1),Todo("swimming", 2) )
todoList.foreach(_.print)
// 1. Walking
// 2. Swimming
Scope
The scope rules of extension methods are the simular to the scope of given
imported extensioncurrent scopecompanion object
imported extension
object TodoExtension:
extension (todo: Todo)
def print: Unit =
println(s"${todo.priority}. ${todo.task.capitalize}")
@main
def main(): Unit =
val todoList = List(Todo("walking", 1),Todo("swimming", 2) )
import TodoExtension.*
todoList.foreach(_.print)
For extensions the wildcard * import is used.
This is different from to the given import.
Rule of thumb
The rules are similar to the given:
- Put the most frequently used extension in the companion object
- Put the other extensions in separate objects with a clear names
Exercises
Exercise 1
val cities = List("London", "Paris", "Lisbon", "Berlin")
Create a list of cities:
- In an object create an extension method
printto theStringclass. - In another object create a second extension method
printto the String that prints in ALL-CAPS - Choice which
printmethod you will use with animport
Create a case class CapitalCity with a city and a country
Create a list of capital cities
- Create an extension method
printto the CapitalCity companion object - Print the list of capital cities
Exercise 2
Create a case class Person with a name and age
Create a list of persons
- Create an extension method
printto the Person companion object. - In another object create an extension method
printto the Persion that prints in ALL-CAPS - Choice which
printmethod you will use with animport
Conversion
Exercises
Exercise 1
case class Person(name: String)
def greet(): String = s"Hello, $name"
- Write a conversion function from Person to Int that calculate the length of the name
- Put it in a separate
object - Use it with an
import
Exercise 2
case class User(name: String)
def login(): String = s"Logged in: $name"
- Write a conversion from Person to User
- Write a conversion from User to Person
- Test both
Type Classes
Type Classes Exercises
Exercise: Json Converter
Json Converter
Creating a JSON converter using type classes in Scala allows for flexible, reusable serialization logic that can be applied to various types without requiring modifications to those types. This approach is particularly useful when working with third-party classes or when you want to keep serialization logic decoupled from domain logic. Below is a simplified example demonstrating how to implement a JSON converter using type classes in Scala 3, utilizing the given and using syntax for clarity and explicitness.
Step 1: Define the JSON Type Class
First, define a trait that represents the ability to convert a value of type T to JSON.
trait JsonConverter[T] {
def toJson(value: T): String
}
Step 2: Create Instances of the Type Class
Next, provide given instances of the JsonConverter for the types you want to support. Let's start with a few basic types like String and Int, and then create a converter for a custom class.
given JsonConverter[String] with {
def toJson(value: String): String = s""""$value""""
}
given JsonConverter[Int] with {
def toJson(value: Int): String = value.toString
}
// A sample case class for demonstration
case class Person(name: String, age: Int)
// Creating a JsonConverter for the Person case class
given JsonConverter[Person] with {
def toJson(person: Person): String =
s"""{"name": "${person.name}", "age": ${person.age}}"""
}
Step 3: Implement a Generic toJSON Function
Now, define a generic function that uses the JsonConverter type class to convert any supported type to JSON. This function will use the using clause to specify that it requires a JsonConverter for the type T.
def toJson[T](value: T)(using converter: JsonConverter[T]): String = {
converter.toJson(value)
}
Step 4: Using the JSON Converter
Finally, you can use the toJson function to serialize different types to JSON. The compiler will automatically use the appropriate given instance based on the type of the value passed to toJson.
val name = "John Doe"
val age = 30
val person = Person(name, age)
println(toJson(name)) // Outputs: "John Doe"
println(toJson(age)) // Outputs: 30
println(toJson(person)) // Outputs: {"name": "John Doe", "age": 30}
Extensibility
One of the strengths of this approach is its extensibility. You can easily add support for new types by defining new given instances of the JsonConverter type class. This does not require modifying existing code, adhering to the open/closed principle.
Monads
The railway metaphor is a popular way to explain monads in a more intuitive and less abstract manner. It helps visualize the flow of data through transformations, especially in a language like Scala, where monads play a crucial role in handling computations, side effects, and more.
Imagine a railway system where trains (data) travel from one station (function) to the next. Each station transforms the train in some way, and the tracks guide where the train goes. In a perfect world, the train goes from start to finish without any issues. However, real life (and code) involves complications like missing tracks (exceptions) or stations that can't handle the train (errors).
The Tracks: Happy Path and Error Path
The railway has two parallel tracks: the happy path and the error path.
-
Happy Path: This is where everything goes right. The train moves from one station to the next, getting transformed along the way without any issues. In Scala, this is akin to operations on monads (like
Option,Try, orFuture) 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.Nonerepresents 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
aandbareSome(value), the train successfully travels through both stations and arrives at its destination with the sum ofxandy(Some(x+y)). - If either
aorbisNone, 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 isNone, bypassing any further computation.
The Monad Laws: Ensuring Reliable Railway Operations
Monads follow certain laws that ensure the reliability and predictability of the railway:
-
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.
-
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.
-
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:
flatMap(also known asbindin other languages): Allows chaining operations on monadic values.unit(often available as a constructor in Scala, such asSome,List(), orFuture.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): IfAis a subtype ofB, thenBox[A]is a subtype ofBox[B]. - Contravariance (
-T): IfAis a subtype ofB, thenBox[B]is a subtype ofBox[A]. - Invariance: By default, generic types in Scala are invariant. If
Ais a subtype ofB, there is no relationship betweenBox[A]andBox[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
Tto another type. -
Context Bounds (
[T: Ordering]): Useful for requiring an implicit value of a certain type, such as anOrdering[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
- Clarity: Type aliases can make complex type signatures clearer and easier to understand.
- 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.
- 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
- 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.
- 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.
- Encapsulation: Opaque types allow you to hide implementation details and expose only the operations and constructors that make sense for a given abstraction.
Defining Opaque Types
Opaque types are defined within an object, trait, or class and are only visible within their defining scope. Here's an example of how to define and use an opaque type:
object Lengths:
opaque type Meter = Double
opaque type Kilometer = Double
// Constructors
def Meter(value: Double): Meter = value
def Kilometer(value: Double): Kilometer = value
// Extension methods
extension (m: Meter)
def toKilometers: Kilometer = m / 1000
extension (km: Kilometer)
def toMeters: Meter = km * 1000
import Lengths._
val distanceInMeters: Meter = Meter(1500)
val distanceInKilometers: Kilometer = distanceInMeters.toKilometers
In this example, Meter and Kilometer are opaque types for Double. They are treated as distinct types outside of the Lengths object, thus providing type safety for operations dealing with lengths and distances. The extension methods allow you to define operations on these opaque types, making them more useful and expressive.
Comparing Opaque Types with Type Aliases
Scala already has type aliases, which let you give a new name to an existing type. However, type aliases are transparent, meaning the alias and the original type are interchangeable everywhere. Opaque types, on the other hand, provide a stronger separation between the alias and the underlying type, making them distinct outside their defining scope.
Usage Patterns
Opaque types are useful for a wide range of applications, including but not limited to:
- Wrapping primitive types for additional type safety without the overhead of case classes.
- Creating units of measure to prevent mixing up values with the same underlying type but different semantic meanings (like meters and kilometers).
- Encapsulating implementation details of data structures while exposing a minimal, safe API to the users.
Conclusion
Opaque types in Scala 3 offer a powerful mechanism for improving type safety and encapsulation in your Scala applications without sacrificing performance. They provide a means to distinguish between types that are structurally the same but semantically different, allowing for safer and more expressive code.
Exercises
MyList - step 1
MyList
We will create a MyList similar to the List in the standard libraries
We start with a trait
trait MyList[A]:
def isEmpty: Boolean
def head: A
def tail: MyList[A]
Exercise
Implement the trait in a Cons node and an Empty node And then create a MyList with them.
case class Empty[A]() extends MyList[A]
case class Cons[A]() extends MyList[A]
Solution
Empty
case class Empty[A]() extends MyList[A]:
override def isEmpty: Boolean = true
override def head: A = throw new NoSuchElementException()
override def tail: MyList[A] = throw new NoSuchElementException()
Cons
We put the head and tail in the constructor
case class Cons[A](override val head: A, override val tail: MyList[A]) extends MyList[A]:
override def isEmpty: Boolean = false
main
@main
def main(): Unit =
val myList: MyList[Int] = Cons(1, Cons(2, Cons(3, Empty())))
println(myList)
// Cons(1,Cons(2,Cons(3,Empty())))
MyList - step 2
ToString
Add the toString method to MyList
trait MyList[A]:
...
def toString: String
- Make a recursive version
- A Tail recursive version
- A pretty print version that prints
MyList(3,2,1)
Exercise
Implement the add method in Cons and Empty And test it in main
Solution
Empty
override def toString: String = ""
Recursive
We have to walk through the linked list get the head and then jump to the tail recursively. The case is the empty node.
override def toString: String =
def concat(remainder: MyList[A]): String =
if !remainder.isEmpty then
current.head.toString + " " + concat(remainder.tail)
else
""
concat(this)
Tail Recursive
In the tail recursive version we add a accumulator to the parameters. In every iteration we add the head to the accumulator And the last step in returning the accumulator
override def toString: String =
def concat(remainder: MyList[A], accumulator: String): String =
if !remainder.isEmpty then
concat(remainder.tail, accumulator + " " + remainder.head)
else
s"MyList($accumulator)"
concat(this, "")
Pretty print
To make in pretty print version we add a comma in every iteration. But then we get as many comma's as there are elements and that is one comma too much.
Iterating one less is starting the recursive loop with the tail
And the accumulator start with the head
override def toString: String =
def concat(remainder: MyList[A], accumulator: String): String =
if !remainder.isEmpty then
concat(remainder.tail, accumulator + ", " + remainder.head)
else
accumulator
val elements = concat(tail, head.toString)
s"MyList($elements)"
main
@main
def main(): Unit =
val myList: MyList[Int] = Empty() + 1 + 2 + 3
println(myList)
// 3 2 1
// MyList(3, 2, 1)
The order is reversed now.
MyList - step 3
Add method
- Add the
addmethod - Add the
+method - Create a companion object
Exercise 1
trait MyList[A]:
...
def add(element: A): MyList[A]
- Give the MyList trait an
addmethod - Implement it in Cons and Empty
- And test it in main with:
Empty().add(1).add(2).add(3)
Exercise 2
trait MyList[A]:
...
def +(element: A): MyList[A]
- Implement + operator as alias for add method
- And test it in main with:
Empty() + 1 + 2 + 3
Exercise 3
object MyList:
def apply[A](elements: A*): MyList[A]
- Implement the companion object
- And test it in main with:
MyList(1,2,3)
Add method
Empty
override def add(element: A): MyList[A] = Cons(element, this)
Adding an element to Empty means that Empty is not empty anymore and becomes a Cons with the tail Empty (this)
Cons
override def add(element: A): MyList[A] = Cons(element, this)
Adding an element to Cons means adding new head and the tail becomes the current Cons (this)
trait
trait MyList[A]:
...
def add(element: A): MyList[A] = Cons(element, this)
The implementation in Empty and Cons are the same.
So we can move it up to the MyList trait (and remove them from Empty and Cons).
Via inheritance, they are available in Empty and Cons again
main
@main
def main(): Unit =
val myList: MyList[Int] = Empty().add(1).add(2).add(3)
println(myList)
// MyList(3, 2, 1)
The order is reversed now.
Solution 2
trait MyList[A]:
...
def add(element: A): MyList[A] = Cons(element, this)
infix def + (element: A): MyList[A] = add(element)
In Scala + is a valid function name
With the infix modifier we do not have to use the braces in the function call.
@main
def main(): Unit =
val myList: MyList[Int] = Empty() + 1 + 2 + 3
println(myList)
// MyList(3, 2, 1)
Solution 3
companion object
object MyList:
def apply[A](elements: A*): MyList[A] =
def build(elements: Seq[A], acc: MyList[A]): MyList[A] =
if elements.isEmpty then acc
else build(elements.tail, acc + elements.head)
build(elements.reverse, Empty())
In the companion object we add the apply method with a varargs param list.
In a tail recursive build function we add the elements to MyList with our own infix +
Because the recursive call will build the MyList in reversed order, we start with reversing the elements
main
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1,2,3)
println(myList)
// MyList(1, 2, 3)
MyList - step 4
Foreach, Map and Filter
Exercise
Add the following methods to MyList
- foreach
- map
- filter
trait MyList[A]:
...
def foreach(f: A => B): Unit
def map[B](f: A => B): MyList[B]
def filter(f: A => Boolean): MyList[A]
Solution Foreach
Empty
override def foreach(f: A => Unit): Unit = ()
Should return Unit. The implementation of Unit is ()
Cons
override def foreach(f: A => Unit): Unit =
f(head)
tail.foreach(f)
We have to walk through the linked list get the call the function on the head
and then jump to the tail recursively.
Solution Map
Empty
override def map[B](f: A => B): MyList[B] = Empty[B]()
Transforming an empty list of type A gives us an empty list of type B.
This feels a weird. That because of our definition Empty
If we had it defined as:
case object Empty extends MyList[Nothing]
But then we have implement a covariant/contravariant version op the type A. That is something for the advanced course.
You could leave out the type on the Cons because the compiler know the type from the return type MyList[B]
Empty()
Cons
override def map[B](f: A => B): MyList[B] =
Cons[B](f(head), tail.map(f))
We have to walk through the linked list of type A
call the function on the head
and walk the tail recursively.
And wrap inside a new Cons of type B
You could leave out the type on the Cons because the compiler know the type from the return type MyList[B]
Cons(f(head), tail.map(f))
Solution Filter
Empty
override def filter[B](f: A => Boolean): MyList[A] = this
Filtering an empty list gives an empty list.
Cons
override def filter[B](f: A => Boolean): MyList[A] =
if !f(head) then
tail.filter(f)
else
Cons(head, tail.filter(f))
We check if the predicate f on the head is false then we go further on filtering the tail
If the predicate f is true then the head is added to new Cons, and then we go further on the filtering the tail
main
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1,2,3)
myList.foreach(x => println(x + 2))
println(myList.map(x => x * 2))
println(myList.filter(x => x < 2))
// 3
// 4
// 5
// MyList(2, 4, 6)
// MyList(1)
MyList - step 5
Concat and Flatmap
We will add two new methods to MyList
- ++ (concatenation)
- flatMap
Exercise
trait MyList[A]:
...
infix def ++(other: MyList[A]): MyList[A]
def flatMap[B](f: A => MyList[B]): MyList[B]
Solution Concatenation
Empty
override infix def ++(other: MyList[A]): MyList[A] = other
Another list added to an empty list gives the other list
Cons
override infix def ++(other: MyList[A]): MyList[A] =
tail ++ other + head
We could also write this as
Cons(head, tail ++ other)
A recursion example flow
[1,2,3] ++ [4,5,6]
Cons(1, [2,3] ++ [4,5,6])
Cons(1, Cons(2, [3] ++ [4,5,6]))
Cons(1, Cons(2, Cons(3, [] ++ [4,5,6])))
Cons(1, Cons(2, Cons(3, [4,5,6])))
[1,2,3,4,5,6]
Solution FlatMap
Empty
override def flatMap[B](f: A => B): MyList[B] = Empty[B]()
Like the map method flatMap returns an Empty list
Cons
override def flatMap[B](f: A => MyList[B]): MyList[B] =
f(head) ++ tail.flatMap(f)
With the implementation of concatenation (++) flatMap becomes simple
The function call f(head) returns a MyList
which is concatenated with the recursive call of flatMap on the tail
A recursion example flow
[1,2,3].flatMap(a => [a, a + 1])
[1,2] ++ [2,3].flatMap(f)
[1,2] ++ [2,3] ++ [3].flatMap(f)
[1,2] ++ [2,3] ++ [3,4] ++ [].flatMap(f)
[1,2] ++ [2,3] ++ [3,4] ++ []
[1,2,2,3,3,4]
main
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1,2,3)
val otherList = MyList(4, 5)
println( myList ++ otherList )
println( myList.flatMap(a => MyList(a, a + 1)) )
// MyList(1, 2, 3, 4, 5)
// MyList(1, 2, 2, 3, 3, 4)
MyList - step 6
For Comprehension
The MyList has a map and flatMap function
So we can use in a for comprehension
Exercise
@main
def main(): Unit =
val result = for
a <- MyList(1, 2, 3)
b <- MyList(a, a + 1)
yield
b
println(result)
This is the same as the example in the for
@main
def main(): Unit =
val result = for
a <- MyList(1, 2, 3)
b <- MyList(a, a + 1)
yield
b
println(result)
WithFilter
def withFilter[A](f: A => MyList[B]): MyList[B]
In the scala for comprehension we can filter elements.
This is not the filter method but the withFilter method.
In the standard libraries the withFilter is lazy, but that is for the advanced course
So for now we use our filter method as implementation
def withFilter[A](f: A => MyList[B]): MyList[B] = filter(f)
@main
def main(): Unit =
val result = for
a <- MyList(1, 2, 3, 4, 5) if a < 3
yield
a
println(result)
MyList - step 7
The lazy list can handle an infinite list.
In the end we take some elements that are calculated
Lazy List Exercises
Exercise 1
We will use MyList as a reference. Refactor the MyList name to a LzList (shift-F6) Nearly all the methods are the same.
Cons
class Cons[A](hd: => A, tl: => LazyList[A]) extends LazyList[A] {
def isEmpty: Boolean = false
override lazy val head: A = hd
override lazy val tail: LazyList[A] = tl
Here we do a call by need on the head and the tail And we need a class because call by name are not allow on case classes.
And new Cons is used in instantiate a Cons
Run the LzList to see if everything still works
Exercise 2
To use the laziness of the list we will create an inifite list and take method
- take
- infinite list
def take(n :Int): LzList[A]
Implement those in Empty and Cons
def generate(start: Int)(next: Int => Int): LzList[Int] = ???
In the companion object a generate method is added of type Int
Solution 2
Empty
override def take(n: Int): LzList[A] = this
Cons
override def take(n: Int): LzList[A] =
def loop(remainder: LzList[A], count: Int): LzList[A] =
if count == 0 then Empty()
else new Cons(remainder.head, loop (remainder.tail, count - 1))
loop(this, n)
object LzList
def generate(start: Int)(next: Int => Int): LzList[Int] =
new Cons[Int](start, generate(next(start))(next))
main
val genList: LzList[Int] = LzList.generate(1)( _ + 1)
val genMap = genList.map(_ * 100)
println(genMap.take(10))
println(genMap.take(100))
println(genMap.take(100000))
Library
case class Book(title: String, author: String, year: Int, category: String)
object LibraryManagement extends App {
var library = List(
Book("1984", "George Orwell", 1949, "Dystopian"),
Book("To Kill a Mockingbird", "Harper Lee", 1960, "Fiction"),
Book("The Great Gatsby", "F. Scott Fitzgerald", 1925, "Classic"),
Book("Brave New World", "Aldous Huxley", 1932, "Dystopian"),
Book("Moby Dick", "Herman Melville", 1851, "Classic"),
Book("The Catcher in the Rye", "J.D. Salinger", 1951, "Fiction")
)
def addBook(library: List[Book], book: Book): List[Book] = {
book :: library
}
def searchBooks(library: List[Book], query: String): List[Book] = {
library.filter(book => book.title.contains(query) || book.author.contains(query))
}
def filterByCategory(library: List[Book], category: String): List[Book] = {
library.filter(_.category == category)
}
def totalBooks(library: List[Book]): Int = {
library.length
}
def averagePublicationYear(library: List[Book]): Double = {
if (library.isEmpty) 0.0
else library.map(_.year).sum.toDouble / library.length
}
println("Initial Library:")
library.foreach(println)
// Add a new book
val newBook = Book("Sapiens", "Yuval Noah Harari", 2011, "Non-Fiction")
library = addBook(library, newBook)
println("\nLibrary after adding a new book:")
library.foreach(println)
// Search for books
val searchQuery = "George Orwell"
val searchResults = searchBooks(library, searchQuery)
println(s"\nSearch results for '$searchQuery':")
searchResults.foreach(println)
// Filter by category
val category = "Dystopian"
val dystopianBooks = filterByCategory(library, category)
println(s"\nBooks in the '$category' category:")
dystopianBooks.foreach(println)
// Calculate statistics
val total = totalBooks(library)
val averageYear = averagePublicationYear(library)
println(s"\nTotal number of books: $total")
println(f"Average publication year: $averageYear%.2f")
}
Enhanced Library
case class Book(title: String, author: String, year: Int, category: String)
object EnhancedLibraryManagement extends App {
var library = List(
Book("1984", "George Orwell", 1949, "Dystopian"),
Book("To Kill a Mockingbird", "Harper Lee", 1960, "Fiction"),
Book("The Great Gatsby", "F. Scott Fitzgerald", 1925, "Classic"),
Book("Brave New World", "Aldous Huxley", 1932, "Dystopian"),
Book("Moby Dick", "Herman Melville", 1851, "Classic"),
Book("The Catcher in the Rye", "J.D. Salinger", 1951, "Fiction")
)
def addBook(library: List[Book], book: Book): List[Book] = {
book :: library
}
def searchBooks(library: List[Book], query: String): List[Book] = {
library.filter(book => book.title.contains(query) || book.author.contains(query))
}
def filterByCategory(library: List[Book], category: String): List[Book] = {
library.filter(_.category == category)
}
def totalBooks(library: List[Book]): Int = {
library.length
}
def averagePublicationYear(library: List[Book]): Double = {
if (library.isEmpty) 0.0
else library.map(_.year).sum.toDouble / library.length
}
def sortBooksByTitle(library: List[Book]): List[Book] = {
library.sortBy(_.title)
}
def sortBooksByYear(library: List[Book]): List[Book] = {
library.sortBy(_.year)
}
def groupBooksByCategory(library: List[Book]): Map[String, List[Book]] = {
library.groupBy(_.category)
}
def partitionBooksByYear(library: List[Book], year: Int): (List[Book], List[Book]) = {
library.partition(_.year < year)
}
def collectTitlesAfterYear(library: List[Book], year: Int): List[String] = {
library.collect {
case Book(title, _, y, _) if y > year => title
}
}
}
Library with Shopping Basket
Step-by-Step Enhancements
case class Book(title: String, author: String, year: Int, category: String, price: Double)
class ShoppingBasket {
private var items: List[Book] = List()
def addBook(book: Book): Unit = {
items = book :: items
}
def totalCost: Double = {
items.map(_.price).sum
}
def showBasket(): Unit = {
println("Shopping Basket:")
items.foreach(book => println(s"${book.title} - ${book.price}"))
}
}
object EnhancedLibraryManagement extends App {
var library = List(
Book("1984", "George Orwell", 1949, "Dystopian", 15.99),
Book("To Kill a Mockingbird", "Harper Lee", 1960, "Fiction", 10.99),
Book("The Great Gatsby", "F. Scott Fitzgerald", 1925, "Classic", 8.99),
Book("Brave New World", "Aldous Huxley", 1932, "Dystopian", 12.99),
Book("Moby Dick", "Herman Melville", 1851, "Classic", 9.99),
Book("The Catcher in the Rye", "J.D. Salinger", 1951, "Fiction", 14.99)
)
def addBook(library: List[Book], book: Book): List[Book] = {
book :: library
}
def searchBooks(library: List[Book], query: String): List[Book] = {
library.filter(book => book.title.contains(query) || book.author.contains(query))
}
def filterByCategory(library: List[Book], category: String): List[Book] = {
library.filter(_.category == category)
}
def totalBooks(library: List[Book]): Int = {
library.length
}
def averagePublicationYear(library: List[Book]): Double = {
if (library.isEmpty) 0.0
else library.map(_.year).sum.toDouble / library.length
}
def sortBooksByTitle(library: List[Book]): List[Book] = {
library.sortBy(_.title)
}
def sortBooksByYear(library: List[Book]): List[Book] = {
library.sortBy(_.year)
}
def groupBooksByCategory(library: List[Book]): Map[String, List[Book]] = {
library.groupBy(_.category)
}
def partitionBooksByYear(library: List[Book], year: Int): (List[Book], List[Book]) = {
library.partition(_.year < year)
}
def collectTitlesAfterYear(library: List[Book], year: Int): List[String] = {
library.collect {
case Book(title, _, y, _, _) if y > year => title
}
}
def averagePublicationYearByCategory(library: List[Book]): Map[String, Double] = {
val groupedByCategory = library.groupBy(_.category)
groupedByCategory.mapValues(books => books.map(_.year).sum.toDouble / books.length)
}
println("Initial Library:")
library.foreach(println)
// Add a new book
val newBook = Book("Sapiens", "Yuval Noah Harari", 2011, "Non-Fiction", 19.99)
library = addBook(library, newBook)
println("\nLibrary after adding a new book:")
library.foreach(println)
// Search for books
val searchQuery = "George Orwell"
val searchResults = searchBooks(library, searchQuery)
println(s"\nSearch results for '$searchQuery':")
searchResults.foreach(println)
// Filter by category
val category = "Dystopian"
val dystopianBooks = filterByCategory(library, category)
println(s"\nBooks in the '$category' category:")
dystopianBooks.foreach(println)
// Calculate statistics
val total = totalBooks(library)
val averageYear = averagePublicationYear(library)
println(s"\nTotal number of books: $total")
println(f"Average publication year: $averageYear%.2f")
// Sort books by title
val sortedByTitle = sortBooksByTitle(library)
println("\nBooks sorted by title:")
sortedByTitle.foreach(println)
// Sort books by year
val sortedByYear = sortBooksByYear(library)
println("\nBooks sorted by year:")
sortedByYear.foreach(println)
// Group books by category
val groupedByCategory = groupBooksByCategory(library)
println("\nBooks grouped by category:")
groupedByCategory.foreach { case (category, books) =>
println(s"$category:")
books.foreach(println)
}
// Partition books by year
val (before2000, after2000) = partitionBooksByYear(library, 2000)
println("\nBooks published before 2000:")
before2000.foreach(println)
println("\nBooks published after 2000:")
after2000.foreach(println)
// Collect titles of books published after 1950
val titlesAfter1950 = collectTitlesAfterYear(library, 1950)
println("\nTitles of books published after 1950:")
titlesAfter1950.foreach(println)
// Calculate average publication year by category
val avgYearByCategory = averagePublicationYearByCategory(library)
println("\nAverage publication year by category:")
avgYearByCategory.foreach { case (category, avgYear) =>
println(s"$category: $avgYear%.2f")
}
// Shopping Basket functionality
val basket = new ShoppingBasket()
basket.addBook(library.head)
basket.addBook(library(1))
basket.addBook(library(2))
basket.showBasket()
println(s"Total cost: ${basket.totalCost}")
}
Library with Author
case class Author(name: String, nationality: String)
case class Book(title: String, author: Author, year: Int, category: String, price: Double)
class ShoppingBasket {
private var items: List[Book] = List()
def addBook(book: Book): Unit = {
items = book :: items
}
def totalCost: Double = {
items.map(_.price).sum
}
def showBasket(): Unit = {
println("Shopping Basket:")
items.foreach(book => println(s"${book.title} by ${book.author.name} - ${book.price}"))
}
}
object EnhancedLibraryManagement extends App {
val authors = List(
Author("George Orwell", "British"),
Author("Harper Lee", "American"),
Author("F. Scott Fitzgerald", "American"),
Author("Aldous Huxley", "British"),
Author("Herman Melville", "American"),
Author("J.D. Salinger", "American"),
Author("Yuval Noah Harari", "Israeli")
)
var library = List(
Book("1984", authors(0), 1949, "Dystopian", 15.99),
Book("To Kill a Mockingbird", authors(1), 1960, "Fiction", 10.99),
Book("The Great Gatsby", authors(2), 1925, "Classic", 8.99),
Book("Brave New World", authors(3), 1932, "Dystopian", 12.99),
Book("Moby Dick", authors(4), 1851, "Classic", 9.99),
Book("The Catcher in the Rye", authors(5), 1951, "Fiction", 14.99)
)
def addBook(library: List[Book], book: Book): List[Book] = {
book :: library
}
def searchBooks(library: List[Book], query: String): List[Book] = {
library.filter(book => book.title.contains(query) || book.author.name.contains(query))
}
def filterByCategory(library: List[Book], category: String): List[Book] = {
library.filter(_.category == category)
}
def totalBooks(library: List[Book]): Int = {
library.length
}
def averagePublicationYear(library: List[Book]): Double = {
if (library.isEmpty) 0.0
else library.map(_.year).sum.toDouble / library.length
}
def sortBooksByTitle(library: List[Book]): List[Book] = {
library.sortBy(_.title)
}
def sortBooksByYear(library: List[Book]): List[Book] = {
library.sortBy(_.year)
}
def groupBooksByCategory(library: List[Book]): Map[String, List[Book]] = {
library.groupBy(_.category)
}
def partitionBooksByYear(library: List[Book], year: Int): (List[Book], List[Book]) = {
library.partition(_.year < year)
}
def collectTitlesAfterYear(library: List[Book], year: Int): List[String] = {
library.collect {
case Book(title, _, y, _, _) if y > year => title
}
}
def averagePublicationYearByCategory(library: List[Book]): Map[String, Double] = {
val groupedByCategory = library.groupBy(_.category)
groupedByCategory.mapValues(books => books.map(_.year).sum.toDouble / books.length)
}
println("Initial Library:")
library.foreach(println)
// Add a new book
val newBook = Book("Sapiens", authors(6), 2011, "Non-Fiction", 19.99)
library = addBook(library, newBook)
println("\nLibrary after adding a new book:")
library.foreach(println)
// Search for books
val searchQuery = "George Orwell"
val searchResults = searchBooks(library, searchQuery)
println(s"\nSearch results for '$searchQuery':")
searchResults.foreach(println)
// Filter by category
val category = "Dystopian"
val dystopianBooks = filterByCategory(library, category)
println(s"\nBooks in the '$category' category:")
dystopianBooks.foreach(println)
// Calculate statistics
val total = totalBooks(library)
val averageYear = averagePublicationYear(library)
println(s"\nTotal number of books: $total")
println(f"Average publication year: $averageYear%.2f")
// Sort books by title
val sortedByTitle = sortBooksByTitle(library)
println("\nBooks sorted by title:")
sortedByTitle.foreach(println)
// Sort books by year
val sortedByYear = sortBooksByYear(library)
println("\nBooks sorted by year:")
sortedByYear.foreach(println)
// Group books by category
val groupedByCategory = groupBooksByCategory(library)
println("\nBooks grouped by category:")
groupedByCategory.foreach { case (category, books) =>
println(s"$category:")
books.foreach(println)
}
// Partition books by year
val (before2000, after2000) = partitionBooksByYear(library, 2000)
println("\nBooks published before 2000:")
before2000.foreach(println)
println("\nBooks published after 2000:")
after2000.foreach(println)
// Collect titles of books published after 1950
val titlesAfter1950 = collectTitlesAfterYear(library, 1950)
println("\nTitles of books published after 1950:")
titlesAfter1950.foreach(println)
// Calculate average publication year by category
val avgYearByCategory = averagePublicationYearByCategory(library)
println("\nAverage publication year by category:")
avgYearByCategory.foreach { case (category, avgYear) =>
println(s"$category: $avgYear%.2f")
}
// Shopping Basket functionality
val basket = new ShoppingBasket()
basket.addBook(library.head)
basket.addBook(library(1))
basket.addBook(library(2))
basket.showBasket()
println(s"Total cost: ${basket.totalCost}")
}