Learning Scala
Scala 3 is a multi-paradigm programming language that supports both object-oriented and functional programming.
Why Scala
- Concise and expressive code: Scala's functional programming features allow developers to write code that is more concise and expressive. This is because functional programming encourages the use of higher-order functions, which can be used to abstract away common patterns in code.
- Better code quality: Functional programming encourages immutability and pure functions, which can result in code that is easier to reason about and has fewer bugs.
- Parallel and distributed computing: Functional programming allows for easy parallel and distributed computing, as pure functions can be executed in parallel without the need for locks or other synchronization mechanisms.
- Type safety: Scala is a statically typed language, which means that type errors are caught at compile time. This can help catch errors early on in the development process and make code more reliable.
- Easy integration with other systems: Scala is built on top of the JVM, which makes it easy to integrate with existing Java systems. Additionally, Scala is compatible with many other technologies, such as Akka and Apache Spark.
- Overall, functional programming in Scala offers many benefits, including increased code quality, better parallel and distributed computing, and easy integration with existing systems.
Getting Started
Create a 'Hello, World' scala application
In a browser
- 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
This Scala code creates a value named "x" and assigns it the value of 5.
The "val" keyword is used to declare an immutable variable in Scala, which means that once the value is assigned, it cannot be changed.
Here's an example of how you can use this value in Scala:
val x = 5
println(x) // Output: 5
In this example, the value of "x" is printed to the console using the "println" method. Since "x" is immutable, you cannot reassign it to a new value later in the code.
Inference
Scala is a statically-typed programming language, which means that every variable and expression must have a declared type. However, Scala also supports type inference, which allows the compiler to deduce the type of an expression automatically based on its context.
Type inference in Scala is performed by the compiler using a process called type unification. When a variable or expression is declared without an explicit type, the compiler attempts to infer the type by examining the types of other expressions in the same context. For example, consider the following code snippet:
val x = 5
val y = "Hello, world!"
val z = x + y
In this case, x is assigned an integer value, so its type is inferred to be Int. y is assigned a string value, so its type is inferred to be String. When the expression x + y is assigned to z, the compiler determines that the + operator is only defined for two arguments of numeric types, so it infers that x must also be a String and produces a compile-time error.
Type inference in Scala can greatly reduce the amount of boilerplate code required for variable and function declarations while still maintaining strong type safety. However, it's important to keep in mind that type inference is not always possible, particularly in complex or ambiguous situations, so it's still necessary to understand the type system and use explicit type annotations when appropriate.
Immutable
val
A val is an immutable variable. A constant.
It can not be reassigned and will give an error when you try.
scala> x = 4
-- `[E052]` Type Error: ----------------------------------------------------------
1 |x = 4
|^^^^^
|Reassignment to val x
Variable Names
The Scala variable names follow the Java conventions:
- The name of a variable must begin with a letter, a dollar sign $ or a underscore _
- The following characters can be letters or numbers. For example: number, A1, $value, _number
- A letter is a Unicode character. This means that you can choose from 34,186 characters
- White space is not allowed in variable name.
- Uppercase, lowercase and accented letters are allowed.
- The maximum length is unlimited
- Scala distinguishes uppercase and lowercase letters, so KM, Km, kM, and km are different names.
- Keywords are not allowed.
Valid names
number, thisIsANumber, this_is_a_number, n1, A1, $value, _number
Invalid names
1value, a.b, a!, b?, type
Number Types
Scala is a strong type language. Every variable must has a type.
We first take a look at the basic number types inherited from Java.
| naam | role | size(bits) | min to max value |
|---|---|---|---|
| Byte | whole | 8 | -128 to +127 |
| Short | whole | 16 | 32.768 to 32.767 |
| Int | whole | 32 | -2.147.483.648 to +2.147.483.747 |
| Long | whole | 64 | -9.223.372.036.854.775.808 to 9.223.372.036.854.775.808 |
| Float | real | 32 | 3,40292347E+38 to +3,40292347E+38 |
| Double | real | 64 | -1,79769313486231570E+308 to +1,79769313486231570E+308 |
| Char | letter | 16 | Unicode '\u0000 to '/uFFFF |
| Boolean | truth value | true or false |
When you need a whole number take an Int and only when an Int is too small take a Long.
My advice is not to use a Byte and Short, because there are some corner cases.
When you work with texts you will use a String.
Most libraries will have functions on String and not on Char even if the text contains only one letter.
Variable Exercises
Examples
Create a val called pi and assign it the value of 3.14159:
val pi: Double = 3.14159
Create a val called message and assign it a string value:
val message: String = "Hello, world!"
Create a val called age and assign it an integer value:
val age: Int = 30
Create a val called isTall and assign it a boolean value:
val isTall: Boolean = true
Create a val called name and assign it a string value, then print a message that includes the value of name:
val name: String = "Alice"
println(s"My name is $name.")
In this example, the string interpolation syntax is used to include the value of name in the printed message. The dollar sign ($) followed by the variable name is replaced with the value of the variable. The s at the beginning of the string indicates that this is a string interpolation.
Operators
In a Computer Everything is a Number.
Strings, Images, Audio, Video are All Encoded as Numbers.
Scala does have some operators to do the basic calculations.
But with these basic thing you can do everything
- toUpperCase on a text
- darken an image
- lower the volume on audio
- calculate the average on a dataset
Arithmetic operators
val a = 5
val b = 2
val sum = a + b // Addition operator
val difference = a - b // Subtraction operator
val product = a * b // Multiplication operator
val quotient = a / b // Division operator
val remainder = a % b // Modulus operator
| operator | function |
|---|---|
| + | add |
| - | subtract |
| * | multiply |
| / | divide |
| % | modulo |
Examples
scala>
val a: Int = 4
val b: Int = 3
scala> val add = a + b
val add: Int = 7
scala> val subtract = a - b
val subtract: Int = 1
scala> val multiply = a * b
val multiply: Int = 12
scala> val divide = a / b
val divide: Int = 1
This is an integer division. So it will give an integer value.
modulo
Modulo is the rest of an integer division
scala> val modulo = a % b
val modulo: Int = 1
divide floating point
If you want a floating point division then you have to cast an Int to a Double
scala> val d: Double = a / b
val d: Double = 1.0
To change to type to a Double is no enough, also a typecast is needed: toDouble
scala> val d: Double = a / b.toDouble
val d: Double = 1.3333333333333333
Comparison operators
| operator | function |
|---|---|
| > | greater then |
| < | smaller then |
| == | equals |
| >= | greater and equals |
| <= | smaller and equals |
val x = 10
val y = 5
val isGreaterThan = x > y // Greater than operator
val isLessThan = x < y // Less than operator
val isEqualTo = x == y // Equality operator
val isNotEqualTo = x != y // Inequality operator
scala> b < 4
val res0: Boolean = true
scala> b > 4
val res1: Boolean = false
scala> b == 3
val res2: Boolean = true
scala> b >= 4
val res3: Boolean = false
scala> b <= 4
val res4: Boolean = true
Logical operators
| operator | function |
|---|---|
| && | AND |
| || | OR |
| ! | NOT |
val p = true
val q = false
val andResult = p && q // Logical AND operator
val orResult = p || q // Logical OR operator
val notResult = !p // Logical NOT operator
scala> b != 4
val res5: Boolean = true
scala> b > 2 && b < 4
val res6: Boolean = true
scala> b < 2 || b > 4
val res7: Boolean = false
Choices
If
The if statement is a fundamental control flow mechanism in Scala, as in many other programming languages. It allows you to execute a block of code conditionally, depending on whether a Boolean expression evaluates to true or false. Scala's if statement is similar to that in Java, C, and many other languages, but with some functional programming twists that enhance its utility and flexibility.
Basic if Statement
The simplest form of an if statement in Scala is:
if condition then
// Block of code to execute if the condition is true
For example:
val age = 18
if age >= 18 then
println("You are an adult.")
This code checks if age is 18 or older and prints a message if the condition is true.
if-else Statement
The if-else statement extends the if statement by allowing you to execute an alternative block of code if the condition is false:
if condition then
// Block of code to execute if the condition is true
else
// Block of code to execute if the condition is false
For example:
if age >= 18 then
println("You are an adult.")
else
println("You are a minor.")
if-else if Ladder
For multiple conditions, Scala supports the if-else if ladder, similar to other languages:
if condition1 then
// Block of code for condition1
else if condition2 then
// Block of code for condition2
else
// Block of code if none of the above conditions are true
Example:
if age < 13 then
println("You are a child.")
else if age < 18 then
println("You are a teenager.")
else
println("You are an adult.")
if Expressions and Value Assignment
One of Scala's functional programming features is treating if-else as an expression that returns a value. This allows you to assign the result of an if-else directly to a variable:
val classification = if age < 13 then "child"
else if age < 18 then "teenager"
else "adult"
println(s"You are a $classification.")
This feature eliminates the need for the ternary operator (condition ? trueValue : falseValue) found in other languages, as if-else in Scala can be used in the same concise way.
Nested if Statements
if statements can be nested within each other to check multiple conditions:
val isSunny = true
val temperature = 25 // Celsius
if isSunny then
if temperature > 20 then
println("It's a nice day for a walk.")
else
println("It's cool outside, but sunny.")
else
println("It might rain today.")
Match
The match expression in Scala is a powerful feature that extends the concept of switch-case statements found in other languages. It allows you to match against a value, then execute a block of code depending on the match. Scala's match is more potent than a traditional switch-case because it can match types, values, patterns, and even guard expressions. It's a cornerstone of Scala's pattern matching capabilities, enabling concise and expressive code.
value match
case pattern1 => // Block of code for pattern1
case pattern2 => // Block of code for pattern2
// ...
case _ => // Block of code if none of the patterns match (default case)
Examples
Matching Simple Values
val dayOfWeek = 3
val dayName = dayOfWeek match
case 1 => "Sunday"
case 2 => "Monday"
case 3 => "Tuesday"
case 4 => "Wednesday"
case 5 => "Thursday"
case 6 => "Friday"
case 7 => "Saturday"
case _ => "Invalid day"
println(dayName) // Output: Tuesday
Multiple match
In Scala, the match expression can also be used to match multiple patterns and execute the same code for all matched patterns. This is done using the | operator.
val x: Int = 2
val result: String = x match
case 1 | 2 | 3 => "x is between 1 and 3"
case 4 | 5 | 6 => "x is between 4 and 6"
case _ => "x is outside the range"
println(result)
In this example, we use the | operator to match the value of x against multiple patterns. The code in the first block will execute if x is 1, 2, or 3. The code in the second block will execute if x is 4, 5, or 6. The code in the last block, which uses the underscore _ pattern, will execute if no other pattern matches.
Note that the | operator can be used with any pattern, including custom patterns.
Matching Types
def process(value: Any): Unit = value match
case s: String => println(s"String of length ${s.length}")
case i: Int => println(s"Integer: $i")
case _ => println("Unknown type!")
process("Hello")
process(123)
process(3.14)
Matching with Guards
You can add conditional expressions (guards) to your cases using if:
val number = 10
val parity = number match
case n if n % 2 == 0 => "even"
case _ => "odd"
}
println(parity) // Output: even
Tips for Using Match Expressions
- Exhaustiveness: Scala's compiler checks if the match cases are exhaustive, meaning all possible cases are covered. This helps prevent runtime errors.
- Default Case: Always provide a default case (
case _ =>) to handle unexpected values, unless you are sure the match cases are exhaustive. - Pattern Guards: Use guards to refine your match cases with additional conditions.
Loops
While
The while loop in Scala, as in many other programming languages, is a control flow statement that allows code to be executed repeatedly based on a given Boolean condition. The loop will continue to execute as long as the condition evaluates to true. It's a fundamental construct that's useful in scenarios where the number of iterations is not known before the loop starts.
while condition do
// Code block to be executed
Here, condition is a Boolean expression. If condition evaluates to true, the code block inside the while loop is executed. After each execution, condition is evaluated again, and if it's still true, the loop continues. This repeats until condition becomes false.
Example of while Loop
Let's look at a simple example where we use a while loop to iterate until a variable x becomes less than or equal to zero:
var x = 10
while x > 0 do
println(x)
x -= 1 // Decrement x by 1
In this example, we start with x equal to 10, and in each iteration of the loop, we decrement x by 1 and print the current value of x. The loop terminates when x is no longer greater than 0.
Considerations When Using while Loops
- Mutability:
whileloops often rely on mutable state (like thexvariable in the examples) to control the loop's execution. This can be at odds with Scala's emphasis on immutability and functional programming principles. - Functional Alternatives: In many cases, especially when working with collections, functional alternatives such as
foreach,for,map,filter, andfoldcan achieve the same results as awhileloop but in a more concise and expressive way. These constructs also encourage immutability and side-effect-free programming.
Example of Replacing while with Functional Constructs
Consider a scenario where we want to sum all numbers from 1 to n. Instead of using a while loop with mutable state, we could use a functional and immutable approach:
val n = 10
val sum = (1 to n).sum
println(sum)
This code snippet uses a range (1 to n) and the sum method to calculate the total sum in a concise, readable, and immutable manner.
For
for item <- collection do
// Do something with item
Examples
Iterating Over a Range
for i <- 1 to 5 do
println(i)
This will print numbers 1 through 5. Scala ranges are inclusive by default when you use to. If you want an exclusive range, you can use until:
for i <- 1 until 5 do
println(i)
This prints numbers 1 through 4.
Iterating Over a Collection
val fruits = List("apple", "banana", "cherry")
for fruit <- fruits do
println(fruit)
This iterates over each element in the fruits list and prints it.
For-Comprehensions
Scala's for loop can also be used to create new collections from existing ones, a feature known as for-comprehensions. When used in this way, you enclose the iteration logic in parentheses and use the yield keyword to produce values that form a new collection.
Example: Squaring Numbers
val numbers = List(1, 2, 3, 4, 5)
val squaredNumbers = for n <- numbers yield n * n
println(squaredNumbers) // List(1, 4, 9, 16, 25)
Adding Filters
You can add filters within a for loop using an if clause to control which elements should be included in the iteration.
Example: Filtering Even Numbers
for
n <- 1 to 10
if n % 2 == 0
do
println(n)
This prints even numbers between 1 and 10.
Nested Loops
Scala allows nesting of for loops by adding multiple generators separated by semicolons.
Example: Generating Combinations
for
x <- 1 to 3
y <- 1 to 3
do
println(s"($x, $y)")
This prints all combinations of (x, y) pairs where x and y range from 1 to 3.
For-Comprehensions with Guards
Just like simple for loops, for-comprehensions can also include guards.
Example: Filtering and Mapping
val evensSquared =
for
n <- 1 to 10
if n % 2 == 0
yield n * n
println(evensSquared) // Vector(4, 16, 36, 64, 100)
This creates a collection of the squares of even numbers between 1 and 10.
String
Strings in Scala are sequences of characters. Scala strings are instances of the String class in Java, which means you
can use any method from the Java String class in Scala. Additionally, Scala adds its own set of methods to strings
through implicit conversions to StringOps to facilitate common operations in a more Scala-like way.
Creating Strings
Creating a string in Scala is straightforward and similar to other languages:
val greeting = "Hello, World!"
String Interpolation
Scala supports string interpolation, allowing you to embed variable references directly in string literals.
- s-Interpolator: Prepend
sto the string and use$to insert variables.
val name = "Scala"
val message = s"Hello, $name!"
println(message) // Output: Hello, Scala!
You can also use ${} to insert more complex expressions:
val temperature = 20.5
val weatherMessage = s"The current temperature is ${temperature}°C."
println(weatherMessage) // Output: The current temperature is 20.5°C.
- f-Interpolator: Similar to
s, but allows formatting.
val height = 1.9
val formattedMessage = f"$name%s is $height%2.2f meters tall."
println(formattedMessage) // Output: Scala is 1.90 meters tall.
- raw-Interpolator: Works like
sbut does not escape literals.
println(raw"New\nLine") // Output: New\nLine
Common Operations
- Length: Get the number of characters.
val length = greeting.length
- Concatenation: Combine strings using
+.
val fullGreeting = greeting + " How are you?"
- Substrings: Extract parts of a string.
val hello = greeting.substring(0, 5) // "Hello"
- Comparisons: Compare two strings, optionally ignoring case.
val isEqual = "Scala" == "scala" // false
val isEqualIgnoreCase = "Scala".equalsIgnoreCase("scala") // true
- Searching: Check if a string contains a sequence or matches a pattern.
val containsWorld = greeting.contains("World") // true
- Splitting: Split a string into an array of substrings.
val words = greeting.split(", ") // Array("Hello", "World!")
- Trimming: Remove leading and trailing whitespaces.
val spaced = " Hello, World! "
val trimmed = spaced.trim // "Hello, World!"
- Replacing: Replace parts of the string.
val replaced = greeting.replace("World", "Scala") // "Hello, Scala!"
- Case Conversion: Convert to upper or lower case.
val upper = greeting.toUpperCase // "HELLO, WORLD!"
val lower = greeting.toLowerCase // "hello, world!"
Multiline Strings
Scala supports multiline strings using triple quotes, preserving line breaks and spaces:
val multilineString =
"""This is a
|multiline string
|in Scala."""
.stripMargin
The .stripMargin method removes leading spaces up to and including the character |, making it easier to format
multiline strings neatly.
Functions
A function is a number of instructions grouped together.
A function has a name, input parameters and a return value.
In scala there are two types of functions: funtion and method.
Method
an example of a simple function in Scala that takes two integers as parameters and returns their sum:
def add(x: Int, y: Int): Int =
x + y
// Example usage
val result = add(3, 5) // 8
In this example, the add function takes two parameters of type Int, named x and y. The function body simply adds these two parameters together and returns the result as an Int. The function definition ends with a colon followed by the return type of the function, which in this case is Int. Scala supports type inference, so in simple cases like this, you can often omit the return type and let the compiler figure it out.
Function
In Scala, a val can hold a function as a value. Here's an example of defining a function as a val:
val add: (Int, Int) => Int = (x, y) => x + y
// Example usage
val result = add(3, 5) // 8
In this example, we define a val named add which has the type (Int, Int) => Int. This means that add is a function that takes two parameters of type Int and returns a value of type Int.
The right-hand side of the = sign is a function literal, which is an anonymous function that takes two parameters x and y, adds them together, and returns the result.
Because the Scala compiler can usually infer the types of function parameters and return values, we can usually omit the type annotations and simply write:
val add = (x: Int, y: Int) => x + y
Default Parameters
Scala allows you to specify default values for function parameters. If the caller omits those parameters, Scala uses the default values.
def greet(name: String, greeting: String = "Hello"): String = s"$greeting, $name!"
println(greet("Alice")) // Output: Hello, Alice!
println(greet("Alice", "Hi")) // Output: Hi, Alice!
Named Arguments
When calling functions, you can specify arguments by name, rather than strictly by position. This is particularly useful when a function has multiple parameters with default values.
def describePerson(name: String, age: Int, country: String = "Unknown"): String =
s"$name is $age years old from $country."
println(describePerson(age = 30, name = "Bob")) // Output: Bob is 30 years old from Unknown.
Variable Arguments (Varargs)
Scala allows you to define functions that take an arbitrary number of arguments of the same type. This is achieved using the * syntax.
def sum(nums: Int*): Int = nums.sum
println(sum(1, 2, 3, 4, 5)) // Output: 15
Anonymous Functions (Function Literals)
Scala allows the definition of anonymous functions, which are handy for short snippets of code that are passed to higher-order functions or used for creating quick and simple function objects.
val addOne = (x: Int) => x + 1
println(addOne(9)) // Output: 10
Tuples
Tuples can hold between two and twenty-two items. They are instantiated by enclosing the items in parentheses, separated by commas.
val myTuple = (1, "hello", true)
This creates a tuple containing an Int, a String, and a Boolean.
Accessing Tuple Elements
Elements of a tuple are accessed using the ._n syntax, where n is the 1-based index of the element.
val number = myTuple._1 // 1
val greeting = myTuple._2 // "hello"
val flag = myTuple._3 // true
Tuple Destructuring
Scala allows "destructuring" tuples into individual variables:
val (num, greet, flg) = myTuple
After this operation, num will be 1, greet will be "hello", and flg will be true.
Using Tuples in Functions
Tuples are particularly useful for functions that need to return multiple values. Here's an example:
def minMax(values: Array[Int]): (Int, Int) = {
(values.min, values.max)
}
val limits = minMax(Array(1, 2, 3, 4, 5))
// limits is now (1, 5)
Tuples and Maps
Tuples are often used with Scala's Map collection since a map is essentially a collection of key-value pairs, and each key-value pair can be represented as a tuple:
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
// Adding a new key-value pair
val newCapitals = capitals + ("USA" -> "Washington D.C.")
Here, "France" -> "Paris" is syntactic sugar for creating a tuple of two elements.
Operations
| Operation | Example | Result |
|---|---|---|
| size | (1, 2, 3).size | 3 |
| head | (3 *: 4 *: 5 *: EmptyTuple).head | 3 |
| tail | (3 *: 4 *: 5 *: EmptyTuple).tail | (4, 5) |
| *: | 3 *: 4 *: 5 *: 6 *: EmptyTuple | (3, 4, 5, 6) |
| drop | (1, 2, 3).drop(2) | (3) |
| take | (1, 2, 3).take(2) | (1, 2) |
| apply | (1, 2, 3)(2) | 3 |
| splitAt | (1, 2, 3, 4, 5).splitAt(2) | ((1, 2), (3, 4, 5)) |
| zip | (1, 2, 3).zip(('a', 'b')) | ((1 'a'), (2, 'b')) |
| toList | (1, 'a', 2).toList | List(1, 'a', 2) |
| toArray | (1, 'a', 2).toArray | Array(1, '1', 2) |
| toIArray | (1, 'a', 2).toIArray | IArray(1, '1', 2) |
Object Oriented
Scala runs on the JVM which based on Java.
So Scala can do everything Java can.
And has all the Java object-oriented features and more
Basic concepts:
- class and object
- inheritance
- abstraction (abstract class, trait)
- polymorphism
- data hiding
Class
A class has members:
- field
- method
A class is instantiated to become an object.
Fields are variables in the object scope.
Methods are functions in the object scope and can implicitly
access the fields and other methods.
class Greeter:
val name = "John" // field
def sayHello() = // method
s"Hello, $name" // access the field
val greeter = new Greeter // instance
greeter.name = "Lucy" // dot operator
val greeting = greeter.sayHello() // call the method
println(greeting)
Constructor
It is a good practise to make classes immutable.
This means that fields are private and written in the constructor
class Greeter (name: String = "John"): // constructor
def sayHello() =
s"Hello, $name"
val greeter = new Greeter("Lucy") // call the constructor
val greeting = greeter.sayHello()
println(greeting)
Constructor args
class Number(x: Int) // private
class Number(val x: Int) // getter
class Number(var x: Int) // getter and setter
Object
In Scala an object is a singleton class.
An object only has one instance. So every object is the same instance.
It can be used as counterpart of static methods and fields in Java.
An object has a special method apply.
Apply is called with the object name and a parameter list.
In the example below the
Greeter("Lucy")callsGreeter.apply("Lucy")
object Greeter:
private var name: String = "John" // private field
def apply(name: String) = // apply method
this.name = name
def sayHello() =
s"Hello, $name"
Greeter("Lucy") // call apply method
val greeting = Greeter.sayHello() // only one instance
println(greeting)
Inheritance
We can extend a class with inheritance. You could see inheritance as a copy-and-paste without copying-and-pasting
All the member from the base class are available in the inherited class.
class Meeter(name: String = "John") extends Greeter(name): // inheritance
def greetAndMeet() = // extra method
"Meet and " + sayHello() // from base class
val meeter = new Meeter("Lucy") // call the constructor
val greeting = meeter.greetAndMeet()
println(greeting)
Information hiding
You can restrict the access to class members. Scala has the next levels of access
| syntax | level | can access |
|---|---|---|
| public | everybody | |
| protected | protected | neighbours and children |
| private[pkg] | package private | in package |
| private | private | only me and companion |
| private[this] | strict private | not the companion object |
In Scala the default is public. This you do not have to write
class AccessLevels:
def everybody() = {}
protected def children() = {}
private[util] def neighbours() = {}
private def fromCompanion() = {}
private[this] def onlyMe() = {}
Traits
abstract class Shape(w: Int, h: Int): // abstract class
def area: Double // abstract method
class Rect(w: Int, h: Int) extends Shape(w, h):
override def area: Double = w * h // implement method
trait Drawable: // trait
def draw: Unit // abstract method
abstract class Shape(w: Int, h: Int): // abstract class
def area: Double // abstract method
class Rect(w: Int, h: Int) extends Shape(w, h) with Drawable:
override def area: Double = w * h // from Shape
override def draw: Unit = println("draw") // from Drawable
Case Class
Case classes in Scala are a special type of class that is optimized for use in pattern matching and immutability by default. They come with several boilerplate features out of the box, such as sensible toString, equals, and hashCode implementations, as well as the ability to be deconstructed in pattern matching. Case classes are immensely useful for defining simple data-holding objects, making them a staple in functional programming and domain modeling in Scala.
Basic Case Class
Defining a case class is straightforward:
case class Person(name: String, age: Int)
This declaration automatically provides:
- Immutable fields:
nameandageare public val fields by default. - Sensible
toString,equals, andhashCodemethods based on the class's fields. - An
applymethod, allowing you to instantiate the class without thenewkeyword. - An
unapplymethod, making it eligible for use in pattern matching.
Instantiating a Case Class
You can instantiate a case class without the new keyword, thanks to the automatically provided apply method:
val alice = Person("Alice", 30)
Copying
Case classes come with a copy method, which is useful for creating a new instance of the class with some changed attributes while keeping the rest unchanged:
val bob = alice.copy(name = "Bob")
This creates a new Person instance with the name "Bob" and the same age as alice.
Pattern Matching
Case classes shine when used in pattern matching, thanks to their unapply method:
alice match
case Person(name, age) => println(s"Name: $name, Age: $age")
case _ => println("Unknown person")
This pattern matching checks if alice is a Person instance and then extracts and prints the name and age.
Companion Objects
A companion object for the case class is automatically generated, containing the apply and unapply methods among others. This means you can add additional static utility methods or values in the companion object if needed:
object Person:
def isAdult(person: Person): Boolean = person.age >= 18
val adultCheck = Person.isAdult(alice) // true
Use Cases
Case classes are ideal for:
- Domain models: Representing data entities in your domain.
- Data transfer objects (DTOs): Encapsulating data sent between processes or systems.
- Immutable data structures: Building complex data structures that benefit from immutability.
Pattern Matching
Pattern matching with case classes is a powerful feature in Scala that allows for concise and expressive handling of different data structures. Case classes are regular classes which are immutable by default and decomposable through pattern matching. They are especially useful in functional programming paradigms for matching complex data types. Here's a basic overview of how you can use pattern matching with case classes:
-
Define Case Classes: These are special classes in Scala defined using the
case classkeyword. They automatically provide implementations oftoString,equals, andhashCodemethods, making them useful for pattern matching. -
Pattern Matching Syntax: You use the
matchkeyword followed by cases. Each case can destructure an instance of a case class, extracting its parts for further processing.
Example
Let's define a simple example to demonstrate pattern matching with case classes. Suppose we are dealing with a system that models basic geometric shapes:
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Square(side: Double) extends Shape
Here, Shape is a sealed trait, which means all implementations of Shape must be in the same file. This is useful for pattern matching because the compiler can warn about missing cases.
Next, let's define a function that uses pattern matching to compute the area of a shape:
def area(shape: Shape): Double = shape match
case Circle(radius) => Math.PI * radius * radius
case Rectangle(width, height) => width * height
case Square(side) => side * side
In this function, shape is matched against each case class (Circle, Rectangle, Square), and the appropriate formula is used to calculate the area based on the shape type.
Benefits of Using Case Classes with Pattern Matching
- Readability: The intent of the code is clear and concise.
- Safety: The compiler checks for exhaustiveness in match cases, ensuring that all possible cases are handled.
- Decomposition: Pattern matching allows for easy decomposition of complex data types, making it straightforward to access and manipulate their components.
Class Hierarchy
The Scala class hierarchy has Any at the top, from which all other types derive. It splits into two main branches: AnyVal (value types) and AnyRef (reference types). Additionally, Nothing and Null are special types that serve specific purposes in the hierarchy.
Any
├── AnyVal
│ ├── Unit
│ ├── Boolean
│ ├── Char
│ ├── Byte
│ ├── Short
│ ├── Int
│ ├── Long
│ ├── Float
│ └── Double
├── AnyRef (alias: Object)
│ ├── String
│ ├── List
│ ├── Option
│ │ ├── Some
│ │ └── None
│ ├── Map
│ ├── Set
│ ├── ... (other classes and user-defined classes)
│ └── Null (subtype of every AnyRef type)
└── Nothing (subtype of every other type)
Key Points
Any
- The root of the type hierarchy.
- Defines common methods such as
==,!=,equals,hashCode, andtoString.
AnyVal
- The root class of value types, which includes Scala's primitive types.
- Subclasses:
Unit,Boolean,Char,Byte,Short,Int,Long,Float,Double.
AnyRef (alias Object)
- The root class of all reference types.
- Subclasses:
String,List,Option,Map,Set, and user-defined classes.
Option
- A container for an optional value.
- Subclasses:
Some(contains a value),None(represents the absence of a value).
Null
- A subtype of every reference type (
AnyRef). - Can be assigned to any reference type but not to value types.
- The
nullliteral is of typeNull.
Nothing
- A subtype of every other type (both
AnyValandAnyRef). - Represents the type of "no value" or "no normal return".
- Useful for functions that never return normally (e.g., throw an exception).
Examples
Value Types (AnyVal)
val unitValue: Unit = ()
val booleanValue: Boolean = true
val charValue: Char = 'A'
val byteValue: Byte = 1
val shortValue: Short = 1
val intValue: Int = 1
val longValue: Long = 1L
val floatValue: Float = 1.0f
val doubleValue: Double = 1.0
Reference Types (AnyRef)
val stringValue: String = "Hello, Scala!"
val listValue: List[Int] = List(1, 2, 3)
val optionValue: Option[Int] = Some(1)
val noneValue: Option[Int] = None
val mapValue: Map[String, Int] = Map("one" -> 1)
val setValue: Set[Int] = Set(1, 2, 3)
class UserDefinedClass
val userDefinedInstance = new UserDefinedClass
val nullValue: String = null
Special Types
Nothing
def fail(message: String): Nothing = throw new RuntimeException(message)
// This function never returns normally, so it has type Nothing
val failed = fail("This is an error")
// This line is never reached
println("This will never be printed")
None
val maybeValue: Option[Int] = None
maybeValue match
case Some(value) => println(s"Found value: $value")
case None => println("No value found")
// Output: No value found
Mixins
In Scala, mixins are a powerful feature that allows a class to inherit functionality from multiple traits. This is similar to multiple inheritance but avoids the complexities and pitfalls associated with it. Mixins enable you to compose behaviors from different traits and integrate them into a single class.
Defining and Using Mixins
To define mixins, you use traits. Traits are similar to interfaces in Java but can also contain method implementations and fields.
Here's a step-by-step guide on how to define and use mixins in Scala:
-
Define Traits: Traits can define methods and fields. You can then mix these traits into a class.
trait Logger: def log(message: String): Unit = println(s"Log: $message")
trait Greeter: def greet(name: String): Unit = println(s"Hello, $name!")
2. **Create a Class with Mixins:**
You can mix traits into a class using the `extends` and `with` keywords.
```scala
class Person(val name: String) extends Logger with Greeter:
def introduce(): Unit =
log(s"Introducing $name")
greet(name)
-
Instantiate and Use the Class:
val person = new Person("Alice") person.introduce()This will produce the following output:
Log: Introducing Alice Hello, Alice!
Mixing Multiple Traits
You can mix multiple traits into a single class, and they can have their own method implementations and fields.
trait Runner:
def run(): Unit =
println("Running!")
class Athlete(name: String) extends Person(name) with Runner:
def compete(): Unit =
log(s"$name is competing")
run()
greet(name)
val athlete = new Athlete("Bob")
athlete.introduce()
athlete.compete()
This will produce the following output:
Log: Introducing Bob
Hello, Bob!
Log: Bob is competing
Running!
Hello, Bob!
Overriding Methods in Traits
You can override methods defined in traits in the class that mixes them in, or in the traits themselves if they extend other traits.
trait Speaker:
def speak(): Unit =
println("Speaking generically.")
trait Philosopher extends Speaker:
override def speak(): Unit =
println("Speaking philosophically.")
class Person(name: String) extends Philosopher:
override def speak(): Unit =
println(s"$name is speaking wisely.")
val person = new Person("Charlie")
person.speak()
This will produce the following output:
Charlie is speaking wisely.
Linearization of Traits
Scala uses a linearization technique to determine the order in which traits are initialized and their methods are called. This ensures a consistent and predictable method resolution order.
Consider the following example to understand linearization:
trait A:
def message(): String = "A"
trait B extends A:
override def message(): String = "B " + super.message()
trait C extends A:
override def message(): String = "C " + super.message()
class D extends B with C:
override def message(): String = "D " + super.message()
val d = new D()
println(d.message())
In this case, the output will be:
D C B A
In Scala, self-types are a way to express dependencies between traits. They allow you to declare that a trait can only be mixed into classes that also mix in another specified trait. This can be particularly useful for modularizing code and ensuring that certain traits are only used in the correct contexts.
Here’s a detailed guide on using self-types with mixins in Scala:
Defining Self-Types
A self-type is declared using the this: Type => syntax inside a trait. This specifies that the trait can only be mixed into classes or traits that conform to the specified type.
trait Logger:
def log(message: String): Unit = println(s"Log: $message")
trait AuthService:
this: Logger => // Self-type declaration
def authenticate(user: String, password: String): Boolean =
log(s"Authenticating user: $user")
user == "admin" && password == "password"
class AuthApp extends AuthService with Logger
val app = new AuthApp
app.authenticate("admin", "password")
In this example:
- The
AuthServicetrait has a self-typeLogger, meaning it requires an implementation ofLoggerto be mixed in. - The
AuthAppclass mixes in bothAuthServiceandLogger, satisfying the self-type requirement.
Summary
- Traits: Define reusable methods and fields.
- Mixins: Use
extendsandwithto mix traits into classes. - Method Override: Override trait methods in classes or other traits.
- Linearization: Ensures a consistent method resolution order when mixing multiple traits.
- Self-Types: Declare dependencies between traits using
this: Type =>syntax.
Design Patterns
Design patterns are common solutions to recurring problems in software design. In Scala, many design patterns can be implemented concisely and elegantly due to the language's rich features such as case classes, traits, and functional programming constructs. Here are examples of some common design patterns in Scala:
1. Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
object Singleton:
def doSomething(): Unit =
println("Doing something...")
// Usage
Singleton.doSomething()
2. Factory Pattern
The Factory pattern provides an interface for creating objects without specifying the exact class of the object that will be created.
sealed trait Animal:
def makeSound(): String
case class Dog() extends Animal:
def makeSound(): String = "Woof"
case class Cat() extends Animal:
def makeSound(): String = "Meow"
object AnimalFactory:
def createAnimal(animalType: String): Animal =
animalType.toLowerCase match
case "dog" => Dog()
case "cat" => Cat()
case _ => throw new IllegalArgumentException("Unknown animal type")
// Usage
val dog = AnimalFactory.createAnimal("dog")
println(dog.makeSound()) // Woof
val cat = AnimalFactory.createAnimal("cat")
println(cat.makeSound()) // Meow
3. Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern lets the algorithm vary independently from the clients that use it.
trait PaymentStrategy:
def pay(amount: Double): String
class CreditCardPayment extends PaymentStrategy:
def pay(amount: Double): String = s"Paid $$amount using credit card."
class PayPalPayment extends PaymentStrategy:
def pay(amount: Double): String = s"Paid $$amount using PayPal."
class PaymentContext(strategy: PaymentStrategy):
def executeStrategy(amount: Double): String = strategy.pay(amount)
// Usage
val creditCardPayment = new PaymentContext(new CreditCardPayment)
println(creditCardPayment.executeStrategy(100)) // Paid $100.0 using credit card.
val payPalPayment = new PaymentContext(new PayPalPayment)
println(payPalPayment.executeStrategy(200)) // Paid $200.0 using PayPal.
4. Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
trait Observer:
def update(message: String): Unit
class ConcreteObserver(name: String) extends Observer:
def update(message: String): Unit =
println(s"$name received message: $message")
trait Subject:
private var observers: List[Observer] = List()
def addObserver(observer: Observer): Unit =
observers = observer :: observers
def removeObserver(observer: Observer): Unit =
observers = observers.filterNot(_ == observer)
def notifyObservers(message: String): Unit =
observers.foreach(_.update(message))
class ConcreteSubject extends Subject:
def changeState(message: String): Unit =
println(s"Subject state changed: $message")
notifyObservers(message)
// Usage
val subject = new ConcreteSubject
val observer1 = new ConcreteObserver("Observer1")
val observer2 = new ConcreteObserver("Observer2")
subject.addObserver(observer1)
subject.addObserver(observer2)
subject.changeState("New state") // Both observers receive the message
5. Decorator Pattern
The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
trait Coffee:
def cost: Double
def description: String
class SimpleCoffee extends Coffee:
def cost: Double = 2.0
def description: String = "Simple coffee"
class MilkDecorator(coffee: Coffee) extends Coffee:
def cost: Double = coffee.cost + 0.5
def description: String = coffee.description + ", milk"
class SugarDecorator(coffee: Coffee) extends Coffee:
def cost: Double = coffee.cost + 0.2
def description: String = coffee.description + ", sugar"
// Usage
val coffee = new SimpleCoffee
println(s"${coffee.description} costs ${coffee.cost}") // Simple coffee costs 2.0
val milkCoffee = new MilkDecorator(coffee)
println(s"${milkCoffee.description} costs ${milkCoffee.cost}") // Simple coffee, milk costs 2.5
val milkSugarCoffee = new SugarDecorator(milkCoffee)
println(s"${milkSugarCoffee.description} costs ${milkSugarCoffee.cost}") // Simple coffee, milk, sugar costs 2.7
6. Command Pattern
The Command pattern encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of requests.
trait Command:
def execute(): Unit
class Light:
def on(): Unit = println("The light is on")
def off(): Unit = println("The light is off")
class LightOnCommand(light: Light) extends Command:
def execute(): Unit = light.on()
class LightOffCommand(light: Light) extends Command:
def execute(): Unit = light.off()
class RemoteControl:
private var command: Option[Command] = None
def setCommand(command: Command): Unit =
this.command = Some(command)
def pressButton(): Unit =
command.foreach(_.execute())
// Usage
val light = new Light
val lightOn = new LightOnCommand(light)
val lightOff = new LightOffCommand(light)
val remote = new RemoteControl
remote.setCommand(lightOn)
remote.pressButton() // The light is on
remote.setCommand(lightOff)
remote.pressButton() // The light is off
Enum
Enums in Scala 3 provide a way to define a group of named values, known as enumeration constants. Scala 3's enum is a significant enhancement over the older Enumeration class in Scala 2, offering more capabilities and integrating more seamlessly with the type system. Enums are useful for representing a fixed set of constants, such as days of the week, states of a process, or categories of items, with the benefits of type safety and easy pattern matching.
Basic Enum Declaration
To declare an enum in Scala 3, use the enum keyword followed by the name of the enum and a block containing its cases:
enum Color:
case Red, Green, Blue
Here, Color is an enum with three possible values: Red, Green, and Blue.
Using Enums
You can use enums in your Scala code like this:
val myColor: Color = Color.Red
myColor match
case Color.Red => println("Chosen color is Red")
case Color.Green => println("Chosen color is Green")
case Color.Blue => println("Chosen color is Blue")
This code demonstrates defining an enum variable and using pattern matching to perform different actions based on the enum's value.
Parameterized Enums
Enums in Scala 3 can also have parameters, allowing each case to carry additional information:
enum Planet(mass: Double, radius: Double)
case Earth extends Planet(5.972e24, 6.371e6)
case Jupiter extends Planet(1.898e27, 6.9911e7)
case Mars extends Planet(6.39e23, 3.3895e6)
// A method that calculates the surface gravity of a planet
def surfaceGravity: Double = Planet.G * mass / (radius * radius)
}
object Planet:
private final val G = 6.67430e-11 // gravitational constant
In this example, each Planet case has its mass and radius. This makes enums very powerful, as they can encapsulate data and behavior.
Enum Methods and Values
Scala enums can have their methods, as shown with the surfaceGravity method in the Planet example. You can also list all enum values using the .values method:
val planets = Planet.values
planets.foreach(planet =>
println(s"${planet}: Surface gravity = ${planet.surfaceGravity}")
)
This code prints the surface gravity for each planet defined in the Planet enum.
Enumerations with Sealed Trait Pattern (Scala 2 Approach)
Before Scala 3, a common way to simulate enums was using a sealed trait and case objects:
sealed trait Direction
case object North extends Direction
case object East extends Direction
case object South extends Direction
case object West extends Direction
While this pattern is still useful, especially for compatibility with Scala 2 codebases, Scala 3 enums offer a more integrated and straightforward way to define enumeration types.
List
1. Immutable List-Like Collections
1.1 List
- Description:
Listis a linear, immutable collection that maintains order. It is the most commonly used collection in Scala for sequences of elements. - Characteristics:
- Immutable: Once created, it cannot be modified.
- Linked List: Internally implemented as a singly linked list.
- Efficient prepends (
::), but appends (:+) are O(n).
- Use Case: General-purpose sequence where immutability is desired.
val list = List(1, 2, 3)
1.2 Vector
- Description:
Vectoris an immutable indexed sequence that offers an alternative toList. It is designed to provide more efficient random access and updates. - Characteristics:
- Immutable: Once created, it cannot be modified.
- Indexed: Provides efficient random access (O(log32 n)).
- Fast prepend, append, and random access.
- Use Case: When you need fast random access and efficient append/prepend operations.
val vector = Vector(1, 2, 3)
1.3 Seq
- Description:
Seqis a general trait for ordered collections.ListandVectorare common implementations ofSeq. - Characteristics:
- Immutable:
Seqis immutable by default in thescala.collection.immutablepackage. - Maintains order.
- Immutable:
- Use Case: When you want to work with sequences generically without worrying about the specific implementation.
val seq: Seq[Int] = Seq(1, 2, 3) // Immutable by default
1.4 Range
- Description:
Rangeis a special type of sequence representing an ordered sequence of integers. It is often used for looping or generating sequences of numbers. - Characteristics:
- Immutable.
- Efficient representation of ranges of integers.
- Use Case: When you need to generate a sequence of numbers or iterate over a range of integers.
val range = 1 to 10 // Range from 1 to 10
1.5 Stream (deprecated, use LazyList instead)
- Description:
Streamwas a lazily evaluated list, where elements are computed as needed. It has been replaced byLazyListin Scala 2.13. - Characteristics:
- Immutable.
- Lazy: Elements are computed only when accessed.
- Use Case: When working with potentially infinite sequences or when you want to delay computation of elements.
val lazyList = LazyList(1, 2, 3, 4, 5)
1.6 LazyList
- Description:
LazyListis the replacement forStream, offering lazy evaluation with more consistent performance. - Characteristics:
- Immutable.
- Lazy: Elements are computed as needed.
- Use Case: For lazy evaluation of sequences, particularly useful for infinite sequences.
val lazyList = LazyList.from(1) // Infinite lazy list starting from 1
2. Mutable List-Like Collections
2.1 ListBuffer
- Description:
ListBufferis a mutable collection that is efficient for building lists incrementally. Once constructed, it can be converted to an immutableList. - Characteristics:
- Mutable: Supports in-place modification.
- Efficient append/prepend operations.
- Converts easily to an immutable
List.
- Use Case: When you need to build a list incrementally before converting it to an immutable list.
import scala.collection.mutable.ListBuffer
val listBuffer = ListBuffer(1, 2, 3)
listBuffer += 4
val immutableList = listBuffer.toList
2.2 ArrayBuffer
- Description:
ArrayBufferis a mutable, indexed sequence that provides fast random access and append/prepend operations. - Characteristics:
- Mutable.
- Indexed: Allows random access.
- Efficient append/prepend operations.
- Use Case: When you need a mutable, indexed sequence with efficient access and update operations.
import scala.collection.mutable.ArrayBuffer
val arrayBuffer = ArrayBuffer(1, 2, 3)
arrayBuffer += 4
2.3 Array
- Description:
Arrayis a mutable, fixed-size sequence of elements. It provides fast access and update operations. - Characteristics:
- Mutable.
- Indexed: Allows random access.
- Fixed size: The length of an array cannot be changed after creation.
- Use Case: When you need a fixed-size, mutable sequence with efficient access and update operations.
val array = Array(1, 2, 3)
array(0) = 0
2.4 MutableList (deprecated, use ListBuffer instead)
- Description:
MutableListis a mutable linked list. It has been largely replaced byListBufferin most use cases. - Characteristics:
- Mutable.
- Linked list structure.
- Use Case: Use
ListBufferinstead for most cases.
3. Other List-Like Collections
3.1 Queue
- Description:
Queueis a mutable collection that allows elements to be added at the end and removed from the front (FIFO - First In, First Out). - Characteristics:
- Mutable.
- FIFO: First In, First Out order.
- Use Case: When you need a mutable, ordered collection with FIFO semantics.
import scala.collection.mutable.Queue
val queue = Queue(1, 2, 3)
queue.enqueue(4)
queue.dequeue() // Removes 1
3.2 Stack (deprecated, use List or Vector instead)
- Description:
Stackis a mutable collection that allows elements to be added and removed in a LIFO (Last In, First Out) manner. It has been replaced by usingListorVectorfor stack-like behavior. - Characteristics:
- Mutable.
- LIFO: Last In, First Out order.
- Use Case: Use
ListorVectorwith operations like::andtailinstead ofStack.
import scala.collection.mutable.Stack
val stack = Stack(1, 2, 3)
stack.push(4)
stack.pop() // Removes 4
3.3 Deque
- Description:
Deque(Double-ended Queue) allows elements to be added and removed from both ends. Available as a mutable collection. - Characteristics:
- Mutable.
- Allows operations on both ends (FIFO and LIFO).
- Use Case: When you need a mutable sequence that supports adding/removing elements from both ends.
import scala.collection.mutable.ArrayDeque
val deque = ArrayDeque(1, 2, 3)
deque.append(4)
deque.prepend(0)
4. Summary
Scala provides a wide range of list-like collections, each suited for different purposes:
-
Immutable Collections:
List: The go-to immutable sequence.Vector: Better for random access.LazyList: For lazy evaluation.Range: For generating sequences of numbers.
-
Mutable Collections:
ListBuffer: For building lists incrementally.ArrayBuffer: For fast indexed access.Array: For fixed-size, mutable sequences.Queue: For FIFO operations.Deque: For double-ended operations.
mkString
The mkString method is available for all collections in Scala, including lists, sets, arrays, etc. It can be used with or without delimiters.
Basic mkString without Delimiters
Concatenates all elements of the collection into a single string.
val numbers = List(1, 2, 3, 4, 5)
val result = numbers.mkString
println(result) // "12345"
mkString with a Separator
Concatenates all elements of the collection into a single string with a specified separator between elements.
val numbers = List(1, 2, 3, 4, 5)
val result = numbers.mkString(", ")
println(result) // "1, 2, 3, 4, 5"
mkString with Prefix, Separator, and Suffix
Concatenates all elements of the collection into a single string with a specified prefix, separator, and suffix.
val numbers = List(1, 2, 3, 4, 5)
val result = numbers.mkString("[", ", ", "]")
println(result) // "[1, 2, 3, 4, 5]"
Use Cases
Joining File Paths
val parts = List("home", "user", "documents", "file.txt")
val filePath = parts.mkString("/")
println(filePath) // "home/user/documents/file.txt"
Generating CSV Lines
val headers = List("Name", "Age", "City")
val row1 = List("Alice", "30", "New York")
val row2 = List("Bob", "25", "Los Angeles")
val csvHeaders = headers.mkString(", ")
val csvRow1 = row1.mkString(", ")
val csvRow2 = row2.mkString(", ")
println(csvHeaders) // "Name, Age, City"
println(csvRow1) // "Alice, 30, New York"
println(csvRow2) // "Bob, 25, Los Angeles"
Creating SQL Query IN Clause
val ids = List(1, 2, 3, 4, 5)
val sqlInClause = ids.mkString("(", ", ", ")")
println(sqlInClause) // "(1, 2, 3, 4, 5)"
Hierarchy
Traversable
└── Iterable
├── Seq
│ ├── LinearSeq
│ │ ├── List
│ │ ├── LazyList
│ │ ├── Queue
│ │ └── Stack
│ ├── IndexedSeq
│ │ ├── Vector
│ │ ├── Range
│ │ ├── String (Implicit)
│ │ └── Array (Implicit)
│ ├── Mutable
│ │ ├── ArrayBuffer
│ │ ├── ArraySeq
│ │ └── StringBuilder
├── Set
│ ├── SortedSet
│ │ └── TreeSet
│ ├── HashSet
│ ├── BitSet
│ ├── Mutable
│ │ ├── HashSet
│ │ ├── LinkedHashSet
│ │ └── TreeSet
├── Map
│ ├── SortedMap
│ │ └── TreeMap
│ ├── HashMap
│ ├── ListMap
│ ├── Mutable
│ │ ├── HashMap
│ │ ├── LinkedHashMap
│ │ ├── TreeMap
│ │ └── WeakHashMap
Key Points
- Traversable is the root of the collection hierarchy.
- Iterable extends Traversable and adds the
iteratormethod. - Seq, Set, and Map are the main collection types, each with mutable and immutable versions.
- LinearSeq and IndexedSeq are subtypes of Seq for specific sequence implementations.
- SortedSet and SortedMap are specialized collections for sorted elements.
List
A List in Scala is an immutable, ordered collection of elements. Once created, a List cannot be changed. It's a linked list, meaning that it consists of nodes, where each node contains a value and a reference to the next node.
Creating Lists
You can create a List using the List companion object.
val numbers = List(1, 2, 3, 4, 5)
val fruits = List("apple", "banana", "cherry")
Basic Operations on Lists
Accessing Elements
- Head: The first element of the list.
- Tail: A list consisting of all elements except the head.
- IsEmpty: Checks if the list is empty.
val numbers = List(1, 2, 3, 4, 5)
val head = numbers.head // 1
val tail = numbers.tail // List(2, 3, 4, 5)
val isEmpty = numbers.isEmpty // false
List Concatenation
You can concatenate lists using the ::: operator or the ++ method.
val list1 = List(1, 2, 3)
val list2 = List(4, 5, 6)
val concatenated = list1 ::: list2 // List(1, 2, 3, 4, 5, 6)
val concatenated2 = list1 ++ list2 // List(1, 2, 3, 4, 5, 6)
Prepending and Appending Elements
- Prepend: Add an element to the beginning of the list using the
::operator. - Append: Add an element to the end of the list using the
:+method.
val numbers = List(2, 3, 4)
val withPrepended = 1 :: numbers // List(1, 2, 3, 4)
val withAppended = numbers :+ 5 // List(2, 3, 4, 5)
Common List Methods
Mapping
The map method transforms each element of the list using a given function.
val numbers = List(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map(n => n * n) // List(1, 4, 9, 16, 25)
Filtering
The filter method selects elements of the list that satisfy a predicate.
val numbers = List(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter(_ % 2 == 0) // List(2, 4)
Reducing
The reduce method combines the elements of the list using a binary operation.
val numbers = List(1, 2, 3, 4, 5)
val sum = numbers.reduce(_ + _) // 15
val product = numbers.reduce(_ * _) // 120
Sorting
The sorted method sorts a list based on the natural ordering of its elements or a provided comparator.
val numbers = List(5, 3, 1, 4, 2)
val sortedNumbers = numbers.sorted
println(sortedNumbers) // List(1, 2, 3, 4, 5)
val fruits = List("banana", "apple", "cherry")
val sortedFruits = fruits.sorted
println(sortedFruits) // List(apple, banana, cherry)
foreach
The foreach method applies a function to each element of the list.
val numbers = List(1, 2, 3, 4, 5)
numbers.foreach(println)
zip
The zip method combines two lists into a list of pairs.
val numbers = List(1, 2, 3)
val letters = List("a", "b", "c")
val zipped = numbers.zip(letters)
println(zipped) // List((1, "a"), (2, "b"), (3, "c"))
Immutable Nature of Lists
A key feature of List is its immutability. Once a List is created, it cannot be changed. Any operation that transforms a list will return a new list, leaving the original list unchanged.
val originalList = List(1, 2, 3)
val newList = originalList.map(_ * 2)
println(originalList) // List(1, 2, 3)
println(newList) // List(2, 4, 6)
Map
A Map is a collection of key-value pairs, where each key is unique. Maps can be either mutable or immutable, but by default, Scala uses immutable maps.
Creating Maps
You can create a Map using the Map companion object.
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)
val numbers = Map(1 -> "one", 2 -> "two", 3 -> "three")
Basic Operations on Maps
Accessing Elements
You can access the value associated with a key using the apply method, which is called using parentheses.
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)
println(fruitPrices("apple")) // 1.0
println(fruitPrices("banana")) // 0.5
If the key does not exist, apply throws an exception. To avoid this, you can use the get method, which returns an Option.
println(fruitPrices.get("apple")) // Some(1.0)
println(fruitPrices.get("orange")) // None
Adding and Removing Elements
- Add: Use the
+operator to add a key-value pair. - Remove: Use the
-operator to remove a key-value pair.
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5)
val newFruitPrices = fruitPrices + ("cherry" -> 2.0)
println(newFruitPrices) // Map(apple -> 1.0, banana -> 0.5, cherry -> 2.0)
val lessFruitPrices = newFruitPrices - "banana"
println(lessFruitPrices) // Map(apple -> 1.0, cherry -> 2.0)
For mutable maps, you can modify the map in place.
import scala.collection.mutable
val fruitPrices = mutable.Map("apple" -> 1.0, "banana" -> 0.5)
fruitPrices += ("cherry" -> 2.0)
println(fruitPrices) // Map(apple -> 1.0, banana -> 0.5, cherry -> 2.0)
fruitPrices -= "banana"
println(fruitPrices) // Map(apple -> 1.0, cherry -> 2.0)
Common Map Methods
Mapping
The map method transforms each key-value pair in the map.
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)
val discountedPrices = fruitPrices.map { case (fruit, price) => (fruit, price * 0.9) }
println(discountedPrices) // Map(apple -> 0.9, banana -> 0.45, cherry -> 1.8)
Filtering
The filter method selects key-value pairs that satisfy a predicate.
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)
val expensiveFruits = fruitPrices.filter { case (fruit, price) => price > 1.0 }
println(expensiveFruits) // Map(cherry -> 2.0)
Iterating
You can iterate over key-value pairs in a map using a for loop or the foreach method.
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5, "cherry" -> 2.0)
for ((fruit, price) <- fruitPrices) {
println(s"The price of $fruit is $$ $price")
}
fruitPrices.foreach { case (fruit, price) =>
println(s"The price of $fruit is $$ $price")
}
Immutable vs Mutable Maps
Immutable maps cannot be changed after they are created, while mutable maps can be updated in place.
Immutable Map
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5)
val newFruitPrices = fruitPrices + ("cherry" -> 2.0)
println(fruitPrices) // Map(apple -> 1.0, banana -> 0.5)
println(newFruitPrices) // Map(apple -> 1.0, banana -> 0.5, cherry -> 2.0)
Mutable Map
import scala.collection.mutable
val fruitPrices = mutable.Map("apple" -> 1.0, "banana" -> 0.5)
fruitPrices += ("cherry" -> 2.0)
println(fruitPrices) // Map(apple -> 1.0, banana -> 0.5, cherry -> 2.0)
Advanced Map Operations
Merging Maps
You can merge two maps using the ++ operator or the ++= method for mutable maps.
val map1 = Map(1 -> "one", 2 -> "two")
val map2 = Map(2 -> "two revised", 3 -> "three")
val mergedMap = map1 ++ map2
println(mergedMap) // Map(1 -> one, 2 -> two revised, 3 -> three)
For mutable maps:
val map1 = mutable.Map(1 -> "one", 2 -> "two")
val map2 = Map(2 -> "two revised", 3 -> "three")
map1 ++= map2
println(map1) // Map(1 -> one, 2 -> two revised, 3 -> three)
Grouping
The groupBy method groups elements of a collection by a specified key function.
val words = List("apple", "banana", "cherry", "date", "apricot")
val groupedByFirstLetter = words.groupBy(_.head)
println(groupedByFirstLetter)
// Map(a -> List(apple, apricot), b -> List(banana), c -> List(cherry), d -> List(date))
Transforming Keys and Values
You can transform the keys and values of a map using the mapKeys and mapValues methods (these methods are available in more recent versions of Scala).
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5)
// Transform keys
val upperCaseKeys = fruitPrices.map { case (fruit, price) => (fruit.toUpperCase, price) }
println(upperCaseKeys) // Map(APPLE -> 1.0, BANANA -> 0.5)
// Transform values
val discountedPrices = fruitPrices.mapValues(price => price * 0.9)
println(discountedPrices) // Map(apple -> 0.9, banana -> 0.45)
Conversions
You can convert maps to other collection types, such as lists or arrays.
val fruitPrices = Map("apple" -> 1.0, "banana" -> 0.5)
val fruitList = fruitPrices.toList
println(fruitList) // List((apple,1.0), (banana,0.5))
val fruitArray = fruitPrices.toArray
println(fruitArray.mkString(", ")) // (apple,1.0), (banana,0.5)
Set
A Set is a collection of unique elements. The Set trait has two subtraits, scala.collection.immutable.Set and scala.collection.mutable.Set. By default, Scala uses immutable sets.
Creating Sets
You can create a Set using the Set companion object.
val fruit = Set("apple", "banana", "cherry")
val numbers = Set(1, 2, 3, 4, 5)
Basic Operations on Sets
Adding and Removing Elements
- Add: Use the
+operator to add an element. - Remove: Use the
-operator to remove an element.
val fruit = Set("apple", "banana", "cherry")
val moreFruit = fruit + "date"
println(moreFruit) // Set(apple, banana, cherry, date)
val lessFruit = fruit - "banana"
println(lessFruit) // Set(apple, cherry)
For mutable sets, you can modify the set in place.
import scala.collection.mutable
val fruit = mutable.Set("apple", "banana", "cherry")
fruit += "date"
println(fruit) // Set(apple, banana, cherry, date)
fruit -= "banana"
println(fruit) // Set(apple, cherry, date)
Checking Membership
Use the contains method or the apply method (which is called using parentheses).
val fruit = Set("apple", "banana", "cherry")
println(fruit.contains("banana")) // true
println(fruit("banana")) // true
println(fruit("date")) // false
Common Set Methods
Union, Intersection, and Difference
- Union: Combines two sets, including all elements from both sets.
- Intersection: Returns elements that are present in both sets.
- Difference: Returns elements that are present in the first set but not in the second.
val set1 = Set(1, 2, 3)
val set2 = Set(3, 4, 5)
val unionSet = set1 | set2 // Set(1, 2, 3, 4, 5)
val intersectionSet = set1 & set2 // Set(3)
val differenceSet = set1 &~ set2 // Set(1, 2)
println(unionSet)
println(intersectionSet)
println(differenceSet)
Subsets and Supersets
- Subset: Checks if a set is a subset of another set.
- Superset: Checks if a set is a superset of another set.
val set1 = Set(1, 2, 3)
val set2 = Set(1, 2)
println(set2.subsetOf(set1)) // true
println(set1.subsetOf(set2)) // false
Immutable vs Mutable Sets
Immutable sets cannot be changed after they are created, while mutable sets can be updated in place.
Immutable Set
val fruit = Set("apple", "banana", "cherry")
val moreFruit = fruit + "date"
println(fruit) // Set(apple, banana, cherry)
println(moreFruit) // Set(apple, banana, cherry, date)
Mutable Set
import scala.collection.mutable
val fruit = mutable.Set("apple", "banana", "cherry")
fruit += "date"
println(fruit) // Set(apple, banana, cherry, date)
Conversions
You can convert sets to other collection types, such as lists or arrays.
val numbers = Set(1, 2, 3, 4, 5)
val numberList = numbers.toList
println(numberList) // List(1, 2, 3, 4, 5)
val numberArray = numbers.toArray
println(numberArray.mkString(", ")) // 1, 2, 3, 4, 5
Lazy List
A LazyList is a lazy, immutable collection that represents a sequence of elements. Elements of a LazyList are computed only when needed, which allows for efficient handling of potentially infinite sequences.
Creating LazyLists
You can create a LazyList using the LazyList companion object.
val lazyList = LazyList(1, 2, 3, 4, 5)
You can also create an infinite LazyList using the LazyList.cons method or the #:: operator.
// Infinite LazyList of natural numbers
def from(n: Int): LazyList[Int] = n #:: from(n + 1)
val naturals = from(1)
Accessing Elements
Head and Tail
- Head: The first element of the LazyList.
- Tail: The rest of the LazyList (which is lazily evaluated).
val lazyList = LazyList(1, 2, 3, 4, 5)
println(lazyList.head) // 1
println(lazyList.tail) // LazyList(<not computed>)
Taking Elements
You can take the first n elements of a LazyList using the take method.
val naturals = from(1)
val firstFive = naturals.take(5)
println(firstFive.toList) // List(1, 2, 3, 4, 5)
Basic Operations on LazyLists
Mapping
The map method transforms each element of the LazyList using a given function.
val lazyList = LazyList(1, 2, 3, 4, 5)
val squaredLazyList = lazyList.map(n => n * n)
println(squaredLazyList.take(5).toList) // List(1, 4, 9, 16, 25)
Filtering
The filter method selects elements of the LazyList that satisfy a predicate.
val lazyList = LazyList(1, 2, 3, 4, 5)
val evenLazyList = lazyList.filter(_ % 2 == 0)
println(evenLazyList.take(2).toList) // List(2, 4)
Reducing
The reduce method combines the elements of the LazyList using a binary operation.
val lazyList = LazyList(1, 2, 3, 4, 5)
val sum = lazyList.reduce(_ + _)
println(sum) // 15
Folding
The fold method combines elements of the LazyList using a binary operation and a starting value.
val lazyList = LazyList(1, 2, 3, 4, 5)
val sum = lazyList.fold(0)(_ + _)
println(sum) // 15
Infinite LazyLists
One of the key advantages of LazyLists is the ability to handle infinite sequences.
val naturals = from(1)
println(naturals.take(10).toList) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
You can create more complex infinite sequences.
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map { case (a, b) => a + b }
println(fibs.take(10).toList) // List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
Advanced Operations
Lazy Evaluation
LazyLists are lazily evaluated, meaning elements are computed only when accessed.
val lazyList = LazyList(1, 2, 3, 4, 5)
println(lazyList) // LazyList(<not computed>)
println(lazyList.force) // LazyList(1, 2, 3, 4, 5)
Pattern Matching
You can use pattern matching to deconstruct LazyLists.
val lazyList = LazyList(1, 2, 3, 4, 5)
lazyList match {
case LazyList() => println("The LazyList is empty")
case head #:: tail => println(s"The head is $head and the tail is $tail")
}
// Output: The head is 1 and the tail is LazyList(<not computed>)
Conversions
You can convert LazyLists to other collection types, such as lists or arrays.
val lazyList = LazyList(1, 2, 3, 4, 5)
val list = lazyList.toList
println(list) // List(1, 2, 3, 4, 5)
val array = lazyList.toArray
println(array.mkString(", ")) // 1, 2, 3, 4, 5
List all
Scala's List collection is a fundamental data structure in the Scala standard library, providing a wide range of functions for creating, transforming, querying, and manipulating lists. Here is a comprehensive overview of the most common functions available for List in Scala:
Creating Lists
-
apply: Creates a list from the given elements.
val list = List(1, 2, 3) -
fill: Creates a list filled with a specified value.
val list = List.fill(3)("a") // List("a", "a", "a") -
tabulate: Creates a list based on a tabulation function.
val list = List.tabulate(5)(n => n * n) // List(0, 1, 4, 9, 16) -
range: Creates a list containing a sequence of numbers.
val list = List.range(1, 5) // List(1, 2, 3, 4)
Basic Operations
-
head: Returns the first element.
val head = list.head -
tail: Returns the list without the first element.
val tail = list.tail -
isEmpty: Checks if the list is empty.
val empty = list.isEmpty -
length: Returns the length of the list.
val length = list.length
Adding Elements to a List
- Prepending Elements
-
::: Prepend a single element to the beginning of the list.
val list = List(1, 2, 3) val prependedList = 0 :: list println(prependedList) // List(0, 1, 2, 3) -
:::: Prepend another list to the beginning of the list.
val list1 = List(2, 3, 4) val list2 = List(0, 1) val combinedList = list2 ::: list1 println(combinedList) // List(0, 1, 2, 3, 4)
- Appending Elements
-
:+: Append a single element to the end of the list.
val list = List(1, 2, 3) val appendedList = list :+ 4 println(appendedList) // List(1, 2, 3, 4) -
++: Append another list to the end of the list.
val list1 = List(1, 2, 3) val list2 = List(4, 5, 6) val combinedList = list1 ++ list2 println(combinedList) // List(1, 2, 3, 4, 5, 6)
ListBuffer Adding and Removing
Because a ListBuffer is mutable the adding and removing can be done on the same instance of the list.
All the mutable collection do have these functions
They a located in the Growable trait
Apart from these functions List and ListBuffer do have the same functions This is also valid for: Array, Vector, Seq, Range
import scala.collection.mutable.ListBuffer
val buffer = ListBuffer(1, 2, 3)
-
+=: Appends an element to the buffer.
buffer += 4 // ListBuffer(1, 2, 3, 4) -
++=: Appends multiple elements to the buffer.
buffer ++= Seq(5, 6) // ListBuffer(1, 2, 3, 4, 5, 6) -
-=: Removes an element from the buffer.
buffer -= 3 // ListBuffer(1, 2, 4, 5, 6) -
--=: Removes multiple elements from the buffer.
buffer --= Seq(1, 4) // ListBuffer(2, 5, 6) -
clear: Removes all elements from the buffer.
buffer.clear() // ListBuffer()
Element Access
-
apply: Accesses the element at a specific index.
val elem = list(1) -
indexOf: Finds the index of the first occurrence of an element.
val index = list.indexOf(2) -
lastIndexOf: Finds the index of the last occurrence of an element.
val lastIndex = list.lastIndexOf(2)
Transformations
-
map: Transforms each element by applying a function.
val mapped = list.map(_ * 2) -
flatMap: Transforms each element using a function and flattens the result.
val flatMapped = list.flatMap(x => List(x, x * 2)) -
collect: Transforms elements using a partial function.
val collected = list.collect { case x if x % 2 == 0 => x * 2 }
Filtering
-
filter: Selects elements that satisfy a predicate.
val filtered = list.filter(_ % 2 == 0) -
filterNot: Selects elements that do not satisfy a predicate.
val filterNot = list.filterNot(_ % 2 == 0) -
partition: Splits the list into two lists based on a predicate.
val (evens, odds) = list.partition(_ % 2 == 0)
Aggregation
-
reduce: Combines elements using a binary function.
val sum = list.reduce(_ + _) -
fold: Combines elements using a binary function and an initial value.
val sum = list.fold(0)(_ + _) -
scan: Produces a list of intermediate results.
val scanSum = list.scan(0)(_ + _)
Slicing and Dicing
-
slice: Extracts a sublist from a specified range.
val sliced = list.slice(1, 3) -
take: Takes the first n elements.
val taken = list.take(2) -
drop: Drops the first n elements.
val dropped = list.drop(2) -
splitAt: Splits the list at a specified position.
val (left, right) = list.splitAt(2) -
takeWhile: Takes elements while a predicate is true.
val takenWhile = list.takeWhile(_ < 3) -
dropWhile: Drops elements while a predicate is true.
val droppedWhile = list.dropWhile(_ < 3)
Searching
-
exists: Checks if any element satisfies a predicate.
val hasEven = list.exists(_ % 2 == 0) -
forall: Checks if all elements satisfy a predicate.
val allEven = list.forall(_ % 2 == 0) -
find: Finds the first element that satisfies a predicate.
val firstEven = list.find(_ % 2 == 0)
Zipping and Unzipping
-
zip: Combines two lists into a list of pairs.
val zipped = list.zip(List("a", "b", "c")) -
zipWithIndex: Pairs elements with their indices.
val indexed = list.zipWithIndex -
unzip: Splits a list of pairs into two lists.
val (numbers, letters) = List((1, 'a'), (2, 'b')).unzip
Sorting
-
sorted: Sorts the list.
val sorted = list.sorted -
sortBy: Sorts the list by a specified function.
val sortedBy = list.sortBy(-_) -
sortWith: Sorts the list using a comparator function.
val sortedWith = list.sortWith(_ > _)
Grouping
- groupBy: Groups elements by a specified function.
val grouped = list.groupBy(_ % 2)
Conversion
-
toArray: Converts the list to an array.
val array = list.toArray -
toSet: Converts the list to a set.
val set = list.toSet -
mkString: Converts the list to a string with a specified separator.
val str = list.mkString(", ")
Advanced Functions
-
permutations: Returns an iterator of all permutations of the list.
val perms = list.permutations.toList -
combinations: Returns an iterator of all combinations of the list.
val combs = list.combinations(2).toList -
sliding: Returns an iterator over sliding windows.
val windows = list.sliding(2).toList
Example Usage
Here are some example usages of the above functions:
val numbers = List(1, 2, 3, 4, 5)
// Basic Operations
val head = numbers.head // 1
val tail = numbers.tail // List(2, 3, 4, 5)
val isEmpty = numbers.isEmpty // false
val length = numbers.size // 5
// Element Access
val elem = numbers(1) // 2
val index = numbers.indexOf(2) // 1
// Transformations
val mapped = numbers.map(_ * 2) // List(2, 4, 6, 8, 10)
val flatMapped = numbers.flatMap(x => List(x, x * 2)) // List(1, 2, 2, 4, 3, 6, 4, 8, 5, 10)
// Filtering
val filtered = numbers.filter(_ % 2 == 0) // List(2, 4)
// Aggregation
val sum = numbers.reduce(_ + _) // 15
// Slicing and Dicing
val sliced = numbers.slice(1, 3) // List(2, 3)
val taken = numbers.take(2) // List(1, 2)
val dropped = numbers.drop(2) // List(3, 4, 5)
// Searching
val hasEven = numbers.exists(_ % 2 == 0) // true
// Zipping and Unzipping
val zipped = numbers.zip(List("a", "b", "c")) // List((1, "a"), (2, "b"), (3, "c"))
// Sorting
val sorted = numbers.sorted // List(1, 2, 3, 4, 5)
// Grouping
val grouped = numbers.groupBy(_ % 2) // Map(1 -> List(1, 3, 5), 0 -> List(2, 4))
// Conversion
val array = numbers.toArray // Array(1, 2, 3, 4, 5)
val set = numbers.toSet // Set(1, 2, 3, 4, 5)
val str = numbers.mkString(", ") // "1, 2, 3, 4, 5"
Map
In Scala, Map is a collection of key-value pairs where each key is unique. Scala provides a wide variety of methods to create, transform, query, and manipulate maps. Here's a comprehensive overview of the most common functions available for Map in Scala:
Creating Maps
-
apply: Creates a map from given key-value pairs.
val map = Map(1 -> "one", 2 -> "two", 3 -> "three") -
empty: Creates an empty map.
val emptyMap = Map.empty[Int, String]
Basic Operations
-
get: Retrieves the value associated with a key, returning
Option.val value = map.get(2) // Some("two") val missing = map.get(4) // None -
apply: Retrieves the value associated with a key, throwing an exception if the key is not found.
val value = map(2) // "two" -
contains: Checks if the map contains a key.
val exists = map.contains(2) // true val notExists = map.contains(4) // false -
isEmpty: Checks if the map is empty.
val empty = map.isEmpty // false -
size: Returns the size of the map.
val size = map.size // 3
Adding and Removing Elements
-
+: Adds a key-value pair to the map.
val newMap = map + (4 -> "four") -
-: Removes a key-value pair by key.
val smallerMap = map - 2 -
++: Adds multiple key-value pairs to the map.
val morePairs = map ++ Map(4 -> "four", 5 -> "five") -
--: Removes multiple keys from the map.
val fewerPairs = map -- List(1, 2)
Transformations
-
map: Transforms each key-value pair using a function.
val mapped = map.map { case (k, v) => (k, v.toUpperCase) } -
flatMap: Transforms each key-value pair using a function that returns an iterable, and flattens the result.
val flatMapped = map.flatMap { case (k, v) => List(k -> v, k -> v.toUpperCase) } -
collect: Transforms key-value pairs using a partial function.
val collected = map.collect { case (k, v) if k % 2 == 0 => (k, v.toUpperCase) }
Filtering
-
filter: Selects key-value pairs that satisfy a predicate.
val filtered = map.filter { case (k, v) => k % 2 == 0 } -
filterKeys: Selects key-value pairs whose keys satisfy a predicate.
val filteredKeys = map.filterKeys(_ % 2 == 0) -
filterNot: Selects key-value pairs that do not satisfy a predicate.
val filterNot = map.filterNot { case (k, v) => k % 2 == 0 }
Aggregation
-
foldLeft: Aggregates values from left to right.
val foldedLeft = map.foldLeft("") { case (acc, (k, v)) => acc + v } -
foldRight: Aggregates values from right to left.
val foldedRight = map.foldRight("") { case ((k, v), acc) => acc + v }
Merging Maps
- ++: Merges two maps, with the second map overwriting keys in the first map.
val merged = map ++ Map(2 -> "TWO", 4 -> "four")
Submaps and Key Sets
-
keys: Returns an iterable of all keys.
val keys = map.keys // Iterable(1, 2, 3) -
values: Returns an iterable of all values.
val values = map.values // Iterable("one", "two", "three") -
keySet: Returns a set of all keys.
val keySet = map.keySet // Set(1, 2, 3)
Transformations by Keys and Values
-
mapValues: Transforms the values using a function.
val mappedValues = map.mapValues(_.toUpperCase) -
transform: Transforms both keys and values using a function.
val transformed = map.transform { case (k, v) => v.toUpperCase }
Grouping
- groupBy: Groups the map by a function applied to the keys.
val groupedByMod = map.groupBy { case (k, v) => k % 2 }
Conversion
-
toList: Converts the map to a list of key-value pairs.
val list = map.toList // List((1, "one"), (2, "two"), (3, "three")) -
toArray: Converts the map to an array of key-value pairs.
val array = map.toArray // Array((1, "one"), (2, "two"), (3, "three")) -
toSeq: Converts the map to a sequence of key-value pairs.
val seq = map.toSeq // Seq((1, "one"), (2, "two"), (3, "three"))
Example Usage
Here are some example usages of the above functions:
val map = Map(1 -> "one", 2 -> "two", 3 -> "three")
// Basic Operations
val value = map.get(2) // Some("two")
val missing = map.get(4) // None
val exists = map.contains(2) // true
val size = map.size // 3
// Adding and Removing Elements
val newMap = map + (4 -> "four") // Map(1 -> "one", 2 -> "two", 3 -> "three", 4 -> "four")
val smallerMap = map - 2 // Map(1 -> "one", 3 -> "three")
val morePairs = map ++ Map(4 -> "four", 5 -> "five") // Map(1 -> "one", 2 -> "two", 3 -> "three", 4 -> "four", 5 -> "five")
val fewerPairs = map -- List(1, 2) // Map(3 -> "three")
// Transformations
val mapped = map.map { case (k, v) => (k, v.toUpperCase) } // Map(1 -> "ONE", 2 -> "TWO", 3 -> "THREE")
val flatMapped = map.flatMap { case (k, v) => List(k -> v, k -> v.toUpperCase) } // Map(1 -> "one", 1 -> "ONE", 2 -> "two", 2 -> "TWO", 3 -> "three", 3 -> "THREE")
val collected = map.collect { case (k, v) if k % 2 == 0 => (k, v.toUpperCase) } // Map(2 -> "TWO")
// Filtering
val filtered = map.filter { case (k, v) => k % 2 == 0 } // Map(2 -> "two")
val filteredKeys = map.filterKeys(_ % 2 == 0) // Map(2 -> "two")
val filterNot = map.filterNot { case (k, v) => k % 2 == 0 } // Map(1 -> "one", 3 -> "three")
// Aggregation
val foldedLeft = map.foldLeft("") { case (acc, (k, v)) => acc + v } // "onetwothree"
val foldedRight = map.foldRight("") { case ((k, v), acc) => acc + v } // "threetwoone"
// Merging Maps
val merged = map ++ Map(2 -> "TWO", 4 -> "four") // Map(1 -> "one", 2 -> "TWO", 3 -> "three", 4 -> "four")
// Submaps and Key Sets
val keys = map.keys // Iterable(1, 2, 3)
val values = map.values // Iterable("one", "two", "three")
val keySet = map.keySet // Set(1, 2, 3)
// Transformations by Keys and Values
val mappedValues = map.mapValues(_.toUpperCase) // Map(1 -> "ONE", 2 -> "TWO", 3 -> "THREE")
val transformed = map.transform { case (k, v) => v.toUpperCase } // Map(1 -> "ONE", 2 -> "TWO", 3 -> “THREE”)
// Grouping
val groupedByMod = map.groupBy { case (k, v) => k % 2 } // Map(0 -> Map(2 -> “two”), 1 -> Map(1 -> “one”, 3 -> “three”))
// Conversion
val list = map.toList // List((1, “one”), (2, “two”), (3, “three”))
val array = map.toArray // Array((1, “one”), (2, “two”), (3, “three”))
val seq = map.toSeq // Seq((1, “one”), (2, “two”), (3, “three”))
Set
In Scala, Set is a collection that contains no duplicate elements and has no defined order. It provides various methods for creating, transforming, querying, and manipulating sets. Here’s a comprehensive overview of the most common functions available for Set in Scala:
Creating Sets
-
apply: Creates a set from the given elements.
val set = Set(1, 2, 3) -
empty: Creates an empty set.
val emptySet = Set.empty[Int]
Basic Operations
-
contains: Checks if the set contains a specific element.
val containsTwo = set.contains(2) // true -
isEmpty: Checks if the set is empty.
val empty = set.isEmpty // false -
size: Returns the size of the set.
val size = set.size // 3 -
add: Adds an element to the set (returns a new set, since sets are immutable).
val newSet = set + 4 // Set(1, 2, 3, 4) -
remove: Removes an element from the set (returns a new set).
val smallerSet = set - 2 // Set(1, 3)
Transformations
-
map: Transforms each element by applying a function.
val mapped = set.map(_ * 2) // Set(2, 4, 6) -
flatMap: Transforms each element using a function and flattens the result.
val flatMapped = set.flatMap(x => Set(x, x * 2)) // Set(1, 2, 3, 4, 6) -
collect: Transforms elements using a partial function.
val collected = set.collect { case x if x % 2 == 0 => x * 2 } // Set(4)
Filtering
-
filter: Selects elements that satisfy a predicate.
val filtered = set.filter(_ % 2 == 0) // Set(2) -
filterNot: Selects elements that do not satisfy a predicate.
val filterNot = set.filterNot(_ % 2 == 0) // Set(1, 3) -
partition: Splits the set into two sets based on a predicate.
val (evens, odds) = set.partition(_ % 2 == 0) println(evens) // Set(2) println(odds) // Set(1, 3)
Aggregation
-
reduce: Combines elements using a binary function.
val sum = set.reduce(_ + _) // 6 -
fold: Combines elements using a binary function and an initial value.
val sum = set.fold(0)(_ + _) // 6
Combining Sets
-
union: Returns the union of two sets.
val otherSet = Set(3, 4, 5) val unionSet = set.union(otherSet) // Set(1, 2, 3, 4, 5) // or val unionSet2 = set | otherSet // Set(1, 2, 3, 4, 5) -
intersect: Returns the intersection of two sets.
val intersectSet = set.intersect(otherSet) // Set(3) // or val intersectSet2 = set & otherSet // Set(3) -
diff: Returns the difference of two sets.
val diffSet = set.diff(otherSet) // Set(1, 2) // or val diffSet2 = set &~ otherSet // Set(1, 2)
Subset and Superset
-
subsetOf: Checks if one set is a subset of another.
val isSubset = Set(1, 2).subsetOf(set) // true -
supersetOf: Checks if one set is a superset of another.
val isSuperset = set.supersetOf(Set(1, 2)) // true
Conversion
-
toList: Converts the set to a list.
val list = set.toList // List(1, 2, 3) -
toArray: Converts the set to an array.
val array = set.toArray // Array(1, 2, 3) -
toSeq: Converts the set to a sequence.
val seq = set.toSeq // Seq(1, 2, 3) -
toMap: Converts a set of key-value pairs to a map.
val pairSet = Set((1, "one"), (2, "two")) val map = pairSet.toMap // Map(1 -> "one", 2 -> "two")
Example Usage
Here are some example usages of the above functions:
val set = Set(1, 2, 3)
// Basic Operations
val containsTwo = set.contains(2) // true
val empty = set.isEmpty // false
val size = set.size // 3
// Transformations
val mapped = set.map(_ * 2) // Set(2, 4, 6)
val flatMapped = set.flatMap(x => Set(x, x * 2)) // Set(1, 2, 3, 4, 6)
// Filtering
val filtered = set.filter(_ % 2 == 0) // Set(2)
// Aggregation
val sum = set.reduce(_ + _) // 6
// Combining Sets
val otherSet = Set(3, 4, 5)
val unionSet = set.union(otherSet) // Set(1, 2, 3, 4, 5)
val intersectSet = set.intersect(otherSet) // Set(3)
val diffSet = set.diff(otherSet) // Set(1, 2)
// Subset and Superset
val isSubset = Set(1, 2).subsetOf(set) // true
val isSuperset = set.supersetOf(Set(1, 2)) // true
// Conversion
val list = set.toList // List(1, 2, 3)
val array = set.toArray // Array(1, 2, 3)
val seq = set.toSeq // Seq(1, 2, 3)
val pairSet = Set((1, "one"), (2, "two"))
val map = pairSet.toMap // Map(1 -> "one", 2 -> "two")
// Advanced Functions
val combs = set.subsets(2).toList // List(Set(1, 2), Set(1, 3), Set(2, 3))
val allSubsets = set.subsets().toList // List(Set(), Set(1), Set(2), Set(3), Set(1, 2), Set(1, 3), Set(2, 3), Set(1, 2, 3))
Generics
scala> List(1,2,3,4)
val res0: List[Int] = List(1, 2, 3, 4)
scala> val optX = Option(3)
val optX: Option[Int] = Some(3)
case class Pair[A](first: A, second: A)
// defined case class Pair
scala> val intPair = Pair[Int](3, 4)
val intPair: Pair[Int] = Pair(3,4)
scala> val doublePair = Pair[Double](1.1, 3.14)
val doublePair: Pair[Double] = Pair(1.1,3.14)
Defining Generic Classes and Traits
In Scala, you can define generic classes and traits using square brackets to specify type parameters.
class Box[T] {
private var content: Option[T] = None
def put(item: T): Unit = {
content = Some(item)
}
def get(): Option[T] = content
}
trait Comparable[T] {
def compareTo(other: T): Int
}
Using Generic Classes
val intBox = new Box[Int]
intBox.put(123)
val stringBox = new Box[String]
stringBox.put("Hello Generics")
println(intBox.get()) // Outputs: Some(123)
println(stringBox.get()) // Outputs: Some("Hello Generics")
Defining and Using Generic Methods
Scala allows defining methods with their own type parameters, which can be useful for utility methods.
object Utils {
def getFirstElement[T](list: List[T]): Option[T] = list.headOption
}
val numbers = List(1, 2, 3)
val names = List("Alice", "Bob", "Charlie")
println(Utils.getFirstElement(numbers)) // Outputs: Some(1)
println(Utils.getFirstElement(names)) // Outputs: Some("Alice")
Function values
In Scala a function is a value like other values. It can be
- assigned to another variable,
- passed as a function parameter
- returned as function value
Function as variable
The function value is the function name.
Calling the function is adding the parameter list between round brackets
This way we can convert a method into a function
def add(a: Int, b: Int): Int = a + b
val adder = add
val result = adder(3, 4)
In the example usage, we define a sequence of input numbers and pass it to transformNumbers, along with a function that squares its input. The result is a new sequence with the squares of the input values.
Function as parameter
In Scala, you can pass functions as parameters to other functions. This allows you to define higher-order functions, which are functions that take other functions as arguments or return functions as values.
Here's an example of defining a function that takes another function as a parameter:
def transformNumbers(numbers: List[Int], f: Int => Int): List[Int] =
numbers.map(f)
// Example usage
val input = List(1, 2, 3, 4, 5)
val output = transformNumbers(input, x => x * x) // List(1, 4, 9, 16, 25)
In this example, we define a function named transformNumbers that takes two parameters: a sequence of Int values named numbers, and a function named f that takes an Int and returns an Int. The function body uses the map method to apply f to each element of numbers, returning a new sequence with the transformed values.
Functions as return values
In Scala, you can define functions that return other functions as their result. This allows you to create new functions with specialized behavior by partially applying parameters or selecting behavior based on runtime conditions.
Here's an example of defining a function that returns another function:
def createGreetingFunction(prefix: String): String => String =
(name: String) => s"$prefix, $name!"
// Example usage
val greetHello = createGreetingFunction("Hello")
val greetHi = createGreetingFunction("Hi")
val message1 = greetHello("Mary") // "Hello, Mary!"
val message2 = greetHi("John") // "Hi, John!"
In this example, we define a function named createGreetingFunction that takes a single parameter prefix of type String, and returns another function that takes a String parameter and returns a String. The returned function concatenates the prefix and the input name parameter with a comma and space in between.
In the example usage, we call createGreetingFunction twice to create two new functions, greetHello and greetHi, which prepend the strings "Hello" and "Hi" to their input arguments, respectively. We then call these functions with different input strings to produce two different output messages.
Recursion
Recursion is a technique used in functional programming where a function calls itself to solve a problem. Recursion is a powerful tool that can help simplify complex problems by breaking them down into smaller, simpler problems. Scala supports recursion, and here are some examples of recursive functions in Scala:
Factorial
The factorial of a non-negative integer n is the product of all positive integers less than or equal to n. The factorial function can be defined recursively as follows:
def factorial(n: Int): Int =
if n == 0 then 1
else n * factorial(n - 1)
// example usage
factorial(5) // returns 120
Fibonacci
The Fibonacci sequence is a series of numbers in which each number is the sum of the two preceding numbers. The sequence starts with 0 and 1, and the next number is always the sum of the previous two numbers. The Fibonacci sequence can be defined recursively as follows:
def fibonacci(n: Int): Int =
if n <= 1 then n
else fibonacci(n - 1) + fibonacci(n - 2)
// example usage
fibonacci(6) // returns 8
Binary search
Binary search is a search algorithm that works by repeatedly dividing the search interval in half. The algorithm starts with an interval covering the whole array. If the value of the search key is less than the item in the middle of the interval, narrow the interval to the lower half. Otherwise, narrow it to the upper half. Binary search can be defined recursively as follows:
def binarySearch(arr: Array[Int], target: Int, low: Int, high: Int): Int =
if low > high then -1
else
val mid = low + (high - low) / 2
if arr(mid) == target then mid
else if arr(mid) < target then
binarySearch(arr, target, mid + 1, high)
else
binarySearch(arr, target, low, mid - 1)
// example usage
val arr = Array(1, 3, 5, 7, 9)
binarySearch(arr, 5, 0, arr.length - 1) // returns 2
Recursion Drawbacks
Recursion is a powerful technique that can simplify complex problems in functional programming. However, there are some potential disadvantages to using recursion that should be considered when writing recursive functions:
-
Stack overflow: One of the main disadvantages of recursion is the possibility of stack overflow, especially if the recursive function is called with a large input. Each recursive call adds a new frame to the call stack, and if the stack becomes too deep, it can result in a stack overflow error. While this can be mitigated in some languages with tail recursion optimization, Scala does not support tail recursion optimization for all cases, so it's important to be careful when writing recursive functions.
-
Performance: Recursive functions can sometimes be slower than their iterative counterparts due to the overhead of creating new stack frames with each recursive call. This can be especially true in languages that don't support tail recursion optimization, like Scala. In some cases, an iterative approach may be more efficient.
-
Readability: Recursive functions can sometimes be more difficult to understand than their iterative counterparts, especially for programmers who are not familiar with recursion. Recursive functions can require some mental gymnastics to understand, as they depend on the function calling itself, which can be a tricky concept to grasp.
-
Debugging: Debugging recursive functions can be more challenging than debugging iterative functions, especially if the recursion is deep. It can be difficult to keep track of the sequence of function calls and the state of variables at each step.
Overall, recursion can be a powerful tool in functional programming, but it's important to be aware of its potential drawbacks and use it judiciously.
tail-recursion
Abstract Data Type
Option
With Option you can eliminate the use of null in Scala.
An Option has two values Some or None.
- None wraps the null pointer
- Some wraps the value
scala> val optStr = Option("hello")
val optStr: Option[String] = Some(hello)
scala> optStr match
| case Some(value) => s"the result is: $value"
| case None => "there is no result"
|
val res5: String = the result is: hello
scala> val optStr = Option(null)
val optStr: Option[Null] = None
scala> optStr match
| case Some(value) => s"the result is: $value"
| case None => "there is no result"
|
val res6: String = there is no result
scala> val map = Map(1 -> "a", 2 -> "b", 3 -> "c")
val map: Map[Int, String] = Map(1 -> a, 2 -> b, 3 -> c)
scala> map(3)
val res0: String = c
scala> map(4)
java.util.NoSuchElementException: key not found: 4
at scala.collection.immutable.Map$Map3.apply(Map.scala:399)
... 40 elided
scala> map.get(3)
val res1: Option[String] = Some(c)
scala> map.get(4)
val res2: Option[String] = None
Try
`Try' wraps an exception.
An Option has two values Some or None.
- Failure wraps the exception
- Success wraps the value
val x = 3 / 0
java.lang.ArithmeticException: / by zero
... 40 elided
scala> import scala.util.{Try, Success, Failure}
scala> val x = Try(3 / 0)
val x: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)
scala> val x = Try(3 / 1)
val x: scala.util.Try[Int] = Success(3)
scala> x match
| case Success(value) => s"calculation successful with the value: $value"
| case Failure(ex) => s"calculation failure with exception: $ex"
|
val res1: String = calculation successful with the value: 3
Either
An Either also wrap two value Right and Left.
It is something between the Option and the Try and more general, but less used.
In this example an exception is wrapped inside an Either.
Try would be more natural.
def returnEither(value: String): Either[NumberFormatException, Int] =
try
Right(value.toInt)
catch
case ex: NumberFormatException => Left(ex)
def resultEither(value: String) =
returnEither(value) match
case Right(value) => s"Right value: $value"
case Left(ex) => s"Left exception: $ex"
scala> resultEither("12")
val res2: String = Right value: 12
scala> resultEither("ab")
val res3: String = Left exception: java.lang.NumberFormatException: For input string: "ab"
Exercises
Exercise 1
- 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] = ???
For Comprehension
Understanding flatMap
flatMap is used when you have a collection of elements, and you want to apply an operation to each element where that operation itself produces a collection. flatMap then merges all these collections into one. The signature of flatMap in the context of a collection looks something like this:
def flatMap[B](f: A => IterableOnce[B]): Iterable[B]
- A: The type of elements in the original collection.
- B: The type of elements in the returned collections and the final flattened collection.
Example Usage of flatMap:
val sentences = List("Hello World", "Scala is fun")
val words = sentences.flatMap(sentence => sentence.split(" "))
// words: List[String] = List("Hello", "World", "Scala", "is", "fun")
In this example, flatMap splits each string into words, producing a list of words for each sentence, and then flattens these lists into a single list.
For-Comprehensions
For-comprehensions provide a syntactic sugar for chaining multiple operations, including flatMap, map, and withFilter (a lazy version of filter). They allow for more readable code, especially when dealing with nested flatMap and map operations.
The general form of a for-comprehension is:
for
x <- xs
y <- ys
if condition
yield expression
This is equivalent to:
xs.flatMap(x => ys.withFilter(y => condition).map(y => expression))
Example Using flatMap and for-Comprehension:
Imagine you want to find all pairs of numbers from two lists that sum up to a certain value.
Using flatMap and map:
val listA = List(1, 2, 3)
val listB = List(3, 4, 5)
val targetSum = 7
val pairs = listA.flatMap(a => listB.map(b => (a, b))).filter { case (a, b) => a + b == targetSum }
// pairs: List[(Int, Int)] = List((2,5), (3,4))
Using for-comprehension:
val pairsFor = for
a <- listA
b <- listB
if a + b == targetSum
yield (a, b)
// pairsFor: List[(Int, Int)] = List((2,5), (3,4))
Try
Handling exceptions is a crucial part of developing robust Scala applications. Scala provides a try-catch construct similar to other languages like Java, but with some functional twists that make it powerful and expressive. Scala's approach encourages the use of immutable values and provides mechanisms to deal with exceptions in a functional way.
Basic Try-Catch
In Scala, you use try-catch blocks to catch exceptions. The catch block uses pattern matching to handle different types of exceptions.
try
// Code that might throw an exception
val result = 10 / 0
catch
case e: ArithmeticException => println("Arithmetic Exception caught: " + e.getMessage)
case e: Exception => println("General exception caught: " + e.getMessage)
finally
// Optional finally block executes regardless of whether an exception was caught
println("Finally block executed")
The Try Type
Scala provides a Try type that represents a computation that may either result in an exception (Failure) or return a successfully computed value (Success). It is a better way to handle exceptions when working with functional programming paradigms.
To use Try, you need to import it from the Scala library:
import scala.util.{Try, Success, Failure}
You can wrap a computation in a Try, which will catch any non-fatal exceptions and return a Success with the value if the computation is successful, or a Failure with the exception if it is not.
val result: Try[Int] = Try(10 / 0)
You can then pattern match on the result:
result match
case Success(value) => println(s"Computation successful: $value")
case Failure(exception) => println(s"Computation failed with exception: ${exception.getMessage}")
Chaining Operations with Try
One of the benefits of using Try is the ability to chain operations without having to explicitly check for exceptions at each step.
def divide(a: Int, b: Int): Try[Int] = Try(a / b)
val result = divide(10, 0).map(_ * 2)
result match
case Success(value) => println(s"Result: $value")
case Failure(exception) => println(s"Error: ${exception.getMessage}")
For-Comprehensions with Try
For-comprehensions can be used with Try to perform multiple operations that may fail, in a clean and readable way:
val forResult =
for
a <- Try(10 / 5) // This succeeds
b <- Try(a / 0) // This fails
yield b * 2
forResult match {
case Success(value) => println(s"Result: $value")
case Failure(exception) => println(s"Error: ${exception.getMessage}")
}
In the above example, the computation automatically stops at the first failure, and forResult becomes a Failure containing the exception.
Throw
In Scala, unlike Java, you're not required to declare checked exceptions using throws in the method signature. Scala doesn't distinguish between checked and unchecked exceptions; all exceptions are unchecked, meaning the compiler does not force you to catch or declare any exceptions. However, for documentation purposes or when interfacing with Java code, you might want to indicate that a method can throw an exception.
To annotate a method with the information that it might throw an exception, you can use the @throws annotation. This can improve readability and maintainability of your Scala code, especially for developers coming from a Java background or when Scala code is being called from Java.
Here’s how to use the @throws annotation in Scala:
def divide(a: Int, b: Int): Int =
if (b == 0) then
throw new ArithmeticException("Division by zero.")
else
a / b
// Annotating the method with @throws
@throws(classOf[ArithmeticException])
def divideWithAnnotation(a: Int, b: Int): Int =
if (b == 0) then
throw new ArithmeticException("Division by zero.")
else
a / b
In this example, the divideWithAnnotation method is explicitly annotated to indicate that it might throw an ArithmeticException. The @throws annotation takes the class of the exception you're warning about as a parameter.
This annotation is particularly useful when Scala methods are invoked from Java code, as it will inform Java developers about the potential exceptions, allowing them to handle these exceptions appropriately.
Remember, while the @throws annotation can be helpful for documentation and interoperability with Java, it does not change how Scala code behaves or is compiled. Scala treats all exceptions as unchecked, and the use of @throws is purely informational.
FP Design Patterns
Functional programming emphasizes the use of functions, immutability, and expressions over statements. Many traditional design patterns can be adapted to a functional style in Scala. Here are examples of functional programming design patterns in Scala:
1. Function Composition
Function composition is a fundamental concept in functional programming, where multiple functions are combined to produce a new function.
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
val addOneAndDouble: Int => Int = addOne.andThen(double)
// Usage
println(addOneAndDouble(3)) // 8 (3 + 1, then 4 * 2)
2. Higher-Order Functions
Higher-order functions are functions that take other functions as parameters or return functions as results.
def applyOperation(x: Int, y: Int, operation: (Int, Int) => Int): Int = operation(x, y)
val add: (Int, Int) => Int = _ + _
val multiply: (Int, Int) => Int = _ * _
// Usage
println(applyOperation(3, 4, add)) // 7
println(applyOperation(3, 4, multiply)) // 12
3. Currying
Currying transforms a function with multiple parameters into a series of functions, each with a single parameter.
def add(x: Int)(y: Int): Int = x + y
val addThree: Int => Int = add(3)
// Usage
println(addThree(4)) // 7
println(add(2)(3)) // 5
4. Partial Function Application
Partial function application involves fixing a few arguments of a function, producing another function of smaller arity.
def multiply(x: Int, y: Int, z: Int): Int = x * y * z
val multiplyBy2And3: Int => Int = multiply(2, 3, _)
// Usage
println(multiplyBy2And3(4)) // 24 (2 * 3 * 4)
5. Memoization
Memoization is an optimization technique to cache the results of expensive function calls and return the cached result when the same inputs occur again.
def memoize[I, O](f: I => O): I => O = {
val cache = scala.collection.mutable.Map[I, O]()
(input: I) =>
cache.getOrElseUpdate(input, f(input))
}
val fib: Int => Int = {
def fibHelper(n: Int): Int = n match {
case 0 => 0
case 1 => 1
case _ => fib(n - 1) + fib(n - 2)
}
memoize(fibHelper)
}
// Usage
println(fib(10)) // 55
6. Functor
A functor is a structure that can be mapped over. In Scala, the map method on collections is an example.
val list = List(1, 2, 3)
val doubledList = list.map(_ * 2)
// Usage
println(doubledList) // List(2, 4, 6)
7. Monad
A monad is a design pattern used to handle program-wide concerns in a functional way. The flatMap method is used to chain operations.
val list = List(1, 2, 3)
val result = list.flatMap(x => List(x, x * 2))
// Usage
println(result) // List(1, 2, 2, 4, 3, 6)
8. Option Monad
The Option monad is used to represent optional values, providing a way to handle the absence of a value.
val someValue: Option[Int] = Some(5)
val noValue: Option[Int] = None
val result = someValue.map(_ * 2).getOrElse(0)
val result2 = noValue.map(_ * 2).getOrElse(0)
// Usage
println(result) // 10
println(result2) // 0
9. Either Monad
The Either monad is used to represent a value of one of two possible types (a disjoint union). It is often used for error handling.
def divide(x: Int, y: Int): Either[String, Int] = {
if (y == 0) Left("Division by zero")
else Right(x / y)
}
val result = divide(10, 2) match {
case Right(value) => s"Result: $value"
case Left(error) => s"Error: $error"
}
val result2 = divide(10, 0) match {
case Right(value) => s"Result: $value"
case Left(error) => s"Error: $error"
}
// Usage
println(result) // Result: 5
println(result2) // Error: Division by zero
10. Lazy Evaluation
Lazy evaluation is a technique where expressions are not evaluated until their results are needed.
lazy val expensiveComputation: Int = {
println("Computing...")
42
}
// Usage
println("Before accessing lazy value")
println(expensiveComputation) // "Computing..." followed by 42
println(expensiveComputation) // 42 (without recomputing)
11. Type Classes
Type classes enable ad-hoc polymorphism in a functional way.
trait Show[A] {
def show(a: A): String
}
object ShowInstances {
implicit val intShow: Show[Int] = (a: Int) => a.toString
implicit val stringShow: Show[String] = (a: String) => a
}
object Show {
def apply[A](implicit instance: Show[A]): Show[A] = instance
def show[A: Show](a: A): String = Show[A].show(a)
}
// Usage
import ShowInstances._
println(Show.show(123)) // "123"
println(Show.show("Hello")) // "Hello"
Testing in Scala
Testing is a crucial part of software development that ensures your code behaves as expected. Scala, being a popular language on the JVM, has several testing frameworks that you can use to write unit tests, integration tests, and more. Here are some of the most widely used testing frameworks in the Scala ecosystem:
1. ScalaTest
ScalaTest is one of the most flexible and comprehensive testing libraries available for Scala. It supports different styles of testing, making it adaptable to various testing needs and preferences. ScalaTest can be used for unit testing, property-based testing, and even integration testing. It integrates seamlessly with other tools and libraries like SBT (Scala Build Tool), Maven, Jenkins, and IntelliJ IDEA.
Features:
- Supports multiple testing styles (FlatSpec, FunSuite, WordSpec, etc.).
- Easy integration with mocking frameworks.
- Rich matchers for more readable assertions.
2. Specs2
Specs2 is a library for writing executable software specifications. With Specs2, you can write specifications for your Scala code using a very expressive Domain Specific Language (DSL). It's designed to produce well-formatted and detailed reports.
Features:
- Supports Behavior-Driven Development (BDD) and Acceptance Test-Driven Development (ATDD).
- Integrates with ScalaCheck for property-based testing.
- Provides matchers for various types of assertions.
- Supports mocking through integration with libraries like Mockito.
3. ScalaCheck
ScalaCheck is a library for property-based testing. It allows you to specify properties that your code should satisfy, and then automatically generates test data to verify those properties. It's inspired by Haskell's QuickCheck and can be used standalone or integrated with ScalaTest or Specs2.
Features:
- Automatic test data generation.
- Supports custom generators and shrinkers.
- Can be integrated with ScalaTest and Specs2.
4. uTest
uTest is a simple testing framework with a minimalistic design. It aims to provide the essentials for testing Scala code without additional complexity. uTest's syntax is straightforward, making it easy to write and read tests.
Features:
- Simple and concise syntax.
- Supports asynchronous testing.
- Provides detailed test reports.
5. minitest
minitest is a small, opinionated library for writing tests in Scala. It aims to be simple and fast, with a focus on being functional. minitest supports both synchronous and asynchronous tests, making it suitable for a wide range of testing scenarios.
Features:
- Simple and lightweight.
- Supports asynchronous testing out of the box.
- Easy integration with SBT.
Choosing a Testing Framework
The choice of a testing framework depends on your project's specific needs, your team's preferences, and the kind of tests you plan to write. ScalaTest and Specs2 are more feature-rich and offer more flexibility, making them suitable for larger projects with diverse testing requirements. ScalaCheck is excellent for property-based testing, adding another layer of confidence in your code's correctness. For simpler projects, or if you prefer a more minimalistic approach, uTest or minitest might be more appropriate.
Regardless of the framework you choose, the important thing is to write tests. Testing helps catch bugs early, improves code quality, and can even guide your design, leading to more maintainable and robust Scala applications.
FlatSpec
ScalaTest is a versatile testing library in Scala that supports different testing styles, making it adaptable to various testing needs. Here's a basic tutorial on how to get started with ScalaTest, covering setup, writing tests, and running them.
Setting Up
To use ScalaTest, you need to add it as a dependency in your build tool configuration. If you're using sbt, add the following to your build.sbt:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test
Ensure you're using the latest version of ScalaTest by checking the ScalaTest website.
Writing Tests
ScalaTest supports multiple testing styles. We'll use the FlatSpec style for this tutorial, which is a good choice for behavior-driven development (BDD).
Create a Scala class or object for the component you want to test. Here's a simple example:
object Calculator {
def add(a: Int, b: Int): Int = a + b
}
Now, create a test class in your src/test/scala directory. Here's how you might test the Calculator object:
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class CalculatorSpec extends AnyFlatSpec with Matchers {
"The Calculator" should "add two numbers correctly" in {
Calculator.add(2, 3) should be (5)
}
it should "add negative numbers correctly" in {
Calculator.add(-1, -1) should be (-2)
}
it should "add a number to zero correctly" in {
Calculator.add(0, 5) should be (5)
}
}
Running Tests
If you're using sbt, you can run your tests from the terminal with the following command:
sbt test
This command will compile your test classes and run all tests, reporting the results in the terminal.
Understanding the Test Code
AnyFlatSpec: This is a base class for writing tests in a "flat" specification style. You describe behaviors and write tests for each behavior.Matchers: This trait provides a domain-specific language (DSL) for expressing assertions about values in your tests. For example,should beis part of this DSL."The Calculator" should "add two numbers correctly" in: This syntax describes a behavior and a test scenario. The block followingincontains the test code for that scenario.
Further Testing Capabilities
ScalaTest offers much more than simple assertions:
- ScalaTest with ScalaCheck: For property-based testing, ScalaTest can be integrated with ScalaCheck to automatically generate test cases.
- Asynchronous Testing: ScalaTest supports testing asynchronous code, making it easier to test Futures and other async operations.
- Before and After Hooks: ScalaTest provides
BeforeAndAfterandBeforeAndAfterEachtraits that you can mix into your test classes to set up preconditions or clean up after your tests.
FunSuite
Using AnyFunSuite with ScalaTest allows for writing tests in a very flexible and straightforward way, combining the benefits of specification and testing. Let's apply this to a Calculator example, focusing on creating a suite of tests to verify its functionality.
Step 1: Implement the Calculator
Create a simple Calculator object with basic arithmetic operations. Place this in your src/main/scala directory if you're following standard Scala project structure.
object Calculator {
def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
def multiply(a: Int, b: Int): Int = a * b
def divide(a: Int, b: Int): Option[Int] = if (b == 0) None else Some(a / b)
}
Step 2: Set Up ScalaTest Dependency
Make sure ScalaTest is included in your build.sbt:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test
Step 3: Write Tests Using AnyFunSuite
Now, create a test class for the Calculator using AnyFunSuite. This class should be in the src/test/scala directory.
import org.scalatest.funsuite.AnyFunSuite
class CalculatorTest extends AnyFunSuite {
test("Calculator.add should return the sum of two numbers") {
assert(Calculator.add(1, 2) === 3)
}
test("Calculator.subtract should return the difference of two numbers") {
assert(Calculator.subtract(5, 3) === 2)
}
test("Calculator.multiply should return the product of two numbers") {
assert(Calculator.multiply(4, 3) === 12)
}
test("Calculator.divide should return the quotient of two numbers") {
assert(Calculator.divide(10, 2) === Some(5))
}
test("Calculator.divide should return None when dividing by zero") {
assert(Calculator.divide(5, 0) === None)
}
}
Step 4: Running Tests
To run your tests, use sbt from the command line in your project root:
sbt test
This command will execute all tests in your project, including the CalculatorTest suite you just defined.
Understanding the Test Code
- AnyFunSuite: This is a base class for writing tests in a "fun" style, which is a simple way to write tests where you define tests as functions.
- test Method: Each test is defined using the
testmethod, where you provide a descriptive name for the test and a block of code that implements the test. - Assertions: The
assertmethod is used to verify that the operation performed by theCalculatoryields the expected result.
FlatSpec
Using FlatSpec in ScalaTest allows for behavior-driven development (BDD) style testing, which is very expressive and helps in writing readable tests. Let's apply FlatSpec to test the Calculator example, focusing on behavior descriptions and testing scenarios.
Step 1: Implement the Calculator
First, define a simple Calculator object with basic arithmetic operations:
object Calculator {
def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
def multiply(a: Int, b: Int): Int = a * b
def divide(a: Int, b: Int): Either[String, Int] =
if (b == 0) Left("Division by zero")
else Right(a / b)
}
Notice the divide method returns an Either[String, Int] to handle division by zero gracefully.
Step 2: Set Up ScalaTest Dependency
Ensure ScalaTest is added to your build.sbt:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test
Step 3: Write Tests Using FlatSpec
Create a test class for Calculator using FlatSpec. This style allows you to specify the behavior of an operation and then describe tests that confirm the behavior.
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class CalculatorSpec extends AnyFlatSpec with Matchers {
"The Calculator add method" should "return the sum of two numbers" in {
Calculator.add(1, 2) should be (3)
}
it should "return the correct sum when adding negative numbers" in {
Calculator.add(-1, -2) should be (-3)
}
"The Calculator subtract method" should "return the difference of two numbers" in {
Calculator.subtract(5, 3) should be (2)
}
"The Calculator multiply method" should "return the product of two numbers" in {
Calculator.multiply(5, 4) should be (20)
}
"The Calculator divide method" should "return the quotient of two numbers" in {
Calculator.divide(10, 2) should be (Right(5))
}
it should "return an error for division by zero" in {
Calculator.divide(5, 0) should be (Left("Division by zero"))
}
}
Step 4: Running Tests
To run your tests, use the sbt command line tool in your project root:
sbt test
This will compile and execute all test suites in your project, including the CalculatorSpec.
Understanding the Test Code
AnyFlatSpecwithMatchers: This combination allows for a BDD style testing.Matchersprovide a readable way to assert conditions.- Behavior Descriptions: Using
shouldand strings, you can describe the behavior you're testing. It's a natural language-like approach that makes tests easier to read and understand. - Tests for Each Behavior: Under each behavior description, use
it shouldto specify different scenarios or conditions you're testing for that behavior.
Async Test
Testing asynchronous code in Scala, especially when working with futures, requires a testing framework that can handle asynchronous results. ScalaTest provides excellent support for writing tests for asynchronous operations through its AsyncTestSuite traits. Let's demonstrate how to write an asynchronous test for a hypothetical asynchronous version of the Calculator that returns Future[Int] results.
Step 1: Implement the Async Calculator
First, we'll define an asynchronous Calculator object. For demonstration purposes, we'll make the add method asynchronous, returning a Future[Int].
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
object AsyncCalculator {
def add(a: Int, b: Int): Future[Int] = Future {
// Simulate a computation that takes time
Thread.sleep(100)
a + b
}
}
Step 2: Set Up ScalaTest Dependency
Ensure you have ScalaTest added to your build.sbt file:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test
Step 3: Write an Asynchronous Test
ScalaTest's AsyncFunSuite is designed for testing asynchronous code. It allows tests to return Future[Assertion]. Here's how you can test the asynchronous add method of our AsyncCalculator.
import org.scalatest.funsuite.AsyncFunSuite
import org.scalatest.matchers.should.Matchers
class AsyncCalculatorSpec extends AsyncFunSuite with Matchers {
test("AsyncCalculator.add should correctly add two numbers") {
val sumFuture = AsyncCalculator.add(1, 2) // This returns a Future[Int]
sumFuture.map(sum => sum should be (3)) // The assertion is wrapped in a map and returns a Future[Assertion]
}
}
Step 4: Running the Test
Execute your asynchronous tests by running:
sbt test
Understanding the Test Code
- AsyncFunSuite: This suite is specifically designed for asynchronous code, allowing tests to return
Future[Assertion]directly. ScalaTest handles the future and correctly reports the test result once the future completes. - Future.map: The assertion is made within the
mapof theFuture, which transforms the result of the future (Int) into a test assertion. The entire expression returns aFuture[Assertion], whichAsyncFunSuiteawaits.
Functional Testing
Functional test suites in Scala can be created using various testing frameworks like ScalaTest, Specs2, or ScalaCheck. These frameworks allow for testing Scala applications in a way that aligns with functional programming principles, ensuring that functions and methods behave as expected across a wide range of inputs and conditions. Functional testing typically involves testing the software against its functional requirements and specifications to ensure it performs its intended tasks correctly.
Using ScalaTest for Functional Testing
ScalaTest is a versatile testing framework that supports multiple styles of testing, making it well-suited for functional testing. Here's a brief guide on setting up functional test suites with ScalaTest.
Setup
Ensure ScalaTest is added to your build.sbt:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test
Writing Functional Tests
Suppose you have a function that you want to test functionally. For example, a simple service that processes user data:
case class User(id: Int, name: String, email: String)
trait UserService {
def findUserById(id: Int): Option[User]
def updateUser(user: User): Option[User]
}
You can write functional tests to verify the behavior of these methods under various conditions:
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers
class UserServiceSpec extends AnyFunSpec with Matchers {
describe("UserService") {
val mockUsers = Map(
1 -> User(1, "John Doe", "john@example.com"),
2 -> User(2, "Jane Doe", "jane@example.com")
)
// Mock implementation for testing
val userService: UserService = new UserService {
def findUserById(id: Int): Option[User] = mockUsers.get(id)
def updateUser(user: User): Option[User] = Some(user.copy(name = user.name.toUpperCase))
}
describe("findUserById") {
it("should return a user for a valid id") {
userService.findUserById(1) should be(Some(User(1, "John Doe", "john@example.com")))
}
it("should return None for an invalid id") {
userService.findUserById(999) should be(None)
}
}
describe("updateUser") {
it("should update the user's name to upper case") {
val originalUser = User(2, "Jane Doe", "jane@example.com")
val updatedUser = userService.updateUser(originalUser)
updatedUser should be(Some(User(2, "JANE DOE", "jane@example.com")))
}
}
}
}
Running Tests
Execute your tests using sbt:
sbt test
Advantages of Functional Test Suites
- Specification Validation: Functional tests ensure that the application behaves according to its specifications, verifying each feature's correct implementation.
- Comprehensive Coverage: By focusing on the software's functionality, you can cover more cases and usage scenarios, which might be missed by unit tests alone.
- Refactoring Confidence: Functional tests provide a safety net that allows developers to refactor code with confidence, knowing that changes won't break the application's intended behavior.
Specs2
Specs2 is a powerful Scala library designed for writing software specifications and tests. It encourages behavior-driven development (BDD) and is well-suited for both unit and acceptance testing. Let's use Specs2 to test the Calculator example, focusing on behavior and readability.
Setting Up
First, ensure Specs2 is included in your build.sbt dependencies:
libraryDependencies ++= Seq(
"org.specs2" %% "specs2-core" % "4.10.6" % Test,
"org.specs2" %% "specs2-matcher-extra" % "4.10.6" % Test // For extra matchers
)
Check for the latest version of Specs2 to use in your project.
The Calculator Object
Here's a simple Calculator object to test:
object Calculator {
def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
def multiply(a: Int, b: Int): Int = a * b
def divide(a: Int, b: Int): Option[Int] = if (b == 0) None else Some(a / b)
}
Writing Specifications with Specs2
Create a new specification in your src/test/scala directory. Specs2 supports a flexible way to write tests, including the should and must styles. Here, we'll use the should style for clarity:
import org.specs2.mutable.Specification
class CalculatorSpec extends Specification {
"Calculator" should {
"correctly add two numbers" in {
Calculator.add(1, 2) must_== 3
}
"correctly subtract two numbers" in {
Calculator.subtract(5, 3) must_== 2
}
"correctly multiply two numbers" in {
Calculator.multiply(3, 4) must_== 12
}
"return None when dividing by zero" in {
Calculator.divide(5, 0) must beNone
}
"correctly divide two numbers" in {
Calculator.divide(10, 2) must beSome(5)
}
}
}
Running Tests
To run your Specs2 tests, use sbt:
sbt test
This command will compile and execute all test specifications.
Understanding the Specs2 Specification
Specification: This class is extended to define a Specs2 specification. Each specification contains examples of expected behavior.must_==: This matcher tests for equality. It's part of Specs2's rich set of matchers that allow for expressive tests.beNoneandbeSome: These option matchers are used for testing ScalaOptionvalues, making it easy to write clear and concise tests for optional values.
Parallel Programming
Let's break down what happens when the following code is run in parallel:
var x = 3
x = x + 1
x = x * 2
println(x)
Running in Parallel
When the code runs sequentially in a single thread, it executes predictably. However, if the operations are run in parallel, the behavior can become unpredictable due to race conditions. Let's examine this in the context of parallel execution.
Potential Parallel Execution:
Let's assume we try to run the increment and multiplication steps in parallel threads:
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._
var x = 3
val increment = Future {
x = x + 1
}
val multiply = Future {
x = x * 2
}
Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x)
Potential Issues in Parallel Execution:
- Race Condition:
- A race condition occurs when two or more threads access shared data and try to change it simultaneously. The result depends on the order in which the threads execute.
- Indeterminate Results:
- The final value of
xdepends on the timing of theincrementandmultiplyoperations. - If
incrementruns first and completes beforemultiplystarts,xwill be incremented to 4 first, then multiplied by 2 to get 8. - If
multiplyruns first and completes beforeincrementstarts,xwill be multiplied by 2 to get 6 first, then incremented by 1 to get 7. - If both operations interfere with each other, intermediate states can result in completely unpredictable values.
Examples of Possible Outcomes:
-
Outcome 1:
incrementreadsxas 3.multiplyreadsxas 3.incrementwritesxas 4.multiplywritesxas 6.- Final value of
xis 6.
-
Outcome 2:
multiplyreadsxas 3.incrementreadsxas 3.multiplywritesxas 6.incrementwritesxas 7.- Final value of
xis 7.
-
Outcome 3:
incrementcompletes entirely first,xbecomes 4.multiplycompletes next,xbecomes 8.- Final value of
xis 8.
Solutions to Ensure Correctness:
To ensure that the operations on x are performed correctly and predictably, you can use synchronization mechanisms:
- Synchronized Block:
- Ensure that only one thread can modify
xat a time.
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._
var x = 3
val lock = new AnyRef
val increment = Future {
lock.synchronized {
x = x + 1
}
}
val multiply = Future {
lock.synchronized {
x = x * 2
}
}
Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x)
- Atomic Variables:
- Use atomic variables to ensure atomicity of operations.
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._
val x = new AtomicInteger(3)
val increment = Future {
x.incrementAndGet()
}
val multiply = Future {
x.updateAndGet(n => n * 2)
}
Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x.get())
Semaphore
Using locks in parallel programming is a common way to ensure that only one thread accesses a critical section of code at a time. This helps prevent race conditions and ensures data consistency. Scala relies on Java’s concurrency utilities for lock management. Here, we'll use ReentrantLock from the java.util.concurrent.locks package.
Using ReentrantLock in Scala
Importing the necessary classes:
import java.util.concurrent.locks.ReentrantLock
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
import scala.concurrent.duration._
Creating and using a ReentrantLock:
-
Create the lock:
val lock = new ReentrantLock() -
Acquire and release the lock:
Use
lock.lock()to acquire the lock andlock.unlock()to release it. It’s crucial to ensure that the lock is always released, typically using atry-finallyblock.def accessResource(threadName: String): Unit = { lock.lock() try { println(s"$threadName is accessing the resource.") Thread.sleep(1000) // Simulate some work with the resource } finally { println(s"$threadName is releasing the resource.") lock.unlock() } } -
Run tasks in parallel:
Use Scala's
Futureto run tasks in parallel.val tasks = for (i <- 1 to 10) yield Future { accessResource(s"Thread-$i") } val aggregatedFuture = Future.sequence(tasks) -
Wait for all tasks to complete:
Use
Await.resultto wait for all futures to complete.Await.result(aggregatedFuture, 10.seconds)
Complete Example
Here is the complete code example putting all the pieces together:
import java.util.concurrent.locks.ReentrantLock
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
import scala.concurrent.duration._
object LockExample extends App {
// Create a reentrant lock
val lock = new ReentrantLock()
// Define a function to access the resource
def accessResource(threadName: String): Unit = {
lock.lock()
try {
println(s"$threadName is accessing the resource.")
Thread.sleep(1000) // Simulate some work with the resource
} finally {
println(s"$threadName is releasing the resource.")
lock.unlock()
}
}
// Run tasks in parallel using Futures
val tasks = for (i <- 1 to 10) yield Future {
accessResource(s"Thread-$i")
}
// Aggregate all futures
val aggregatedFuture = Future.sequence(tasks)
// Wait for all tasks to complete
Await.result(aggregatedFuture, 10.seconds)
}
LockExample.main(Array())
Explanation
- ReentrantLock Creation:
new ReentrantLock()creates a reentrant lock. - Access Resource Function:
accessResourcefunction locks the critical section, performs the operation (simulated byThread.sleep), and then unlocks the critical section. Thetry-finallyblock ensures the lock is always released. - Parallel Tasks: A sequence of 10 futures is created, each calling
accessResourcewith a different thread name. - Aggregated Future:
Future.sequencecombines the futures into a single future that completes when all the tasks are done. - Await Completion:
Await.resultwaits for the aggregated future to complete, ensuring the main program does not exit prematurely.
Alternative: Using synchronized
In addition to using ReentrantLock, Scala provides a simpler way to achieve mutual exclusion using the synchronized keyword. This is a more concise way to protect critical sections of code.
Using synchronized:
object SynchronizedExample extends App {
// Define a shared resource
var sharedCounter = 0
// Define a function to access the resource
def incrementCounter(threadName: String): Unit = synchronized {
println(s"$threadName is accessing the resource.")
sharedCounter += 1
println(s"$threadName incremented the counter to $sharedCounter.")
Thread.sleep(1000) // Simulate some work with the resource
}
// Run tasks in parallel using Futures
val tasks = for (i <- 1 to 10) yield Future {
incrementCounter(s"Thread-$i")
}
// Aggregate all futures
val aggregatedFuture = Future.sequence(tasks)
// Wait for all tasks to complete
Await.result(aggregatedFuture, 10.seconds)
}
SynchronizedExample.main(Array())
Explanation
- Synchronized Block: The
synchronizedkeyword is used to ensure that only one thread can execute theincrementCounterfunction at a time. - Shared Resource: A shared variable
sharedCounteris incremented by each thread, ensuring mutual exclusion. - Parallel Tasks: A sequence of 10 futures is created, each calling
incrementCounterwith a different thread name. - Aggregated Future:
Future.sequencecombines the futures into a single future that completes when all the tasks are done. - Await Completion:
Await.resultwaits for the aggregated future to complete, ensuring the main program does not exit prematurely.
Threads
Using threads in Scala is similar to using threads in Java since Scala runs on the JVM and can leverage the Java concurrency utilities. Below are examples demonstrating how to create and use threads in Scala.
1. Creating and Starting a Thread
You can create a thread by extending the Thread class and overriding its run method.
class MyThread extends Thread {
override def run(): Unit = {
println(s"Thread ${Thread.currentThread().getName} is running")
}
}
// Usage
val thread1 = new MyThread()
val thread2 = new MyThread()
thread1.start()
thread2.start()
thread1.join() // Wait for thread1 to finish
thread2.join() // Wait for thread2 to finish
2. Using Runnable
Another common way is to implement the Runnable interface and pass an instance to a Thread object.
class MyRunnable extends Runnable {
override def run(): Unit = {
println(s"Runnable ${Thread.currentThread().getName} is running")
}
}
// Usage
val runnable1 = new MyRunnable()
val runnable2 = new MyRunnable()
val thread1 = new Thread(runnable1)
val thread2 = new Thread(runnable2)
thread1.start()
thread2.start()
thread1.join() // Wait for thread1 to finish
thread2.join() // Wait for thread2 to finish
3. Using Anonymous Runnable
You can also use an anonymous Runnable instance.
val thread1 = new Thread(new Runnable {
override def run(): Unit = {
println(s"Anonymous Runnable ${Thread.currentThread().getName} is running")
}
})
val thread2 = new Thread(new Runnable {
override def run(): Unit = {
println(s"Anonymous Runnable ${Thread.currentThread().getName} is running")
}
})
thread1.start()
thread2.start()
thread1.join() // Wait for thread1 to finish
thread2.join() // Wait for thread2 to finish
4. Using Lambda Expressions
In Scala, you can use lambda expressions to create Runnable instances more concisely.
val thread1 = new Thread(() => println(s"Lambda Runnable ${Thread.currentThread().getName} is running"))
val thread2 = new Thread(() => println(s"Lambda Runnable ${Thread.currentThread().getName} is running"))
thread1.start()
thread2.start()
thread1.join() // Wait for thread1 to finish
thread2.join() // Wait for thread2 to finish
5. Synchronization
To avoid race conditions, you might need to synchronize critical sections of your code.
object Counter {
private var count = 0
def increment(): Unit = synchronized {
count += 1
println(s"Count: $count by ${Thread.currentThread().getName}")
}
def getCount: Int = count
}
class IncrementThread extends Thread {
override def run(): Unit = {
for (_ <- 1 to 1000) {
Counter.increment()
}
}
}
// Usage
val threads = List.fill(10)(new IncrementThread())
threads.foreach(_.start())
threads.foreach(_.join())
println(s"Final count: ${Counter.getCount}")
6. Using Executors
For a higher-level approach, you can use the ExecutorService from the java.util.concurrent package to manage a pool of threads.
import java.util.concurrent.{Executors, ExecutorService}
val executor: ExecutorService = Executors.newFixedThreadPool(3)
val tasks = List(
new Runnable {
def run(): Unit = println(s"Task 1 running on ${Thread.currentThread().getName}")
},
new Runnable {
def run(): Unit = println(s"Task 2 running on ${Thread.currentThread().getName}")
},
new Runnable {
def run(): Unit = println(s"Task 3 running on ${Thread.currentThread().getName}")
}
)
tasks.foreach(executor.submit)
executor.shutdown() // Prevent new tasks from being submitted
executor.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS) // Wait for all tasks to complete
7. Using Future and Promise
For more advanced and functional approaches to concurrency, you can use Future and Promise in Scala.
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.util.{Success, Failure}
def longRunningTask(x: Int): Int = {
Thread.sleep(1000) // Simulate a long computation
x * x
}
val future1 = Future {
longRunningTask(10)
}
val future2 = Future {
longRunningTask(20)
}
// Combining futures
val combinedFuture = for {
result1 <- future1
result2 <- future2
} yield result1 + result2
combinedFuture.onComplete {
case Success(value) => println(s"Combined result: $value")
case Failure(exception) => println(s"Failed: $exception")
}
// Waiting for the future to complete (for demonstration purposes only, typically avoid blocking)
Await.result(combinedFuture, 5.seconds)
Parallel Patterns
Parallel programming in Scala can be achieved using various libraries and constructs that leverage the power of modern multi-core processors. Below are some common parallel programming patterns and techniques in Scala, including examples using Scala's Future, parallel collections, and Akka.
1. Futures and Promises
Futures provide a way to handle asynchronous computations. They can be used to perform operations in parallel and handle the results once they are completed.
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.util.{Success, Failure}
def slowComputation(x: Int): Int = {
Thread.sleep(1000) // Simulate a long computation
x * x
}
// Creating a Future
val futureResult: Future[Int] = Future {
slowComputation(10)
}
// Handling the result
futureResult.onComplete {
case Success(value) => println(s"Result: $value")
case Failure(exception) => println(s"Failed: $exception")
}
// Waiting for the future to complete (for demonstration purposes only, typically avoid blocking)
Await.result(futureResult, 2.seconds)
2. Parallel Collections
Parallel collections provide a simple way to parallelize operations on collections.
import scala.collection.parallel.CollectionConverters._
val list = List(1, 2, 3, 4, 5)
// Using parallel collections
val parallelList = list.par
val result = parallelList.map(_ * 2)
// Converting back to sequential collection
val sequentialResult = result.seq
// Usage
println(sequentialResult) // List(2, 4, 6, 8, 10)
3. Akka Actors
Akka provides a powerful model for building concurrent, distributed, and fault-tolerant applications using actors.
First, add the Akka dependencies to your build.sbt:
libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.20"
Then, you can define and use actors:
import akka.actor._
case object Start
case class Compute(x: Int)
class Worker extends Actor {
def receive: Receive = {
case Compute(x) =>
val result = x * x
println(s"Computed $result for $x")
}
}
class Master extends Actor {
val worker = context.actorOf(Props[Worker], "worker")
def receive: Receive = {
case Start =>
for (i <- 1 to 5) worker ! Compute(i)
}
}
// Creating the actor system
val system = ActorSystem("MyActorSystem")
val master = system.actorOf(Props[Master], "master")
// Starting the computation
master ! Start
// Shutting down the system
system.terminate()
4. Parallel Execution with Future Combinators
You can also combine multiple futures to perform parallel operations and combine their results.
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.util.{Success, Failure}
val future1 = Future { slowComputation(10) }
val future2 = Future { slowComputation(20) }
val future3 = Future { slowComputation(30) }
// Combining futures
val combinedFuture = for {
result1 <- future1
result2 <- future2
result3 <- future3
} yield result1 + result2 + result3
// Handling the result
combinedFuture.onComplete {
case Success(value) => println(s"Combined result: $value")
case Failure(exception) => println(s"Failed: $exception")
}
// Waiting for the combined future to complete (for demonstration purposes only, typically avoid blocking)
Await.result(combinedFuture, 5.seconds)
5. Parallel Streams with Akka Streams
Akka Streams provide a powerful way to handle streaming data and back-pressure. Add the Akka Streams dependency to your build.sbt:
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.6.20"
Then, you can define and run a stream:
import akka.actor.ActorSystem
import akka.stream._
import akka.stream.scaladsl._
implicit val system = ActorSystem("MyActorSystem")
implicit val materializer = Materializer(system)
val source = Source(1 to 10)
val sink = Sink.foreach[Int](println)
val flow = Flow[Int].map(_ * 2)
// Connecting the source, flow, and sink
val runnableGraph = source.via(flow).to(sink)
// Running the stream
runnableGraph.run()
// Shutting down the system
system.terminate()
Structure
The Play Framework is known for its simplicity and scalability, largely due to its architecture, which is designed to make web application development more productive and performance-oriented. Here's an overview of the Play Framework's architecture, highlighting its main components and how they interact to serve web applications:
Core Components of Play Framework Architecture
-
Web Server: Play includes a built-in web server (Akka HTTP or Netty in older versions) that eliminates the need for deploying your application on an external web server. This simplification speeds up development and testing processes.
-
Request Handler: At its core, Play uses a lightweight, stateless model to handle requests. When a request comes in, it's handled by a routing mechanism that dispatches it to the appropriate controller action based on the defined routes.
-
Router: The router is a key component that maps incoming HTTP requests to controller actions. Routes are defined in a simple and readable format within the
conf/routesfile, allowing for clean URL design and RESTful architectures. -
Controllers: Controllers are responsible for handling incoming requests and returning responses to the client. They act as the middleman between the user interface and the application's data logic, processing data, calling business logic, and preparing the response (e.g., rendering HTML, returning JSON).
-
Actions: Actions are the actual methods in controllers that process the requests. Each action can perform operations like accessing databases, performing computations, or calling other services before generating a result to send back to the client.
-
Results: Results are what actions return in response to HTTP requests. They include status codes, headers, and the body (which could be HTML, JSON, XML, etc.), telling the client what happened with their request.
-
Models: In Play, models represent the application's data and are typically used in conjunction with a database. Play does not enforce a specific ORM (Object-Relational Mapping) tool, giving developers the freedom to choose the best tool for their application, such as Ebean, Slick, or JPA.
-
Views: Play uses Twirl as its template engine for rendering HTML. Views in Play are essentially Scala functions that take model data and produce HTML output. This allows for dynamic content generation based on the application's state.
Asynchronous and Non-Blocking
One of the key architectural features of Play is its support for asynchronous and non-blocking operations. This is crucial for building scalable applications that can handle long-running operations or IO-bound tasks without blocking threads. Play achieves this by integrating with Scala's Futures and Akka, allowing developers to write non-blocking code that can scale across multiple cores and servers.
Modular Architecture
Play is modular, meaning that applications can include only the components they need. This keeps the application lightweight and efficient. Developers can add modules for various functionalities, including but not limited to database access, authentication, and caching.
Built on Akka
The framework is built on top of Akka, a toolkit for building highly concurrent, distributed, and resilient message-driven applications. This foundation provides Play applications with a robust concurrency model and lets them easily scale out across multiple servers.
Getting started
1. Install sbt (Scala Build Tool)
First, you need to have sbt, the interactive build tool for Scala, installed on your machine. You can download it from the official sbt website. Follow the installation instructions specific to your operating system.
2. Create a New Play Application
Open a terminal or command prompt and run the following command to create a new Play project:
sbt new playframework/play-scala-seed.g8
This command creates a new directory with a simple Play application template. Navigate into your project directory to start working on your application.
3. Explore the Project Structure
The generated project has several important directories and files:
- app/: Contains your Scala application code, including controllers and models.
- conf/: Contains configuration files, including
routeswhich defines URL mappings to controllers. - public/: Contains static assets like images, JavaScript, and CSS files.
- test/: Contains tests for your application.
4. Define Routes
Open the conf/routes file. This is where you define the URLs that your application responds to. Each line in this file consists of an HTTP method, a path, and an action. For example:
GET /hello controllers.HomeController.hello()
5. Implement Controller Actions
Controllers handle incoming requests and return responses. They are located in the app/controllers directory. To respond to our /hello route, you could create a HomeController like this:
package controllers
import play.api.mvc._
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
def hello() = Action { implicit request: Request[AnyContent] =>
Ok("Hello, Play Framework!")
}
}
6. Run Your Application
To run your Play application, open a terminal in your project directory and execute:
sbt run
Your application will start on port 9000 by default. Open a web browser and go to http://localhost:9000/hello to see your application in action.
7. Learn More
This tutorial only scratches the surface of what you can do with the Play Framework. Here are some suggestions for next steps:
- Explore Templates: Learn how to use Twirl, Play's templating engine, to create dynamic HTML content.
- Database Access: Look into integrating a database using Play's built-in support for Slick or other ORMs.
- Authentication: Implement user authentication and authorization for your application.
- Asynchronous Programming: Utilize Play's support for asynchronous programming to handle long-running operations without blocking.
The official Play documentation is an excellent resource for in-depth exploration of these and other topics. The Scala community and Stack Overflow are also great places to find help and learn from others' experiences.
Twirl
Twirl is the templating engine used by the Play Framework for generating dynamic HTML content. It allows developers to write Scala code within HTML templates, which makes it possible to render dynamic data on web pages efficiently. Here's a basic tutorial to get you started with Twirl in a Play Framework application:
Step 1: Understanding Twirl Template Syntax
Twirl templates are basically HTML files with Scala code embedded in them. The files have a .scala.html extension and are located in the app/views directory of a Play project. The syntax for embedding Scala code is straightforward:
@expressionto output the value of an expression.@{code block}for more complex Scala code.@for(item <- items) { ... }to iterate over collections.@if(condition) { ... }for conditionals.
Step 2: Create a Twirl Template
- Navigate to the
app/viewsdirectory. - Create a new file named
hello.scala.html. - Add the following content to create a simple template that takes a
Stringparameter and displays a greeting:
@(name: String)
<!DOCTYPE html>
<html>
<head>
<title>Hello, @name!</title>
</head>
<body>
<h1>Hello, @name!</h1>
</body>
</html>
This template expects a name parameter and uses it to display a greeting.
Step 3: Use the Template in a Controller
Now, let's use this template in a controller to render a dynamic web page:
- Open or create a controller in the
app/controllersdirectory, for example,HomeController.scala. - Add a new action method that renders the
hellotemplate with a name:
package controllers
import play.api.mvc._
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
def hello(name: String) = Action { implicit request: Request[AnyContent] =>
Ok(views.html.hello(name))
}
}
This controller action calls the hello template, passing a name parameter to it, and returns the rendered HTML as an Ok HTTP response.
Step 4: Define a Route
To make the new action accessible via HTTP, you need to define a route:
- Open the
conf/routesfile. - Add a new route definition for the
helloaction:
GET /hello/:name controllers.HomeController.hello(name)
This route maps GET requests to /hello/[name] to the hello action in the HomeController, passing the [name] part of the URL as a parameter.
Step 5: Test Your Application
- Start your Play application by running
sbt runfrom the terminal in your project's root directory. - Open a web browser and navigate to
http://localhost:9000/hello/YourName, replacingYourNamewith any name you wish. - You should see the greeting displayed on the page, dynamically generated by the Twirl template.
Twirl Flow
for
Twirl templates are located in the app/views folder and typically have the .scala.html extension. Let's create a simple template that demonstrates the use of variables, loops, and conditionals.
- Create a new file called
list.scala.htmlin theapp/viewsdirectory. - Add the following content to
list.scala.html:
@(items: List[String], title: String)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
<h1>@title</h1>
@if(items.isEmpty) {
<p>No items to display.</p>
} else {
<ul>
@for(item <- items) {
<li>@item</li>
}
</ul>
}
</body>
</html>
This template accepts a list of strings (items) and a title (title), displaying each item in a list. It also demonstrates how to use conditionals to display a message if the list is empty.
Creating a Controller
Now, let's create a controller that uses our template to render a page.
- Create a new controller file named
ListController.scalain theapp/controllersdirectory. - Add the following Scala code to the controller:
package controllers
import play.api.mvc._
class ListController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
def showList = Action {
val fruits = List("Apple", "Banana", "Cherry")
Ok(views.html.list(fruits, "Fruit List"))
}
}
This controller defines an action called showList that creates a list of fruits and passes it to the list template, along with a title.
Defining Routes
To make the showList action accessible via a web browser, you need to define a route in the conf/routes file.
- Open the
conf/routesfile. - Add the following line to define a route for your action:
GET /list controllers.ListController.showList
This route maps GET requests to /list to the showList action in the ListController.
if
Imagine you have a web application where you need to display a welcome message to logged-in users and a different message to guests. You can use an if statement in a Twirl template to achieve this.
First, create a new Twirl template in the app/views directory of your Play application. Let's name it welcome.scala.html. In this template, you'll check whether the user is logged in by evaluating a boolean parameter isLoggedIn.
@(isLoggedIn: Boolean)
<!DOCTYPE html>
<html>
<head>
<title>Welcome Page</title>
</head>
<body>
@if(isLoggedIn) {
<h1>Welcome back, user!</h1>
} else {
<h1>Welcome, guest!</h1>
<p>Please log in to enjoy our services.</p>
}
</body>
</html>
In this example, the @if(isLoggedIn) statement checks if isLoggedIn is true. If it is, the template renders a welcome back message for the user. If isLoggedIn is false, it displays a welcome message for guests and prompts them to log in.
if-else
You can also use if-else statements to handle multiple conditions. For example, you might want to display different messages based on the user's role:
@(userRole: String)
<!DOCTYPE html>
<html>
<head>
<title>Role-Based Message</title>
</head>
<body>
@if(userRole == "admin") {
<h1>Welcome, Admin!</h1>
} else if(userRole == "member") {
<h1>Welcome, Member!</h1>
} else {
<h1>Welcome, Guest!</h1>
}
</body>
</html>
In this template, userRole is checked to determine the user's role. Based on the role, a corresponding message is displayed. This example shows how you can use else if to add more conditions to your logic.
Integrating with Play Controllers
To render the template with dynamic data, you would pass the necessary information (e.g., isLoggedIn or userRole) from your Play controller when returning the template as a response. Here's an example controller method that renders the first template:
def welcomePage = Action {
val userIsLoggedIn = checkIfUserIsLoggedIn() // Assume this is a method that checks the user's login status
Ok(views.html.welcome(userIsLoggedIn))
}
Login Form
Step 1: Create the Login Form Model
Define a simple case class to represent the login form data. You can place this in a Scala file in the models package, for instance, LoginForm.scala:
package models
case class LoginForm(username: String, password: String)
Step 2: Create the Login Controller
Create a controller that will render the login form and handle form submission. If the username and password match the hardcoded credentials, it will redirect the user to a secure page; otherwise, it will reload the login page with an error.
In the controllers directory, create a file named LoginController.scala:
package controllers
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import models.LoginForm
import javax.inject._
@Singleton
class LoginController @Inject()(cc: MessagesControllerComponents) extends MessagesAbstractController(cc) {
val loginForm: Form[LoginForm] = Form(
mapping(
"username" -> text,
"password" -> text
)(LoginForm.apply)(LoginForm.unapply)
)
def login = Action { implicit request: MessagesRequest[AnyContent] =>
Ok(views.html.login(loginForm))
}
def authenticate = Action { implicit request: MessagesRequest[AnyContent] =>
loginForm.bindFromRequest.fold(
formWithErrors => BadRequest(views.html.login(formWithErrors)),
formData => {
if (formData.username == "user" && formData.password == "password") {
Redirect(routes.HomeController.index()).flashing("success" -> "You are logged in.")
} else {
Redirect(routes.LoginController.login()).flashing("error" -> "Invalid username or password.")
}
}
)
}
}
Step 3: Define Routes
In the conf/routes file, add routes for your login form and authentication process:
GET /login controllers.LoginController.login
POST /authenticate controllers.LoginController.authenticate
GET / controllers.HomeController.index
Ensure you have a route and controller action for /, which will serve as the landing page after a successful login.
Step 4: Create the Login Form View
Create a new Scala HTML file named login.scala.html in the app/views directory. This view will render the login form:
@import helper._
@(form: Form[LoginForm])(implicit request: MessagesRequestHeader)
@main("Login") {
@form(routes.LoginController.authenticate()) {
@CSRF.formField
@inputText(form("username"), '_label -> "Username")
@inputPassword(form("password"), '_label -> "Password")
<button type="submit">Login</button>
}
@request.flash.get("error").map { error =>
<div class="error">@error</div>
}
}
Make sure you have a main template file (main.scala.html) that this login template references to structure the page.
Step 5: Running Your Application
Execute your application:
sbt run
Navigate to http://localhost:9000/login in your browser to see the login form. Try logging in with the username user and the password password.
Todo List
Creating a simple Todo List application with the Play Framework in Scala involves several steps, including setting up your Play project, defining the model, creating controllers and views, and setting up routes for handling HTTP requests. Here's a step-by-step guide to get you started:
1. Setting Up Your Play Project
First, create a new Play Framework project using sbt (Scala's build tool). Open a terminal and run:
sbt new playframework/play-scala-seed.g8
Follow the prompts to name your project. This creates a basic structure for your Play application.
2. Defining the Todo Model
Define a case class to represent a Todo item. You can create a Scala file Todo.scala in the models directory:
package models
case class Todo(id: Long, task: String, isDone: Boolean)
3. Creating the Application Controller
Create a controller to handle the logic for your Todo List application. You might name it TodoController.scala and place it under the controllers directory. Here's how you might start:
package controllers
import models.Todo
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import javax.inject._
@Singleton
class TodoController @Inject()(cc: MessagesControllerComponents) extends MessagesAbstractController(cc) {
private val todos = scala.collection.mutable.ArrayBuffer(
Todo(1, "Learn Play Framework", isDone = false),
Todo(2, "Build a Todo List App", isDone = false)
)
private val todoForm: Form[Todo] = Form(
mapping(
"id" -> ignored(0L),
"task" -> nonEmptyText,
"isDone" -> boolean
)(Todo.apply)(Todo.unapply)
)
def listTodos = Action { implicit request: MessagesRequest[AnyContent] =>
Ok(views.html.listTodos(todos.toList, todoForm))
}
def addTodo = Action { implicit request: MessagesRequest[AnyContent] =>
todoForm.bindFromRequest.fold(
errorForm => BadRequest(views.html.listTodos(todos.toList, errorForm)),
todo => {
val newId = if (todos.isEmpty) 1L else todos.map(_.id).max + 1
todos += todo.copy(id = newId)
Redirect(routes.TodoController.listTodos)
}
)
}
}
4. Defining Routes
In the conf/routes file, add routes for displaying the Todo List and adding a new Todo item:
GET /todos controllers.TodoController.listTodos
POST /todos/add controllers.TodoController.addTodo
5. Creating the Views
You need to create Scala HTML templates for displaying and adding Todo items. You can place these in the views directory. For instance, create listTodos.scala.html:
@(todos: List[Todo], todoForm: Form[Todo])(implicit request: MessagesRequestHeader)
@import helper._
@main("Todo List") {
<h1>Todo List</h1>
<ul>
@for(todo <- todos) {
<li>
@if(todo.isDone) {
<s>@todo.task</s>
} else {
@todo.task
}
</li>
}
</ul>
<h2>Add a new task</h2>
@form(routes.TodoController.addTodo) {
@CSRF.formField
@inputText(todoForm("task"))
<button type="submit">Add</button>
}
}
6. Running Your Application
Use sbt to run your Play application:
sbt run
After your application starts, you can access it by navigating to http://localhost:9000/todos in your web browser.
Notes:
- This example uses an in-memory collection to store Todo items, which means your data won't persist across application restarts. For a real application, you would typically use a database.
- Security measures like input validation and CSRF protection are included, but always consider additional security practices and validations based on your application's needs.
- The UI is very basic. You can enhance it using CSS frameworks like Bootstrap for a better look and feel.
Anorm, Rest-API
Integrating Anorm with an MySQL database in a Play Framework project involves configuring your database connection, setting up the database evolutions, and adjusting your application code to interact with the database. Anorm is a simple data access layer that uses plain SQL to interact with the database, making it a flexible choice for Scala developers. Here’s how you can set it up:
Step 1: Add Dependencies
First, ensure you have the necessary dependencies in your build.sbt file. You'll need the Play Slick dependency and the H2 database driver for local development. If not already included, add them like so:
libraryDependencies ++= Seq(
guice,
jdbc,
"mysql" % "mysql-connector-java" % "8.0.33",
"org.playframework.anorm" %% "anorm" % "2.7.0",
"org.scalatestplus.play" %% "scalatestplus-play" % "7.0.1" % Test,
)
Replace version numbers with the latest versions suitable for your project setup.
Step 2: Configure the Database
In the conf/application.conf file, configure your MySQL database connection:
db.default.driver=com.mysql.cj.jdbc.Driver
db.default.url="jdbc:mysql://localhost:3306/todo"
db.default.user="root"
db.default.password= "mysql"
Setup the database connection pool: hikaricp
# Number of database connections
# See https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
fixedConnectionPool = 9
play.db {
prototype {
hikaricp.minimumIdle = ${fixedConnectionPool}
hikaricp.maximumPoolSize = ${fixedConnectionPool}
}
}
# Job queue sized to HikariCP connection pool
database.dispatcher {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = ${fixedConnectionPool}
}
}
Create the DatabaseExecutionContext
package models
import org.apache.pekko.actor.ActorSystem
import play.api.libs.concurrent.CustomExecutionContext
import javax.inject.*
/**
* This class is a pointer to an execution context configured to point to "database.dispatcher"
* in the "application.conf" file.
*/
@Singleton
class DatabaseExecutionContext @Inject()(system: ActorSystem) extends CustomExecutionContext(system, "database.dispatcher")
Step 3: Create database and table
It uses a todo database, which we'll have to create ourselves inside MySQL
And a todo table has is created
create database todo;
use todo;
create table todo (
id bigint auto_increment primary key,
task varchar(255)
);
Step 4: Interacting with the Database Using Anorm
Here we create a class Todo and the database service TodoRepository
package models
import javax.inject.Inject
import scala.util.{ Failure, Success }
import anorm._
import anorm.SqlParser.{ get, str }
import play.api.db.DBApi
import scala.concurrent.Future
case class Todo(id: Option[Long] = None, task: String)
object Todo {
implicit def toParameters: ToParameterList[Todo] =
Macro.toParameters[Todo]
def unapply(c: Todo): Option[(Option[Long], String)]= Some((c.id, c.task))
}
@javax.inject.Singleton
class TodoRepository @Inject()(dbapi: DBApi)(implicit ec: DatabaseExecutionContext) {
private val db = dbapi.database("default")
private[models] val simple = {
get[Option[Long]]("todo.id") ~ str("todo.task") map {
case id ~ name => Todo(id, name)
}
}
def list(): Future[List[Todo]] = Future {
db.withConnection { implicit connection =>
SQL"SELECT * FROM todo".as(simple.*)
}
}
def create(todo: Todo): Future[Option[Long]] = Future {
db.withConnection { implicit connection =>
SQL("""insert into todo values ( {id}, {task})""")
.bind(todo).executeInsert()
}
}(ec)
}
Step 5: HomeController Implementation
In your Play application under the controllers directory, create a file named HomeController.scala:
package controllers
import models.{Todo, TodoRepository}
import javax.inject.*
import play.api.*
import play.api.libs.json.{Json, OFormat}
import play.api.mvc.*
import scala.concurrent.ExecutionContext
@Singleton
class HomeController @Inject()(todoService: TodoRepository,
cc: MessagesControllerComponents)(implicit ec: ExecutionContext)
extends MessagesAbstractController(cc) {
implicit val toDoFormat: OFormat[Todo] = Json.format[Todo]
def list: Action[AnyContent] = Action.async { implicit request =>
todoService.list().map { todos =>
Ok(Json.toJson(todos))
}
}
def create: Action[AnyContent] = Action.async { implicit request =>
val json = request.body.asJson.get
val todoItem = json.as[Todo]
todoService.create(todoItem).map { id =>
Ok(Json.obj("id" -> id))
}
}
}
routes
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
GET /todos controllers.HomeController.list()
POST /todos controllers.HomeController.create()
Step 6: Running Your Application
Start your Play application using sbt:
sbt run
Step 7: Test with cURL
Test the todo app with cURL
POST
curl -X POST http://localhost:9000/todos \
-H "Content-Type: application/json" \
-d '{"task":"programming"}'
GET
curl -H "Accept: application/json" http://localhost:9000/todos
Result:
[
{
"task":"programming",
"id":1
}
]
With Akka
Integrating Akka actors in a Play Framework application allows you to take advantage of the actor model for handling concurrency, scalability, and fault tolerance. Actors are ideal for scenarios where you need to manage state in an asynchronous, non-blocking manner or when you need to perform long-running or compute-intensive tasks in response to HTTP requests. Here's a basic guide on how to use Akka actors within a Play Framework application:
Step 1: Define an Actor
First, define your actor class. Actors handle messages and can maintain state. Create a Scala class file in the app/actors directory of your Play project (you might need to create the directory first). Here's an example of a simple actor that echoes messages back:
package actors
import akka.actor.Actor
class EchoActor extends Actor {
def receive = {
case message: String => sender() ! message // Echoes the message back to the sender
}
}
Step 2: Configure Actor System
Play Framework automatically provides an ActorSystem for you. You can access this system and use it to create actor instances. The ActorSystem is a heavyweight structure that controls the lifecycle of your actors and is capable of managing hundreds or thousands of actors.
Step 3: Create and Use an Actor in a Controller
Next, create a controller that sends a message to your actor and waits for a response. This example demonstrates how to do this asynchronously using ask pattern from Akka. You'll need to modify your controller to inject the ActorSystem and use it to create actor instances.
Add the following imports to your controller file:
import akka.actor.ActorSystem
import akka.util.Timeout
import actors.EchoActor
import akka.pattern.ask
import scala.concurrent.duration._
import scala.concurrent.Future
import play.api.mvc._
import javax.inject._
Then, define your controller and action method:
@Singleton
class EchoController @Inject()(val controllerComponents: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends BaseController {
implicit val timeout: Timeout = 5.seconds
def echo(message: String) = Action.async {
val echoActor = actorSystem.actorOf(Props[EchoActor], "echo-actor")
val futureResponse: Future[String] = (echoActor ? message).mapTo[String]
futureResponse.map { response =>
Ok(response)
}
}
}
In this controller:
- An
EchoActorinstance is created using theActorSystem. - The
echoaction method sends a message to theEchoActorusing theaskpattern and waits for a response. - The response from the actor is sent back to the client as an HTTP response.
Step 4: Add Routes
Define a route for your action in the conf/routes file:
GET /echo/:message controllers.EchoController.echo(message)
Step 5: Test Your Actor
Run your Play application, and access the route /echo/Hello in your browser or via a tool like curl. You should receive the echoed message in response.
Note
Using actors for simple echo tasks might be overkill. The real power of actors comes into play when managing complex states, performing long-running tasks, or handling a high number of concurrent operations. Remember, actors are just one of many tools offered by Play and Akka for building reactive applications, and they're best used when their specific capabilities align with your application's requirements.
Actor Intro
The Actor Model provides a high-level abstraction for writing concurrent and distributed systems, addressing some of the challenges associated with traditional concurrent programming techniques. Below is an overview tutorial covering the need for actors, basic concepts, communication, and how these are implemented in Akka, a popular toolkit for Scala and Java.
The Need for Actors
Concurrent programming in traditional models (using threads and locks) is hard. It's prone to errors such as deadlocks, race conditions, and complexity in managing state. The Actor Model simplifies concurrency by encapsulating state, behavior, and communication in actors, reducing the cognitive load on developers and minimizing common concurrency issues.
The Actor Model
The Actor Model treats actors as the fundamental unit of computation. In this model, an actor can:
- Process messages asynchronously.
- Maintain private state.
- Create more actors.
- Send messages to other actors.
This model helps in building systems that are naturally concurrent and distributed, promoting message-passing over shared state for interactions.
Actor Concepts
Actors are objects that encapsulate state and behavior. They interact using asynchronous message passing, ensuring that each actor operates concurrently without interfering with the internal state of others.
State: Actors maintain their own state privately, making state changes in response to received messages.
Behavior: The logic that defines how an actor responds to messages.
Mailbox: Each actor has a mailbox where incoming messages are queued before being processed.
Supervision: Actors are arranged in a hierarchy. Parent actors supervise their child actors, deciding on the course of action (e.g., restart, stop) when a child encounters a failure.
Actor Communications
Actors communicate exclusively through asynchronous message passing. This mechanism ensures that actors remain loosely coupled, scalable, and isolated, making the system more resilient. Messages should be immutable to avoid shared state and ensure thread safety.
Akka Actors
Akka is a toolkit and runtime for building highly concurrent, distributed, and fault-tolerant applications on the JVM. It implements the Actor Model, providing a comprehensive set of features for managing actors, communication, and fault tolerance.
Key Features:
- Location transparency: Actors can communicate regardless of their location in the cluster.
- Lightweight: Millions of actors can be spawned on a single machine.
- Fault tolerance: Supervision strategies help manage actor failures.
Akka Actor Application Structure and Naming
An Akka application is structured around actors and their interactions. Actors are organized hierarchically, forming a tree structure where each actor can have child actors.
Naming: Actors in Akka have a path representing their location in the hierarchy, akin to file paths in a filesystem. This path includes the actor system name and the names of parent actors, providing a way to uniquely identify and locate actors within the system.
Actor
Akka Actor Tutorial: Untyped Actors
Akka is a powerful toolkit and runtime for building concurrent, distributed, and fault-tolerant applications on the JVM. In this tutorial, we'll explore Akka's untyped actors, which were the default actors in Akka before the introduction of typed actors in Akka 2.6.
1. Setting Up the Project
First, create a new Scala project and add the Akka dependencies to your build.sbt:
libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.x"
Replace x with the latest minor version of Akka.
2. Basic Concepts
- Actor: A unit of computation that processes messages asynchronously.
- Actor System: A hierarchical group of actors that share common configuration.
- Message: Immutable data passed between actors.
- Mailbox: Queue where incoming messages are stored until processed by the actor.
3. Creating an Actor System
Start by creating an ActorSystem, which will manage and supervise actors:
import akka.actor.{ActorSystem, Props, Actor, ActorRef}
// Create an Actor System
val system = ActorSystem("my-actor-system")
4. Creating a Simple Actor
To define an untyped actor, extend the Actor trait and implement the receive method, which handles incoming messages:
class SimpleActor extends Actor {
def receive: Receive = {
case "hello" => println("Hello, World!")
case "goodbye" => println("Goodbye, World!")
case msg: String => println(s"Received message: $msg")
}
}
5. Instantiating and Sending Messages to an Actor
Actors are instantiated using the Props class, and messages are sent using the ! (tell) operator:
val simpleActor: ActorRef = system.actorOf(Props[SimpleActor], "simple-actor")
// Send messages to the actor
simpleActor ! "hello"
simpleActor ! "How are you?"
simpleActor ! "goodbye"
6. Actor Lifecycle
Actors have a lifecycle managed by the Akka framework:
- Pre-start: Executed when the actor is created.
- Post-stop: Executed when the actor is stopped.
- Restart: Executed when the actor is restarted after a failure.
You can override lifecycle hooks in your actor:
class LifecycleActor extends Actor {
override def preStart(): Unit = println("Actor is starting")
override def postStop(): Unit = println("Actor has stopped")
def receive: Receive = {
case "stop" => context.stop(self)
case msg => println(s"Received: $msg")
}
}
val lifecycleActor: ActorRef = system.actorOf(Props[LifecycleActor], "lifecycle-actor")
lifecycleActor ! "test"
lifecycleActor ! "stop"
7. Actor Hierarchy and Supervision
Actors are organized hierarchically, with parent actors supervising child actors. Supervision strategies define how to handle child actor failures (e.g., restart, stop, resume).
Example of creating child actors:
class ParentActor extends Actor {
override def preStart(): Unit = {
val child = context.actorOf(Props[SimpleActor], "child-actor")
child ! "hello"
}
def receive: Receive = {
case _ =>
}
}
val parentActor: ActorRef = system.actorOf(Props[ParentActor], "parent-actor")
8. Stopping Actors
Actors can be stopped using context.stop(actorRef) or PoisonPill:
simpleActor ! PoisonPill
Stopping an actor triggers its postStop lifecycle hook.
9. Handling Actor Failure
You can define a supervisor strategy to handle child actor failures:
import akka.actor.SupervisorStrategy._
import akka.actor.OneForOneStrategy
class SupervisorActor extends Actor {
override val supervisorStrategy = OneForOneStrategy() {
case _: Exception => Restart
}
def receive: Receive = {
case _ =>
}
}
10. Graceful Shutdown
To gracefully shut down an ActorSystem, use:
system.terminate()
11. Example: Simple Chat System
Let's create a simple chat system with actors representing users and a chat room.
User Actor
class UserActor(name: String) extends Actor {
def receive: Receive = {
case msg: String => println(s"$name received: $msg")
}
}
Chat Room Actor
class ChatRoomActor extends Actor {
var users: Set[ActorRef] = Set()
def receive: Receive = {
case Join(user) => users += user
case Leave(user) => users -= user
case Broadcast(msg) => users.foreach(_ ! msg)
}
}
// Messages
case class Join(user: ActorRef)
case class Leave(user: ActorRef)
case class Broadcast(message: String)
Main
val chatRoom: ActorRef = system.actorOf(Props[ChatRoomActor], "chat-room")
val user1: ActorRef = system.actorOf(Props(new UserActor("Alice")), "user1")
val user2: ActorRef = system.actorOf(Props(new UserActor("Bob")), "user2")
chatRoom ! Join(user1)
chatRoom ! Join(user2)
chatRoom ! Broadcast("Hello, everyone!")
chatRoom ! Leave(user1)
chatRoom ! Broadcast("Goodbye, Alice!")
system.terminate()
Actor
Actors in Scala are a part of the Akka toolkit, a powerful concurrency and distributed computing library. Actors make it easier to write safe and efficient concurrent and parallel systems. The Actor Model provides a higher level of abstraction for writing concurrent and distributed systems, enabling you to think about your application in terms of actors sending messages to each other, rather than worrying about threads and locks.
Here's a basic tutorial on how to use actors in Scala with Akka.
Setting Up
First, you need to include Akka dependency in your project. If you're using sbt, add the following to your build.sbt file:
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.8.5"
Basic Concepts
- Actor System: The root level where actors live. It's a heavyweight structure that will allocate 1...N threads for your actors.
- Actor: Encapsulates state and behavior. Actors communicate by exchanging messages.
- Message: Immutable data that actors send to each other.
Creating Actors
Actors in Akka are created by defining actor behavior and spawning actors from an actor system.
Defining Actor Behavior
Actor behavior is defined by implementing the Behavior interface. Let's start with a simple example: an actor that says "Hello" when it receives a message.
First, define the messages your actor will handle. It's a good practice to define messages as case classes inside a companion object of your actor:
object Greeter {
final case class SayHello(name: String)
}
Next, define the actor behavior:
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import Greeter.SayHello
object Greeter {
def apply(): Behavior[SayHello] = Behaviors.receive { (context, message) =>
println(s"Hello, ${message.name}!")
Behaviors.same
}
}
Spawning Actors
To use the actor, you must create an actor system and spawn an instance of your actor:
import akka.actor.typed.ActorSystem
object HelloAkka {
def main(args: Array[String]): Unit = {
val greeter: ActorSystem[SayHello] = ActorSystem(Greeter(), "greeter")
greeter ! SayHello("Akka")
}
}
This program will print "Hello, Akka!" to the console.
Handling Different Messages
Actors can handle different types of messages. Here's an example of how to extend our Greeter to handle different kinds of greetings:
object Greeter {
sealed trait Command
final case class SayHello(name: String) extends Command
final case class SayGoodbye(name: String) extends Command
object Greeter {
def apply(): Behavior[Command] = Behaviors.receive { (context, message) =>
message match {
case SayHello(name) =>
println(s"Hello, $name!")
Behaviors.same
case SayGoodbye(name) =>
println(s"Goodbye, $name!")
Behaviors.stopped // Stop this actor
}
}
}
More messages
Handling different messages is a fundamental aspect of working with actors in systems like Akka. Actors need to respond to various message types, each potentially requiring different handling logic. This capability allows actors to participate in complex workflows and maintain internal state accordingly.
Scala Example with Akka Typed
Akka Typed encourages more explicit and safer message handling by defining a protocol of messages an actor can handle. Let's look at a Scala example where an actor can handle different message types:
First, define a sealed trait (or an abstract class) that acts as a protocol for all messages the actor can receive. Then, implement case classes or case objects for each specific message:
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.Behavior
sealed trait Message
final case class Greet(name: String) extends Message
final case class Configure(setting: String) extends Message
case object Shutdown extends Message
Next, define the actor behavior that matches against these messages:
object MyActor {
def apply(): Behavior[Message] = Behaviors.receive { (context, message) =>
message match {
case Greet(name) =>
context.log.info(s"Hello, $name!")
Behaviors.same
case Configure(setting) =>
context.log.info(s"Configuring with setting: $setting")
Behaviors.same
case Shutdown =>
context.log.info("Shutting down.")
Behaviors.stopped
}
}
}
Finally, create the actor system and send some messages to the actor:
object ActorSystemExample extends App {
val system: ActorSystem[Message] = ActorSystem(MyActor(), "myActorSystem")
system ! Greet("Akka")
system ! Configure("verbose")
system ! Shutdown
}
Supervisor
Handling errors and supervising child actors are core functionalities of the Akka Actor model. The supervision strategy allows a parent actor to decide how to handle errors thrown by its children, making the system more resilient. Let's dive into how you can implement a parent actor and manage error handling in an Akka system.
Understanding Supervision Strategies
In Akka, when an actor throws an exception, it's suspended and the supervision strategy of its parent actor is invoked. The parent can decide to:
- Resume the child actor, keeping its internal state.
- Restart the child actor, resetting its internal state.
- Stop the child actor permanently.
- Escalate the failure, causing the parent itself to stop and be supervised by its own parent.
Step 1: Setting Up Your Project
First, ensure your build.sbt file includes dependencies for Akka Typed:
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.15"
)
Step 2: Defining the Child Actor
Create a simple child actor that can throw an exception based on a received message. This example defines a Worker actor that supports DoWork and Fail messages:
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.Behavior
object Worker {
sealed trait Command
case object DoWork extends Command
case object Fail extends Command
def apply(): Behavior[Command] = Behaviors.receiveMessage {
case DoWork =>
println("Worker is doing work")
Behaviors.same
case Fail =>
throw new RuntimeException("Worker failed")
}
}
Step 3: Creating the Parent Actor with Supervision Strategy
Define a parent actor that creates the Worker actor and specifies a supervision strategy for it. This example uses a supervision strategy to restart the child actor upon failure:
import akka.actor.typed.ActorSystem
import akka.actor.typed.SupervisorStrategy
import akka.actor.typed.scaladsl.Behaviors
object Supervisor {
def apply(): Behavior[Worker.Command] = Behaviors
.supervise[Worker.Command] {
Behaviors.setup { context =>
val worker = context.spawn(Worker(), "workerActor")
// Example: sending messages to the worker
worker ! Worker.DoWork
worker ! Worker.Fail // This will cause the worker to throw an exception
Behaviors.receiveMessage { message =>
worker ! message
Behaviors.same
}
}
}
.onFailure[Exception](SupervisorStrategy.restart) // Supervision strategy to restart the child
}
Step 4: Running the System
Create and run an ActorSystem with the Supervisor actor:
object Main extends App {
val system: ActorSystem[Worker.Command] = ActorSystem(Supervisor(), "supervisorSystem")
// Optionally, interact with the system
// e.g., system ! Worker.DoWork
}
Observing Behavior
When the Worker actor throws an exception due to the Fail message:
- The parent's supervision strategy is invoked.
- The strategy decides to restart the
Workeractor. - The
Workeractor's state is reset, and it's ready to receive messages again.
Conclusion
This tutorial introduced the basics of actor supervision in Akka. Supervision strategies empower you to build resilient systems by effectively managing failures. Experimenting with different strategies and understanding their impact on actor states is crucial for leveraging Akka's full potential for building fault-tolerant systems.
More Actors
To illustrate using two actors in an Akka system, let's create a simple Scala application where one actor sends a message to another actor. This example demonstrates basic actor interaction, message passing, and how actors can work together to achieve a task.
We'll create a scenario with two actors: Sender and Receiver. The Sender actor will send a greeting message to the Receiver actor, which will then log the greeting.
Step 1: Define Messages
First, define the messages that actors will use for communication. For simplicity, we'll have a single message type that includes a string payload.
sealed trait Message
final case class Greet(message: String) extends Message
Step 2: Define the Receiver Actor
The Receiver actor will handle the Greet message by printing it to the console.
import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.Behaviors
object Receiver {
def apply(): Behavior[Message] = Behaviors.receive { (context, message) =>
message match {
case Greet(greeting) =>
context.log.info(s"Received message: $greeting")
Behaviors.same
}
}
}
Step 3: Define the Sender Actor
The Sender actor will send a Greet message to the Receiver. It needs a reference to the Receiver actor, which we'll pass as a constructor parameter.
object Sender {
def apply(receiver: ActorRef[Message]): Behavior[Message] = Behaviors.setup { context =>
// Sending the greeting message to the receiver
receiver ! Greet("Hello, Akka Typed Actors!")
Behaviors.same
}
}
Step 4: Set Up the Actor System
Now, set up the ActorSystem, create both actors, and start the interaction.
import akka.actor.typed.ActorSystem
object AkkaQuickStart extends App {
// Create the Actor System
val actorSystem: ActorSystem[Message] = ActorSystem(Receiver(), "receiverSystem")
// Create the Receiver Actor
val receiver: ActorRef[Message] = actorSystem.systemActorOf(Receiver(), "receiver")
// Create the Sender Actor and pass the Receiver's ActorRef
actorSystem.systemActorOf(Sender(receiver), "sender")
}
In this example, when the AkkaQuickStart application runs, it sets up an ActorSystem and creates both Sender and Receiver actors. The Sender immediately sends a Greet message to the Receiver upon initialization, demonstrating basic actor communication. The Receiver actor logs the message to the console as its way of "handling" the message.
Todo App
Creating a simple Todo app using Akka's untyped actors can be an excellent way to demonstrate how to manage state, handle concurrency, and build a basic application using Akka's actor model. Below, I will walk you through the process of building a Todo app with the following functionalities:
- 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.
Akka Http
Akka HTTP is a suite of libraries for building and consuming HTTP-based services in Scala and Java. It's part of the Akka project, which provides tools for building concurrent, distributed, and resilient message-driven applications. Akka HTTP is designed around the actor model and asynchronous, non-blocking I/O operations, making it ideal for high-performance applications that need to handle many concurrent connections. Here’s a simple tutorial to get you started with Akka HTTP in Scala.
Setting Up the Project
First, make sure you have sbt (Scala Build Tool) installed. Create a new sbt project and add the following dependencies to your build.sbt file to include Akka HTTP and Akka Streams:
name := "akka-http-tutorial"
version := "0.1"
scalaVersion := "2.13.6"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % "10.2.4",
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.14",
"com.typesafe.akka" %% "akka-stream" % "2.6.14"
)
Creating a Simple HTTP Server
Next, let’s create a simple HTTP server that responds with "Hello, Akka HTTP!" when accessed. Create a Scala object in src/main/scala named WebServer.scala:
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import scala.io.StdIn
object WebServer {
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem("mySystem")
import system.dispatcher // needed for the future flatMap/onComplete in the end
val route =
pathEndOrSingleSlash { // matches the root path
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Hello, Akka HTTP!</h1>"))
}
val bindingFuture = Http().newServerAt("localhost", 8080).bind(route)
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
StdIn.readLine() // let it run until user presses return
bindingFuture
.flatMap(_.unbind()) // trigger unbinding from the port
.onComplete(_ => system.terminate()) // and shutdown when done
}
}
This code does the following:
- Initializes an
ActorSystem, which is a heavyweight structure that will govern the lifecycle of our application. - Defines a simple route that matches the root URL path and responds with an HTML greeting.
- Starts an HTTP server on localhost port 8080, binding it to the defined route.
- Waits for the user to press RETURN before shutting down the server.
Running Your Server
To run your server, use sbt to compile and execute your application:
sbt run
Navigate to http://localhost:8080 in your web browser, and you should see the greeting "Hello, Akka HTTP!" displayed.
Expanding Your Application
Akka HTTP is not limited to simple text responses. You can serve JSON, handle form submissions, manage websockets, and much more. For a slightly more complex example, let's modify the route to handle different paths and HTTP methods:
val route =
concat(
pathEndOrSingleSlash {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Welcome to Akka HTTP!</h1>"))
},
path("hello") {
get {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h2>Hello, GET request!</h2>"))
} ~
post {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h2>Hello, POST request!</h2>"))
}
}
)
This expanded route now also matches the /hello path and distinguishes between GET and POST requests, providing different responses for each.
Project: Bookstore
To create an Akka Actor-based version of the bookstore, we need to use Akka to manage concurrency and message passing between different components of the application. Below is an example implementation.
Step 1: Add Akka Dependencies
First, add Akka dependencies to your build.sbt file:
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.6.18"
Step 2: Define the Project Structure
Create directories and files as follows:
src/main/scala/
├── models/
│ ├── Book.scala
│ ├── Customer.scala
│ └── Order.scala
├── actors/
│ ├── BookActor.scala
│ ├── CustomerActor.scala
│ └── OrderActor.scala
└── Main.scala
Step 3: Define Models
models/Book.scala
package models
case class Book(id: Int, title: String, author: String, price: Double, stock: Int)
models/Customer.scala
package models
case class Customer(id: Int, name: String, email: String, address: String)
models/Order.scala
package models
case class Order(id: Int, customerId: Int, bookId: Int, quantity: Int, status: String)
Step 4: Implement Actors
actors/BookActor.scala
package actors
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.Book
object BookActor {
sealed trait Command
case class AddBook(title: String, author: String, price: Double, stock: Int, replyTo: ActorRef[Response]) extends Command
case class ListBooks(replyTo: ActorRef[Response]) extends Command
case class FindBookById(id: Int, replyTo: ActorRef[Response]) extends Command
case class UpdateStock(id: Int, newStock: Int, replyTo: ActorRef[Response]) extends Command
sealed trait Response
case class BookAdded(book: Book) extends Response
case class BooksListed(books: List[Book]) extends Response
case class BookFound(book: Option[Book]) extends Response
case class StockUpdated(book: Option[Book]) extends Response
def apply(): Behavior[Command] = {
var books: Map[Int, Book] = Map.empty
var nextBookId: Int = 1
Behaviors.receiveMessage {
case AddBook(title, author, price, stock, replyTo) =>
val book = Book(nextBookId, title, author, price, stock)
books += (nextBookId -> book)
nextBookId += 1
replyTo ! BookAdded(book)
Behaviors.same
case ListBooks(replyTo) =>
replyTo ! BooksListed(books.values.toList)
Behaviors.same
case FindBookById(id, replyTo) =>
replyTo ! BookFound(books.get(id))
Behaviors.same
case UpdateStock(id, newStock, replyTo) =>
val updatedBook = books.get(id).map(book => book.copy(stock = newStock))
updatedBook.foreach(book => books += (id -> book))
replyTo ! StockUpdated(updatedBook)
Behaviors.same
}
}
}
actors/CustomerActor.scala
package actors
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.Customer
object CustomerActor {
sealed trait Command
case class AddCustomer(name: String, email: String, address: String, replyTo: ActorRef[Response]) extends Command
case class ListCustomers(replyTo: ActorRef[Response]) extends Command
case class FindCustomerById(id: Int, replyTo: ActorRef[Response]) extends Command
sealed trait Response
case class CustomerAdded(customer: Customer) extends Response
case class CustomersListed(customers: List[Customer]) extends Response
case class CustomerFound(customer: Option[Customer]) extends Response
def apply(): Behavior[Command] = {
var customers: Map[Int, Customer] = Map.empty
var nextCustomerId: Int = 1
Behaviors.receiveMessage {
case AddCustomer(name, email, address, replyTo) =>
val customer = Customer(nextCustomerId, name, email, address)
customers += (nextCustomerId -> customer)
nextCustomerId += 1
replyTo ! CustomerAdded(customer)
Behaviors.same
case ListCustomers(replyTo) =>
replyTo ! CustomersListed(customers.values.toList)
Behaviors.same
case FindCustomerById(id, replyTo) =>
replyTo ! CustomerFound(customers.get(id))
Behaviors.same
}
}
}
actors/OrderActor.scala
package actors
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.{Order, Book, Customer}
object OrderActor {
sealed trait Command
case class PlaceOrder(customerId: Int, bookId: Int, quantity: Int, replyTo: ActorRef[Response]) extends Command
case class ListOrders(replyTo: ActorRef[Response]) extends Command
case class ListOrdersByCustomer(customerId: Int, replyTo: ActorRef[Response]) extends Command
sealed trait Response
case class OrderPlaced(order: Option[Order]) extends Response
case class OrdersListed(orders: List[Order]) extends Response
case class OrdersByCustomerListed(orders: List[Order]) extends Response
def apply(bookActor: ActorRef[BookActor.Command], customerActor: ActorRef[CustomerActor.Command]): Behavior[Command] = {
var orders: List[Order] = List.empty
var nextOrderId: Int = 1
Behaviors.receiveMessage {
case PlaceOrder(customerId, bookId, quantity, replyTo) =>
val order = Order(nextOrderId, customerId, bookId, quantity, "Placed")
orders = orders :+ order
nextOrderId += 1
replyTo ! OrderPlaced(Some(order))
Behaviors.same
case ListOrders(replyTo) =>
replyTo ! OrdersListed(orders)
Behaviors.same
case ListOrdersByCustomer(customerId, replyTo) =>
replyTo ! OrdersByCustomerListed(orders.filter(_.customerId == customerId))
Behaviors.same
}
}
}
Step 5: Create the Main Application
Main.scala
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}
import actors.{BookActor, CustomerActor, OrderActor}
import scala.util.{Try, Success, Failure}
import utils.Utils._
object Main extends App {
sealed trait Command
case class Run() extends Command
def apply(): Behavior[Command] = Behaviors.setup { context =>
val bookActor = context.spawn(BookActor(), "BookActor")
val customerActor = context.spawn(CustomerActor(), "CustomerActor")
val orderActor = context.spawn(OrderActor(bookActor, customerActor), "OrderActor")
def run(): Unit = {
var continue = true
while (continue) {
println("\nBookstore App")
println("1. Add Book")
println("2. List Books")
println("3. Add Customer")
println("4. List Customers")
println("5. Place Order")
println("6. List Orders")
println("7. List Orders by Customer")
println("8. Exit")
print("Choose an option: ")
scala.io.StdIn.readLine() match {
case "1" => addBook(bookActor)
case "2" => listBooks(bookActor)
case "3" => addCustomer(customerActor)
case "4" => listCustomers(customerActor)
case "5" => placeOrder(orderActor)
case "6" => listOrders(orderActor)
case "7" => listOrdersByCustomer(orderActor)
case "8" => continue = false
case _ => println("Invalid option. Please try again.")
}
}
}
def addBook(bookActor: ActorRef[BookActor.Command]): Unit = {
val title = readLineWithPrompt("Enter book title: ")
val author = readLineWithPrompt("Enter book author: ")
val priceTry = readDoubleWithPrompt("Enter book price: ")
val stockTry = readIntWithPrompt("Enter book stock: ")
(priceTry, stockTry) match {
case (Success(price), Success(stock)) =>
bookActor ! BookActor.AddBook(title, author, price, stock, context.self)
println(s"Book added: $title by $author")
case (Failure(priceEx), _) =>
println(s"Invalid price input. Error: ${priceEx.getMessage}")
case (_, Failure(stockEx)) =>
println(s"Invalid stock input. Error: ${stockEx.getMessage}")
}
}
def listBooks(bookActor: ActorRef[BookActor.Command]): Unit = {
bookActor ! BookActor.ListBooks(context.self)
}
def addCustomer(customerActor: ActorRef[CustomerActor.Command]): Unit = {
val name = readLineWithPrompt("Enter customer name: ")
val email = readLineWithPrompt("Enter customer email: ")
val address = readLineWithPrompt("Enter customer address: ")
customerActor ! CustomerActor.AddCustomer(name, email, address, context.self)
println(s"Customer added: $name")
}
def listCustomers(customerActor: ActorRef[CustomerActor.Command]): Unit = {
customerActor ! CustomerActor.ListCustomers(context.self)
}
def placeOrder(orderActor: ActorRef[OrderActor.Command]): Unit = {
val customerIdTry = readIntWithPrompt("Enter customer id: ")
val bookIdTry = readIntWithPrompt("Enter book id: ")
val quantityTry = readIntWithPrompt("Enter quantity: ")
(customerIdTry, bookIdTry, quantityTry) match {
case (Success(customerId), Success(bookId), Success(quantity)) =>
orderActor ! OrderActor.PlaceOrder(customerId, bookId, quantity, context.self)
println(s"Order placed: Customer $customerId ordered Book $bookId")
case (Failure(customerIdEx), _, _) =>
println(s"Invalid customer id input. Error: ${customerIdEx.getMessage}")
case (_, Failure(bookIdEx), _) =>
println(s"Invalid book id input. Error: ${bookIdEx.getMessage}")
case (_, _, Failure(quantityEx)) =>
println(s"Invalid quantity input. Error: ${quantityEx.getMessage}")
}
}
def listOrders(orderActor: ActorRef[OrderActor.Command]): Unit = {
orderActor ! OrderActor.ListOrders(context.self)
}
def listOrdersByCustomer(orderActor: ActorRef[OrderActor.Command]): Unit = {
val customerIdTry = readIntWithPrompt("Enter customer id: ")
customerIdTry match {
case Success(customerId) =>
orderActor ! OrderActor.ListOrdersByCustomer(customerId, context.self)
case Failure(ex) =>
println(s"Invalid customer id input. Error: ${ex.getMessage}")
}
}
Behaviors.receiveMessage {
case Run() =>
run()
Behaviors.same
}
}
val system = ActorSystem(Main(), "BookstoreSystem")
system ! Run()
}
Step 6: Implement Utility Functions
utils/Utils.scala
package utils
import scala.util.{Try, Success, Failure}
object Utils {
def readLineWithPrompt(prompt: String): String = {
print(prompt)
scala.io.StdIn.readLine()
}
def readIntWithPrompt(prompt: String): Try[Int] = {
print(prompt)
Try(scala.io.StdIn.readInt())
}
def readDoubleWithPrompt(prompt: String): Try[Double] = {
print(prompt)
Try(scala.io.StdIn.readDouble())
}
}
Running Your Akka Actor-based Application
To run your application, use SBT:
sbt run
Why Futures
Asynchronous programming is a crucial concept in modern software development, especially important in Scala when dealing with concurrent operations. Utilizing Scala's Futures is a common way to write non-blocking code that can perform long-running computations, IO operations, or network requests in parallel, improving the overall efficiency and responsiveness of applications. Here are key reasons why asynchronous programming is vital:
1. Improved Scalability
Asynchronous programming allows a system to handle more work concurrently without blocking for each operation to complete. For example, in a web server context, this means being able to handle more incoming requests without waiting for each request to be fully processed (including waiting for database queries or external API calls). This model significantly improves the ability to scale applications to support high loads.
2. Better Resource Utilization
By not blocking threads on long-running operations, you make better use of system resources. Threads, which are limited and expensive resources, can be reused for other tasks instead of idling and waiting for I/O operations or other blocking calls to complete. This efficient use of threads can lead to lower memory usage and less overhead for context switching.
3. Enhanced Responsiveness
Asynchronous operations can improve the responsiveness of applications, both from a user interface perspective and system architecture perspective. User interfaces remain responsive to user interactions even while the application is performing background tasks. Similarly, backend systems can continue to accept and process incoming requests while waiting for other operations to complete.
4. Simplified Concurrent Programming
Scala's Future and for-comprehension provide a powerful yet straightforward abstraction for handling asynchronous computations. They allow developers to write code that looks sequential but executes concurrently, simplifying error handling and the composition of asynchronous operations. This leads to more readable and maintainable code compared to traditional callback-based approaches.
5. Error Handling and Composition
Scala Futures make it easier to handle errors and compose multiple asynchronous operations. Using for-comprehensions and map/flatMap operations, you can chain Futures in a way that automatically handles threading and error propagation. This approach simplifies the management of complex asynchronous workflows, where operations depend on the result of previous ones.
Example: Using Scala Futures
Here’s a simple example demonstrating the use of Scala Future for asynchronous programming:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
val futureResult: Future[Int] = Future {
// Simulate a long-running computation
Thread.sleep(1000)
42
}
futureResult.onComplete {
case Success(value) => println(s"The result is $value")
case Failure(exception) => println(s"Failed with $exception")
}
Future
Scala's Future provides a powerful abstraction for working with asynchronous computations. It represents a value that may become available at some point, allowing your program to continue executing while waiting for the result. This is particularly useful for operations that involve IO, database queries, network requests, or any long-running computations.
Here's a simple example demonstrating how to use Scala Futures to perform asynchronous computations:
Setup
First, ensure you have the Scala execution context in scope, as it's needed to execute the futures:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
Basic Future Usage
The following example shows how to create a future, perform an operation asynchronously, and handle the result once it's available:
// Define a future that simulates a long-running computation
val futureComputation: Future[Int] = Future {
Thread.sleep(1000) // Simulating a long-running task
42 // The answer to the ultimate question of life, the universe, and everything
}
// Handling the future's result
futureComputation.onComplete {
case Success(result) => println(s"Result: $result")
case Failure(e) => println(s"An error occurred: ${e.getMessage}")
}
Working with Results
Often, you'll want to chain operations on futures or combine them. Here's how to use map, flatMap, and for comprehensions to work with future results:
val futureSquare: Future[Int] = futureComputation.map(x => x * x)
futureSquare.onComplete {
case Success(square) => println(s"Square of the result: $square")
case Failure(e) => println(s"Failed to calculate the square: ${e.getMessage}")
}
// Combining futures with for-comprehension
val anotherFutureComputation: Future[Int] = Future {
Thread.sleep(500) // Another simulated task
10
}
val combinedResult: Future[Int] = for {
result1 <- futureComputation
result2 <- anotherFutureComputation
} yield result1 + result2
combinedResult.onComplete {
case Success(sum) => println(s"Sum of results: $sum")
case Failure(e) => println(s"Failed to combine results: ${e.getMessage}")
}
Error Handling
Handling errors with futures can be done using recover or recoverWith methods:
val riskyFuture: Future[Int] = Future {
throw new RuntimeException("Something went wrong")
}
val recoveredFuture: Future[Int] = riskyFuture.recover {
case _: RuntimeException => -1
}
recoveredFuture.onComplete {
case Success(value) => println(s"Recovered value: $value")
case Failure(e) => println("This should not be printed if recovered properly")
}
Promise
In Scala, a Promise is a writable, single-assignment container which completes a Future. Essentially, Promise and Future are two sides of the same coin: while Future provides a way to read a value that may not yet exist, Promise provides a way to write that value once it becomes available. This mechanism is particularly useful in scenarios where you need to manually complete a Future based on the outcome of some computation or asynchronous operation.
Basic Usage of Promise
Here's how you can create and use a Promise in Scala:
- Creating a Promise
First, you need to import the necessary classes:
import scala.concurrent.{Promise, Future}
import scala.concurrent.ExecutionContext.Implicits.global
Then, create a Promise:
val promise: Promise[Int] = Promise[Int]()
- Obtaining the Future from a Promise
You can obtain a Future associated with a Promise. This Future is completed when the Promise is fulfilled.
val future: Future[Int] = promise.future
- Completing a Promise
A Promise can be completed with a value or an exception. Completing the Promise also completes its associated Future.
// Completing the promise with a value
promise.success(42)
// Alternatively, completing the promise with a failure
// promise.failure(new RuntimeException("Failure"))
- Using the Future
You can work with the Future obtained from a Promise just like any other Future. For example, you can add callbacks to handle completion:
future.onComplete {
case Success(value) => println(s"The result is $value")
case Failure(exception) => println(s"Failed with $exception")
}
Example: Asynchronous Computation
Here's a more concrete example, demonstrating how a Promise can be used to wrap an asynchronous computation, such as a network request:
def asyncNetworkRequest(url: String): Future[String] = {
val promise = Promise[String]()
// Simulate an asynchronous network request
global.execute(() => {
try {
// Simulating request delay
Thread.sleep(1000)
promise.success(s"Response from $url")
} catch {
case e: Exception => promise.failure(e)
}
})
promise.future
}
val url = "http://example.com"
val responseFuture: Future[String] = asyncNetworkRequest(url)
responseFuture.onComplete {
case Success(response) => println(response)
case Failure(exception) => println(s"Request failed: $exception")
}
In this example, asyncNetworkRequest simulates making an asynchronous network request. It uses a Promise to represent the eventual result of the request. The Promise is completed with a success or failure once the simulated request finishes, and the associated Future is used to handle the response.
todoapp
shoppingbasket
async-test
More Functions
- partial applied
- currying
- partial functions
Partial Applied Functions
Partial applied mean that not all the parameters are provided. Then the return type is a new function, which can be used with the rest of parameters
scala> val addFunction = (x: Int, y: Int) => x + y
val addFunction: (Int, Int) => Int = Lambda...
scala> addFunction(7, 8)
val res1: Int = 15
scala> val partialAdd1 = addFunction(7, _: Int)
val partialAdd1: Int => Int = Lambda...
scala> partialAdd1(8)
val res2: Int = 15
scala> val partialAdd2 = (x: Int) => addFunction(x, 7)
val partialAdd2: Int => Int = Lambda...
scala> partialAdd2(8)
val res3: Int = 15
scala> def partialAdd3(x: Int) = addFunction(x, 7)
def partialAdd3(x: Int): Int
scala> partialAdd3(8)
val res4: Int = 15
These are partial applied functions Also methods can be partial applied
scala> def addMethod(x: Int, y: Int): Int = x + y
def addMethod(x: Int, y: Int): Int
scala> addMethod(7, 8)
val res5: Int = 15
scala> val partialAdd4 = addMethod(7, _: Int)
val partialAdd4: Int => Int = Lambda...
scala> partialAdd4(8)
val res6: Int = 15
Currying
scala> def curriedAdd(x: Int)(y: Int) = x + y
def curriedAdd(x: Int)(y: Int): Int
scala> curriedAdd(7)(8)
val res7: Int = 15
scala> val partialAdd5 = curriedAdd(7)
val partialAdd5: Int => Int = Lambda...
scala> partialAdd5(8)
val res8: Int = 15
scala> val partialAdd6 = curriedAdd(7)(_:Int)
val partialAdd6: Int => Int = Lambda...
scala> partialAdd6(8)
val res9: Int = 15
def toCurry(f: (a: Int, b: Int) => Int): Int => Int => Int =
x => y => f(x, y)
def fromCurry(f: Int => Int => Int): (Int, Int) => Int =
(x, y) => f(x)(y)
println(toCurry(_ + _)(2)(3))
println(fromCurry(x => y => x + y)(2, 6))
Function composition
def andThen[A, B, C](f: A => B, g: B => C) =
(a: A) => g(f(a))
def compose[A, B, C](f: B => C, g: A => B ) =
(a: A) => f(g(a))
println(compose((z: Int) => z * 2, (y: Int) => y + 10)(3))
println(andThen[Int, Int, Int](z => z * 2, y => y + 10)(3))
Partial Functions
A partial function is a function applicable to a subset of the data it has been defined for.
For example, we could define a function on the Int domain that only works on positive numbers.
val squareRoot: PartialFunction[Double, Double] =
def apply(x: Double) = Math.sqrt(x)
def isDefinedAt(x: Double) = x >= 0
val squareRoot: PartialFunction[Double, Double] =
case x if x >= 0 => Math.sqrt(x)
OrElse
val negativeToPositive: PartialFunction[Int, Int] =
case x if x <= 0 => Math.abs(x)
val positiveToNegative: PartialFunction[Int, Int] =
case x if x > 0 => -1 * x
val swapSign: PartialFunction[Int, Int] =
positiveToNegative orElse negativeToPositive
val partialPrint: PartialFunction[Int, Unit] = {
case x if x > 0 => println(s"$x is greater then zero")
}
(swapSign andThen partialPrint)(-1)
Collect, map and filter
Collect , map and FIlter can be used with partial functions
val parseRange: PartialFunction[Int, Int] = {
case x: Int if x > 5 => x + 1
}
List(15, 3, "hello") collect { parseRange }
scala> List(1, 2) collect { case i: Int => i > 10 }
val res11: List[Boolean] = List(false, false)
scala> List(1, 2) filter { case i: Int => i > 10 }
val res12: List[Int] = List()
scala> List(1, 2) map { case i: Int if i < 10 => i + 1 }
val res13: List[Int] = List(2, 3)
Here we defined the partial function as an anonymous function.
Call by Value, Name, Need
Call by
- value
- name
- need
Call by Value
Evaluated on function call
def functionByValue(x: Long) =
println(x)
println(x)
The param x is evaluated on the function call
From then on the value of x is fixed
So the two print statements will print the same value
Call by Name
Evaluated on usage of x
def functionByName(x: => Long) =
println(x)
println(x)
The param x is evaluated on the moment of usage
So the two print statements will print the different values
Call by Need
Evaluated on the first time usage of x1
def functionByNeed(x: => Long) = {
lazy val x1 = x
println(x1)
println(x1)
The param x is evaluated on the first println usage
From then on the value of lazy x1 is fixed
So the two print statements will print the same value
main
def main(args: Array[String]): Unit =
functionByValue(System.nanoTime())
functionByName(System.nanoTime())
functionByNeed(System.nanoTime())
// 118920267089041
// 118920267089041
// 118920327472000
// 118920327493583
// 118920327830750
// 118920327830750
Given and Using
Sorting
Sorted
@main
def main(): Unit =
val list = List(1, 5, 2, 6, 4, 3)
val sortedList = list.sorted
println(sortedList)
// List(1,2,3,4,5,6)
Ordering
given ordering: Ordering[Int] with
override implicit def compare(x: Int, y: Int): Int = x - y
This is the same as the default implements of the standard libraries
Reverse Ordering
given reversedOrdering: Ordering[Int] with
override implicit def compare(x: Int, y: Int): Int = y - x
Using
def sorting(using order: Ordering[Int])(list: List[Int]): List[Int] =
list.sorted
Here we have a function that is using a given as parameter.
When he compiler sees a using will search for a `given' of that type.
@main
def main(): Unit =
val list = List(1,5,2,6,4,3)
println(sorting(list))
Case Classes
case class Todo (task: String, priority: Int)
given todoOrdering: Ordering[Todo] with
override implicit def compare(a: Todo, b: Todo): Int =
a.priority - b.priority
@main
def main(): Unit =
val todoList = List(Todo("walk", 3), Todo("talk", 5), Todo("sleep", 1))
println(todoList.sorted)
// List(Todo(sleep,1), Todo(walk,3), Todo(talk,5))
Scopes
The compiler will look for available given in scopes in the following order
imported givenscurrent scopecompanion object
Companion Object
object Todo:
given todoOrdering: Ordering[Todo] with
override implicit def compare(a: Todo, b: Todo): Int =
a.priority - b.priority
The ordering is added to the campanion object.
If there is no given in the current scope. The compiler will look in the companion object.
@main
def main(): Unit =
val todoList = List(Todo("walk", 3), Todo("talk", 5), Todo("sleep", 1))
println(todoList.sorted)
// List(Todo(sleep,1), Todo(walk,3), Todo(talk,5))
The using is found in the companion object
import given
object TodoOrdering:
given todoOrdering: Ordering[Todo] with
override implicit def compare(a: Todo, b: Todo): Int =
a.priority - b.priority
The ordering is added to another scope
@main
def main(): Unit =
val todoList = List(Todo("walk", 3), Todo("talk", 5), Todo("sleep", 1))
import TodoOrdering.given
println(todoList.sorted)
// List(Todo(sleep,1), Todo(walk,3), Todo(talk,5))
Now we have to import the given. We can do this in two ways:
- import TodoOrdering.todoOrdering
- import TodoOrdering.given
import TodoOrdering.*
The wildcard * does not work on givens.
Rule of thumb
- Put the most frequently used ordering in the companion object
- Put the other orderings in separate objects with a clear names
Given examples
In Scala 3 (formerly known as Dotty), the given and using clauses represent a sophisticated evolution of Scala's implicits feature, making it more explicit and easier to reason about. These features are part of Scala's type class support, allowing for more expressive and type-safe code. Let's dive into a few examples to understand how given and using work.
Example 1: Basic Type Class
A type class is a sort of interface that defines some behavior. Unlike traditional interfaces, type classes can be "attached" to classes after they've been defined. Let's define a simple JsonSerializer type class and then use given and using to serialize objects to JSON.
Define the Type Class
trait JsonSerializer[T] {
def serialize(value: T): String
}
Implement the Type Class
given JsonSerializer[String] with {
def serialize(value: String): String = s""""$value""""
}
given JsonSerializer[Int] with {
def serialize(value: Int): String = value.toString
}
Use the Type Class
def toJson[T](value: T)(using serializer: JsonSerializer[T]): String = {
serializer.serialize(value)
}
println(toJson("hello")) // Output: "hello"
println(toJson(123)) // Output: 123
Example 2: Contextual Abstractions with using Parameters
Sometimes, you might want to pass additional parameters contextually without cluttering the method signature for every call. This is where using shines.
Define a Context
case class RequestContext(userId: String)
given RequestContext = RequestContext("user123")
A Method that Requires Context
def getUser(using ctx: RequestContext): String = {
s"Fetching data for user: ${ctx.userId}"
}
println(getUser) // Output: Fetching data for user: user123
Example 3: Generic Programming with given
You can also use given instances for generic programming, such as defining a generic sum function for numeric types.
Numeric Type Class
trait Numeric[T] {
def plus(x: T, y: T): T
def zero: T
}
given Numeric[Int] with {
def plus(x: Int, y: Int): Int = x + y
def zero: Int = 0
}
Generic Sum Function
def sum[T](items: List[T])(using numeric: Numeric[T]): T = {
items.foldLeft(numeric.zero)(numeric.plus)
}
println(sum(List(1, 2, 3))) // Output: 6
Extension Methods
extension
extension(i: Int)
def print: Unit =
println(s"some: $i")
@main
def main(): Unit =
val x = 3
x.print
generics
extension[A](a: A)
def print: Unit =
println(s"some: $a")
@main
def main(): Unit =
val x = 3
x.print
val str = "hello"
str.print
case class
case class Todo(task: String, priority: Int)
object Todo:
extension(todo: Todo)
def print: Unit =
println(s"${todo.priority}. ${todo.task.capitalize}")
@main
def main(): Unit =
val todoList = List(Todo("walking", 1),Todo("swimming", 2) )
todoList.foreach(_.print)
// 1. Walking
// 2. Swimming
Scope
The scope rules of extension methods are the simular to the scope of given
imported extensioncurrent scopecompanion object
imported extension
object TodoExtension:
extension (todo: Todo)
def print: Unit =
println(s"${todo.priority}. ${todo.task.capitalize}")
@main
def main(): Unit =
val todoList = List(Todo("walking", 1),Todo("swimming", 2) )
import TodoExtension.*
todoList.foreach(_.print)
For extensions the wildcard * import is used.
This is different from to the given import.
Rule of thumb
The rules are similar to the given:
- Put the most frequently used extension in the companion object
- Put the other extensions in separate objects with a clear names
Conversion
case class
case class Person(name: String)
def greet(): String = s"Hello, $name"
given fromStringToPerson: Conversion[String, Person] = Person(_)
@main def run =
println("John".greet())
Scope
The conversion rules are the same as the given rules
imported conversioncurrent scopecompanion object
Implicit
Scala's implicits are a powerful feature that allows the compiler to "fill in" values or methods automatically, enhancing the language's expressiveness and enabling more concise code. They are especially useful for type class patterns, extension methods, and implicit parameters, among others. Here, we'll explore an introductory tutorial covering several key aspects of Scala implicits.
Implicit Values
Implicit values are automatically used by the compiler to fill in parameters for methods or constructors.
implicit val defaultName: String = "John Doe"
def greet(implicit name: String): Unit = {
println(s"Hello, $name!")
}
greet // Output: Hello, John Doe!
Implicit Conversions
Implicit conversions can automatically convert one type to another, allowing for cleaner code when working with different but related types.
implicit def intToString(value: Int): String = value.toString
val myString: String = 123 // Implicitly converts Int to String
println(myString) // Output: 123
Implicit Classes
Implicit classes allow adding new methods to existing types without modifying their source code, similar to extension methods in other languages.
implicit class RichInt(val i: Int) {
def squared: Int = i * i
}
println(5.squared) // Output: 25
Implicit Parameters
Implicit parameters enable passing context or configuration to methods without cluttering their call sites.
case class Config(prefix: String)
implicit val myConfig: Config = Config("Hello, ")
def greet(name: String)(implicit config: Config): Unit = {
println(config.prefix + name)
}
greet("Scala") // Output: Hello, Scala
Best Practices
While powerful, implicits should be used judiciously:
- Limit Scope: Only bring implicits into scope when necessary to avoid conflicts and confusion.
- Naming Conventions: Use clear and descriptive names for implicit values and parameters.
- Documentation: Document your implicits well to aid in understanding and maintenance.
Type Classes
A type class is the functional equivalent of polymorphism in the object-oriented world.
And it is resolved in compile-time by importing context givens
The Object-Oriented Way
trait Show:
def show: String
case class Person(name: String, age: Int) extends Show:
override def show = s"$name with age $age"
val john = Person("John", 28 )
val showJohn = john.show
- available only for the types we
extend - provide only one
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
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.
Advanced Pattern Matching
Advanced pattern matching in Scala extends beyond simple case class decomposition, offering powerful features that allow for more intricate and nuanced control flow based on the shape and characteristics of data. These features include nested patterns, pattern guards, type patterns, extractor objects, and more. Let's explore some of these advanced concepts:
1. Nested Patterns
Pattern matching can be nested to decompose complex data structures. This is particularly useful when working with nested case classes or tuples.
case class Person(name: String, address: Address)
case class Address(city: String, country: String)
def matchPerson(person: Person): String = person match {
case Person(_, Address("New York", "USA")) => "Lives in New York, USA"
case Person(name, Address(city, _)) => s"$name lives in $city"
}
2. Pattern Guards
Pattern guards provide additional filtering conditions for a match case, using an if clause. This can refine the selection criteria for a particular pattern.
def evaluateNumber(number: Int): String = number match {
case x if x > 0 => "Positive number"
case x if x == 0 => "Zero"
case x if x < 0 => "Negative number"
}
3. Type Patterns
Type patterns allow you to match objects based on their type. This can be particularly useful for polymorphic behavior in pattern matching.
def printType(x: Any): String = x match {
case _: Int => "This is an integer"
case _: String => "This is a string"
case _ => "Unknown type"
}
4. Extractor Objects
Extractor objects allow for custom pattern matching logic by defining an unapply method. This method enables an object to be deconstructed in a custom way for pattern matching.
object Even {
def unapply(arg: Int): Option[Int] = if (arg % 2 == 0) Some(arg) else None
}
def checkEven(number: Int): String = number match {
case Even(n) => s"$n is even"
case _ => s"$n is odd"
}
5. Matching on Collections
Scala allows pattern matching on collections such as Lists, Arrays, and more, with patterns that can match specific collection characteristics.
def listMatcher(list: List[Int]): String = list match {
case List(_, _, third) => s"The third element is $third"
case head :: tail => s"The head is $head"
case Nil => "The list is empty"
}
6. Case Class Sequence Patterns
You can match sequences of case classes, combining the power of case classes with collection pattern matching.
def processShapes(shapes: List[Shape]): String = shapes match {
case Circle(_) :: Rectangle(_, _) :: Nil => "A circle followed by a rectangle"
case _ => "Other shapes sequence"
}
These advanced features greatly enhance the expressiveness and flexibility of pattern matching in Scala, enabling concise and powerful data manipulation and control flow mechanisms.
Variance
Scala's type system includes variance annotations that influence how subtyping between more complex types works, such as between generic classes of those types.
- Covariance (
+T): 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
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.
Path Dependent Types
Path-dependent types are an advanced feature in Scala that ties types to the instances of the objects in which they are defined. This means the type is dependent not just on the class or trait, but also on the instance (or "path") of the object that contains it. Path-dependent types allow for more expressive type relationships in Scala's type system, enabling more precise control over how types are associated with specific instances of classes or traits.
Understanding Path-Dependent Types
To understand path-dependent types, consider a scenario where a class or trait defines a type or class inside of it. The type of the inner class or type is then dependent on the instance of the outer class or trait. This is particularly useful in scenarios where you want the types of certain components to be tightly coupled with the instances they belong to.
Example: Bank Accounts and Currencies
Consider an example of a banking application where you want each Bank to have its own Currency. The type of currency should be specific to the bank instance, preventing mix-ups between different banks' currencies.
class Bank
class Currency(val name: String)
def createCurrency(name: String): Currency = new Currency(name)
val bank1 = new Bank
val bank2 = new Bank
// Create currencies specific to each bank
val dollar = bank1.createCurrency("Dollar")
val euro = bank2.createCurrency("Euro")
// dollar: bank1.Currency
// euro: bank2.Currency
In this example, the type of dollar is bank1.Currency, and the type of euro is bank2.Currency. Even though Dollar and Euro are both currencies, they are tied to their respective bank instances, making them distinct types. This prevents a function that is supposed to accept only bank1.Currency from accidentally accepting bank2.Currency.
Use Case: Safe Associations
Path-dependent types are especially useful for ensuring type-safe associations between objects. For example, you could have a method that only accepts currencies from the same bank, preventing errors at compile time:
def addAmount(amount1: bank1.Currency, amount2: bank1.Currency): Unit =
println(s"Adding ${amount1.name} and ${amount2.name}")
addAmount(dollar, dollar) // Compiles fine
// addAmount(dollar, euro) // This would not compile because types don't match
Advantages of Path-Dependent Types
- Type Safety: They ensure that only compatible types are used together, as determined by their enclosing instance.
- Encapsulation: They help in encapsulating types within instances, making your code more modular and expressive.
- Flexibility: Path-dependent types allow for more flexible and sophisticated type relationships than would be possible with only class-based types.
Higher Kinded Types
Higher-kinded types (HKTs) are a sophisticated feature of Scala's type system, allowing you to abstract over types that themselves take type parameters. They are akin to "generics for generics" and provide a powerful tool for creating highly reusable and generic code that can work with various kinds of containers or abstractions like collections, futures, options, and more.
Understanding Higher-Kinded Types
To grasp higher-kinded types, it's essential to understand that types in Scala can have different levels of abstraction:
- First-order types are concrete types, like
Int,String, orList[Int]. - Type constructors take type parameters to produce first-order types. For example,
Listis a type constructor because you need to provide a type parameter to create a concrete type, likeList[Int]. - Higher-kinded types abstract over type constructors. They allow you to write code that works with any type constructor that fits the abstracted shape.
Why Use Higher-Kinded Types?
Higher-kinded types allow you to write highly generic and reusable code. For instance, you might want to implement a function that wraps a value into any container type, like Option, List, or a custom container. Without higher-kinded types, you'd have to write a separate function for each container type.
Syntax and Examples
Let's dive into how to declare and use higher-kinded types in Scala.
Declaration
You declare higher-kinded types using underscores to represent their type parameters. For example, a type constructor that takes a single type parameter can be represented as F[_].
trait Container[F[_]] {
def put[A](value: A): F[A]
def get[A](container: F[A]): A
}
This trait Container is generic in the type constructor F. It can abstract over any type constructor that takes a single type parameter.
Usage
Suppose you have the following Option and List implementations of Container:
object OptionContainer extends Container[Option] {
def put[A](value: A): Option[A] = Some(value)
def get[A](container: Option[A]): A = container.get
}
object ListContainer extends Container[List] {
def put[A](value: A): List[A] = List(value)
def get[A](container: List[A]): A = container.head
}
You can now write code that works with any Container, regardless of the specific type constructor (Option, List, etc.) it uses.
Working with Higher-Kinded Types
Higher-kinded types are especially useful in functional programming patterns found in libraries like Cats and Scalaz, where they enable a level of abstraction and code reuse that's difficult or impossible to achieve with only first-order types or simple generics.
For instance, the concept of a Monad can be abstractly defined using higher-kinded types, allowing you to write monadic operations and for-comprehensions that work with any monadic structure (like Option, List, Future, etc.).
MyList - step 1
Build a tiny, immutable linked list from scratch. If you come from an object‑oriented background and are new to FP, this guide will walk you through the core ideas step by step.
What we are building (and why)
We will create a minimal version of Scala's standard List, called MyList. It will be:
- Immutable: once created, it never changes.
- Singly linked: each element points to the rest of the list.
- Built from two building blocks (an Algebraic Data Type):
- Empty: represents “no elements.”
- Cons: represents “one element followed by the rest.”
Think of a list as either:
- nothing (Empty), or
- a box with a head value and an arrow pointing to the tail (the rest of the list).
- Cons(1, Cons(2, Cons(3, Empty)))
1 -> 2 -> 3 ->
- Empty
[]
- Cons(1, Cons(2, Empty))
[1] -> [2] -> []
Step 1: the common interface (a trait)
We start with a trait that describes what any MyList can do, regardless of whether it’s Empty or Cons. If you’re new to Scala:
- trait is like an interface with possible default methods.
- [A] means the list is generic (it can hold Int, String, etc.).
trait MyList[A]:
def isEmpty: Boolean
def head: A
def tail: MyList[A]
Notes:
- head returns the first element.
- tail returns “the rest of the list.”
- On an empty list, head and tail don’t make sense; we’ll address that below.
Exercise
Implement the trait with two cases (this is a standard FP pattern called an Algebraic Data Type):
- Empty: the empty list.
- Cons: a node that stores a head element and a tail (another MyList).
Start from these class skeletons and fill them in:
case class Empty[A]() extends MyList[A]
case class Cons[A]() extends MyList[A]
Hints:
- Empty.isEmpty should be true.
- Cons.isEmpty should be false.
- Empty.head and Empty.tail should signal an error (e.g., throw NoSuchElementException), because there is no first element or remainder.
- Cons should store its head and tail in the constructor.
Solution (expand if you’re stuck)
Empty
The empty list has no head or tail, so trying to access them is an error. We’ll use NoSuchElementException to indicate that.
case class Empty[A]() extends MyList[A]:
override def isEmpty: Boolean = true
override def head: A = throw new NoSuchElementException("head of empty list")
override def tail: MyList[A] = throw new NoSuchElementException("tail of empty list")
Cons
A non‑empty list stores its first element (head) and a reference to the rest (tail). We pass both via the constructor. Because we are using case class, Scala will generate useful methods like toString, equals, and copy for free.
case class Cons[A](override val head: A, override val tail: MyList[A]) extends MyList[A]:
override def isEmpty: Boolean = false
Try it out
@main
def main(): Unit =
val myList: MyList[Int] = Cons(1, Cons(2, Cons(3, Empty())))
println(myList) // thanks to case classes, prints the structure
println(myList.head) // 1
println(myList.tail) // Cons(2,Cons(3,Empty()))
What you should see (format may vary):
- Cons(1,Cons(2,Cons(3,Empty())))
- 1
- Cons(2,Cons(3,Empty()))
## Key FP ideas you just practiced
- Immutability: we build new lists by composing nodes; we never mutate an existing node.
- Recursion by structure: a list is defined in terms of a smaller list (its tail). This is the foundation for many list operations you’ll implement next (map, filter, toString, etc.).
- Algebraic Data Types: modeling data with a small set of precise cases (Empty or Cons) leads to simple, predictable code.
In the next step, we’ll add behavior (like a nicer toString) and build more operations on top of this structure.
MyList - step 2
ToString
Add the toString method to MyList
trait MyList[A]:
...
def toString: String
- Make a recursive version
- A Tail recursive version
- A pretty print version that prints
MyList(3,2,1)
Exercise
Implement the add method in Cons and Empty And test it in main
Solution
Empty
override def toString: String = ""
Recursive
We have to walk through the linked list get the head and then jump to the tail recursively. The case is the empty node.
override def toString: String =
def concat(remainder: MyList[A]): String =
if !remainder.isEmpty then
current.head.toString + " " + concat(remainder.tail)
else
""
concat(this)
Tail Recursive
In the tail recursive version we add a accumulator to the parameters. In every iteration we add the head to the accumulator And the last step in returning the accumulator
override def toString: String =
def concat(remainder: MyList[A], accumulator: String): String =
if !remainder.isEmpty then
concat(remainder.tail, accumulator + " " + remainder.head)
else
s"MyList($accumulator)"
concat(this, "")
Pretty print
To make in pretty print version we add a comma in every iteration. But then we get as many comma's as there are elements and that is one comma too much.
Iterating one less is starting the recursive loop with the tail
And the accumulator start with the head
override def toString: String =
def concat(remainder: MyList[A], accumulator: String): String =
if !remainder.isEmpty then
concat(remainder.tail, accumulator + ", " + remainder.head)
else
accumulator
val elements = concat(tail, head.toString)
s"MyList($elements)"
main
@main
def main(): Unit =
val myList: MyList[Int] = Empty() + 1 + 2 + 3
println(myList)
// 3 2 1
// MyList(3, 2, 1)
The order is reversed now.
MyList - step 3
Build up lists by adding elements and make construction ergonomic. In this step we will:
- Add an add method that prepends an element.
- Provide a + operator as a friendly alias.
- Create a MyList companion object with an apply method so you can write MyList(1,2,3).
Why prepend? Our MyList is immutable and singly linked. Prepending is O(1) (make one new node that points to the rest), while appending would be O(n) because you’d have to walk to the end first.
Exercise 1: add
Goal: give all MyList instances a method to add a new head in front of the list.
trait MyList[A]:
...
def add(element: A): MyList[A]
- Implement add in Empty and Cons.
- Try it: Empty().add(1).add(2).add(3)
- Hint: adding to either Empty or Cons returns a new Cons with element as head and the current list as tail.
Exercise 2: + operator
Let’s make the same operation feel natural with an operator.
trait MyList[A]:
...
def +(element: A): MyList[A]
- Implement + as an alias for add.
- Try it: Empty() + 1 + 2 + 3
- Hint: in Scala, symbols like + are valid method names. Using the infix modifier lets you omit dots and parentheses when calling it.
Exercise 3: companion object apply
Build lists from a variable number of arguments.
object MyList:
def apply[A](elements: A*): MyList[A]
- Implement apply so that MyList(1,2,3) creates a list 1 -> 2 -> 3.
- Hint: a small tail-recursive helper that accumulates using + works well. Remember that + prepends, so you’ll probably want to process elements in reverse order to preserve the original left-to-right order.
Solution: add
Empty
override def add(element: A): MyList[A] = Cons(element, this)
Adding to Empty yields a new Cons whose tail is this (the empty list).
Cons
override def add(element: A): MyList[A] = Cons(element, this)
Adding to a non-empty list simply creates a new head and points its tail at the current list.
Put it in the trait
The implementations are identical, so we can provide a default on the trait and remove the duplicates in the cases:
trait MyList[A]:
...
def add(element: A): MyList[A] = Cons(element, this)
Try it
@main
def main(): Unit =
val myList: MyList[Int] = Empty().add(1).add(2).add(3)
println(myList) // MyList(3, 2, 1) once we add pretty printing in step 2
Note the reverse order: because we always prepend, the last added element ends up at the front.
Solution: + operator
trait MyList[A]:
...
def add(element: A): MyList[A] = Cons(element, this)
infix def +(element: A): MyList[A] = add(element)
-
- is just a method name.
- With infix, you can write Empty() + 1 instead of Empty().+(1).
@main
def main(): Unit =
val myList: MyList[Int] = Empty() + 1 + 2 + 3
println(myList) // MyList(3, 2, 1)
Solution: companion object apply
object MyList:
def apply[A](elements: A*): MyList[A] =
def build(rem: Seq[A], acc: MyList[A]): MyList[A] =
if rem.isEmpty then acc
else build(rem.tail, acc + rem.head)
build(elements.reverse, Empty())
- elements: A* declares a varargs parameter.
- We accumulate using our + method.
- Because + prepends, we reverse the incoming sequence first to preserve the original order.
Try it
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1,2,3)
println(myList) // MyList(1, 2, 3)
Key ideas
- Prepending to a singly linked immutable list is O(1) and natural.
- Providing an operator and a companion object makes APIs ergonomic without changing the underlying model.
MyList - step 4
Add behavior: foreach, map, and filter. These are the bread-and-butter operations of functional collections. We’ll implement them recursively on our immutable, singly linked list.
- foreach visits each element and runs a side-effecting function (like printing).
- map transforms every element and returns a new list of the results.
- filter keeps only the elements that satisfy a predicate.
Exercise: add methods to MyList
trait MyList[A]:
...
def foreach(f: A => Unit): Unit
def map[B](f: A => B): MyList[B]
def filter(p: A => Boolean): MyList[A]
Hints:
- Use structural recursion: handle Empty as the base case; for Cons, do work on head, then recurse on tail.
- Keep everything immutable. Construct new Cons nodes rather than mutating.
Solution: foreach
Empty
override def foreach(f: A => Unit): Unit = ()
Doing nothing for the empty list makes sense and returns Unit (written as ()).
Cons
override def foreach(f: A => Unit): Unit =
f(head)
tail.foreach(f)
Apply f to head, then recurse into the tail.
Solution: map
Empty
override def map[B](f: A => B): MyList[B] = Empty[B]()
Mapping over an empty list yields an empty list of the new type B.
Note: If Empty were declared as case object Empty extends MyList[Nothing], we could avoid specifying [B] here thanks to variance. We keep it simple for now and stay with EmptyB.
Cons
override def map[B](f: A => B): MyList[B] =
Cons[B](f(head), tail.map(f))
Transform the head, then recursively map the tail, and rebuild a new list. You can omit [B] in Cons because the type is inferred from the return type: Cons(f(head), tail.map(f))
Solution: filter
Empty
override def filter(p: A => Boolean): MyList[A] = this
Filtering an empty list is still empty.
Cons
override def filter(p: A => Boolean): MyList[A] =
if p(head) then Cons(head, tail.filter(p))
else tail.filter(p)
Keep head if it matches the predicate p; otherwise drop it and continue with the tail.
Try it out
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1, 2, 3)
myList.foreach(x => println(x + 2)) // prints 3, 4, 5
println(myList.map(x => x * 2)) // MyList(2, 4, 6)
println(myList.filter(x => x < 2)) // MyList(1)
Key ideas
- Recursion mirrors the list’s structure: Empty is the base case; Cons does work and recurses on tail.
- Immutability makes it natural to “return a new list” rather than modify in place.
MyList - step 5
Combine and expand lists with concatenation and flatMap. These two operations unlock expressive transformations and composition.
- ++ (concatenation) glues two lists together.
- flatMap maps each element to a list and flattens the results.
Exercise: add ++ and flatMap
trait MyList[A]:
...
infix def ++(other: MyList[A]): MyList[A]
def flatMap[B](f: A => MyList[B]): MyList[B]
Hints:
- For ++, think in terms of rebuilding the left list while pointing the last node to the right list.
- For flatMap, use ++ to combine the list produced from the head with the recursively flatMapped tail.
Solution: concatenation (++)
Empty
override infix def ++(other: MyList[A]): MyList[A] = other
Appending anything to an empty list returns the other list unchanged.
Cons
override infix def ++(other: MyList[A]): MyList[A] =
Cons(head, tail ++ other)
Rebuild the left list node by node, and when you reach the end (Empty), hook it up to other.
Walk-through:
[1,2,3] ++ [4,5,6]
Cons(1, [2,3] ++ [4,5,6])
Cons(1, Cons(2, [3] ++ [4,5,6]))
Cons(1, Cons(2, Cons(3, [] ++ [4,5,6])))
Cons(1, Cons(2, Cons(3, [4,5,6])))
[1,2,3,4,5,6]
Solution: flatMap
Empty
override def flatMap[B](f: A => MyList[B]): MyList[B] = Empty[B]()
Flat-mapping an empty list yields an empty list (regardless of f).
Cons
override def flatMap[B](f: A => MyList[B]): MyList[B] =
f(head) ++ tail.flatMap(f)
- Transform head into a list with f(head).
- Concatenate it with the flat-mapped tail.
Walk-through:
[1,2,3].flatMap(a => [a, a + 1])
[1,2] ++ [2,3].flatMap(f)
[1,2] ++ [2,3] ++ [3].flatMap(f)
[1,2] ++ [2,3] ++ [3,4] ++ [].flatMap(f)
[1,2] ++ [2,3] ++ [3,4] ++ []
[1,2,2,3,3,4]
Try it out
@main
def main(): Unit =
val myList: MyList[Int] = MyList(1, 2, 3)
val otherList: MyList[Int] = MyList(4, 5)
println(myList ++ otherList) // MyList(1, 2, 3, 4, 5)
println(myList.flatMap(a => MyList(a, a + 1))) // MyList(1, 2, 2, 3, 3, 4)
Key ideas
- ++ structurally rebuilds the left list and attaches the right list at the end.
- flatMap is “map then flatten,” and becomes simple once ++ is available.
MyList - step 6
Use Scala’s for-comprehension with MyList. Because we implemented map and flatMap, MyList already supports for-comprehension. We’ll also add withFilter to support if guards inside for.
For-comprehension basics
A for-comprehension like this:
val result = for
a <- MyList(1, 2, 3)
b <- MyList(a, a + 1)
yield b
Is desugared by the compiler roughly into:
MyList(1, 2, 3).flatMap(a => MyList(a, a + 1).map(b => b))
Since we already implemented map and flatMap, this just works.
Try it out
@main
def main(): Unit =
val result = for
a <- MyList(1, 2, 3)
b <- MyList(a, a + 1)
yield b
println(result) // MyList(1, 2, 2, 3, 3, 4)
Filtering in for: withFilter
In for-comprehensions, if-guards (if a < 3) use withFilter under the hood (not filter). The standard library’s withFilter is lazy; we’ll keep it simple and make ours delegate to filter.
Add this to MyList:
trait MyList[A]:
...
def withFilter(p: A => Boolean): MyList[A]
Implementation
override def withFilter(p: A => Boolean): MyList[A] = filter(p)
Example
@main
def main(): Unit =
val result = for
a <- MyList(1, 2, 3, 4, 5) if a < 3
yield a
println(result) // MyList(1, 2)
Key ideas
- for-comprehension is syntax sugar that uses map, flatMap, and withFilter.
- Providing withFilter allows if-guards in for without changing our existing filter behavior.
MyList - step 7
Introduce laziness: build an infinite list and only compute what you need. We’ll adapt our eager MyList into a lazy version (LzList) that can represent infinite sequences safely. Then we’ll implement take and a generator for infinite lists.
Why laziness? With an eager list, trying to build an infinite structure would never finish. With a lazy list, elements are computed on demand and cached (call-by-need).
Exercise 1: from MyList to LzList
We’ll keep the same API where possible, but change evaluation strategy.
- Rename MyList to LzList (your IDE’s rename refactoring helps).
- Replace Cons with a class that takes head and tail by-name (delayed) and stores them as lazy vals (computed at most once).
- Empty remains a case class (or case object) with the same shape.
Lazy Cons
class Cons[A](hd: => A, tl: => LzList[A]) extends LzList[A]:
def isEmpty: Boolean = false
override lazy val head: A = hd
override lazy val tail: LzList[A] = tl
Notes:
- hd and tl are call-by-name parameters (=>). We wrap them into lazy vals so they are computed at most once (call-by-need).
- We use class rather than case class because case class parameters are evaluated eagerly.
- You’ll instantiate with new Cons(...).
After this change, re-run your small examples to confirm existing map/flatMap/filter semantics still hold, just lazily.
Exercise 2: take and an infinite generator
To see laziness in action, we’ll build an infinite list and take only the first n elements.
- Add take to LzList:
def take(n: Int): LzList[A]
- Add a generator on the companion object to build an infinite integer list by repeatedly applying a step function:
object LzList:
def generate(start: Int)(next: Int => Int): LzList[Int] = ???
Solution: take
Empty
override def take(n: Int): LzList[A] = this
Cons
override def take(n: Int): LzList[A] =
def loop(rem: LzList[A], count: Int): LzList[A] =
if count <= 0 || rem.isEmpty then Empty()
else new Cons(rem.head, loop(rem.tail, count - 1))
loop(this, n)
- We stop when count reaches 0 or the list is empty.
- We preserve laziness by constructing Cons with delayed tail.
Solution: generate
object LzList:
def generate(start: Int)(next: Int => Int): LzList[Int] =
new Cons[Int](start, generate(next(start))(next))
This creates an infinite list: start, next(start), next(next(start)), and so on. Because Cons is lazy, values are produced only when needed.
Try it out
@main
def main(): Unit =
val genList: LzList[Int] = LzList.generate(1)(_ + 1) // 1,2,3,4,...
val genMap: LzList[Int] = genList.map(_ * 100) // 100,200,300,... (lazily)
println(genMap.take(10)) // first 10 multiples of 100
println(genMap.take(100)) // first 100
println(genMap.take(100000)) // still fine, computed on demand
Key ideas
- call-by-name (=>) delays evaluation; lazy val caches the result the first time it’s needed.
- Laziness allows infinite data structures when you only observe a finite prefix (via take).
Scala Intro
Introduction to Scala
- Overview of Scala: A high-level programming language that integrates features of object-oriented and functional programming.
- Purpose and Goals: Designed to be concise, elegant, and, most importantly, type-safe.
- Creator: Martin Odersky and his team at EPFL (École Polytechnique Fédérale de Lausanne) in Switzerland.
- Release Date: First released in 2003.
Development and Evolution
- Early Development: The motivations behind Scala's creation, including addressing shortcomings of Java.
- Major Releases and Features:
- Scala 2.0 (March 2006): Introduction of generic classes, abstract type members, and the new collections library.
- Scala 2.8 (July 2010): Major overhaul of the collections library, named and default parameters.
- Scala 2.12 (November 2016): Full support for Java 8, lambda syntax for SAM types, and backend improvements.
- Scala 3.0 (May 2021): Significant language redesign and simplification, introduction of new concepts like implicits redesign, top-level definitions, and enum classes.
- Contributions to the JVM Ecosystem: Impact on the development of other JVM languages and tools.
Key Features and Concepts
- Functional Programming: Immutability, higher-order functions, and pattern matching.
- Object-Oriented Programming: Classes, traits, and mixin composition.
- Type System: Explanation of Scala's powerful type inference and system, including abstract type members, variance annotations, and compound types.
- Concurrency and Distribution: Scala's approach to concurrency, including Futures, Promises, and the Akka framework.
Scala Ecosystem and Community
- Tooling: SBT (Scala Build Tool), Metals (language server), and IDE support (IntelliJ IDEA, Visual Studio Code).
- Libraries and Frameworks: Play Framework (web applications), Akka (concurrency and distributed computing), and Spark (big data processing).
- Community and Adoption: Overview of the Scala community, major companies using Scala, and its presence in open-source projects.
Challenges and Criticisms
- Learning Curve: The complexity of some features and its functional programming aspects can be challenging for newcomers.
- Compilation Speed: Scala's compilation time has been criticized, though efforts like the Dotty compiler aim to address this.
- Market Position: Discussion of Scala's niche in the programming landscape, its competition with other languages, and future outlook.
Variable
Introduction to var and val
- Definition and Usage:
valis used to declare an immutable variable. Once initialized, its value cannot be changed.varis used for mutable variables. Its value can be reassigned after initialization.
- Syntax:
val immutableVariable: Type = initialValue var mutableVariable: Type = initialValue - Key Difference: Mutability vs. immutability.
val - Immutable Variables
- Advantages:
- Encourages functional programming style.
- Safer to use in concurrent or multi-threaded environments.
- Helps in avoiding side effects, making code more predictable.
- Use Cases:
- When the value does not need to change after initialization.
- For constants or read-only properties.
- In method parameters to ensure they remain unchanged.
- Examples:
val pi: Double = 3.14 val greeting: String = "Hello, Scala!"
var - Mutable Variables
- Advantages:
- Provides flexibility when a variable’s value needs to change, e.g., counters, stateful objects.
- Disadvantages:
- Can lead to code that is harder to understand and maintain.
- Increased risk of errors in concurrent or multi-threaded environments.
- Use Cases:
- When a variable’s value needs to be updated, such as in loops or when reacting to external inputs.
- State management within an object.
- Examples:
var counter: Int = 0 var message: String = "Initial message"
Best Practices
- Prefer
valovervar:- Use
valby default to make your code more functional, predictable, and thread-safe.
- Use
- Limit the use of
var:- Restrict
varusage to local scopes or cases where mutability is necessary. - Consider using immutable collections or structures to manage state changes more safely.
- Restrict
- Performance Considerations:
- Immutable objects can sometimes lead to increased memory usage due to the need for creating new objects instead of updating existing ones. However, the benefits of immutability in terms of code clarity and safety often outweigh these concerns.
Transitioning from Mutable to Immutable Patterns
- Strategies:
- Use immutable collections and operations that return new collections instead of modifying the original.
- Apply functional programming techniques, such as mapping and reducing over collections, instead of imperative loops with mutable state.
- Case Study: Refactoring an imperative loop to a functional style.
- Example: Converting a loop that sums numbers using
varto a functional style usingvaland collection methods.
Basic Types
Introduction to Basic Types in Scala
- Overview: Scala integrates object-oriented and functional programming in a statically typed language. Unlike Java, Scala treats all types as objects.
- Type Hierarchy: Explanation of Scala's type hierarchy, with
Anyat the top, splitting intoAnyVal(value types) andAnyRef(reference types). - Immutability: Emphasizes Scala's preference for immutable types to promote functional programming practices.
Numeric Types
Int
- Range: -2^31 to 2^31-1
- Use: Default integer type for general numerical computation.
- Example:
val i: Int = 123456
Long
- Range: -2^63 to 2^63-1
- Use: Storing large integer values.
- Example:
val l: Long = 12345678910L
Double
- Precision: 64-bit IEEE 754 floating-point.
- Use: Default type for floating-point numbers, for double-precision decimals.
- Example:
val d: Double = 123.456
Character and Boolean Types
- Char
- Description: Represents a single 16-bit Unicode character.
- Use: Storing characters or small character-based data.
- Example:
val c: Char = 'A'
- Boolean
- Description: Represents a logical value, either
trueorfalse. - Use: Controlling flow with conditional statements, logical operations.
- Example:
val flag: Boolean = true
String Type
- String
- Description: A sequence of characters. In Scala,
Stringis equivalent tojava.lang.String, fully supported with additional Scala-specific methods. - Use: Storing and manipulating text data.
- Example:
val str: String = "Hello, Scala!"
- Description: A sequence of characters. In Scala,
Unit, Null, and Nothing Types
- Unit
- Equivalent to
voidin Java. Represents a method that does not return a meaningful value. - Example:
def myFunction(): Unit = { println("This function returns no meaningful value") }
- Null
- Represents the absence of a value for reference types. It is a subtype of all reference types (
AnyRef), not value types. - Example:
var str: String = null
- Nothing
- A subtype of every other type; used to indicate abnormal termination or a program location that must not be reached.
Operators
Basic Arithmetic Operations
- Overview: Introduction to performing basic arithmetic operations like addition, subtraction, multiplication, and division in Scala.
- Operations:
- Addition (
+): Adds two numbers. - Subtraction (
-): Subtracts the second number from the first. - Multiplication (
*): Multiplies two numbers. - Division (
/): Divides the first number by the second. Note the behavior with integer division (result is truncated). - Modulus (
%): Returns the remainder of the division of the first number by the second.
- Addition (
Examples:
val sum = 5 + 3 // 8
val diff = 5 - 3 // 2
val product = 5 * 3 // 15
val quotient = 5 / 3 // 1 (if integers), or 1.666... (if floats or doubles)
val remainder = 5 % 3 // 2
Comparison Operations
- Overview: Describes how to compare two numbers in Scala using comparison operators.
- Operations:
- Equal to (
==): Checks if two numbers are equal. - Not equal to (
!=): Checks if two numbers are not equal. - Greater than (
>): Checks if the first number is greater than the second. - Less than (
<): Checks if the first number is less than the second. - Greater than or equal to (
>=): Checks if the first number is greater than or equal to the second. - Less than or equal to (
<=): Checks if the first number is less than or equal to the second.
- Equal to (
- Examples:
val isEqual = 5 == 3 // false val isNotEqual = 5 != 3 // true val isGreaterThan = 5 > 3 // true val isLessThan = 5 < 3 // false
Advanced Mathematical Operations
- Overview: Introduction to more complex mathematical operations available in Scala, including those in the
scala.mathpackage. - Key Functions:
- Exponential (
math.pow,math.exp): Raises a number to the power of another, calculates the exponential. - Logarithm (
math.log,math.log10): Calculates the natural or base 10 logarithm of a number. - Square Root (
math.sqrt): Calculates the square root of a number. - Trigonometry (
math.sin,math.cos,math.tan): Performs trigonometric operations.
- Exponential (
- Examples:
val power = scala.math.pow(2, 3) // 8.0 val squareRoot = scala.math.sqrt(16) // 4.0 val sinValue = scala.math.sin(scala.math.Pi / 2) // 1.0
Numeric Type Conversions
- Overview: Details on how to convert between different numeric types in Scala.
- Conversions:
- Implicit conversions happen automatically when required by the context.
- Explicit conversions can be performed using methods like
toInt,toDouble, etc.
- Examples:
val intVal: Int = 10 val doubleVal: Double = intVal.toDouble // Explicit conversion to Double
Best Practices
- Precision and Accuracy: Choose the appropriate numeric type (
Floatvs.Double,Intvs.Long) based on the required precision and magnitude. - Immutability: Favor immutable values (
val) over mutable ones (var) for better predictability and safety in concurrent environments. - Numeric Literals: Use suffixes (
Lfor Long,Dfor Double,Ffor Float) to clearly indicate the type of numeric literals.
If..else
Understanding if in Scala
- Introduction: Scala's
iftests a condition and executes a block of code if the condition istrue. - Syntax:
if (condition) { // block of code to execute if the condition is true } - Key Points:
conditionmust be a Boolean expression.
if-else Structure
- Extended Syntax:
if (condition) { // block of code if condition is true } else { // block of code if condition is false } - As an Expression: Scala's
if-elsecan be used to directly assign values based on conditions.- Example:
val result = if (condition) "Yes" else "No"
- Example:
if-else if Ladder
- Syntax for Multiple Conditions:
if (condition1) { // block if condition1 is true } else if (condition2) { // block if condition2 is true } else { // block if none of the conditions are true } - Use Case: To test for multiple, mutually exclusive conditions.
if as an Expression
- Functional Programming Feature: In Scala,
ifcan return a value, making it more powerful and expressive. - Examples:
- Value assignment:
val max = if (a > b) a else b - Inlining in function calls:
println(if (condition) "Condition is true" else "Condition is false") - Scala allows
ifwithout an accompanyingelse, but usingifas an expression in such cases will yield a Unit type (()).
- Value assignment:
Best Practices
- Using
ifas an Expression: Leverage Scala’sifas an expression for cleaner and more concise code, particularly in assignments and return statements. - Boolean Expressions: Ensure the condition in
ifis a Boolean expression to avoid compilation errors. - Nested
ifStatements: While Scala allows nestingifstatements, for better readability and maintainability, consider usingmatchexpressions or combining conditions logically. ifand Performance: Usingifexpressions efficiently can lead to more performant Scala code. Avoid unnecessary complex conditions and deep nesting.
match
match Expressions
- Overview: Scala's
matchexpression is a concise way of selecting from multiple possible blocks of code based on the value of an expression. - Syntax:
variable match { case pattern1 => // expression or block of code for pattern1 case pattern2 => // expression or block of code for pattern2 ... case _ => // default case if no patterns match } - Key Points:
- A
matchexpression is exhaustive; it must cover all possible values of the variable. - The
_symbol represents a wildcard pattern that matches any value.
- A
Simple Pattern Matching
- Use Case: Using
matchfor simple value comparisons, similar to a switch-case statement in other languages. - Example:
val day = 4 val dayName = day match { case 1 => "Monday" case 2 => "Tuesday" case 3 => "Wednesday" case 4 => "Thursday" case 5 => "Friday" case 6 => "Saturday" case 7 => "Sunday" case _ => "Invalid day" }
Guarded Patterns
- Use Case: Adding conditions to patterns using if-clauses to make matches more specific.
- Example:
val number = 10 val numberMatch = number match { case x if x < 0 => "Negative number" case x if x % 2 == 0 => "Even number" case _ => "Odd number" }
: Best Practices and Tips
- Exhaustiveness: Always ensure that
matchexpressions are exhaustive to avoidMatchErrorat runtime. - Readability: Use
matchexpressions to replace complexif-elsechains for better readability. - Pattern Guards: Use guards to fine-tune pattern matching conditions, making patterns more flexible and expressive.
while
while Loops
- Overview: The
whileloop in Scala is a control flow statement that allows code to be executed repeatedly based on a given Boolean condition. The loop might not execute at all if the condition is false at the beginning. - Syntax:
while (condition) { // code block to be executed } - Key Points:
- The condition is evaluated before each iteration. If the condition evaluates to true, the loop's body is executed.
- The condition must be a Boolean expression.
- It’s important to modify a variable within the loop to eventually make the condition false and exit the loop, avoiding an infinite loop.
Sheet 2: while Loop Examples
- Simple Counter:
var counter = 0 while (counter < 5) { println(s"Counter is at: $counter") counter += 1 // Important to avoid an infinite loop } - Reading from a Buffer (Pseudo-Code):
var bufferNotEmpty = checkBuffer() // Assume this checks if a buffer is not empty while (bufferNotEmpty) { processBuffer() bufferNotEmpty = checkBuffer() // Re-check to update the loop condition }
Sheet 3: Introduction to do-while Loops
- Overview: The
do-whileloop is similar to thewhileloop but guarantees that the loop's body is executed at least once because the condition is evaluated after the loop's body. - Syntax:
do { // code block to be executed } while (condition) - Key Points:
- Ideal for scenarios where the loop must execute at least once, such as user input validation.
Sheet 4: do-while Loop Examples
- User Input Validation (Pseudo-Code):
var userInput: String = "" do { userInput = readInput() // Assume this reads user input processInput(userInput) } while (userInput != "exit") - Post-Condition Checks:
var continueProcessing = true do { continueProcessing = processChunk() // Assume this processes a chunk of data } while (continueProcessing)
Sheet 5: Best Practices and Recommendations
- Prefer
fororforeachfor Collections: Scala encourages using higher-level abstractions likeforloops orforeachmethods for iterating over collections for more concise and readable code. - Avoid Infinite Loops: Ensure that the loop's condition will eventually become false. It's easy to create an infinite loop if the condition is not properly updated.
- Use While Loops Sparingly: Given Scala's emphasis on functional programming, consider whether a recursive function could be used instead of a loop for better clarity and immutability.
- Side Effects: Be cautious of using
whileanddo-whileloops to modify mutable state or perform side effects. Scala encourages a functional programming style, favoring immutability and expressions over statements.
These sheets provide a fundamental understanding of using while and do-while loops in Scala, including syntax, practical examples, and best practices to ensure efficient and effective use of loops in various programming scenarios.
for
Introduction to for Loops
- Overview: Scala's
forloop is a control structure for iterating over elements in collections, such as lists, arrays, and more. It's more powerful and flexible than traditionalforloops found in many other languages, supporting advanced iteration patterns and for-comprehensions. - Syntax:
for (item <- collection) { // operation using item } - Key Points:
- The loop variable
itemtakes on each value in thecollectionone by one. - Scala's
forcan also iterate through multiple collections simultaneously or nest iterations within one another. - The arrow
<-is used to denote extraction of each element from the collection.
- The loop variable
Filtering with Guards
- Overview: Guards are conditions within
forloops that allow filtering elements of the collection being iterated over. - Syntax:
for (item <- collection if condition) { // operation using item } - Example:
for (num <- 1 to 10 if num % 2 == 0) { println(num) // Prints even numbers between 1 and 10 }
for Comprehensions
- Overview:
forcomprehensions are a powerful feature in Scala that allow for filtering, mapping, and flatMapping operations on collections, all within theforloop syntax. They are particularly useful for working with collections in a more functional style. - Syntax:
val result = for (item <- collection if condition) yield { // transform item } - Example:
val squares = for (num <- 1 to 5) yield num * num // squares: IndexedSeq[Int] = Vector(1, 4, 9, 16, 25)
Nested for Loops
- Overview: Scala supports nesting
forloops within each other to iterate over multiple dimensions or perform complex iteration patterns. - Syntax:
for (item1 <- collection1; item2 <- collection2) { // operation using item1 and item2 } - Example:
for (x <- 1 to 3; y <- 1 to 3) { println(s"($x, $y)") }
Best Practices
- Use Descriptive Names: Choose descriptive names for loop variables to make your code more readable and maintainable.
- Prefer
forComprehensions for Complex Logic: Leverageforcomprehensions to simplify complex collection manipulations, improving readability. - Immutable Collections: When generating new collections with
forcomprehensions, take advantage of Scala's immutable collections to promote functional programming principles. - Performance Considerations: While
forloops are convenient and powerful, be mindful of their performance impact, especially with large collections or complex nested loops.
List
Scala Lists
- Overview: Lists in Scala are ordered collections of elements of the same type. They are immutable, meaning that their elements cannot be changed after the list is created.
- Creating Lists:
val emptyList: List[Nothing] = List() val singleList: List[Int] = List(1) val multiList: List[Int] = List(1, 2, 3) - Key Properties:
- Lists are immutable.
- Lists are recursive structures (a
headelement and ataillist).
Basic Operations on Lists
- Accessing Elements:
- Access by index:
val firstElement = multiList(0) - Head and tail:
val head = multiList.head,val tail = multiList.tail
- Access by index:
- Appending and Prepending Elements:
- Prepend with
::or+::val newList = 0 :: multiList - Append with
:+(less efficient):val appendedList = multiList :+ 4
- Prepend with
- Concatenation:
- Concatenate two lists:
val combinedList = List(1, 2) ::: List(3, 4)
- Concatenate two lists:
Functional Programming with Lists
- Mapping (
map): Applying a function to each element.val doubledList = multiList.map(_ * 2) - Filtering (
filter): Selecting elements based on a condition.val evenList = multiList.filter(_ % 2 == 0) - Reducing (
reduce): Combining elements using a binary operation.val sum = multiList.reduce(_ + _)
Common List Methods
flatMap: Applying a function that returns a sequence for each element, and flattening the results into a single list.foreach: Executing a side-effecting function on each element.foldLeftandfoldRight: Like reduce, but with an initial accumulator value.zip: Combining two lists into a list of pairs.reverse: Reversing the list.- Examples for each method will illustrate their utility and usage.
Best Practices
- Immutability and Performance: Understanding the implications of list immutability on performance, especially for operations like appending.
- Choosing the Right Collection: When to use List vs. other collections (e.g., Vector, Array) based on performance characteristics and use cases.
- Functional Transformations Over Mutations: Emphasizing the use of functional transformations (
map,filter, etc.) over imperative modifications to embrace Scala's functional programming paradigm.
mutable lists
Scala Mutable Lists
-
Overview: Mutable lists in Scala are part of the
scala.collection.mutablepackage. They allow for modification after creation, supporting operations like adding, removing, and updating elements. -
Importing Mutable Lists: Before using mutable lists, you need to import the mutable package.
import scala.collection.mutable.ListBuffer
Creating Mutable Lists
Mutable lists can be instantiated using ListBuffer, which is a resizable array buffer analogous to a list.
val mutableList: ListBuffer[Int] = ListBuffer(1, 2, 3)
Adding Elements
- Append elements using
+=:mutableList += 4 // ListBuffer(1, 2, 3, 4) - Prepend elements using
+=::0 +=: mutableList // ListBuffer(0, 1, 2, 3, 4) - Append collections using
++=:mutableList ++= List(5, 6) // ListBuffer(0, 1, 2, 3, 4, 5, 6)
Removing Elements
- Remove a single element by value:
mutableList -= 3 // ListBuffer(0, 1, 2, 4, 5, 6) - Remove elements by index with
remove:mutableList.remove(0) // Removes the first element; resulting ListBuffer(1, 2, 4, 5, 6)
Best Practices
- Use Cases: Opt for mutable lists when you have a collection that needs to change frequently, such as elements being added or removed based on program logic.
- Performance: Mutable lists offer performance benefits for certain operations like appending, but be mindful of copying costs when converting to immutable collections.
- Thread Safety:
ListBufferis not thread-safe. For concurrent programming, consider using thread-safe collections fromscala.collection.concurrentor using synchronization mechanisms.
array
Scala Arrays
- Overview: Arrays in Scala are a direct mapping to Java's arrays. They provide a sequence of elements that are indexed and have a fixed size. Scala arrays can store any type of elements, including AnyVal (value types) and AnyRef (reference types).
- Creating Arrays:
val nums = Array(1, 2, 3, 4) val names = Array("Alice", "Bob", "Charlie") - Accessing Elements: Arrays allow random access to their elements through an index.
val firstNum = nums(0) // Accessing the first element nums(2) = 10 // Updating the third element - Key Properties:
- Fixed size but mutable in terms of element values.
- Can be multidimensional.
Basic Operations
- Length of an Array: Use the
lengthproperty to get the size of an array.val size = nums.length - Iterating Over Arrays: You can use a
forloop or higher-order functions likeforeach.for (name <- names) println(name) names.foreach(println) - Modifying Elements: Elements can be updated by specifying their index.
nums(0) = 100 // Update the first element to 100
Common Methods
- Mapping and Filtering:
Arrays support functional programming methods such as
mapandfilter.val squaredNums = nums.map(x => x * x) val evenNums = nums.filter(_ % 2 == 0) - Sorting Arrays:
Use the
sortedmethod orsortWithfor custom sorting.val sortedNames = names.sorted val descendingNums = nums.sortWith(_ > _)
Multidimensional Arrays
- Creating and Using Multidimensional Arrays:
Scala supports arrays of arrays, allowing for multidimensional structures.
val matrix = Array.ofDim[Int](2, 2) // 2x2 matrix matrix(0)(0) = 1 matrix(0)(1) = 2 matrix(1)(0) = 3 matrix(1)(1) = 4
Converting Between Arrays and Other Collections
- To and From Collections:
Arrays can be converted to and from other Scala collections like List, Seq, etc.
val numList = nums.toList val numSeq = nums.toSeq val arrayFromList = numList.toArray
Best Practices
- When to Use Arrays:
- Choose arrays for performance-sensitive, low-level programming tasks.
- Ideal when working with a fixed-size collection of elements.
- Interoperability with Java:
- Arrays provide seamless interoperability with Java methods that require Java's native arrays.
- Immutability vs. Mutability:
- Consider using immutable collections like
VectororListfor functional programming practices. Use arrays when mutability is a necessity or for performance reasons.
- Consider using immutable collections like
ArrayBuffer
Introduction to ArrayBuffer
- Overview: An
ArrayBufferis a mutable indexed sequence that allows you to append, prepend, insert, and remove elements efficiently. It is part of the Scala Collections Library. - Importing
ArrayBuffer:import scala.collection.mutable.ArrayBuffer - Creating an
ArrayBuffer:val nums = ArrayBuffer[Int]() // Or new ArrayBuffer[Int] val strings = ArrayBuffer("one", "two", "three")
Adding Elements
- Appending Elements: Use
+=to append one or more elements at the end.nums += 4 nums += (5, 6, 7) - Prepending Elements: Use
+=:orprependto add elements at the start.3 +=: nums nums.prepend(1, 2) - Inserting Elements: Use
insertto add elements at a specific index.nums.insert(2, 8) // Inserts 8 at index 2
Removing Elements
- Removing by Index: Use
removeto delete elements at a specific index.nums.remove(2) // Removes the element at index 2 - Removing Multiple Elements: Specify the index and the number of elements to remove.
nums.remove(0, 3) // Removes 3 elements starting from index 0 - Clearing All Elements: Use
clearto empty the entire buffer.nums.clear()
Accessing and Updating Elements
- Accessing Elements: Use the
applymethod or parentheses.val firstElement = nums(0) - Updating Elements: Assign a new value to a specific index.
nums(0) = 10
Common Operations
- Iterating Over Elements: Use
foreach,forloops, or higher-order functions.nums.foreach(println) - Transforming
ArrayBuffer: Usemap,filter, etc., to produce new collections.val doubledNums = nums.map(_ * 2) - Converting to Arrays: Use
toArrayfor a fixed-size representation.val numsArray = nums.toArray
Performance Considerations
- Amortized Costs: While append operations are generally efficient, be mindful of the amortized cost of resizing, especially for very large collections.
- Memory Overhead:
ArrayBufferuses more memory than a plain array due to its resizable nature.
Use Cases for ArrayBuffer
- When to Use:
ArrayBufferis ideal for building up sequences of elements when the number of elements is unknown upfront or when frequent updates to the sequence are required. - Comparisons: Prefer
ArrayBufferover immutable collections when mutability is necessary for performance reasons, but choose immutable collections when mutability is not a requirement to leverage functional programming benefits.
vector
Scala Vectors
- Overview: Scala's
Vectoris an immutable collection that provides effective random access and updates. It's implemented as a trie (a tree-like data structure) with a branching factor of 32, balancing between quick access and functional updates. - Creating Vectors:
val emptyVector: Vector[Int] = Vector() val vector: Vector[Int] = Vector(1, 2, 3, 4, 5) - Key Properties:
- Immutable.
- Efficient for large collections.
- Provides fast random access and updates.
Basic Operations
- Accessing Elements: Use the apply method or simply parentheses.
val firstElement = vector(0) - Appending and Prepending Elements: Since Vectors are immutable, these operations return a new Vector.
val appended = vector :+ 6 val prepended = 0 +: vector - Concatenation: Combining two Vectors.
val vector2 = Vector(6, 7, 8) val concatenated = vector ++ vector2
Transformations
- Mapping: Apply a function to each element.
val doubled = vector.map(_ * 2) - Filtering: Select elements based on a predicate.
val evens = vector.filter(_ % 2 == 0) - Folding: Reduce elements using a binary operation.
val sum = vector.foldLeft(0)(_ + _)
Advanced Features and Performance
- Random Access Performance: Vectors offer excellent performance for both random reads and writes due to their trie-like structure, making them suitable for large collections.
- Memory Footprint: While providing fast access, Vectors might have a larger memory footprint than simple sequences like Lists, due to their underlying structure.
- Iteration Performance: Iterating over a Vector is efficient and comparable to iterating over an array.
Use Cases and Best Practices
- When to Use Vectors:
- Ideal for large collections requiring frequent reads and updates.
- When you need a general-purpose, immutable collection with good performance characteristics.
- Vector vs. List: Prefer Vectors over Lists for large collections or when you need fast access. Lists can be more efficient for operations that involve primarily head-based operations or when working with small collections.
- Immutable Collections: Embrace the use of immutable collections like Vectors to leverage the benefits of functional programming, such as easier reasoning about code and inherent thread safety.
Converting Between Vectors and Other Collections
- Conversion to Other Collections:
Vectors can be easily converted to and from other collection types.
val listFromVector = vector.toList val arrayFromVector = vector.toArray val seqFromVector = vector.toSeq
Java collections
Overview and Key Differences
- Java Collections: Part of the Java Collections Framework (JCF), focusing on mutable collections with an imperative programming style. Key interfaces include
List,Set,Map, and more specialized collections likeQueueandDeque. - Scala Collections: Designed for both mutable and immutable collections, encouraging a functional programming approach. Scala collections are divided into
scala.collection.immutableandscala.collection.mutablepackages, providing a unified API for manipulating collections in a more expressive manner. - Key Differences:
- Mutability: Java collections are mutable by default, whereas Scala offers both immutable and mutable collections.
- Functional Operations: Scala collections support a rich set of functional operations like
map,filter,reduce, which are not as seamlessly integrated into Java collections without Streams API. - Concurrency: Scala has a separate library (
scala.collection.concurrent) for concurrent collection operations, while Java integrates concurrent collections within the JCF (java.util.concurrent).
: Mutable Collections Comparison
-
Lists:
- Java:
ArrayList,LinkedList - Scala:
mutable.ListBuffer
- Java:
-
Sets:
- Java:
HashSet,LinkedHashSet,TreeSet - Scala:
mutable.HashSet,mutable.LinkedHashSet
- Java:
-
Maps:
- Java:
HashMap,LinkedHashMap,TreeMap - Scala:
mutable.HashMap,mutable.LinkedHashMap
- Java:
-
Usage Example:
- Java: Modifying a
Listrequires direct manipulation. - Scala:
ListBufferallows for easy append/prepend operations, and can be converted to an immutable List for functional operations.
- Java: Modifying a
Immutable Collections Comparison
- Lists:
- Java: Immutable lists can be created with
Collections.unmodifiableListor Java 9'sList.of. - Scala:
Listis the default immutable sequence.
- Java: Immutable lists can be created with
- Sets:
- Java:
Collections.unmodifiableSetor Java 9'sSet.of. - Scala:
Setfor immutable sets.
- Java:
- Maps:
- Java:
Collections.unmodifiableMapor Java 9'sMap.of. - Scala:
Mapfor immutable maps.
- Java:
- Usage Example:
- Java: Immutability is often an afterthought, wrapped around mutable collections.
- Scala: Immutability is the default, encouraging functional programming styles.
Functional Programming with Collections
- Java Streams API (Java 8+): Introduced to support functional-style operations on collections, like
filter,map,reduce. - Scala Collections API: Integrates functional operations directly into the collections, making them more concise and expressive for transformations.
- Example Comparison:
- Java:
List<String> filtered = list.stream().filter(s -> s.startsWith("a")).collect(Collectors.toList()); - Scala:
val filtered = list.filter(_.startsWith("a"))
- Java:
Interoperability and Conversions
- Scala-Java Interoperability: Scala provides implicit conversions between Java and Scala collections using
JavaConvertersorJavaConversions(deprecated) to facilitate interoperability. - Conversion Examples:
- Converting a Java
Listto a ScalaList:import scala.collection.JavaConverters._ val scalaList = javaList.asScala.toList - Converting a Scala
Seqto a JavaList:val javaList = scalaSeq.asJava
- Converting a Java
Choosing Between Java and Scala Collections
- Considerations:
- Performance: Java collections might offer better performance for certain operations, especially in low-latency environments.
- Immutability: Scala's immutable collections are ideal for functional programming and concurrency.
- Interoperability: Use Java collections when working closely with Java codebases; otherwise, prefer Scala collections for Scala projects.
- Best Practice: Leverage the strengths of both ecosystems by choosing the right tool for the task, considering factors like mutability, performance, and the programming paradigm.
function
Functions in Scala
- Overview: Functions in Scala are declared with the
defkeyword, followed by a name, parameter list, return type, and the function body. - Basic Syntax:
def functionName(param1: Type1, param2: Type2): ReturnType = { // Function body // Last expression is the return value } - Example: A simple function to add two numbers.
def add(x: Int, y: Int): Int = x + y - Key Concepts:
- Functions can be assigned to variables and passed as arguments.
- Scala supports both procedure (functions that return
Unit) and functions that return a value.
Higher-Order Functions
-
Overview: Higher-order functions are functions that can take functions as parameters and/or return functions as results.
-
Examples:
- Taking a function as a parameter:
def applyOperation(a: Int, b: Int, operation: (Int, Int) => Int): Int = operation(a, b) - Returning a function:
def multiplier(factor: Int): Int => Int = (x: Int) => x * factor
- Taking a function as a parameter:
-
Use Cases: Higher-order functions are instrumental in enabling functional programming patterns like map, filter, and reduce.
-
Anonymous Functions and Lambdas
- Overview: Scala allows the definition of anonymous functions (or lambdas), which are functions without a name.
- Syntax:
(param1: Type1, param2: Type2) => expression - Example:
val add = (x: Int, y: Int) => x + y val numbers = List(1, 2, 3) val incremented = numbers.map(n => n + 1)
Functions vs Methods
- Differences:
- Definition: Methods in Scala are defined with the
defkeyword and are part of a class or object. Functions are first-class values that can exist independently. - Usage: Methods can be converted to functions if needed (method lifting).
- Definition: Methods in Scala are defined with the
- Example of Method to Function Conversion:
object MathOps { def addMethod(x: Int, y: Int): Int = x + y } val addFunction: (Int, Int) => Int = MathOps.addMethod _
Best Practices and Tips
- Immutability: Favor immutable data structures and operations without side effects for easier reasoning and debugging.
- Composition: Leverage function composition to build more complex operations from simpler ones.
- Type Inference: Utilize Scala's type inference to simplify function declarations without losing clarity.
parameters
Basic Parameters
- Overview: Fundamental to defining functions, basic parameters are specified in the function definition and are required when calling the function.
- Syntax:
def functionName(param1: Type1, param2: Type2): ReturnType = { // Function body } - Example:
def add(x: Int, y: Int): Int = x + y
Default Parameters
- Overview: Default parameters allow functions to be called with fewer arguments than defined, by specifying default values.
- Syntax:
def functionName(param1: Type1 = defaultValue1): ReturnType = { // Function body } - Example:
def log(message: String, level: String = "INFO"): Unit = println(s"[$level] $message") log("System starting") // Uses the default level of INFO
Named Arguments
- Overview: Named arguments enable the specification of arguments out of order by naming them directly in the function call.
- Syntax:
functionName(param1 = value1, param2 = value2) - Example:
def printDetails(name: String, age: Int, country: String): Unit = { println(s"Name: $name, Age: $age, Country: $country") } printDetails(age = 30, name = "Alice", country = "Canada")
Variable Arguments (Varargs)
- Overview: Variable arguments (varargs) allow functions to accept an arbitrary number of arguments of the same type.
- Syntax:
def functionName(args: Type*): ReturnType = { // Function body } - Example:
def sum(numbers: Int*): Int = numbers.sum println(sum(1, 2, 3, 4)) // Outputs 10
Higher-Order Functions as Parameters
- Overview: Functions can take other functions as parameters, allowing for powerful abstractions and operations.
- Syntax:
def functionName(f: (Type1, Type2) => ReturnType): ResultType = { // Function body } - Example:
def applyOperation(a: Int, b: Int, operation: (Int, Int) => Int): Int = operation(a, b) println(applyOperation(5, 3, _ + _)) // Outputs 8
Best Practices
- Use Default Parameters to Avoid Overloading: Instead of creating multiple overloaded methods, use default parameters to simplify the API.
- Leverage Named Arguments for Clarity: Especially in functions with many parameters or boolean flags, named arguments can enhance readability.
- Prefer Varargs for Flexible Argument Lists: When the number of arguments is not fixed, use varargs to make your functions more adaptable.
function calls
Basic Function Calls
- Overview: Calling a function in Scala involves specifying the function name followed by its arguments in parentheses, if any are required.
- Syntax:
functionName(arg1, arg2, ..., argN) - Example:
def add(x: Int, y: Int): Int = x + y val result = add(5, 3) // Calls the add function with 5 and 3 as arguments
Higher-Order Function Calls
- Overview: Higher-order functions are functions that take other functions as parameters or return them as results. Calling them involves passing functions as arguments.
- Example of Passing Functions as Arguments:
def applyOperation(a: Int, b: Int, operation: (Int, Int) => Int): Int = operation(a, b) val sum = applyOperation(5, 3, (x, y) => x + y) // Using a lambda expression as an argument - Example of Using Functions Returned by Other Functions:
def greeting(language: String): String => String = { language match { case "English" => (name: String) => s"Hello, $name!" case "Spanish" => (name: String) => s"Hola, $name!" } } val englishGreeting = greeting("English") println(englishGreeting("Alice")) // Outputs: Hello, Alice!
Calling Functions with Named Arguments
- Overview: Scala allows calling functions with named arguments, letting you specify the name of the parameters explicitly, which can enhance readability and allow for arguments to be passed in any order.
- Syntax:
functionName(paramName1 = value1, paramName2 = value2) - Example:
def printDetails(name: String, age: Int, country: String): Unit = { println(s"Name: $name, Age: $age, Country: $country") } printDetails(age = 30, name = "Alice", country = "Canada") // Arguments are out of order but correctly named
Using Default and Optional Parameters
- Overview: Functions in Scala can define default values for parameters, allowing those parameters to be omitted when the function is called.
- Syntax:
def functionName(param1: Type = defaultValue1, param2: Type = defaultValue2): ReturnType = { ... } - Example:
def log(message: String, level: String = "INFO"): Unit = println(s"[$level] $message") log("System startup") // Level parameter is optional due to a default value
Best Practices in Function Calls
- Clarity and Readability: Prefer named arguments for functions with multiple parameters, especially boolean flags, to enhance code readability.
- Use Default Parameters Wisely: Default parameters can simplify API usage but use them judiciously to avoid confusion about function behavior.
- Leverage Higher-Order Functions: Embrace higher-order functions for more expressive and concise code, especially when working with collections or implementing patterns that benefit from function composition.
composition
Function Composition
- Overview: Function composition is a technique where the result of one function is passed as the input to another function. In Scala, this can be achieved using the
composeandandThenmethods. - Key Concepts:
- Compose: Given two functions,
fandg, thecomposemethod creates a new function wheregis applied first and thenf(f(g(x))). - AndThen: Similar to
compose, but applies the first function followed by the second (g(f(x))).
- Compose: Given two functions,
: Composing Functions with compose
- Syntax and Example:
val f: Int => Double = _ * 1.5 val g: Double => Double = _ + 10 val h = f.compose(g) // h(x) = f(g(x)) // Example usage h(5) // First applies g to 5, then applies f to the result of g(5) - Use Cases:
composeis particularly useful when you want to reverse the order of function application, starting with the function passed as an argument tocompose.
Composing Functions with andThen
- Syntax and Example:
val f: Int => Double = _ * 1.5 val g: Double => Double = _ + 10 val h = f.andThen(g) // h(x) = g(f(x)) // Example usage h(5) // First applies f to 5, then applies g to the result of f(5) - Use Cases:
andThenis useful for creating a chain of function applications where the output of one function is the input to the next in a straightforward, left-to-right sequence.
Function Composition Using Anonymous Functions
- Overview: Anonymous functions (or lambda expressions) can be composed directly without the need for named functions.
- Example:
val multiplyBy2 = (x: Int) => x * 2 val add3 = (y: Int) => y + 3 val composed = multiplyBy2.andThen(add3) // Example usage composed(5) // First multiplies 5 by 2, then adds 3 to the result
Practical Applications
- Data Transformation Pipelines: Use function composition to create pipelines for processing data, where each function performs a transformation or filtering operation.
- Modularizing Code: Break down complex operations into simpler functions that can be composed, making the code easier to understand and test.
Best Practices
- Debuggability: While function composition can make code more concise, it can also make debugging more challenging. Consider readability and the ease of debugging when composing multiple functions.
- Performance: Be mindful of the performance implications of creating deeply composed functions, especially if they are applied frequently or operate on large data sets.
- Type Compatibility: Ensure that the output type of one function matches the input type of the next in the composition chain to avoid compile-time errors.
recursion
Introduction to Recursion
- Overview: Recursion occurs when a function calls itself to solve a problem. It's particularly common in functional programming languages like Scala, where functions are first-class citizens.
- Key Concepts:
- Base Case: The condition under which the recursive function stops calling itself, preventing infinite recursion.
- Recursive Case: The part of the function where the recursion occurs, typically altering the inputs with each recursive call to eventually reach the base case.
: Writing Recursive Functions
- Syntax and Structure:
def functionName(parameters): ReturnType = { if (baseCaseCondition) baseCaseSolution else functionName(modifiedParameters) // Recursive call } - Example - Factorial Function:
def factorial(n: Int): Int = { if (n <= 1) 1 else n * factorial(n - 1) }
Tail Recursion in Scala
- Overview: Tail recursion is a special case of recursion where the recursive call is the last operation in the function. Scala can optimize tail-recursive functions to prevent stack overflow errors.
- Key Concepts:
- Tail Recursive Function: A recursive function where the compiler can optimize the recursive calls to avoid consuming additional stack frames.
- Scala's
@tailrecAnnotation: Used to ensure a function is tail-recursive. If it's not, the compiler will throw an error.
- Example - Tail-Recursive Factorial:
import scala.annotation.tailrec def factorial(n: Int): Int = { @tailrec def loop(acc: Int, n: Int): Int = { if (n <= 1) acc else loop(acc * n, n - 1) } loop(1, n) }
Common Pitfalls in Recursive Functions
- Stack Overflow Error: Occurs when too many recursive calls are made without reaching the base case, exceeding the stack size limit. This is particularly common in non-tail-recursive functions.
- Incorrect Base Case: Leads to infinite recursion if the base case is not correctly defined or never reached.
- Performance Considerations: While recursion can simplify code, it may lead to performance issues compared to iterative solutions, especially if not optimized for tail recursion.
Best Practices
- Choosing Iteration vs. Recursion: Consider using iteration for simple loops to avoid stack overflow risks and potential performance issues, especially when tail recursion cannot be applied.
- Optimizing Recursive Functions: Leverage tail recursion where possible to optimize recursive functions, and use Scala's
@tailrecannotation to ensure the compiler can apply tail call optimization. - Debugging Recursive Functions: Break down the function calls and carefully trace the execution path, paying special attention to the base and recursive cases.
console
: Writing to the Console
- Overview: Writing to the console in Scala is performed using the
println,print, orprintfmethods, which are available by default as they are inherited from Scala’s Predef object. - Key Methods:
println: Outputs a line to the console with a newline character at the end.print: Similar toprintlnbut does not append a newline at the end.printf: Allows formatted output, similar to C'sprintf.
- Example:
println("Hello, World!") // Prints with a newline print("Hello, Scala ") // Prints without a newline printf("Age: %d", 25) // Prints formatted string
Reading from the Console
- Overview: Reading input from the console allows interactive Scala applications to capture user input. This is typically done using
scala.io.StdInmethods. - Importing
StdIn: Before reading from the console, ensure you importStdInmethods:import scala.io.StdIn._ - Key Methods:
readLine: Reads a line of text from the console.readInt,readDouble, etc.: Reads and converts input to a specific type.
- Example:
println("Enter your name: ") val name = readLine() println(s"Hello, $name!")
Handling Input Errors
- Overview: When reading input from the console, it’s important to handle potential errors such as format mismatches (e.g., expecting an integer but receiving text).
- Try-Catch Blocks: Scala’s try-catch blocks can be used to handle errors gracefully when reading input.
- Example:
import scala.io.StdIn._ import scala.util.Try println("Enter an integer: ") val input = readLine() val maybeInt = Try(input.toInt).toOption maybeInt match { case Some(i) => println(s"You entered: $i") case None => println("That's not an integer!") }
: Advanced Console I/O
- Overview: Beyond basic text input and output, Scala can interact with the console in more sophisticated ways, including reading hidden input (like passwords) and redirecting output.
- Reading Hidden Input:
Scala does not provide a built-in method for reading passwords directly from the console without displaying them. However, Java’s
System.console().readPassword()can be utilized within Scala for this purpose. - Redirecting Output: While Scala applications typically write to standard output (stdout), it’s possible to redirect output to files or other streams using Java's I/O capabilities.
Best Practices in Console I/O
- User Experience: Provide clear prompts and feedback when expecting input and displaying output to ensure a good user experience.
- Validation and Error Handling: Always validate and handle errors in user input to prevent crashes or unexpected behavior.
- Avoiding Hardcoding: For more complex applications, consider using external configuration or argument parsing libraries instead of relying heavily on console I/O.
file
Overview of File I/O in Scala
- Introduction: File I/O operations are essential for reading data from files and writing data to files, enabling persistence and data exchange.
- Scala and Java I/O: Scala does not have its own file I/O library and typically uses Java's I/O classes found in
java.ioandjava.niopackages.
Writing to Files
- Using
java.io.PrintWriter: Scala can utilize Java’sPrintWriterfor simple file writing tasks.- Example:
import java.io.PrintWriter val pw = new PrintWriter(new File("example.txt")) try { pw.write("Hello, Scala file I/O!") } finally { pw.close() }
- Example:
- Using
java.nio.file(Java 7+): The newer Java NIO package provides a more modern and flexible approach to file I/O.- Example:
import java.nio.file.{Paths, Files} import java.nio.charset.StandardCharsets val path = Paths.get("example_nio.txt") val lines = Seq("Hello", "Scala", "NIO").asJava Files.write(path, lines, StandardCharsets.UTF_8)
- Example:
Reading from Files
- Using
scala.io.Source: Scala’sSourceclass offers a convenient way to read from files line by line.- Example:
import scala.io.Source val filename = "example.txt" for (line <- Source.fromFile(filename).getLines()) { println(line) }
- Example:
- Using
java.nio.file.Files: Java NIO also provides methods for reading files, such as reading all lines at once into a List.- Example:
import java.nio.file.{Paths, Files} import java.util.stream.Collectors import scala.jdk.CollectionConverters._ val path = Paths.get("example_nio.txt") val lines = Files.readAllLines(path).asScala lines.foreach(println)
- Example:
Exception Handling in File I/O
- Overview: Handling exceptions is critical in file I/O operations to deal with issues like missing files, access rights, or disk space.
- Try-Catch-Finally:
Using
try-catch-finallyblocks ensures that resources are closed properly, even when an error occurs.- Example:
import scala.io.Source import scala.util.Using val filename = "example.txt" try { Using.resource(Source.fromFile(filename)) { source => for (line <- source.getLines()) { println(line) } } } catch { case e: Exception => e.printStackTrace() }
- Example:
Advanced File I/O Operations
- Working with Large Files: For large files, consider using streams or readers that don't load the entire file into memory.
- File Attributes and Operations: Java NIO provides tools to check file attributes, such as size or modification time, and to perform operations like moving or copying files.
Best Practices
- Resource Management: Always ensure that file readers and writers are properly closed to prevent resource leaks.
- File Paths: Be mindful of platform-specific path separators and use the
Pathsutility to construct paths that are portable across operating systems. - Error Handling: Robust error handling and input validation can prevent many common pitfalls in file I/O operations, such as handling non-existent files or permission issues.
internet
Reading Content from a URL
-
Import Necessary Classes: Start by importing Java's
URLclass, which represents a Uniform Resource Locator, and other necessary classes for reading the input stream.import java.net.URL import java.io.{BufferedReader, InputStreamReader} import scala.util.Using -
Create a URL Instance: Create an instance of the
URLclass by passing the desired URL as a string to the constructor.val url = new URL("http://example.com") -
Open a Connection and Read: Use the
openStreammethod to open a connection to the URL and fetch the content. It's recommended to use aBufferedReaderfor efficient reading of text data.Using.resource(new BufferedReader(new InputStreamReader(url.openStream()))) { reader => var inputLine = reader.readLine() while (inputLine != null) { println(inputLine) inputLine = reader.readLine() } }The
Using.resourceblock ensures that the reader is closed properly after use, which is a part of Scala's standard library to manage resources automatically and safely.
Handling Exceptions
When performing network operations, numerous exceptions can occur, such as java.net.MalformedURLException if the URL is not valid, or java.io.IOException if an I/O error occurs. It's crucial to handle these exceptions gracefully:
try {
Using.resource(new BufferedReader(new InputStreamReader(url.openStream()))) { reader =>
var inputLine = reader.readLine()
while (inputLine != null) {
println(inputLine)
inputLine = reader.readLine()
}
}
} catch {
case e: Exception => e.printStackTrace()
}
Asynchronous and Advanced HTTP Requests
For more complex use cases, such as asynchronous requests, handling HTTP methods other than GET, or managing HTTP headers and status codes, you might consider using dedicated Scala libraries like sttp, scalaj-http, or integrating with Java libraries like Apache HttpClient. These libraries offer more control and flexibility over HTTP requests and responses.
swing
Scala Swing
- Overview: Scala Swing is a part of the Scala standard library that provides a high-level API for creating graphical user interfaces. It is built on top of Java Swing, aiming to be more idiomatic to Scala.
- Key Components:
- Simple Components: Labels, Buttons, Text Fields, and Text Areas.
- Containers: Panels, Frames, and Windows.
- Layout Managers: Box Layout, Grid Layout, Border Layout, and Flow Layout.
- Event Handling: Scala Swing simplifies event handling through listeners and reactions, making GUI programming more straightforward and type-safe.
Creating Swing Application
- Basic Structure: A simple Scala Swing application usually includes a main frame and some UI components.
- Example: Creating a simple window with a button.
import scala.swing._ object SimpleApp extends SimpleSwingApplication { def top = new MainFrame { title = "Simple Scala Swing App" contents = new Button { text = "Click me" reactions += { case _: event.ButtonClicked => println("Button clicked!") } } size = new Dimension(300, 200) } } - Explanation: This sheet should explain the components used, such as
MainFrameandButton, and how to set properties liketitle,contents, andsize.
Handling Events
- Overview: Event handling in Scala Swing uses a more declarative approach than Java Swing, utilizing partial functions and pattern matching.
- Example: Adding a reaction to a button click.
button.reactions += { case event.ButtonClicked(_) => println("Button was clicked!") } - Key Concepts:
- Reactions: A collection where event handlers are registered.
- Event Types: Different types of events that can be handled, such as
ButtonClicked,WindowClosing, etc.
Layout Management
- Overview: Scala Swing provides several layout managers to control the arrangement of components within containers.
- Example: Using a
BorderLayout.new BorderPanel { layout += new Button("North") -> BorderPanel.Position.North layout += new Button("South") -> BorderPanel.Position.South } - Explanation: Discuss how to use different layout managers and the importance of positioning components correctly within the GUI.
Swing Components
- Overview: Beyond basic components, Scala Swing offers advanced components for more complex user interfaces, such as Tables, Trees, Menus, and Dialogs.
- Example: Creating a menu bar.
new MenuBar { contents += new Menu("File") { contents += new MenuItem(Action("Open") { println("Open clicked!") }) contents += new MenuItem(Action("Exit") { sys.exit(0) }) } } - Explanation: Introduce some advanced components and how they can be used to build more functional and interactive applications.
Best Practices and Tips
- Design Considerations: Encourage thoughtful UI design, focusing on user experience, accessibility, and responsiveness.
- Separation of Concerns: Advocate for separating the GUI layout and design from the application logic, possibly using the Model-View-Controller (MVC) pattern.
- Performance Tips: Discuss the importance of minimizing unnecessary component updates and using listeners judiciously to maintain responsive and performant applications.
scala 3
Introduction to Indentation Syntax in Scala 3
- Overview: Scala 3 introduces an indentation-based syntax as an alternative to the traditional braces-based syntax, influenced by languages like Python. This change aims to make code more concise and readable.
- Key Concepts:
- Significant Whitespace: Whitespace (indentation) is now used to denote block structure, eliminating the need for curly braces in many cases.
- Compatibility: The indentation-based syntax is optional; Scala 3 codebases can use either or both styles, promoting gradual adoption and compatibility with Scala 2 code.
Basic Rules of Indentation Syntax
-
Indentation Blocks: An indentation block starts after a colon (
:) that indicates a control structure (if,while,for), class, or method definition followed by a newline. The subsequent lines must be indented more than the line with the colon. -
Example - Method Definition:
def sum(a: Int, b: Int): Int = val total = a + b total -
Example - Control Structures:
if a > b then println("a is greater") else println("b is greater or equal") -
Example - Loops:
for 1 to 10 do println("a is greater")
Using Indentation for Classes and Objects
- Classes and Objects: The indentation syntax also applies to class and object definitions, where members are defined within an indentation block.
- Example:
class Person(name: String, age: Int): def greet = println(s"Hi, my name is $name and I am $age years old.") object MyApp: def main(args: Array[String]): Unit = val person = new Person("Alice", 30) person.greet
Indentation with Match Expressions
- Match Expressions: Scala 3 enhances match expressions with indentation, making them more readable and expressive.
- Example:
val result = x match case 1 => "one" case 2 => "two" case _ => "other"
Multiline Expressions and Indentation
- Multiline Expressions: When writing expressions that span multiple lines, ensure that the continuation lines are indented to signal that the expression is not complete.
- Example:
val total = a + b + c
Best Practices
- Indentation: The indentation is the preferred style
- Consistency: Stick to one style (either indentation or braces) within a file to maintain readability.
- Adoption: Start using indentation syntax in smaller, less complex code blocks to become familiar with the concept before applying it to larger codebases.
- Tool Support: Use Scala 3 compatible tools and IDEs that understand and correctly format indentation-based syntax.
Transitioning from Scala 2 to Scala 3
- Mixed Syntax: Scala 3 allows mixing both braces and indentation syntax, facilitating gradual migration from Scala 2 to Scala 3.
- Migration Tools: Leverage Scala 3 migration tools and linters that can automatically convert braces to indentation, aiding in the transition process.
futures
Scala Futures
- Overview: Scala Futures provide a way to perform asynchronous operations, enabling tasks to run concurrently without blocking the main execution flow. They represent a value that may become available at some point in the future.
- Importing Futures:
import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global - Creating a Future: A Future is initialized by passing a block of code that executes asynchronously.
val myFuture: Future[Int] = Future { // Code that returns an Int after some computation Thread.sleep(1000) // Simulating a long computation 42 }
Future Results
- Callbacks: Futures provide methods like
onComplete,onSuccess, andonFailureto handle their results asynchronously.myFuture.onComplete { case Success(value) => println(s"The result is $value") case Failure(exception) => println(s"Failed with $exception") } - Awaiting Results: While generally discouraged, Scala allows blocking until a Future is completed using
Await.result.import scala.concurrent.Await import scala.concurrent.duration._ val result = Await.result(myFuture, 10.seconds)
Composing Futures
- Sequencing Operations: Use methods like
mapandflatMapto chain multiple asynchronous operations without blocking.val anotherFuture: Future[String] = myFuture.map(_.toString) - Parallel Execution: Use
Future.sequenceto transform a sequence of Futures into a Future that contains a sequence of results.val futureList: List[Future[Int]] = List(Future(1), Future(2), Future(3)) val combinedFuture: Future[List[Int]] = Future.sequence(futureList)
Error Handling
- Recovering from Failures: Futures provide
recoverandrecoverWithmethods to handle exceptions and recover from failures.val recoveredFuture: Future[Int] = myFuture.recover { case _: ArithmeticException => 0 // Return a default value in case of failure }
Advanced Techniques
- For Comprehensions: Use for comprehensions for more readable code when working with multiple Futures.
val futureA: Future[Int] = Future(10) val futureB: Future[Int] = Future(20) val resultFuture: Future[Int] = for { a <- futureA b <- futureB } yield a + b - Timeouts: Implementing timeouts for Futures can prevent indefinitely waiting for a result.
import scala.concurrent.duration._ import scala.concurrent.Future import scala.util.{Failure, Success} import scala.concurrent.ExecutionContext.Implicits.global val futureWithTimeout = Future.firstCompletedOf(Seq(myFuture, Future { Thread.sleep(5000) // 5 seconds timeout throw new TimeoutException("Operation timed out") })) futureWithTimeout.onComplete { case Success(value) => println(s"Completed with value: $value") case Failure(exception) => println(s"Completed with exception: $exception") }
Best Practices
- ExecutionContext: Be conscious of the
ExecutionContextused for executing futures. It's responsible for running the computations. The global context is a good default, but specific contexts might be needed for IO-intensive or CPU-bound tasks. - Avoid Blocking: Maximize the benefits of Futures by avoiding blocking operations. Use non-blocking techniques like callbacks and for comprehensions.
- Error Handling: Utilize
recoverandrecoverWithfor robust error handling in asynchronous code paths.
Common Use Cases
- Web Services Calls: Making non-blocking calls to web services, allowing other operations to continue while waiting for responses.
- Database Operations: Performing database operations asynchronously to improve application responsiveness.
- CPU-bound Tasks: Distributing computationally intensive tasks across multiple cores or nodes without blocking the main application flow.
repl
Scala REPL
- Overview: The Scala REPL is an interactive command-line interface for Scala. It reads Scala expressions, evaluates them, and prints the results, making it a useful tool for experimentation and learning.
- Starting the REPL: Typically, you start the REPL by typing
scalain your command line after Scala has been installed on your system. This launches an interactive session where you can type and evaluate Scala expressions. - Basic Usage: In the REPL, you can type Scala expressions or definitions, and they will be immediately compiled and executed. The REPL provides feedback on the types and values of the expressions.
Basic Operations
- Evaluating Expressions: Type any valid Scala expression, and the REPL will evaluate it, showing the result and type.
scala> 1 + 1 res0: Int = 2 - Defining Variables and Functions: You can define variables and functions directly in the REPL.
scala> val x = 5 x: Int = 5 scala> def addOne(a: Int) = a + 1 addOne: (a: Int)Int - Loading External Libraries: Use
:requireor:loadto include external Scala files or libraries in your REPL session for testing or exploration.
Advanced Features
- Tab Completion: The REPL supports tab completion, which can help you find method names, variable names, and more.
- Inspecting Types: Use
:typeto inspect the type of an expression without evaluating it.scala> :type addOne Int => Int - Viewing Imported Names: Use
:importsto see a list of all imported names in the current session.
Customizing Scala REPL
- Custom Initialization: You can customize the REPL environment through a
.scalafile in your home directory named.scalarcthat contains Scala code to be executed at startup. - REPL Command History: The REPL keeps a history of commands. You can navigate through this history using the up and down arrow keys, and search through it using
Ctrl+R.
Integrating with IDEs
- IDE Support: Many Integrated Development Environments (IDEs) offer integrated REPL tools, providing a seamless development experience by combining the benefits of the REPL with the features of the IDE.
- Ammonite REPL: An alternative to the Scala REPL, Ammonite offers additional features like better syntax highlighting, multi-line editing, and more powerful integration capabilities.
#
Best Practices
- Experimentation and Learning: Use the REPL as a playground for learning new Scala features, experimenting with language constructs, or testing library functions.
- Rapid Prototyping: Quickly prototype functions and algorithms in the REPL before integrating them into a larger Scala project.
- Debugging and Testing: Test small pieces of code or expressions related to a larger problem you're debugging.
Limitations
- Performance: The REPL is not optimized for performance testing. The evaluation time in the REPL may not accurately reflect the performance of compiled Scala code.
- State Management: In complex sessions, managing the state and imports can become cumbersome. Restarting the REPL session can sometimes be necessary to clear the state.
tools
Build Tools
-
sbt (Scala Build Tool):
- Overview: sbt is the de facto build tool for Scala projects, offering powerful features for compiling, running, and testing Scala code. It supports incremental compilation and integrates well with continuous integration systems.
- Key Features: Dependency management, multi-project builds, interactive shell, custom tasks and plugins.
- Website: https://www.scala-sbt.org/
-
Mill:
- Overview: Mill is a newer build tool aimed at simplicity and performance, offering a more straightforward configuration than sbt.
- Key Features: Simple configuration syntax, incremental compilation, and integration with common Scala tools and libraries.
- Website: https://com-lihaoyi.github.io/mill/
Integrated Development Environments (IDEs)
-
IntelliJ IDEA:
- Overview: IntelliJ IDEA by JetBrains offers robust Scala support through the Scala plugin, providing an integrated environment for Scala development, including sbt and Play Framework support.
- Key Features: Intelligent code completion, on-the-fly code analysis, refactoring tools, and debugging support.
- Website: https://www.jetbrains.com/idea/
-
Visual Studio Code with Metals:
- Overview: Visual Studio Code (VS Code), combined with the Metals plugin, provides a lightweight yet powerful Scala development experience.
- Key Features: Code completion, diagnostics, goto definition, code formatting, debugging, and integrated terminal.
- Metals Website: https://scalameta.org/metals/
Testing Frameworks
-
ScalaTest:
- Overview: ScalaTest is a flexible testing framework that supports different styles of testing, making it adaptable to various testing needs.
- Key Features: Support for TDD, BDD, and integration testing; easily integrates with sbt and IDEs.
- Website: https://www.scalatest.org/
-
Specs2:
- Overview: Specs2 is a library for writing executable software specifications. With its BDD approach, it's particularly well-suited for unit and acceptance testing.
- Key Features: Support for matchers, mocking, and specification structuring; integrates with sbt and IDEs.
- Website: https://specs2.org/
Linters and Code Formatting Tools
-
Scalafmt:
- Overview: Scalafmt is a code formatter for Scala, ensuring consistent code style across your project and team.
- Key Features: Customizable formatting rules, IDE and build tool integration.
- Website: https://scalameta.org/scalafmt/
-
Scalafix:
- Overview: Scalafix is a refactoring and linting tool for Scala, helping to enforce best practices and migrate codebases to new Scala versions.
- Key Features: Custom linting rules, automatic code fixes, and migration aids for upgrading Scala versions.
- Website: https://scalacenter.github.io/scalafix/
Performance
- Kamon:
- Overview: Kamon provides instrumentation for monitoring applications built with Scala, Akka, and Play Framework, offering insights into application performance and bottlenecks.
- Key Features: Metrics collection, distributed tracing, and context propagation.
- Website: https://kamon.io/