Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3151ef3
Update task.md
stephen-hero May 6, 2024
62615db
Update task.md
stephen-hero May 7, 2024
8e40b40
Update task.md
stephen-hero May 7, 2024
9563f2a
Update task.md
stephen-hero May 7, 2024
62888a7
Update task.md
stephen-hero May 7, 2024
499ebf5
Update task.md
stephen-hero May 13, 2024
e4cc0de
Update task.md
stephen-hero May 13, 2024
d41b239
Update task.md
stephen-hero May 13, 2024
79ae107
Update task.md
stephen-hero May 13, 2024
b4a0c66
Update task.md
stephen-hero May 13, 2024
31d0ecf
Update task.md
stephen-hero May 14, 2024
d33f348
Update task.md
stephen-hero May 14, 2024
4e1a1b1
Update task.md
stephen-hero May 14, 2024
baaaaa0
Update task.md
stephen-hero May 14, 2024
a1b355a
Update task.md
stephen-hero May 14, 2024
22f8e78
Update task.md
stephen-hero May 15, 2024
b8c3469
Update task.md
stephen-hero May 15, 2024
21b095a
Update task.md
stephen-hero May 15, 2024
6a08242
Update task.md
stephen-hero May 15, 2024
bf811d2
Update task.md
stephen-hero May 16, 2024
97c6032
Update task.md
stephen-hero May 16, 2024
17abccf
Update task.md
stephen-hero May 16, 2024
4e05669
Update task.md
stephen-hero May 16, 2024
fb2ca0a
Update task.md
stephen-hero May 16, 2024
a4e1bdc
Update task.md
stephen-hero May 17, 2024
aed7f57
Update task.md
stephen-hero May 17, 2024
482cf52
Update task.md
stephen-hero May 17, 2024
89c86eb
Update task.md
stephen-hero May 17, 2024
7d97044
Update task.md
stephen-hero May 17, 2024
e31335a
Update task.md
stephen-hero May 20, 2024
82a5860
Update task.md
stephen-hero May 20, 2024
f10ac68
Update task.md
stephen-hero May 20, 2024
6bc3271
Update task.md
stephen-hero May 20, 2024
9761989
Update task.md
stephen-hero May 20, 2024
a85570f
Update task.md
stephen-hero May 20, 2024
b9f3683
Update README.md
stephen-hero May 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 31 additions & 31 deletions Early Returns/Baby Steps/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

First, let's consider a concrete example of a program in need of early returns.
Let's assume we have a database of user entries.
The access to the database is resource-heavy, and the user data is large.
Because of this, we only operate on user identifiers and retrieve the user data from the database only if needed.
Accessing this database is resource-intensive, and the user data is extensive.
As a result, we only operate on user identifiers and retrieve the user data from the database only when necessary.

Now, imagine that many of those user entries are invalid in one way or the other.
Now, imagine that many of those user entries are invalid in some way.
For the brevity of the example code, we'll confine our attention to incorrect emails: those that either
contain a space character or have the number of `@` symbols which is different from `1`.
In the latter tasks, we'll also discuss the case when the user with the given ID does not exist in the database.
contain a space character or have a count of `@` symbols different from `1`.
In subsequent tasks, we'll also discuss the case when the user with the given ID does not exist in the database.

We'll start with a sequence of user identifiers.
Given an identifier, we first retrieve the user data from the database.
This operation corresponds to the *conversion* in the previous lesson: we convert an integer number into an
instance of class `UserData`.
instance of the `UserData` class.
Following this step, we run *validation* to check if the email is correct.
Once we found the first valid instance of `UserData`, we should return it immediately without processing
of the rest of the sequence.
Upon locating the first valid instance of `UserData`, we should return it immediately, ceasing any further processing
of the remaining sequence.

```scala 3
object EarlyReturns:
Expand Down Expand Up @@ -60,8 +60,8 @@ object EarlyReturns:
```

The typical imperative approach is to use an early return from a `for` loop.
We perform the conversion followed by validation and, if the data is valid, we return the data, wrapped in `Some`.
If no valid user data has been found, then we return None after going through the whole sequence of identifiers.
We perform the conversion, followed by validation, and if the data is found valid, we return it, wrapped in `Some`.
If no valid user data has been found, we return `None` after traversing the entire sequence of identifiers.

```scala 3
/**
Expand All @@ -74,12 +74,12 @@ If no valid user data has been found, then we return None after going through th
None
```

This solution is underwhelming because it uses `return` which is not idiomatic in Scala.
This solution is underwhelming because it uses `return`, which is not idiomatic in Scala.

A more functional approach is to use higher-order functions over collections.
We can `find` a `userId` in the collection, for which `userData` is valid.
But this necessitates calling `complexConversion` twice, because `find` returns the original identifier instead
of the `userData`.
We can `find` a `userId` in the collection for which the `userData` is valid.
However, this necessitates calling `complexConversion` twice, as `find` returns the original identifier rather
than the `userData`.

```scala 3
/**
Expand All @@ -92,12 +92,12 @@ of the `userData`.
```

Or course, we can run `collectFirst` instead of `find` and `map`.
This implementation is more concise than the previous, but we still cannot avoid running the conversion twice.
In the next lesson, we'll use a custom `unapply` method to get rid of the repeated computations.
This implementation is more concise than the previous one, but it still doesn't allow us to avoid running the conversion twice.
In the next lesson, we'll use a custom `unapply` method to eliminate the need for these repeated computations.

```scala 3
/**
* A more concise implementation which uses `collectFirst`.
* A more concise implementation, which uses `collectFirst`.
*/
def findFirstValidUser3(userIds: Seq[UserId]): Option[UserData] =
userIds.collectFirst {
Expand All @@ -108,23 +108,23 @@ In the next lesson, we'll use a custom `unapply` method to get rid of the repeat

### Exercise

Let's come back to one of our examples from an earlier module.
You are managing a cat shelter and keeping track of cats, their breeds and coats in a database.
Let's revisit one of our examples from an earlier module.
You are managing a cat shelter and keeping track of cats, their breeds, and coat types in a database.

You notice that there are a lot of mistakes in the database introduced by a previous employee: there are short-haired mainecoons, long-haired sphynxes, and other inconsistensies.
You don't have time to fix the database right now, because you see a potential adopter coming into the shelter.
Your task is to find the first valid entry in the database to present the potential adopter with a cat.
You notice numerous mistakes in the database made by a previous employee: there are short-haired Maine Coons, long-haired Sphynxes, and other inconsistensies.
You don't have the time to fix the database right now because you see a potential adopter coming into the shelter.
Your task is to find the first valid entry in the database and present the potential adopter with a cat.

Implement `catConversion` method that fetches a cat from the `catDatabase` in the `Database.scala` file by its identifier.
To do so, you will first need to consult another database "table" `adoptionStatusDatabase` to find out the name of a cat.
Implement the `catConversion` method, which fetches a cat from the `catDatabase` in the `Database.scala` file by its identifier.
To do this, you will first need to consult another database "table", `adoptionStatusDatabase`, to find out the cat's name.

Then implement `furCharacteristicValidation` that checks that the fur characteristics in the database entry makes sense for the cat's particular breed.
Consult the map `breedCharacteristics` for the appropriate fur characteristics for each bread.
Then, implement the `furCharacteristicValidation` that checks if the fur characteristics in the database entry make sense for the cat's particular breed.
Consult the `breedCharacteristics` map for the appropriate fur characteristics for each breed.

Finally, implement the search using the conversion and validation methods:
* `imperativeFindFirstValidCat` that works in the imperative fashion.
* `functionalFindFirstValidCat`, in the functional style.
* `collectFirstFindFirstValidCat` using the `collectFirst` method.
* `imperativeFindFirstValidCat`, which works in an imperative fashion.
* `functionalFindFirstValidCat`, utilizing an functional style.
* `collectFirstFindFirstValidCat`, using the `collectFirst` method.

Ensure that your search does not traverse the whole database.
We put some simple logging in the conversion and validation methods so that you could make sure of that.
Ensure that your search does not traverse the entire database.
We've put some simple logging within the conversion and validation methods so that you can verify this.
10 changes: 5 additions & 5 deletions Early Returns/Breaking Boundaries/task.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
## Breaking Boundaries

Similarly to Java and other popular languages, Scala provides a way to break out of a loop.
Since Scala 3.3, it's achieved with a composition of boundaries and breaks which provides a cleaner alternative to
Since Scala 3.3, it's achieved with a composition of boundaries and breaks, which provides a cleaner alternative to
non-local returns.
With this feature, a computational context is established with `boundary:`, and `break` returns a value from within the
enclosing boundary.
Check out the [implementation](https://github.com/scala/scala3/blob/3.3.0/library/src/scala/util/boundary.scala)
if you want to know how it works under the hood.
One important thing is that it ensures that the users never call `break` without an enclosing `boundary` thus making
One important thing is that it ensures that the users never call `break` without an enclosing `boundary`, thus making
the code much safer.

The following snippet showcases the use of boundary/break in its simplest form.
Expand All @@ -24,15 +24,15 @@ Since it's the end of the method, it immediately returns `Some(userData)`.
None
```

