diff --git a/Early Returns/Baby Steps/task.md b/Early Returns/Baby Steps/task.md index 2e24e06..1d5a3e4 100644 --- a/Early Returns/Baby Steps/task.md +++ b/Early Returns/Baby Steps/task.md @@ -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 @@ -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. diff --git a/Early Returns/Breaking Boundaries/task.md b/Early Returns/Breaking Boundaries/task.md index a233158..1b99535 100644 --- a/Early Returns/Breaking Boundaries/task.md +++ b/Early Returns/Breaking Boundaries/task.md @@ -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 diff --git a/Early Returns/Unapply/task.md b/Early Returns/Unapply/task.md index 43dd14f..4280769 100644 --- a/Early Returns/Unapply/task.md +++ b/Early Returns/Unapply/task.md @@ -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 @@ -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. diff --git a/Expressions over Statements/Using Clause for Carrying an Immutable Context/task.md b/Expressions over Statements/Using Clause for Carrying an Immutable Context/task.md index 23b88e8..bf81614 100644 --- a/Expressions over Statements/Using Clause for Carrying an Immutable Context/task.md +++ b/Expressions over Statements/Using Clause for Carrying an Immutable Context/task.md @@ -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: diff --git a/Functions as Data/filter/task.md b/Functions as Data/filter/task.md index 8151d67..e6e87b7 100644 --- a/Functions as Data/filter/task.md +++ b/Functions as Data/filter/task.md @@ -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/`. @@ -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. diff --git a/Functions as Data/flatMap/task.md b/Functions as Data/flatMap/task.md index 73b63d4..fcad5a3 100644 --- a/Functions as Data/flatMap/task.md +++ b/Functions as Data/flatMap/task.md @@ -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. @@ -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 diff --git a/Functions as Data/foldLeft/task.md b/Functions as Data/foldLeft/task.md index f999a19..777ea08 100644 --- a/Functions as Data/foldLeft/task.md +++ b/Functions as Data/foldLeft/task.md @@ -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. diff --git a/Functions as Data/map/task.md b/Functions as Data/map/task.md index ff5898d..7cf9f2b 100644 --- a/Functions as Data/map/task.md +++ b/Functions as Data/map/task.md @@ -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. diff --git a/Functions as Data/passing_functions_as_arguments/task.md b/Functions as Data/passing_functions_as_arguments/task.md index ebf1d60..fb11d2c 100644 --- a/Functions as Data/passing_functions_as_arguments/task.md +++ b/Functions as Data/passing_functions_as_arguments/task.md @@ -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 diff --git a/Immutability/A View/task.md b/Immutability/A View/task.md index 6222567..631544b 100644 --- a/Immutability/A View/task.md +++ b/Immutability/A View/task.md @@ -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 diff --git a/Immutability/Berliner Pattern/task.md b/Immutability/Berliner Pattern/task.md index 194bb44..9aa8fbd 100644 --- a/Immutability/Berliner Pattern/task.md +++ b/Immutability/Berliner Pattern/task.md @@ -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. diff --git a/Immutability/Lazy Val/task.md b/Immutability/Lazy Val/task.md index 2014ce2..7ed5b7e 100644 --- a/Immutability/Lazy Val/task.md +++ b/Immutability/Lazy Val/task.md @@ -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. diff --git a/Immutability/Scala Collections instead of Imperative Loops/task.md b/Immutability/Scala Collections instead of Imperative Loops/task.md index a663910..5c279e6 100644 --- a/Immutability/Scala Collections instead of Imperative Loops/task.md +++ b/Immutability/Scala Collections instead of Imperative Loops/task.md @@ -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. @@ -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. diff --git a/Monads/Either as an Alternative to Exceptions/task.md b/Monads/Either as an Alternative to Exceptions/task.md index 482a726..a8deffc 100644 --- a/Monads/Either as an Alternative to Exceptions/task.md +++ b/Monads/Either as an Alternative to Exceptions/task.md @@ -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 @@ -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] = @@ -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. diff --git a/Monads/Monadic Laws/task.md b/Monads/Monadic Laws/task.md index 16e8a60..a169010 100644 --- a/Monads/Monadic Laws/task.md +++ b/Monads/Monadic Laws/task.md @@ -1,7 +1,7 @@ There are multiple monads not covered in this course. -Monad is an abstract concept, and any code that fulfills certain criteria can be viewed as one. -What are the criteria we are talking about, you may ask. -They are called monadic laws, namely left and right identity, and associativity. +A monad is an abstract concept, and any code that fulfills certain criteria can be viewed as one. +What are the criteria we are talking about, you may ask? +They are called monadic laws: left identity, right identity, and associativity. ## Identity Laws @@ -13,7 +13,7 @@ Consider subtraction, for which `x - 0 == x`, but `0 - x != x`. As it happens, the `identity` is supposed to be the identity of the `flatMap` method. Let's take a look at what it means exactly. -The left identity law says that if we create a monad from a value `v` with a `identity` method `Monad` and then `flatMap` a function `f` over it, it is equivalent to passing the value `v` straight to the function `f`: +The left identity law says that if we create a monad from a value `v` with an `identity` method `Monad` and then `flatMap` a function `f` over it, it is equivalent to passing the value `v` straight to the function `f`: ```scala 3 def f(value: V): Monad[V] @@ -31,12 +31,12 @@ monad.flatMap(Monad(_)) == monad ## Associativity -Associativity is a property that says that you can put parentheses in a whatever way in an expression and get the same result. +Associativity is a property that says you can put parentheses in any way in an expression and get the same result. For example, `(1 + 2) + (3 + 4)` is the same as `1 + (2 + 3) + 4` and `1 + 2 + 3 + 4`, since addition is associative. -At the same time, subtraction is not associative, and `(1 - 2) - (3 - 4)` is different from `1 - (2 - 3) - 4` and `1 - 2 - 3 - 4`. +However, subtraction is not associative, and `(1 - 2) - (3 - 4)` is different from `1 - (2 - 3) - 4` and `1 - 2 - 3 - 4`. -Associativity is desirable for `flatMap` because it means that we can unnest them and use for-comprehensions safely. -In particular, let's consider two monadic actions `mA` and `mB` followed by some running `doSomething` function over the resulting values. +Associativity is desirable for `flatMap` because it allows us to unnest them and use for-comprehensions safely. +In particular, let's consider two monadic actions `mA` and `mB`, followed by some function `doSomething` operating on the resulting values. This code fragment is equivalent to putting parentheses around the pipelined `mB` and `doSomething`. ```scala 3 @@ -48,7 +48,7 @@ mA.flatMap( a => ``` This can be refactored in the following form, using the `identity` of the corresponding monad. -Here we parenthesise the chaining of the two first monadic actions, and only then flatMap `doSomething` over the result. +Here we parenthesize the chaining of the first two monadic actions and only then `flatMap` `doSomething` over the result. ```scala 3 mA.flatMap { a => @@ -70,7 +70,7 @@ for { ## Do Option and Either Follow the Laws? -Now that we know what the rules are, we can check whether the monads we are familiar with play by them. +Now that we know what the rules are, we can check whether the monads we are familiar with adhere to them. The `identity` of `Option` is `{ x => Some(x) }`, while `flatMap` can be implemented in the following way. ```scala 3 @@ -85,7 +85,7 @@ The left identity law is straightforward: `Some(x).flatMap(f)` just runs `f(x)`. To prove the right identity, let's consider the two possibilities for `monad` in `monad.flatMap(Monad(_))`. The first is `None`, and `monad.flatMap(Option(_)) == None.flatMap(Option(_)) == None`. The second is `Some(x)` for some `x`. Then, `monad.flatMap(Option(_)) == Some(x).flatMap(Option(_)) == Some(x)`. -In both cases, we arrived to the value that is the save as the one we started with. +In both cases, we arrived to the value that is the same as the one we started with. Carefully considering the cases is how we prove associativity. @@ -112,15 +112,15 @@ Which is evaluated to: Some(x, y).flatMap { case (a, b) => doSomething(a, b) } ``` -Finally, we get `doSomething(x, y)` which is exactly what we wanted. +Finally, we get `doSomething(x, y)`, which is exactly what we wanted. If you want to make sure you grasp the concepts of monadic laws, go ahead and prove that `Either` is also a monad. ## Beyond Failure We only covered monads capable of describing failures and non-determinism. -There are many other *computational effects* that are expressed via monads. -They include logging, reading from a global memory, state manipulation, different flavours of non-determinism and many more. +There are many other *computational effects* that can be expressed via monads. +These include logging, reading from global memory, state manipulation, different flavors of non-determinism, and many more. We encourage you to explore these monads on your own. Once you feel comfortable with the basics, take a look at the [scalaz](https://scalaz.github.io/7/) and [cats](https://typelevel.org/cats/) libraries. diff --git a/Monads/Non-Determinism with Lists/task.md b/Monads/Non-Determinism with Lists/task.md index 386f1fa..55d3fbe 100644 --- a/Monads/Non-Determinism with Lists/task.md +++ b/Monads/Non-Determinism with Lists/task.md @@ -1,12 +1,12 @@ Monads can express different computational effects, and failure is just one of them. -Another is non-determinism, the ability of a program to have multiple possible results. +Another is non-determinism, which allows a program to have multiple possible results. One way to encapsulate different outcomes is by using a `List`. Consider a program that computes a factor of a number. -For non-prime numbers, there is at least one factor that is not either 1 or the number itself, and multiple factors exist for many numbers. +For non-prime numbers, there is at least one factor that is neither 1 nor the number itself, and many numbers have multiple factors. The question is: which of the factors should we return? -Of course, we can return a random factor, but a more functional way is to return all of them, packed in some collection such as a `List`. -In this case, the caller can decide on a proper treatment. +Of course, we could return a random factor, but a more functional way is to return all of them, packed in a collection such as a `List`. +In this case, the caller can decide on the appropriate treatment. ```scala // The non-deterministic function to compute all factors of a number @@ -19,13 +19,13 @@ def factors(n: Int): List[Int] = { Let's now discuss the List monad. The `identity` method simply creates a singleton list with its argument inside, indicating that the computation has finished with only one value. -`flatMap` applies the monadic action to each element in a list, and then concatenates the results. -If we run `factors(4).flatMap(factors)`, we get `List(1,2,4).flatMap(factors)` which concatenates `List(1)`, `List(1,2)`, and `List(1,2,4)` for the final result `List(1,1,2,1,2,4)`. +`flatMap` applies the monadic action to each element in a list and then concatenates the results. +If we run `factors(4).flatMap(factors)`, we get `List(1,2,4).flatMap(factors)`, which concatenates `List(1)`, `List(1,2)`, and `List(1,2,4)` for the final result `List(1,1,2,1,2,4)`. `List` is not the only collection that can describe non-determinism; another is `Set`. The difference between the two is that the latter doesn't care about repeats, while the former retains all of them. You can choose the suitable collection based on the problem at hand. -For `factors`, it may make sense to use `Set`, because we only care about unique factors. +For `factors`, it may make sense to use `Set` because we only care about unique factors. ```scala // The non-deterministic function to compute all factors of a number @@ -39,12 +39,12 @@ def factors(n: Int): Set[Int] = { ## Exercise -To make our model of users a little more realistic, we should take into an account that a user may have many children. +To make our model of users a little more realistic, we should take into an account that a user may have multiple children. This makes our `getGrandchild` function non-deterministic. -Let's reflect that in the names, types, and the implementations. +Let's reflect that in the names, types, and implementations. Now, function `getGrandchildren` aggregates all grandchildren of a particular user. Since each person is unique, we use `Set`. However, there might be some grandchildren whose ages are the same, and we don't want to lose this information. Because of that, `List` is used as the return type of the `getGrandchildrenAges` function. -Note that there is no need to explicitly report errors any longer, because an empty collection signifies the failure on its own. +Note that there is no need to explicitly report errors any longer because an empty collection signifies the failure on its own. diff --git a/Monads/Option as an Alternative to null/task.md b/Monads/Option as an Alternative to null/task.md index 589a518..2d6d543 100644 --- a/Monads/Option as an Alternative to null/task.md +++ b/Monads/Option as an Alternative to null/task.md @@ -6,28 +6,28 @@ Simplified, this method has the following type: `def flatMap[B](f: A => M[B]): M[B]` -It executes a monadic computation that yields some value of type `A`, and then applies the function `f` to this value, resulting in a new monadic computation. +It executes a monadic computation that yields some value of type `A` and then applies the function `f` to this value, resulting in a new monadic computation. This process enables sequential computations in a concise manner. In addition to this, there should be a way to create the simplest instance of a monad. -Many monad tutorials written for Scala call it `unit`, but it may be misleading due to existence of `Unit`, the class with only one instance. +Many monad tutorials written for Scala call it `unit`, but it may be misleading due to the existence of `Unit`, the class with only one instance. A better name for this method is `identity`, `pure` or `return`. We will be calling it `identity` for reasons that will become clear when we talk about monadic laws, a set of rules each monad should satisfy. -Its type is `def identity[A](x: A): M[A]`, meaning that it just wraps its argument into a monad, and in most cases it is just the `apply` methods of the corresponding class. +Its type is `def identity[A](x: A): M[A]`, meaning that it just wraps its argument into a monad, and in most cases, it is just the `apply` methods of the corresponding class. In this lesson, we'll consider our first monad that should already be familiar to you. -As you've probably already noticed, many real world functions are partial. -For example, when dividing by 0, you get an error, and it fully aligns with our view of the world. -To make division a total function, we can use `Double.Infinity` or `Double.NaN` but this is only valid for this narrow case. -More often, a `null` is returned from a partial function or, even worse, an exception is thrown. +As you've probably already noticed, many real-world functions are partial. +For example, when dividing by 0, you get an error, which fully aligns with our view of the world. +To make division a total function, we can use `Double.Infinity` or `Double.NaN`, but this is only valid for this narrow case. +More often, a `null` is returned from a partial function, or, even worse, an exception is thrown. Using `null` is called a billion-dollar mistake for a reason and should be avoided. -Throwing exceptions is the same as throwing your hands in the air and giving up trying to solve a problem, passing it to someone else instead. +Throwing exceptions is akin to throwing your hands in the air and giving up trying to solve a problem, passing it to someone else instead. These practices were once common, but now that better ways to handle failing computations have been developed, it's good to use them instead. -`Option[A]` is the simplest way to express a computation, which can fail. +`Option[A]` is the simplest way to express a computation that can fail. It has two subclasses: `None` and `Some[A]`. -The former corresponds to an absence of a result, or a failure, while the latter wraps a successful result. -A safe, total, division can be implemented as follows: +The former corresponds to an absence of a result or a failure, while the latter wraps a successful result. +A safe, total division can be implemented as follows: ```scala 3 def div(x: Double, y: Double): Option[Double] = @@ -37,8 +37,8 @@ def div(x: Double, y: Double): Option[Double] = Now, let's consider that you need to make a series of divisions in a chain. For example, you want to calculate how many visits your website gets per user per day. -You should first divide the total number of visits by number of users and then by number of days during which you collected the data. -This calculation can fail twice, and pattern matching each intermediate results gets boring quickly. +You should first divide the total number of visits by the number of users and then by the number of days during which you collected the data. +This calculation can fail twice, and pattern matching each intermediate result gets boring quickly. Instead, you can chain the operations using `flatMap`. If any of the divisions fail, then the whole chain stops. @@ -68,7 +68,7 @@ Option(result).foreach { res => } ``` -In short, `None` indicates that something went wrong, and `flatMap` allows to chain function calls which do not fail. +In short, `None` indicates that something went wrong, and `flatMap` allows chaining function calls that do not fail. ## Exercise @@ -76,9 +76,9 @@ Let's consider users who are represented with the `User` class. Each user has a name, an age, and, sometimes, a child. `UserService` represents a database of users along with some functions to search for them. -Your task is to implement `getGrandchild` which retrieve a grandchild of the user with the name given if the grandchild exists. -Here we've already put two calls to `flatMap` to chain some functions together, your task is to fill in what functions they are. +Your task is to implement `getGrandchild`, which retrieve a grandchild of the user with the given name if the grandchild exists. +Here we've already put two calls to `flatMap` to chain some functions together; your task is to fill in what functions they are. -Then implement `getGrandchildAge` which returns the age of the grandchild if they exist. +Then implement `getGrandchildAge`, which returns the age of the grandchild if they exist. Use `flatMap` here and avoid pattern matching. diff --git a/Monads/Syntactic Sugar and For-Comprehensions/task.md b/Monads/Syntactic Sugar and For-Comprehensions/task.md index 9849415..0839031 100644 --- a/Monads/Syntactic Sugar and For-Comprehensions/task.md +++ b/Monads/Syntactic Sugar and For-Comprehensions/task.md @@ -1,4 +1,4 @@ -In case of any monad, be it `Option`, `Either`, `Try`, or any other, it's possible to chain multiple functions together with `flatMap`. +In the case of any monad, be it `Option`, `Either`, `Try`, or any other, it's possible to chain multiple functions together with `flatMap`. We've seen many examples where a successfully computed result is passed straight to the next function: `foo(a).flatMap(bar).flatMap(baz)`. In many real-world situations, there is some additional logic that is executed in between calls. Consider the following realistic example: @@ -19,7 +19,7 @@ val res = client.getTeamMembers(teamId).flatMap { members => It doesn't look pretty, does it? There is a new nesting level for every call, and it's rather complicated to untangle the mess to understand what is happening. -Thankfully, Scala provides syntactic sugar called *for-comprehensions* reminiscent of the do-notation in Haskell. +Thankfully, Scala provides syntactic sugar called *for-comprehensions*, reminiscent of the do-notation in Haskell. The same code can be written more succinctly using `for/yield`: ```scala 3 @@ -40,11 +40,11 @@ We start by binding the successful results of retrieving team members with `memb Note that the first line in a for-comprehension must contain the left arrow. This is how Scala compiler understands what type the monadic action has. -After that, a message is logged and priority levels are fetched. -Note that we don't use the arrow to the left of the `log` function, because it's a regular function and not a monadic operation which is not chained with `flatMap` in the original piece of code. -We also don't care about the value returned by `log` and because of that use the underscore to the left of the equal sign. +After that, a message is logged, and priority levels are fetched. +Note that we don't use the arrow to the left of the `log` function because it's a regular function and not a monadic operation that is chained with `flatMap` in the original piece of code. +We also don't care about the value returned by `log`, and because of that, we use the underscore to the left of the equal sign. After all this is done, the `yield` block computes the final values to be returned. -If any line fails, the computation is aborted and the whole comprehension results in a failure. +If any line fails, the computation is aborted, and the whole comprehension results in a failure. ## Exercise diff --git a/Monads/Use Try Instead of try-catch/task.md b/Monads/Use Try Instead of try-catch/task.md index fe04b50..71570db 100644 --- a/Monads/Use Try Instead of try-catch/task.md +++ b/Monads/Use Try Instead of try-catch/task.md @@ -1,6 +1,6 @@ -When all code is in our control, it's easy to avoid throwing exceptions by using `Option` or `Either`. +When all code is under our control, it's easy to avoid throwing exceptions by using `Option` or `Either`. However, we often interact with Java libraries where exceptions are omnipresent, for example, in the context of working with databases, files, or internet services. -One option to bridge this gap is by using `try/catch` and converting exception code into monadic one: +One option to bridge this gap is by using `try/catch` and converting exception code into monadic code: ```scala 3 def foo(data: Data): Either[Throwable, Result] = @@ -13,7 +13,7 @@ def foo(data: Data): Either[Throwable, Result] = ``` This case is so common that Scala provides a special monad `Try[A]`. -`Try[A]` functions as a version of `Either[Throwable, A]` specially designed to handle failures coming from JVM. +`Try[A]` functions as a version of `Either[Throwable, A]` specially designed to handle failures coming from the JVM. You can think of this as a necessary evil: in the ideal world, there wouldn't be any exceptions, but since there is no such thing as the ideal world and exceptions are everywhere, we have `Try` to bridge the gap. Using `Try` simplifies the conversion significantly: @@ -26,9 +26,9 @@ def foo(data: Data): Try[Result] = The former wraps the result of the successful computation, while the latter signals failure by wrapping the exception thrown. Since `Try` is a monad, you can use `flatMap` to pipeline functions, and whenever any of them throws an exception, the computation is aborted. -Sometimes, an exception is not fatal and you know how to recover from it. -Here, you can use the `recover` or `recoverWith`. -The `recover` method takes a partial function that for some exceptions produces a value which is then wrapped in `Success`, while with all other exceptions result in `Failure`. +Sometimes, an exception is not fatal, and you know how to recover from it. +Here, you can use the `recover` or `recoverWith` methods. +The `recover` method takes a partial function that, for some exceptions, produces a value which is then wrapped in `Success`, while with all other exceptions, it results in `Failure`. A more flexible treatment is possible with the `recoverWith` method: its argument is a function that can decide on the appropriate way to react to particular errors. diff --git a/Pattern Matching/A Custom unapply Method/task.md b/Pattern Matching/A Custom unapply Method/task.md index cdb1ecf..85618b4 100644 --- a/Pattern Matching/A Custom unapply Method/task.md +++ b/Pattern Matching/A Custom unapply Method/task.md @@ -45,7 +45,7 @@ class Applicant(name: String, age: Int, vehicleType: VehicleType) Now, somewhere in our code, we have a sequence of all applicants, and we want to get the names of those who are eligible for a driver's license based on their age and the vehicle type they're applying to drive. Just as we did in the previous chapter when searching for cats older than one year, we could define a Universal Apply Method -and use guards within pattern matching. However instead of `foreach`, this time we will use `collect`: +and use guards within pattern matching. However, instead of `foreach`, this time we will use `collect`: ```scala 3 object Applicant: @@ -87,7 +87,7 @@ could prove valuable, depending on the situation. Given that each component in the RGB range can only be between `0` and `255`, it only uses 8 bits. The 4 components of the RGB representation fit neatly into a 32-bit integer, which allows for better memory usage. Many color operations can be performed directly using bitwise operations on this integer representation. -However, sometimes it's more convenient to access each components as a number, +However, sometimes it's more convenient to access each component as a number, and this is where the custom `unapply` method may come in handy. Implement the `unapply` method for the int-based RGB representation. diff --git a/Pattern Matching/The Newtype Pattern/task.md b/Pattern Matching/The Newtype Pattern/task.md index f71c2ab..3bde523 100644 --- a/Pattern Matching/The Newtype Pattern/task.md +++ b/Pattern Matching/The Newtype Pattern/task.md @@ -17,7 +17,7 @@ case class ProductId(value: Int) extends AnyVal These are called value classes in Scala. `AnyVal` is a special trait in Scala — when extended by a case class that has only a single field, you're telling the compiler that you want to use the newtype pattern. The compiler uses this information to catch any bugs, such as confusing integers used -for user IDs with yjose used for product IDs. However, at a later phase, it strips the type information from the data, +for user IDs with those used for product IDs. However, at a later phase, it strips the type information from the data, leaving only a bare `Int`, so that your code incurs no runtime overhead. Now, if you have a function that accepts a `UserId`, you can no longer mistakenly pass a `ProductId` to it: