Scala Workbook
Workbook to the Scala 3 book.
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?
- 1n
- ~n
- this_is_a_number
- n1?
- A1
- match
- function()
Exercise
- Create a val called pi and assign it the value of 3.14159:
- Create a val called message and assign it a string value:
- Create a val called age and assign it an integer value:
- Create a val called isTall and assign it a boolean value:
- Create a val called name and assign it a string value, then print a message that includes the value of name:
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
Write a program that takes an integer input and prints whether it is even or odd.
Exercise 2: Larger
Write a program that takes two integers as input and prints the larger of the two.
Exercise 3: Maximum
Write a program that takes three integers as input and prints the largest of the three.
Exercise 4: Vowel or Consonant
Write a program that takes a character input and prints whether it is a vowel or a consonant.
In this example, we use the match expression to match the input character against a set of patterns to check if it is a vowel or consonant We use the match to return a 'Vowel' or a 'Consonant'.
Exercise 5: Weekend
Write a program that takes a day of the week as input and prints whether it is a weekday or a weekend day.
In this example, we use the match expression to match the input day against multiple patterns. If the input is "Saturday" or "Sunday", the code in the first block will execute and return "Weekend". Otherwise, the code in the last block will execute and return "Weekday".
Exercise 6: Number name
Write a program that takes an integer input and prints its English name. For example, if the input is 2, the program should print "two".
Solutions
Exercise 1: Even Odd
val x: Int = 10
val result =
if x % 2 == 0 then "Even"
else "Odd"
println(result)
Exercise 2: Larger
val x: Int = 10
val y: Int = 20
val larger: Int = if x > y then x else y
println(s"The larger number is $larger.")
Exercise 3: Maximum
val x: Int = 10
val y: Int = 20
val z: Int = 15
val largest: Int = if x > y && x > z then
x
else if y > z then
y
else
z
println(s"The largest number is $largest.")
Exercise 4: Vowel or Consonant
val ch: Char = 'a'
val result: String = ch match
case 'a' | 'e' | 'i' | 'o' | 'u' => "Vowel"
case _ => "Consonant"
println(result)
Exercise 5: Weekend
val day: String = "Saturday"
val result: String = day match
case "Saturday" | "Sunday" => "Weekend"
case _ => "Weekday"
println(result)
Exercise 6: Number name
val x: Int = 4
val result: String = x match
case 1 => "one"
case 2 => "two"
case 3 => "three"
case 4 => "four"
case 5 => "five"
case 6 => "six"
case 7 => "seven"
case 8 => "eight"
case 9 => "nine"
case _ => "invalid"
println(result)
Loops
Exercise 1 integers
Create a list of integers from 1 to 10, and use a for loop with yield to create a new list containing the squares of each integer.
Exercise 2 string lengths
Create a list of strings, and use a for loop with yield to create a new list containing the lengths of each string.
Exercise 3 evens
Create a list of integers, and use a for loop with yield to create a new list containing only the even integers.
Exercise 4 tuples
Create a list of tuples containing a name and an age, and use a for loop with yield to create a new list containing only the names of people who are under 30 years old.
Exercise 5 string contains
Create a list of strings, and use a for loop with yield to create a new list containing only the strings that contain the letter "a".
String
Exercise 1 length
- 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
Write a function that takes a list of strings and returns a new list with only the strings that contain the letter 'a'.
Exercise 2 even numbers
Write a function that takes a list of integers and returns a new list with only the even numbers.
Exercise 3 string lengths
Write a function that takes a list of strings and returns a new list with the length of each string.
Exercise 4: Default Parameters
Task: Define a function greet that takes two parameters: a name of type String and a greeting of type String with a default value of "Hello". The function should print a greeting message.
Exercise 5: Named Arguments
Task: Define a function describePerson that takes three parameters: name (String), age (Int), and country (String) with a default value of "unknown". Use named arguments to call this function, specifying age and name but not country.
Exercise 6: Varargs
Task: Write a function sum that accepts an arbitrary number of integer arguments and returns their sum.
Exercise 7: Anonymous Functions and Map
Task: Given a list of integers, use an anonymous function to increment each element by 1, using the map method.
Solutions
Exercise 4: Default Parameters
def greet(name: String, greeting: String = "Hello"): Unit =
println(s"$greeting, $name!")
greet("Scala") // Should print "Hello, Scala!"
greet("World", "Hi") // Should print "Hi, World!"
Exercise 5: Named Arguments
def describePerson(name: String, age: Int, country: String = "unknown"): Unit =
println(s"$name is $age years old from $country.")
describePerson(age = 25, name = "Alice") // Should print "Alice is 25 years old from unknown."
Exercise 6: Varargs
def sum(numbers: Int*): Int = numbers.sum
println(sum(1, 2, 3, 4)) // Should print 10
println(sum()) // Should print 0
Exercise 7: Anonymous Functions and Map
Task: Given a list of integers, use an anonymous function to increment each element by 1, using the map method.
val numbers = List(1, 2, 3, 4)
val incrementedNumbers = numbers.map(n => n + 1)
println(incrementedNumbers) // Should print List(2, 3, 4, 5)
Tuples
Exercise 1: Creating and Accessing Tuples
Task: Create a tuple representing a book. The tuple should contain three elements: the title of the book as a String, the year of publication as an Int, and a Boolean indicating whether you have read it. Then, write code to print each element of the tuple separately.
Exercise 2: Tuple Destructuring
Task: Given a tuple representing a person (firstName, lastName, age), destructure it into separate variables and print a formatted string using these variables.
Exercise 3: Using Tuples in Functions
Task: Write a function that takes two numbers as parameters, returns a tuple containing the sum and product of the two numbers.
Spring Boot Tutorial
1. Create a Spring Project
Goto start.spring.io and choose:
- maven
- add dependencies:
-
- web
-
- data-jpa
-
- h2
-
2. Project Structure
Your project structure will look similar to a typical Java project but will include Scala source files:
scala-java-spring
|-- src
|-- main
|-- scala
|-- controllers
|-- TodoController.scala
|-- model
|-- Todo.scala
|-- repository
|-- TodoRepository.scala
|-- resources
|-- application.properties
|-- test
|-- scala
|-- Your Scala test files here
|-- pom.xml
3. Update the pom.xml
You need to add dependencies and plugins to your pom.xml for Scala, Spring Boot, and to integrate Scala with Maven. Below is a simplified example of what the pom.xml might include:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala3-library_3</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>4.8.1</version>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4. Write Your Scala Application
Inside the src/main/scala directory, create your Scala application. For example, a simple Spring Boot application in Scala might look like this:
package com.example.demo
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
class DemoApplication
object DemoApplication extends App:
SpringApplication.run(classOf[DemoApplication])
5. Todo class
package com.example.demo.model
import jakarta.persistence.{Entity, GeneratedValue, GenerationType, Id}
import scala.annotation.meta.companionObject
import scala.beans.BeanProperty
@Entity
class Todo:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@BeanProperty
var id: Long = _
@BeanProperty
var task: String = _
6. Todo Repository
package com.example.demo.repository
import com.example.demo.model.Todo
import org.springframework.data.jpa.repository.JpaRepository
trait TodoRepository extends JpaRepository[Todo, Long]
7. Todo Controller
package com.example.demo.controllers
import com.example.demo.model.Todo
import com.example.demo.repository.TodoRepository
import org.springframework.web.bind.annotation.{GetMapping, PostMapping, RequestBody, RequestMapping, RestController}
import scala.jdk.CollectionConverters.*
@RequestMapping(path= Array("/todo"))
@RestController
class TodoController(todoRepository: TodoRepository):
@PostMapping
def save(@RequestBody todo : Todo): Todo =
todoRepository.save(todo)
import java.util
@GetMapping
def findAll(): util.List[Todo] =
todoRepository.findAll()
8. Running Your Application
You can run your Scala-Spring Boot application using Maven commands such as mvn spring-boot:run or by running the main method directly from your IDE.
9. Integratie with Java
|-- src
|-- main
|-- java
|-- OtherController.java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OtherController {
@GetMapping("/hello")
String sayHello() {
return "hello";
}
}
And it just works!
So you can integrate Java and Scala easily.
For example by writing your tests in Scala.
10. RestClient
Add a companion object to the Todo class
So we have a decent constructor
@companionObject
object Todo:
def apply(task: String): Todo =
val todo = new Todo()
todo.task = task
todo
package com.example.demo
import com.example.demo.model.Todo
import org.springframework.http.MediaType
import org.springframework.web.client.RestClient
@main
def main(): Unit =
val todo = Todo("driving") // call the companion object
val uriBase = "http://localhost:8080"
val response: Todo =
RestClient.create
.post
.uri(uriBase + "/todo")
.contentType(MediaType.APPLICATION_JSON)
.body(todo)
.retrieve
.body(classOf[Todo])
println(response.task)
You could put it in: src --> test --> scala --> TodoRestClient.scala
Exercises
@main
def read(): Unit =
val scanner = new Scanner(System.in)
print("read: ")
val s = scanner.nextLine
println("write: " + s)
Exercise 1 Begin
Create new project.
Create a @main function
Add the code and run
Exercise 2 Welcome
Ask for a user name
Print a welcome message with the name: Hello, [name]
Exercise 3 Add
Ask the user for two numbers n1 and n2
Add and print the numbers
Hint: use readInt()
Next divide the numbers What do you see. Is this OK?
Read a Double Hint use toDouble()
Exercise 4 Menu
Create een choice menu with the next layout
Menu Choose
(1) - print hello
(2) - print bye
Your choice: 1
hello
Exercise 5 Calculator
Create a calculator that can add, subtract, multiply and divide
Use the next layout
Calculator
Give the first number: 3
Give the second number: 4
Give the operator: *
3 * 4 = 12
Exercise 6 Drawing
We will use nested for-loops to draw shapes.
for i <- 0 to 3 do
for j <- 0 to 3 do
print("* ")
println()
println()
An example of a rectangle
Draw the next shapes
*
* *
* * *
* * * *
* * * *
* * *
* *
*
* * * *
* * *
* *
*
*
*
*
*
Exercise 7 List
Create list in the range from 0 to 10
Print de list with a for-loop
var list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Reverse and print the list
Add all the values
Calculate the average value
Exercise 8 String
Ask the user to give a text string
Calculate the length of the text.
Split de text in words Hint use: split
How many words are in the tekst.
Exercise 9 Function
Write a function that add two numbers.
Write function log(text: String) print the date and the text.
Projects
Create a new scala project in IntelliJ
Add scala-swing to sbt
// build.sbt
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "3.3.3"
lazy val root = (project in file("."))
.settings(
name := "untitled"
)
libraryDependencies += "org.scala-lang.modules" %% "scala-swing" % "3.0.0"
Project 1: Celcius Fahrenheit Converter
import scala.swing._
import scala.swing.event.ButtonClicked
object CelsiusFahrenheitConverter extends SimpleSwingApplication {
def top = new MainFrame {
title = "Celsius to Fahrenheit Converter"
// Components
val textField = new TextField { columns = 10 }
val convertButton = new Button { text = "Convert" }
val resultLabel = new Label { text = "Result: " }
// Layout
contents = new FlowPanel {
contents += new Label("Celsius: ")
contents += textField
contents += convertButton
contents += resultLabel
}
// Logic for conversion
listenTo(convertButton)
reactions += {
case ButtonClicked(_) =>
val celsius = textField.text.toDouble
val fahrenheit = celsius * 9/5 + 32
resultLabel.text = f"Result: $fahrenheit%.2f Fahrenheit"
}
size = new Dimension(300, 100)
centerOnScreen()
}
}
Project 2a: Number guess
Write a number guess game on the command line
- generate a random
- ask the user to guess the number
- tell the user if the number is smaller, larger or equal.
Project 2b: Number guess
Write a number guess game as a swing application
Project 3a: Todo List
Create a todo list on the command line
Project 3b: Todo List
Create a todo list as a swing application
Project 4: TicTacToe
Do this one after the Project: Calculator
Project: Calculator
Step 1 Project
Create a new scala project in IntelliJ
Add scala-swing to build.sbt
libraryDependencies += "org.scala-lang.modules" %% "scala-swing" % "3.0.0"
It will only work with Scala 2.13. So we'll have to use the Scala 2 syntax (e.g. braces, new) The compiler is backwards compatible, so it will work with Scala 3.
Step 2: MainFrame
import scala.swing.*
@main
def calculator(): Unit = {
new MainFrame() {
title = "Hello world"
size = new Dimension(300, 300)
centerOnScreen()
open()
}
}
Step 3: Add Components
val label = new Label("hello, world")
val button = new Button("OK")
contents = new BorderPanel {
add(label, BorderPanel.Position.North)
add(button, BorderPanel.Position.South)
}
Step 4: Event Handling
button.reactions += {
case ButtonClicked(e) => click(e)
}
def click(but: AbstractButton): Unit = {
println(but.text)
}
Step 5: More Buttons
val labels = List(
"7", "8", "9", "/",
"4", "5", "6", "*",
"1", "2", "3", "+",
"C", "0", "=", "-")
val buttonGrid = new GridPanel(4, 4)
val buttons = labels.foreach(lbl => {
val btn = new Button(lbl)
btn.reactions += {
case ButtonClicked(e) => click(e)
}
buttonGrid.contents += btn
}
add(buttonGrid, BorderPanel.Position.Center)
Step 6: State Machine
but.text match
case "7"|"8"|"9"|"4"|"5"|"6"|"1"|"2"|"3"|"0" =>
println("number")
label.text += but.text
case "*"|"/"|"+"|"-" =>
println("operator")
operator = but.text
num1 = label.text.toInt
label.text = ""
case "=" =>
calculate()
case _ =>
println("error")
var num1 = 0
var operator = ""
Step 7: Calculate
def calculate(): Unit = {
val num2 = label.text.toInt
val result = operator match
case "+" => num1 + num2
case "-" => num1 - num2
case "*" => num1 * num2
case "/" => num1 / num2
label.text = result.toString
}
Step 8: Layout
label.preferredSize = new Dimension(200, 30)
label.xAlignment = Alignment.Left
label.font = Font("Arial", Font.Plain, 18)
Quiz
Question 1: Variable Declarations
Consider the following Scala code snippet:
val x: Int = 10
var y: Int = 20
y = x + 10
val z: Int = y + 10
What is the value of z at the end of this execution?
A) 20
B) 30
C) 40
D) 50
Question 2: Function Definition
Review the Scala function below:
def multiply(a: Int, b: Int): Int = a * b
What does this function do?
A) Adds two numbers
B) Subtracts two numbers
C) Multiplies two numbers
D) Divides two numbers
Question 3: Immutable List Operations
Given the Scala code:
val numbers = List(1, 2, 3, 4, 5)
val doubled = numbers.map(_ * 2)
What does the doubled list contain?
A) List(2, 4, 6, 8, 10)
B) List(1, 2, 3, 4, 5)
C) List(0, 2, 4, 6, 8)
D) List(3, 4, 5, 6, 7)
Question 4: Pattern Matching
Consider this Scala code snippet using pattern matching:
val greeting = "Hello"
greeting match {
case "Hello" => "World"
case "Goodbye" => "Everyone"
case _ => "Unknown"
}
What is the result of the pattern matching?
A) World
B) Everyone
C) Unknown
D) Error
Question 5: Variable Mutability
Which keyword is used to declare a mutable variable in Scala?
A) let
B) var
C) val
D) const
Question 6: Scala's Type Inference
What does Scala's type inference feature allow you to do?
A) Automatically detect errors in your code
B) Infer the type of variables at runtime
C) Omit the type of a variable when it is declared
D) Change the type of a variable after it is initialized
Question 7: Function Declarations
How do you declare a function in Scala that takes two integers and returns their sum?
A) def sum(x: Int, y: Int): Int = { return x + y }
B) function sum(x: Int, y: Int): Int = x + y
C) def sum(x: Int, y: Int) => x + y
D) Both A and C are correct
Question 8: Using a while Loop
Consider the following Scala code snippet:
var sum = 0
var i = 1
while (i <= 5) {
sum += i
i += 1
}
println(sum)
What is the output of this code?
A) 10
B) 15
C) 5
D) 20
Question 9: Iterating with for Loop
Analyze the Scala code below:
var product = 1
for (i <- 1 to 4) {
product *= i
}
println(product)
What does this code print?
A) 10
B) 24
C) 12
D) 4
Question 10: Nested for Loops
Consider the following Scala code snippet that uses nested for loops:
val rows = 3
val cols = 2
for (row <- 1 to rows) {
for (col <- 1 to cols) {
print(s"($row,$col) ")
}
println()
}
What does this code print?
A) (1,1) (1,2)
(2,1) (2,2)
(3,1) (3,2)
B) (1,1) (2,1) (3,1)
(1,2) (2,2) (3,2)
C) (1,1) (1,2) (2,1) (2,2) (3,1) (3,2)
D) None of the above
Question 11: Basic if-else Logic
Consider the Scala code snippet below:
val age = 20
val canVote = if (age >= 18) {
println("You can vote.")
true
} else {
println("You cannot vote.")
false
}
println(s"Can vote: $canVote")
What is the output of this code?
A) You can vote.
Can vote: true
B) You cannot vote.
Can vote: false
C) You can vote.
Can vote: false
D) Compilation error.
Question 12: Nested if-else with Logical Operators
Review the Scala code snippet involving nested if-else and logical operators:
val age = 25
val hasLicense = true
if (age >= 18) {
if (hasLicense) {
println("You can drive.")
} else {
println("You cannot drive without a license.")
}
} else {
println("You are too young to drive.")
}
What is the output of this program?
A) You can drive.
B) You cannot drive without a license.
C) You are too young to drive.
D) None of the above
Question 13: if-else with Compound Conditions
Consider the following Scala code snippet:
val temperature = 30
val isRaining = false
if (temperature > 25 && !isRaining) {
println("It's a good day for a walk.")
} else if (temperature > 25 && isRaining) {
println("It's warm, but it's raining.")
} else {
println("Stay indoors.")
}
What does this code print?
A) It's a good day for a walk.
B) It's warm, but it's raining.
C) Stay indoors.
D) Compilation error.
Question 14: Complex if-else with Function Calls
def checkEligibility(age: Int, hasPermit: Boolean): String = {
if (age >= 18) {
if (hasPermit) "Eligible" else "Not eligible: No permit"
} else {
"Not eligible: Too young"
}
}
val result = checkEligibility(17, hasPermit = true)
println(result)
What is the output of this code?
A) Eligible
B) Not eligible: No permit
C) Not eligible: Too young
D) Compilation error
Question 15: Basic Pattern Matching
Consider the following Scala code snippet:
val number = 3
val result = number match {
case 1 => "One"
case 2 => "Two"
case 3 => "Three"
case _ => "Other"
}
println(result)
What is the output of this code?
A) One
B) Two
C) Three
D) Other
Question 16: Defining and Calling a Simple Function
def greet(name: String): String = {
return "Hello, " + name + "!"
}
val greeting = greet("Alice")
println(greeting)
What is the output of this code?
A) Hello, Alice
B) Hello, Alice!
C) Greet: Alice
D) None of the above
Object Oriented
Class
- 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
Task: Define a case class named Book with two fields: title (String) and authors (List[String]). Create instances of this class for three different books.
Exercise 2: Pattern Matching on Case Classes
Task: Write a function describeBook that accepts a Book and returns a string describing the book. If the book has more than one author, the description should note that it's written by multiple authors.
Exercise 3: Copying and Modifying Case Classes
Task: Using the Book case class from Exercise 1, create a copy of book1 but with an additional author "Venners Bill". Use the copy method provided by case classes.
Exercise 4: Case Classes in Collections
Task: Given a list of books, write a function titlesByAuthor that takes an author's name and returns a list of titles by that author.
Pattern Matching
Exercise 1: Basic Case Class and Pattern Matching
Task: Define a case class for a Person with two fields: name (String) and age (Int). Write a function that accepts a Person and returns a greeting message that varies based on the person's age.
Exercise 2: Using Case Classes in Collections
Enum
Exercises
Scala 3 introduces enums as a first-class feature to define a group of named values. Enums are a significant improvement over Scala 2's sealed trait and case object pattern for enumerations, providing a cleaner syntax and more functionality. Here are some exercises to help you understand and practice using enums in Scala 3.
Exercise 1: Basic Enum
Task: Define an enum DayOfWeek representing the days of the week. Then, write a function isWeekend that takes a DayOfWeek and returns true if it's either Saturday or Sunday, and false otherwise.
Exercise 2: Enum with Parameters
Task: Define an enum TrafficLight where each value can have a color name as a String parameter. Implement a method color that returns the color of the traffic light.
Exercise 3: Enums in Collections
Task: Given a list of TrafficLight enums, write a function that counts how many of each light are in the list and prints the result.
Exercises
Exercise 1A
Create a Car class with the fields: mark and color. Give the Car the behavoir drive() and brake() with the state speed. Create a @main function Instantiate a Auto as a red Tesla as myCar. Let myCar drive and brake and print the speed.
class Car:
var mark: String = ""
var color: String = ""
var speed = 0
def drive() = speed += 10
def brake() = speed -= 10
@main
def main(): Unit =
val myCar = new Car()
myCar.mark = "Tesla"
myCar.color = "red"
myCar.drive()
println(s"speed: ${myCar.speed}")
Exercise 1B
Create hisCar a gray Suzuki.
Let it drive three times and print the speed
Exercise 2
Create a constructor on the fields: mark and color
Change the main function to use the constructor
Exercise 3
Make the Car immutable by changing var by val
Change de main function if needed.
exercise 4
Make the speed private. Generate the toString method Change de main function .
exercise 5
Create a new class Truck, inherited from the Car class.
Give the Truck an extra field freight.
Give the Truck a constructor met de fields mark and color
In de main function create a blue Volvo with the variable name: truck1 En give truck1 a freight of 1000.
Exercise 6
Override the function drive() and brake() with a slower drive and brake speed
Exercise 7
Create RaceAuto inherited from Car. Give it a field topSpeed
Instantiate a red Ferrari as secondCar
exercise 8
Create a trait Vehicle met de functions: drive() and brake() make the Car clas implementing this trait.
Exercise 9
Create a Bicycle class from the Vehicle trait.
Instantiate a gray VanMook as myBike
Project: Todo App
Step 1: Set Up Your Scala Project
First, ensure you have Scala and SBT (Scala Build Tool) installed on your machine. You can check by running the following commands:
scala -version
sbt -version
If you don't have them installed, follow the installation instructions from the official Scala website.
Next, in IntelliJ create a new SBT project
Step 2: Define the To-Do App Structure
Create a Scala object to hold your application logic. You can do this by creating a new file in the src/main/scala directory. Let's call it TodoApp.scala.
import scala.io.StdIn.readLine
import scala.collection.mutable.ListBuffer
object TodoApp:
case class Task(id: Int, description: String)
val tasks: ListBuffer[Task] = ListBuffer.empty
var nextId: Int = 1
def main(args: Array[String]): Unit =
var continue = true
while (continue)
println("\nTODO App")
println("1. Add Task")
println("2. List Tasks")
println("3. Delete Task")
println("4. Exit")
print("Choose an option: ")
readLine() match {
case "1" => addTask()
case "2" => listTasks()
case "3" => deleteTask()
case "4" => continue = false
case _ => println("Invalid option. Please try again.")
def addTask(): Unit =
print("Enter task description: ")
val description = readLine()
tasks += Task(nextId, description)
nextId += 1
println(s"Task added with id $nextId")
def listTasks(): Unit =
if tasks.isEmpty {
println("No tasks available.")
} else {
tasks.foreach(task => println(s"${task.id}. ${task.description}"))
}
def deleteTask(): Unit =
print("Enter task id to delete: ")
val id = readLine().toInt
val taskIndex = tasks.indexWhere(_.id == id)
if taskIndex != -1 {
tasks.remove(taskIndex)
println(s"Task with id $id deleted.")
} else {
println(s"Task with id $id not found.")
}
}
Step 3: Running Your Application
To run your application, use SBT:
sbt run
This command compiles and runs your Scala application. You should see the menu and be able to interact with your to-do list by adding, listing, and deleting tasks.
Explanation
- 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
Step 1 Paint Rectangle
import java.awt.{Color, Graphics2D}
import scala.swing.BorderPanel.Position.Center
import scala.swing.{BorderPanel, Dimension, MainFrame, Panel, SimpleSwingApplication}
object Scribble1 extends SimpleSwingApplication:
def top = new MainFrame:
title = "Scribble App"
object canvas extends Panel:
override def paintComponent(g: Graphics2D): Unit =
super.paintComponent(g)
g.setColor(Color.black)
g.drawRect(20, 20, 20, 20)
contents = new BorderPanel:
layout(canvas) = Center
size = new Dimension(500, 500)
step 2 Mouse Events
add the mouse events to the canvas
object canvas extends Panel:
listenTo(mouse.clicks)
var x1 = 0
var y1 = 0
var x2 = 0
var y2 = 0
reactions +=
case e: MousePressed =>
x1 = e.point.x
y1 = e.point.y
case e: MouseReleased =>
x2 = e.point.x
y2 = e.point.y
repaint()
step 3: Rectangle class
class Rect(var x1: Int, var y1: Int, var x2: Int, var y2: Int):
def draw(g: Graphics2D): Unit =
g.drawRect(x1, y1, x2 - x1, y2 - y1)
step 4 Shape trait
trait Shape:
var x1: Int
var y1: Int
var x2: Int
var y2: Int
def draw(g: Graphics2D): Unit
Also create an Oval class
step 5 More shapes
private val shapes: ListBuffer[Shape] = ListBuffer()
case e: MousePressed =>
if e.triggersPopup then
shp = Oval(e.point.x, e.point.y, 0, 0)
else
shp = Rect(e.point.x, e.point.y, 0, 0)
shapes.append(shp)
step 6 Button Panel
val modePanel = new FlowPanel:
val rectangleButton = new Button("Rectangle")
val ovalButton = new Button("Oval")
contents += rectangleButton
contents += ovalButton
listenTo(rectangleButton, ovalButton)
reactions += {
case ButtonClicked(`rectangleButton`) => currentMode = Rectangle
case ButtonClicked(`ovalButton`) => currentMode = Oval
}
// backticks: In pattern matching, it helps you to use variable as if its a concrete value.
Exercises
Exercise 1: Basic List Operations
Create a list of integers from 1 to 10. Then, perform the following tasks:
- Extract the first element.
- Extract the last element.
- Get all elements except the first.
- Get all elements except the last.
- Check if the list contains the number 5.
Exercise 2: Concatenation and Addition
- Create two lists: one with even numbers from 2 to 10, and another with odd numbers from 1 to 9.
- Concatenate these lists into a single list.
- Add the number 0 to the beginning of the concatenated list.
Exercise 3: Mapping and Filtering
Given a list of numbers from 1 to 10:
- Multiply each number by 2.
- Filter the resulting list to keep only numbers greater than 10.
Exercise 4: Folding and Reducing
- Create a list of integers from 1 to 5.
- Compute the sum of all elements using
foldLeft. - Compute the product of all elements using
reduce.
Exercise 5: Using ListBuffer
- Create a mutable
ListBufferand add elements 1, 2, and 3 to it. - Append the element 4 to the list buffer.
- Remove the element 2 from the list buffer.
- Convert the
ListBufferto an immutable list.
Exercise 6: Working with Arrays
- Create an array of integers from 1 to 5.
- Update the third element (index 2) to 10.
- Print the length of the array.
- Iterate over the array and print each element.
Exercise 7: Zipping and Unzipping
- Create two lists: one with numbers 1 to 3, and another with the strings "one", "two", and "three".
- Zip these lists together.
- Unzip the zipped list back into two separate lists.
Exercise 8: Using Range to Create Lists
- Create a list of numbers from 1 to 10 using
Range. - Create a list of even numbers from 2 to 20 using
Range. - Create a list of numbers from 10 to 1 (in reverse order) using
Range.
Exercise 9: LazyList
- Create a
LazyListof the first 10 positive integers. - Define a
LazyListthat generates an infinite sequence of even numbers. - Take the first 10 elements from the infinite
LazyListand print them.
Exercise 10: Grouping and Partitioning
- Create a list of integers from 1 to 10.
- Group the integers by whether they are even or odd.
- Partition the list into two lists: one containing numbers less than or equal to 5, and another containing numbers greater than 5.
Exercises Bookstore filter map
The bookstore
case class Author(name: String, nationality: String)
case class Book(title: String, author: Author, year: Int, category: String, price: Double, description: Option[String])
val authors = List(
Author("George Orwell", "British"),
Author("Harper Lee", "American"),
Author("F. Scott Fitzgerald", "American"),
Author("Aldous Huxley", "British"),
Author("Herman Melville", "American"),
Author("J.D. Salinger", "American"),
Author("Yuval Noah Harari", "Israeli")
)
val library = List(
Book("1984", authors(0), 1949, "Dystopian", 15.99, Some("A dystopian social science fiction novel and cautionary tale.")),
Book("To Kill a Mockingbird", authors(1), 1960, "Fiction", 10.99, Some("A novel about the serious issues of rape and racial inequality.")),
Book("The Great Gatsby", authors(2), 1925, "Classic", 8.99, Some("A story of the mysteriously wealthy Jay Gatsby and his love for Daisy Buchanan.")),
Book("Brave New World", authors(3), 1932, "Dystopian", 12.99, None),
Book("Moby Dick", authors(4), 1851, "Classic", 9.99, Some("The narrative of Captain Ahab's obsessive quest to kill the giant white sperm whale Moby Dick.")),
Book("The Catcher in the Rye", authors(5), 1951, "Fiction", 14.99, Some("A novel about teenage rebellion and alienation."))
)
Use Case 1: Finding Books by a Specific Author
Objective: Find all books written by a specific author.
def findBooksByAuthor(library: List[Book], authorName: String): List[Book] = {
// TODO
}
val booksByOrwell = findBooksByAuthor(library, "George Orwell")
println(s"Books by George Orwell: ${booksByOrwell.map(_.title)}")
Use Case 2: Filtering Books Based on Price
Objective: Filter books that are within a certain price range.
def filterBooksByPrice(library: List[Book], minPrice: Double, maxPrice: Double): List[Book] = {
// TODO
}
val affordableBooks = filterBooksByPrice(library, 10.0, 15.0)
println(s"Affordable books: ${affordableBooks.map(_.title)}")
Use Case 3: Filtering Books by Category
Objective: Filter books belonging to a specific category.
def filterBooksByCategory(library: List[Book], category: String): List[Book] = {
// TODO
}
val dystopianBooks = filterBooksByCategory(library, "Dystopian")
println(s"Dystopian books: ${dystopianBooks.map(_.title)}")
Use Case 4: Displaying Book Titles with Prices
Objective: Create a display list of book titles along with their prices.
def displayBookTitlesWithPrices(library: List[Book]): List[String] = {
// TODO
}
val bookTitlesWithPrices = displayBookTitlesWithPrices(library)
bookTitlesWithPrices.foreach(println)
Use Case 5: Finding Books Published After a Certain Year
Objective: Find all books published after a specified year.
def findBooksPublishedAfter(library: List[Book], year: Int): List[Book] = {
// TODO
}
val modernBooks = findBooksPublishedAfter(library, 1950)
println(s"Books published after 1950: ${modernBooks.map(_.title)}")
Use Case 6: Creating a Summary Description for Books
Objective: Create a summary description for each book, combining title, author, and category.
def createBookSummaries(library: List[Book]): List[String] = {
// TODO
}
val bookSummaries = createBookSummaries(library)
bookSummaries.foreach(println)
Use Case 7: Finding Discounted Books
Objective: Apply a discount to all books and return the new prices.
def applyDiscount(library: List[Book], discountRate: Double): List[(String, Double)] = {
// TODO
}
val discountedBooks = applyDiscount(library, 0.10)
discountedBooks.foreach { case (title, price) => println(s"$title - $price") }
Exercises Bookstore collect
The Bookstore
case class Author(name: String, nationality: String)
case class Book(title: String, author: Author, year: Int, category: String, price: Double, description: Option[String])
val authors = List(
Author("George Orwell", "British"),
Author("Harper Lee", "American"),
Author("F. Scott Fitzgerald", "American"),
Author("Aldous Huxley", "British"),
Author("Herman Melville", "American"),
Author("J.D. Salinger", "American"),
Author("Yuval Noah Harari", "Israeli")
)
val library = List(
Book("1984", authors(0), 1949, "Dystopian", 15.99, Some("A dystopian social science fiction novel and cautionary tale.")),
Book("To Kill a Mockingbird", authors(1), 1960, "Fiction", 10.99, Some("A novel about the serious issues of rape and racial inequality.")),
Book("The Great Gatsby", authors(2), 1925, "Classic", 8.99, Some("A story of the mysteriously wealthy Jay Gatsby and his love for Daisy Buchanan.")),
Book("Brave New World", authors(3), 1932, "Dystopian", 12.99, None),
Book("Moby Dick", authors(4), 1851, "Classic", 9.99, Some("The narrative of Captain Ahab's obsessive quest to kill the giant white sperm whale Moby Dick.")),
Book("The Catcher in the Rye", authors(5), 1951, "Fiction", 14.99, Some("A novel about teenage rebellion and alienation."))
)
Use Case 1: Collecting Books with Descriptions
Objective: Extract the titles and descriptions of books that have descriptions.
def collectBooksWithDescriptions(library: List[Book]): List[String] = {
// TODO
}
val booksWithDescriptions = collectBooksWithDescriptions(library)
booksWithDescriptions.foreach(println)
Use Case 2: Calculating Total Value of Books
Objective: Calculate the total value of all books in the library using foldLeft.
def calculateTotalValue(library: List[Book]): Double = {
// TODO
}
val totalValue = calculateTotalValue(library)
println(s"Total value of books: $$${totalValue}")
Use Case 3: Finding the Oldest Book
Objective: Find the oldest book in the library using foldLeft.
def findOldestBook(library: List[Book]): Option[Book] = {
// TODO
}
val oldestBook = findOldestBook(library)
oldestBook match {
case Some(book) => println(s"Oldest book: ${book.title}, published in ${book.year}")
case None => println("No books in the library")
}
Use Case 4: Grouping Books by Decade
Objective: Group books by the decade they were published using collect.
def groupBooksByDecade(library: List[Book]): Map[Int, List[Book]] = {
// TODO
}
val booksByDecade = groupBooksByDecade(library)
booksByDecade.foreach { case (decade, books) =>
println(s"$decade:")
books.foreach(book => println(s" ${book.title}"))
}
Use Case 5: Counting Books by Category
Objective: Count the number of books in each category using foldLeft.
def countBooksByCategory(library: List[Book]): Map[String, Int] = {
// TODO
}
val booksCountByCategory = countBooksByCategory(library)
booksCountByCategory.foreach { case (category, count) =>
println(s"$category: $count books")
}
Exercises Bookstore zip
The Bookstore
case class Author(name: String, nationality: String)
case class Book(title: String, author: Author, year: Int, category: String, price: Double, description: Option[String])
val authors = List(
Author("George Orwell", "British"),
Author("Harper Lee", "American"),
Author("F. Scott Fitzgerald", "American"),
Author("Aldous Huxley", "British"),
Author("Herman Melville", "American"),
Author("J.D. Salinger", "American"),
Author("Yuval Noah Harari", "Israeli")
)
val library = List(
Book("1984", authors(0), 1949, "Dystopian", 15.99, Some("A dystopian social science fiction novel and cautionary tale.")),
Book("To Kill a Mockingbird", authors(1), 1960, "Fiction", 10.99, Some("A novel about the serious issues of rape and racial inequality.")),
Book("The Great Gatsby", authors(2), 1925, "Classic", 8.99, Some("A story of the mysteriously wealthy Jay Gatsby and his love for Daisy Buchanan.")),
Book("Brave New World", authors(3), 1932, "Dystopian", 12.99, None),
Book("Moby Dick", authors(4), 1851, "Classic", 9.99, Some("The narrative of Captain Ahab's obsessive quest to kill the giant white sperm whale Moby Dick.")),
Book("The Catcher in the Rye", authors(5), 1951, "Fiction", 14.99, Some("A novel about teenage rebellion and alienation."))
)
Use Case 1: Creating Pairs of Book Titles and Prices with zip
Objective: Pair each book title with its price.
def pairTitlesWithPrices(library: List[Book]): List[(String, Double)] = {
// TODO
}
val titlesWithPrices = pairTitlesWithPrices(library)
titlesWithPrices.foreach { case (title, price) => println(s"$title: $$${price}") }
Use Case 2: Creating Pairs of Book Titles and Their Indexes with zipWithIndex
Objective: Pair each book title with its index in the list.
def pairTitlesWithIndexes(library: List[Book]): List[(String, Int)] = {
// TODO
}
val titlesWithIndexes = pairTitlesWithIndexes(library)
titlesWithIndexes.foreach { case (title, index) => println(s"$index: $title") }
Use Case 3: Matching Authors to Their Books with zip
Objective: Pair each author with their respective book titles.
def matchAuthorsToBooks(library: List[Book]): List[(String, List[String])] = {
// TODO
}
val authorsWithBooks = matchAuthorsToBooks(library)
authorsWithBooks.foreach { case (author, books) =>
println(s"$author: ${books.mkString(", ")}")
}
Use Case 4: Pairing Book Titles with Publication Years with zip
Objective: Pair each book title with its publication year.
def pairTitlesWithYears(library: List[Book]): List[(String, Int)] = {
// TODO
}
val titlesWithYears = pairTitlesWithYears(library)
titlesWithYears.foreach { case (title, year) => println(s"$title: $year") }
Use Case 5: Creating Pairs of Original and Discounted Prices with zip
Objective: Create pairs of original and discounted prices for each book.
def pairOriginalWithDiscountedPrices(library: List[Book], discountRate: Double): List[(Double, Double)] = {
// TODO
}
val originalWithDiscountedPrices = pairOriginalWithDiscountedPrices(library, 0.10)
originalWithDiscountedPrices.foreach { case (original, discounted) => println(f"Original: $$${original}%.2f, Discounted: $$${discounted}%.2f") }
Use Case 6: Creating a List of Book Titles with Their Indexes for Display Purposes with zipWithIndex
Objective: Pair each book title with its index to create a display list.
def displayBooksWithIndexes(library: List[Book]): List[String] = {
// TODO
}
val booksWithIndexes = displayBooksWithIndexes(library)
booksWithIndexes.foreach(println)
Exercises Bookstore GroupBy
The Bookstore
case class Author(name: String, nationality: String)
case class Book(title: String, author: Author, year: Int, category: String, price: Double, description: Option[String])
val authors = List(
Author("George Orwell", "British"),
Author("Harper Lee", "American"),
Author("F. Scott Fitzgerald", "American"),
Author("Aldous Huxley", "British"),
Author("Herman Melville", "American"),
Author("J.D. Salinger", "American"),
Author("Yuval Noah Harari", "Israeli")
)
val library = List(
Book("1984", authors(0), 1949, "Dystopian", 15.99, Some("A dystopian social science fiction novel and cautionary tale.")),
Book("To Kill a Mockingbird", authors(1), 1960, "Fiction", 10.99, Some("A novel about the serious issues of rape and racial inequality.")),
Book("The Great Gatsby", authors(2), 1925, "Classic", 8.99, Some("A story of the mysteriously wealthy Jay Gatsby and his love for Daisy Buchanan.")),
Book("Brave New World", authors(3), 1932, "Dystopian", 12.99, None),
Book("Moby Dick", authors(4), 1851, "Classic", 9.99, Some("The narrative of Captain Ahab's obsessive quest to kill the giant white sperm whale Moby Dick.")),
Book("The Catcher in the Rye", authors(5), 1951, "Fiction", 14.99, Some("A novel about teenage rebellion and alienation."))
)
Use Case 1: Grouping Books by Category
Objective: Group all books by their category.
def groupBooksByCategory(library: List[Book]): Map[String, List[Book]] = {
// TODO
}
val booksByCategory = groupBooksByCategory(library)
booksByCategory.foreach { case (category, books) =>
println(s"$category:")
books.foreach(book => println(s" ${book.title} by ${book.author.name}"))
}
Use Case 2: Grouping Books by Author
Objective: Group all books by their author.
def groupBooksByAuthor(library: List[Book]): Map[String, List[Book]] = {
// TODO
}
val booksByAuthor = groupBooksByAuthor(library)
booksByAuthor.foreach { case (author, books) =>
println(s"$author:")
books.foreach(book => println(s" ${book.title} [${book.category}]"))
}
Use Case 3: Grouping Books by Decade
Objective: Group all books by the decade they were published.
def groupBooksByDecade(library: List[Book]): Map[Int, List[Book]] = {
// TODO
}
val booksByDecade = groupBooksByDecade(library)
booksByDecade.foreach { case (decade, books) =>
println(s"$decade:")
books.foreach(book => println(s" ${book.title} by ${book.author.name}"))
}
Use Case 4: Grouping Books by Price Range
Objective: Group all books by price range (e.g., <$10, $10-$20, >$20).
def groupBooksByPriceRange(library: List[Book]): Map[String, List[Book]] = {
// TODO
}
val booksByPriceRange = groupBooksByPriceRange(library)
booksByPriceRange.foreach { case (priceRange, books) =>
println(s"$priceRange:")
books.foreach(book => println(s" ${book.title} by ${book.author.name} - $$${book.price}"))
}
Use Case 5: Grouping Books by Availability of Description
Objective: Group all books by whether they have a description or not.
def groupBooksByDescription(library: List[Book]): Map[String, List[Book]] = {
// TODO
}
val booksByDescription = groupBooksByDescription(library)
booksByDescription.foreach { case (descriptionStatus, books) =>
println(s"$descriptionStatus:")
books.foreach(book => println(s" ${book.title} by ${book.author.name}"))
}
Exercises Bookstore more
Use Case 1: Partition Books by Availability of Description
Objective: Partition books into two lists: those with a description and those without.
def partitionBooksByDescription(library: List[Book]): (List[Book], List[Book]) = {
// TODO
}
val (booksWithDescription, booksWithoutDescription) = partitionBooksByDescription(library)
println("Books with Description:")
booksWithDescription.foreach(book => println(s"${book.title} by ${book.author.name}"))
println("\nBooks without Description:")
booksWithoutDescription.foreach(book => println(s"${book.title} by ${book.author.name}"))
Use Case 2: Find the Most Expensive Book
Objective: Find the most expensive book in the library.
def findMostExpensiveBook(library: List[Book]): Option[Book] = {
// TODO
}
val mostExpensiveBook = findMostExpensiveBook(library)
mostExpensiveBook match {
case Some(book) => println(s"Most expensive book: ${book.title} by ${book.author.name} - $$${book.price}")
case None => println("No books in the library")
}
Use Case 3: Get Distinct Authors
Objective: Get a list of distinct authors in the library.
def distinctAuthors(library: List[Book]): List[Author] = {
// TODO
}
val authorsList = distinctAuthors(library)
println("Distinct Authors:")
authorsList.foreach(author => println(s"${author.name} (${author.nationality})"))
Use Case 4: Flatten Nested List of Book Lists
Objective: Flatten a list of lists of books into a single list of books.
val moreBooks = List(
List(Book("Sapiens", authors(6), 2011, "Non-Fiction", 19.99, Some("A brief history of humankind."))),
List(Book("Homo Deus", authors(6), 2015, "Non-Fiction", 21.99, Some("A brief history of tomorrow."))),
List(Book("Brave New World", authors(3), 1932, "Dystopian", 12.99, None))
)
def flattenBookLists(bookLists: List[List[Book]]): List[Book] = {
// TODO
}
val allBooks = flattenBookLists(moreBooks)
println("All Books:")
allBooks.foreach(book => println(s"${book.title} by ${book.author.name}"))
Use Case 5: Extract Titles of Books by a Specific Author Using flatMap
Objective: Extract the titles of books written by a specific author using flatMap.
def titlesByAuthor(library: List[Book], authorName: String): List[String] = {
// TODO
}
val orwellTitles = titlesByAuthor(library, "George Orwell")
println("Titles by George Orwell:")
orwellTitles.foreach(println)
Use Case 6: Calculate Total Price of Books Using foldLeft
Objective: Calculate the total price of all books in the library using foldLeft.
def totalPrice(library: List[Book]): Double = {
// TODO
}
val totalCost = totalPrice(library)
println(f"Total cost of all books: $$${totalCost}%.2f")
Use Case 7: Combine Books and Authors into a Map
Objective: Create a map of authors to their books.
def mapAuthorsToBooks(library: List[Book]): Map[Author, List[Book]] = {
// TODO
}
val authorBooksMap = mapAuthorsToBooks(library)
authorBooksMap.foreach { case (author, books) =>
println(s"${author.name}'s books:")
books.foreach(book => println(s" ${book.title}"))
}
Project: Bookstore
Let's build a simple version with functionalities to
- add books
- list books
- add customers
- list customers
- place orders
Define the Bookstore Structure
Create a Scala object to hold your application logic. Create a new file in the src/main/scala directory named BookstoreApp.scala.
import scala.io.StdIn.readLine
import scala.collection.mutable.ListBuffer
object BookstoreApp {
// TODO: case class Book, Customer, Order
// TODO: Mutable lists of books, customers and orders
var nextBookId: Int = 1
var nextCustomerId: Int = 1
var nextOrderId: Int = 1
def main(args: Array[String]): Unit = {
var continue = true
while (continue) {
println("\nBookstore App")
println("1. Add Book")
println("2. List Books")
println("3. Add Customer")
println("4. List Customers")
println("5. Place Order")
println("6. List Orders")
println("7. Exit")
print("Choose an option: ")
readLine() match {
case "1" => addBook()
case "2" => listBooks()
case "3" => addCustomer()
case "4" => listCustomers()
case "5" => placeOrder()
case "6" => listOrders()
case "7" => continue = false
case _ => println("Invalid option. Please try again.")
}
}
}
def addBook(): Unit = {
// TODO
}
def listBooks(): Unit = {
// TODO
}
def addCustomer(): Unit = {
// TODO
}
def listCustomers(): Unit = {
// TODO
}
def placeOrder(): Unit = {
// TODO
}
def listOrders(): Unit = {
// TODO
}
Structure
- Case Classes:
Book: Represents a book withid,title,author, andprice.Customer: Represents a customer withid,name, andemail.Order: Represents an order withid,customerId, andbookId.
- Lists:
books: A mutable list to hold books.customers: A mutable list to hold customers.orders: A mutable list to hold orders.
- Counters:
nextBookId: A counter to assign unique IDs to books.nextCustomerId: A counter to assign unique IDs to customers.nextOrderId: A counter to assign unique IDs to orders.
- Methods:
addBook(): Allows adding a new book.listBooks(): Lists all books.addCustomer(): Allows adding a new customer.listCustomers(): Lists all customers.placeOrder(): Allows placing an order.listOrders(): Lists all orders.
Quiz
Question 1: Collection Filtering
Review the Scala code below:
val nums = List(-1, 2, -3, 4, 5)
val positiveNums = nums.filter(_ > 0)
What does positiveNums contain?
A) List(-1, -3)
B) List(2, 4, 5)
C) List(1, 2, 3, 4, 5)
D) List(-3, -1, 5, 4, 2)
Question 2: Tuples and Destructuring
Consider the Scala tuple:
val person = ("John", 30)
How do you access the age from the person tuple?
A) person(1)
B) person._2
C) person[1]
D) person.age
Question 3: Defining Classes
How do you correctly define a simple class in Scala that takes one parameter?
A) class MyClass(param: Type)
B) def MyClass(param: Type)
C) object MyClass { val param: Type }
D) new class MyClass(param: Type)
Question 4: Scala Collections
Which of the following collections is immutable?
A) ArrayBuffer
B) List
C) Array
D) ListBuffer
Question 5: Object-Oriented Programming
Which of the following statements about Scala is true?
A) Scala supports multiple inheritance through classes.
B) Scala does not allow the use of generic types.
C) Scala allows defining singleton objects using the object keyword.
D) Scala classes cannot have parameters.
Question 68: Defining and Instantiating a Simple Class
Consider the following Scala code snippet:
class Person(name: String, age: Int) {
def greet(): String = s"Hello, my name is $name and I am $age years old."
}
val alice = new Person("Alice", 30)
println(alice.greet())
val bob = new Person("Bob", 25)
println(bob.greet())
What is the output of this code?
A) Hello, my name is Alice and I am 30 years old.
Hello, my name is Bob and I am 25 years old.
B) Compilation error
C) Hello, my name is Person and I am 30 years old.
Hello, my name is Person and I am 25 years old.
D) None of the above
Question 6: Class with Private Members
Review this Scala code snippet for a class with private members and methods:
class Counter {
private var count = 0
private def checkLimit(): Boolean = count < 10
def increment(): Unit = {
if (checkLimit()) count += 1
}
def getCount(): Int = count
}
val myCounter = new Counter()
for (_ <- 1 to 15) myCounter.increment()
println(myCounter.getCount())
What is the output of this program?
A) 10
B) 15
C) 0
D) Compilation error
Question 8: Overriding Methods in Subclasses
Consider the following Scala code snippet demonstrating method overriding:
class Animal {
def sound(): String = "Some sound"
}
class Dog extends Animal {
override def sound(): String = "Woof"
}
val myAnimal = new Animal()
println(myAnimal.sound())
val myDog = new Dog()
println(myDog.sound())
What does this code print?
A) Some sound
Woof
B) Woof
Woof
C) Some sound
Some sound
D) Compilation error
Question 9: Abstract Classes and Traits
Analyze the Scala code snippet involving an abstract class and its implementation:
abstract class Shape {
def area: Double
}
class Circle(radius: Double) extends Shape {
def area: Double = Math.PI * radius * radius
}
val circle = new Circle(5)
println(f"Circle area: ${circle.area}%.2f")
What is the output of this code?
A) Circle area: 78.54
B) Circle area: 25.00
C) Compilation error
D) None of the above
Question 10: Defining and Implementing Traits
Consider the following Scala code snippet demonstrating a trait and its implementation:
trait Greeter {
def greet(name: String): Unit
}
class FormalGreeter extends Greeter {
def greet(name: String): Unit = {
println(s"Good day, $name.")
}
}
val greeter = new FormalGreeter()
greeter.greet("Alice")
What is the output of this code?
A) Good day, Alice.
B) Hello, Alice.
C) Compilation error
D) None of the above
Question 11: Mixing in Multiple Traits
Analyze the Scala code snippet below, which mixes multiple traits into a class:
trait Walker {
def walk(): Unit = println("Walking...")
}
trait Runner {
def run(): Unit = println("Running...")
}
class Person extends Walker with Runner
val p = new Person()
p.walk()
p.run()
What does this code print?
A) Walking...
Running...
B) Compilation error
C) Walking...
D) Running...
Question 12: Overriding Trait Methods in Classes
Review the following Scala code snippet involving overriding trait methods in a class:
trait Animal {
def makeSound(): Unit
}
class Dog extends Animal {
override def makeSound(): Unit = {
println("Woof")
}
}
val myDog = new Dog()
myDog.makeSound()
What is the output of this program?
A) Woof
B) Compilation error
C) The program prints nothing
D) None of the above
Question 13: Abstract and Concrete Methods in Traits
Consider the Scala code snippet demonstrating a trait with both abstract and concrete methods:
trait Calculator {
def add(a: Int, b: Int): Int
def subtract(a: Int, b: Int): Int = a - b
}
class BasicCalculator extends Calculator {
def add(a: Int, b: Int): Int = a + b
}
val calc = new BasicCalculator()
println(calc.add(5, 3))
println(calc.subtract(5, 3))
What does this code print?
A) 8
2
B) Compilation error
C) 8
D) 2
Question 14: Combining map and filter
Review the Scala code snippet involving a combination of map and filter:
val names = List("Alice", "Bob", "Charlie", "David")
val filteredNames = names.filter(_.length > 4).map(_.toUpperCase)
println(filteredNames)
What is the output of this program?
A) List("ALICE", "CHARLIE", "DAVID")
B) List("BOB", "DAVID")
C) List("ALICE", "CHARLIE")
D) List("CHARLIE", "DAVID")
Question 15: Matching Tuples
Review the Scala code snippet involving matching tuples:
val pair = (1, "apple")
val description = pair match {
case (i, s) if i == 1 => s"One $s"
case (i, s) if i > 1 => s"$i ${s}s"
case _ => "Unknown"
}
println(description)
What is the output of this program?
A) One apple
B) 1 apples
C) Unknown
D) Compilation error
Question 16: Matching with Lists
Consider the following Scala code snippet that matches lists:
val myList = List(1, 2, 3)
val listDescription = myList match {
case List(_, _, _) => "A list with three elements."
case List(_, _) => "A list with two elements."
case _ => "A list with a different number of elements."
}
println(listDescription)
What does this code print?
A) A list with three elements.
B) A list with two elements.
C) A list with a different number of elements.
D) None of the above
Question 17: Matching with Guards
Examine the Scala code snippet involving match expressions with guards:
val age = 25
val accessLevel = age match {
case a if a >= 18 && a < 21 => "Partial access"
case a if a >= 21 => "Full access"
case _ => "No access"
}
println(accessLevel)
What is the output of this code?
A) Partial access
B) Full access
C) No access
D) Compilation error
Question 18: For-Comprehension with Multiple Generators
Review this Scala for-comprehension involving multiple generators:
val xValues = List(1, 2, 3)
val yValues = List(4, 5)
val pairs = for {
x <- xValues
y <- yValues
} yield (x, y)
println(pairs)
What does this code print?
A) List((1,4), (1,5), (2,4), (2,5), (3,4), (3,5))
B) List((1,4), (2,5), (3,4))
C) List([1,4], [1,5], [2,4], [2,5], [3,4], [3,5])
D) Compilation error
Function values
Exercise 1: Filter List with Function Parameter
Task: Implement a function filterList that takes a List[Int] and a predicate function from Int to Boolean. It should return a new list containing only the elements that satisfy the predicate.
Exercise 2: Implement a Custom map Function
Task: Define a function mapList that behaves like the map method for lists. It should take a List[A] and a function from A to B, and return a List[B].
Exercise 3: A Higher-order Function that Returns a Function
Task: Write a higher-order function multiplier that takes an Int and returns a new function that takes an Int and multiplies it by the first number.
Exercise 4: Sorting with a Custom Comparator
Task: Write a function sortWithFunction that takes a List[A] and a comparison function (A, A) => Boolean which returns true if the first element should come before the second. It should return a list sorted according to this function.
Solutions
Exercise 1: Filter List with Function Parameter
def filterList(lst: List[Int], predicate: Int => Boolean): List[Int] = lst.filter(predicate)
// Test
println(filterList(List(1, 2, 3, 4, 5), _ % 2 == 0)) // Should print: List(2, 4)
Exercise 2: Implement a Custom map Function
def mapList[A, B](lst: List[A], func: A => B): List[B] = lst.map(func)
// Test
println(mapList(List("1", "2", "3"), _.toInt)) // Should print: List(1, 2, 3)
Exercise 3: A Higher-order Function that Returns a Function
def multiplier(factor: Int): Int => Int = number => number * factor
// Test
val triple = multiplier(3)
println(triple(5)) // Should print: 15
Exercise 4: Sorting with a Custom Comparator
def sortWithFunction[A](lst: List[A], comparator: (A, A) => Boolean): List[A] = lst.sortWith(comparator)
// Test
println(sortWithFunction(List(3, 1, 4, 2), (x: Int, y: Int) => x < y)) // Should print: List(1, 2, 3, 4)
Recursion
Exercise 1 sum
Write a function that takes a positive integer n and returns the sum of all the integers from 1 to n.
def sum(n: Int): Int =
// TODO
// example usage
sum(5) // returns 15
Exercise 2 sumBetween
Write a function that takes two integers and returns the sum of all the integers between them, including the endpoints.
def sumBetween(x: Int, y: Int): Int =
// TODO
// example usage
sumBetween(1, 5) // returns 15
Exercise 3 sumList
Write a function that takes a list of integers and returns the sum of all the integers in the list.
def sumList(list: List[Int]): Int = {
// TODO
}
// example usage
sumList(List(1, 2, 3, 4, 5)) // returns 15
Exercise 4 filter even
Write a function that takes a list of integers and returns a new list with all the even numbers.
def filterEven(list: List[Int]): List[Int] =
// TODO
// example usage
filterEven(List(1, 2, 3, 4, 5, 6)) // returns List(2, 4, 6)
Exercise 5 longest string
Write a function that takes a list of strings and returns the length of the longest string in the list.
def longestString(list: List[String]): Int =
// TODO
// example usage
longestString(List("apple", "banana", "orange", "pear")) // returns 6
tail-recursion
Option
Exercise 1
- Write a function that return 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 return the name of the day or an error message
def dayOfTheWeek(day: Int, map: [Int, String]): Option[String] = ???
Exercise 3
Write a vector with the number 1 to 10
val vector = Vector(1,2,3,4,5,6,7,8,9,10)
Write a function indexOf that
- return the number at the index
- or an error message when the index is out of bounds.
def indexOf(index: Int, vector: Vector[Int]): Try[Int] = ???
Call the indexOf function and print the result in a pattern match
indexOf(2, vector) match ???
Exercise 4
Do the same as in Exercise 3 but now use 'Either'
def indexOf(index: Int, vector: Vector[Int]): Either[Int] = ???
Try
Handling exceptions is a crucial part of developing robust Scala applications. Scala provides a try-catch construct similar to other languages like Java, but with some functional twists that make it powerful and expressive. Scala's approach encourages the use of immutable values and provides mechanisms to deal with exceptions in a functional way.
Basic Try-Catch
In Scala, you use try-catch blocks to catch exceptions. The catch block uses pattern matching to handle different types of exceptions.
try
// Code that might throw an exception
val result = 10 / 0
catch
case e: ArithmeticException => println("Arithmetic Exception caught: " + e.getMessage)
case e: Exception => println("General exception caught: " + e.getMessage)
finally
// Optional finally block executes regardless of whether an exception was caught
println("Finally block executed")
The Try Type
Scala provides a Try type that represents a computation that may either result in an exception (Failure) or return a successfully computed value (Success). It is a better way to handle exceptions when working with functional programming paradigms.
To use Try, you need to import it from the Scala library:
import scala.util.{Try, Success, Failure}
You can wrap a computation in a Try, which will catch any non-fatal exceptions and return a Success with the value if the computation is successful, or a Failure with the exception if it is not.
val result: Try[Int] = Try(10 / 0)
You can then pattern match on the result:
result match
case Success(value) => println(s"Computation successful: $value")
case Failure(exception) => println(s"Computation failed with exception: ${exception.getMessage}")
Chaining Operations with Try
One of the benefits of using Try is the ability to chain operations without having to explicitly check for exceptions at each step.
def divide(a: Int, b: Int): Try[Int] = Try(a / b)
val result = divide(10, 0).map(_ * 2)
result match
case Success(value) => println(s"Result: $value")
case Failure(exception) => println(s"Error: ${exception.getMessage}")
For-Comprehensions with Try
For-comprehensions can be used with Try to perform multiple operations that may fail, in a clean and readable way:
val forResult =
for
a <- Try(10 / 5) // This succeeds
b <- Try(a / 0) // This fails
yield b * 2
forResult match {
case Success(value) => println(s"Result: $value")
case Failure(exception) => println(s"Error: ${exception.getMessage}")
}
In the above example, the computation automatically stops at the first failure, and forResult becomes a Failure containing the exception.
Throw
In Scala, unlike Java, you're not required to declare checked exceptions using throws in the method signature. Scala doesn't distinguish between checked and unchecked exceptions; all exceptions are unchecked, meaning the compiler does not force you to catch or declare any exceptions. However, for documentation purposes or when interfacing with Java code, you might want to indicate that a method can throw an exception.
To annotate a method with the information that it might throw an exception, you can use the @throws annotation. This can improve readability and maintainability of your Scala code, especially for developers coming from a Java background or when Scala code is being called from Java.
Here’s how to use the @throws annotation in Scala:
def divide(a: Int, b: Int): Int =
if (b == 0) then
throw new ArithmeticException("Division by zero.")
else
a / b
// Annotating the method with @throws
@throws(classOf[ArithmeticException])
def divideWithAnnotation(a: Int, b: Int): Int =
if (b == 0) then
throw new ArithmeticException("Division by zero.")
else
a / b
In this example, the divideWithAnnotation method is explicitly annotated to indicate that it might throw an ArithmeticException. The @throws annotation takes the class of the exception you're warning about as a parameter.
This annotation is particularly useful when Scala methods are invoked from Java code, as it will inform Java developers about the potential exceptions, allowing them to handle these exceptions appropriately.
Remember, while the @throws annotation can be helpful for documentation and interoperability with Java, it does not change how Scala code behaves or is compiled. Scala treats all exceptions as unchecked, and the use of @throws is purely informational.
Exercises
Exercise 1: Add two Option number
def addOptions(optA: Option[Int], optB: Option[Int]): Option[Int]
Use pattern matching and for comprehension
(Extra) Use map and flatMap
Exercise 2: Read a file
Read from file
with try-catch
def readFileWithTryCatch(filePath: String): String
with Try, Success and Failure
def readFileWithTry(filePath: String): Try[String]
Exercise 3: Login with password check
Implement a simple login function using Either.
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.
Quiz
Question 1: Basic try-catch Usage
Consider the following Scala code snippet demonstrating basic usage of try-catch:
try {
val result = 10 / 0
println(result)
} catch {
case e: ArithmeticException => println("Cannot divide by zero.")
} finally {
println("Operation attempted.")
}
What is the output of this code?
A) Cannot divide by zero.
Operation attempted.
B) Operation attempted.
C) 10 / 0
Operation attempted.
D) Compilation error
Question 2: Catching Multiple Exceptions
Analyze the Scala code snippet below that catches multiple types of exceptions:
try {
val arr = Array(1, 2, 3)
println(arr(10))
} catch {
case e: ArithmeticException => println("Arithmetic exception caught.")
case e: ArrayIndexOutOfBoundsException => println("Array index out of bounds.")
} finally {
println("Search attempted.")
}
What does this code print?
A) Arithmetic exception caught.
Search attempted.
B) Array index out of bounds.
Search attempted.
C) Search attempted.
D) Compilation error
Question 3: Using try-catch with a Return Value
Review the Scala code snippet involving try-catch with a return value:
def safeDivide(a: Int, b: Int): String = {
try {
val result = a / b
s"Result: $result"
} catch {
case e: ArithmeticException => "Error: Division by zero."
} finally {
println("Division operation processed.")
}
}
println(safeDivide(10, 0))
println(safeDivide(10, 5))
What is the output of this program?
A) Division operation processed.
Error: Division by zero.
Division operation processed.
Result: 2
B) Error: Division by zero.
Result: 2
C) Division operation processed.
Division operation processed.
D) Compilation error
Question 4: Using Option for Safe Value Access
Consider the following Scala code snippet demonstrating the use of Option to safely handle potentially missing values:
def safeFindFirstChar(str: String): Option[Char] = {
if (str.isEmpty) None
else Some(str.charAt(0))
}
val charA = safeFindFirstChar("apple")
println(charA)
val charB = safeFindFirstChar("")
println(charB)
What is the output of this code?
A) Some(a)
None
B) a
None
C) Some(a)
""
D) None of the above
Question 5: Handling Multiple Errors with Either
Analyze the Scala code snippet below that uses Either to handle multiple types of errors:
def divide(x: Int, y: Int): Either[String, Int] = {
if (y == 0) Left("Cannot divide by zero.")
else Right(x / y)
}
val result1 = divide(10, 2)
println(result1)
val result2 = divide(10, 0)
println(result2)
What does this code print?
A) Right(5)
Left("Cannot divide by zero.")
B) 5
Cannot divide by zero.
C) Right(5)
Right("Cannot divide by zero.")
D) None of the above
Question 6: Graceful Error Handling with Try
Review the Scala code snippet involving Try for graceful error handling:
import scala.util.{Try, Success, Failure}
def toInt(s: String): Try[Int] = Try(s.toInt)
val num1 = toInt("100")
println(num1)
val num2 = toInt("abc")
println(num2)
What is the output of this program?
A) Success(100)
Failure(java.lang.NumberFormatException)
B) 100
Error
C) Success(100)
Failure
D) None of the above
Question 7: Combining Option Values with flatMap
Consider the following Scala code snippet that demonstrates combining Option values using flatMap:
def parsePositiveInt(str: String): Option[Int] = {
val i = Try(str.toInt).toOption
i.flatMap { case x if x > 0 => Some(x) case _ => None }
}
val numA = parsePositiveInt("5")
println(numA)
val numB = parsePositiveInt("-5")
println(numB)
What does this code print?
A) Some(5)
None
B) 5
-5
C) Some(5)
Some(-5)
D) None of the above
Question 8: Using for-comprehension with Option
Analyze the Scala code snippet for using for-comprehension to chain operations on Option values:
def findPerson(id: Int): Option[String] = Some("John Doe")
def findAge(person: String): Option[Int] = Some(30)
val personAge = for {
person <- findPerson(1)
age <- findAge(person)
} yield age
println(personAge)
What is the output of this code?
A) Some(30)
B) 30
C) None
D) None of the above
Question 9: Safely Accessing Potentially Null Objects
Consider the following Scala code snippet demonstrating a safe way to access potentially null objects:
case class User(name: String, age: Option[Int])
def getUserAge(user: User): String = {
user.age match {
case Some(age) => s"User age: $age"
case None => "Age not provided"
}
}
val userWithAge = User("Alice", Some(30))
println(getUserAge(userWithAge))
val userWithoutAge = User("Bob", None)
println(getUserAge(userWithoutAge))
What is the output of this code?
A) User age: 30
Age not provided
B) User age: Some(30)
Age not provided
C) User age: 30
User age: None
D) None of the above
Question 10: Avoiding Nulls with Option in Method Returns
Analyze the Scala code snippet below that uses Option in method returns to avoid nulls:
def findUsername(userId: Int): Option[String] = {
if (userId == 1) Some("JohnDoe")
else None
}
val username1 = findUsername(1).getOrElse("Unknown")
println(username1)
val username2 = findUsername(2).getOrElse("Unknown")
println(username2)
What does this code print?
A) JohnDoe
Unknown
B) Some(JohnDoe)
Unknown
C) JohnDoe
None
D) None of the above
Question 11: Pattern Matching with Options to Handle Nulls
Consider the following Scala code snippet that uses pattern matching with options to handle potential null values:
def getDescription(word: Option[String]): String = {
word match {
case Some(w) => s"Found word: $w"
case None => "No word found"
}
}
val word1 = Some("Scala")
println(getDescription(word1))
val word2: Option[String] = None
println(getDescription(word2))
What does this code print?
A) Found word: Scala
No word found
B) Found word: Some(Scala)
No word found
C) Scala
None
D) None of the above
Question 12: Encapsulating Nullable References with Options
Analyze the Scala code snippet for encapsulating nullable references within Option to prevent NullPointerExceptions:
def getLength(s: Option[String]): Int = {
s.getOrElse("").length
}
val str1: Option[String] = Some("Hello")
println(getLength(str1))
val str2: Option[String] = None
println(getLength(str2))
What is the output of this code?
A) 5
0
B) Some(5)
None
C) 5
Some(0)
D) None of the above
Question 13: Creating and Accessing a Map
Consider the following Scala code snippet demonstrating how to create and access elements of a Map:
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo", "India" -> "New Delhi")
println(capitals.get("France"))
println(capitals.getOrElse("Germany", "Not found"))
What is the output of this code?
A) Some(Paris)
Not found
B) Paris
Not found
C) Some(Paris)
Some(Not found)
D) None of the above
Question 14: Updating and Adding Elements to a Map
Analyze the Scala code snippet below that updates and adds elements to an immutable Map:
var numbers = Map(1 -> "one", 2 -> "two")
numbers = numbers + (3 -> "three")
numbers = numbers.updated(2, "TWO")
println(numbers)
What does this code print?
A) Map(1 -> one, 2 -> TWO, 3 -> three)
B) Map(1 -> "one", 2 -> "two", 3 -> "three")
C) Map(1 -> "one", 2 -> "TWO", 3 -> "three")
D) Compilation error
Question 15: Iterating Over a Map
Review the Scala code snippet involving iterating over a Map:
val ages = Map("Alice" -> 30, "Bob" -> 25, "Charlie" -> 28)
for ((name, age) <- ages) {
println(s"$name is $age years old")
}
What is the output of this program?
A) Alice is 30 years old
Bob is 25 years old
Charlie is 28 years old
B) Alice: 30
Bob: 25
Charlie: 28
C) Compilation error
D) None of the above
Question 16: Using Options for Safe Access
Examine the Scala code snippet involving Option:
val maybeNum: Option[Int] = Some(5)
val result = maybeNum match {
case Some(n) => n * 2
case None => 0
}
println(result)
What is the output of this code?
A) 5
B) 10
C) 0
D) None
Question 17: Handling Exceptions with Try and Match
Review the following Scala code snippet for exception handling:
import scala.util.{Try, Success, Failure}
def divide(a: Int, b: Int): Try[Int] = Try(a / b)
val result = divide(10, 0) match {
case Success(value) => s"Result: $value"
case Failure(exception) => s"Error: ${exception.getMessage}"
}
println(result)
What is the output of this code?
A) Result: 10
B) Error: / by zero
C) Error: ArithmeticException
D) Compilation error
Question 18: Using flatMap to Flatten and Transform
Consider the following Scala code snippet that demonstrates the use of the flatMap combinator:
val lists = List(List(1, 2, 3), List(4, 5), List(6))
val flattenedAndDoubled = lists.flatMap(numbers => numbers.map(_ * 2))
println(flattenedAndDoubled)
What does this code print?
A) List(2, 4, 6, 8, 10, 12)
B) List(List(2, 4, 6), List(8, 10), List(12))
C) List(1, 2, 3, 4, 5, 6)
D) None of the above
Question 19: Folding a List with foldLeft
Analyze the Scala code snippet for folding a list:
val numbers = List(1, 2, 3, 4, 5)
val sum = numbers.foldLeft(0)((acc, number) => acc + number)
println(sum)
What is the output of this code?
A) 15
B) 10
C) 5
D) None of the above
Question 20: Basic Usage of zip
Consider the following Scala code snippet demonstrating basic usage of the zip function:
val names = List("Alice", "Bob", "Charlie")
val ages = List(25, 30, 28)
val people = names.zip(ages)
println(people)
What is the output of this code?
A) List(("Alice", 25), ("Bob", 30), ("Charlie", 28))
B) List(Alice25, Bob30, Charlie28)
C) [("Alice", 25), ("Bob", 30), ("Charlie", 28)]
D) None of the above
Question 21: zip with Unequal Collection Sizes
Analyze the Scala code snippet below that uses the zip function with collections of unequal sizes:
val numbers = List(1, 2, 3, 4)
val letters = List('a', 'b', 'c')
val zippedList = numbers.zip(letters)
println(zippedList)
What does this code print?
A) List((1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'))
B) List((1, 'a'), (2, 'b'), (3, 'c'))
C) List((1, 'a'), (2, 'b'), (3, 'c'), (4, null))
D) Compilation error
Question 22: Using zipWithIndex
Review the Scala code snippet involving the zipWithIndex function:
val fruits = List("apple", "banana", "cherry")
val indexedFruits = fruits.zipWithIndex
println(indexedFruits)
What is the output of this program?
A) List(("apple", 0), ("banana", 1), ("cherry", 2))
B) List(("apple", 1), ("banana", 2), ("cherry", 3))
C) [("apple", 0), ("banana", 1), ("cherry", 2)]
D) None of the above
Question 23: Basic Usage of groupBy
Consider the following Scala code snippet demonstrating the basic usage of the groupBy function:
val words = List("apple", "banana", "pear", "apricot", "peach")
val groupedByFirstLetter = words.groupBy(word => word.head)
println(groupedByFirstLetter)
What is the output of this code?
A) Map('a' -> List("apple", "apricot"), 'b' -> List("banana"), 'p' -> List("pear", "peach"))
B) Map(a -> List("apple", "apricot"), b -> List("banana"), p -> List("pear", "peach"))
C) List("apple", "banana", "pear", "apricot", "peach")
D) None of the above
Correct Answer: B) Map(a -> List("apple", "apricot"), b -> List("banana"), p -> List("pear", "peach"))
Question 24: Higher-Order Functions
Analyze this Scala code snippet that demonstrates a higher-order function:
def performOperation(x: Int, operation: Int => Int): Int = {
operation(x)
}
val result = performOperation(5, y => y * y)
println(result)
What does this code print?
A) 10
B) 25
C) 5
D) None of the above
Question 25: Simple Recursive Function for Summation
Consider the Scala code snippet below for calculating the sum of numbers from 1 to N:
def sumToN(n: Int): Int = {
if (n <= 0) 0
else n + sumToN(n - 1)
}
val sum10 = sumToN(10)
println(sum10)
val sum5 = sumToN(5)
println(sum5)
What is the output of this code?
A) 55 and 15
B) 50 and 15
C) 55 and 10
D) 45 and 10
Exercises
Below are three exercises designed to help you practice writing tests using ScalaTest. These exercises cover different testing styles and use cases, including unit testing, property-based testing, and integration testing.
Exercise 1: Unit Testing with FunSuite
Objective: Write unit tests for a StringUtility object that provides a method to reverse a string and another method to check if a string is a palindrome.
- Implement the
StringUtilityobject:
object StringUtility {
def reverse(s: String): String = s.reverse
def isPalindrome(s: String): Boolean = s == s.reverse
}
- Write tests using
FunSuite: Create a test classStringUtilityTestusingFunSuiteto test both methods. Test normal strings, empty strings, and palindromes.
Exercise 2: Testing a Shopping Cart
Objective: Write tests for a ShoppingCart class that supports adding items, removing items, and calculating the total price. The cart should handle quantities for items and provide an option to apply a discount code.
- Implement the
ShoppingCartandItemclasses:
case class Item(id: String, name: String, price: Double, quantity: Int)
class ShoppingCart {
private var items: Map[String, Item] = Map()
def addItem(item: Item): Unit = {
items = items.get(item.id) match {
case Some(existingItem) => items.updated(item.id, existingItem.copy(quantity = existingItem.quantity + item.quantity))
case None => items + (item.id -> item)
}
}
def removeItem(itemId: String): Unit = {
items -= itemId
}
def applyDiscount(code: String): Unit = {
val discountRate = code match {
case "DISCOUNT10" => 0.1
case _ => 0.0
}
items = items.view.mapValues(item => item.copy(price = item.price * (1 - discountRate))).toMap
}
def total: Double = {
items.values.map(item => item.price * item.quantity).sum
}
}
- Write tests to verify:
- Adding and removing items updates the cart correctly.
- The total price is calculated correctly, with and without a discount code.
Exercise 3: Testing a Password Validator
Objective: Write tests for a PasswordValidator object that checks if a password meets certain criteria: it must be at least 8 characters long, contain both uppercase and lowercase letters, and at least one number.
- Implement the
PasswordValidator:
object PasswordValidator {
def isValid(password: String): Boolean = {
password.length >= 8 &&
password.exists(_.isUpper) &&
password.exists(_.isLower) &&
password.exists(_.isDigit)
}
}
- Write tests to verify:
- A password meeting all criteria is valid.
- Passwords missing each specific criterion are invalid.
- Edge cases, such as an empty string or a password with only spaces.
Exercise 4: Property-Based Testing for a String Concatenation Utility
Objective: Use ScalaCheck to write property-based tests for a StringConcatenationUtility that concatenates two strings with a space between them.
- Implement the
StringConcatenationUtility:
object StringConcatenationUtility {
def concatenate(str1: String, str2: String): String = s"$str1 $str2"
}
- Write property-based tests to verify:
- Concatenating two non-empty strings results in a string that contains both, separated by a space.
- Concatenating an empty string with any string returns the other string with a leading or trailing space.
- The length of the result is the sum of the lengths of the two strings plus one (for the space).
Exercise 5: Testing a Fibonacci Number Generator
Objective: Write tests for a FibonacciGenerator that generates the nth Fibonacci number. Consider both iterative and recursive implementations.
- Implement the
FibonacciGenerator:
object FibonacciGenerator {
def fibonacci(n: Int): Int = n match {
case 0 | 1 => n
case _ => fibonacci(n - 1) + fibonacci(n - 2)
}
}
- Write tests to verify:
- Correct Fibonacci numbers are generated for known values (e.g., 0, 1, 1, 2, 3, 5, 8, 13, ...).
- Negative numbers return 0 or an appropriate error/exception.
- Performance or stack overflow issues for larger
n(e.g.,n = 50), if applicable.
Exercise 6: Integration Testing for a File Processing Utility
Objective: Write integration tests for a FileProcessingUtility that reads a file, processes its content, and writes the result to another file. Assume the processing involves converting all text to uppercase.
- Implement the
FileProcessingUtility(simplified for exercise):
import scala.io.Source
import java.nio.file.{Files, Paths}
object FileProcessingUtility {
def processFile(inputPath: String, outputPath: String): Unit = {
val source = Source.fromFile(inputPath)
val content = source.getLines().mkString("\n").toUpperCase
source.close()
Files.write(Paths.get(outputPath), content.getBytes)
}
}
- Write tests to verify:
- Correct processing of an input file with known content.
- Handling of non-existent input files.
- The output file is created in the specified location with the expected processed content.
Project
Todo List
In this project we will add tests tp the todolist application
Create a class
TodoItem with a field
taskpriority
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
Write test on all the functions.
Shopping basket
In this project we will create a shoppingbasket application
Create the classes
Article with the fields
namepriceamount
ShoppingBasket with the methods
add- give an error if an article already existslist- give an error if the list has no itemsdelete- give an error if an item does not exist
Add filter function on prices between an low and high value
Write tests on all the functions.
Exercises
- Hello World
- Create a Twirl template that accepts a string parameter
nameand displays "Hello,name!" in an HTML paragraph tag.
- List Rendering
- Make a Twirl template that takes a List of strings and renders each string in an unordered list (
<ul>). Ensure to handle an empty list gracefully by displaying a friendly message.
- Form Submission Display
- Develop a form in Twirl that posts a user's first and last name to a Play controller. Then, display the submitted names on a new page using another Twirl template.
- Conditional Content
- Design a Twirl template that accepts a Boolean value. Depending on the value, it should display different messages or HTML elements.
- Nested Templates
- Create a main layout template that includes a header, footer, and a placeholder for content. Then, create a child template that extends this layout and populates the content area with specific information.
- Loop with Conditional
- Write a Twirl template that takes a list of integers. Display each number in a list item (
<li>). Highlight numbers that are divisible by 3 by making them bold.
Tips for Getting Started
- To add a Twirl template to your Play project, create a file with the
.scala.htmlextension in theapp/viewsdirectory. - Pass data to your Twirl templates by adding parameters to the template function declaration. For example, to pass a name to your template, start the file with
@(name: String). - Use Scala control structures directly in your templates for loops and conditionals.
Project: Shopping Basket
Implement the ShoppingBasket from previous chapters in Play
Step1: With database
Step2: With Anorm
Step3: With Slick
Step4: Rest API
Step5: Error Handling
Step6: Add Bootstrap
Step7: More and more
Exercises
Working with untyped actors in Akka (a popular toolkit for building concurrent, distributed, and fault-tolerant systems on the JVM) is a great way to understand actor-based concurrency. Below are a few exercises that can help you practice working with untyped actors.
Exercise 1: Simple Ping-Pong
Objective: Implement two actors that send messages to each other.
- Actor 1 (PingActor): Sends a "Ping" message to Actor 2.
- Actor 2 (PongActor): Responds with a "Pong" message when it receives a "Ping".
- PingActor: Upon receiving "Pong", sends another "Ping" after a short delay.
Task:
- Implement the
PingActorandPongActorclasses. - Ensure the actors can send messages back and forth indefinitely.
Exercise 2: Counter Actor
Objective: Implement an actor that maintains a state.
- CounterActor: This actor should maintain an integer counter. It should handle the following messages:
- "Increment": Increments the counter by 1.
- "Decrement": Decrements the counter by 1.
- "Get": Replies with the current value of the counter.
Task:
- Implement the
CounterActor. - Create a main method where you create an instance of
CounterActor, send several messages to it, and print out the final counter value.
Exercise 3: Actor Hierarchy and Supervision
Objective: Understand actor hierarchy and supervision.
- ParentActor: This actor creates a child actor (
ChildActor). It supervises the child and restarts it when it fails. - ChildActor: This actor might occasionally throw an exception when it receives a "DoWork" message.
Task:
- Implement the
ParentActorandChildActor. - Set up supervision such that the
ParentActorrestarts theChildActorupon failure. - Create a scenario where the
ChildActoroccasionally fails, and observe how supervision works.
Exercise 4: Bank Account Actor
Objective: Model a simple bank account system using actors.
- BankAccountActor: This actor should handle the following messages:
- "Deposit(amount)": Adds the specified amount to the account balance.
- "Withdraw(amount)": Subtracts the specified amount if there are sufficient funds.
- "GetBalance": Replies with the current balance.
Task:
- Implement the
BankAccountActor. - Create a main method where you create an instance of
BankAccountActor, perform several deposits and withdrawals, and then print the final balance.
Exercise 5: Master-Worker Pattern
Objective: Implement a simple Master-Worker pattern using actors.
- MasterActor: Distributes tasks among several worker actors and collects the results.
- WorkerActor: Performs a computation and returns the result to the master.
Task:
- Implement the
MasterActorandWorkerActor. - The
MasterActorshould divide a list of integers into sublists and send each sublist to a worker actor to compute the sum. - Collect and print the final sum of all integers.
Todo App
Creating a simple Todo app using Akka's untyped actors can be an excellent way to demonstrate how to manage state, handle concurrency, and build a basic application using Akka's actor model. Below, I will walk you through the process of building a Todo app with the following functionalities:
- Add a new Todo item.
- Remove a Todo item.
- Mark a Todo item as completed.
- List all Todo items.
1. Setting Up the Project
Start by setting up your Scala project with the necessary Akka dependencies in your build.sbt:
libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.x"
2. Defining the Messages
First, define the messages that actors will use to communicate. Messages are immutable case classes:
// Messages
case class AddTodo(id: Int, task: String)
case class RemoveTodo(id: Int)
case class CompleteTodo(id: Int)
case object GetTodos
3. Creating the Todo Actor
The TodoActor will manage the list of Todo items. It will handle messages to add, remove, complete, and retrieve Todo items:
import akka.actor.{Actor, ActorSystem, Props}
// Todo Item case class
case class TodoItem(id: Int, task: String, completed: Boolean = false)
class TodoActor extends Actor {
var todos: Map[Int, TodoItem] = Map()
def receive: Receive = {
case AddTodo(id, task) =>
todos += id -> TodoItem(id, task)
println(s"Added Todo: $task")
case RemoveTodo(id) =>
todos.get(id) match {
case Some(todo) =>
todos -= id
println(s"Removed Todo: ${todo.task}")
case None =>
println(s"Todo with id $id not found.")
}
case CompleteTodo(id) =>
todos.get(id) match {
case Some(todo) =>
todos += id -> todo.copy(completed = true)
println(s"Completed Todo: ${todo.task}")
case None =>
println(s"Todo with id $id not found.")
}
case GetTodos =>
if (todos.isEmpty) {
println("No Todos available.")
} else {
todos.values.foreach { todo =>
val status = if (todo.completed) "Completed" else "Pending"
println(s"${todo.id}: ${todo.task} [$status]")
}
}
}
}
4. Creating the Main Application
Now, let's create a main application that will instantiate the ActorSystem, create the TodoActor, and interact with it:
object TodoApp extends App {
// Create Actor System
val system = ActorSystem("TodoSystem")
// Create TodoActor
val todoActor = system.actorOf(Props[TodoActor], "todoActor")
// Interacting with the TodoActor
todoActor ! AddTodo(1, "Buy milk")
todoActor ! AddTodo(2, "Go to the gym")
todoActor ! GetTodos
todoActor ! CompleteTodo(1)
todoActor ! GetTodos
todoActor ! RemoveTodo(2)
todoActor ! GetTodos
// Shutdown the Actor System
system.terminate()
}
5. Running the Application
To run the application, simply execute the TodoApp object in your Scala environment. The output will be:
Added Todo: Buy milk
Added Todo: Go to the gym
1: Buy milk [Pending]
2: Go to the gym [Pending]
Completed Todo: Buy milk
1: Buy milk [Completed]
2: Go to the gym [Pending]
Removed Todo: Go to the gym
1: Buy milk [Completed]
No Todos available.
Todo App
Creating a simple Todo application using Akka actors in Scala involves several steps, including defining the messages that actors will use for communication, creating the actor behaviors, and setting up the actor system. This application will consist of a main actor, TodoManager, which manages todo items, and a simplified interface for adding and listing todos.
Step 1: Define the Messages
First, define the messages that will be used for interaction. In a Todo app, we typically need to add todos and list all existing todos.
sealed trait Command
final case class AddTodo(description: String) extends Command
final case object ListTodos extends Command
final case class Todos(items: List[String])
Step 2: Define the TodoManager Actor
The TodoManager actor will handle AddTodo and ListTodos messages. It maintains a list of todo items in its state.
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
object TodoManager {
def apply(): Behavior[Command] = manageTodos(Nil)
private def manageTodos(todos: List[String]): Behavior[Command] =
Behaviors.receive { (context, message) =>
message match {
case AddTodo(description) =>
context.log.info(s"Adding todo: $description")
manageTodos(todos :+ description)
case ListTodos =>
context.log.info(s"Current todos: $todos")
Behaviors.same
}
}
}
Step 3: Set Up the Actor System and Interaction
Now, create the ActorSystem, and demonstrate adding some todos and listing them.
import akka.actor.typed.ActorSystem
object TodoApp extends App {
val system: ActorSystem[Command] = ActorSystem(TodoManager(), "todoSystem")
system ! AddTodo("Learn Akka Actors")
system ! AddTodo("Build a TodoApp")
system ! ListTodos
// Shutdown the actor system after a delay to see the output
import scala.concurrent.duration._
import system.executionContext
system.scheduler.scheduleOnce(2.seconds) {
system.terminate()
}
}
Running the Application
When you run this application, it will:
- Create an
ActorSystemwith theTodoManageractor. - Send
AddTodomessages to add new todos. - Send a
ListTodosmessage to print out all current todos. - Finally, it schedules a system termination after a short delay so you can see the output before the application exits.
Shopping Basket
To convert the Shopping Basket system from Akka typed actors to untyped actors, we need to modify the code so that it aligns with the untyped actor model in Akka. Below is the step-by-step process to achieve this.
Step 1: Define Messages and Data Models
The message and data model definitions remain mostly the same, but we'll omit the sealed trait for commands, as untyped actors don't enforce the use of sealed trait in the same way.
// Messages
case class AddItem(item: Item)
case object Checkout
// Data Models
case class Item(name: String, price: BigDecimal)
case class Receipt(items: List[Item], total: BigDecimal)
Step 2: Define the BasketActor
Now, let's define the BasketActor. In untyped actors, we extend the Actor trait and implement the receive method. The actor's state can be managed as mutable variables inside the actor.
import akka.actor.{Actor, ActorLogging, Props}
class BasketActor extends Actor with ActorLogging {
var items: List[Item] = List.empty
override def receive: Receive = {
case AddItem(item) =>
log.info(s"Item added to basket: ${item.name}")
items = items :+ item
case Checkout =>
val total = items.map(_.price).sum
log.info(s"Checking out with total: $total")
// In a real application, you might want to send the receipt to another actor or persist it.
context.stop(self)
}
}
object BasketActor {
def props(): Props = Props(new BasketActor)
}
Step 3: Set Up the Actor System and Test
Finally, let's set up the ActorSystem, create the BasketActor, and send messages to test the functionality. The main difference in untyped actors is that we use system.actorOf to create actors and ! (tell) to send messages.
import akka.actor.{ActorSystem, Props}
object ShoppingApp extends App {
// Create Actor System
val system = ActorSystem("shoppingSystem")
// Create BasketActor
val basketActor = system.actorOf(BasketActor.props(), "basketActor")
// Interact with the BasketActor
basketActor ! AddItem(Item("Apple", BigDecimal("0.60")))
basketActor ! AddItem(Item("Banana", BigDecimal("0.40")))
basketActor ! Checkout
// Shutdown the actor system after a short delay to see the output.
Thread.sleep(1000) // Not ideal for real applications, just for demonstration
system.terminate()
}
Explanation:
- Actor Creation: In untyped actors,
BasketActoris created usingsystem.actorOf(BasketActor.props(), "basketActor"). - Message Handling: The
receivemethod handles incoming messages by pattern matching on the message type. - State Management: State (i.e., the list of
items) is maintained as a mutable variable inside the actor. - Logging: The
ActorLoggingtrait is mixed in to provide easy access to logging within the actor.
Shopping Basket App
Creating a shopping basket system using Akka actors in Scala involves several key steps, including defining messages for adding items to the basket and checking out, as well as creating actor behaviors to handle these actions. Below is a simplified example demonstrating these concepts.
Step 1: Define Messages and Data Models
First, define the messages that will be used to interact with the actors, and any data models needed for the shopping basket.
sealed trait BasketCommand
final case class AddItem(item: Item) extends BasketCommand
final case object Checkout extends BasketCommand
final case class Item(name: String, price: BigDecimal)
final case class Receipt(items: List[Item], total: BigDecimal)
Step 2: Define the BasketActor
The BasketActor will maintain a list of items as the state and handle AddItem and Checkout messages.
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
object BasketActor {
def apply(): Behavior[BasketCommand] = basket(List.empty)
private def basket(items: List[Item]): Behavior[BasketCommand] =
Behaviors.receive { (context, message) =>
message match {
case AddItem(item) =>
context.log.info(s"Item added to basket: ${item.name}")
basket(items :+ item)
case Checkout =>
val total = items.map(_.price).sum
context.log.info(s"Checking out with total: $total")
// In a real application, you might want to send the receipt to another actor or persist it.
Behaviors.stopped
}
}
}
Step 3: Set Up the Actor System and Test
Now, you can set up an ActorSystem, create a BasketActor, and send messages to test the functionality.
import akka.actor.typed.ActorSystem
object ShoppingApp extends App {
val system: ActorSystem[BasketCommand] = ActorSystem(BasketActor(), "shoppingSystem")
system ! AddItem(Item("Apple", BigDecimal("0.60")))
system ! AddItem(Item("Banana", BigDecimal("0.40")))
system ! Checkout
// Shutdown the actor system after a short delay to see the output.
Thread.sleep(1000) // Not ideal for real applications, just for demonstration
system.terminate()
}
Considerations for a Real Application
- Persistence: For a production system, you would want to persist the shopping basket's state using Akka Persistence to handle failures and system restarts without data loss.
- Security: Ensure that each shopping basket is associated with a specific user session or account.
- Concurrency Handling: Be mindful of concurrent modifications if your system allows for simultaneous updates to the basket from the same user.
- Scalability: Consider how your actors will be distributed in a clustered environment to handle scaling requirements.
- Integration: Think about how this system will integrate with other components, like inventory management, payment processing, and user authentication.
Exercises
Exercise 1: Hello Actor
Task: Create an actor named HelloActor that receives a String message and prints out Hello, [message]!.
Exercise 2: Counter Actor
Task: Implement a CounterActor that can receive messages to increment, decrement, and print its internal count.
Exercise 3: Ping-Pong Actors
Task: Create two actors, PingActor and PongActor. PingActor should send a ping message to PongActor, and PongActor should respond with a pong message.
Projects
Todo List
In this project we will create a todolist application with Akka Actors
Creata a class
TodoItem with a field
task
TodoList with methods
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
Todo App
Creating a Todo application that leverages Scala's Futures and Promises involves handling tasks asynchronously. This is particularly useful for operations that might take a long time, such as database interactions. In this simplified example, we'll implement a basic Todo app with asynchronous add and get functionalities.
Step 1: Setting Up the Environment
First, ensure you have the Scala execution context in scope for running your futures:
import scala.concurrent.{Future, ExecutionContext, Promise}
import scala.util.{Success, Failure}
import scala.concurrent.ExecutionContext.Implicits.global
Step 2: Define the Todo Model and Storage
Define a simple Todo model and a mock asynchronous storage for todos. In a real application, this storage might interact with a database.
case class Todo(id: Int, task: String, isCompleted: Boolean)
object TodoStorage {
private var todos: List[Todo] = List()
// Asynchronous add method
def add(todo: Todo): Future[Unit] = Future {
Thread.sleep(1000) // Simulate database delay
todos = todo :: todos
}
// Asynchronous get method
def get(id: Int): Future[Option[Todo]] = Future {
Thread.sleep(1000) // Simulate database delay
todos.find(_.id == id)
}
}
Step 3: Implement the TodoService
The TodoService will interact with the TodoStorage. It includes error handling and demonstrates how to use a Promise to create a custom future that might be completed outside the usual flow.
object TodoService {
def addTask(task: String): Future[Int] = {
val todo = Todo(
id = TodoStorage.todos.length + 1, // Simple ID generation strategy
task = task,
isCompleted = false
)
for {
_ <- TodoStorage.add(todo)
} yield todo.id
}
def completeTask(id: Int): Future[Boolean] = {
val promise = Promise[Boolean]()
TodoStorage.get(id).onComplete {
case Success(Some(todo)) =>
TodoStorage.add(todo.copy(isCompleted = true)).onComplete {
case Success(_) => promise.success(true)
case Failure(_) => promise.failure(new Exception("Failed to update the todo."))
}
case Success(None) => promise.failure(new NoSuchElementException("Todo not found."))
case Failure(exception) => promise.failure(exception)
}
promise.future
}
}
Step 4: Usage Example
Here’s how you might use TodoService to add and complete tasks:
val added = TodoService.addTask("Learn Scala Futures and Promises")
added.onComplete {
case Success(id) =>
println(s"Added todo with id: $id")
TodoService.completeTask(id).onComplete {
case Success(_) => println(s"Todo $id completed.")
case Failure(exception) => println(s"Error completing todo: ${exception.getMessage}")
}
case Failure(exception) => println(s"Error adding todo: ${exception.getMessage}")
}
Shopping Basket
Creating a shopping basket application with Futures and Promises in Scala involves handling asynchronous operations, such as querying a database or making network requests, to add, remove, and fetch items in the shopping basket. Here's a simplified version of such an application.
Step 1: Setting Up the Environment
First, make sure to import necessary Scala concurrent execution contexts and other utilities:
import scala.concurrent.{Future, Promise, ExecutionContext}
import scala.util.{Success, Failure}
import scala.concurrent.ExecutionContext.Implicits.global
Step 2: Define the Models
Define a simple Item model and the ShoppingBasket class with asynchronous methods:
case class Item(id: String, name: String, price: Double, quantity: Int = 1)
class ShoppingBasket {
private var items: Map[String, Item] = Map()
def addItem(item: Item): Future[Unit] = Future {
// Simulate a delay, e.g., database operation
Thread.sleep(100)
items.get(item.id) match {
case Some(existingItem) =>
items = items.updated(item.id, existingItem.copy(quantity = existingItem.quantity + item.quantity))
case None =>
items += (item.id -> item)
}
}
def removeItem(itemId: String): Future[Unit] = Future {
// Simulate a delay
Thread.sleep(100)
items -= itemId
}
def getTotal: Future[Double] = Future {
// Simulate a computation delay
Thread.sleep(100)
items.values.map(item => item.price * item.quantity).sum
}
}
Step 3: Implementing Asynchronous Operations
Here's how you might interact with the ShoppingBasket asynchronously, demonstrating adding items, removing an item, and calculating the total:
val basket = new ShoppingBasket()
val itemFutures = Future.sequence(Seq(
basket.addItem(Item("1", "Apple", 0.60, 2)),
basket.addItem(Item("2", "Banana", 0.40, 3)),
basket.addItem(Item("3", "Carrot", 0.25, 4))
))
itemFutures.onComplete {
case Success(_) =>
println("Items added successfully.")
basket.getTotal.onComplete {
case Success(total) =>
println(s"Total before removal: $total")
basket.removeItem("2").onComplete {
case Success(_) =>
basket.getTotal.onComplete {
case Success(newTotal) => println(s"Total after removal: $newTotal")
case Failure(e) => println(s"Failed to get total after removal: ${e.getMessage}")
}
case Failure(e) => println(s"Failed to remove item: ${e.getMessage}")
}
case Failure(e) => println(s"Failed to get total: ${e.getMessage}")
}
case Failure(e) => println(s"Failed to add items: ${e.getMessage}")
}
Step 4: Error Handling and Promises
Using Promises is particularly useful when you need more control over the completion of a future, for example, in complex error handling or when integrating with callback-based APIs.
Let's say you have a method to apply a discount code, which is unpredictable and requires a Promise for better control:
def applyDiscount(code: String): Future[Double] = {
val promise = Promise[Double]()
// Simulate validating discount code asynchronously
Future {
Thread.sleep(100) // Simulate delay
if (code == "DISCOUNT10") promise.success(0.1) // 10% discount
else promise.failure(new IllegalArgumentException("Invalid discount code"))
}
promise.future.flatMap { discountRate =>
getTotal.map(total => total * (1 - discountRate))
}
}
In this example, applyDiscount uses a Promise to manually complete a future based on a discount code validation operation. It then uses the result to calculate the discounted total.
Exercises
Here are three exercises focusing on Scala Futures to practice asynchronous programming. These exercises cover different aspects of working with Futures, including basic usage, composition, error handling, and integration with external services or databases.
Exercise 1: Asynchronous Data Processing
Objective: Implement an asynchronous method that processes a list of integers. The processing should square each number and then return the sum of all squared numbers. Use Futures to perform the squaring operations in parallel.
Task:
- 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.
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.
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.
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
Exercise 1
Extension Methods
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
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
A type class is the functional equivalent of polymorphism in the object-oriented world.
And it is resolved in compile-time by importing context givens
The Object-Oriented Way
trait Show:
def show: String
case class Person(name: String, age: Int) extends Show:
override def show = s"$name with age $age"
val john = Person("John", 28 )
val showJohn = john.show
- available only for the types we
extend - provide only one
overrideimplementation
The Pattern Matching Way
def show(value: Any): String = value match
case Person(name, age) => s"$name with age $age"
case _ => throw new IllegalArgumentException("not supported")
- lose type safety
- extends match for every pattern
- has one
caseimplementation
Type Classes
Create a type class has the following steps
- Type Class definition
- Type Class instances
- the API
- Extension methods
Type Class Definition
trait Shower[T] {
def show(value: T): String
}
Type Class Instances
given userShower: Shower[Person] with
override def show(value: Person) =
val Person(name, age) = value
s"$name with age $age"}
val john = Person("John", 28)
val showJohn = userShower.show(john)
API
object Shower {
def show[T](value: T)(using shower: Shower[T]): String =
shower.show(value)
def apply[T](using shower: Shower[T]): Shower[T] = shower
}
val showJohn = Shower.show(john)
Extension method
object ShowSyntax:
extension [T](value: T)
def show(using shower: Shower[T]): String = shower.show(value)
import ShowSyntax.*
val showJohn = john.show
- can define showers for other types
- multiple shower for the same type
Example Dog ad Cat
In Scala 3, the mechanism for defining and using type classes has been significantly revamped with the introduction of given and using clauses, replacing the older implicit keyword. This change aims to make the definition and usage of type classes more explicit and readable, addressing some of the common criticisms of Scala's implicit system.
Defining Type Classes with given
To define a type class instance with Scala 3, you use the given keyword. This replaces the implicit val or implicit object definitions used in Scala 2.
Example: Defining a SoundMaker type class and its instances.
trait SoundMaker[T]:
def makeSound(value: T): Unit
// Define instances of the SoundMaker type class
given SoundMaker[Dog] with
def makeSound(dog: Dog): Unit = println("Woof")
given SoundMaker[Cat] with
def makeSound(cat: Cat): Unit = println("Meow")
Here, given declarations define how Dog and Cat types fulfill the SoundMaker contract. These instances are globally available and automatically used wherever a SoundMaker[T] is required.
Using Type Classes with using
To use a type class instance, Scala 3 introduces the using clause. This explicitly specifies that a function requires a type class instance for its operation, making the function's dependencies clear.
Example: Using the SoundMaker type class to implement a polymorphic playSound function.
def playSound[T](value: T)(using maker: SoundMaker[T]): Unit = maker.makeSound(value)
The using clause in the function signature tells the compiler to search for an implicit SoundMaker[T] instance for whatever type T is passed into the function. This search is based on the instances defined with given.
Putting It All Together
class Dog(val name: String)
class Cat(val name: String)
// Assuming the given instances and the playSound method are defined as above
val myDog = Dog("Rex")
val myCat = Cat("Whiskers")
playSound(myDog) // Outputs: Woof
playSound(myCat) // Outputs: Meow
Type Classes Exercises
Exercise: Json Converter
Json Converter
Creating a JSON converter using type classes in Scala allows for flexible, reusable serialization logic that can be applied to various types without requiring modifications to those types. This approach is particularly useful when working with third-party classes or when you want to keep serialization logic decoupled from domain logic. Below is a simplified example demonstrating how to implement a JSON converter using type classes in Scala 3, utilizing the given and using syntax for clarity and explicitness.
Step 1: Define the JSON Type Class
First, define a trait that represents the ability to convert a value of type T to JSON.
trait JsonConverter[T] {
def toJson(value: T): String
}
Step 2: Create Instances of the Type Class
Next, provide given instances of the JsonConverter for the types you want to support. Let's start with a few basic types like String and Int, and then create a converter for a custom class.
given JsonConverter[String] with {
def toJson(value: String): String = s""""$value""""
}
given JsonConverter[Int] with {
def toJson(value: Int): String = value.toString
}
// A sample case class for demonstration
case class Person(name: String, age: Int)
// Creating a JsonConverter for the Person case class
given JsonConverter[Person] with {
def toJson(person: Person): String =
s"""{"name": "${person.name}", "age": ${person.age}}"""
}
Step 3: Implement a Generic toJSON Function
Now, define a generic function that uses the JsonConverter type class to convert any supported type to JSON. This function will use the using clause to specify that it requires a JsonConverter for the type T.
def toJson[T](value: T)(using converter: JsonConverter[T]): String = {
converter.toJson(value)
}
Step 4: Using the JSON Converter
Finally, you can use the toJson function to serialize different types to JSON. The compiler will automatically use the appropriate given instance based on the type of the value passed to toJson.
val name = "John Doe"
val age = 30
val person = Person(name, age)
println(toJson(name)) // Outputs: "John Doe"
println(toJson(age)) // Outputs: 30
println(toJson(person)) // Outputs: {"name": "John Doe", "age": 30}
Extensibility
One of the strengths of this approach is its extensibility. You can easily add support for new types by defining new given instances of the JsonConverter type class. This does not require modifying existing code, adhering to the open/closed principle.
Monads
The railway metaphor is a popular way to explain monads in a more intuitive and less abstract manner. It helps visualize the flow of data through transformations, especially in a language like Scala, where monads play a crucial role in handling computations, side effects, and more.
Imagine a railway system where trains (data) travel from one station (function) to the next. Each station transforms the train in some way, and the tracks guide where the train goes. In a perfect world, the train goes from start to finish without any issues. However, real life (and code) involves complications like missing tracks (exceptions) or stations that can't handle the train (errors).
The Tracks: Happy Path and Error Path
The railway has two parallel tracks: the happy path and the error path.
-
Happy Path: This is where everything goes right. The train moves from one station to the next, getting transformed along the way without any issues. In Scala, this is akin to operations on monads (like
Option,Try, 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]
Cons
We put the head and tail in the constructor
case class Cons[A](override val head: A, override val tail: MyList[A]) extends MyList[A]:
override def isEmpty: Boolean = false
main
@main
def main(): Unit =
val myList: MyList[Int] = Cons(1, Cons(2, Cons(3, Empty())))
println(myList)
// Cons(1,Cons(2,Cons(3,Empty())))
MyList - step 2
ToString
Add the toString method to MyList
trait MyList[A]:
...
def toString: String
- Make a recursive version
- A Tail recursive version
- A pretty print version that prints
MyList(3,2,1)
Exercise
Implement the add method in Cons and Empty And test it in main
Recursive
We have to walk through the linked list get the head and then jump to the tail recursively. The case is the empty node.
override def toString: String =
def concat(remainder: MyList[A]): String =
if !remainder.isEmpty then
current.head.toString + " " + concat(remainder.tail)
else
""
concat(this)
Tail Recursive
In the tail recursive version we add a accumulator to the parameters. In every iteration we add the head to the accumulator And the last step in returning the accumulator
override def toString: String =
def concat(remainder: MyList[A], accumulator: String): String =
if !remainder.isEmpty then
concat(remainder.tail, accumulator + " " + remainder.head)
else
s"MyList($accumulator)"
concat(this, "")
Pretty print
To make in pretty print version we add a comma in every iteration. But then we get as many comma's as there are elements and that is one comma too much.
Iterating one less is starting the recursive loop with the tail
And the accumulator start with the head
override def toString: String =
def concat(remainder: MyList[A], accumulator: String): String =
if !remainder.isEmpty then
concat(remainder.tail, accumulator + ", " + remainder.head)
else
accumulator
val elements = concat(tail, head.toString)
s"MyList($elements)"
main
@main
def main(): Unit =
val myList: MyList[Int] = Empty() + 1 + 2 + 3
println(myList)
// 3 2 1
// MyList(3, 2, 1)
The order is reversed now.
MyList - step 3
Add method
- Add the
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)
Add method
Empty
override def add(element: A): MyList[A] = Cons(element, this)
Adding an element to Empty means that Empty is not empty anymore and becomes a Cons with the tail Empty (this)
Cons
override def add(element: A): MyList[A] = Cons(element, this)
Adding an element to Cons means adding new head and the tail becomes the current Cons (this)
trait
trait MyList[A]:
...
def add(element: A): MyList[A] = Cons(element, this)
The implementation in Empty and Cons are the same.
So we can move it up to the MyList trait (and remove them from Empty and Cons).
Via inheritance, they are available in Empty and Cons again
main
@main
def main(): Unit =
val myList: MyList[Int] = Empty().add(1).add(2).add(3)
println(myList)
// MyList(3, 2, 1)
The order is reversed now.
main
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1,2,3)
println(myList)
// MyList(1, 2, 3)
MyList - step 4
Foreach, Map and Filter
Exercise
Add the following methods to MyList
- foreach
- map
- filter
trait MyList[A]:
...
def foreach(f: A => B): Unit
def map[B](f: A => B): MyList[B]
def filter(f: A => Boolean): MyList[A]
main
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1,2,3)
myList.foreach(x => println(x + 2))
println(myList.map(x => x * 2))
println(myList.filter(x => x < 2))
// 3
// 4
// 5
// MyList(2, 4, 6)
// MyList(1)
MyList - step 5
Concat and Flatmap
We will add two new methods to MyList
- ++ (concatenation)
- flatMap
Exercise
trait MyList[A]:
...
infix def ++(other: MyList[A]): MyList[A]
def flatMap[B](f: A => MyList[B]): MyList[B]
main
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1,2,3)
val otherList = MyList(4, 5)
println( myList ++ otherList )
println( myList.flatMap(a => MyList(a, a + 1)) )
// MyList(1, 2, 3, 4, 5)
// MyList(1, 2, 2, 3, 3, 4)
MyList - step 6
For Comprehension
Exercise
The MyList has a map and flatMap function
So we can use in a for comprehension
WithFilter
def withFilter[A](f: A => MyList[B]): MyList[B]
In the scala for comprehension we can filter elements.
This is not the filter method but the withFilter method.
In the standard libraries the withFilter is lazy, but that is for the advanced course
So for now we use our filter method as implementation
def withFilter[A](f: A => MyList[B]): MyList[B] = filter(f)
@main
def main(): Unit =
val result = for
a <- MyList(1, 2, 3, 4, 5) if a < 3
yield
a
println(result)
MyList - step 7
The lazy list can handle an infinite list.
In the end we take some elements that are calculated
Lazy List Exercises
Exercise 1
We will use MyList as a reference. Refactor the MyList name to a LzList (shift-F6) Nearly all the methods are the same.
Cons
class Cons[A](hd: => A, tl: => LazyList[A]) extends LazyList[A] {
def isEmpty: Boolean = false
override lazy val head: A = hd
override lazy val tail: LazyList[A] = tl
Here we do a call by need on the head and the tail And we need a class because call by name are not allow on case classes.
And new Cons is used in instantiate a Cons
Run the LzList to see if everything still works
Exercise 2
To use the laziness of the list we will create an inifite list and take method
- take
- infinite list
def take(n :Int): LzList[A]
Implement those in Empty and Cons
def generate(start: Int)(next: Int => Int): LzList[Int] = ???
In the companion object a generate method is added of type Int
main
val genList: LzList[Int] = LzList.generate(1)( _ + 1)
val genMap = genList.map(_ * 100)
println(genMap.take(10))
println(genMap.take(100))
println(genMap.take(100000))
Library
Step-by-Step
-
Define a Book Case Class: Define a case class to represent a book with attributes like title, author, year of publication, and category.
-
Create a List of Books: Create an initial list of books.
-
Add a Book to the Library: Define a function to add a book to the library.
-
Search for Books by Title or Author: Define a function to search for books by title or author.
-
Filter Books by Category: Define a function to filter books by category.
-
Calculate Statistics: Define functions to calculate statistics like the total number of books and the average publication year.
Enhanced Library
Step-by-Step Enhancements
- Sorting Books:
- Sort books by title.
- Sort books by year of publication.
- Grouping Books by Category:
- Group books by their category.
- Partitioning Books by Year:
- Partition books into those published before and after a certain year.
- Using Collect to Extract Specific Information:
- Use the
collectfunction to extract titles of books published after a certain year.
- Calculating Average Publication Year by Category:
- Calculate the average publication year for each category.
Library with Shopping Basket
Step-by-Step Enhancements
-
Extend the Book Case Class: Add a price attribute to the
Bookcase class. -
Create a Shopping Basket: Define a class to manage the shopping basket, including adding books and calculating the total cost.
-
Update the Library Management System: Update the main application to include the shopping basket functionality.
Library with Author
Step-by-Step Enhancements
-
Define the Author Case Class: Add a case class to represent an author with attributes like name and nationality.
-
Update the Book Case Class: Modify the
Bookcase class to include anAuthor. -
Create a Shopping Basket Class: Define a class to manage the shopping basket, including adding books and calculating the total cost.
-
Update the Library Management System: Update the main application to include the new
Authorcase class and the shopping basket functionality.