Sometimes there are multiple boundaries, in this case one can add labels to `break` calls.
Sometimes, there are multiple boundaries, and in such cases, one can add labels to `break` calls.
This is especially important when there are embedded loops.
One example of using labels can be found [here](https://gist.github.com/bishabosha/95880882ee9ba6c53681d21c93d24a97).
An example of using labels can be found [here](https://gist.github.com/bishabosha/95880882ee9ba6c53681d21c93d24a97).

### Exercise

Finally, let's use boundaries to achieve the same result.

Let's try using lazy collection to achieve the same goal as in the previous tasks.
Let's try using a lazy collection to achieve the same goal as in the previous tasks.

* Use a boundary to implement `findFirstValidCat`.
* Copy the implementations of `furCharacteristicValidation` and `nonAdoptedCatConversion` from the previous task.
14 changes: 7 additions & 7 deletions Early Returns/Lazy Collection to the Rescue/task.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
## Lazy Collection to the Resque

One more way to achieve the same effect of an early return is to use the concept of a lazy collection.
A lazy collection doesn't store all its elements computed and ready to access.
A lazy collection doesn't store all its elements computed and ready for access.
Instead, it stores a way to compute an element once it's needed somewhere.
This makes it possible to simply traverse the collection until we encounter the element which fulfills the conditions.
Since we aren't interested in the rest of the collection, its elements won't be computed.
This makes it possible to simply traverse the collection until we encounter an element that fulfills the conditions.
Since we aren't interested in the rest of the collection, those elements won't be computed.

As we've already seen a couple of modules ago, there are several ways to make a collection lazy.
The first one is by using [iterators](https://www.scala-lang.org/api/current/scala/collection/Iterator.html): we can call the `iterator` method on our sequence of identifiers.
Another way is to use [views](https://www.scala-lang.org/api/current/scala/collection/View.html) as we've done in one of the previous modules.
As we've already seen a couple of modules ago, there are several ways to convert a collection into a lazy one.
The first is by using [iterators](https://www.scala-lang.org/api/current/scala/collection/Iterator.html): we can call the `iterator` method on our sequence of identifiers.
Another way is to use [views](https://www.scala-lang.org/api/current/scala/collection/View.html), as we've done in one of the previous modules.
Try comparing the two approaches on your own.

```scala 3
Expand All @@ -22,7 +22,7 @@ Try comparing the two approaches on your own.

### Exercise

Let's try using lazy collection to achieve the same goal as in the previous tasks.
Let's try using a lazy collection to achieve the same goal as in the previous tasks.

* Use a lazy collection to implement `findFirstValidCat`.
* Copy the implementations of `furCharacteristicValidation` and `nonAdoptedCatConversion` from the previous task.
48 changes: 24 additions & 24 deletions Early Returns/The Problem/task.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
## The Problem

It is often the case that we do not need to go through all the elements in a collection to solve a specific problem.
For example, in the Recursion chapter of the previous module we saw a function to search for a key in a box.
It was enough to find a key, any key, and there wasn't any point continuing the search in the box after one had been found.
For example, in the Recursion chapter of the previous module, we saw a function to search for a key in a box.
It was enough to find a single key, and there was no point in continuing the search in the box after one had been found.

The problem might get trickier the more complex data is.
Consider an application designed to track the members of your team, detailing which projects they worked on and the
The problem might get trickier as data becomes more complex.
Consider an application designed to track your team members, detailing the projects they worked on and the
specific days they were involved.
Then the manager of the team may use the application to run complicated queries such as the following:
* Find an occurrence of a day when the team worked more person-hours than X.
* Find an example of a bug which took more than Y days to fix.

It's common to run some kind of conversion on an element of the original data collection into a derivative entry which
describes the problem domain better.
Then this converted entry is validated with a predicate to decide whether it's a suitable example.
Both the conversion and the verification may be expensive, which makes the naive implementation such as we had for the
key searching problem inefficient.
In languages such as Java you can use `return` to stop the exploration of the collection once you've found your answer.
You would have an implementation which looks somewhat like this:
Then, the team manager could use the application to run complicated queries such as the following:
* Find an instance when the team worked more person-hours than X in a day.
* Find an example of a bug that took longer than Y days to fix.

It's common to run some kind of conversion on an element of the original data collection into a derivative entry that
better describes the problem domain.
Then, this converted entry is validated with a predicate to decide whether it's a suitable example.
Both the conversion and the verification may be expensive, which makes a naive implementation, like our
key search example, inefficient.
In languages such as Java, you can use `return` to stop the exploration of the collection once you've found your answer.
The implementation might look something like this:

```java
Bar complexConversion(Foo foo) {
Expand All @@ -37,14 +37,14 @@ Bar findFirstValidBar(Collection<Foo> foos) {
}
```

Here we enumerate the elements of the collection `foos` in order, running the `complexConversion` on them followed by
the `complexValidation`.
If we find the element for which `complexValidation(bar)` succeeds, than the converted entry is immediately returned
Here, we enumerate the elements of the collection `foos` sequentially, running `complexConversion` on them, followed by
`complexValidation`.
If we find the element for which `complexValidation(bar)` succeeds, the converted entry is immediately returned,
and the enumeration is stopped.
If there was no such element, then `null` is returned after all the elements of the collection are explored in vain.
If there was no such element, then `null` is returned after the entire collection has been explored without success.

How do we apply this pattern in Scala?
It's tempting to translate this code line-by-line in Scala:
It's tempting to translate this code line-by-line directly into Scala:

```scala 3
def complexConversion(foo: Foo): Bar = ...
Expand All @@ -59,16 +59,16 @@ def findFirstValidBar(seq: Seq[Foo]): Option[Bar] = {
}
```

We've replaced `null` with the more appropriate `None`, but otherwise the code stayed the same.
We've replaced `null` with the more appropriate `None`, but otherwise, the code remains the same.
However, this is not good Scala code, where the use of `return` is not idiomatic.
Since every block of code in Scala is an expression, the last expression within the block is what is returned.
You can write `x` instead of `return x` for the last expression, and it would have the same semantics.
Once `return` is used in the middle of a block, the programmer can no longer rely on that the last statement is the one
Once `return` is used in the middle of a block, the programmer can no longer rely on the last statement as the one
returning the result from the block.
This makes the code less readable, makes it harder to inline code and ruins referential transparency.
This makes the code less readable, makes it harder to inline code, and ruins referential transparency.
Thus, using `return` is considered a code smell and should be avoided.

In this module we'll explore more idiomatic ways to do early returns in Scala.
In this module, we'll explore more idiomatic ways to do early returns in Scala.



Expand Down
Loading