Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Early Returns/Baby Steps/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ If no valid user data has been found, we return `None` after traversing the enti

```scala 3
/**
* Imperative approach that uses un-idiomatic `return`.
* Imperative approach that uses unidiomatic `return`.
*/
def findFirstValidUser1(userIds: Seq[UserId]): Option[UserData] =
for userId <- userIds do
Expand Down Expand Up @@ -121,7 +121,7 @@ Consult the `breedCharacteristics` map for the appropriate fur characteristics f

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

Ensure that your search does not traverse the entire database.
Expand Down
2 changes: 1 addition & 1 deletion Early Returns/Breaking Boundaries/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ One important thing is that it ensures that the users never call `break` without
the code much safer.

The following snippet showcases the use of boundary/break in its simplest form.
If our conversion and validation work out then `break(Some(userData))` jumps out of the loop labeled with `boundary:`.
If our conversion and validation work out, then `break(Some(userData))` jumps out of the loop labeled with `boundary:`.
Since it's the end of the method, it immediately returns `Some(userData)`.

```scala 3
Expand Down
4 changes: 2 additions & 2 deletions Early Returns/Unapply/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ the `for` loop.

Unlike the imperative approach, the functional implementation separates the logic of conversion and validation
from the sequence traversal, which results in more readable code.
Taking care of possible missing records in the database amounts to modifying the unapply method, while the
Taking care of possible missing records in the database amounts to modifying the `unapply` method, while the
search function stays the same.

```scala 3
Expand All @@ -77,7 +77,7 @@ Imagine that there is a user who doesn't have an email.
In this case, `complexValidation` returns `false`, but the user might still be valid.
For example, it may be an account that belongs to a child of another user.
We don't need to message the child; instead, it's enough to reach out to their parent.
Even though this case is less common than the one we started with, we still need to keep it mind.
Even though this case is less common than the one we started with, we still need to keep it in mind.
To account for it, we can create a different extractor object with its own `unapply` and pattern match against it
if the first validation fails.
We do run the conversion twice in this case, but its impact is less significant due to the rarity of this scenario.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
## `Using` Clause for Carrying Immutable Context

When writing pure functions, we often end up carrying some immutable context, such as configurations, as extra arguments.
A common example is when a function expects a specific comparator of objects, such as in computing the maximum value or sorting:

Expand Down
4 changes: 2 additions & 2 deletions Functions as Data/filter/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ val blackCats = cats.filter { cat => cat.color == Black }

In the exercises, we will be working with a more detailed representation of cats than in the lessons.
Check out the `Cat` class in `src/Cat.scala`.
A cat has multiple characteristics: its name, breed, color, pattern, and a set of additional fur characteristics, such as
A cat has multiple characteristics: its name, breed, color pattern, and a set of additional fur characteristics, such as
`Fluffy` or `SleekHaired`.
Familiarize yourself with the corresponding definitions in other files in `src/`.

Expand All @@ -48,4 +48,4 @@ There are multiple cats available, and you wish to adopt a cat with one of the f
* The cat is fluffy.
* The cat is of the Abyssinian breed.

To simplify decision making, you first identify all the cats which possess at least one of the characteristics above. Your task is to implement the necessary functions and then apply the filter.
To simplify decision making, you first identify all the cats that possess at least one of the characteristics above. Your task is to implement the necessary functions and then apply the filter.
3 changes: 1 addition & 2 deletions Functions as Data/flatMap/task.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# `flatMap`
`def flatMap[B](f: A => IterableOnce[B]): Iterable[B]`

You can consider `flatMap` as a generalized version of the `map` method. The function `f`, used by `flatMap`, takes every element of the original collection and, instead of returning just one new element of a different (or the same) type, produces a whole new collection of new elements. These collections are then "flattened" by the `flatMap` method, resulting in just one large collection being returned.
Expand All @@ -7,7 +6,7 @@ Essentially, the same effect can be achieved with `map` followed by `flatten`. `

However, the applications of `flatMap` extend far beyond this simplistic use case. It's probably the most crucial method in functional programming in Scala. We will talk more about this in later chapters about monads and chains of execution. For now, let's focus on a straightforward example to demonstrate how exactly `flatMap` works.

Just as in the `map` example, we will use a list of four cats. But this time, for every cat, we will create a list of cars of different brands but the same color as the cat. Finally, we will use `flatMap to combine all four lists of cars of different brands and colors into one list.
Just as in the `map` example, we will use a list of four cats. But this time, for every cat, we will create a list of cars of different brands but the same color as the cat. Finally, we will use `flatMap` to combine all four lists of cars of different brands and colors into one list.

```scala
// We define the Color enum
Expand Down
2 changes: 1 addition & 1 deletion Functions as Data/foldLeft/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ We call the `foldLeft` method on the numbers range, stating that the accumulator
The second argument to `foldLeft` is a function that takes the current accumulator value (`acc`) and an element from the numbers range (`n`).
This function calls our `fizzBuzz` method with the number and appends the result to the accumulator list using the `:+` operator.

Once all the elements have been processed, `foldLeft returns the final accumulator value, which is the complete list of numbers and strings "Fizz", "Buzz", and "FizzBuzz", replacing the numbers that were divisible by 3, 5, and 15, respectively.
Once all the elements have been processed, `foldLeft` returns the final accumulator value, which is the complete list of numbers and strings "Fizz", "Buzz", and "FizzBuzz", replacing the numbers that were divisible by 3, 5, and 15, respectively.

Finally, we print out the results.

Expand Down
2 changes: 1 addition & 1 deletion Functions as Data/map/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The `map` method works on any Scala collection that implements `Iterable`.
It takes a function `f` and applies it to each element in the collection, similar to `foreach`. However, in the case of `map`, we are more interested in the results of `f` than its side effects.
As you can see from the declaration of `f`, it takes an element of the original collection of type `A` and returns a new element of type `B`.
Finally, the map method returns a new collection of elements of type `B`.
Finally, the `map` method returns a new collection of elements of type `B`.
In a special case, `B` can be the same as `A`. So, for example, we could use the `map` method to take a collection of cats of certain colors and create a new collection of cats of different colors.
But, we could also take a collection of cats and create a collection of cars with colors that match the colors of our cats.

Expand Down
2 changes: 1 addition & 1 deletion Functions as Data/passing_functions_as_arguments/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Then, we create a class `Cat`, which includes a value for the color of the cat.
Finally, we use the `filter` method and provide it with an anonymous function as an argument. This function takes an argument of the `Cat` class and returns `true` if the cat's color is black.
The `filter` method will apply this function to each cat in the original set and create a new set containing only those cats for which the function returns `true`.

However, our function that checks if the cat is black doesn't have to be anonymous. The `filter method will work just as well with a named function.
However, our function that checks if the cat is black doesn't have to be anonymous. The `filter` method will work just as well with a named function.

```scala
def isCatBlack(cat: Cat): Boolean = cat.color == Color.Black
Expand Down
4 changes: 1 addition & 3 deletions Immutability/A View/task.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
## A View

A view in Scala collections is a lazy rendition of a standard collection.
While a lazy list needs intentional construction, you can create a view from any "eager" Scala collection simply by calling `.view` on it.
A view computes its transformations (like map, filter, etc.) in a lazy manner,
meaning these operations are not immediately executed; instead, they are computed on the fly each time a new element is requested.
This can enhabce both performance and memory usage.
This can enhance both performance and memory usage.
On top of that, with a view, you can chain multiple operations without the need for intermediary collections —
the operations are applied to the elements of the original "eager" collection only when requested.
This can be particularly beneficial in scenarios where operations like map and filter are chained, so a significant number of
Expand Down
2 changes: 1 addition & 1 deletion Immutability/Berliner Pattern/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The application can be thought of as being divided into three layers:
but the good news is that there is no need to do so.
* The internal layer, where we connect to databases or write to files.
This part of the application is usually performance-critical, so it's only natural to use mutable data structures here.
* The middle layer, which connect the previous two.
* The middle layer, which connects the previous two.
This is where our business logic resides and where functional programming shines.

Pushing mutability to the thin inner and outer layers offers several benefits.
Expand Down
2 changes: 0 additions & 2 deletions Immutability/Lazy Val/task.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
## Lazy `val`

**Laziness** refers to the deferral of computation until it is necessary.
This strategy can enhance performance and allow programmers to work with infinite data structures, among other benefits.
With a lazy evaluation strategy, expressions are not evaluated when bound to a variable, but rather when used for the first time.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
In the imperative programming style, you will often find the following pattern: a variable is initially set to some
default value, such as an empty collection, an empty string, zero, or null.
Then, step-by-step, initialization code runs in a loop to create the proper value.
Then, step by step, initialization code runs in a loop to create the proper value.
Beyond this process, the value assigned to the variable does not change anymore — or if it does,
it’s done in a way that could be replaced by resetting the variable to its default value and rerunning the initialization.
However, the potential for modification remains, despite its redundancy.
Expand All @@ -9,7 +9,7 @@ Throughout the whole lifespan of the program, it hangs like a loose end of an el
Functional programming, on the other hand, allows us to build useful values without the need for initial default values or temporary mutability.
Even a highly complex data structure can be computed extensively using a higher-order function before being
assigned to a constant, thus preventing future modifications.
If we need an updated version, we can create a new data structure rather than modifying the old one.
If we need an updated version, we can create a new data structure instead of modifying the old one.

Scala provides a rich library of collections — `Array`, `List`, `Vector`, `Set`, `Map`, and many others —
and includes methods for manipulating these collections and their elements.
Expand Down
22 changes: 11 additions & 11 deletions Monads/Either as an Alternative to Exceptions/task.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
Sometimes you want to know a little more about the reason why a particular function failed.
Sometimes, you may need additional information to understand why a particular function failed.
This is why we have multiple types of exceptions: apart from sending a panic signal, we also explain what happened.
`Option` is not suitable to convey this information, and `Either[A, B]` is used instead.
An instance of `Either[A, B]` can only contain a value of type `A`, or a value of type `B`, but not simultaneously.
An instance of `Either[A, B]` can only contain a value of type `A` or a value of type `B`, but not simultaneously.
This is achieved by `Either` having two subclasses: `Left[A]` and `Right[B]`.
Every time there is a partial function `def partialFoo(...): B` that throws exceptions and returns type `B`, we can replace it with a total function `def totalFoo(...): Either[A, B]` where `A` describes the possible errors.
Whenever there is a partial function `def partialFoo(...): B` that throws exceptions and returns type `B`, we can replace it with a total function `def totalFoo(...): Either[A, B]` where `A` describes the possible errors.

Like `Option`, `Either` is a monad that means it allows chaining of succeeding computations.
The convention is that the failure is represented with `Left`, while `Right` wraps the value computed in the case of success.
Like `Option`, `Either` is a monad that allows chaining of succeeding computations.
The convention is that the failure is represented by `Left`, while `Right` wraps the value computed in the case of success.
Which subclass to use for which scenario is an arbitrary decision and everything would work the same way if we were to choose differently and reflect the choice in the implementation of `flatMap`.
However, a useful mnemonic is that `Right` is for cases when everything went *right*.
Thus, `identity` wraps the value in the `Right` constructor, and `flatMap` runs the second function only if the first function resulted in `Right`.
If an error happens and `Left` appears at any point, then the execution stops and that error is reported.
However, a useful mnemonic is that `Right` is for cases when everything goes *right*.
Thus, `identity` wraps the value in the `Right` constructor, and `flatMap` runs the second function only if the first function results in `Right`.
If an error occurs and `Left` appears at any point, then the execution stops and the error is reported.
Take a minute to write the implementations of the two methods on your own.

Consider a case where you read two numbers from the input stream and divide one by the other.
This function can fail in two ways: if the user provides a non-numeric input, or if a division-by-zero error occurs.
This function can fail in two ways: if the user provides a non-numeric input or if a division-by-zero error occurs.
We can implement this as a sequence of two functions:

```scala 3
Expand All @@ -40,7 +40,7 @@ Note that we have used `String` for errors here, but we could have used a custom
We could even create a whole hierarchy of errors if we wished to do so.
For example, we could make `Error` into a trait and then implement classes for IO errors, network errors, invalid state errors, and so on.
Another option is to use the standard Java hierarchy of exceptions, like in the following `safeDiv` implementation.
Note that no exception is actually thrown here, instead you can retrieve the kind of error by pattern matching on the result.
Note that no exception is actually thrown here; instead, you can retrieve the kind of error by pattern matching on the result.

```scala 3
def safeDiv(x: Double, y: Double): Either[Throwable, Double] =
Expand All @@ -60,7 +60,7 @@ There are three possible reasons why `getGrandchild` may fail:
To explain the failure to the caller, we created the `SearchError` enum and changed the types of the `findUser`, `getGrandchild`, `getGrandchildAge` functions to be `Either[SearchError, _]`.

Your task is to implement the functions providing the appropriate error message.
There is a helper function `getChild` to implement so that `getGrandchild` could use `flatMap`s naturally.
There is a helper function, `getChild`, to implement so that `getGrandchild` can use `flatMap`s naturally.



Loading