From 20f48ec7db05b65299a29ced9dd649dbd18e0e25 Mon Sep 17 00:00:00 2001 From: Ekaterina Verbitskaia Date: Mon, 1 Apr 2024 20:02:43 +0200 Subject: [PATCH 01/65] Lessons for module 5 --- Early Returns/Baby Steps/build.sbt | 4 + Early Returns/Baby Steps/src/Task.scala | 3 + Early Returns/Baby Steps/task-info.yaml | 8 + Early Returns/Baby Steps/task.md | 118 ++++++++++++++ Early Returns/Baby Steps/test/TestSpec.scala | 8 + Early Returns/Breaking Boundaries/build.sbt | 4 + .../Breaking Boundaries/src/Task.scala | 16 ++ .../Breaking Boundaries/task-info.yaml | 8 + Early Returns/Breaking Boundaries/task.md | 29 ++++ .../Breaking Boundaries/test/TestSpec.scala | 8 + .../Lazy Collection to the Rescue/build.sbt | 4 + .../Lazy Collection to the Rescue/task.md | 25 +++ Early Returns/The Problem/build.sbt | 4 + Early Returns/The Problem/src/Main.scala | 5 + Early Returns/The Problem/task-info.yaml | 6 + Early Returns/The Problem/task.md | 42 ++--- Early Returns/Unapply/build.sbt | 4 + Early Returns/Unapply/src/Task.scala | 3 + Early Returns/Unapply/task-info.yaml | 8 + Early Returns/Unapply/task.md | 150 ++++++++++++++++++ Early Returns/Unapply/test/TestSpec.scala | 8 + Early Returns/lesson-info.yaml | 3 + course-info.yaml | 1 + 23 files changed, 448 insertions(+), 21 deletions(-) create mode 100644 Early Returns/Baby Steps/build.sbt create mode 100644 Early Returns/Baby Steps/src/Task.scala create mode 100644 Early Returns/Baby Steps/task-info.yaml create mode 100644 Early Returns/Baby Steps/task.md create mode 100644 Early Returns/Baby Steps/test/TestSpec.scala create mode 100644 Early Returns/Breaking Boundaries/build.sbt create mode 100644 Early Returns/Breaking Boundaries/src/Task.scala create mode 100644 Early Returns/Breaking Boundaries/task-info.yaml create mode 100644 Early Returns/Breaking Boundaries/task.md create mode 100644 Early Returns/Breaking Boundaries/test/TestSpec.scala create mode 100644 Early Returns/Lazy Collection to the Rescue/build.sbt create mode 100644 Early Returns/Lazy Collection to the Rescue/task.md create mode 100644 Early Returns/The Problem/build.sbt create mode 100644 Early Returns/The Problem/src/Main.scala create mode 100644 Early Returns/The Problem/task-info.yaml create mode 100644 Early Returns/Unapply/build.sbt create mode 100644 Early Returns/Unapply/src/Task.scala create mode 100644 Early Returns/Unapply/task-info.yaml create mode 100644 Early Returns/Unapply/task.md create mode 100644 Early Returns/Unapply/test/TestSpec.scala diff --git a/Early Returns/Baby Steps/build.sbt b/Early Returns/Baby Steps/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Early Returns/Baby Steps/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Early Returns/Baby Steps/src/Task.scala b/Early Returns/Baby Steps/src/Task.scala new file mode 100644 index 00000000..63d3f9a5 --- /dev/null +++ b/Early Returns/Baby Steps/src/Task.scala @@ -0,0 +1,3 @@ +class Task { + //put your task here +} \ No newline at end of file diff --git a/Early Returns/Baby Steps/task-info.yaml b/Early Returns/Baby Steps/task-info.yaml new file mode 100644 index 00000000..4ef9637a --- /dev/null +++ b/Early Returns/Baby Steps/task-info.yaml @@ -0,0 +1,8 @@ +type: edu +files: + - name: src/Task.scala + visible: true + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Early Returns/Baby Steps/task.md b/Early Returns/Baby Steps/task.md new file mode 100644 index 00000000..aff3e079 --- /dev/null +++ b/Early Returns/Baby Steps/task.md @@ -0,0 +1,118 @@ +## Baby Steps + +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. + +Now, imagine that many of those user entries are invalid in one way or the other. +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. + +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`. +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. + +```scala 3 +object EarlyReturns: + type UserId = Int + type Email = String + + case class UserData(id: UserId, name: String, email: Email) + + private val database = Seq( + UserData(1, "John Doe", "john@@gmail.com"), + UserData(2, "Jane Smith", "jane smith@yahoo.com"), + UserData(3, "Michael Brown", "michaeloutlook.com"), + UserData(4, "Emily Johnson", "emily at icloud.com"), + UserData(5, "Daniel Wilson", "daniel@hotmail.com"), + UserData(6, "Sophia Martinez", "sophia@aol.com"), + UserData(7, "Christopher Taylor", "christopher@mail.com"), + UserData(8, "Olivia Anderson", "olivia@live.com"), + UserData(9, "James Thomas", "james@protonmail.com"), + UserData(10, "Isabella Jackson", "isabella@gmail.com"), + UserData(11, "Alexander White", "alexander@yahoo.com") + ) + + private val identifiers = 1 to 11 + + /** + * This is our "complex conversion" method. + * We assume that it is costly to retrieve user data, so we want to avoid + * calling it unless it's absolutely necessary. + * + * This version of the method assumes that the user data always exists for a given user id. + * @param userId the identifier of a user for whom we want to retrieve the data + * @return the user data + */ + def complexConversion(userId: UserId): UserData = + database.find(_.id == userId).get + + /** + * Similar to `complexConversion`, the validation of user data is costly + * and we shouldn't do it too often. + * + * @param user user data + * @return true if the user data is valid, false otherwise + */ + def complexValidation(user: UserData): Boolean = + !user.email.contains(' ') && user.email.count(_ == '@') == 1 +``` + +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. + +```scala 3 + /** + * Imperative approach that uses un-idiomatic `return`. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser1(userIds: Seq[UserId]): Option[UserData] = + for userId <- userIds do + val userData = complexConversion(userId) + if (complexValidation(userData)) return Some(userData) + None +``` + +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`. + +```scala 3 + /** + * Naive functional approach: calls `complexConversion`` twice on the selected ID. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser2(userIds: Seq[UserId]): Option[UserData] = + userIds + .find(userId => complexValidation(complexConversion(userId))) + .map(complexConversion) +``` + +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. + +```scala 3 + /** + * A more concise implementation which uses `collectFirst`. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser3(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case userId if complexValidation(complexConversion(userId)) => complexConversion(userId) + } + +``` diff --git a/Early Returns/Baby Steps/test/TestSpec.scala b/Early Returns/Baby Steps/test/TestSpec.scala new file mode 100644 index 00000000..f73ac3d4 --- /dev/null +++ b/Early Returns/Baby Steps/test/TestSpec.scala @@ -0,0 +1,8 @@ +import org.scalatest.FunSuite + +class TestSpec extends FunSuite { + //TODO: implement your test here + test("First test") { + assert(false, "Tests not implemented for the task") + } +} diff --git a/Early Returns/Breaking Boundaries/build.sbt b/Early Returns/Breaking Boundaries/build.sbt new file mode 100644 index 00000000..3bbf5690 --- /dev/null +++ b/Early Returns/Breaking Boundaries/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.3.0" \ No newline at end of file diff --git a/Early Returns/Breaking Boundaries/src/Task.scala b/Early Returns/Breaking Boundaries/src/Task.scala new file mode 100644 index 00000000..c8c2a84c --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/Task.scala @@ -0,0 +1,16 @@ +import scala.util.boundary +import scala.util.boundary.break + +object EarlyReturns: + type UserId = Int + type Email = String + + case class UserData(id: UserId, name: String, email: Email) + def findFirstValidUser10(userIds: Seq[UserId]): Option[UserData] = + boundary: + for userId <- userIds do + safeComplexConversion(userId).foreach { userData => + if (complexValidation(userData)) break(Some(userData)) + } + None + diff --git a/Early Returns/Breaking Boundaries/task-info.yaml b/Early Returns/Breaking Boundaries/task-info.yaml new file mode 100644 index 00000000..4ef9637a --- /dev/null +++ b/Early Returns/Breaking Boundaries/task-info.yaml @@ -0,0 +1,8 @@ +type: edu +files: + - name: src/Task.scala + visible: true + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Early Returns/Breaking Boundaries/task.md b/Early Returns/Breaking Boundaries/task.md new file mode 100644 index 00000000..6f150ff9 --- /dev/null +++ b/Early Returns/Breaking Boundaries/task.md @@ -0,0 +1,29 @@ +## 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 +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 +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:`. +Since it's the end of the method, it immediately returns `Some(userData)`. + +```scala 3 + def findFirstValidUser10(userIds: Seq[UserId]): Option[UserData] = + boundary: + for userId <- userIds do + safeComplexConversion(userId).foreach { userData => + if (complexValidation(userData)) break(Some(userData)) + } + None +``` + +Sometimes there are multiple boundaries, in this case 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). diff --git a/Early Returns/Breaking Boundaries/test/TestSpec.scala b/Early Returns/Breaking Boundaries/test/TestSpec.scala new file mode 100644 index 00000000..f73ac3d4 --- /dev/null +++ b/Early Returns/Breaking Boundaries/test/TestSpec.scala @@ -0,0 +1,8 @@ +import org.scalatest.FunSuite + +class TestSpec extends FunSuite { + //TODO: implement your test here + test("First test") { + assert(false, "Tests not implemented for the task") + } +} diff --git a/Early Returns/Lazy Collection to the Rescue/build.sbt b/Early Returns/Lazy Collection to the Rescue/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Early Returns/Lazy Collection to the Rescue/task.md b/Early Returns/Lazy Collection to the Rescue/task.md new file mode 100644 index 00000000..de2ad9e0 --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/task.md @@ -0,0 +1,25 @@ +## 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. +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. + +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. +Try comparing the two approaches on your own. + +```scala 3 + def findFirstValidUser9(userIds: Seq[UserId]): Option[UserData] = + userIds + .iterator + .map(safeComplexConversion) + .find(_.exists(complexValidation)) + .flatten +``` + +### Exercise + +Implement the early return by using a lazy collection. \ No newline at end of file diff --git a/Early Returns/The Problem/build.sbt b/Early Returns/The Problem/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Early Returns/The Problem/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Early Returns/The Problem/src/Main.scala b/Early Returns/The Problem/src/Main.scala new file mode 100644 index 00000000..78b54a87 --- /dev/null +++ b/Early Returns/The Problem/src/Main.scala @@ -0,0 +1,5 @@ +object Main { + def main(args: Array[String]): Unit = { + // Write your solution here + } +} \ No newline at end of file diff --git a/Early Returns/The Problem/task-info.yaml b/Early Returns/The Problem/task-info.yaml new file mode 100644 index 00000000..92253a5d --- /dev/null +++ b/Early Returns/The Problem/task-info.yaml @@ -0,0 +1,6 @@ +type: theory +files: + - name: src/Main.scala + visible: true + - name: build.sbt + visible: false diff --git a/Early Returns/The Problem/task.md b/Early Returns/The Problem/task.md index 3940c451..8b21e4fe 100644 --- a/Early Returns/The Problem/task.md +++ b/Early Returns/The Problem/task.md @@ -1,23 +1,23 @@ -## The Problem +## 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 has been found. +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. -This problem may be more complicated if the 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 the more complex data is. +Consider an application designed to track the members of your team, detailing which 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. +* 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. +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. +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 something like this: +You would have an implementation which looks somewhat like this: ```java Bar complexConversion(Foo foo) { @@ -37,14 +37,14 @@ Bar findFirstValidBar(Collection 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 -and the enumeration is stopped. +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 +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. 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 in Scala: ```scala 3 def complexConversion(foo: Foo): Bar = ... @@ -63,10 +63,10 @@ We've replaced `null` with the more appropriate `None`, but otherwise the code s 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 that the last statement is the one returning the result from the block. -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. +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. diff --git a/Early Returns/Unapply/build.sbt b/Early Returns/Unapply/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Early Returns/Unapply/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Early Returns/Unapply/src/Task.scala b/Early Returns/Unapply/src/Task.scala new file mode 100644 index 00000000..63d3f9a5 --- /dev/null +++ b/Early Returns/Unapply/src/Task.scala @@ -0,0 +1,3 @@ +class Task { + //put your task here +} \ No newline at end of file diff --git a/Early Returns/Unapply/task-info.yaml b/Early Returns/Unapply/task-info.yaml new file mode 100644 index 00000000..4ef9637a --- /dev/null +++ b/Early Returns/Unapply/task-info.yaml @@ -0,0 +1,8 @@ +type: edu +files: + - name: src/Task.scala + visible: true + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Early Returns/Unapply/task.md b/Early Returns/Unapply/task.md new file mode 100644 index 00000000..c2a55350 --- /dev/null +++ b/Early Returns/Unapply/task.md @@ -0,0 +1,150 @@ +## Unapply + +Unapply methods form a basis of pattern matching. +Its goal is to extract data compacted in objects. +We can create a custom extractor object for user data validation with the suitable unapply method, for example: + +```scala 3 + object ValidUser: + def unapply(userId: UserId): Option[UserData] = + val userData = complexConversion(userId) + if complexValidation(userData) then Some(userData) else None +``` + +When we pattern match on `ValidUser`, its `unapply` method is called. +It runs the conversion and validation and only returns valid user data. +As a result, we get this short definition of our search function. + +```scala 3 + /** + * The custom `unapply` method runs conversion and validation and only returns valid user data. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser4(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser(user) => user + } +``` + +It's at this point that an observant reader is likely to protest. +This solution is twice as long as the imperative one we started with, and it doesn't seem to do anything extra! +One thing to notice here is that the imperative implementation is only concerned with the "happy" path. +What if there are no records in the database for some of the user identifiers? +The conversion function becomes partial, and, being true to the functional method, we need to return optional value: + +```scala 3 + /** + * This function takes into account that some IDs can be left out from the database + */ + def safeComplexConversion(userId: UserId): Option[UserData] = database.find(_.id == userId) +``` + +The partiality of the conversion will unavoidably complicate the imperative search function. +The code still has the same shape, but it has to go through additional hoops to accommodate partiality. +Note that every time a new complication arises in the business logic, it has to be reflected inside +the `for` loop. + +```scala 3 + /** + * Partiality of `safeComplexConversion` trickles into the search function. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser5(userIds: Seq[UserId]): Option[UserData] = + for userId <- userIds do + safeComplexConversion(userId) match + case Some(user) if complexValidation(user) => return Some(user) + case _ => + None +``` + +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 +search function stays the same. + +```scala 3 + /** + * This custom `unapply` method performs the safe conversion and then validation. + */ + object ValidUser6: + def unapply(userId: UserId): Option[UserData] = + safeComplexConversion(userId).find(complexValidation) + + def findFirstValidUser6(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser6(user) => user + } +``` + +In general, there might be several ways in which user data might be valid. +Imagine that there is a user who doesn't have an email. +In this case `complexValidation` returns `false`, but the user may 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. +To do it, we can create a different extractor object with its own `unapply` and pattern match against it +if the first validation failed. +We do run the conversion twice in this case, but it is less important because of how rare this case is. + +```scala 3 + object ValidUserInADifferentWay: + def otherValidation(userData: UserData): Boolean = /* check that it's a child user */ + def unapply(userId: UserId): Option[UserData] = safeComplexConversion(userId).find(otherValidation) + + def findFirstValidUser7(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser6(user) => user + case ValidUserInADifferentWay(user) => user + } +``` + +Both extractor objects work in the same way. +They run a conversion method, which may or may not succeed. +If conversion succeeds, its result is validated and returned when valid. +All this is done with the `unapply` method whose implementation stays the same regardless of the other methods. +This forms a nice framework which can be abstracted as a trait we call `Deconstruct`. +It has the `unapply` method which calls two abstract methods `convert` and `validate` that operate on generic +types `From` and `To`. + +```scala 3 + /** + * @tparam From The type we initially operate on + * @tparam To The type of the data we want to retrieve if it's valid + */ + trait Deconstruct[From, To]: + def convert(from: From): Option[To] + def validate(to: To): Boolean + def unapply(from: From): Option[To] = convert(from).find(validate) +``` + +In our case, the concrete implementation of the `Deconstruct` trait works on types `From` = `UserId` and +`To` = `UserData`. +It uses `safeComplexConversion` and `complexValidation` respectively. + +```scala 3 + object ValidUser8 extends Deconstruct[UserId, UserData]: + override def convert(userId: UserId): Option[UserData] = safeComplexConversion(userId) + override def validate(user: UserData): Boolean = complexValidation(user) +``` + +Finally, the search function stays the same, but now it uses the `unapply` method defined in +the `Deconstruct` trait while pattern matching: + +```scala 3 + def findFirstValidUser8(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser8(user) => user + } +``` + + + + + + + + + + diff --git a/Early Returns/Unapply/test/TestSpec.scala b/Early Returns/Unapply/test/TestSpec.scala new file mode 100644 index 00000000..f73ac3d4 --- /dev/null +++ b/Early Returns/Unapply/test/TestSpec.scala @@ -0,0 +1,8 @@ +import org.scalatest.FunSuite + +class TestSpec extends FunSuite { + //TODO: implement your test here + test("First test") { + assert(false, "Tests not implemented for the task") + } +} diff --git a/Early Returns/lesson-info.yaml b/Early Returns/lesson-info.yaml index 78c0e8e7..3d563e29 100644 --- a/Early Returns/lesson-info.yaml +++ b/Early Returns/lesson-info.yaml @@ -1,3 +1,6 @@ content: - The Problem + - Baby Steps + - Unapply - Lazy Collection to the Rescue + - Breaking Boundaries diff --git a/course-info.yaml b/course-info.yaml index 83b5dbb2..a2d213aa 100644 --- a/course-info.yaml +++ b/course-info.yaml @@ -17,6 +17,7 @@ content: - Pattern Matching - Immutability - Expressions over Statements + - Early Returns - Conclusion environment_settings: jvm_language_level: JDK_17 From 14607e38ee60701ef8db12f40a0dc70688dbdff3 Mon Sep 17 00:00:00 2001 From: Ekaterina Verbitskaia Date: Tue, 2 Apr 2024 14:44:44 +0200 Subject: [PATCH 02/65] Example code clean up --- .../Baby Steps/src/EarlyReturns.scala | 80 ++++++++++ Early Returns/Baby Steps/task-info.yaml | 2 + Early Returns/Baby Steps/task.md | 21 +-- .../Breaking Boundaries/src/Task.scala | 50 +++++- .../src/EarlyReturns.scala | 57 +++++++ .../src/Task.scala | 4 +- .../task-info.yaml | 2 + Early Returns/The Problem/src/Main.scala | 6 +- Early Returns/Unapply/src/EarlyReturns.scala | 148 ++++++++++++++++++ Early Returns/Unapply/task-info.yaml | 2 + Early Returns/Unapply/task.md | 4 - 11 files changed, 345 insertions(+), 31 deletions(-) create mode 100644 Early Returns/Baby Steps/src/EarlyReturns.scala create mode 100644 Early Returns/Lazy Collection to the Rescue/src/EarlyReturns.scala create mode 100644 Early Returns/Unapply/src/EarlyReturns.scala diff --git a/Early Returns/Baby Steps/src/EarlyReturns.scala b/Early Returns/Baby Steps/src/EarlyReturns.scala new file mode 100644 index 00000000..dcf5a2e7 --- /dev/null +++ b/Early Returns/Baby Steps/src/EarlyReturns.scala @@ -0,0 +1,80 @@ +object EarlyReturns: + private type UserId = Int + private type Email = String + + case class UserData(id: UserId, name: String, email: Email) + + /** + * Pretend database of user data. + */ + private val database = Seq( + UserData(1, "John Doe", "john@@gmail.com"), + UserData(2, "Jane Smith", "jane smith@yahoo.com"), + UserData(3, "Michael Brown", "michaeloutlook.com"), + UserData(4, "Emily Johnson", "emily at icloud.com"), + UserData(5, "Daniel Wilson", "daniel@hotmail.com"), + UserData(6, "Sophia Martinez", "sophia@aol.com"), + UserData(7, "Christopher Taylor", "christopher@mail.com"), + UserData(8, "Olivia Anderson", "olivia@live.com"), + UserData(9, "James Thomas", "james@protonmail.com"), + UserData(10, "Isabella Jackson", "isabella@gmail.com"), + UserData(11, "Alexander White", "alexander@yahoo.com") + ) + + private val identifiers = 1 to 11 + + /** + * This is our pretend "complex conversion" method. + * We assume that it is costly to retrieve user data, so we want to avoid + * calling it unless it's absolutely necessary. + * + * This version of the method assumes that the user data always exists for a given user id. + * + * @param userId the identifier of a user for whom we want to retrieve the data + * @return the user data + */ + def complexConversion(userId: UserId): UserData = + database.find(_.id == userId).get + + /** + * Similar to `complexConversion`, the validation of user data is costly + * and we shouldn't do it too often. + * + * @param user user data + * @return true if the user data is valid, false otherwise + */ + def complexValidation(user: UserData): Boolean = + !user.email.contains(' ') && user.email.count(_ == '@') == 1 + + /** + * Imperative approach that uses un-idiomatic `return`. + * + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser1(userIds: Seq[UserId]): Option[UserData] = + for userId <- userIds do + val userData = complexConversion(userId) + if (complexValidation(userData)) return Some(userData) + None + + /** + * Naive functional approach: calls `complexConversion`` twice on the selected ID. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser2(userIds: Seq[UserId]): Option[UserData] = + userIds + .find(userId => complexValidation(complexConversion(userId))) + .map(complexConversion) + + /** + * A more concise implementation which uses `collectFirst`. + * + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser3(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case userId if complexValidation(complexConversion(userId)) => complexConversion(userId) + } \ No newline at end of file diff --git a/Early Returns/Baby Steps/task-info.yaml b/Early Returns/Baby Steps/task-info.yaml index 4ef9637a..74b2af7d 100644 --- a/Early Returns/Baby Steps/task-info.yaml +++ b/Early Returns/Baby Steps/task-info.yaml @@ -6,3 +6,5 @@ files: visible: false - name: build.sbt visible: false + - name: src/EarlyReturns.scala + visible: true diff --git a/Early Returns/Baby Steps/task.md b/Early Returns/Baby Steps/task.md index aff3e079..db914848 100644 --- a/Early Returns/Baby Steps/task.md +++ b/Early Returns/Baby Steps/task.md @@ -46,19 +46,14 @@ object EarlyReturns: * We assume that it is costly to retrieve user data, so we want to avoid * calling it unless it's absolutely necessary. * - * This version of the method assumes that the user data always exists for a given user id. - * @param userId the identifier of a user for whom we want to retrieve the data - * @return the user data + * This version of the method assumes that the user data always exists for a given user id. */ def complexConversion(userId: UserId): UserData = database.find(_.id == userId).get /** * Similar to `complexConversion`, the validation of user data is costly - * and we shouldn't do it too often. - * - * @param user user data - * @return true if the user data is valid, false otherwise + * and we shouldn't do it too often. */ def complexValidation(user: UserData): Boolean = !user.email.contains(' ') && user.email.count(_ == '@') == 1 @@ -70,9 +65,7 @@ If no valid user data has been found, then we return None after going through th ```scala 3 /** - * Imperative approach that uses un-idiomatic `return`. - * @param userIds the sequence of all user identifiers - * @return `Some` of the first valid user data or `None` if no valid user data is found + * Imperative approach that uses un-idiomatic `return`. */ def findFirstValidUser1(userIds: Seq[UserId]): Option[UserData] = for userId <- userIds do @@ -90,9 +83,7 @@ of the `userData`. ```scala 3 /** - * Naive functional approach: calls `complexConversion`` twice on the selected ID. - * @param userIds the sequence of all user identifiers - * @return `Some` of the first valid user data or `None` if no valid user data is found + * Naive functional approach: calls `complexConversion` twice on the selected ID. */ def findFirstValidUser2(userIds: Seq[UserId]): Option[UserData] = userIds @@ -106,9 +97,7 @@ In the next lesson, we'll use a custom `unapply` method to get rid of the repeat ```scala 3 /** - * A more concise implementation which uses `collectFirst`. - * @param userIds the sequence of all user identifiers - * @return `Some` of the first valid user data or `None` if no valid user data is found + * A more concise implementation which uses `collectFirst`. */ def findFirstValidUser3(userIds: Seq[UserId]): Option[UserData] = userIds.collectFirst { diff --git a/Early Returns/Breaking Boundaries/src/Task.scala b/Early Returns/Breaking Boundaries/src/Task.scala index c8c2a84c..fc27c517 100644 --- a/Early Returns/Breaking Boundaries/src/Task.scala +++ b/Early Returns/Breaking Boundaries/src/Task.scala @@ -2,10 +2,55 @@ import scala.util.boundary import scala.util.boundary.break object EarlyReturns: - type UserId = Int - type Email = String + private type UserId = Int + private type Email = String case class UserData(id: UserId, name: String, email: Email) + + /** + * Pretend database of user data. + */ + private val database = Seq( + UserData(1, "John Doe", "john@@gmail.com"), + UserData(2, "Jane Smith", "jane smith@yahoo.com"), + UserData(3, "Michael Brown", "michaeloutlook.com"), + UserData(4, "Emily Johnson", "emily at icloud.com"), + UserData(5, "Daniel Wilson", "daniel@hotmail.com"), + UserData(6, "Sophia Martinez", "sophia@aol.com"), + UserData(7, "Christopher Taylor", "christopher@mail.com"), + UserData(8, "Olivia Anderson", "olivia@live.com"), + UserData(9, "James Thomas", "james@protonmail.com"), + UserData(10, "Isabella Jackson", "isabella@gmail.com"), + UserData(11, "Alexander White", "alexander@yahoo.com") + ) + + private val identifiers = 1 to 11 + + /** + * This is our "complex conversion" method. + * We assume that it is costly to retrieve user data, so we want to avoid + * calling it unless it's absolutely necessary. + * + * This function takes into account that some IDs can be left out from the database + */ + def safeComplexConversion(userId: UserId): Option[UserData] = database.find(_.id == userId) + + + /** + * Similar to `safeComplexConversion`, the validation of user data is costly + * and we shouldn't do it too often. + * + * @param user user data + * @return true if the user data is valid, false otherwise + */ + def complexValidation(user: UserData): Boolean = + !user.email.contains(' ') && user.email.count(_ == '@') == 1 + + /** + * Using `boundary` we create a computation context to which `break` returns the value. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ def findFirstValidUser10(userIds: Seq[UserId]): Option[UserData] = boundary: for userId <- userIds do @@ -13,4 +58,3 @@ object EarlyReturns: if (complexValidation(userData)) break(Some(userData)) } None - diff --git a/Early Returns/Lazy Collection to the Rescue/src/EarlyReturns.scala b/Early Returns/Lazy Collection to the Rescue/src/EarlyReturns.scala new file mode 100644 index 00000000..8ab5a07c --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/src/EarlyReturns.scala @@ -0,0 +1,57 @@ +object EarlyReturns: + private type UserId = Int + private type Email = String + + case class UserData(id: UserId, name: String, email: Email) + + /** + * Pretend database of user data. + */ + private val database = Seq( + UserData(1, "John Doe", "john@@gmail.com"), + UserData(2, "Jane Smith", "jane smith@yahoo.com"), + UserData(3, "Michael Brown", "michaeloutlook.com"), + UserData(4, "Emily Johnson", "emily at icloud.com"), + UserData(5, "Daniel Wilson", "daniel@hotmail.com"), + UserData(6, "Sophia Martinez", "sophia@aol.com"), + UserData(7, "Christopher Taylor", "christopher@mail.com"), + UserData(8, "Olivia Anderson", "olivia@live.com"), + UserData(9, "James Thomas", "james@protonmail.com"), + UserData(10, "Isabella Jackson", "isabella@gmail.com"), + UserData(11, "Alexander White", "alexander@yahoo.com") + ) + + private val identifiers = 1 to 11 + + /** + * This is our "complex conversion" method. + * We assume that it is costly to retrieve user data, so we want to avoid + * calling it unless it's absolutely necessary. + * + * This function takes into account that some IDs can be left out from the database + */ + def safeComplexConversion(userId: UserId): Option[UserData] = database.find(_.id == userId) + + + /** + * Similar to `safeComplexConversion`, the validation of user data is costly + * and we shouldn't do it too often. + * + * @param user user data + * @return true if the user data is valid, false otherwise + */ + def complexValidation(user: UserData): Boolean = + !user.email.contains(' ') && user.email.count(_ == '@') == 1 + + /** + * Using `iterator` creates a lazy collection that won't be evaluated + * after the first suitable user is found. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser9(userIds: Seq[UserId]): Option[UserData] = + userIds + .iterator + .map(safeComplexConversion) + .find(_.exists(complexValidation)) + .flatten \ No newline at end of file diff --git a/Early Returns/Lazy Collection to the Rescue/src/Task.scala b/Early Returns/Lazy Collection to the Rescue/src/Task.scala index 63d3f9a5..66d32cbc 100644 --- a/Early Returns/Lazy Collection to the Rescue/src/Task.scala +++ b/Early Returns/Lazy Collection to the Rescue/src/Task.scala @@ -1,3 +1 @@ -class Task { - //put your task here -} \ No newline at end of file +object Task \ No newline at end of file diff --git a/Early Returns/Lazy Collection to the Rescue/task-info.yaml b/Early Returns/Lazy Collection to the Rescue/task-info.yaml index 4ef9637a..74b2af7d 100644 --- a/Early Returns/Lazy Collection to the Rescue/task-info.yaml +++ b/Early Returns/Lazy Collection to the Rescue/task-info.yaml @@ -6,3 +6,5 @@ files: visible: false - name: build.sbt visible: false + - name: src/EarlyReturns.scala + visible: true diff --git a/Early Returns/The Problem/src/Main.scala b/Early Returns/The Problem/src/Main.scala index 78b54a87..8e1a993b 100644 --- a/Early Returns/The Problem/src/Main.scala +++ b/Early Returns/The Problem/src/Main.scala @@ -1,5 +1 @@ -object Main { - def main(args: Array[String]): Unit = { - // Write your solution here - } -} \ No newline at end of file +object Main \ No newline at end of file diff --git a/Early Returns/Unapply/src/EarlyReturns.scala b/Early Returns/Unapply/src/EarlyReturns.scala new file mode 100644 index 00000000..b7b50ea5 --- /dev/null +++ b/Early Returns/Unapply/src/EarlyReturns.scala @@ -0,0 +1,148 @@ +object EarlyReturns: + private type UserId = Int + private type Email = String + + case class UserData(id: UserId, name: String, email: Email) + + /** + * Pretend database of user data. + */ + private val database = Seq( + UserData(1, "John Doe", "john@@gmail.com"), + UserData(2, "Jane Smith", "jane smith@yahoo.com"), + UserData(3, "Michael Brown", "michaeloutlook.com"), + UserData(4, "Emily Johnson", "emily at icloud.com"), + UserData(5, "Daniel Wilson", "daniel@hotmail.com"), + UserData(6, "Sophia Martinez", "sophia@aol.com"), + UserData(7, "Christopher Taylor", "christopher@mail.com"), + UserData(8, "Olivia Anderson", "olivia@live.com"), + UserData(9, "James Thomas", "james@protonmail.com"), + UserData(10, "Isabella Jackson", "isabella@gmail.com"), + UserData(11, "Alexander White", "alexander@yahoo.com") + ) + + private val identifiers = 1 to 11 + + /** + * This is our "complex conversion" method. + * We assume that it is costly to retrieve user data, so we want to avoid + * calling it unless it's absolutely necessary. + * + * This version of the method assumes that the user data always exists for a given user id. + * + * @param userId the identifier of a user for whom we want to retrieve the data + * @return the user data + */ + def complexConversion(userId: UserId): UserData = + database.find(_.id == userId).get + + /** + * Similar to `complexConversion`, the validation of user data is costly + * and we shouldn't do it too often. + * + * @param user user data + * @return true if the user data is valid, false otherwise + */ + def complexValidation(user: UserData): Boolean = + !user.email.contains(' ') && user.email.count(_ == '@') == 1 + + object ValidUser: + def unapply(userId: UserId): Option[UserData] = + val userData = complexConversion(userId) + if complexValidation(userData) then Some(userData) else None + + /** + * The custom `unapply` method runs conversion and validation and only returns valid user data. + * + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser4(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser(user) => user + } + + /** + * This function takes into account that some IDs can be left out from the database + */ + def safeComplexConversion(userId: UserId): Option[UserData] = database.find(_.id == userId) + + /** + * Partiality of `safeComplexConversion` trickles into the search function. + * + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser5(userIds: Seq[UserId]): Option[UserData] = + for userId <- userIds do + safeComplexConversion(userId) match + case Some(user) if complexValidation(user) => return Some(user) + case _ => + None + + /** + * This custom `unapply` method performs the safe conversion and then validation. + */ + object ValidUser6: + def unapply(userId: UserId): Option[UserData] = + safeComplexConversion(userId).find(complexValidation) + + /** + * This custom `unapply` method performs the safe conversion and then validation. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser6(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser6(user) => user + } + + /** + * There might be multiple ways to validate the same user. + * Adding a new object with a different validation function unapply allows supporting both validations. + */ + object ValidUserInADifferentWay: + def otherValidation(userData: UserData): Boolean = false /* check that it's a child user */ + def unapply(userId: UserId): Option[UserData] = safeComplexConversion(userId).find(otherValidation) + + /** + * The two possible ways to validate a user are used one after another in this method. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser7(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser6(user) => user + case ValidUserInADifferentWay(user) => user + } + + /** + * This trait neatly abstracts the described process. + * Now the programmer only need to supply the `conversion` and `validation` methods, while `unapply` is standard. + * @tparam From The type we initially operate on + * @tparam To The type of the data we want to retrieve if it's valid + */ + trait Deconstruct[From, To]: + def convert(from: From): Option[To] + + def validate(to: To): Boolean + + def unapply(from: From): Option[To] = convert(from).find(validate) + + /** + * By extending `Deconstruct` we don't need to bother with `unapply`. + */ + object ValidUser8 extends Deconstruct[UserId, UserData]: + override def convert(userId: UserId): Option[UserData] = safeComplexConversion(userId) + + override def validate(user: UserData): Boolean = complexValidation(user) + + /** + * The `unapply` method works as it did before. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser8(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser8(user) => user + } \ No newline at end of file diff --git a/Early Returns/Unapply/task-info.yaml b/Early Returns/Unapply/task-info.yaml index 4ef9637a..74b2af7d 100644 --- a/Early Returns/Unapply/task-info.yaml +++ b/Early Returns/Unapply/task-info.yaml @@ -6,3 +6,5 @@ files: visible: false - name: build.sbt visible: false + - name: src/EarlyReturns.scala + visible: true diff --git a/Early Returns/Unapply/task.md b/Early Returns/Unapply/task.md index c2a55350..b3019c3e 100644 --- a/Early Returns/Unapply/task.md +++ b/Early Returns/Unapply/task.md @@ -18,8 +18,6 @@ As a result, we get this short definition of our search function. ```scala 3 /** * The custom `unapply` method runs conversion and validation and only returns valid user data. - * @param userIds the sequence of all user identifiers - * @return `Some` of the first valid user data or `None` if no valid user data is found */ def findFirstValidUser4(userIds: Seq[UserId]): Option[UserData] = userIds.collectFirst { @@ -48,8 +46,6 @@ the `for` loop. ```scala 3 /** * Partiality of `safeComplexConversion` trickles into the search function. - * @param userIds the sequence of all user identifiers - * @return `Some` of the first valid user data or `None` if no valid user data is found */ def findFirstValidUser5(userIds: Seq[UserId]): Option[UserData] = for userId <- userIds do From 9a082ab4dd36a9f3829124e034fdbb94aaaecfbe Mon Sep 17 00:00:00 2001 From: Ekaterina Verbitskaia Date: Tue, 16 Apr 2024 23:32:00 +0200 Subject: [PATCH 03/65] Module 5 finished --- Early Returns/Baby Steps/src/Breed.scala | 13 ++ Early Returns/Baby Steps/src/Cat.scala | 5 + Early Returns/Baby Steps/src/Color.scala | 11 ++ Early Returns/Baby Steps/src/Database.scala | 92 ++++++++++++++ .../Baby Steps/src/EarlyReturns.scala | 22 ++-- .../Baby Steps/src/FurCharacteristic.scala | 8 ++ Early Returns/Baby Steps/src/Pattern.scala | 28 +++++ Early Returns/Baby Steps/src/Task.scala | 80 +++++++++++- Early Returns/Baby Steps/task-info.yaml | 28 +++++ Early Returns/Baby Steps/task.md | 23 ++++ Early Returns/Baby Steps/test/TestSpec.scala | 38 +++++- .../Breaking Boundaries/src/Breed.scala | 13 ++ .../Breaking Boundaries/src/Cat.scala | 5 + .../Breaking Boundaries/src/Color.scala | 11 ++ .../Breaking Boundaries/src/Database.scala | 92 ++++++++++++++ .../src/EarlyReturns.scala | 60 +++++++++ .../src/FurCharacteristic.scala | 8 ++ .../Breaking Boundaries/src/Pattern.scala | 28 +++++ .../Breaking Boundaries/src/Task.scala | 87 ++++++------- .../Breaking Boundaries/task-info.yaml | 18 ++- Early Returns/Breaking Boundaries/task.md | 9 ++ .../Breaking Boundaries/test/TestSpec.scala | 29 ++++- .../src/Breed.scala | 13 ++ .../src/Cat.scala | 5 + .../src/Color.scala | 11 ++ .../src/Database.scala | 92 ++++++++++++++ .../src/EarlyReturns.scala | 26 ++-- .../src/FurCharacteristic.scala | 8 ++ .../src/Pattern.scala | 28 +++++ .../src/Task.scala | 58 ++++++++- .../task-info.yaml | 26 +++- .../Lazy Collection to the Rescue/task.md | 5 +- .../test/TestSpec.scala | 29 ++++- Early Returns/Unapply/src/Breed.scala | 13 ++ Early Returns/Unapply/src/Cat.scala | 5 + Early Returns/Unapply/src/Color.scala | 11 ++ Early Returns/Unapply/src/Database.scala | 92 ++++++++++++++ Early Returns/Unapply/src/EarlyReturns.scala | 22 ++-- .../Unapply/src/FurCharacteristic.scala | 8 ++ Early Returns/Unapply/src/Pattern.scala | 28 +++++ Early Returns/Unapply/src/Task.scala | 118 +++++++++++++++++- Early Returns/Unapply/task-info.yaml | 41 +++++- Early Returns/Unapply/task.md | 16 ++- Early Returns/Unapply/test/TestSpec.scala | 33 ++++- 44 files changed, 1279 insertions(+), 117 deletions(-) create mode 100644 Early Returns/Baby Steps/src/Breed.scala create mode 100644 Early Returns/Baby Steps/src/Cat.scala create mode 100644 Early Returns/Baby Steps/src/Color.scala create mode 100644 Early Returns/Baby Steps/src/Database.scala create mode 100644 Early Returns/Baby Steps/src/FurCharacteristic.scala create mode 100644 Early Returns/Baby Steps/src/Pattern.scala create mode 100644 Early Returns/Breaking Boundaries/src/Breed.scala create mode 100644 Early Returns/Breaking Boundaries/src/Cat.scala create mode 100644 Early Returns/Breaking Boundaries/src/Color.scala create mode 100644 Early Returns/Breaking Boundaries/src/Database.scala create mode 100644 Early Returns/Breaking Boundaries/src/EarlyReturns.scala create mode 100644 Early Returns/Breaking Boundaries/src/FurCharacteristic.scala create mode 100644 Early Returns/Breaking Boundaries/src/Pattern.scala create mode 100644 Early Returns/Lazy Collection to the Rescue/src/Breed.scala create mode 100644 Early Returns/Lazy Collection to the Rescue/src/Cat.scala create mode 100644 Early Returns/Lazy Collection to the Rescue/src/Color.scala create mode 100644 Early Returns/Lazy Collection to the Rescue/src/Database.scala create mode 100644 Early Returns/Lazy Collection to the Rescue/src/FurCharacteristic.scala create mode 100644 Early Returns/Lazy Collection to the Rescue/src/Pattern.scala create mode 100644 Early Returns/Unapply/src/Breed.scala create mode 100644 Early Returns/Unapply/src/Cat.scala create mode 100644 Early Returns/Unapply/src/Color.scala create mode 100644 Early Returns/Unapply/src/Database.scala create mode 100644 Early Returns/Unapply/src/FurCharacteristic.scala create mode 100644 Early Returns/Unapply/src/Pattern.scala diff --git a/Early Returns/Baby Steps/src/Breed.scala b/Early Returns/Baby Steps/src/Breed.scala new file mode 100644 index 00000000..8e19f8f5 --- /dev/null +++ b/Early Returns/Baby Steps/src/Breed.scala @@ -0,0 +1,13 @@ +enum Breed: + case Siamese + case Persian + case MaineCoon + case Ragdoll + case Bengal + case Abyssinian + case Birman + case OrientalShorthair + case Sphynx + case DevonRex + case ScottishFold + case Metis \ No newline at end of file diff --git a/Early Returns/Baby Steps/src/Cat.scala b/Early Returns/Baby Steps/src/Cat.scala new file mode 100644 index 00000000..b90e29cc --- /dev/null +++ b/Early Returns/Baby Steps/src/Cat.scala @@ -0,0 +1,5 @@ +case class Cat(name: String, + breed: Breed, + primaryColor: Color, + pattern: Pattern, + furCharacteristics: Set[FurCharacteristic]) diff --git a/Early Returns/Baby Steps/src/Color.scala b/Early Returns/Baby Steps/src/Color.scala new file mode 100644 index 00000000..b860b8ab --- /dev/null +++ b/Early Returns/Baby Steps/src/Color.scala @@ -0,0 +1,11 @@ +enum Color: + case Lavender + case White + case Cream + case Fawn + case Cinnamon + case Chocolate + case Orange + case Lilac + case Blue + case Black \ No newline at end of file diff --git a/Early Returns/Baby Steps/src/Database.scala b/Early Returns/Baby Steps/src/Database.scala new file mode 100644 index 00000000..2989ffe0 --- /dev/null +++ b/Early Returns/Baby Steps/src/Database.scala @@ -0,0 +1,92 @@ +import Breed._ +import Color._ +import FurCharacteristic._ +import Pattern._ +import TabbySubtype._ +import ShadingSubtype._ +import BicolorSubtype._ +import TricolorSubtype._ + +object Database: + type CatId = Int + + /** + * @param id a unique cat identifier + * @param name a cat's name + * @param adopted a flag that is true if the cat has been adopted + */ + case class CatAdoptionStatus(id: CatId, name: String, adopted: Boolean) + + val identifiers: Seq[CatId] = 1 to 30 + + /** + * This database "table" tracks whether a cat has already been adopted. + */ + val adoptionStatusDatabase: Seq[CatAdoptionStatus] = Seq( + CatAdoptionStatus(1, "Luna", true), + CatAdoptionStatus(2, "Max", false), + CatAdoptionStatus(3, "Charlie", true), + CatAdoptionStatus(4, "Daisy", false), + CatAdoptionStatus(5, "Simba", true), + CatAdoptionStatus(6, "Oliver", false), + CatAdoptionStatus(7, "Molly", true), + CatAdoptionStatus(8, "Lucy", true), + CatAdoptionStatus(9, "Buddy", false), + CatAdoptionStatus(10, "Rocky", true), + CatAdoptionStatus(11, "Jack", false), + CatAdoptionStatus(12, "Sadie", true), + CatAdoptionStatus(13, "Ginger", false), + CatAdoptionStatus(14, "Leo", true), + CatAdoptionStatus(15, "Misty", false), + CatAdoptionStatus(16, "Rex", true), + CatAdoptionStatus(17, "Bella", true), + CatAdoptionStatus(18, "Tiger", true), + CatAdoptionStatus(19, "Zara", false), + CatAdoptionStatus(20, "Sophie", true), + CatAdoptionStatus(21, "Ollie", false), + CatAdoptionStatus(22, "Pixie", true), + CatAdoptionStatus(23, "Fuzz", false), + CatAdoptionStatus(24, "Scotty", true), + CatAdoptionStatus(25, "Mixie", true), + CatAdoptionStatus(26, "Cleo", true), + CatAdoptionStatus(27, "Milo", false), + CatAdoptionStatus(28, "Nala", true), + CatAdoptionStatus(29, "Loki", false), + CatAdoptionStatus(30, "Shadow", true) + ) + + /** + * This database "table" contains the basic information about each cat. + */ + val catDatabase: Seq[Cat] = Seq( + Cat("Luna", Siamese, Blue, Pattern.SolidColor, Set(Fluffy)), // Invalid + Cat("Max", Persian, Black, Pattern.Tabby(Mackerel), Set(ShortHaired)), // Invalid + Cat("Charlie", MaineCoon, Orange, Pattern.SolidColor, Set(SleekHaired)), // Invalid + Cat("Daisy", Ragdoll, Cream, Pattern.Bicolor(Van), Set(ShortHaired)), // Invalid + Cat("Simba", Bengal, Blue, Pattern.SolidColor, Set(LongHaired)), // Invalid + Cat("Oliver", ScottishFold, Cinnamon, Pattern.Spots, Set(WireHaired)), // Invalid + Cat("Molly", Persian, White, Pattern.Shading(Shaded), Set(Fluffy, DoubleCoated)), // Valid + Cat("Lucy", Metis, Blue, Pattern.Pointed, Set(SleekHaired, Fluffy)), // Metis can be anything + Cat("Buddy", Siamese, Cream, Pattern.Tabby(Mackerel), Set(LongHaired)), // Invalid + Cat("Rocky", MaineCoon, Black, Pattern.Tricolor(Tortie), Set(ShortHaired)), // Invalid + Cat("Jack", Bengal, Chocolate, Pattern.Tabby(Spotted), Set(ShortHaired, Plush)), // Invalid + Cat("Sadie", Birman, White, Pattern.Bicolor(Van), Set(LongHaired, Fluffy)), // Valid + Cat("Ginger", Abyssinian, Orange, Pattern.Tabby(Ticked), Set(ShortHaired, SleekHaired)), // Valid + Cat("Leo", Siamese, Cream, Pattern.SolidColor, Set(ShortHaired, SleekHaired)), // Valid + Cat("Misty", Persian, White, Pattern.SolidColor, Set(LongHaired, Fluffy)), // Valid + Cat("Rex", MaineCoon, Black, Pattern.Tabby(Classic), Set(LongHaired, DoubleCoated)), // Valid + Cat("Bella", Ragdoll, Blue, Pattern.Pointed, Set(Fluffy)), // Valid + Cat("Tiger", Bengal, Orange, Pattern.Spots, Set(ShortHaired)), // Valid + Cat("Zara", Abyssinian, Cinnamon, Pattern.Tabby(Ticked), Set(ShortHaired)), // Valid + Cat("Sophie", Birman, Lilac, Pattern.Pointed, Set(LongHaired, Fluffy)), // Valid + Cat("Ollie", OrientalShorthair, Lavender, Pattern.SolidColor, Set(SleekHaired)), // Valid + Cat("Pixie", Sphynx, Lilac, Pattern.SolidColor, Set(WireHaired)), // Valid + Cat("Fuzz", DevonRex, Cream, Pattern.SolidColor, Set(ShortHaired, WireHaired)), // Valid + Cat("Scotty", ScottishFold, White, Pattern.Bicolor(Tuxedo), Set(ShortHaired)), // Valid + Cat("Mixie", Metis, Chocolate, Pattern.Tricolor(Calico), Set(DoubleCoated)), // Metis can be anything + Cat("Cleo", Abyssinian, Fawn, Pattern.Tricolor(Tortie), Set(Fluffy)), // Invalid + Cat("Milo", Birman, Lavender, Pattern.SolidColor, Set(SleekHaired)), // Invalid + Cat("Nala", OrientalShorthair, Black, Pattern.Shading(Chinchilla), Set(Fluffy)), // Invalid + Cat("Loki", Sphynx, White, Pattern.Shading(Shaded), Set(LongHaired)), // Invalid + Cat("Shadow", DevonRex, Lilac, Pattern.SolidColor, Set(LongHaired)) // Invalid + ) \ No newline at end of file diff --git a/Early Returns/Baby Steps/src/EarlyReturns.scala b/Early Returns/Baby Steps/src/EarlyReturns.scala index dcf5a2e7..8a5b151b 100644 --- a/Early Returns/Baby Steps/src/EarlyReturns.scala +++ b/Early Returns/Baby Steps/src/EarlyReturns.scala @@ -8,17 +8,17 @@ object EarlyReturns: * Pretend database of user data. */ private val database = Seq( - UserData(1, "John Doe", "john@@gmail.com"), - UserData(2, "Jane Smith", "jane smith@yahoo.com"), - UserData(3, "Michael Brown", "michaeloutlook.com"), - UserData(4, "Emily Johnson", "emily at icloud.com"), - UserData(5, "Daniel Wilson", "daniel@hotmail.com"), - UserData(6, "Sophia Martinez", "sophia@aol.com"), - UserData(7, "Christopher Taylor", "christopher@mail.com"), - UserData(8, "Olivia Anderson", "olivia@live.com"), - UserData(9, "James Thomas", "james@protonmail.com"), - UserData(10, "Isabella Jackson", "isabella@gmail.com"), - UserData(11, "Alexander White", "alexander@yahoo.com") + UserData(1, "Ayaan Sharma", "ayaan@gmail.com"), + UserData(2, "Lei Zhang", "lei_zhang@yahoo.cn"), + UserData(3, "Fatima Al-Fassi", "fatima.alfassi@outlook.sa"), + UserData(4, "Ana Sofia Ruiz", "ana_sofia@icloud.es"), + UserData(5, "Oluwaseun Adeyemi", "oluwaseun@hotmail.ng"), + UserData(6, "Maria Ivanova", "maria.ivanova@aol.ru"), + UserData(7, "Yuto Nakamura", "yuto.nakamura@mail.jp"), + UserData(8, "Chiara Rossi", "chiara@live.it"), + UserData(9, "Lucas Müller", "lucas@protonmail.de"), + UserData(10, "Sara Al-Bahrani", "sara.albahrani@gmail.com"), + UserData(11, "Min-Jun Kim", "minjun.kim@yahoo.kr") ) private val identifiers = 1 to 11 diff --git a/Early Returns/Baby Steps/src/FurCharacteristic.scala b/Early Returns/Baby Steps/src/FurCharacteristic.scala new file mode 100644 index 00000000..ae243fa8 --- /dev/null +++ b/Early Returns/Baby Steps/src/FurCharacteristic.scala @@ -0,0 +1,8 @@ +enum FurCharacteristic: + case LongHaired + case ShortHaired + case Fluffy + case Plush + case SleekHaired + case WireHaired + case DoubleCoated \ No newline at end of file diff --git a/Early Returns/Baby Steps/src/Pattern.scala b/Early Returns/Baby Steps/src/Pattern.scala new file mode 100644 index 00000000..fc9b0651 --- /dev/null +++ b/Early Returns/Baby Steps/src/Pattern.scala @@ -0,0 +1,28 @@ +enum TabbySubtype: + case Classic + case Ticked + case Mackerel + case Spotted + case Patched + +enum ShadingSubtype: + case Chinchilla + case Shaded + case Smoke + +enum BicolorSubtype: + case Tuxedo + case Van + +enum TricolorSubtype: + case Calico + case Tortie + +enum Pattern: + case Tabby(val subType: TabbySubtype) + case Pointed + case Shading(val subType: ShadingSubtype) + case SolidColor + case Bicolor(val subType: BicolorSubtype) + case Tricolor(val subType: TricolorSubtype) + case Spots diff --git a/Early Returns/Baby Steps/src/Task.scala b/Early Returns/Baby Steps/src/Task.scala index 63d3f9a5..8d76587e 100644 --- a/Early Returns/Baby Steps/src/Task.scala +++ b/Early Returns/Baby Steps/src/Task.scala @@ -1,3 +1,77 @@ -class Task { - //put your task here -} \ No newline at end of file +import Database._ +import Breed._ +import FurCharacteristic._ + +object Task: + /** + * Implement the conversion method: find a cat's name by its ID in the adoptionStatusDatabase, + * and then fetch the cat data from the catsDatabase. + * + * @param catId the identifier of a cat for whom we want to retrieve the data + * @return the cat data + */ + def catConversion(catId: CatId): Cat = + print(s"Cat conversion: $catId\n") + val status = adoptionStatusDatabase.find(_.id == catId).get + catDatabase.find(_.name == status.name).get + + private val breedCharacteristics: Map[Breed, Set[FurCharacteristic]] = Map( + Siamese -> Set(ShortHaired, SleekHaired), + Persian -> Set(LongHaired, Fluffy, DoubleCoated), + MaineCoon -> Set(LongHaired, Fluffy, DoubleCoated), + Ragdoll -> Set(LongHaired, Fluffy, DoubleCoated), + Bengal -> Set(ShortHaired), + Abyssinian -> Set(ShortHaired, SleekHaired), + Birman -> Set(LongHaired, Fluffy, DoubleCoated), + OrientalShorthair -> Set(ShortHaired, SleekHaired), + Sphynx -> Set(WireHaired, Plush), + DevonRex -> Set(ShortHaired, Plush, WireHaired), + ScottishFold -> Set(ShortHaired, LongHaired, DoubleCoated), + Metis -> Set(LongHaired, ShortHaired, Fluffy, Plush, SleekHaired, WireHaired, DoubleCoated) // Assuming Metis can have any characteristics + ) + + /** + * Implement the validation: the characteristics of the cat's fur should feed their breed. + * + * @param cat cat data + * @return true if the user data is valid, false otherwise + */ + def furCharacteristicValidation(cat: Cat): Boolean = + print(s"Validation of fur characteristics: ${cat.name}\n") + val validCharacteristics = breedCharacteristics(cat.breed) + cat.furCharacteristics.forall(validCharacteristics.contains) + + + /** + * Imperative approach that uses un-idiomatic `return`. + * + * @param catIds the sequence of all cat identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def imperativeFindFirstValidCat(catIds: Seq[CatId]): Option[Cat] = + for catId <- catIds do + val catData = catConversion(catId) + if (furCharacteristicValidation(catData)) return Some(catData) + None + + /** + * Implement the naive functional approach. + * + * @param catIds the sequence of all cat identifiers + * @return `Some` of the first valid user data or `None` if no valid cat data is found + */ + def functionalFindFirstValidCat(catIds: Seq[CatId]): Option[Cat] = + catIds + .find(catId => furCharacteristicValidation(catConversion(catId))) + .map(catConversion) + + /** + * Use `collectFirst` here. + * + * @param catIds the sequence of all cat identifiers + * @return `Some` of the first valid cat data or `None` if no valid cat data is found + */ + def collectFirstFindFirstValidCat(userIds: Seq[CatId]): Option[Cat] = + userIds.collectFirst { + case catId if furCharacteristicValidation(catConversion(catId)) => catConversion(catId) + } \ No newline at end of file diff --git a/Early Returns/Baby Steps/task-info.yaml b/Early Returns/Baby Steps/task-info.yaml index 74b2af7d..5a4470f2 100644 --- a/Early Returns/Baby Steps/task-info.yaml +++ b/Early Returns/Baby Steps/task-info.yaml @@ -2,9 +2,37 @@ type: edu files: - name: src/Task.scala visible: true + placeholders: + - offset: 434 + length: 107 + placeholder_text: /* TODO */ + - offset: 1642 + length: 123 + placeholder_text: /* TODO */ + - offset: 2061 + length: 141 + placeholder_text: /* TODO */ + - offset: 2484 + length: 104 + placeholder_text: /* TODO */ + - offset: 2856 + length: 122 + placeholder_text: /* TODO */ - name: test/TestSpec.scala visible: false - name: build.sbt visible: false - name: src/EarlyReturns.scala visible: true + - name: src/Cat.scala + visible: true + - name: src/FurCharacteristic.scala + visible: true + - name: src/Pattern.scala + visible: true + - name: src/Color.scala + visible: true + - name: src/Breed.scala + visible: true + - name: src/Database.scala + visible: true diff --git a/Early Returns/Baby Steps/task.md b/Early Returns/Baby Steps/task.md index db914848..0cd46dfe 100644 --- a/Early Returns/Baby Steps/task.md +++ b/Early Returns/Baby Steps/task.md @@ -105,3 +105,26 @@ 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. + +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. + +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. + +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. + +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. + +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. \ No newline at end of file diff --git a/Early Returns/Baby Steps/test/TestSpec.scala b/Early Returns/Baby Steps/test/TestSpec.scala index f73ac3d4..387d1392 100644 --- a/Early Returns/Baby Steps/test/TestSpec.scala +++ b/Early Returns/Baby Steps/test/TestSpec.scala @@ -1,8 +1,36 @@ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite +import Task._ +import Database._ -class TestSpec extends FunSuite { - //TODO: implement your test here - test("First test") { - assert(false, "Tests not implemented for the task") +class TaskSpec extends AnyFunSuite { + def runTest(expectedCatId: Int, additionalMsg: String, findCat: Seq[CatId] => Option[Cat]): Unit = + val stream = new java.io.ByteArrayOutputStream() + + def logMessage(id: CatId) = + s"Cat conversion: $id\nValidation of fur characteristics: ${catDatabase(id - 1).name}" + + Console.withOut(stream) { + val cat = findCat(identifiers) + + assert(cat === Some(catDatabase(expectedCatId - 1))) + + val expected = (1 to expectedCatId).map(id => logMessage(id)).mkString("\n").concat(additionalMsg).trim + val actual = stream.toString().replaceAll("\r\n", "\n").trim + + assert(actual == expected) + } + + test("Imperative Find First Valid Cat returns the valid cat and doesn't traverse the whole collection") { + runTest(7, "", imperativeFindFirstValidCat) + } + + test("Functional Find First Valid Cat returns the valid cat and doesn't traverse the whole collection") { + val expectedCatId = 7 + runTest(expectedCatId, s"\nCat conversion: $expectedCatId", functionalFindFirstValidCat) + } + + test("CollectFirst Find First Valid Cat returns the valid cat and doesn't traverse the whole collection") { + val expectedCatId = 7 + runTest(expectedCatId, s"\nCat conversion: $expectedCatId", collectFirstFindFirstValidCat) } } diff --git a/Early Returns/Breaking Boundaries/src/Breed.scala b/Early Returns/Breaking Boundaries/src/Breed.scala new file mode 100644 index 00000000..8e19f8f5 --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/Breed.scala @@ -0,0 +1,13 @@ +enum Breed: + case Siamese + case Persian + case MaineCoon + case Ragdoll + case Bengal + case Abyssinian + case Birman + case OrientalShorthair + case Sphynx + case DevonRex + case ScottishFold + case Metis \ No newline at end of file diff --git a/Early Returns/Breaking Boundaries/src/Cat.scala b/Early Returns/Breaking Boundaries/src/Cat.scala new file mode 100644 index 00000000..b90e29cc --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/Cat.scala @@ -0,0 +1,5 @@ +case class Cat(name: String, + breed: Breed, + primaryColor: Color, + pattern: Pattern, + furCharacteristics: Set[FurCharacteristic]) diff --git a/Early Returns/Breaking Boundaries/src/Color.scala b/Early Returns/Breaking Boundaries/src/Color.scala new file mode 100644 index 00000000..b860b8ab --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/Color.scala @@ -0,0 +1,11 @@ +enum Color: + case Lavender + case White + case Cream + case Fawn + case Cinnamon + case Chocolate + case Orange + case Lilac + case Blue + case Black \ No newline at end of file diff --git a/Early Returns/Breaking Boundaries/src/Database.scala b/Early Returns/Breaking Boundaries/src/Database.scala new file mode 100644 index 00000000..2989ffe0 --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/Database.scala @@ -0,0 +1,92 @@ +import Breed._ +import Color._ +import FurCharacteristic._ +import Pattern._ +import TabbySubtype._ +import ShadingSubtype._ +import BicolorSubtype._ +import TricolorSubtype._ + +object Database: + type CatId = Int + + /** + * @param id a unique cat identifier + * @param name a cat's name + * @param adopted a flag that is true if the cat has been adopted + */ + case class CatAdoptionStatus(id: CatId, name: String, adopted: Boolean) + + val identifiers: Seq[CatId] = 1 to 30 + + /** + * This database "table" tracks whether a cat has already been adopted. + */ + val adoptionStatusDatabase: Seq[CatAdoptionStatus] = Seq( + CatAdoptionStatus(1, "Luna", true), + CatAdoptionStatus(2, "Max", false), + CatAdoptionStatus(3, "Charlie", true), + CatAdoptionStatus(4, "Daisy", false), + CatAdoptionStatus(5, "Simba", true), + CatAdoptionStatus(6, "Oliver", false), + CatAdoptionStatus(7, "Molly", true), + CatAdoptionStatus(8, "Lucy", true), + CatAdoptionStatus(9, "Buddy", false), + CatAdoptionStatus(10, "Rocky", true), + CatAdoptionStatus(11, "Jack", false), + CatAdoptionStatus(12, "Sadie", true), + CatAdoptionStatus(13, "Ginger", false), + CatAdoptionStatus(14, "Leo", true), + CatAdoptionStatus(15, "Misty", false), + CatAdoptionStatus(16, "Rex", true), + CatAdoptionStatus(17, "Bella", true), + CatAdoptionStatus(18, "Tiger", true), + CatAdoptionStatus(19, "Zara", false), + CatAdoptionStatus(20, "Sophie", true), + CatAdoptionStatus(21, "Ollie", false), + CatAdoptionStatus(22, "Pixie", true), + CatAdoptionStatus(23, "Fuzz", false), + CatAdoptionStatus(24, "Scotty", true), + CatAdoptionStatus(25, "Mixie", true), + CatAdoptionStatus(26, "Cleo", true), + CatAdoptionStatus(27, "Milo", false), + CatAdoptionStatus(28, "Nala", true), + CatAdoptionStatus(29, "Loki", false), + CatAdoptionStatus(30, "Shadow", true) + ) + + /** + * This database "table" contains the basic information about each cat. + */ + val catDatabase: Seq[Cat] = Seq( + Cat("Luna", Siamese, Blue, Pattern.SolidColor, Set(Fluffy)), // Invalid + Cat("Max", Persian, Black, Pattern.Tabby(Mackerel), Set(ShortHaired)), // Invalid + Cat("Charlie", MaineCoon, Orange, Pattern.SolidColor, Set(SleekHaired)), // Invalid + Cat("Daisy", Ragdoll, Cream, Pattern.Bicolor(Van), Set(ShortHaired)), // Invalid + Cat("Simba", Bengal, Blue, Pattern.SolidColor, Set(LongHaired)), // Invalid + Cat("Oliver", ScottishFold, Cinnamon, Pattern.Spots, Set(WireHaired)), // Invalid + Cat("Molly", Persian, White, Pattern.Shading(Shaded), Set(Fluffy, DoubleCoated)), // Valid + Cat("Lucy", Metis, Blue, Pattern.Pointed, Set(SleekHaired, Fluffy)), // Metis can be anything + Cat("Buddy", Siamese, Cream, Pattern.Tabby(Mackerel), Set(LongHaired)), // Invalid + Cat("Rocky", MaineCoon, Black, Pattern.Tricolor(Tortie), Set(ShortHaired)), // Invalid + Cat("Jack", Bengal, Chocolate, Pattern.Tabby(Spotted), Set(ShortHaired, Plush)), // Invalid + Cat("Sadie", Birman, White, Pattern.Bicolor(Van), Set(LongHaired, Fluffy)), // Valid + Cat("Ginger", Abyssinian, Orange, Pattern.Tabby(Ticked), Set(ShortHaired, SleekHaired)), // Valid + Cat("Leo", Siamese, Cream, Pattern.SolidColor, Set(ShortHaired, SleekHaired)), // Valid + Cat("Misty", Persian, White, Pattern.SolidColor, Set(LongHaired, Fluffy)), // Valid + Cat("Rex", MaineCoon, Black, Pattern.Tabby(Classic), Set(LongHaired, DoubleCoated)), // Valid + Cat("Bella", Ragdoll, Blue, Pattern.Pointed, Set(Fluffy)), // Valid + Cat("Tiger", Bengal, Orange, Pattern.Spots, Set(ShortHaired)), // Valid + Cat("Zara", Abyssinian, Cinnamon, Pattern.Tabby(Ticked), Set(ShortHaired)), // Valid + Cat("Sophie", Birman, Lilac, Pattern.Pointed, Set(LongHaired, Fluffy)), // Valid + Cat("Ollie", OrientalShorthair, Lavender, Pattern.SolidColor, Set(SleekHaired)), // Valid + Cat("Pixie", Sphynx, Lilac, Pattern.SolidColor, Set(WireHaired)), // Valid + Cat("Fuzz", DevonRex, Cream, Pattern.SolidColor, Set(ShortHaired, WireHaired)), // Valid + Cat("Scotty", ScottishFold, White, Pattern.Bicolor(Tuxedo), Set(ShortHaired)), // Valid + Cat("Mixie", Metis, Chocolate, Pattern.Tricolor(Calico), Set(DoubleCoated)), // Metis can be anything + Cat("Cleo", Abyssinian, Fawn, Pattern.Tricolor(Tortie), Set(Fluffy)), // Invalid + Cat("Milo", Birman, Lavender, Pattern.SolidColor, Set(SleekHaired)), // Invalid + Cat("Nala", OrientalShorthair, Black, Pattern.Shading(Chinchilla), Set(Fluffy)), // Invalid + Cat("Loki", Sphynx, White, Pattern.Shading(Shaded), Set(LongHaired)), // Invalid + Cat("Shadow", DevonRex, Lilac, Pattern.SolidColor, Set(LongHaired)) // Invalid + ) \ No newline at end of file diff --git a/Early Returns/Breaking Boundaries/src/EarlyReturns.scala b/Early Returns/Breaking Boundaries/src/EarlyReturns.scala new file mode 100644 index 00000000..baf623b1 --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/EarlyReturns.scala @@ -0,0 +1,60 @@ +import scala.util.boundary +import scala.util.boundary.break + +object EarlyReturns: + private type UserId = Int + private type Email = String + + case class UserData(id: UserId, name: String, email: Email) + + /** + * Pretend database of user data. + */ + private val database = Seq( + UserData(1, "Ayaan Sharma", "ayaan@gmail.com"), + UserData(2, "Lei Zhang", "lei_zhang@yahoo.cn"), + UserData(3, "Fatima Al-Fassi", "fatima.alfassi@outlook.sa"), + UserData(4, "Ana Sofia Ruiz", "ana_sofia@icloud.es"), + UserData(5, "Oluwaseun Adeyemi", "oluwaseun@hotmail.ng"), + UserData(6, "Maria Ivanova", "maria.ivanova@aol.ru"), + UserData(7, "Yuto Nakamura", "yuto.nakamura@mail.jp"), + UserData(8, "Chiara Rossi", "chiara@live.it"), + UserData(9, "Lucas Müller", "lucas@protonmail.de"), + UserData(10, "Sara Al-Bahrani", "sara.albahrani@gmail.com"), + UserData(11, "Min-Jun Kim", "minjun.kim@yahoo.kr") + ) + + private val identifiers = 1 to 11 + + /** + * This is our "complex conversion" method. + * We assume that it is costly to retrieve user data, so we want to avoid + * calling it unless it's absolutely necessary. + * + * This function takes into account that some IDs can be left out from the database + */ + def safeComplexConversion(userId: UserId): Option[UserData] = database.find(_.id == userId) + + + /** + * Similar to `safeComplexConversion`, the validation of user data is costly + * and we shouldn't do it too often. + * + * @param user user data + * @return true if the user data is valid, false otherwise + */ + def complexValidation(user: UserData): Boolean = + !user.email.contains(' ') && user.email.count(_ == '@') == 1 + + /** + * Using `boundary` we create a computation context to which `break` returns the value. + * @param userIds the sequence of all user identifiers + * @return `Some` of the first valid user data or `None` if no valid user data is found + */ + def findFirstValidUser10(userIds: Seq[UserId]): Option[UserData] = + boundary: + for userId <- userIds do + safeComplexConversion(userId).foreach { userData => + if (complexValidation(userData)) break(Some(userData)) + } + None diff --git a/Early Returns/Breaking Boundaries/src/FurCharacteristic.scala b/Early Returns/Breaking Boundaries/src/FurCharacteristic.scala new file mode 100644 index 00000000..ae243fa8 --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/FurCharacteristic.scala @@ -0,0 +1,8 @@ +enum FurCharacteristic: + case LongHaired + case ShortHaired + case Fluffy + case Plush + case SleekHaired + case WireHaired + case DoubleCoated \ No newline at end of file diff --git a/Early Returns/Breaking Boundaries/src/Pattern.scala b/Early Returns/Breaking Boundaries/src/Pattern.scala new file mode 100644 index 00000000..fc9b0651 --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/Pattern.scala @@ -0,0 +1,28 @@ +enum TabbySubtype: + case Classic + case Ticked + case Mackerel + case Spotted + case Patched + +enum ShadingSubtype: + case Chinchilla + case Shaded + case Smoke + +enum BicolorSubtype: + case Tuxedo + case Van + +enum TricolorSubtype: + case Calico + case Tortie + +enum Pattern: + case Tabby(val subType: TabbySubtype) + case Pointed + case Shading(val subType: ShadingSubtype) + case SolidColor + case Bicolor(val subType: BicolorSubtype) + case Tricolor(val subType: TricolorSubtype) + case Spots diff --git a/Early Returns/Breaking Boundaries/src/Task.scala b/Early Returns/Breaking Boundaries/src/Task.scala index fc27c517..96e5b20e 100644 --- a/Early Returns/Breaking Boundaries/src/Task.scala +++ b/Early Returns/Breaking Boundaries/src/Task.scala @@ -1,60 +1,61 @@ +import Database.* +import Breed.* +import FurCharacteristic.* +import Pattern.* +import TabbySubtype.* +import ShadingSubtype.* +import BicolorSubtype.* +import EarlyReturns.{UserData, UserId, complexValidation, safeComplexConversion} +import TricolorSubtype.* + import scala.util.boundary import scala.util.boundary.break -object EarlyReturns: - private type UserId = Int - private type Email = String - - case class UserData(id: UserId, name: String, email: Email) - - /** - * Pretend database of user data. - */ - private val database = Seq( - UserData(1, "John Doe", "john@@gmail.com"), - UserData(2, "Jane Smith", "jane smith@yahoo.com"), - UserData(3, "Michael Brown", "michaeloutlook.com"), - UserData(4, "Emily Johnson", "emily at icloud.com"), - UserData(5, "Daniel Wilson", "daniel@hotmail.com"), - UserData(6, "Sophia Martinez", "sophia@aol.com"), - UserData(7, "Christopher Taylor", "christopher@mail.com"), - UserData(8, "Olivia Anderson", "olivia@live.com"), - UserData(9, "James Thomas", "james@protonmail.com"), - UserData(10, "Isabella Jackson", "isabella@gmail.com"), - UserData(11, "Alexander White", "alexander@yahoo.com") +object Task: + private val breedCharacteristics: Map[Breed, Set[FurCharacteristic]] = Map( + Siamese -> Set(ShortHaired, SleekHaired), + Persian -> Set(LongHaired, Fluffy, DoubleCoated), + MaineCoon -> Set(LongHaired, Fluffy, DoubleCoated), + Ragdoll -> Set(LongHaired, Fluffy, DoubleCoated), + Bengal -> Set(ShortHaired), + Abyssinian -> Set(ShortHaired, SleekHaired), + Birman -> Set(LongHaired, Fluffy, DoubleCoated), + OrientalShorthair -> Set(ShortHaired, SleekHaired), + Sphynx -> Set(WireHaired, Plush), + DevonRex -> Set(ShortHaired, Plush, WireHaired), + ScottishFold -> Set(ShortHaired, LongHaired, DoubleCoated), + Metis -> Set(LongHaired, ShortHaired, Fluffy, Plush, SleekHaired, WireHaired, DoubleCoated) // Assuming Metis can have any characteristics ) - private val identifiers = 1 to 11 - /** - * This is our "complex conversion" method. - * We assume that it is costly to retrieve user data, so we want to avoid - * calling it unless it's absolutely necessary. + * Implement the validation: the characteristics of the cat's fur should feed their breed. * - * This function takes into account that some IDs can be left out from the database + * @param cat cat data + * @return true if the user data is valid, false otherwise */ - def safeComplexConversion(userId: UserId): Option[UserData] = database.find(_.id == userId) - + def furCharacteristicValidation(cat: Cat): Boolean = + print(s"Validation of fur characteristics: ${cat.name}\n") + val validCharacteristics = breedCharacteristics(cat.breed) + cat.furCharacteristics.forall(validCharacteristics.contains) /** - * Similar to `safeComplexConversion`, the validation of user data is costly - * and we shouldn't do it too often. - * - * @param user user data - * @return true if the user data is valid, false otherwise + * This function takes into account that some IDs can be left out from the database + * and only selects a cat who has not been adopted. */ - def complexValidation(user: UserData): Boolean = - !user.email.contains(' ') && user.email.count(_ == '@') == 1 + def nonAdoptedCatConversion(catId: CatId): Option[Cat] = + print(s"Non-adopted cat conversion: $catId\n") + val status = adoptionStatusDatabase.find(cat => cat.id == catId && !cat.adopted) + status.flatMap(status => catDatabase.find(_.name == status.name)) /** - * Using `boundary` we create a computation context to which `break` returns the value. - * @param userIds the sequence of all user identifiers - * @return `Some` of the first valid user data or `None` if no valid user data is found + * Use `boundary` to find the first cat with valid fur characteristics. + * @param catIds the sequence of all cat identifiers + * @return `Some` of the first valid cat data or `None` if no valid cat data is found */ - def findFirstValidUser10(userIds: Seq[UserId]): Option[UserData] = + def findFirstValidCat(catIds: Seq[CatId]): Option[Cat] = boundary: - for userId <- userIds do - safeComplexConversion(userId).foreach { userData => - if (complexValidation(userData)) break(Some(userData)) + for catId <- catIds do + nonAdoptedCatConversion(catId).foreach { cat => + if (furCharacteristicValidation(cat)) break(Some(cat)) } None diff --git a/Early Returns/Breaking Boundaries/task-info.yaml b/Early Returns/Breaking Boundaries/task-info.yaml index 4ef9637a..998f33c3 100644 --- a/Early Returns/Breaking Boundaries/task-info.yaml +++ b/Early Returns/Breaking Boundaries/task-info.yaml @@ -1,8 +1,22 @@ type: edu files: - - name: src/Task.scala - visible: true - name: test/TestSpec.scala visible: false - name: build.sbt visible: false + - name: src/EarlyReturns.scala + visible: true + - name: src/Task.scala + visible: true + - name: src/Breed.scala + visible: true + - name: src/FurCharacteristic.scala + visible: true + - name: src/Color.scala + visible: true + - name: src/Pattern.scala + visible: true + - name: src/Database.scala + visible: true + - name: src/Cat.scala + visible: true diff --git a/Early Returns/Breaking Boundaries/task.md b/Early Returns/Breaking Boundaries/task.md index 6f150ff9..8d4a5ee7 100644 --- a/Early Returns/Breaking Boundaries/task.md +++ b/Early Returns/Breaking Boundaries/task.md @@ -27,3 +27,12 @@ Since it's the end of the method, it immediately returns `Some(userData)`. Sometimes there are multiple boundaries, in this case 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). + +### 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. + +* Use a boundary to implement `findFirstValidCat`. +* Copy the implementations of `furCharacteristicValidation` and `nonAdoptedCatConversion` from the previous task. diff --git a/Early Returns/Breaking Boundaries/test/TestSpec.scala b/Early Returns/Breaking Boundaries/test/TestSpec.scala index f73ac3d4..7b7350bb 100644 --- a/Early Returns/Breaking Boundaries/test/TestSpec.scala +++ b/Early Returns/Breaking Boundaries/test/TestSpec.scala @@ -1,8 +1,27 @@ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite +import Task._ +import Database._ -class TestSpec extends FunSuite { - //TODO: implement your test here - test("First test") { - assert(false, "Tests not implemented for the task") +class TaskSpec extends AnyFunSuite { + def runTest(expectedCatId: Int, validationMsg: String, findCat: Seq[CatId] => Option[Cat]): Unit = + val stream = new java.io.ByteArrayOutputStream() + + def logMessage(id: CatId) = + val validationMessage = if !adoptionStatusDatabase(id-1).adopted then s"\n$validationMsg: ${catDatabase(id - 1).name}" else "" + s"Non-adopted cat conversion: $id$validationMessage" + + Console.withOut(stream) { + val cat = findCat(identifiers) + + assert(cat === Some(catDatabase(expectedCatId - 1))) + + val expected = (1 to expectedCatId).map(id => logMessage(id)).mkString("\n").trim + val actual = stream.toString().replaceAll("\r\n", "\n").trim + + assert(actual == expected) + } + + test("Find First Valid Cat returns the valid cat and doesn't traverse the whole collection. Validation is only run when a cat is in the database and has not been adopted") { + runTest(13, "Validation of fur characteristics", findFirstValidCat) } } diff --git a/Early Returns/Lazy Collection to the Rescue/src/Breed.scala b/Early Returns/Lazy Collection to the Rescue/src/Breed.scala new file mode 100644 index 00000000..8e19f8f5 --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/src/Breed.scala @@ -0,0 +1,13 @@ +enum Breed: + case Siamese + case Persian + case MaineCoon + case Ragdoll + case Bengal + case Abyssinian + case Birman + case OrientalShorthair + case Sphynx + case DevonRex + case ScottishFold + case Metis \ No newline at end of file diff --git a/Early Returns/Lazy Collection to the Rescue/src/Cat.scala b/Early Returns/Lazy Collection to the Rescue/src/Cat.scala new file mode 100644 index 00000000..b90e29cc --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/src/Cat.scala @@ -0,0 +1,5 @@ +case class Cat(name: String, + breed: Breed, + primaryColor: Color, + pattern: Pattern, + furCharacteristics: Set[FurCharacteristic]) diff --git a/Early Returns/Lazy Collection to the Rescue/src/Color.scala b/Early Returns/Lazy Collection to the Rescue/src/Color.scala new file mode 100644 index 00000000..b860b8ab --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/src/Color.scala @@ -0,0 +1,11 @@ +enum Color: + case Lavender + case White + case Cream + case Fawn + case Cinnamon + case Chocolate + case Orange + case Lilac + case Blue + case Black \ No newline at end of file diff --git a/Early Returns/Lazy Collection to the Rescue/src/Database.scala b/Early Returns/Lazy Collection to the Rescue/src/Database.scala new file mode 100644 index 00000000..2989ffe0 --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/src/Database.scala @@ -0,0 +1,92 @@ +import Breed._ +import Color._ +import FurCharacteristic._ +import Pattern._ +import TabbySubtype._ +import ShadingSubtype._ +import BicolorSubtype._ +import TricolorSubtype._ + +object Database: + type CatId = Int + + /** + * @param id a unique cat identifier + * @param name a cat's name + * @param adopted a flag that is true if the cat has been adopted + */ + case class CatAdoptionStatus(id: CatId, name: String, adopted: Boolean) + + val identifiers: Seq[CatId] = 1 to 30 + + /** + * This database "table" tracks whether a cat has already been adopted. + */ + val adoptionStatusDatabase: Seq[CatAdoptionStatus] = Seq( + CatAdoptionStatus(1, "Luna", true), + CatAdoptionStatus(2, "Max", false), + CatAdoptionStatus(3, "Charlie", true), + CatAdoptionStatus(4, "Daisy", false), + CatAdoptionStatus(5, "Simba", true), + CatAdoptionStatus(6, "Oliver", false), + CatAdoptionStatus(7, "Molly", true), + CatAdoptionStatus(8, "Lucy", true), + CatAdoptionStatus(9, "Buddy", false), + CatAdoptionStatus(10, "Rocky", true), + CatAdoptionStatus(11, "Jack", false), + CatAdoptionStatus(12, "Sadie", true), + CatAdoptionStatus(13, "Ginger", false), + CatAdoptionStatus(14, "Leo", true), + CatAdoptionStatus(15, "Misty", false), + CatAdoptionStatus(16, "Rex", true), + CatAdoptionStatus(17, "Bella", true), + CatAdoptionStatus(18, "Tiger", true), + CatAdoptionStatus(19, "Zara", false), + CatAdoptionStatus(20, "Sophie", true), + CatAdoptionStatus(21, "Ollie", false), + CatAdoptionStatus(22, "Pixie", true), + CatAdoptionStatus(23, "Fuzz", false), + CatAdoptionStatus(24, "Scotty", true), + CatAdoptionStatus(25, "Mixie", true), + CatAdoptionStatus(26, "Cleo", true), + CatAdoptionStatus(27, "Milo", false), + CatAdoptionStatus(28, "Nala", true), + CatAdoptionStatus(29, "Loki", false), + CatAdoptionStatus(30, "Shadow", true) + ) + + /** + * This database "table" contains the basic information about each cat. + */ + val catDatabase: Seq[Cat] = Seq( + Cat("Luna", Siamese, Blue, Pattern.SolidColor, Set(Fluffy)), // Invalid + Cat("Max", Persian, Black, Pattern.Tabby(Mackerel), Set(ShortHaired)), // Invalid + Cat("Charlie", MaineCoon, Orange, Pattern.SolidColor, Set(SleekHaired)), // Invalid + Cat("Daisy", Ragdoll, Cream, Pattern.Bicolor(Van), Set(ShortHaired)), // Invalid + Cat("Simba", Bengal, Blue, Pattern.SolidColor, Set(LongHaired)), // Invalid + Cat("Oliver", ScottishFold, Cinnamon, Pattern.Spots, Set(WireHaired)), // Invalid + Cat("Molly", Persian, White, Pattern.Shading(Shaded), Set(Fluffy, DoubleCoated)), // Valid + Cat("Lucy", Metis, Blue, Pattern.Pointed, Set(SleekHaired, Fluffy)), // Metis can be anything + Cat("Buddy", Siamese, Cream, Pattern.Tabby(Mackerel), Set(LongHaired)), // Invalid + Cat("Rocky", MaineCoon, Black, Pattern.Tricolor(Tortie), Set(ShortHaired)), // Invalid + Cat("Jack", Bengal, Chocolate, Pattern.Tabby(Spotted), Set(ShortHaired, Plush)), // Invalid + Cat("Sadie", Birman, White, Pattern.Bicolor(Van), Set(LongHaired, Fluffy)), // Valid + Cat("Ginger", Abyssinian, Orange, Pattern.Tabby(Ticked), Set(ShortHaired, SleekHaired)), // Valid + Cat("Leo", Siamese, Cream, Pattern.SolidColor, Set(ShortHaired, SleekHaired)), // Valid + Cat("Misty", Persian, White, Pattern.SolidColor, Set(LongHaired, Fluffy)), // Valid + Cat("Rex", MaineCoon, Black, Pattern.Tabby(Classic), Set(LongHaired, DoubleCoated)), // Valid + Cat("Bella", Ragdoll, Blue, Pattern.Pointed, Set(Fluffy)), // Valid + Cat("Tiger", Bengal, Orange, Pattern.Spots, Set(ShortHaired)), // Valid + Cat("Zara", Abyssinian, Cinnamon, Pattern.Tabby(Ticked), Set(ShortHaired)), // Valid + Cat("Sophie", Birman, Lilac, Pattern.Pointed, Set(LongHaired, Fluffy)), // Valid + Cat("Ollie", OrientalShorthair, Lavender, Pattern.SolidColor, Set(SleekHaired)), // Valid + Cat("Pixie", Sphynx, Lilac, Pattern.SolidColor, Set(WireHaired)), // Valid + Cat("Fuzz", DevonRex, Cream, Pattern.SolidColor, Set(ShortHaired, WireHaired)), // Valid + Cat("Scotty", ScottishFold, White, Pattern.Bicolor(Tuxedo), Set(ShortHaired)), // Valid + Cat("Mixie", Metis, Chocolate, Pattern.Tricolor(Calico), Set(DoubleCoated)), // Metis can be anything + Cat("Cleo", Abyssinian, Fawn, Pattern.Tricolor(Tortie), Set(Fluffy)), // Invalid + Cat("Milo", Birman, Lavender, Pattern.SolidColor, Set(SleekHaired)), // Invalid + Cat("Nala", OrientalShorthair, Black, Pattern.Shading(Chinchilla), Set(Fluffy)), // Invalid + Cat("Loki", Sphynx, White, Pattern.Shading(Shaded), Set(LongHaired)), // Invalid + Cat("Shadow", DevonRex, Lilac, Pattern.SolidColor, Set(LongHaired)) // Invalid + ) \ No newline at end of file diff --git a/Early Returns/Lazy Collection to the Rescue/src/EarlyReturns.scala b/Early Returns/Lazy Collection to the Rescue/src/EarlyReturns.scala index 8ab5a07c..a0fecaa2 100644 --- a/Early Returns/Lazy Collection to the Rescue/src/EarlyReturns.scala +++ b/Early Returns/Lazy Collection to the Rescue/src/EarlyReturns.scala @@ -8,17 +8,17 @@ object EarlyReturns: * Pretend database of user data. */ private val database = Seq( - UserData(1, "John Doe", "john@@gmail.com"), - UserData(2, "Jane Smith", "jane smith@yahoo.com"), - UserData(3, "Michael Brown", "michaeloutlook.com"), - UserData(4, "Emily Johnson", "emily at icloud.com"), - UserData(5, "Daniel Wilson", "daniel@hotmail.com"), - UserData(6, "Sophia Martinez", "sophia@aol.com"), - UserData(7, "Christopher Taylor", "christopher@mail.com"), - UserData(8, "Olivia Anderson", "olivia@live.com"), - UserData(9, "James Thomas", "james@protonmail.com"), - UserData(10, "Isabella Jackson", "isabella@gmail.com"), - UserData(11, "Alexander White", "alexander@yahoo.com") + UserData(1, "Ayaan Sharma", "ayaan@gmail.com"), + UserData(2, "Lei Zhang", "lei_zhang@yahoo.cn"), + UserData(3, "Fatima Al-Fassi", "fatima.alfassi@outlook.sa"), + UserData(4, "Ana Sofia Ruiz", "ana_sofia@icloud.es"), + UserData(5, "Oluwaseun Adeyemi", "oluwaseun@hotmail.ng"), + UserData(6, "Maria Ivanova", "maria.ivanova@aol.ru"), + UserData(7, "Yuto Nakamura", "yuto.nakamura@mail.jp"), + UserData(8, "Chiara Rossi", "chiara@live.it"), + UserData(9, "Lucas Müller", "lucas@protonmail.de"), + UserData(10, "Sara Al-Bahrani", "sara.albahrani@gmail.com"), + UserData(11, "Min-Jun Kim", "minjun.kim@yahoo.kr") ) private val identifiers = 1 to 11 @@ -49,9 +49,9 @@ object EarlyReturns: * @param userIds the sequence of all user identifiers * @return `Some` of the first valid user data or `None` if no valid user data is found */ - def findFirstValidUser9(userIds: Seq[UserId]): Option[UserData] = + def findFirstValidCat(userIds: Seq[UserId]): Option[UserData] = userIds .iterator .map(safeComplexConversion) .find(_.exists(complexValidation)) - .flatten \ No newline at end of file + .flatten diff --git a/Early Returns/Lazy Collection to the Rescue/src/FurCharacteristic.scala b/Early Returns/Lazy Collection to the Rescue/src/FurCharacteristic.scala new file mode 100644 index 00000000..ae243fa8 --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/src/FurCharacteristic.scala @@ -0,0 +1,8 @@ +enum FurCharacteristic: + case LongHaired + case ShortHaired + case Fluffy + case Plush + case SleekHaired + case WireHaired + case DoubleCoated \ No newline at end of file diff --git a/Early Returns/Lazy Collection to the Rescue/src/Pattern.scala b/Early Returns/Lazy Collection to the Rescue/src/Pattern.scala new file mode 100644 index 00000000..fc9b0651 --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/src/Pattern.scala @@ -0,0 +1,28 @@ +enum TabbySubtype: + case Classic + case Ticked + case Mackerel + case Spotted + case Patched + +enum ShadingSubtype: + case Chinchilla + case Shaded + case Smoke + +enum BicolorSubtype: + case Tuxedo + case Van + +enum TricolorSubtype: + case Calico + case Tortie + +enum Pattern: + case Tabby(val subType: TabbySubtype) + case Pointed + case Shading(val subType: ShadingSubtype) + case SolidColor + case Bicolor(val subType: BicolorSubtype) + case Tricolor(val subType: TricolorSubtype) + case Spots diff --git a/Early Returns/Lazy Collection to the Rescue/src/Task.scala b/Early Returns/Lazy Collection to the Rescue/src/Task.scala index 66d32cbc..d065178d 100644 --- a/Early Returns/Lazy Collection to the Rescue/src/Task.scala +++ b/Early Returns/Lazy Collection to the Rescue/src/Task.scala @@ -1 +1,57 @@ -object Task \ No newline at end of file +import Database._ +import Breed._ +import FurCharacteristic._ +import Pattern._ +import TabbySubtype._ +import ShadingSubtype._ +import BicolorSubtype._ +import TricolorSubtype._ + +object Task: + private val breedCharacteristics: Map[Breed, Set[FurCharacteristic]] = Map( + Siamese -> Set(ShortHaired, SleekHaired), + Persian -> Set(LongHaired, Fluffy, DoubleCoated), + MaineCoon -> Set(LongHaired, Fluffy, DoubleCoated), + Ragdoll -> Set(LongHaired, Fluffy, DoubleCoated), + Bengal -> Set(ShortHaired), + Abyssinian -> Set(ShortHaired, SleekHaired), + Birman -> Set(LongHaired, Fluffy, DoubleCoated), + OrientalShorthair -> Set(ShortHaired, SleekHaired), + Sphynx -> Set(WireHaired, Plush), + DevonRex -> Set(ShortHaired, Plush, WireHaired), + ScottishFold -> Set(ShortHaired, LongHaired, DoubleCoated), + Metis -> Set(LongHaired, ShortHaired, Fluffy, Plush, SleekHaired, WireHaired, DoubleCoated) // Assuming Metis can have any characteristics + ) + + /** + * Implement the validation: the characteristics of the cat's fur should feed their breed. + * + * @param cat cat data + * @return true if the user data is valid, false otherwise + */ + def furCharacteristicValidation(cat: Cat): Boolean = + print(s"Validation of fur characteristics: ${cat.name}\n") + val validCharacteristics = breedCharacteristics(cat.breed) + cat.furCharacteristics.forall(validCharacteristics.contains) + + /** + * This function takes into account that some IDs can be left out from the database + * and only selects a cat who has not been adopted. + */ + def nonAdoptedCatConversion(catId: CatId): Option[Cat] = + print(s"Non-adopted cat conversion: $catId\n") + val status = adoptionStatusDatabase.find(cat => cat.id == catId && !cat.adopted) + status.flatMap(status => catDatabase.find(_.name == status.name)) + + /** + * Implement the search by making the collection lazy. + * + * @param catIds the sequence of all user identifiers + * @return `Some` of the first valid cat data or `None` if no valid cat data is found + */ + def findFirstValidCat(catIds: Seq[CatId]): Option[Cat] = + catIds + .iterator + .map(nonAdoptedCatConversion) + .find(_.exists(furCharacteristicValidation)) + .flatten \ No newline at end of file diff --git a/Early Returns/Lazy Collection to the Rescue/task-info.yaml b/Early Returns/Lazy Collection to the Rescue/task-info.yaml index 74b2af7d..ae310627 100644 --- a/Early Returns/Lazy Collection to the Rescue/task-info.yaml +++ b/Early Returns/Lazy Collection to the Rescue/task-info.yaml @@ -1,10 +1,32 @@ type: edu files: - - name: src/Task.scala - visible: true - name: test/TestSpec.scala visible: false - name: build.sbt visible: false - name: src/EarlyReturns.scala visible: true + - name: src/FurCharacteristic.scala + visible: true + - name: src/Color.scala + visible: true + - name: src/Cat.scala + visible: true + - name: src/Database.scala + visible: true + - name: src/Breed.scala + visible: true + - name: src/Pattern.scala + visible: true + - name: src/Task.scala + visible: true + placeholders: + - offset: 1285 + length: 123 + placeholder_text: /* TODO */ + - offset: 1676 + length: 150 + placeholder_text: /* TODO */ + - offset: 2109 + length: 124 + placeholder_text: /* TODO */ diff --git a/Early Returns/Lazy Collection to the Rescue/task.md b/Early Returns/Lazy Collection to the Rescue/task.md index de2ad9e0..fe97ce01 100644 --- a/Early Returns/Lazy Collection to the Rescue/task.md +++ b/Early Returns/Lazy Collection to the Rescue/task.md @@ -22,4 +22,7 @@ Try comparing the two approaches on your own. ### Exercise -Implement the early return by using a lazy collection. \ No newline at end of file +Let's try using 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. diff --git a/Early Returns/Lazy Collection to the Rescue/test/TestSpec.scala b/Early Returns/Lazy Collection to the Rescue/test/TestSpec.scala index f73ac3d4..7b7350bb 100644 --- a/Early Returns/Lazy Collection to the Rescue/test/TestSpec.scala +++ b/Early Returns/Lazy Collection to the Rescue/test/TestSpec.scala @@ -1,8 +1,27 @@ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite +import Task._ +import Database._ -class TestSpec extends FunSuite { - //TODO: implement your test here - test("First test") { - assert(false, "Tests not implemented for the task") +class TaskSpec extends AnyFunSuite { + def runTest(expectedCatId: Int, validationMsg: String, findCat: Seq[CatId] => Option[Cat]): Unit = + val stream = new java.io.ByteArrayOutputStream() + + def logMessage(id: CatId) = + val validationMessage = if !adoptionStatusDatabase(id-1).adopted then s"\n$validationMsg: ${catDatabase(id - 1).name}" else "" + s"Non-adopted cat conversion: $id$validationMessage" + + Console.withOut(stream) { + val cat = findCat(identifiers) + + assert(cat === Some(catDatabase(expectedCatId - 1))) + + val expected = (1 to expectedCatId).map(id => logMessage(id)).mkString("\n").trim + val actual = stream.toString().replaceAll("\r\n", "\n").trim + + assert(actual == expected) + } + + test("Find First Valid Cat returns the valid cat and doesn't traverse the whole collection. Validation is only run when a cat is in the database and has not been adopted") { + runTest(13, "Validation of fur characteristics", findFirstValidCat) } } diff --git a/Early Returns/Unapply/src/Breed.scala b/Early Returns/Unapply/src/Breed.scala new file mode 100644 index 00000000..8e19f8f5 --- /dev/null +++ b/Early Returns/Unapply/src/Breed.scala @@ -0,0 +1,13 @@ +enum Breed: + case Siamese + case Persian + case MaineCoon + case Ragdoll + case Bengal + case Abyssinian + case Birman + case OrientalShorthair + case Sphynx + case DevonRex + case ScottishFold + case Metis \ No newline at end of file diff --git a/Early Returns/Unapply/src/Cat.scala b/Early Returns/Unapply/src/Cat.scala new file mode 100644 index 00000000..b90e29cc --- /dev/null +++ b/Early Returns/Unapply/src/Cat.scala @@ -0,0 +1,5 @@ +case class Cat(name: String, + breed: Breed, + primaryColor: Color, + pattern: Pattern, + furCharacteristics: Set[FurCharacteristic]) diff --git a/Early Returns/Unapply/src/Color.scala b/Early Returns/Unapply/src/Color.scala new file mode 100644 index 00000000..b860b8ab --- /dev/null +++ b/Early Returns/Unapply/src/Color.scala @@ -0,0 +1,11 @@ +enum Color: + case Lavender + case White + case Cream + case Fawn + case Cinnamon + case Chocolate + case Orange + case Lilac + case Blue + case Black \ No newline at end of file diff --git a/Early Returns/Unapply/src/Database.scala b/Early Returns/Unapply/src/Database.scala new file mode 100644 index 00000000..2989ffe0 --- /dev/null +++ b/Early Returns/Unapply/src/Database.scala @@ -0,0 +1,92 @@ +import Breed._ +import Color._ +import FurCharacteristic._ +import Pattern._ +import TabbySubtype._ +import ShadingSubtype._ +import BicolorSubtype._ +import TricolorSubtype._ + +object Database: + type CatId = Int + + /** + * @param id a unique cat identifier + * @param name a cat's name + * @param adopted a flag that is true if the cat has been adopted + */ + case class CatAdoptionStatus(id: CatId, name: String, adopted: Boolean) + + val identifiers: Seq[CatId] = 1 to 30 + + /** + * This database "table" tracks whether a cat has already been adopted. + */ + val adoptionStatusDatabase: Seq[CatAdoptionStatus] = Seq( + CatAdoptionStatus(1, "Luna", true), + CatAdoptionStatus(2, "Max", false), + CatAdoptionStatus(3, "Charlie", true), + CatAdoptionStatus(4, "Daisy", false), + CatAdoptionStatus(5, "Simba", true), + CatAdoptionStatus(6, "Oliver", false), + CatAdoptionStatus(7, "Molly", true), + CatAdoptionStatus(8, "Lucy", true), + CatAdoptionStatus(9, "Buddy", false), + CatAdoptionStatus(10, "Rocky", true), + CatAdoptionStatus(11, "Jack", false), + CatAdoptionStatus(12, "Sadie", true), + CatAdoptionStatus(13, "Ginger", false), + CatAdoptionStatus(14, "Leo", true), + CatAdoptionStatus(15, "Misty", false), + CatAdoptionStatus(16, "Rex", true), + CatAdoptionStatus(17, "Bella", true), + CatAdoptionStatus(18, "Tiger", true), + CatAdoptionStatus(19, "Zara", false), + CatAdoptionStatus(20, "Sophie", true), + CatAdoptionStatus(21, "Ollie", false), + CatAdoptionStatus(22, "Pixie", true), + CatAdoptionStatus(23, "Fuzz", false), + CatAdoptionStatus(24, "Scotty", true), + CatAdoptionStatus(25, "Mixie", true), + CatAdoptionStatus(26, "Cleo", true), + CatAdoptionStatus(27, "Milo", false), + CatAdoptionStatus(28, "Nala", true), + CatAdoptionStatus(29, "Loki", false), + CatAdoptionStatus(30, "Shadow", true) + ) + + /** + * This database "table" contains the basic information about each cat. + */ + val catDatabase: Seq[Cat] = Seq( + Cat("Luna", Siamese, Blue, Pattern.SolidColor, Set(Fluffy)), // Invalid + Cat("Max", Persian, Black, Pattern.Tabby(Mackerel), Set(ShortHaired)), // Invalid + Cat("Charlie", MaineCoon, Orange, Pattern.SolidColor, Set(SleekHaired)), // Invalid + Cat("Daisy", Ragdoll, Cream, Pattern.Bicolor(Van), Set(ShortHaired)), // Invalid + Cat("Simba", Bengal, Blue, Pattern.SolidColor, Set(LongHaired)), // Invalid + Cat("Oliver", ScottishFold, Cinnamon, Pattern.Spots, Set(WireHaired)), // Invalid + Cat("Molly", Persian, White, Pattern.Shading(Shaded), Set(Fluffy, DoubleCoated)), // Valid + Cat("Lucy", Metis, Blue, Pattern.Pointed, Set(SleekHaired, Fluffy)), // Metis can be anything + Cat("Buddy", Siamese, Cream, Pattern.Tabby(Mackerel), Set(LongHaired)), // Invalid + Cat("Rocky", MaineCoon, Black, Pattern.Tricolor(Tortie), Set(ShortHaired)), // Invalid + Cat("Jack", Bengal, Chocolate, Pattern.Tabby(Spotted), Set(ShortHaired, Plush)), // Invalid + Cat("Sadie", Birman, White, Pattern.Bicolor(Van), Set(LongHaired, Fluffy)), // Valid + Cat("Ginger", Abyssinian, Orange, Pattern.Tabby(Ticked), Set(ShortHaired, SleekHaired)), // Valid + Cat("Leo", Siamese, Cream, Pattern.SolidColor, Set(ShortHaired, SleekHaired)), // Valid + Cat("Misty", Persian, White, Pattern.SolidColor, Set(LongHaired, Fluffy)), // Valid + Cat("Rex", MaineCoon, Black, Pattern.Tabby(Classic), Set(LongHaired, DoubleCoated)), // Valid + Cat("Bella", Ragdoll, Blue, Pattern.Pointed, Set(Fluffy)), // Valid + Cat("Tiger", Bengal, Orange, Pattern.Spots, Set(ShortHaired)), // Valid + Cat("Zara", Abyssinian, Cinnamon, Pattern.Tabby(Ticked), Set(ShortHaired)), // Valid + Cat("Sophie", Birman, Lilac, Pattern.Pointed, Set(LongHaired, Fluffy)), // Valid + Cat("Ollie", OrientalShorthair, Lavender, Pattern.SolidColor, Set(SleekHaired)), // Valid + Cat("Pixie", Sphynx, Lilac, Pattern.SolidColor, Set(WireHaired)), // Valid + Cat("Fuzz", DevonRex, Cream, Pattern.SolidColor, Set(ShortHaired, WireHaired)), // Valid + Cat("Scotty", ScottishFold, White, Pattern.Bicolor(Tuxedo), Set(ShortHaired)), // Valid + Cat("Mixie", Metis, Chocolate, Pattern.Tricolor(Calico), Set(DoubleCoated)), // Metis can be anything + Cat("Cleo", Abyssinian, Fawn, Pattern.Tricolor(Tortie), Set(Fluffy)), // Invalid + Cat("Milo", Birman, Lavender, Pattern.SolidColor, Set(SleekHaired)), // Invalid + Cat("Nala", OrientalShorthair, Black, Pattern.Shading(Chinchilla), Set(Fluffy)), // Invalid + Cat("Loki", Sphynx, White, Pattern.Shading(Shaded), Set(LongHaired)), // Invalid + Cat("Shadow", DevonRex, Lilac, Pattern.SolidColor, Set(LongHaired)) // Invalid + ) \ No newline at end of file diff --git a/Early Returns/Unapply/src/EarlyReturns.scala b/Early Returns/Unapply/src/EarlyReturns.scala index b7b50ea5..dc49cdd3 100644 --- a/Early Returns/Unapply/src/EarlyReturns.scala +++ b/Early Returns/Unapply/src/EarlyReturns.scala @@ -8,17 +8,17 @@ object EarlyReturns: * Pretend database of user data. */ private val database = Seq( - UserData(1, "John Doe", "john@@gmail.com"), - UserData(2, "Jane Smith", "jane smith@yahoo.com"), - UserData(3, "Michael Brown", "michaeloutlook.com"), - UserData(4, "Emily Johnson", "emily at icloud.com"), - UserData(5, "Daniel Wilson", "daniel@hotmail.com"), - UserData(6, "Sophia Martinez", "sophia@aol.com"), - UserData(7, "Christopher Taylor", "christopher@mail.com"), - UserData(8, "Olivia Anderson", "olivia@live.com"), - UserData(9, "James Thomas", "james@protonmail.com"), - UserData(10, "Isabella Jackson", "isabella@gmail.com"), - UserData(11, "Alexander White", "alexander@yahoo.com") + UserData(1, "Ayaan Sharma", "ayaan@gmail.com"), + UserData(2, "Lei Zhang", "lei_zhang@yahoo.cn"), + UserData(3, "Fatima Al-Fassi", "fatima.alfassi@outlook.sa"), + UserData(4, "Ana Sofia Ruiz", "ana_sofia@icloud.es"), + UserData(5, "Oluwaseun Adeyemi", "oluwaseun@hotmail.ng"), + UserData(6, "Maria Ivanova", "maria.ivanova@aol.ru"), + UserData(7, "Yuto Nakamura", "yuto.nakamura@mail.jp"), + UserData(8, "Chiara Rossi", "chiara@live.it"), + UserData(9, "Lucas Müller", "lucas@protonmail.de"), + UserData(10, "Sara Al-Bahrani", "sara.albahrani@gmail.com"), + UserData(11, "Min-Jun Kim", "minjun.kim@yahoo.kr") ) private val identifiers = 1 to 11 diff --git a/Early Returns/Unapply/src/FurCharacteristic.scala b/Early Returns/Unapply/src/FurCharacteristic.scala new file mode 100644 index 00000000..ae243fa8 --- /dev/null +++ b/Early Returns/Unapply/src/FurCharacteristic.scala @@ -0,0 +1,8 @@ +enum FurCharacteristic: + case LongHaired + case ShortHaired + case Fluffy + case Plush + case SleekHaired + case WireHaired + case DoubleCoated \ No newline at end of file diff --git a/Early Returns/Unapply/src/Pattern.scala b/Early Returns/Unapply/src/Pattern.scala new file mode 100644 index 00000000..fc9b0651 --- /dev/null +++ b/Early Returns/Unapply/src/Pattern.scala @@ -0,0 +1,28 @@ +enum TabbySubtype: + case Classic + case Ticked + case Mackerel + case Spotted + case Patched + +enum ShadingSubtype: + case Chinchilla + case Shaded + case Smoke + +enum BicolorSubtype: + case Tuxedo + case Van + +enum TricolorSubtype: + case Calico + case Tortie + +enum Pattern: + case Tabby(val subType: TabbySubtype) + case Pointed + case Shading(val subType: ShadingSubtype) + case SolidColor + case Bicolor(val subType: BicolorSubtype) + case Tricolor(val subType: TricolorSubtype) + case Spots diff --git a/Early Returns/Unapply/src/Task.scala b/Early Returns/Unapply/src/Task.scala index 63d3f9a5..4f2fecda 100644 --- a/Early Returns/Unapply/src/Task.scala +++ b/Early Returns/Unapply/src/Task.scala @@ -1,3 +1,115 @@ -class Task { - //put your task here -} \ No newline at end of file +import Database._ +import Breed._ +import FurCharacteristic._ +import Pattern._ +import TabbySubtype._ +import ShadingSubtype._ +import BicolorSubtype._ +import TricolorSubtype._ + +object Task: + private val breedCharacteristics: Map[Breed, Set[FurCharacteristic]] = Map( + Siamese -> Set(ShortHaired, SleekHaired), + Persian -> Set(LongHaired, Fluffy, DoubleCoated), + MaineCoon -> Set(LongHaired, Fluffy, DoubleCoated), + Ragdoll -> Set(LongHaired, Fluffy, DoubleCoated), + Bengal -> Set(ShortHaired), + Abyssinian -> Set(ShortHaired, SleekHaired), + Birman -> Set(LongHaired, Fluffy, DoubleCoated), + OrientalShorthair -> Set(ShortHaired, SleekHaired), + Sphynx -> Set(WireHaired, Plush), + DevonRex -> Set(ShortHaired, Plush, WireHaired), + ScottishFold -> Set(ShortHaired, LongHaired, DoubleCoated), + Metis -> Set(LongHaired, ShortHaired, Fluffy, Plush, SleekHaired, WireHaired, DoubleCoated) // Assuming Metis can have any characteristics + ) + + /** + * Implement the validation: the characteristics of the cat's fur should feed their breed. + * + * @param cat cat data + * @return true if the user data is valid, false otherwise + */ + def furCharacteristicValidation(cat: Cat): Boolean = + print(s"Validation of fur characteristics: ${cat.name}\n") + val validCharacteristics = breedCharacteristics(cat.breed) + cat.furCharacteristics.forall(validCharacteristics.contains) + + /** + * This function takes into account that some IDs can be left out from the database + * and only selects a cat who has not been adopted. + */ + def nonAdoptedCatConversion(catId: CatId): Option[Cat] = + print(s"Non-adopted cat conversion: $catId\n") + val status = adoptionStatusDatabase.find(cat => cat.id == catId && !cat.adopted) + status.flatMap(status => catDatabase.find(_.name == status.name)) + + /** + * Implement a custom unapply method that uses nonAdoptedCatConversion and firCharacteristicValidation. + */ + object ValidCat: + def unapply(catId: CatId): Option[Cat] = + nonAdoptedCatConversion(catId).find(furCharacteristicValidation) + + + /** + * Use the custom `unapply` method. + * @param catIds the sequence of all cat identifiers + * @return `Some` of the first valid cat data or `None` if no valid cat data is found + */ + def unapplyFindFirstValidCat(catIds: Seq[CatId]): Option[Cat] = + catIds.collectFirst { + case ValidCat(cat) => cat + } + + val breedPatterns: Map[Breed, Set[Pattern]] = Map( + Siamese -> Set(Pattern.Pointed), + Persian -> Set(Pattern.SolidColor), + MaineCoon -> Set(Pattern.Tabby(Classic), Pattern.Tabby(Mackerel), Pattern.Tabby(Patched)), + Ragdoll -> Set(Pattern.Pointed, Pattern.Bicolor(Van)), + Bengal -> Set(Pattern.Spots), + Abyssinian -> Set(Pattern.Tabby(Ticked)), + Birman -> Set(Pattern.Pointed, Pattern.SolidColor), + OrientalShorthair -> Set(Pattern.SolidColor, Pattern.Pointed), + Sphynx -> Set(Pattern.SolidColor), + DevonRex -> Set(Pattern.SolidColor, Pattern.Tabby(Classic)), + ScottishFold -> Set(Pattern.SolidColor, Pattern.Tabby(Classic), Pattern.Tabby(Mackerel)), + Metis -> Set(Tabby(Classic), Tabby(Ticked), Tabby(Mackerel), Tabby(Spotted), Tabby(Patched), Pointed, + Shading(Chinchilla), Shading(Shaded), Shading(Smoke), SolidColor, + Bicolor(Tuxedo), Bicolor(Van), Tricolor(Calico), Tricolor(Tortie), Spots) + ) + + def validatePattern(cat: Cat): Boolean = + print(s"Validation of fur pattern: ${cat.name}\n") + breedPatterns(cat.breed).contains(cat.pattern) + + /** + * Now use Deconstruct idiom for the validation of the pattern of the cat + * @tparam From The type we initially operate on + * @tparam To The type of the data we want to retrieve if it's valid + */ + trait Deconstruct[From, To]: + def convert(from: From): Option[To] + + def validate(to: To): Boolean + + def unapply(from: From): Option[To] = convert(from).find(validate) + + /** + * Now validate that a coat pattern corresponds to the cat's breed by extending `Deconstruct`. + */ + object ValidPattern extends Deconstruct[CatId, Cat]: + override def convert(catId: CatId): Option[Cat] = + nonAdoptedCatConversion(catId) + + override def validate(cat: Cat): Boolean = + validatePattern(cat) + + /** + * Find the first cat with a valid pattern using the Deconstruct idiom. + * @param catIds the sequence of all cat identifiers + * @return `Some` of the first valid cat data or `None` if no valid cat data is found + */ + def findFirstCatWithValidPattern(catIds: Seq[CatId]): Option[Cat] = + catIds.collectFirst { + case ValidPattern(cat) => cat + } \ No newline at end of file diff --git a/Early Returns/Unapply/task-info.yaml b/Early Returns/Unapply/task-info.yaml index 74b2af7d..b13977e8 100644 --- a/Early Returns/Unapply/task-info.yaml +++ b/Early Returns/Unapply/task-info.yaml @@ -1,10 +1,47 @@ type: edu files: - - name: src/Task.scala - visible: true - name: test/TestSpec.scala visible: false - name: build.sbt visible: false - name: src/EarlyReturns.scala visible: true + - name: src/Pattern.scala + visible: true + - name: src/FurCharacteristic.scala + visible: true + - name: src/Color.scala + visible: true + - name: src/Breed.scala + visible: true + - name: src/Database.scala + visible: true + - name: src/Cat.scala + visible: true + - name: src/Task.scala + visible: true + placeholders: + - offset: 1285 + length: 123 + placeholder_text: /* TODO */ + - offset: 1676 + length: 150 + placeholder_text: /* TODO */ + - offset: 2016 + length: 64 + placeholder_text: /* TODO */ + - offset: 2346 + length: 59 + placeholder_text: /* TODO */ + - offset: 3479 + length: 46 + placeholder_text: /* TODO */ + - offset: 4142 + length: 30 + placeholder_text: /* TODO */ + - offset: 4227 + length: 20 + placeholder_text: /* TODO */ + - offset: 4552 + length: 63 + placeholder_text: /* TODO */ diff --git a/Early Returns/Unapply/task.md b/Early Returns/Unapply/task.md index b3019c3e..d89ff8e4 100644 --- a/Early Returns/Unapply/task.md +++ b/Early Returns/Unapply/task.md @@ -86,7 +86,7 @@ We do run the conversion twice in this case, but it is less important because of ```scala 3 object ValidUserInADifferentWay: - def otherValidation(userData: UserData): Boolean = /* check that it's a child user */ + def otherValidation(userData: UserData): Boolean = false /* check that it's a child user */ def unapply(userId: UserId): Option[UserData] = safeComplexConversion(userId).find(otherValidation) def findFirstValidUser7(userIds: Seq[UserId]): Option[UserData] = @@ -135,12 +135,18 @@ the `Deconstruct` trait while pattern matching: } ``` +### Exercise +You have noticed that the first cat with a valid fur pattern you found had already been adopted. +Now you need to include the check whether a cat is still in the shelter in the validation. +* Implement `nonAdoptedCatConversion` to only select the cats that are still up for adoption +* Copy your implementation of the `furCharacteristicValidation` function from the previous task. +* Implement your custom `unapply` method for the `ValidCat` object, and use it to write the `unapplyFindFirstValidCat` function. Validation of the fur characteristics should not be run on cats who have been adopted. +Next, you notice that there are some inaccuracies in coat patterns: no bengal cat can be of solid color! - - - - +* Implement the validation of the coat pattern using a custom `unapply` method. +* Use `ValidPattern` object that extends the `Deconstruct` trait. +* Use the custom `unapply` method in the `findFirstCatWithValidPattern` function. diff --git a/Early Returns/Unapply/test/TestSpec.scala b/Early Returns/Unapply/test/TestSpec.scala index f73ac3d4..8f675c89 100644 --- a/Early Returns/Unapply/test/TestSpec.scala +++ b/Early Returns/Unapply/test/TestSpec.scala @@ -1,8 +1,31 @@ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite +import Task._ +import Database._ -class TestSpec extends FunSuite { - //TODO: implement your test here - test("First test") { - assert(false, "Tests not implemented for the task") +class TaskSpec extends AnyFunSuite { + def runTest(expectedCatId: Int, validationMsg: String, findCat: Seq[CatId] => Option[Cat]): Unit = + val stream = new java.io.ByteArrayOutputStream() + + def logMessage(id: CatId) = + val validationMessage = if !adoptionStatusDatabase(id-1).adopted then s"\n$validationMsg: ${catDatabase(id - 1).name}" else "" + s"Non-adopted cat conversion: $id$validationMessage" + + Console.withOut(stream) { + val cat = findCat(identifiers) + + assert(cat === Some(catDatabase(expectedCatId - 1))) + + val expected = (1 to expectedCatId).map(id => logMessage(id)).mkString("\n").trim + val actual = stream.toString().replaceAll("\r\n", "\n").trim + + assert(actual == expected) + } + + test("Unapply Find First Valid Cat returns the valid cat and doesn't traverse the whole collection. Validation is only run when a cat is in the database and has not been adopted") { + runTest(13, "Validation of fur characteristics", unapplyFindFirstValidCat) + } + + test("Find First Cat With Valid Pattern returns the valid cat and doesn't traverse the whole collection. Validation is only run when a cat is in the database and has not been adopted") { + runTest(4, "Validation of fur pattern", findFirstCatWithValidPattern) } } From 3151ef31bea3947813f064ec3b026f6074c7aeb6 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 6 May 2024 19:29:51 +0300 Subject: [PATCH 04/65] Update task.md language checked --- Early Returns/Baby Steps/task.md | 62 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/Early Returns/Baby Steps/task.md b/Early Returns/Baby Steps/task.md index 0cd46dfe..0b2c0ff0 100644 --- a/Early Returns/Baby Steps/task.md +++ b/Early Returns/Baby Steps/task.md @@ -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: @@ -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 /** @@ -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 /** @@ -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 { @@ -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. \ No newline at end of file +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. From 62615db0e97e6081bee6e4118d8eb74480944cc1 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 7 May 2024 14:08:37 +0300 Subject: [PATCH 05/65] Update task.md language checked --- Early Returns/Breaking Boundaries/task.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Early Returns/Breaking Boundaries/task.md b/Early Returns/Breaking Boundaries/task.md index 8d4a5ee7..3625cd19 100644 --- a/Early Returns/Breaking Boundaries/task.md +++ b/Early Returns/Breaking Boundaries/task.md @@ -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. @@ -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. From 8e40b40a0febfa925e07a1997504bda179b51908 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 7 May 2024 14:24:18 +0300 Subject: [PATCH 06/65] Update task.md language checked --- .../Lazy Collection to the Rescue/task.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Early Returns/Lazy Collection to the Rescue/task.md b/Early Returns/Lazy Collection to the Rescue/task.md index fe97ce01..817e471d 100644 --- a/Early Returns/Lazy Collection to the Rescue/task.md +++ b/Early Returns/Lazy Collection to the Rescue/task.md @@ -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 @@ -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. From 9563f2ae51b1e612bc97cad4a07f6930c6c711eb Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 7 May 2024 14:48:10 +0300 Subject: [PATCH 07/65] Update task.md language checked --- Early Returns/The Problem/task.md | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Early Returns/The Problem/task.md b/Early Returns/The Problem/task.md index 8b21e4fe..71b8e6d7 100644 --- a/Early Returns/The Problem/task.md +++ b/Early Returns/The Problem/task.md @@ -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) { @@ -37,14 +37,14 @@ Bar findFirstValidBar(Collection 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 = ... @@ -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. From 62888a7a0c06138756a84b91284fcd96865aa4ab Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 7 May 2024 15:21:11 +0300 Subject: [PATCH 08/65] Update task.md language checked --- Early Returns/Unapply/task.md | 48 +++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Early Returns/Unapply/task.md b/Early Returns/Unapply/task.md index d89ff8e4..45ba11e2 100644 --- a/Early Returns/Unapply/task.md +++ b/Early Returns/Unapply/task.md @@ -1,8 +1,8 @@ ## Unapply -Unapply methods form a basis of pattern matching. -Its goal is to extract data compacted in objects. -We can create a custom extractor object for user data validation with the suitable unapply method, for example: +Unapply methods form the basis of pattern matching. +Their goal is to extract data encapsulated in objects. +We can create a custom extractor object for user data validation with a suitable unapply method, for example: ```scala 3 object ValidUser: @@ -25,11 +25,11 @@ As a result, we get this short definition of our search function. } ``` -It's at this point that an observant reader is likely to protest. +At this point, an observant reader is likely to protest. This solution is twice as long as the imperative one we started with, and it doesn't seem to do anything extra! One thing to notice here is that the imperative implementation is only concerned with the "happy" path. -What if there are no records in the database for some of the user identifiers? -The conversion function becomes partial, and, being true to the functional method, we need to return optional value: +But what if there are no records in the database for some of the user identifiers? +The conversion function becomes partial, and, adhering to the functional method, we need to return an optional value: ```scala 3 /** @@ -39,8 +39,8 @@ The conversion function becomes partial, and, being true to the functional metho ``` The partiality of the conversion will unavoidably complicate the imperative search function. -The code still has the same shape, but it has to go through additional hoops to accommodate partiality. -Note that every time a new complication arises in the business logic, it has to be reflected inside +The code still has the same shape, but it has to go through additional loops to accommodate partiality. +Note that every time a new complication arises in the business logic, it has to be reflected within the `for` loop. ```scala 3 @@ -74,15 +74,15 @@ search function stays the same. } ``` -In general, there might be several ways in which user data might be valid. +In general, there might be several ways in which user data could be valid. Imagine that there is a user who doesn't have an email. -In this case `complexValidation` returns `false`, but the user may still be valid. +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. +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. -To do it, we can create a different extractor object with its own `unapply` and pattern match against it -if the first validation failed. -We do run the conversion twice in this case, but it is less important because of how rare this case is. +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. ```scala 3 object ValidUserInADifferentWay: @@ -98,10 +98,10 @@ We do run the conversion twice in this case, but it is less important because of Both extractor objects work in the same way. They run a conversion method, which may or may not succeed. -If conversion succeeds, its result is validated and returned when valid. -All this is done with the `unapply` method whose implementation stays the same regardless of the other methods. +If the conversion succeeds, its result is validated and returned when it is valid. +All of this is done with the `unapply` method, whose implementation stays the same regardless of the other methods. This forms a nice framework which can be abstracted as a trait we call `Deconstruct`. -It has the `unapply` method which calls two abstract methods `convert` and `validate` that operate on generic +It has the `unapply` method that calls two abstract methods, `convert` and `validate`, which operate on generic types `From` and `To`. ```scala 3 @@ -126,7 +126,7 @@ It uses `safeComplexConversion` and `complexValidation` respectively. ``` Finally, the search function stays the same, but now it uses the `unapply` method defined in -the `Deconstruct` trait while pattern matching: +the `Deconstruct` trait during pattern matching: ```scala 3 def findFirstValidUser8(userIds: Seq[UserId]): Option[UserData] = @@ -137,16 +137,16 @@ the `Deconstruct` trait while pattern matching: ### Exercise -You have noticed that the first cat with a valid fur pattern you found had already been adopted. -Now you need to include the check whether a cat is still in the shelter in the validation. +You have noticed that the first cat found with a valid fur pattern has already been adopted. +Now you need to include a check in the validation to ensure that the cat is still in the shelter. -* Implement `nonAdoptedCatConversion` to only select the cats that are still up for adoption +* Implement `nonAdoptedCatConversion` to only select cats that are still up for adoption. * Copy your implementation of the `furCharacteristicValidation` function from the previous task. -* Implement your custom `unapply` method for the `ValidCat` object, and use it to write the `unapplyFindFirstValidCat` function. Validation of the fur characteristics should not be run on cats who have been adopted. +* Implement your custom `unapply` method for the `ValidCat` object, and use it to write the `unapplyFindFirstValidCat` function. The validation of fur characteristics should not be run on cats who have been adopted. -Next, you notice that there are some inaccuracies in coat patterns: no bengal cat can be of solid color! +Next, you notice that there are some inaccuracies in the coat patterns: no Bengal cat can be of a solid color! * Implement the validation of the coat pattern using a custom `unapply` method. -* Use `ValidPattern` object that extends the `Deconstruct` trait. +* Use the `ValidPattern` object that extends the `Deconstruct` trait. * Use the custom `unapply` method in the `findFirstCatWithValidPattern` function. From 499ebf5323a1ac8be560db2b0a7e35778c6c4ea2 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 13 May 2024 14:43:09 +0300 Subject: [PATCH 09/65] Update task.md language checked --- .../Pure vs Impure Functions/task.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Expressions over Statements/Pure vs Impure Functions/task.md b/Expressions over Statements/Pure vs Impure Functions/task.md index 4938b117..52942bea 100644 --- a/Expressions over Statements/Pure vs Impure Functions/task.md +++ b/Expressions over Statements/Pure vs Impure Functions/task.md @@ -25,13 +25,13 @@ Its performance should neither be influenced by the external world nor impact it You might argue that pure functions seem entirely useless. If they cannot interact with the outer world or mutate anything, how is it possible to derive any value from them? -Why even use pure functions? -The fact is, they conform much better than impure counterparts. +Why should we even use pure functions? +The fact is, they conform much better than their impure counterparts. Since there are no hidden interactions, it's much easier to verify that your pure function does what it is supposed to do and nothing more. Moreover, they are much easier to test, as you do not need to mock a database if the function never interacts with one. -Some programming languages, such as Haskell, restrict impurity and reflect any side effects in types. +Some programming languages, such as Haskell, limit impurity and reflect any side effects in their types. However, it can be quite restricting and is not an approach utilized in Scala. The idiomatic method is to write your code in such a way that the majority of it is pure, and impurity is only used where it is absolutely necessary, similar to what we did with mutable data. @@ -49,5 +49,5 @@ def g(x: Int): Int = ### Exercise -Implement the pure function `calculateAndLogPure` which does the same thing as `calculateAndLogImpure`, but without -using global variable. +Implement the pure function `calculateAndLogPure`, which does the same thing as `calculateAndLogImpure`, but without +using a global variable. From e4cc0de82baaac4ec8c3480fcf1c03f10d8fe979 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 13 May 2024 22:30:42 +0300 Subject: [PATCH 10/65] Update task.md language checked --- .../Tail Recursion/task.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Expressions over Statements/Tail Recursion/task.md b/Expressions over Statements/Tail Recursion/task.md index 9127a62b..b5cc6834 100644 --- a/Expressions over Statements/Tail Recursion/task.md +++ b/Expressions over Statements/Tail Recursion/task.md @@ -7,7 +7,7 @@ Each time a function is called, some information regarding the call is placed on allocated. This information is kept there until all computations within the function are completed, after which the stack is deallocated (the information about the function call is removed from the stack), and the computed value is returned. -If a function calls another function, the stack is allocated again before deallocating the previous function's call. What is worse, we wait until the inner call is complete, its stack frame is deallocated, and its value returned to compute +If a function calls another function, the stack is allocated again before deallocating the previous function's call. What is worse, we wait until the inner call is complete, its stack frame is deallocated, and its value returned before we can compute the result of the caller function. This is especially significant for recursive functions because the depth of the call stack can be astronomical. @@ -37,7 +37,7 @@ Calling `factorial` with a large enough argument (like `10000` on my computer) r computation doesn't produce any result. Don't get discouraged! -There is a well-known optimisation technique capable of mitigating this issue. +There is a well-known optimization technique capable of mitigating this issue. It involves rewriting your recursive function into a tail-recursive form. In this form, the recursive call should be the last operation the function performs. For example, `factorial` can be rewritten as follows: @@ -50,13 +50,13 @@ def factorial(n: BigInt): BigInt = go(n, 1) ``` -We add a new parameter `accumulator` to the recursive function where we keep track of the partially computed +We add a new parameter `accumulator` to the recursive function to keep track of the partially computed multiplication. -Notice that the recursive call to `go` is the last operation that happens in the `else` branch of the `if` condition. +Notice that the recursive call to `go` is the last operation in the `else` branch of the `if` condition. Whatever value the recursive call returns is simply returned by the caller. There is no reason to allocate any stack frames because nothing is awaiting the result of the recursive call to enable further computation. -Smart enough compilers (and the Scala compiler is one of them) is capable to optimize away the unnecessary stack +Smart enough compilers (and the Scala compiler is one of them) can optimize away the unnecessary stack allocations in this case. Go ahead and try to find an `n` such that the tail-recursive `factorial` results in a stack overflow. Unless something goes horribly wrong, you should not be able to find such an `n`. @@ -64,12 +64,12 @@ Unless something goes horribly wrong, you should not be able to find such an `n` By the way, do you remember the key searching function we implemented in the previous task? Have you wondered how we got away not keeping track of a collection of boxes to look through? The trick is that the stack replaces that collection. -All the boxes to be considered are somewhere on the stack, patiently waiting their turn. +All the boxes to be considered are somewhere on the stack, patiently awaiting their turn. Is there a way we can make that function tail-recursive? Yes, of course, there is! Similar to the `factorial` function, we can create a helper function `go` with an extra parameter `boxesToLookIn` -to keep track of the boxes to search the key in. +to keep track of the boxes to search for the key in. This way, we can ensure that `go` is tail-recursive, i.e., either returns a value or calls itself as its final step. ```scala 3 @@ -90,10 +90,10 @@ def lookForKey(box: Box): Option[Key] = In Scala, there is a way to ensure that your function is tail-recursive: the `@tailrec` annotation from `scala.annotation.tailrec`. It checks if your implementation is tail-recursive and triggers a compiler error if it is not. -We recommend using this annotation to ensure that the compiler is capable of optimizing your code, even through its +We recommend using this annotation to ensure that the compiler is capable of optimizing your code, even through future changes. ### Exercise Implement tail-recursive functions for reversing a list and finding the sum of digits in a non-negative number. -We annotated the helper functions with `@tailrec` so that the compiler can verify this property for us. +We've annotated the helper functions with `@tailrec` so that the compiler can verify this property for us. From d41b239ae739da90ed1af8b5826356b3ce22c7ed Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 13 May 2024 22:48:56 +0300 Subject: [PATCH 11/65] Update task.md language checked --- Expressions over Statements/What is an Expression/task.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Expressions over Statements/What is an Expression/task.md b/Expressions over Statements/What is an Expression/task.md index 1d8831fd..8f684864 100644 --- a/Expressions over Statements/What is an Expression/task.md +++ b/Expressions over Statements/What is an Expression/task.md @@ -57,7 +57,7 @@ Depending on the condition, we execute one of the two `println` statements. Notice that no value is returned. Instead, everything the function does is a side effect of printing to the console. This style is not considered idiomatic in Scala. -Instead, it's preferably for a function to return a string value, which is then printed, like so: +Instead, it's preferable for a function to return a string value, which is then printed, like so: ```scala 3 def even(number: Int): String = { @@ -72,7 +72,7 @@ def main(): Unit = { } ``` -This way, you separate the logic of computing the values from outputting them. +This way, you separate the logic of computing values from outputting them. It also makes your code more readable. ### Exercise From 79ae107cdd0cd81391827842c4c39793764f2f01 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 13 May 2024 23:05:33 +0300 Subject: [PATCH 12/65] Update task.md language checked --- Functions as Data/anonymous_functions/task.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Functions as Data/anonymous_functions/task.md b/Functions as Data/anonymous_functions/task.md index ae71bf1c..67dbd6b5 100644 --- a/Functions as Data/anonymous_functions/task.md +++ b/Functions as Data/anonymous_functions/task.md @@ -1,7 +1,7 @@ # Anonymous functions -An anonymous function is a function that, quite literally, does not have a name. I -t is defined only by its arguments list and computations. +An anonymous function is a function that, quite literally, does not have a name. It +is defined only by its argument list and computations. Anonymous functions are also known as lambda functions, or simply lambdas. Anonymous functions are particularly useful when we need to pass a function as an argument to another function, or when we want to create a function that is only used once and is not worth defining separately. @@ -21,7 +21,7 @@ To do that, we use the `map` method. We define an anonymous function `x => x * 2` and give it to the `map` method as its only argument. The `map` method applies this anonymous function to each element of `numbers` and returns a new list, which we call `doubled`, containing the doubled values. -Anonymous functions can access variables that are in scope at their definition. +Anonymous functions can access variables that are within scope at the time of their definition. Consider the `multiplyList` function, which multiplies every number in a list by a `multiplier`. The parameter `multiplier` can be used inside `map` without any issues. @@ -32,7 +32,7 @@ def multiplyList(multiplier: Int, numbers: List[Int]): List[Int] = ``` -When a parameter is only used once in the anonymous function, Scala allows omitting the argument's name by using `_` instead. +When a parameter is used only once in the anonymous function, Scala allows omitting the argument's name and using `_` instead. However, note that if a parameter is used multiple times, you must use names to avoid confusion. The Scala compiler will report an error if you fail to adhere to this rule. @@ -44,15 +44,15 @@ def multiplyPairs(numbers: List[(Int, Int)]): List[Int] = numbers.map((x, y) => // Scala associates the wildcards with the parameters in the order they are passed. def multiplyPairs1(numbers: List[(Int, Int)]): List[Int] = numbers.map(_ * _) -// We compute a square of each element of the list using an anonymous function. +// We compute the square of each element in the list using an anonymous function. def squareList(numbers: List[Int]): List[Int] = numbers.map(x => x * x) // In this case, omitting parameters' names is disallowed. -// You can see how it can be confusing, if you compare it with `multiplyPairs1`. +// You can see how it can be confusing if you compare it with `multiplyPairs1`. def squareList1(numbers: List[Int]): List[Int] = numbers.map(_ * _) ``` ## Exercise -Implement the `multiplyAndOffsetList` function that multiplies and offsets each element of the list. +Implement the `multiplyAndOffsetList` function, which multiplies and offsets each element in the list. Use `map` and an anonymous function. From b4a0c6699191859b96593197478f938a6c45b810 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 13 May 2024 23:13:39 +0300 Subject: [PATCH 13/65] Update task.md language checked --- Functions as Data/filter/task.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Functions as Data/filter/task.md b/Functions as Data/filter/task.md index 454fa542..7811e032 100644 --- a/Functions as Data/filter/task.md +++ b/Functions as Data/filter/task.md @@ -20,7 +20,7 @@ case class Cat(name: String, color: Color) // Let’s import the Color enum values for better readability import Color._ -// We create four cats, two black, one white, and one ginger +// We create four cats: two black, one white, and one ginger val felix = Cat("Felix", Black) val snowball = Cat("Snowball", White) val garfield = Cat("Garfield", Ginger) @@ -48,6 +48,6 @@ There are multiple cats available, and you wish to adopt a cat with one of the f * The cat is calico. * The cat is fluffy. -* The cat's breed is Abyssinian. +* The cat is of the Abyssinian breed. -To simplify decision making, you first identify all 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 which possess at least one of the characteristics above. Your task is to implement the necessary functions and then apply the filter. From 31d0ecfddcf2cc452cfd0874a90b4038d5be546d Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 14 May 2024 14:32:08 +0300 Subject: [PATCH 14/65] Update task.md language checked --- Functions as Data/foldLeft/task.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Functions as Data/foldLeft/task.md b/Functions as Data/foldLeft/task.md index 515921f3..6826ba27 100644 --- a/Functions as Data/foldLeft/task.md +++ b/Functions as Data/foldLeft/task.md @@ -2,23 +2,23 @@ `def foldLeft[B](acc: B)(f: (B, A) => B): B` -The `foldLeft` method is another method in Scala collections that can be percieved as a generalized version of `map`, but generalized differently than `flatMap`. +The `foldLeft` method is another method in Scala collections that can be perceived as a generalized version of `map`, but generalized differently than `flatMap`. Let's say that we call `foldLeft` on a collection of elements of type `A`. `foldLeft` takes two arguments: the initial "accumulator" of type `B` (usually different from `A`) and a function `f`, which again takes two arguments: the accumulator (of type `B`) and the element from the original collection (of type `A`). `foldLeft` starts its work by taking the initial accumulator and the first element of the original collection and assigning them to `f`. The `f` function uses these two to produce a new version of the accumulator — i.e., a new value of type `B`. -This new value, the new accumulator, is again provided to `f`, this time together with the second element in the collection. -The process repeats itself until all elements of the original collection have been iterated over. +This new value, the updated accumulator, is again provided to `f`, this time together with the second element in the collection. +The process repeats until all elements of the original collection have been iterated over. The final result of `foldLeft` is the accumulator value after processing the last element of the original collection. -The "fold" part of the `foldLeft` method's name derives from the idea that `foldLeft`'s operation might be viewed as "folding" a collection of elements, one into another, until ultimately, a single value — the final result. -The suffix "left" is there to indicate that in the case of ordered collections, we are proceeding from the beginning of the collection (left) to its end (right). +The "fold" part of the `foldLeft` method's name derives from the idea that `foldLeft`'s operation might be viewed as "folding" a collection of elements, one into another, until ultimately, a single value — the final result — is produced. +The suffix "left" indicates that in the case of ordered collections, we are proceeding from the beginning of the collection (left) to its end (right). There is also `foldRight`, which works in the reverse direction. Let's see how we can implement a popular coding exercise, *FizzBuzz*, using `foldLeft`. In *FizzBuzz*, we are supposed to print out a sequence of numbers from 1 to a given number (let's say 100). However, each time the number we are to print out is divisible by 3, we print "Fizz"; if it's divisible by 5, we print "Buzz"; and if it's divisible by 15, we print "FizzBuzz". -Here is how we can accomplish this with foldLeft in Scala 3: +Here is how we can accomplish this with `foldLeft` in Scala 3: ```scala def fizzBuzz(n: Int): Int | String = n match @@ -36,9 +36,9 @@ val fizzBuzzList = numbers.foldLeft[List[Int | String]](Nil) { (acc, n) => acc : println(fizzBuzzList) ``` -First, we write the `fizzBuzz` method, which takes an `Int` and returns either an `Int` (the number that it received) or a `String: "Fizz", "Buzz", or "FizzBuzz". +First, we write the `fizzBuzz` method, which takes an `Int` and returns either an `Int` (the number that it received) or a `String`: "Fizz", "Buzz", or "FizzBuzz". With the introduction of union types in Scala 3, -we can declare that our method can return any of two or more unrelated types, but it will definitely be one of them. +we can declare that our method can return any one of two or more unrelated types. However, it is guaranteed that the return will be one of them. Next, we create a range of numbers from 1 to 100 using `1 to 100`. @@ -47,7 +47,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", replaceing 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. From d33f34834c5411455fa54832736da64bc336fef8 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 14 May 2024 14:38:34 +0300 Subject: [PATCH 15/65] Update task.md language checked --- Functions as Data/foreach/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Functions as Data/foreach/task.md b/Functions as Data/foreach/task.md index 96d14d6a..32d1ad7e 100644 --- a/Functions as Data/foreach/task.md +++ b/Functions as Data/foreach/task.md @@ -8,7 +8,7 @@ We assume that `f` performs side effects (we can ignore the `U` result type of t You can think of the `foreach` method as a simple for-loop that iterates over a collection of elements without altering them. Note that in functional programming, we try to avoid side effects. -In this course, you will learn how to achieve the same results functionally, but in the beginning, `foreach` can be helpful display computing results, debug, and experiment. +In this course, you will learn how to achieve the same results functionally, but in the beginning, `foreach` can be helpful for displaying computing results, debugging, and experimentation. In the following example, we will use `foreach` to print out the name and color of each of our four cats. From 4e1a1b10ce642cadbd357b67b73a5b78cfdb4b16 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 14 May 2024 14:52:14 +0300 Subject: [PATCH 16/65] Update task.md language checked --- .../functions_returning_functions/task.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Functions as Data/functions_returning_functions/task.md b/Functions as Data/functions_returning_functions/task.md index 4cda4c90..5140fe70 100644 --- a/Functions as Data/functions_returning_functions/task.md +++ b/Functions as Data/functions_returning_functions/task.md @@ -17,7 +17,7 @@ val calc = new CalculatorPlusN(3) calc.add(1 , 2) ``` -Now, instead of having a class that stores this additional number `n`, we can create and return the adder function to achieve the same result: +Now, instead of having a class that stores this additional number `n`, we can create and return the `adder` function to achieve the same result: ```scala // Define a function that takes a fixed number and returns a new function that adds it to its input @@ -30,12 +30,12 @@ val add = addFixedNumber(3) add(1, 2) ``` -In the above example, we define a function `addFixedNumber` that takes an integer `n` and returns a new function that takes two integers, `x` and `y`, and returns the sum of `n` and `x` and `y`. +In the above example, we define a function `addFixedNumber` that takes an integer `n` and returns a new function, which takes two integers, `x` and `y`, and returns the sum of `n`, `x`, and `y`. Note the return type of `addFixedNumber` — it's a function type `(Int, Int) => Int`. -Then, we define the new function adder inside `addFixedNumber`, which captures the value of `n` and adds it to its own two arguments, `x` and `y`. +Then, we define the new function, `adder`, inside `addFixedNumber`, which captures the value of `n` and adds it to its own two arguments, `x` and `y`. The `adder` function is then returned as the result of `addFixedNumber`. -We then construct a specialized function add by calling `addFixedNumber(n: Int)` with `n` equal to `3`. +We then construct a specialized function `add` by calling `addFixedNumber(n: Int)` with `n` equal to `3`. Now, we can call `add` on any two integers; as a result, we will get the sum of these integers plus `3`. Scala provides special syntax for functions returning functions, as shown below: @@ -49,7 +49,7 @@ val add = addFixedNumber(3) The first argument of the function `addFixedNumber` is enclosed within its own set of parentheses, while the second and third arguments are enclosed within another pair of parentheses. The function `addFixedNumber` can then be supplied with only the first argument, which creates a function expecting the next two arguments: `x` and `y`. -You can also call the function with all three arguments, but they should be enclosed in separate parentheses: `addFixedNumber1(3)(4, 5)` instead of `addFixedNumber(3,4,5)`. +You can also call the function with all three arguments, but they should be enclosed in separate parentheses: `addFixedNumber1(3)(4, 5)` rather than `addFixedNumber(3,4,5)`. Notice that you cannot pass only two arguments into the function written in this syntax: `addFixedNumber1(3)(4)` is not allowed. From baaaaa0b81bef753f81d23aa76f4269a7e9a2c2e Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 14 May 2024 15:05:38 +0300 Subject: [PATCH 17/65] Update task.md language checked --- Functions as Data/map/task.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Functions as Data/map/task.md b/Functions as Data/map/task.md index 1c5a7536..cccf643c 100644 --- a/Functions as Data/map/task.md +++ b/Functions as Data/map/task.md @@ -3,11 +3,11 @@ `def map[B](f: A => B): Iterable[B]` 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` and not its side effects. +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`. -In a special case, `B` can be the same as `A`, so for example, we 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 can also, for example, take a collection of cats and create a collection of cars with colors that match the colors of our cats. +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. ```scala // We define the Color enum @@ -46,6 +46,6 @@ Therefore, instead of a `Set`, we need a collection that allows multiple identic ## Exercise -In functional programming, we usually separate performing side effects from computations. +In functional programming, we usually separate side effects from computations. For example, if we want to print all fur characteristics of a cat, we first transform each characteristic into a `String`, and then output each one in a separate step. Implement the `furCharacteristicsDescription` function, which completes this transformation using `map`. From a1b355a6db6147022cc18253e39f37027d7f2a4c Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 14 May 2024 15:17:03 +0300 Subject: [PATCH 18/65] Update task.md language checked --- Functions as Data/partial_fucntion_application/task.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Functions as Data/partial_fucntion_application/task.md b/Functions as Data/partial_fucntion_application/task.md index 97adb5fe..5cdafd98 100644 --- a/Functions as Data/partial_fucntion_application/task.md +++ b/Functions as Data/partial_fucntion_application/task.md @@ -1,11 +1,11 @@ # Partial function application Returning functions from functions is related to, but not the same as, [partial application](https://en.wikipedia.org/wiki/Partial_application). -The former allows you create functions that behave as though they have a "hidden" list of arguments that you provide at the moment of creation, rather than at the moment of usage. -Each function returns a new function that accepts the next argument until all arguments are accounted for, and the final function returns the result. +The former allows you to create functions that behave as though they have a "hidden" list of arguments provided at the moment of creation, rather than at the moment of use. +Each function returns a new function that accepts the next argument until all arguments have been processed. The final function then returns the result. -On the other hand, partial function application refers the process of assigning fixed values to some of the arguments of a function and returning a new function that only takes the remaining arguments. +On the other hand, partial function application refers to the process of assigning fixed values to some of a function's arguments and returning a new function that only takes the remaining arguments. The new function is a specialized version of the original function with some arguments already provided. -This technique enables code reuse — we can write a more generic function and then construct its specialized versions to use in various contexts. +This technique enables code reuse — we can write a more generic function and then construct its specialized versions for use in various contexts. Here's an example: ```scala @@ -24,6 +24,6 @@ Finally, we call `add3` with only two arguments, obtaining the same result as wi ## Exercise -Implement a function `filterList` that can then be partially applied. +Implement a function `filterList`, which can then be partially applied. You can use the `filter` method in the implementation. From 22f8e78c9923353f893e1e3437fd7f1dd9e6b97b Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Wed, 15 May 2024 15:17:18 +0300 Subject: [PATCH 19/65] Update task.md language checked --- .../passing_functions_as_arguments/task.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Functions as Data/passing_functions_as_arguments/task.md b/Functions as Data/passing_functions_as_arguments/task.md index ababb5df..addc8f8b 100644 --- a/Functions as Data/passing_functions_as_arguments/task.md +++ b/Functions as Data/passing_functions_as_arguments/task.md @@ -3,7 +3,7 @@ We can pass a named function as an argument to another function just as we would pass any other value. This is useful, for example, when we want to manipulate data in a collection. There are many methods in Scala collections classes that operate by accepting a function as an argument and applying it in some way to each element of the collection. -In the previous chapter, we saw how we can use the map method on a sequence of numbers to double them. +In the previous chapter, we saw how we can use the `map` method on a sequence of numbers to double them. Now let's try something different. Imagine that you have a bag of cats with different colors, and you want to separate out only the black cats. @@ -25,11 +25,11 @@ val bagOfBlackCats = bagOfCats.filter(cat => cat.color == Color.Black) ``` In Scala 3, we can use enums to define colors. -Then, we create a class `Cat`, which has a value for the color of the cat. Next, we create a "bag" of cats, which is a set with three cats: one black, one white, and one ginger. -Finally, we use the `filter` method and provide it with an anonymous function as an argument. This function takes an argument of the class `Cat` and will return `true` if the color of the cat is black. -The `filter` method will apply this function to each cat in the original set and create a new set with only those cats for which the function returns `true`. +Then, we create a class `Cat`, which includes a value for the color of the cat. Next, we create a "bag" of cats, which is a set containing three cats: one black, one white, and one ginger. +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 with a named function just as well. +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 @@ -42,4 +42,4 @@ So far, you've seen examples of how this is done with `map` and `filter` — two ## Exercise -Implement a function to check whether the cat is white or ginger and pass it as an argument to `filter` to create a bag of white or ginger cats. +Implement a function to check whether the cat is white or ginger and pass it as an argument to `filter` to create a bag containing only white or ginger cats. From b8c3469110f19d850d33264de1dc63a7c52f7c1d Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Wed, 15 May 2024 15:28:45 +0300 Subject: [PATCH 20/65] Update task.md language checked --- Functions as Data/scala_collections_overview/task.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Functions as Data/scala_collections_overview/task.md b/Functions as Data/scala_collections_overview/task.md index 3899fd59..f0521b2b 100644 --- a/Functions as Data/scala_collections_overview/task.md +++ b/Functions as Data/scala_collections_overview/task.md @@ -10,11 +10,11 @@ By default, Scala encourages the use of immutable collections because they are s Here's an overview of the main traits and classes: 1. `Iterable`: All collections that can be traversed in a linear sequence extend `Iterable`. It provides methods like `iterator`, `map`, `flatMap`, `filter`, and others, which we will discuss shortly. -2. `Seq`: This trait represents sequences, i.e., ordered collections of elements. It extends `Iterable` and provides methods like `apply(index: Int): T` (which allows you access an element at a specific index) and `indexOf(element: T): Int` (which returns the index of the first occurrence in the sequence that matches the provided element, or -1 if the element can't be found). Some essential classes implementing the `Seq` trait include `List`, `Array`, `Vector`, and `Queue`. -3. `Set`: Sets are unordered collections of unique elements. It extends Iterable but not `Seq` — you can't assign fixed indices to its elements. The most common implementation of `Set` is `HashSet`. -4. `Map`: A map is a collection of key-value pairs. It extends Iterable and provides methods like `get`, `keys`, `values`, `updated`, and more. It's unordered, similar to `Set`. The most common implementation of `Map` is `HashMap`. +2. `Seq`: This trait represents sequences, i.e., ordered collections of elements. It extends `Iterable` and provides methods like `apply(index: Int): T` (which allows you to access an element at a specific index) and `indexOf(element: T): Int` (which returns the index of the first occurrence in the sequence that matches the provided element, or -1 if the element can't be found). Some essential classes implementing the `Seq` trait include `List`, `Array`, `Vector`, and `Queue`. +3. `Set`: Sets are unordered collections of unique elements. It extends `Iterable` but not `Seq` — you can't assign fixed indices to its elements. The most common implementation of `Set` is `HashSet`. +4. `Map`: A map is a collection of key-value pairs. It extends `Iterable` and provides methods like `get`, `keys`, `values`, `updated`, and more. It's unordered, similar to `Set`. The most common implementation of `Map` is `HashMap`. We will now quickly review some of the most frequently used methods of Scala collections: `filter`, `find`, `foreach`, `map`, `flatMap`, and `foldLeft`. In each case, you will see a code example and be asked to do an exercise using the given method. -Please note that many other methods exist. We encourage you to consult the [Scala collections documentation](https://www.scala-lang.org/api/current/scala/collection/index.html) and browse through them. Being aware of their existence and realizing that you can use them instead of constructing some logic yourself may save you a substantial amount of effort. +Please note that there are many other methods available. We encourage you to consult the [Scala collections documentation](https://www.scala-lang.org/api/current/scala/collection/index.html) and browse through them. Being aware of their existence and realizing that you can use them instead of constructing your own logic may save a substantial amount of effort. From 21b095a873d4e30e5b5e86e2bbc4011d7178f409 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Wed, 15 May 2024 15:55:34 +0300 Subject: [PATCH 21/65] Update task.md language checked --- .../total_and_partial_functions/task.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Functions as Data/total_and_partial_functions/task.md b/Functions as Data/total_and_partial_functions/task.md index e7856a11..bf4cafe1 100644 --- a/Functions as Data/total_and_partial_functions/task.md +++ b/Functions as Data/total_and_partial_functions/task.md @@ -58,15 +58,15 @@ val blackCats: Seq[Cat] = animals.collect { } ``` In this example, we first create an enum `Color` with three values: `Black`, `White`, and `Ginger`. -We define a trait, `Animal`, with two abstract methods: `name` and `color`. -We create case classes, `Cat` and `Dog`, that extend the `Animal` trait, and override the name and color methods with respective values. +We define a trait `Animal` with two abstract methods: `name` and `color`. +We create case classes `Cat` and `Dog` that extend the `Animal` trait, and override the `name` and `color` methods with respective values. Then, we create three instances of `Cat` (two black and one ginger) and two instances of `Dog` (one black and one white). -We consolidate them all into a sequence of type `Seq[Animal]`. +We consolidate all these instances into a sequence of type `Seq[Animal]`. Ultimately, we use the `collect` method on the sequence to create a new sequence containing only black cats. -The collect method applies a partial function to the original collection and constructs a new collection with only the elements for which the partial function is defined. -You can perceive it as combibibg the filter and map methods. -In the above example, we provide collect with the following partial function: +The `collect` method applies a partial function to the original collection and constructs a new collection containing only the elements for which the partial function is defined. +You can perceive it as the combination of the `filter` and `map` methods. +In the above example, we provide `collect` with the following partial function: ```scala case cat: Cat if cat.color == Black => cat @@ -74,8 +74,8 @@ case cat: Cat if cat.color == Black => cat The `case` keyword at the beginning tells us that the function will provide a valid result only in the following case: the input value needs to be of the type `Cat` (not just any `Animal` from our original sequence), and the color of that cat needs to be `Black`. -If these conditions are met, the function will return the cat, however, as an instance of the type `Cat`, not just `Animal`. -Thanks to this, we can specify that the new collection created by the collect method is a sequence of the type `Seq[Cat]`. +If these conditions are met, the function will return the cat, but as an instance of the type `Cat`, not just `Animal`. +As a result, we can specify that the new collection created by the `collect` method is a sequence of the type `Seq[Cat]`. ## Exercise From 6a0824267c2be8b79df4b0b91c930e744780773b Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Wed, 15 May 2024 18:28:07 +0300 Subject: [PATCH 22/65] Update task.md language checked --- Functions as Data/what_is_a_function/task.md | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Functions as Data/what_is_a_function/task.md b/Functions as Data/what_is_a_function/task.md index 34ddb05c..1f08a673 100644 --- a/Functions as Data/what_is_a_function/task.md +++ b/Functions as Data/what_is_a_function/task.md @@ -1,12 +1,12 @@ # What is a function? A function is a standalone block of code that takes arguments, performs some calculations, and returns a result. -It may or may not have side effects; that is, it may have access to the data in the program, and should the data be modifiable, the function might alter it. +It may or may not have side effects; that is, it may access the data in the program, and if the data is modifiable, the function might alter it. If it doesn't — meaning, if the function operates solely on its arguments — we state that the function is pure. -In functional programming, we use pure functions whenever possible, although this rule does have important exceptions, -which we will discuss them later. +In functional programming, we use pure functions whenever possible, although this rule does have important exceptions, +which we will discuss later. The main difference between a function and a method is that a method is associated with a class or an object. -On the other hand, a function is treated just like any other value in the program: it can be created in any place in the code, passed as an argument, returned from another function or method, etc. +On the other hand, a function is treated just like any other value in the program: it can be created anywhere in the code, passed as an argument, returned from another function or method, etc. Consider the following code: ```Scala @@ -26,18 +26,18 @@ class Calculator: Both `add` functions take two input parameters, `x` and `y`, perform a pure computation of adding them together, and return the result. They do not alter any external state. In the first case, we define a function with the `def` keyword. -After def comes the function's name, then the list of arguments with their types, then the result type of the function, and then the function's calculations, that is, `x + y`. +After `def` comes the function's name, then the list of arguments with their types, then the result type of the function, and then the function's calculations, that is, `x + y`. -Compare this with the second approach to define a function, with the `val` keyword, which we also use for all other kinds of data. -Here, after `val` comes the function's name, then the type of the function, `(Int, Int) => Int`, -which consists of both the argument types and the result type, then come the arguments (this time without the types), and finally the implementation. +Compare this with the second approach to define a function using the `val` keyword, which we also use for all other kinds of data. +Here, after `val`, comes the function's name, followed by the type of the function, `(Int, Int) => Int`. +This consists of both the argument types and the result type. Next come the arguments (this time without the types), and finally the implementation. You will probably find the first way to define functions more readable, and you will use it more often. -However, it is important to remember that in Scala, a function is data, just like integers, strings, and instances of case classes — and it can be defined as data if needed. +However, it is important to remember that in Scala, a function is treated as data, just like integers, strings, and instances of case classes — and it can be defined as data if needed. -The third example illustrates a method. -We simply call it `add`. -Its definition appears the same as the definition of the function `addAsFunction`, but we refer to add as a method because it is associated with the class `Calculator`. -In this way, if we create an instance of `Calculator`, we can call `add` on it, and it will have access to the internal state of the instance. +The third example illustrates a method, +which we simply call `add`. +Its definition mirrors that of the `addAsFunction`, however, we refer to `add` as a method because it is associated with the `Calculator` class. +In this way, if we create an instance of `Calculator`, we can call `add` on it, granting us access to the internal state of the instance. It is also possible, for example, to override it in a subclass of `Calculator`. ```scala @@ -68,6 +68,6 @@ A video: ## Exercise -Implement multiplication as both a function and as a value; additionally, implement multiplication as a method of a class. +Implement multiplication both as a function and a value; additionally, implement multiplication as a method within a class. From bf811d26f7528b0e876a50cf2afae1bbe6dd2b70 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Thu, 16 May 2024 17:34:37 +0300 Subject: [PATCH 23/65] Update task.md language checked --- Immutability/A View/task.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Immutability/A View/task.md b/Immutability/A View/task.md index 4413d66c..91732db6 100644 --- a/Immutability/A View/task.md +++ b/Immutability/A View/task.md @@ -1,16 +1,16 @@ ## A View A view in Scala collections is a lazy rendition of a standard collection. -While a lazy list needs to be constructed as such, you can create a view from any "eager" Scala collection by calling `.view` on it. +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, -which can enhabce both performance and memory usage. +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. 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 elements can be filtered out, eliminating the need for subsequent operations on them. -Let's consider an example where we use a view to find the first even number squared that is greater than 100 from a list of numbers. +Let's consider an example where we use a view to find the first squared even number in a list that is greater than 100. ```scala 3 val numbers = (1 to 100).toList @@ -38,7 +38,7 @@ println(firstEvenSquareGreaterThan100_View) Without using a view, all the numbers in the list are initially squared and then filtered, even though we are only interested in the first square that satisfies the condition. With a view, transformation operations are computed lazily. -Therefore, squares are calculated, and conditions are checked for each element sequentially until the first match is found. +Therefore, squares are calculated and conditions are checked sequentially for each element until the first match is found. This avoids unnecessary calculations and hence is more efficient in this scenario. To learn more about the methods of Scala View, read its [documentation](https://www.scala-lang.org/api/current/scala/collection/View.html). @@ -47,5 +47,5 @@ To learn more about the methods of Scala View, read its [documentation](https:// Consider a simplified log message: it is a comma-separated string where the first substring before the comma specifies its severity, the second substring is the numerical error code, and the last one is the message itself. -Implement the function `findLogMessage`, which searches for the first log message with the given `severity` and `errorCode` within a list. +Implement the function `findLogMessage`, which searches for the first log message matching a given `severity` and `errorCode` within a list. As the list is assumed to be large, utilize `view` to avoid creating intermediate data structures. From 97c60329e937f7fc152045b9fced09a9bf71b2c4 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Thu, 16 May 2024 18:03:27 +0300 Subject: [PATCH 24/65] Update task.md language checked --- Immutability/Berliner Pattern/task.md | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Immutability/Berliner Pattern/task.md b/Immutability/Berliner Pattern/task.md index eaaf2a23..f1b9941f 100644 --- a/Immutability/Berliner Pattern/task.md +++ b/Immutability/Berliner Pattern/task.md @@ -7,29 +7,29 @@ Thankfully, you can get the best of both worlds with the languages that combine In particular, Scala was specifically designed with this fusion in mind. The Berliner Pattern is an architectural pattern introduced by Bill Venners and Frank Sommers at Scala Days 2018 in Berlin. -Its goal is to restrict mutability to only those parts of a program for where it is unavoidable. -The application can be thought of as divided into three layers: +Its goal is to restrict mutability to only those parts of a program where it is unavoidable. +The application can be thought of as being divided into three layers: -* The external layer, which has to interact with the outside world. - This layer enables the application to communicate with other programs, services, or the operating system. +* The external layer, which has to interact with the outside world, + enabling the application to communicate with other programs, services, or the operating system. It's practically impossible to implement this layer in a purely functional way, but the good news is that there is no need to do so. -* The internal layer, where we connect to databases or write into files. +* 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. This is where our business logic resides and where functional programming shines. -Pushing mutability to the thin inner and outer layers offers its advantages. -First of all, the more we restrict the data, the more future-proof our code becomes. -We not only provide more information to the compiler, but also signal future developers that some data ought not to be modified. +Pushing mutability to the thin inner and outer layers offers several benefits. +First of all, the more we restrict data, the more future-proof our code becomes. +We not only provide more information to the compiler, but we also signal to future developers that some data should not be modified. Secondly, it simplifies the writing of concurrent code. When multiple threads can modify the same data, we may quickly end up in an invalid state, making it complicated to debug. -There is no need to resort to mutexes, monitors, or other patterns when there is no actual way to modify data. +There is no need to resort to mutexes, monitors, or other such patterns when there is no actual way to modify the data. -Finally, the common pattern in imperative programming with mutable data involves first assigning some default value to a variable, +Finally, a common pattern in imperative programming with mutable data involves first assigning some default value to a variable, and then modifying it. -For example, you start with an empty collection and then populate it with some specific values. +For example, you might start with an empty collection and then populate it with some specific values. However, default values are evil. Coders often forget to change them into something meaningful, leading to many bugs, such as the billion-dollar mistake caused by using `null`. @@ -38,14 +38,14 @@ We encourage you to familiarize yourself with this pattern by watching the [orig ### Exercise -We provide you with a sample implementation of the application that handles creating, modifying, and deleting users in a database. -We mock the database and http layers, and your task will be to implement methods processing requests following the Berliner pattern. +We provide you with a sample implementation of an application that handles creating, modifying, and deleting users in a database. +We mock the database and HTTP layers, and your task is to implement methods for processing requests following the Berliner pattern. Start by implementing the `onNewUser` and `onVerification` methods in `BerlinerPatternTask.scala`. -We provide the implementations for the database and the client for these methods, so you could familiarize yourself +We provide the implementations for the database and client for these methods so you can familiarize yourself with the application. Execute the `run` script in `HttpClient.scala` to make sure your implementation works correctly. -Then implement the functionality related to changing the password as well as removing users. +Then, implement the functionality related to password changes and user removals. You will need to implement all layers for these methods, so check out `Database.scala` and `HttpClient.scala`. Don't forget to uncomment the last several lines in the `run` script for this task. From 17abccf918dff7c6851fe7b7df3047f5265ae7d3 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Thu, 16 May 2024 19:35:02 +0300 Subject: [PATCH 25/65] Update task.md language checked --- Immutability/Case Class Copy/task.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Immutability/Case Class Copy/task.md b/Immutability/Case Class Copy/task.md index 9cb36a64..dc23fe20 100644 --- a/Immutability/Case Class Copy/task.md +++ b/Immutability/Case Class Copy/task.md @@ -1,25 +1,25 @@ ## Case Class Copy In Scala, case classes automatically come equipped with a few handy methods upon declaration, one of which is the `copy` method. -The `copy` method is used to create a new instance of the case class, which is a copy of the original instance; however, you can also +The `copy` method is used to create a new instance of the case class, which is a copy of the original one; however, you can also modify some (or none) of the fields during the copying process. This feature adheres to functional programming principles, where immutability is often favored. -You can derive new instances while maintaining the immutability of existing instances, and so, for example, -avoid bugs where two threads work on the same data structure, each assuming that it is the sole modifier of it. +You can derive new instances while maintaining the immutability of existing ones. Consequently, this helps +prevent bugs that may occur when two threads work on the same data structure, each assuming that it is the sole modifier of it. Another valuable characteristic of the `copy` method is that it’s a convenient and readable means of creating new instances of the same case class. Instead of building one from scratch, you can grab an existing instance and make a copy modified to your liking. Below, you will find a Scala example using a User case class with mandatory `firstName` and `lastName` fields, along with optional `email`, `twitterHandle`, and `instagramHandle` fields. -We will first create one user with its default constructor and then another with the `copy` method of the first one. +We will first create a user with its default constructor and then generate another user with the `copy` method from the first one. Note that: * `originalUser` is initially an instance of `User` with `firstName = "Jane"`, `lastName = "Doe"`, and `email = "jane.doe@example.com"`. The other fields use their default values (i.e., `None`). -* `updatedUser` is created using the copy method on `originalUser`. - This creates a new instance with the same field values as `originalUser`, except for the fields provided as parameters to `copy`: +* `updatedUser` is created using the `copy` method on `originalUser`. + This creates a new instance with the same field values as `originalUser`, except for those provided as parameters to `copy`: * `email` is updated to `"new.jane.doe@example.com"` * `twitterHandle` is set to `"@newJaneDoe"` * `originalUser` remains unmodified after the `copy` method is used, adhering to the principle of immutability. @@ -51,6 +51,6 @@ println(s"Updated user: $updatedUser") ### Exercise Let's unravel the `copy` function. -Implement your own function `myCopy` that operates in exactly the same way as `copy` does. -You should be able to pass values only for those fields you wish to modify. +Implement your own function, `myCopy`, which operates identically to `copy`. +You should be able to pass values only for those fields that you wish to modify. As a result, a new copy of the instance should be created. From 4e056694d0a44b8b573003b0bd670187a04604b4 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Thu, 16 May 2024 19:52:23 +0300 Subject: [PATCH 26/65] Update task.md language checked --- .../task.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Immutability/Comparison of View and Lazy Collection/task.md b/Immutability/Comparison of View and Lazy Collection/task.md index f9d239c9..d2d40e09 100644 --- a/Immutability/Comparison of View and Lazy Collection/task.md +++ b/Immutability/Comparison of View and Lazy Collection/task.md @@ -1,19 +1,19 @@ ## Comparison of View and Lazy List -Now you may wonder why Scala has both lazy lists and views, and when to use which one. -Here's a short list of key differences of both approaches to lazy computation: +Now you may be wondering why Scala has both lazy lists and views, and when to use which one. +Here's a short list highlighting the key differences between these two approaches to lazy computation: * Construction: - * View: You can create a view of any Scala collection by calling `.view` on it. - * Lazy List: You must create it from scratch with the `#::` operator or other methods. + * View: You can create a view from any Scala collection by calling `.view` on it. + * Lazy List: You must create it from scratch with the `#::` operator or other specific methods. * Caching: - * View: Does not cache results. Each access recomputes values through the transformation pipeline unless forced into + * View: It does not cache results. Each access recomputes values through the transformation pipeline unless forced into a concrete collection. - * Lazy List: Once an element is computed, it is cached for future access, preventing recomputation. + * Lazy List: Once an element is computed, it is cached for future access to prevent unnecessary recomputation. * Commonly used for: - * View: Chain transformations on collections when we want to avoid the creation of intermediate collections. - * Lazy List: Ideal when working with potentially infinite sequences and when previously computed results might be + * View: Perfect for chaining transformations on collections when we want to avoid creating intermediate collections. + * Lazy List: Ideal for working with potentially infinite sequences and when previously computed results might be accessed multiple times. From fb2ca0ab078ecf2e7069a6d9ed82d9a0c6c716c5 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Thu, 16 May 2024 20:22:57 +0300 Subject: [PATCH 27/65] Update task.md language checked --- Immutability/Lazy List/task.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Immutability/Lazy List/task.md b/Immutability/Lazy List/task.md index 6cb1f651..e4e26fe2 100644 --- a/Immutability/Lazy List/task.md +++ b/Immutability/Lazy List/task.md @@ -1,16 +1,16 @@ ## Lazy List -A lazy list in Scala is a collection that evaluates its elements lazily: each element is computed only once, +A lazy list in Scala is a collection that evaluates its elements lazily, with each element computed just once, the first time it is needed, and then stored for subsequent access. -Lazy lists can be infinite: their elements are computed on-demand. Hence, if your program keeps accessing the next element +Lazy lists can be infinite, with their elements computed on-demand. Hence, if your program keeps accessing the next element in a loop, the lazy list will inevitably grow until the program fails with an out-of-memory error. In practice, however, you will likely need only a finite number of elements. -While this number might be large and unknown from the start, since the lazy list will compute only -explicitly requested values, it allows developers to work with large datasets or sequences in a memory-efficient manner. -In such cases, a lazy list provides a convenient method to implement the logic for computing the consecutive elements +While this number might be large and unknown from the start, the lazy list will compute only +explicitly requested values, enabling developers to work with large datasets or sequences in a memory-efficient manner. +In such cases, a lazy list provides a convenient method to implement the logic for computing consecutive elements until you decide to stop. -You can use it in certain specific cases where otherwise, you would need to code an elaborate data structure with mutable fields -and a method that would compute new values for those fields. +You can use it in certain specific cases where you would otherwise need to code an elaborate data structure with mutable fields +and a method to compute new values for those fields. Below is an example of how to generate a Fibonacci sequence using a lazy list in Scala: @@ -35,18 +35,18 @@ In the above code: immediately upon the lazy list's construction, but only later when `fib` already exists and we want to access one of its elements. * `fib.zip(fib.tail)` takes two sequences, `fib` and its tail (i.e., `fib` without its first element), and zips them together into pairs. The Fibonacci sequence is generated by summing each pair `(a, b) => a + b` of successive Fibonacci numbers. -* `take(10)` is used to fetch the first 10 Fibonacci numbers from the lazy list, and `foreach(println)` prints them. - Note that the Fibonacci sequence is theoretically infinite, but it doesn't cause any issues or out-of-memory errors +* `take(10)` is used to fetch the first 10 Fibonacci numbers from the lazy list, and `foreach(println)` prints them out. + Note that the Fibonacci sequence is theoretically infinite, but this doesn't cause any issues or out-of-memory errors (at least not yet), thanks to lazy evaluation. * Alternatively, you can use `takeWhile` to compute consecutive elements of the lazy list until a certain requirement is fulfilled. * The methods opposite to `take` and `takeWhile` — `drop` and `dropWhile` — can be used to compute and then ignore - a certain number of elements in the lazy list or compute and ignore elements until a certain requirement is met. - These methods can be chained. - For example, `fib.drop(5).take(5)` will compute the first 10 elements of the Fibonacci sequence but will ignore the first 5 of them. + a certain number of elements in the lazy list or to compute and ignore elements until a certain requirement is met. + These methods can be chained together. + For example, `fib.drop(5).take(5)` will compute the first 10 elements of the Fibonacci sequence but will disregard the first 5. -To learn more about the methods of Scala's `LazyList`, read its [documentation](https://www.scala-lang.org/api/current/scala/collection/immutable/LazyList.html). +To learn more about the methods of Scala's `LazyList`, read the [documentation](https://www.scala-lang.org/api/current/scala/collection/immutable/LazyList.html). ### Exercise -Implement the function that generates an infinite lazy list of prime numbers in ascending order. +Implement a function that generates an infinite lazy list of prime numbers in ascending order. Use the Sieve of Eratosthenes algorithm. From a4e1bdc7f4b85adaba5a403b37b7585102585e1c Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Fri, 17 May 2024 12:35:37 +0300 Subject: [PATCH 28/65] Update task.md language checked --- Immutability/Lazy Val/task.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Immutability/Lazy Val/task.md b/Immutability/Lazy Val/task.md index 771849f9..2014ce28 100644 --- a/Immutability/Lazy Val/task.md +++ b/Immutability/Lazy Val/task.md @@ -2,20 +2,20 @@ **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. -In a lazy evaluation strategy, expressions are not evaluated when bound to a variable but when used for the first time. +With a lazy evaluation strategy, expressions are not evaluated when bound to a variable, but rather when used for the first time. If they are never used, they are never evaluated. -In some contexts, lazy evaluation can also avert exceptions, since it can prevent the evaluation of erroneous computations. +In some contexts, lazy evaluation can also prevent exceptions by avoiding the evaluation of erroneous computations. In Scala, the keyword `lazy` is used to implement laziness. -When `lazy` is used in a `val` declaration, the initialization of that `val` is deferred until its first access. +When `lazy` is used in a `val` declaration, the initialization of that `val` is deferred until it's first accessed. Here’s a breakdown of how `lazy val` works internally: * **Declaration**: When a `lazy val` is declared, no memory space is allocated for the value, and no initialization code is executed. * **First access**: Upon the first access of the `lazy val`, the expression on the right-hand side of the `=` operator is evaluated, and the resultant value is stored. - This computation generally happens thread-safe to avoid potential issues in a multi-threaded context - (there’s a check-and-double-check mechanism to ensure that the value is only computed once, even in a concurrent environment). -* **Subsequent accesses**: For any subsequent accesses, the previously computed and stored value is returned directly + This computation generally happens in a thread-safe manner to avoid potential issues in a multi-threaded context. + There’s a check-and-double-check mechanism to ensure the value is computed only once, even in a concurrent environment. +* **Subsequent accesses**: During any subsequent accesses, the previously computed and stored value is returned directly, without re-evaluating the initializing expression. Consider the following example: @@ -43,7 +43,7 @@ println(s"time now is $now") // should take only a few milliseconds at most In the above code: * The `lazy val lazyComputedValue` is declared but not computed immediately upon declaration. -* Once it is accessed in the first `println` that includes it, the computation is executed, `"Computing..."` is printed to the console, - and the computation (here simulated with `Thread.sleep(1000)`) takes place before returning the value `42`. -* Any subsequent accesses to `lazyComputedValue`, like the second `println`, do not trigger the computation again. +* Once it is accessed in the first `println` statement that includes it, the computation is executed, `"Computing..."` is printed to the console, + and the computation (here simulated with `Thread.sleep(1000)`) takes place before the value `42` is returned. +* Any subsequent accesses to `lazyComputedValue`, like in the second `println` statement, do not trigger the computation again. The stored value (`42`) is used directly. From aed7f571c91a4434c19c79baf20ea3341503b8f6 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Fri, 17 May 2024 12:50:26 +0300 Subject: [PATCH 29/65] Update task.md language checked --- .../task.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Immutability/Scala Collections instead of Imperative Loops/task.md b/Immutability/Scala Collections instead of Imperative Loops/task.md index 2daa92a3..66dabb4d 100644 --- a/Immutability/Scala Collections instead of Imperative Loops/task.md +++ b/Immutability/Scala Collections instead of Imperative Loops/task.md @@ -1,20 +1,20 @@ ## Scala Collections instead of Imperative Loops 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, a zero, or null. -Then, step-by-step, initialization code runs in a loop to create the proper value . -After this process, the value assigned to the variable does not change anymore — or if it does, +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. +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. Throughout the whole lifespan of the program, it hangs like a loose end of an electric cable, tempting everyone to touch it. -Functional Programming, on the other hand, allows us to build useful values without the need for initial default values and temporary mutability. -Even a highly complex data structure can be computed using a higher-order function extensively and then -assigned to a constant, preventing future modifications. -If we need an updated version, we can create a new data structure instead of modifying the old one. +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. 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. -You have already learned about some of those methods in the first chapter. +You have already learned about some of these methods in the first chapter. In this chapter, you will learn more about how to avoid mutability and leverage immutability to write safer and sometimes even more performant code. From 482cf520b3d32fc9ca70d78eb8236fc84deeef31 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Fri, 17 May 2024 16:01:44 +0300 Subject: [PATCH 30/65] Update task.md language checked --- Immutability/The Builder Pattern/task.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Immutability/The Builder Pattern/task.md b/Immutability/The Builder Pattern/task.md index 75152421..2aaf21fc 100644 --- a/Immutability/The Builder Pattern/task.md +++ b/Immutability/The Builder Pattern/task.md @@ -1,28 +1,28 @@ ## The Builder Pattern -The Builder pattern is a design pattern often used in object-oriented programming to provide +The builder pattern is a design pattern often used in object-oriented programming to provide a flexible solution for constructing complex objects. It's especially handy when an object needs to be created with numerous possible configuration options. The pattern involves separating the construction of a complex object from its representation -so that the same construction process can make different representations. +so that the same construction process can yield different representations. -Here's why the Builder Pattern is used: +Here's why the builder pattern is used: * To encapsulate the construction logic of a complex object. * To allow an object to be constructed step by step, often through method chaining. * To avoid having constructors with many parameters, which can be confusing and error-prone (often referred to as the telescoping constructor anti-pattern). -Below is a Scala example using the Builder Pattern to create instances of a `User` case class, with mandatory `firstName` +Below is a Scala example using the builder pattern to create instances of a `User` case class, with mandatory `firstName` and `lastName` fields and optional `email`, `twitterHandle`, and `instagramHandle` fields. Note that: -* The `User` case class defines a user with mandatory `firstName` and `lastName`, optional `email`, `twitterHandle`, and `instagramHandle`. -* `UserBuilder` facilitates the creation of a `User` object. - The mandatory parameters are specified in the builder's constructor, while methods like `setEmail`, `setTwitterHandle`, +* The `User` case class defines a user with mandatory `firstName` and `lastName` fields, along with optional `email`, `twitterHandle`, and `instagramHandle` fields. +* `UserBuilder` facilitates the creation of a `User` object, with + mandatory parameters specified in the builder's constructor. Methods like `setEmail`, `setTwitterHandle`, and `setInstagramHandle` are available to set optional parameters. Each of these methods returns the builder itself, enabling method chaining. * Finally, the execution of the `build` method employs all specified parameters (whether default or set) to construct a `User` object. -This pattern keeps object creation understandable and clean, mainly when dealing with objects that can have multiple optional parameters. +This pattern keeps the process of object creation clear and straightforward, particularly when dealing with objects possessing multiple optional parameters. From 89c86ebc0692e0382c826e5e0983aa20a65f0bdc Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Fri, 17 May 2024 17:46:41 +0300 Subject: [PATCH 31/65] Update task.md language checked --- Pattern Matching/Case Class/task.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Pattern Matching/Case Class/task.md b/Pattern Matching/Case Class/task.md index b373b81c..c0036ba9 100644 --- a/Pattern Matching/Case Class/task.md +++ b/Pattern Matching/Case Class/task.md @@ -9,15 +9,15 @@ otherwise we would have to code manually: unidiomatic in Scala. Instances of case classes should serve as immutable data structures, as modifying them can result in less intuitive and readable code. -2. A case class provides a default constructor with public, read-only parameters, thus reducing theboiler-plate associated with case class instantiation. +2. A case class provides a default constructor with public, read-only parameters, thus reducing the boilerplate associated with case class instantiation. 3. Scala automatically defines some useful methods for case classes, such as `toString`, `hashCode`, and `equals`. The `toString` method gives a string representation of the object, `hashCode` is used for hashing collections like `HashSet` and `HashMap`, - and `equals` checks structural equality, rather than reference equality - (i.e., checks the equality of the respective fields of the case class, - rather than verifying if the two references point to the same object). -4. Case classes come with the `copy` method that can be used to create a copy of the case class instance: - exactly the same as the original or with some parameters modified + and `equals` checks structural equality rather than reference equality. + In other words, it checks the equality of the respective fields of the case class, + rather than verifying if the two references point to the same object. +4. Case classes come with a `copy` method that can be used to create a copy of the case class instance. + This can be exactly the same as the original or with some parameters modified (the signature of the `copy` method mirrors that of the default constructor). 5. Scala automatically creates a companion object for the case class, which contains factory `apply` and `unapply` methods. @@ -28,13 +28,13 @@ otherwise we would have to code manually: 7. On top of that, case classes are conventionally not extended. They can extend traits and other classes, but they shouldn't be used as superclasses for other classes. Technically though, extending case classes is not forbidden by default. - If you want to ensure that a case class is be extended, mark it with the `final` keyword. + If you want to ensure that a case class isn't extended, mark it with the `final` keyword. You should already be familiar with some of these features, as we used them in the previous module. The difference here is that we want you to focus on distinct aspects that you'll see in the examples and exercises. Below is a simple example of a case class that models cats. -We create a `Cat` instance called `myCat` and then use pattern matching against `Cat` to access its name and color. +We create a `Cat` instance called `myCat` and then use pattern matching on `Cat` to access its name and color. ```scala 3 case class Cat(name: String, color: String) From 7d97044cc2b1b294b8adfe1bb282cff53e2311f9 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Fri, 17 May 2024 18:07:22 +0300 Subject: [PATCH 32/65] Update task.md language checked --- Pattern Matching/Case Objects/task.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Pattern Matching/Case Objects/task.md b/Pattern Matching/Case Objects/task.md index 593f6646..95ea2dd7 100644 --- a/Pattern Matching/Case Objects/task.md +++ b/Pattern Matching/Case Objects/task.md @@ -1,16 +1,16 @@ # Case Objects -You might have noticed in the example of a binary tree implemented with sealed trait hierarchies, +You might have noticed in the example of a binary tree implemented with sealed trait hierarchies that we used a *case object* to introduce the `Stump` type. In Scala, a case object is a special type of object that combines characteristics and benefits of both a case class and an object. Similar to a case class, a case object comes equipped with a number of auto-generated methods like `toString`, `hashCode`, and `equals`, and they can be directly used in pattern matching. On the other hand, just like any regular object, a case object is a singleton, i.e., there's exactly one instance of it in the entire JVM. -Case objects are used in place of case classes when there's no need for parametrization — when you don't need to carry data, -but you still want to benefit from pattern matching capabilities of case classes. -In Scala 2, case objects implementing a common trait were the default way of achieving enum functionality. +Case objects are used in place of case classes when there's no need for parametrization — when you don't need to carry data +yet still want to benefit from the pattern matching capabilities of case classes. +In Scala 2, implementing a common trait using case objects was the default way of achieving enum functionality. This is no longer necessary in Scala 3, which introduced enums, but case objects are still useful in more complex situations. For example, you may have noticed that to use case objects as enums, we make them extend a shared sealed trait. @@ -24,11 +24,11 @@ case object Unauthorized extends AuthorizationStatus def authorize(userId: UserId): AuthorizationStatus = ... ``` -Here, `AuthorizationStatus` is a sealed trait and `Authorized` and `Unauthorized` are the only two case objects extending it. -This means that the result of calling the authorize method can only ever be either `Authorized` or `Unauthorized`. +Here, `AuthorizationStatus` is a sealed trait, and `Authorized` and `Unauthorized` are the only two case objects extending it. +This means that the result of calling the authorize method can be either `Authorized` or `Unauthorized`. There is no other response possible. -However, imagine that you're working on code which uses a library or a module you no longer want to modify. +However, imagine that you're working on code that uses a library or module you no longer want to modify. In that case, the initial author of that library or module might have used case objects extending a non-sealed trait to make it easier for you to add your own functionality: @@ -52,16 +52,16 @@ override def authorize(userId: UserId): AuthorizationStatus = ``` Here, we extend the functionality of the original code by adding a possibility that the user, despite being authorized to perform a given operation, -encountered an issue and was logged out. +encounters an issue and is logged out. Now they need to log in again before they are able to continue. This is not the same as simply being `Unauthorized`, so we add a third case object to the set of those extending `AuthorizationStatus`: we call it `LoggedOut`. -If the original author had used a sealed trait to define `AuthorizationStatus`, or if they had used an enum, we wouldn't have been able to do that. +If the original author had used a sealed trait to define `AuthorizationStatus` or had used an enum, we wouldn't have been able to do that. ### Exercise We're modeling bots that move on a 2D plane (see the `Coordinates` case class). -There are various kinds of bots (see the `Bot` trait), which move a distinct number of cells at a time. +There are various kinds of bots (see the `Bot` trait), each moving a distinct number of cells at a time. Each bot moves in one of four directions (see the `Direction` trait). Determine whether the traits should be sealed or not and modify them accordingly. Implement the `move` function. From e31335a11fef531d60351bcce8a2d016ccd0f0cf Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 20 May 2024 14:42:19 +0300 Subject: [PATCH 33/65] Update task.md language checked --- Pattern Matching/Destructuring/task.md | 40 +++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Pattern Matching/Destructuring/task.md b/Pattern Matching/Destructuring/task.md index 6c0609fa..5ece98b7 100644 --- a/Pattern Matching/Destructuring/task.md +++ b/Pattern Matching/Destructuring/task.md @@ -2,19 +2,19 @@ Destructuring in Scala refers to the practice of breaking down an instance of a given type into its constituent parts. You can think of it as the inversion of construction. -In a constructor, or an `apply` method, we use a collection of parameters to create a new instance of a given type. +In a constructor or an `apply` method, we use a collection of parameters to create a new instance of a given type. When destructuring, we start with an instance of a given type and decompose it into values that, at least in theory, could be used again to create an exact copy of the original instance. -Additionally, similar to how an apply method can serve as a smart constructor that performs certain complex operations before creating an instance, +Additionally, just as an `apply` method can serve as a smart constructor that performs certain complex operations before creating an instance, we can implement a custom method, called `unapply`, that intelligently deconstructs the original instance. It's a very powerful and expressive feature of Scala, often seen in idiomatic Scala code. The `unapply` method should be defined in the companion object. -It usually takes the instance of the associated class as its only argument, and returns an option of what’s contained within the instance. -In the simplest case, this will just be the class's fields: one if there is only one field in the class, -otherwise a pair, triple, quadruple, and so on. +It usually takes the instance of the associated class as its only argument and returns an option of what’s contained within the instance. +In the simplest case, this will just be the class's fields: one if there is only one field, +or otherwise a pair, triple, quadruple, and so on. Scala automatically generates simple `unapply` methods for case classes. -In such case, unapply will just break the given instance into a collection of its fields, as shown in the following example: +In such cases, `unapply` just breaks the given instance into a collection of its fields, as shown in the following example: ```scala 3 case class Person(name: String, age: Int) @@ -24,12 +24,12 @@ val Person(johnsName, johnsAge) = john println(s"$johnsName is $johnsAge years old.") ``` -As you can notice, similarly to how we don't need to explicitly write `apply` to create an instance of the `Person` case class, +As you can notice, just as we don't need to explicitly write `apply` to create an instance of the `Person` case class, we also don't need to explicitly write `unapply` to break an instance of the `Person` case class back into its fields: `johnsName` and `johnsAge`. -However, you will not see this way of using destructuring very often in Scala. -After all, if you already know exactly what case class you have, and you only need to read its public fields, +However, you will not often see this way of using destructuring in Scala. +After all, if you already know exactly which case class you have and you only need to read its public fields, you can do so directly — in this example, with `john.name` and `john.age`. Instead, `unapply` becomes much more valuable when used together with pattern matching. @@ -53,7 +53,7 @@ val snowy = Cat("Snowy", White, 1) val midnight = Cat("Midnight", Black, 4) ``` -We have two cats (Fluffy and Snowy) that are one year old, and three cats (Mittens, Ginger, and Midnight) that are older than one year. +We have two cats (Fluffy and Snowy) who are one year old, and three cats (Mittens, Ginger, and Midnight) who are older than one year. Next, let's put these cats in a Seq: ```scala 3 @@ -72,15 +72,15 @@ cats.foreach { ``` In this code, we're using pattern matching to destructure each Cat object. -We're also using a guard `if age > 1` to check the age of the cat. +We're also using a guard, `if age > 1`, to check the age of the cat. If the age is more than one, we print out the message for adult cats. -If the age is not more than one (i.e., it's one or less), we print out the message for kittens. +If the age is one or less, we print out the message for kittens. Note that in the second case expression, we're using the wildcard operator `_` to ignore the age value, -because we don't need to check it — if a cat instance is destructured in the second case, +since we don't need to check it — if a cat instance is destructured in the second case, it means that the cat's age was already checked in the first case and failed that test. -Also, if we wanted to handle a case where one of the fields has a single constant value -(unlike in the first case above, where any age larger than `1` suits as well), we can simply substitute it for the field: +Also, if we need to handle a case where one of the fields has a specific constant value +(unlike in the first case above, where any age greater than `1` is suitable), we can directly specify that value in place of the field: ```scala 3 cats.foreach { @@ -96,15 +96,15 @@ cats.foreach { ### Exercise RGB stands for Red, Green, and Blue. It is a color model used in digital imagining -that represents colors by combining intensities of these three primary colors. This allowing electronic devices +that represents colors by combining intensities of these three primary colors. This allows electronic devices to create a wide spectrum of colors. Sometimes, a fourth component called Alpha is also used to describe the transparency. -Each component can be any integer number withing the range `0 .. 255`, with `0` meaning no color, +Each component can be any integer withing the range `0 .. 255`, with `0` meaning no color, and `255` representing the maximum color intensity. For example, the color red is represented when Red is `255`, while Green and Blue are `0`. -In this exercise, implement the function `colorDescription`, which transforms the given RGB color into a string. -It should pattern destruct the color, examine the RGB components, and return the name of the color in case it is one of +In this exercise, implement the function `colorDescription`, which transforms a given RGB color into a string. +It should deconstruct the color, examine the RGB components, and return the name of the color in case it is one of the following: `"Black", "Red", "Green", "Blue", "Yellow", "Cyan", "Magenta", "White"`. -Otherwise, it should just return the result of the `toString()` application. +Otherwise, it should just return the result of the `toString()` method. Please ignore the alpha channel when determining the color name. From 82a5860b0c74bd7cccf7d421861c0f6f0c572e1d Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 20 May 2024 14:59:46 +0300 Subject: [PATCH 34/65] Update task.md language checked --- Pattern Matching/Enums/task.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Pattern Matching/Enums/task.md b/Pattern Matching/Enums/task.md index e5b794f2..356806a5 100644 --- a/Pattern Matching/Enums/task.md +++ b/Pattern Matching/Enums/task.md @@ -1,16 +1,16 @@ # Enum An enumeration (or enum) is a type that represents a finite set of distinct values. -Enumerations are commonly used to limit the set of possible values of a field, +Enumerations are commonly used to limit the set of possible values for a field, thus improving code clarity and reliability. Since a field cannot be set to something outside a small set of well-known values, -we can make sure that the logic we implement handles all possibile options and -there are no unconsidered scenarios. +we can make sure that the logic we implement handles all possible options and +that there are no unconsidered scenarios. In Scala 3, enumerations are created using the `enum` keyword. Each value of the enum is an object of the *enumerated type*. -Scala 3 enums can also have parameterized values and methods. -You have already seen this in our previous examples where we used enums to define the colors for cat fur: +Scala 3 enums can also have parameterized values and methods. +You have already seen this in our previous examples, where we used enums to define the colors for cat fur: ```scala 3 enum Color: @@ -21,7 +21,7 @@ However, Scala 3 enums are even more powerful than that. In fact, they are more versatile than their counterparts in many other programming languages. Enums in Scala 3 can also be used as algebraic data types (also known as sealed trait hierarchies in Scala 2). -You can have an enum with cases that carry different data. +You can have an enum with cases that carry different types of data. Here's an example: ```scala 3 @@ -50,7 +50,7 @@ val tree: Tree[Int] = In this example, `Tree` is an enum that models a binary tree data structure. Binary trees are used in many areas of computer science, including sorting, searching, and efficient data access. -They consist of nodes where each node can have at most two subtrees. +They consist of nodes, each of which can have at most two subtrees. Here, we implement a binary tree with an enum `Tree[A]`, which allows the nodes of the tree to be one of three possible kinds: * a `Branch`, which has a value of type `A` and two subtrees, `left` and `right`, @@ -59,17 +59,17 @@ to be one of three possible kinds: Please note that our implementation of a binary tree is slightly different from the classic one. You may notice that ours is a bit redundant: -a `Leaf` is, in all practical sense, the same as a `Branch` where both subtrees are stumps. -But having `Leaf` as a separate enum case allows us to write the code for building +a `Leaf` is, in all practical senses, the same as a `Branch` where both subtrees are stumps. +However, having `Leaf` as a separate enum case allows us to write the code for building the tree in a more concise way. ## Exercise -Implement a function that checks if the tree is balanced. +Implement a function that checks if a tree is balanced. A balanced binary tree meets the following conditions: * The absolute difference between the heights of the left and right subtrees at any node is no greater than 1. -* For each node, its left subtree is a balanced binary tree -* For each node, its right subtree is a balanced binary tree +* For each node, its left subtree is a balanced binary tree. +* For each node, its right subtree is a balanced binary tree. -For an extra challenge, try to accomplish this in one pass. +For an extra challenge, try to accomplish this in a single pass. From f10ac68f8945e5f37126a0bce3cd86530723459b Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 20 May 2024 15:08:10 +0300 Subject: [PATCH 35/65] Update task.md language checked --- Pattern Matching/Pattern Matching/task.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pattern Matching/Pattern Matching/task.md b/Pattern Matching/Pattern Matching/task.md index c45794ed..6931d2f8 100644 --- a/Pattern Matching/Pattern Matching/task.md +++ b/Pattern Matching/Pattern Matching/task.md @@ -1,11 +1,11 @@ # Pattern Matching Pattern matching is one of the most important features in Scala. -It’s so vital that we might risk saying that it’s not *a* feature of Scala, but *the* defining feature. -It affects every other part of the programming language to the point where it’s difficult to talk about anything in Scala +It’s so vital that we might risk saying it’s not just *a* feature of Scala, but *the* defining feature. +It affects every other part of the programming language to the extent that it’s difficult to discuss any aspect of Scala without at least mentioning or using pattern matching in a code example. You have already seen it — the match/case statements, the partial functions, and the destructuring of instances of case classes. -In this lesson, we will touch upon case classes and objects, ways to construct and deconstruct them, enums, and a neat +In this lesson, we will explore case classes and objects, ways to construct and deconstruct them, enums, and a neat programming trick called the `newtype` pattern. From 6bc32718199f006c71367f3985f5c914a774617a Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 20 May 2024 15:20:38 +0300 Subject: [PATCH 36/65] Update task.md language checked --- Pattern Matching/Sealed Traits Hierarchies/task.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Pattern Matching/Sealed Traits Hierarchies/task.md b/Pattern Matching/Sealed Traits Hierarchies/task.md index 06494655..1f820bb6 100644 --- a/Pattern Matching/Sealed Traits Hierarchies/task.md +++ b/Pattern Matching/Sealed Traits Hierarchies/task.md @@ -1,13 +1,13 @@ # Sealed Traits Hierarchies -Sealed traits in Scala are used to represent restricted class hierarchies that provide exhaustive type checking. +Sealed traits in Scala are used to represent restricted class hierarchies, providing exhaustive type checking. When a trait is declared as sealed, it can only be extended within the same file. -This allows the compiler to know all the subtypes, which allows for more precise compile-time checking. +This restriction enables the compiler to identify all subtypes, allowing for more precise compile-time checking. -With the introduction of enums in Scala 3, many use cases of sealed traits are now covered by them, and their syntax is more concise. -However, sealed traits are more flexible than enums — they allow for the addition of new behavior in each subtype. +With the introduction of enums in Scala 3, many use cases of sealed traits are now covered by enums, and their syntax is more concise. +However, sealed traits are more flexible than enums — they allow for the addition of new behaviors in each subtype. For instance, we can override the default implementation of a given method differently in each case class that extends the parent trait. -In enums, all enum cases share the same methods and fields. +In contrast, in enums, all cases share the same methods and fields. ```scala 3 sealed trait Tree[+A]: @@ -39,7 +39,7 @@ val tree: Tree[Int] = ## Exercise Our trees are immutable, so we can compute their heights and check if they are balanced at the time of creation. -To do this, we added the `height` and `isBalanced` members into the `Tree` trait declaration. +To do this, we added the `height` and `isBalanced` members to the `Tree` trait declaration. The only thing that is left is to override these members in all classes that extend the trait in this exercise. This way, no extra passes are needed to determine whether a tree is balanced. From 9761989e25f2b1ca686abec85a75a6ddcd02ac01 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 20 May 2024 16:28:49 +0300 Subject: [PATCH 37/65] Update task.md language checked --- .../task.md | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Pattern Matching/Smart Constructors and the apply Method/task.md b/Pattern Matching/Smart Constructors and the apply Method/task.md index f25e0b04..f8bd253e 100644 --- a/Pattern Matching/Smart Constructors and the apply Method/task.md +++ b/Pattern Matching/Smart Constructors and the apply Method/task.md @@ -10,12 +10,12 @@ class Cat: cat() // returns "meow" ``` -Technically this sums it up — you can implement `apply` any way you want, for any reason you want. -However, by convention, the most popular way to use `apply` is as a smart constructor. -This convention is very important, and we would advise you to follow it. +Technically, this sums it up — you can implement `apply` any way you want, for any purpose. +However, by convention, `apply` is most popularly used as a smart constructor. +This convention is very important, and we strongly advise adhering to it. There are a few other ways you can use `apply`. -For example, the Scala collections library often uses it to retrieve data from a collection. This might look +For example, the Scala collections library often employs it to retrieve data from a collection. This usage might appear as if Scala has traded the square brackets, common in more traditional languages, for parentheses: ```scala 3 @@ -37,8 +37,8 @@ This pattern can be especially useful in situations where: * You need to enforce a specific protocol for object creation, such as caching objects, creating singleton objects, or generating objects through a factory. The idiomatic way to use `apply` as a smart constructor is to place it in the companion object of a class -and call it with the name of the class and a pair of parentheses. -For example, let's consider again the `Cat` class with a companion object that has an `apply` method: +and call it by using the name of the class followed by a pair of parentheses. +For example, let's consider the `Cat` class again, which has a companion object that includes an `apply` method: ```scala 3 class Cat private (val name: String, val age: Int) @@ -51,11 +51,11 @@ object Cat: val fluffy = Cat("Fluffy", -5) // the age of Fluffy is set to 0, not -5 ``` -The `Cat` class has a primary constructor that takes a `String` and an `Int` to set the name and age of the new cat, respectivelym. +The `Cat` class has a primary constructor that takes a `String` and an `Int` to set the name and age of the new cat, respectively. Besides, we create a companion object and define the `apply` method in it. -This way, when we later call `Cat("fluffy", -5)`, the `apply` method, not the primary constructor, is invoked. +This way, when we later call `Cat("Fluffy", -5)`, the `apply` method, not the primary constructor, is invoked. In the `apply` method, we check the provided age of the cat, and if it's less than zero, we create a cat instance -with the age set to zero, instead of the input age. +with the age set to zero, instead of using the input age. Please also notice how we distinguish between calling the primary constructor and the `apply` method. When we call `Cat("Fluffy", -5)`, the Scala 3 compiler checks if a matching `apply` method exists. @@ -63,22 +63,22 @@ If it does, the `apply` method is called. Otherwise, Scala 3 calls the primary constructor (again, if the signature matches). This makes the `apply` method transparent to the user. If you need to call the primary constructor explicitly, bypassing the `apply` method, you can use the `new` keyword, -for example, `new Cat(name age)`. +for example, `new Cat(name, age)`. We use this trick in the given example to avoid endless recursion — if we didn't, calling `Cat(name, age)` or `Cat(name, 0)` -would again call the `apply` method. +would again trigger the `apply` method. -You might wonder how to prevent the user from bypassing our `apply` method by calling the primary constructor `new Cat("Fluffy", -5)`. +You might wonder how to prevent users from bypassing our `apply` method by calling the primary constructor `new Cat("Fluffy", -5)`. Notice that in the first line of the example, where we define the `Cat` class, there is a `private` keyword between the name of the class and the parentheses. -The `private` keyword in this position means that the primary constructor of the class `Cat` can be called only by -the methods of the class or its companion object. +The `private` keyword in this position means that the primary constructor of the `Cat` class can only be called by +methods within the class or its companion object. This way, we can still use `new Cat(name, age)` in the `apply` method, since it is in the companion object, -but it's unavailable to the user. +but it remains unavailable to the user. ## Exercise Consider the `Dog` class, which contains fields for `name`, `breed`, and `owner`. -Sometimes a dog get lost, and the person who finds it knows as little about the dog as its name on the collar. +Sometimes a dog gets lost, and the person who finds it may know as little about the dog as its name on the collar. Until the microchip is read, there is no way to know who the dog's owner is or what breed the dog is. To allow for the creation of `Dog` class instances in these situations, it's wise to use a smart constructor. We represent the potentially unknown `breed` and `owner` fields with `Option[String]`. From a85570ff964ebf240a981ad89fc9f8130713974a Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 20 May 2024 18:07:53 +0300 Subject: [PATCH 38/65] Update task.md language checked --- Pattern Matching/The Newtype Pattern/task.md | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Pattern Matching/The Newtype Pattern/task.md b/Pattern Matching/The Newtype Pattern/task.md index 3e936e58..342e1013 100644 --- a/Pattern Matching/The Newtype Pattern/task.md +++ b/Pattern Matching/The Newtype Pattern/task.md @@ -2,25 +2,25 @@ The *newtype pattern* in Scala is a way of creating new types from existing ones that are distinct at compile time but share the same runtime representation. -This can be useful for adding more meaning to simple types, to enforce type safety, and to avoid mistakes. +This approach can be useful for adding more meaning to simple types, enforcing type safety, and avoiding mistakes. For example, consider a scenario where you are dealing with user IDs and product IDs in your code. Both IDs are of type `Int`, but they represent completely different concepts. Using `Int` for both may lead to bugs where you accidentally pass a user ID where a product ID was expected, or vice versa. -The compiler wouldn't catch these errors because both IDs are of the same type, Int. +The compiler wouldn't catch these errors because both IDs are of the same type, `Int`. -With the newtype pattern, you can create distinct types for `UserId` and `ProductId` that wrap around Int, providing more safety: +With the newtype pattern, you can create distinct types for `UserId` and `ProductId` that wrap around `Int`, providing more safety: ```scala 3 case class UserId(value: Int) extends AnyVal case class ProductId(value: Int) extends AnyVal ``` -These are called value classes in Scala. `AnyVal` is a special trait in Scala — when you extend it with a case class +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 will use this information to catch any bugs that could arise if you were to confuse integers used -for user IDs with integers used for product IDs. But then, at a later phase, it strips the type information from the data, -leaving only a bare `Int`, so that your code incurs no overhead at runtime. +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, +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: ```scala 3 @@ -36,8 +36,8 @@ val productId = ProductId(456) val user = getUser(userId) // This is fine ``` -In Scala 3, a new syntax has been introduced for creating newtypes using *opaque type aliases*, but the concept remains the same. -The above example would look like as follows in Scala 3: +In Scala 3, a new syntax has been introduced for creating newtypes using *opaque type aliases*, although the concept remains the same. +The above example would look as follows in Scala 3: ```scala 3 object Ids: @@ -61,16 +61,16 @@ val user = getUser(userId) // This is fine ``` As you can see, some additional syntax is required. -Since an opaque type is just a kind of type alias, not a case class, we need to manually define `apply` methods +Since an opaque type is essentially a type alias and not a case class, we need to manually define `apply` methods for both `UserId` and `ProductId`. -Also, it's essential to define them inside an object or a class — they cannot be top-level definitions. -On the other hand, opaque types integrate very well with extension methods, which is another new feature in Scala 3. +Also, it's essential to define these methods within an object or a class — they cannot be top-level definitions. +On the other hand, opaque types integrate very well with extension methods, another new feature in Scala 3. We will discuss this in more detail later. ### Exercise -One application of the opaque types is expressing units of measure. -For example, in a fitness tracker, the distance can be input by the user in either feet or meters, +One application of opaque types is expressing units of measure. +For example, in a fitness tracker, users can input the distance either in feet or meters, based on their preferred measurement system. -Implement functions for tracking the distance in different units and the `show` function to display +Implement functions for tracking distance in different units and a `show` function to display the tracked distance in the preferred units. From b9f368350be06a6fc192063765784861680366ef Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 20 May 2024 18:13:43 +0300 Subject: [PATCH 39/65] Update README.md language checked --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bf21ef69..be69fa99 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Functional Programming in Scala [![official JetBrains project](http://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) -

This is an introductory course to Functional Programming in Scala. The course is designed for learners who already have some basic knowledge of Scala. The course covers the core concepts of functional programming, including functions as data, immutability, and pattern matching. The course also includes hands-on examples and exercises to help students practice their new skills.

+

This is an introductory course on Functional Programming in Scala, designed for learners who already have some basic knowledge of Scala. The course covers core concepts of functional programming, including functions as data, immutability, and pattern matching. It also includes hands-on examples and exercises to help students practice their new skills.

Have fun and good luck!

## Want to know more? -If you have questions about the course or the tasks, or if you find any errors, feel free to ask questions and participate in discussions within the repository [issues](https://github.com/jetbrains-academy/Functional_Programming_Scala/issues). +If you have any questions about the course or the tasks, or if you find any errors, feel free to ask questions and participate in discussions within the repository [issues](https://github.com/jetbrains-academy/Functional_Programming_Scala/issues). ## Contribution Please be sure to review the [project's contributing guidelines](https://github.com/jetbrains-academy#contribution-guidelines) to learn how to help the project. From a3258c7d37e3364cb848fdc5014d2768eb232842 Mon Sep 17 00:00:00 2001 From: Ekaterina Verbitskaia Date: Tue, 28 May 2024 20:24:21 +0200 Subject: [PATCH 40/65] Monads theory --- .../build.sbt | 4 + .../src/Task.scala | 26 ++++ .../task-info.yaml | 8 ++ .../task.md | 52 +++++++ .../test/TestSpec.scala | 8 ++ Monads/Monadic Laws/build.sbt | 4 + Monads/Monadic Laws/src/Task.scala | 1 + Monads/Monadic Laws/task-info.yaml | 8 ++ Monads/Monadic Laws/task.md | 134 ++++++++++++++++++ Monads/Monadic Laws/test/TestSpec.scala | 8 ++ .../build.sbt | 4 + .../src/Task.scala | 12 ++ .../task-info.yaml | 8 ++ .../Option as an Alternative to null/task.md | 51 +++++++ .../test/TestSpec.scala | 8 ++ .../build.sbt | 4 + .../src/Task.scala | 3 + .../task-info.yaml | 8 ++ .../task.md | 48 +++++++ .../test/TestSpec.scala | 8 ++ Monads/Use Try Instead of try-catch/build.sbt | 4 + .../src/Task.scala | 18 +++ .../task-info.yaml | 8 ++ Monads/Use Try Instead of try-catch/task.md | 52 +++++++ .../test/TestSpec.scala | 8 ++ Monads/lesson-info.yaml | 6 + course-info.yaml | 1 + 27 files changed, 504 insertions(+) create mode 100644 Monads/Either as an Alternative to Exceptions/build.sbt create mode 100644 Monads/Either as an Alternative to Exceptions/src/Task.scala create mode 100644 Monads/Either as an Alternative to Exceptions/task-info.yaml create mode 100644 Monads/Either as an Alternative to Exceptions/task.md create mode 100644 Monads/Either as an Alternative to Exceptions/test/TestSpec.scala create mode 100644 Monads/Monadic Laws/build.sbt create mode 100644 Monads/Monadic Laws/src/Task.scala create mode 100644 Monads/Monadic Laws/task-info.yaml create mode 100644 Monads/Monadic Laws/task.md create mode 100644 Monads/Monadic Laws/test/TestSpec.scala create mode 100644 Monads/Option as an Alternative to null/build.sbt create mode 100644 Monads/Option as an Alternative to null/src/Task.scala create mode 100644 Monads/Option as an Alternative to null/task-info.yaml create mode 100644 Monads/Option as an Alternative to null/task.md create mode 100644 Monads/Option as an Alternative to null/test/TestSpec.scala create mode 100644 Monads/Syntactic Sugar and For-Comprehensions/build.sbt create mode 100644 Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala create mode 100644 Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml create mode 100644 Monads/Syntactic Sugar and For-Comprehensions/task.md create mode 100644 Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala create mode 100644 Monads/Use Try Instead of try-catch/build.sbt create mode 100644 Monads/Use Try Instead of try-catch/src/Task.scala create mode 100644 Monads/Use Try Instead of try-catch/task-info.yaml create mode 100644 Monads/Use Try Instead of try-catch/task.md create mode 100644 Monads/Use Try Instead of try-catch/test/TestSpec.scala create mode 100644 Monads/lesson-info.yaml diff --git a/Monads/Either as an Alternative to Exceptions/build.sbt b/Monads/Either as an Alternative to Exceptions/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Monads/Either as an Alternative to Exceptions/src/Task.scala b/Monads/Either as an Alternative to Exceptions/src/Task.scala new file mode 100644 index 00000000..2cda2cf9 --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/src/Task.scala @@ -0,0 +1,26 @@ +import scala.io.StdIn.readLine + +object Task : + + def readNumbers(x: String, y: String): Either[String, (Double, Double)] = + (x.toDoubleOption, y.toDoubleOption) match + case (Some(x), Some(y)) => Right (x, y) + case (None, Some(y)) => Left("First string is not a number") + case (Some(x), None) => Left("Second string is not a number") + case (None, None) => Left("Both strings are not numbers") + + def safeDiv(x: Double, y: Double): Either[String, Double] = + if (y == 0) Left("Division by zero") + else Right(x / y) + +// def safeDiv(x: Double, y: Double): Either[Throwable, Double] = +// if (y == 0) Left(new IllegalArgumentException("Division by zero")) +// else Right(x / y) + + @main + def main() = + val x = readLine() + val y = readLine() + print(readNumbers(x, y).flatMap(safeDiv)) + + diff --git a/Monads/Either as an Alternative to Exceptions/task-info.yaml b/Monads/Either as an Alternative to Exceptions/task-info.yaml new file mode 100644 index 00000000..4ef9637a --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/task-info.yaml @@ -0,0 +1,8 @@ +type: edu +files: + - name: src/Task.scala + visible: true + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Monads/Either as an Alternative to Exceptions/task.md b/Monads/Either as an Alternative to Exceptions/task.md new file mode 100644 index 00000000..8dbfaa24 --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/task.md @@ -0,0 +1,52 @@ +Sometimes you want to know a little more about the reason 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. +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. + +Like `Option`, `Either` is a monad that means it allows chaining of successful operations. +The convention is that the failure is represented with `Left`, while `Right` wraps the value computed in the case of success. +It's an arbitrary decision and everything would work the same way if we were to choose differently. +However, a useful mnemonic is that `Right` is for cases when everything went right. +Thus, `unit` 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. + +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. +We can implement this as a sequence of two functions: + +```scala 3 +def readNumbers(x: String, y: String): Either[String, (Double, Double)] = + (x.toDoubleOption, y.toDoubleOption) match + case (Some(x), Some(y)) => Right (x, y) + case (None, Some(y)) => Left("First string is not a number") + case (Some(x), None) => Left("Second string is not a number") + case (None, None) => Left("Both strings are not numbers") + +def safeDiv(x: Double, y: Double): Either[String, Double] = + if (y == 0) Left("Division by zero") + else Right(x / y) + +@main +def main() = + val x = readLine() + val y = readLine() + print(readNumbers(x, y).flatMap(safeDiv)) +``` + +Note that we have used `String` for errors here, but we could have used a custom data type. +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. + +```scala 3 +def safeDiv(x: Double, y: Double): Either[Throwable, Double] = + if (y == 0) Left(new IllegalArgumentException("Division by zero")) + else Right(x / y) +``` + + + + diff --git a/Monads/Either as an Alternative to Exceptions/test/TestSpec.scala b/Monads/Either as an Alternative to Exceptions/test/TestSpec.scala new file mode 100644 index 00000000..f73ac3d4 --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/test/TestSpec.scala @@ -0,0 +1,8 @@ +import org.scalatest.FunSuite + +class TestSpec extends FunSuite { + //TODO: implement your test here + test("First test") { + assert(false, "Tests not implemented for the task") + } +} diff --git a/Monads/Monadic Laws/build.sbt b/Monads/Monadic Laws/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Monads/Monadic Laws/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Monads/Monadic Laws/src/Task.scala b/Monads/Monadic Laws/src/Task.scala new file mode 100644 index 00000000..971c0b14 --- /dev/null +++ b/Monads/Monadic Laws/src/Task.scala @@ -0,0 +1 @@ +object Task: \ No newline at end of file diff --git a/Monads/Monadic Laws/task-info.yaml b/Monads/Monadic Laws/task-info.yaml new file mode 100644 index 00000000..8993dd57 --- /dev/null +++ b/Monads/Monadic Laws/task-info.yaml @@ -0,0 +1,8 @@ +type: theory +files: + - name: src/Task.scala + visible: true + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Monads/Monadic Laws/task.md b/Monads/Monadic Laws/task.md new file mode 100644 index 00000000..0dc85f96 --- /dev/null +++ b/Monads/Monadic Laws/task.md @@ -0,0 +1,134 @@ +There are multiple other 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. + +### Identity laws + +The first two properties are concerned with `unit`, the constructor to create monads. +Identity laws mean that there is a special value that does nothing when a binary operator is applied to it. +For example, `0 + x == x + 0 == x` for any possible number `x`. +Such an element may not exist for some operations, or it may only work on one side of the operator. +Consider subtraction, for which `x - 0 == x`, but `0 - x != x`. +As it happens, the unit 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 unit 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] + +Monad(v).flatMap(f) == f(v) +``` +The right identity law states that by passing the unit method into a `flatMap` is equivalent to not doing that at all. +This reflects the idea that unit only wraps whatever value it receives and produces no additional action. + +```scala 3 +val monad: Monad[_] = ... + +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. +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`. + +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. +This code fragment is equivalent to putting parentheses around the pipelined `mB` and `doSomething`. + +```scala 3 +mA.flatMap( a => + mB.flatMap( b => + doSomething(a, b) + ) +) +``` + +This can be refactored in the following form, using the unit of the corresponding monad. +Here we parenthesise the chaining of the two first monadic actions, and only then flatMap `doSomething` over the result. + +```scala 3 +mA.flatMap { a => + mB.flatMap(b => Monad((a, b))) +}.flatMap { case (a, b) => + doSomething(a, b) +} +``` + +We can make this code pretty by sprinkling some syntactic sugar. + +```scala 3 +for { + a <- mA + b <- mB + res <- doSomething(a, b) +} yield res +``` + +### 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. +The unit of `Option` is `{ x => Some(x) }`, while `flatMap` can be implemented in the following way. + +```scala 3 +def flatMap[B](f: A => Option[B]): Option[B] = this match { + case Some(b) => f(b) + case _ => None +} +``` + +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. + +Carefully considering the cases is how we prove associativity. + +1. If `mA == None`, both expressions are immediately `None`. +2. If `mA == Some(x)` and `mB == None`, then both expressions are eventually `None`. +3. If `mA == Some(x)` and `mB == Some(y)`, then the first expression results in `doSomething(x, y)`. Let's prove that the second expression is evaluated to the same value. + +```scala 3 +Some(x).flatMap { a => + Some(y).flatMap(b => Some((a, b))) +}.flatMap { case (a, b) => doSomething(a, b) } +``` + +This expression gets evaluated to: + +```scala +Some(b).flatMap(b => Some((x, b))) + .flatMap { case (a, b) => doSomething(a, b) } +``` + +Which is evaluated to: + +```scala 3 +Some(x, y).flatMap { case (a, b) => doSomething(a, b) } +``` + +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 `Option`, `Either`, and `Try` that are very similar in a way: all of them describe failing computations. +There are many other *computational effects* that are expressed via monads. +They include logging, reading from a global memory, state manipulation, non-determinism and many more. +We encourage you to explore these monads on your own. +Start with lists and see where it gets you. +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/Monadic Laws/test/TestSpec.scala b/Monads/Monadic Laws/test/TestSpec.scala new file mode 100644 index 00000000..f73ac3d4 --- /dev/null +++ b/Monads/Monadic Laws/test/TestSpec.scala @@ -0,0 +1,8 @@ +import org.scalatest.FunSuite + +class TestSpec extends FunSuite { + //TODO: implement your test here + test("First test") { + assert(false, "Tests not implemented for the task") + } +} diff --git a/Monads/Option as an Alternative to null/build.sbt b/Monads/Option as an Alternative to null/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Monads/Option as an Alternative to null/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Monads/Option as an Alternative to null/src/Task.scala b/Monads/Option as an Alternative to null/src/Task.scala new file mode 100644 index 00000000..ca05da37 --- /dev/null +++ b/Monads/Option as an Alternative to null/src/Task.scala @@ -0,0 +1,12 @@ +object Task: + + def div(x: Double, y: Double): Option[Double] = + if (y == 0) None + else Some(x / y) + + @main + def main() = + print(div(100, 2).flatMap { div(_, 4) }) + + Option(null).foreach { res => print(res) } + Option(42).foreach { res => print(res) } diff --git a/Monads/Option as an Alternative to null/task-info.yaml b/Monads/Option as an Alternative to null/task-info.yaml new file mode 100644 index 00000000..4ef9637a --- /dev/null +++ b/Monads/Option as an Alternative to null/task-info.yaml @@ -0,0 +1,8 @@ +type: edu +files: + - name: src/Task.scala + visible: true + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Monads/Option as an Alternative to null/task.md b/Monads/Option as an Alternative to null/task.md new file mode 100644 index 00000000..499fb7fd --- /dev/null +++ b/Monads/Option as an Alternative to null/task.md @@ -0,0 +1,51 @@ +Monad is a powerful concept popular in functional programming. +It's a design pattern capable of describing failing computations, managing state, and handling arbitrary side effects. +Unlike in Haskell, there is no specific Monad trait in the standard library of Scala. +Instead, a monad is a wrapper class that has a static method `unit` and implements `flatMap`. +The method `unit` accepts a value and creates a monad with it inside, while `flatMap` chains operations on the monad. +For a class to be a monad, it should satisfy a set of rules, called monadic laws. +We'll cover them at a later stage. +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. +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. +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. +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: + +```scala 3 +def div(x: Double, y: Double): Option[Double] = + if (y == 0) None + else Some(x / y) +``` + +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. +Instead, you can chain the operations using `flatMap`. +If any of the divisions fail, then the whole chain stops. + +```scala 3 +div(totalVisits, numberOfUsers).flatMap { div(_, numberOfDays) } +``` + +There is one more special case in Scala: if you pass `null` as an argument to the `Option` constructor, then you receive `None`. +You should avoid doing this explicitly, but when you need to call a third-party Java library, which can return `nulls`: + +```scala 3 +val result = javaLib.getSomethingOrNull(bar) +Option(result).foreach { res => + // will only be executed if the `result` is not null + } +``` + +In short, `None` indicates that something went wrong, and `flatMap` allows to chain function calls which do not fail. + diff --git a/Monads/Option as an Alternative to null/test/TestSpec.scala b/Monads/Option as an Alternative to null/test/TestSpec.scala new file mode 100644 index 00000000..f73ac3d4 --- /dev/null +++ b/Monads/Option as an Alternative to null/test/TestSpec.scala @@ -0,0 +1,8 @@ +import org.scalatest.FunSuite + +class TestSpec extends FunSuite { + //TODO: implement your test here + test("First test") { + assert(false, "Tests not implemented for the task") + } +} diff --git a/Monads/Syntactic Sugar and For-Comprehensions/build.sbt b/Monads/Syntactic Sugar and For-Comprehensions/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala b/Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala new file mode 100644 index 00000000..63d3f9a5 --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala @@ -0,0 +1,3 @@ +class Task { + //put your task here +} \ No newline at end of file diff --git a/Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml b/Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml new file mode 100644 index 00000000..4ef9637a --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml @@ -0,0 +1,8 @@ +type: edu +files: + - name: src/Task.scala + visible: true + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Monads/Syntactic Sugar and For-Comprehensions/task.md b/Monads/Syntactic Sugar and For-Comprehensions/task.md new file mode 100644 index 00000000..40b60ec3 --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/task.md @@ -0,0 +1,48 @@ +In 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: + +```scala 3 +val res = client.getTeamMembers(teamId).flatMap { members => + storage.getUserData(members.map(_.userId)).flatMap { users => + log(s”members: $members, users: $users”) + system.getPriorityLevels(teamId).flatMap { + case levels if levels.size > 1 => + doSomeStuffOrFail(members, users, levels) + case _ => + doSomeOtherStuffOrFail(members, users) + } + } +} +``` + +It doesn't look pretty, 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. +The same code can be written more succinctly using `for/yield`: + +```scala 3 +val res = for { + members <- client.getTeamMembers(teamId) + users <- storage.getUserData(members.map(_.userId)) + _ = log(s"members: $members, users: $users") + levels <- system.getPriorityLevels(teamId) +} yield + if (levels.size > 1) + doSomeStuffOrFail(members, users, levels) + else + doSomeOtherStuffOrFail(members, users) +``` + +Each line with a left arrow corresponds to a `flatMap` call, where the variable name to the left of the arrow represents the name of the variable in the lambda function. +We start by binding the successful results of retrieving team members with `members`, then get user data based on the members' ids and bind it with `users`. +Note that the first line in a for-comprehension must contain the left arrow. +This is how Scala compiler understands it is a monadic action, and what type it 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 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. + + diff --git a/Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala b/Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala new file mode 100644 index 00000000..f73ac3d4 --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala @@ -0,0 +1,8 @@ +import org.scalatest.FunSuite + +class TestSpec extends FunSuite { + //TODO: implement your test here + test("First test") { + assert(false, "Tests not implemented for the task") + } +} diff --git a/Monads/Use Try Instead of try-catch/build.sbt b/Monads/Use Try Instead of try-catch/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Monads/Use Try Instead of try-catch/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Monads/Use Try Instead of try-catch/src/Task.scala b/Monads/Use Try Instead of try-catch/src/Task.scala new file mode 100644 index 00000000..33d843a1 --- /dev/null +++ b/Monads/Use Try Instead of try-catch/src/Task.scala @@ -0,0 +1,18 @@ +import scala.util.* + +object Task: + + case class Result(text: String) + + val t: Try[Result] = + Try(javaLib.getSomethingOrThrowException(data)) + + t.recover { + case ex: IOException => defaultResult + } + + t.recoverWith { + case ex: IOException => + if (ignoreErrors) Success(defaultResult) + else Failure(ex) + } \ No newline at end of file diff --git a/Monads/Use Try Instead of try-catch/task-info.yaml b/Monads/Use Try Instead of try-catch/task-info.yaml new file mode 100644 index 00000000..4ef9637a --- /dev/null +++ b/Monads/Use Try Instead of try-catch/task-info.yaml @@ -0,0 +1,8 @@ +type: edu +files: + - name: src/Task.scala + visible: true + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Monads/Use Try Instead of try-catch/task.md b/Monads/Use Try Instead of try-catch/task.md new file mode 100644 index 00000000..fe04b50b --- /dev/null +++ b/Monads/Use Try Instead of try-catch/task.md @@ -0,0 +1,52 @@ +When all code is in 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: + +```scala 3 +def foo(data: Data): Either[Throwable, Result] = + try { + val res: Result = javaLib.getSomethingOrThrowException(data) + Right(res) + } catch { + case NonFatal(err) => Left(err) + } +``` + +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. +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: + +```scala 3 +def foo(data: Data): Try[Result] = + Try(javaLib.getSomethingOrThrowException(data)) +``` + +`Try` comes with two subclasses `Success[A]` and `Failure`, which are like the `Right[A]` and `Left[Throwable]` of `Either[Thowable, A]`. +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`. +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. + + +```scala 3 +val t: Try[Result] = + Try(javaLib.getSomethingOrThrowException(data)) + +t.recover { + case ex: IOException => defaultResult +} + +t.recoverWith { + case ex: IOException => + if (ignoreErrors) Success(defaultResult) + else Failure(ex) +} +``` + +To sum up, we strongly recommend that you use `Try` instead of `try/catch`. + + diff --git a/Monads/Use Try Instead of try-catch/test/TestSpec.scala b/Monads/Use Try Instead of try-catch/test/TestSpec.scala new file mode 100644 index 00000000..f73ac3d4 --- /dev/null +++ b/Monads/Use Try Instead of try-catch/test/TestSpec.scala @@ -0,0 +1,8 @@ +import org.scalatest.FunSuite + +class TestSpec extends FunSuite { + //TODO: implement your test here + test("First test") { + assert(false, "Tests not implemented for the task") + } +} diff --git a/Monads/lesson-info.yaml b/Monads/lesson-info.yaml new file mode 100644 index 00000000..ba921a99 --- /dev/null +++ b/Monads/lesson-info.yaml @@ -0,0 +1,6 @@ +content: + - Option as an Alternative to null + - Either as an Alternative to Exceptions + - Use Try Instead of try-catch + - Syntactic Sugar and For-Comprehensions + - Monadic Laws diff --git a/course-info.yaml b/course-info.yaml index a2d213aa..8173ec7e 100644 --- a/course-info.yaml +++ b/course-info.yaml @@ -18,6 +18,7 @@ content: - Immutability - Expressions over Statements - Early Returns + - Monads - Conclusion environment_settings: jvm_language_level: JDK_17 From 733b53b11f11fef6e56d065881e00491b102077b Mon Sep 17 00:00:00 2001 From: Ekaterina Verbitskaia Date: Wed, 29 May 2024 22:09:48 +0200 Subject: [PATCH 41/65] Monads module done --- .../src/Task.scala | 107 ++++++++++++++---- .../task-info.yaml | 13 +++ .../task.md | 13 +++ .../test/TestSpec.scala | 42 ++++++- Monads/Non-Determinism with Lists/build.sbt | 4 + .../Non-Determinism with Lists/src/Task.scala | 90 +++++++++++++++ .../Non-Determinism with Lists/task-info.yaml | 15 +++ Monads/Non-Determinism with Lists/task.md | 50 ++++++++ .../test/TestSpec.scala | 41 +++++++ .../src/Task.scala | 86 ++++++++++++-- .../task-info.yaml | 10 ++ .../Option as an Alternative to null/task.md | 12 ++ .../test/TestSpec.scala | 28 ++++- .../src/Task.scala | 102 ++++++++++++++++- .../task-info.yaml | 19 ++++ .../task.md | 6 + .../test/TestSpec.scala | 42 ++++++- .../src/Task.scala | 19 +--- .../task-info.yaml | 2 +- Monads/lesson-info.yaml | 1 + 20 files changed, 638 insertions(+), 64 deletions(-) create mode 100644 Monads/Non-Determinism with Lists/build.sbt create mode 100644 Monads/Non-Determinism with Lists/src/Task.scala create mode 100644 Monads/Non-Determinism with Lists/task-info.yaml create mode 100644 Monads/Non-Determinism with Lists/task.md create mode 100644 Monads/Non-Determinism with Lists/test/TestSpec.scala diff --git a/Monads/Either as an Alternative to Exceptions/src/Task.scala b/Monads/Either as an Alternative to Exceptions/src/Task.scala index 2cda2cf9..149de3f1 100644 --- a/Monads/Either as an Alternative to Exceptions/src/Task.scala +++ b/Monads/Either as an Alternative to Exceptions/src/Task.scala @@ -1,26 +1,95 @@ -import scala.io.StdIn.readLine +object Task: + class User(val name: String, val age: Int, val child: Option[User]) -object Task : + object UserService { + enum SearchError: + case NoUserFound(name: String) + case NoChildFound(name: String) + case NoGrandchildFound(userName: String, childName: String) - def readNumbers(x: String, y: String): Either[String, (Double, Double)] = - (x.toDoubleOption, y.toDoubleOption) match - case (Some(x), Some(y)) => Right (x, y) - case (None, Some(y)) => Left("First string is not a number") - case (Some(x), None) => Left("Second string is not a number") - case (None, None) => Left("Both strings are not numbers") + import SearchError.* - def safeDiv(x: Double, y: Double): Either[String, Double] = - if (y == 0) Left("Division by zero") - else Right(x / y) + /** + * Retrieves the user by their name. + * @param name the name of the user + * @return the user wrapped in `Right` if they exist, and `Left UserNotExist` otherwise. + */ + def loadUser(name: String): Either[SearchError, User] = + users.find(u => u.name == name) match + case None => Left(NoUserFound(name)) + case Some(u) => Right(u) -// def safeDiv(x: Double, y: Double): Either[Throwable, Double] = -// if (y == 0) Left(new IllegalArgumentException("Division by zero")) -// else Right(x / y) + /** + * Retrieves the grandchild of the user with the given name. + * @param name the name of the user + * @return the user's grandchild `Right` if they exist, and `Left` with an error otherwise. + */ + def getGrandchild(name: String): Either[SearchError, User] = + def getChild(user: User, error: SearchError): Either[SearchError, User] = + user.child match + case None => Left(error) + case Some(ch) => Right(ch) - @main - def main() = - val x = readLine() - val y = readLine() - print(readNumbers(x, y).flatMap(safeDiv)) + loadUser(name) + .flatMap(u => getChild(u, NoChildFound(name))) + .flatMap(ch => getChild(ch, NoGrandchildFound(name, ch.name))) + /** + * Retrieves the age of a grandchild of the user with the given name. + * @param name the name of the user + * @return the age of the user's grandchild wrapped in `Some` if they exist, and `None` otherwise + */ + def getGrandchildAge(name: String): Either[SearchError, Int] = + getGrandchild(name).flatMap( u => Right(u.age) ) + val users = { + val sofia = new User("Sofia", 10, None) + val takumi = new User("Takumi", 8, None) + val aisha = new User("Aisha", 6, None) + val anastasia = new User("Anastasia", 14, None) + val ivan = new User("Ivan", 45, Some(anastasia)) + val ife = new User("Ife", 7, None) + val omar = new User("Omar", 16, None) + val youssef = new User("Youssef", 50, Some(omar)) + val luca = new User("Luca", 11, None) + val karin = new User("Karin", 9, None) + val bao = new User("Bao", 4, None) + val priya = new User("Priya", 5, None) + val arjun = new User("Arjun", 12, None) + val milos = new User("Milos", 10, None) + val jelena = new User("Jelena", 36, Some(milos)) + val leila = new User("Leila", 15, None) + val eva = new User("Eva", 6, None) + val mateo = new User("Mateo", 13, None) + val nia = new User("Nia", 5, None) + val viktor = new User("Viktor", 11, None) + val tomas = new User("Tomas", 8, None) + val mei = new User("Mei", 9, None) + val jack = new User("Jack", 7, None) + val alejandro = new User("Alejandro", 40, Some(sofia)) + val hiroshi = new User("Hiroshi", 35, Some(takumi)) + val fatima = new User("Fatima", 28, Some(aisha)) + val marfa = new User("Marfa", 65, Some(ivan)) + val chinwe = new User("Chinwe", 32, Some(ife)) + val thu = new User("Thu", 82, Some(youssef)) + val giulia = new User("Giulia", 38, Some(luca)) + val sven = new User("Sven", 42, Some(karin)) + val linh = new User("Linh", 27, None) + val ravi = new User("Ravi", 33, None) + val maya = new User("Maya", 41, Some(arjun)) + val marko = new User("Marko", 59, Some(jelena)) + val mohammed = new User("Mohammed", 44, Some(leila)) + val anna = new User("Anna", 30, Some(eva)) + val carlos = new User("Carlos", 47, Some(mateo)) + val amina = new User("Amina", 29, None) + val igor = new User("Igor", 39, Some(viktor)) + val julia = new User("Julia", 34, Some(tomas)) + val chen = new User("Chen", 37, Some(mei)) + val emily = new User("Emily", 31, Some(jack)) + + List(alejandro, hiroshi, fatima, marfa, chinwe, thu, giulia, sven, linh, ravi, maya, marko, mohammed, anna, + carlos, amina, igor, julia, chen, emily, sofia, takumi, aisha, anastasia, ivan, ife, omar, youssef, luca, + karin, bao, priya, arjun, milos, jelena, leila, eva, mateo, nia, viktor, tomas, mei, jack + ) + } + } diff --git a/Monads/Either as an Alternative to Exceptions/task-info.yaml b/Monads/Either as an Alternative to Exceptions/task-info.yaml index 4ef9637a..a867a771 100644 --- a/Monads/Either as an Alternative to Exceptions/task-info.yaml +++ b/Monads/Either as an Alternative to Exceptions/task-info.yaml @@ -2,6 +2,19 @@ type: edu files: - name: src/Task.scala visible: true + placeholders: + - offset: 676 + length: 963 + placeholder_text: /* TODO */ + - offset: 676 + length: 963 + placeholder_text: /* TODO */ + - offset: 676 + length: 963 + placeholder_text: /* TODO */ + - offset: 676 + length: 963 + placeholder_text: /* TODO */ - name: test/TestSpec.scala visible: false - name: build.sbt diff --git a/Monads/Either as an Alternative to Exceptions/task.md b/Monads/Either as an Alternative to Exceptions/task.md index 8dbfaa24..5802e8ae 100644 --- a/Monads/Either as an Alternative to Exceptions/task.md +++ b/Monads/Either as an Alternative to Exceptions/task.md @@ -47,6 +47,19 @@ def safeDiv(x: Double, y: Double): Either[Throwable, Double] = else Right(x / y) ``` +### Exercise + +Let's get back to our `UserService` from the previous lesson. +There are three possible reasons why `getGrandchild` may fail: + +* The user with the given name can't be found. +* The user doesn't have a child. +* The user's child doesn't have a child. + +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. diff --git a/Monads/Either as an Alternative to Exceptions/test/TestSpec.scala b/Monads/Either as an Alternative to Exceptions/test/TestSpec.scala index f73ac3d4..3cca92a3 100644 --- a/Monads/Either as an Alternative to Exceptions/test/TestSpec.scala +++ b/Monads/Either as an Alternative to Exceptions/test/TestSpec.scala @@ -1,8 +1,40 @@ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite +import Task._ -class TestSpec extends FunSuite { - //TODO: implement your test here - test("First test") { - assert(false, "Tests not implemented for the task") +class TaskSpec extends AnyFunSuite { + + val daria = User("Daria", 13, None) + val sasha = User("Sasha", 33, Some(daria)) + val masha = User("Masha", 60, Some(sasha)) + + val users = UserService.users.concat(List(daria, sasha, masha)) + + def etalonGetGrandchild(name: String): Either[UserService.SearchError, User] = + def getChild(user: User, error: UserService.SearchError): Either[UserService.SearchError, User] = + user.child match + case None => Left(error) + case Some(ch) => Right(ch) + + UserService.loadUser(name) + .flatMap(u => getChild(u, UserService.SearchError.NoChildFound(name))) + .flatMap(ch => getChild(ch, UserService.SearchError.NoGrandchildFound(name, ch.name))) + + def etalonGetGrandchildAge(name: String): Either[UserService.SearchError, Int] = + etalonGetGrandchild(name).flatMap(u => Right(u.age)) + + test("getGrandchild should retrieve a grandchild of the user with the given name if they exist") { + users.foreach { u => + val name = u.name + assertResult(etalonGetGrandchild(name)) + (UserService.getGrandchild(name)) + } + } + + test("getGrandchildAge should retrieve the age of a grandchild") { + users.foreach { u => + val name = u.name + assertResult(etalonGetGrandchildAge(name)) + (UserService.getGrandchildAge(name)) + } } } diff --git a/Monads/Non-Determinism with Lists/build.sbt b/Monads/Non-Determinism with Lists/build.sbt new file mode 100644 index 00000000..4cc14e5d --- /dev/null +++ b/Monads/Non-Determinism with Lists/build.sbt @@ -0,0 +1,4 @@ +scalaSource in Compile := baseDirectory.value / "src" +scalaSource in Test := baseDirectory.value / "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" +scalaVersion := "3.2.0" \ No newline at end of file diff --git a/Monads/Non-Determinism with Lists/src/Task.scala b/Monads/Non-Determinism with Lists/src/Task.scala new file mode 100644 index 00000000..4ea032f9 --- /dev/null +++ b/Monads/Non-Determinism with Lists/src/Task.scala @@ -0,0 +1,90 @@ +object Task: + class User(val name: String, val age: Int, val children: Set[User]) + + object UserService { + /** + * Retrieves the user by their name. + * @param name the name of the user + * @return the user wrapped in `Right` if they exist, and `Left UserNotExist` otherwise. + */ + def loadUser(name: String): Option[User] = + users.find(u => u.name == name) + + /** + * Retrieves the grandchild of the user with the given name. + * @param name the name of the user + * @return the set of user's grandchildren. + */ + def getGrandchildren(name: String): Set[User] = + for { + user <- loadUser(name).toSet + child <- user.children + grandchild <- child.children + } yield grandchild + + /** + * Retrieves the age of a grandchild of the user with the given name. + * @param name the name of the user + * @return the list of all ages of the the user's grandchildren. + */ + def getGrandchildrenAges(name: String): List[Int] = + for { + grandchild <- getGrandchildren(name).toList + } yield grandchild.age + + val users = { + val sofia = new User("Sofia", 10, Set.empty) + val takumi = new User("Takumi", 8, Set.empty) + val aisha = new User("Aisha", 6, Set.empty) + val anastasia = new User("Anastasia", 14, Set.empty) + val maria = new User("Maria", 12, Set.empty) + val ivan = new User("Ivan", 45, Set(anastasia, maria)) + val ife = new User("Ife", 7, Set.empty) + val omar = new User("Omar", 16, Set.empty) + val youssef = new User("Youssef", 50, Set(omar)) + val luca = new User("Luca", 11, Set.empty) + val karin = new User("Karin", 9, Set.empty) + val bao = new User("Bao", 4, Set.empty) + val priya = new User("Priya", 5, Set.empty) + val arjun = new User("Arjun", 12, Set.empty) + val milos = new User("Milos", 10, Set.empty) + val milica = new User("Milica", 10, Set.empty) + val zoran = new User("Zoran", 8, Set.empty) + val jelena = new User("Jelena", 36, Set(milos, milica, zoran)) + val dragoljub = new User("Dragoljub", 6, Set.empty) + val miroslav = new User("Miroslav", 35, Set(dragoljub)) + val leila = new User("Leila", 15, Set.empty) + val eva = new User("Eva", 6, Set.empty) + val mateo = new User("Mateo", 13, Set.empty) + val nia = new User("Nia", 5, Set.empty) + val viktor = new User("Viktor", 11, Set.empty) + val tomas = new User("Tomas", 8, Set.empty) + val mei = new User("Mei", 9, Set.empty) + val jack = new User("Jack", 7, Set.empty) + val alejandro = new User("Alejandro", 40, Set(sofia)) + val hiroshi = new User("Hiroshi", 35, Set(takumi)) + val fatima = new User("Fatima", 28, Set(aisha)) + val marfa = new User("Marfa", 65, Set(ivan)) + val chinwe = new User("Chinwe", 32, Set(ife)) + val thu = new User("Thu", 82, Set(youssef)) + val giulia = new User("Giulia", 38, Set(luca)) + val sven = new User("Sven", 42, Set(karin)) + val linh = new User("Linh", 27, Set.empty) + val ravi = new User("Ravi", 33, Set.empty) + val maya = new User("Maya", 41, Set(arjun)) + val marko = new User("Marko", 59, Set(jelena, miroslav)) + val mohammed = new User("Mohammed", 44, Set(leila)) + val anna = new User("Anna", 30, Set(eva)) + val carlos = new User("Carlos", 47, Set(mateo)) + val amina = new User("Amina", 29, Set.empty) + val igor = new User("Igor", 39, Set(viktor)) + val julia = new User("Julia", 34, Set(tomas)) + val chen = new User("Chen", 37, Set(mei)) + val emily = new User("Emily", 31, Set(jack)) + + List(alejandro, hiroshi, fatima, marfa, chinwe, thu, giulia, sven, linh, ravi, maya, marko, mohammed, anna, + carlos, amina, igor, julia, chen, emily, sofia, takumi, aisha, anastasia, ivan, ife, omar, youssef, luca, + karin, bao, priya, arjun, milos, jelena, leila, eva, mateo, nia, viktor, tomas, mei, jack + ) + } + } \ No newline at end of file diff --git a/Monads/Non-Determinism with Lists/task-info.yaml b/Monads/Non-Determinism with Lists/task-info.yaml new file mode 100644 index 00000000..c3391767 --- /dev/null +++ b/Monads/Non-Determinism with Lists/task-info.yaml @@ -0,0 +1,15 @@ +type: edu +files: + - name: src/Task.scala + visible: true + placeholders: + - offset: 610 + length: 146 + placeholder_text: /* TODO */ + - offset: 1019 + length: 86 + placeholder_text: /* TODO */ + - name: test/TestSpec.scala + visible: false + - name: build.sbt + visible: false diff --git a/Monads/Non-Determinism with Lists/task.md b/Monads/Non-Determinism with Lists/task.md new file mode 100644 index 00000000..1e4e00b0 --- /dev/null +++ b/Monads/Non-Determinism with Lists/task.md @@ -0,0 +1,50 @@ +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. +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. +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. + +```scala +// The non-deterministic function to compute all factors of a number +def factors(n: Int): List[Int] = { + (1 to n).filter(n % _ == 0).toList +} + +// factors(42) == List(1, 2, 3, 6, 7, 14, 21, 42) +``` + +Let's now discuss the List monad. +The unit 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)`. + +`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 be sensible to use `Set`, because we only care about unique factors. + +```scala +// The non-deterministic function to compute all factors of a number +def factors(n: Int): Set[Int] = { + (1 to n).filter(n % _ == 0).toSet +} + +// factors(42) == Set(1, 2, 3, 6, 7, 14, 21, 42) +// factors(6).flatMap(factors) == Set(1, 2, 4) +``` + +### Exercise + +To make our model of users a little more realistic, we should take into an account that a user may have many children. +This makes our `getGrandchild` function non-deterministic. +Let's reflect that in the names, types, and the 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. diff --git a/Monads/Non-Determinism with Lists/test/TestSpec.scala b/Monads/Non-Determinism with Lists/test/TestSpec.scala new file mode 100644 index 00000000..dad41e18 --- /dev/null +++ b/Monads/Non-Determinism with Lists/test/TestSpec.scala @@ -0,0 +1,41 @@ +import org.scalatest.funsuite.AnyFunSuite +import Task._ + +class TaskSpec extends AnyFunSuite { + + val daria = User("Daria", 13, Set.empty) + val sasha = User("Sasha", 33, Set(daria)) + val masha = User("Masha", 60, Set(sasha)) + + val users = UserService.users.concat(List(daria, sasha, masha)) + + def etalonGetGrandchildren(name: String): Set[User] = + for { + user <- UserService.loadUser(name).toSet + child <- user.children + grandchild <- child.children + } yield grandchild + + def etalonGetGrandchildrenAges(name: String): List[Int] = + for { + grandchild <- etalonGetGrandchildren(name).toList + } yield grandchild.age + + + + test("getGrandchildren should retrieve a set of grandchildren of the user with the given name") { + users.foreach { u => + val name = u.name + assertResult(etalonGetGrandchildren(name)) + (UserService.getGrandchildren(name)) + } + } + + test("getGrandchildrenAges should retrieve the ages of grandchild") { + users.foreach { u => + val name = u.name + assertResult(etalonGetGrandchildrenAges(name)) + (UserService.getGrandchildrenAges(name)) + } + } +} diff --git a/Monads/Option as an Alternative to null/src/Task.scala b/Monads/Option as an Alternative to null/src/Task.scala index ca05da37..d1cc1152 100644 --- a/Monads/Option as an Alternative to null/src/Task.scala +++ b/Monads/Option as an Alternative to null/src/Task.scala @@ -1,12 +1,82 @@ object Task: + class User(val name: String, val age: Int, val child: Option[User]) - def div(x: Double, y: Double): Option[Double] = - if (y == 0) None - else Some(x / y) + object UserService { + /** + * Retrieves the user by their name. + * + * @param name the name of the user + * @return the user wrapped in `Some` if they exist, and `None` otherwise + */ + def loadUser(name: String): Option[User] = + users.find(u => u.name == name) - @main - def main() = - print(div(100, 2).flatMap { div(_, 4) }) + /** + * Retrieves the grandchild of the user with the given name. + * + * @param name the name of the user + * @return the user's grandchild wrapped in `Some` if they exist, and `None` otherwise + */ + def getGrandchild(name: String): Option[User] = + loadUser(name).flatMap(_.child).flatMap(_.child) - Option(null).foreach { res => print(res) } - Option(42).foreach { res => print(res) } + /** + * Retrieves the age of a grandchild of the user with the given name. + * + * @param name the name of the user + * @return the age of the user's grandchild wrapped in `Some` if they exist, and `None` otherwise + */ + def getGrandchildAge(name: String): Option[Int] = + getGrandchild(name).flatMap(u => Option(u.age)) + + val users = { + val sofia = new User("Sofia", 10, None) + val takumi = new User("Takumi", 8, None) + val aisha = new User("Aisha", 6, None) + val anastasia = new User("Anastasia", 14, None) + val ivan = new User("Ivan", 45, Some(anastasia)) + val ife = new User("Ife", 7, None) + val omar = new User("Omar", 16, None) + val youssef = new User("Youssef", 50, Some(omar)) + val luca = new User("Luca", 11, None) + val karin = new User("Karin", 9, None) + val bao = new User("Bao", 4, None) + val priya = new User("Priya", 5, None) + val arjun = new User("Arjun", 12, None) + val milos = new User("Milos", 10, None) + val jelena = new User("Jelena", 36, Some(milos)) + val leila = new User("Leila", 15, None) + val eva = new User("Eva", 6, None) + val mateo = new User("Mateo", 13, None) + val nia = new User("Nia", 5, None) + val viktor = new User("Viktor", 11, None) + val tomas = new User("Tomas", 8, None) + val mei = new User("Mei", 9, None) + val jack = new User("Jack", 7, None) + val alejandro = new User("Alejandro", 40, Some(sofia)) + val hiroshi = new User("Hiroshi", 35, Some(takumi)) + val fatima = new User("Fatima", 28, Some(aisha)) + val marfa = new User("Marfa", 65, Some(ivan)) + val chinwe = new User("Chinwe", 32, Some(ife)) + val thu = new User("Thu", 82, Some(youssef)) + val giulia = new User("Giulia", 38, Some(luca)) + val sven = new User("Sven", 42, Some(karin)) + val linh = new User("Linh", 27, None) + val ravi = new User("Ravi", 33, None) + val maya = new User("Maya", 41, Some(arjun)) + val marko = new User("Marko", 59, Some(jelena)) + val mohammed = new User("Mohammed", 44, Some(leila)) + val anna = new User("Anna", 30, Some(eva)) + val carlos = new User("Carlos", 47, Some(mateo)) + val amina = new User("Amina", 29, None) + val igor = new User("Igor", 39, Some(viktor)) + val julia = new User("Julia", 34, Some(tomas)) + val chen = new User("Chen", 37, Some(mei)) + val emily = new User("Emily", 31, Some(jack)) + + List(alejandro, hiroshi, fatima, marfa, chinwe, thu, giulia, sven, linh, ravi, maya, marko, mohammed, anna, + carlos, amina, igor, julia, chen, emily, sofia, takumi, aisha, anastasia, ivan, ife, omar, youssef, luca, + karin, bao, priya, arjun, milos, jelena, leila, eva, mateo, nia, viktor, tomas, mei, jack + ) + } + } diff --git a/Monads/Option as an Alternative to null/task-info.yaml b/Monads/Option as an Alternative to null/task-info.yaml index 4ef9637a..05caa757 100644 --- a/Monads/Option as an Alternative to null/task-info.yaml +++ b/Monads/Option as an Alternative to null/task-info.yaml @@ -2,6 +2,16 @@ type: edu files: - name: src/Task.scala visible: true + placeholders: + - offset: 675 + length: 7 + placeholder_text: /* TODO */ + - offset: 692 + length: 7 + placeholder_text: /* TODO */ + - offset: 1001 + length: 47 + placeholder_text: /* TODO */ - name: test/TestSpec.scala visible: false - name: build.sbt diff --git a/Monads/Option as an Alternative to null/task.md b/Monads/Option as an Alternative to null/task.md index 499fb7fd..003f1e6d 100644 --- a/Monads/Option as an Alternative to null/task.md +++ b/Monads/Option as an Alternative to null/task.md @@ -49,3 +49,15 @@ Option(result).foreach { res => In short, `None` indicates that something went wrong, and `flatMap` allows to chain function calls which do not fail. +### Exercise + +Let's consider users who are represented with a `User` class. +Each user has a name, an age, and possibly 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 only fill in what functions these are. + +Then implement `getGrandchildAge` which returns the age of the grandchild if they exist. +Use `flatMap` here, avoid pattern matching. + diff --git a/Monads/Option as an Alternative to null/test/TestSpec.scala b/Monads/Option as an Alternative to null/test/TestSpec.scala index f73ac3d4..59195688 100644 --- a/Monads/Option as an Alternative to null/test/TestSpec.scala +++ b/Monads/Option as an Alternative to null/test/TestSpec.scala @@ -1,8 +1,26 @@ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite +import Task._ -class TestSpec extends FunSuite { - //TODO: implement your test here - test("First test") { - assert(false, "Tests not implemented for the task") +class TaskSpec extends AnyFunSuite { + val daria = User("Daria", 13, None) + val sasha = User("Sasha", 33, Some(daria)) + val masha = User("Masha", 60, Some(sasha)) + + val users = UserService.users.concat(List(daria, sasha, masha)) + + test("getGrandchild should retrieve a grandchild of the user with the given name if they exist") { + users.foreach { u => + val name = u.name + assertResult(UserService.loadUser(name).flatMap(_.child).flatMap(_.child)) + (UserService.getGrandchild(name)) + } + } + + test("getGrandchildAge should retrieve the age of a grandchild") { + users.foreach { u => + val name = u.name + assertResult(UserService.loadUser(name).flatMap(_.child).flatMap(_.child).flatMap(u => Option(u.age))) + (UserService.getGrandchildAge(name)) + } } } diff --git a/Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala b/Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala index 63d3f9a5..cb11042e 100644 --- a/Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala +++ b/Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala @@ -1,3 +1,99 @@ -class Task { - //put your task here -} \ No newline at end of file +object Task: + class User(val name: String, val age: Int, val child: Option[User]) + + object UserService { + enum SearchError: + case NoUserFound(name: String) + case NoChildFound(name: String) + case NoGrandchildFound(userName: String, childName: String) + + import SearchError.* + + /** + * Retrieves the user by their name. + * @param name the name of the user + * @return the user wrapped in `Right` if they exist, and `Left UserNotExist` otherwise. + */ + def loadUser(name: String): Either[SearchError, User] = + users.find(u => u.name == name) match + case None => Left(NoUserFound(name)) + case Some(u) => Right(u) + + /** + * Retrieves the grandchild of the user with the given name. + * @param name the name of the user + * @return the user's grandchild `Right` if they exist, and `Left` with an error otherwise. + */ + def getGrandchild(name: String): Either[SearchError, User] = + def getChild(user: User, error: SearchError): Either[SearchError, User] = + user.child match + case None => Left(error) + case Some(ch) => Right(ch) + + for { + user <- loadUser(name) + child <- getChild(user, NoChildFound(name)) + grandchild <- getChild(child, NoGrandchildFound(name, child.name)) + } yield grandchild + + /** + * Retrieves the age of a grandchild of the user with the given name. + * @param name the name of the user + * @return the age of the user's grandchild wrapped in `Some` if they exist, and `None` otherwise + */ + def getGrandchildAge(name: String): Either[SearchError, Int] = + for { + grandchild <- getGrandchild(name) + } yield grandchild.age + + val users = { + val sofia = new User("Sofia", 10, None) + val takumi = new User("Takumi", 8, None) + val aisha = new User("Aisha", 6, None) + val anastasia = new User("Anastasia", 14, None) + val ivan = new User("Ivan", 45, Some(anastasia)) + val ife = new User("Ife", 7, None) + val omar = new User("Omar", 16, None) + val youssef = new User("Youssef", 50, Some(omar)) + val luca = new User("Luca", 11, None) + val karin = new User("Karin", 9, None) + val bao = new User("Bao", 4, None) + val priya = new User("Priya", 5, None) + val arjun = new User("Arjun", 12, None) + val milos = new User("Milos", 10, None) + val jelena = new User("Jelena", 36, Some(milos)) + val leila = new User("Leila", 15, None) + val eva = new User("Eva", 6, None) + val mateo = new User("Mateo", 13, None) + val nia = new User("Nia", 5, None) + val viktor = new User("Viktor", 11, None) + val tomas = new User("Tomas", 8, None) + val mei = new User("Mei", 9, None) + val jack = new User("Jack", 7, None) + val alejandro = new User("Alejandro", 40, Some(sofia)) + val hiroshi = new User("Hiroshi", 35, Some(takumi)) + val fatima = new User("Fatima", 28, Some(aisha)) + val marfa = new User("Marfa", 65, Some(ivan)) + val chinwe = new User("Chinwe", 32, Some(ife)) + val thu = new User("Thu", 82, Some(youssef)) + val giulia = new User("Giulia", 38, Some(luca)) + val sven = new User("Sven", 42, Some(karin)) + val linh = new User("Linh", 27, None) + val ravi = new User("Ravi", 33, None) + val maya = new User("Maya", 41, Some(arjun)) + val marko = new User("Marko", 59, Some(jelena)) + val mohammed = new User("Mohammed", 44, Some(leila)) + val anna = new User("Anna", 30, Some(eva)) + val carlos = new User("Carlos", 47, Some(mateo)) + val amina = new User("Amina", 29, None) + val igor = new User("Igor", 39, Some(viktor)) + val julia = new User("Julia", 34, Some(tomas)) + val chen = new User("Chen", 37, Some(mei)) + val emily = new User("Emily", 31, Some(jack)) + + List(alejandro, hiroshi, fatima, marfa, chinwe, thu, giulia, sven, linh, ravi, maya, marko, mohammed, anna, + carlos, amina, igor, julia, chen, emily, sofia, takumi, aisha, anastasia, ivan, ife, omar, youssef, luca, + karin, bao, priya, arjun, milos, jelena, leila, eva, mateo, nia, viktor, tomas, mei, jack + ) + } + } diff --git a/Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml b/Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml index 4ef9637a..f5f03821 100644 --- a/Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml +++ b/Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml @@ -2,6 +2,25 @@ type: edu files: - name: src/Task.scala visible: true + placeholders: + - offset: 1171 + length: 14 + placeholder_text: /* TODO */ + - offset: 1208 + length: 34 + placeholder_text: /* TODO */ + - offset: 1265 + length: 52 + placeholder_text: /* TODO */ + - offset: 1332 + length: 10 + placeholder_text: /* TODO */ + - offset: 1663 + length: 33 + placeholder_text: /* TODO */ + - offset: 1711 + length: 14 + placeholder_text: /* TODO */ - name: test/TestSpec.scala visible: false - name: build.sbt diff --git a/Monads/Syntactic Sugar and For-Comprehensions/task.md b/Monads/Syntactic Sugar and For-Comprehensions/task.md index 40b60ec3..605cbcfd 100644 --- a/Monads/Syntactic Sugar and For-Comprehensions/task.md +++ b/Monads/Syntactic Sugar and For-Comprehensions/task.md @@ -45,4 +45,10 @@ We also don't care about the value returned by `log` and because of that use the 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. +### Exercise + +Use for-comprehensions to implement `getGrandchild` and `getGrandchildAge` from the previous exercise. + + + diff --git a/Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala b/Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala index f73ac3d4..3cca92a3 100644 --- a/Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala +++ b/Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala @@ -1,8 +1,40 @@ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite +import Task._ -class TestSpec extends FunSuite { - //TODO: implement your test here - test("First test") { - assert(false, "Tests not implemented for the task") +class TaskSpec extends AnyFunSuite { + + val daria = User("Daria", 13, None) + val sasha = User("Sasha", 33, Some(daria)) + val masha = User("Masha", 60, Some(sasha)) + + val users = UserService.users.concat(List(daria, sasha, masha)) + + def etalonGetGrandchild(name: String): Either[UserService.SearchError, User] = + def getChild(user: User, error: UserService.SearchError): Either[UserService.SearchError, User] = + user.child match + case None => Left(error) + case Some(ch) => Right(ch) + + UserService.loadUser(name) + .flatMap(u => getChild(u, UserService.SearchError.NoChildFound(name))) + .flatMap(ch => getChild(ch, UserService.SearchError.NoGrandchildFound(name, ch.name))) + + def etalonGetGrandchildAge(name: String): Either[UserService.SearchError, Int] = + etalonGetGrandchild(name).flatMap(u => Right(u.age)) + + test("getGrandchild should retrieve a grandchild of the user with the given name if they exist") { + users.foreach { u => + val name = u.name + assertResult(etalonGetGrandchild(name)) + (UserService.getGrandchild(name)) + } + } + + test("getGrandchildAge should retrieve the age of a grandchild") { + users.foreach { u => + val name = u.name + assertResult(etalonGetGrandchildAge(name)) + (UserService.getGrandchildAge(name)) + } } } diff --git a/Monads/Use Try Instead of try-catch/src/Task.scala b/Monads/Use Try Instead of try-catch/src/Task.scala index 33d843a1..971c0b14 100644 --- a/Monads/Use Try Instead of try-catch/src/Task.scala +++ b/Monads/Use Try Instead of try-catch/src/Task.scala @@ -1,18 +1 @@ -import scala.util.* - -object Task: - - case class Result(text: String) - - val t: Try[Result] = - Try(javaLib.getSomethingOrThrowException(data)) - - t.recover { - case ex: IOException => defaultResult - } - - t.recoverWith { - case ex: IOException => - if (ignoreErrors) Success(defaultResult) - else Failure(ex) - } \ No newline at end of file +object Task: \ No newline at end of file diff --git a/Monads/Use Try Instead of try-catch/task-info.yaml b/Monads/Use Try Instead of try-catch/task-info.yaml index 4ef9637a..8993dd57 100644 --- a/Monads/Use Try Instead of try-catch/task-info.yaml +++ b/Monads/Use Try Instead of try-catch/task-info.yaml @@ -1,4 +1,4 @@ -type: edu +type: theory files: - name: src/Task.scala visible: true diff --git a/Monads/lesson-info.yaml b/Monads/lesson-info.yaml index ba921a99..55123e0d 100644 --- a/Monads/lesson-info.yaml +++ b/Monads/lesson-info.yaml @@ -3,4 +3,5 @@ content: - Either as an Alternative to Exceptions - Use Try Instead of try-catch - Syntactic Sugar and For-Comprehensions + - Non-Determinism with Lists - Monadic Laws From 29c72319b79159428d877a2c0737208c328d51d1 Mon Sep 17 00:00:00 2001 From: Ekaterina Verbitskaia Date: Thu, 30 May 2024 13:00:32 +0200 Subject: [PATCH 42/65] Lessons titles removed. --- Early Returns/Baby Steps/task.md | 6 ++-- Early Returns/Breaking Boundaries/task.md | 4 +-- .../Lazy Collection to the Rescue/task.md | 4 +-- Early Returns/The Problem/task.md | 2 -- Early Returns/Unapply/task.md | 4 +-- .../Nested Methods/task.md | 4 +-- .../Pure vs Impure Functions/task.md | 4 +-- Expressions over Statements/Recursion/task.md | 4 +-- .../Tail Recursion/task.md | 4 +-- .../task.md | 2 +- .../What is an Expression/task.md | 4 +-- Functions as Data/anonymous_functions/task.md | 2 -- Functions as Data/filter/task.md | 2 -- Functions as Data/find/task.md | 2 -- Functions as Data/foldLeft/task.md | 2 -- Functions as Data/foreach/task.md | 2 -- .../functions_returning_functions/task.md | 2 -- Functions as Data/map/task.md | 2 -- .../partial_fucntion_application/task.md | 1 - .../passing_functions_as_arguments/task.md | 2 -- .../scala_collections_overview/task.md | 2 -- .../total_and_partial_functions/task.md | 1 - Functions as Data/what_is_a_function/task.md | 2 -- Immutability/A View/task.md | 2 +- Immutability/Berliner Pattern/task.md | 4 +-- Immutability/Case Class Copy/task.md | 4 +-- .../task.md | 2 -- Immutability/Lazy List/task.md | 4 +-- .../task.md | 2 -- Immutability/The Builder Pattern/task.md | 4 +-- Introduction/Getting to Know You/task.md | 2 -- .../Join Our Discord Community/task.md | 2 -- .../task.md | 32 +++++++++---------- Monads/Monadic Laws/task.md | 19 ++++++----- Monads/Non-Determinism with Lists/task.md | 6 ++-- .../Option as an Alternative to null/task.md | 10 +++--- .../task.md | 9 +++--- .../A Custom unapply Method/task.md | 2 -- Pattern Matching/Case Class/task.md | 1 - Pattern Matching/Case Objects/task.md | 3 -- Pattern Matching/Destructuring/task.md | 2 -- Pattern Matching/Enums/task.md | 2 -- Pattern Matching/Pattern Matching/task.md | 2 -- .../Sealed Traits Hierarchies/task.md | 2 -- .../task.md | 2 -- Pattern Matching/The Newtype Pattern/task.md | 2 -- 46 files changed, 54 insertions(+), 130 deletions(-) diff --git a/Early Returns/Baby Steps/task.md b/Early Returns/Baby Steps/task.md index 0b2c0ff0..2e24e062 100644 --- a/Early Returns/Baby Steps/task.md +++ b/Early Returns/Baby Steps/task.md @@ -1,5 +1,3 @@ -## Baby Steps - 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. Accessing this database is resource-intensive, and the user data is extensive. @@ -106,12 +104,12 @@ In the next lesson, we'll use a custom `unapply` method to eliminate the need fo ``` -### Exercise +## Exercise 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 numerous mistakes in the database made by a previous employee: there are short-haired Maine Coons, long-haired Sphynxes, and other inconsistensies. +You notice numerous mistakes in the database made by a previous employee: there are short-haired Maine Coons, long-haired Sphynxes, and other inconsistencies. 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. diff --git a/Early Returns/Breaking Boundaries/task.md b/Early Returns/Breaking Boundaries/task.md index 3625cd19..a233158e 100644 --- a/Early Returns/Breaking Boundaries/task.md +++ b/Early Returns/Breaking Boundaries/task.md @@ -1,5 +1,3 @@ -## 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 non-local returns. @@ -28,7 +26,7 @@ Sometimes, there are multiple boundaries, and in such cases, one can add labels This is especially important when there are embedded loops. An example of using labels can be found [here](https://gist.github.com/bishabosha/95880882ee9ba6c53681d21c93d24a97). -### Exercise +## Exercise Finally, let's use boundaries to achieve the same result. diff --git a/Early Returns/Lazy Collection to the Rescue/task.md b/Early Returns/Lazy Collection to the Rescue/task.md index 817e471d..a57270ff 100644 --- a/Early Returns/Lazy Collection to the Rescue/task.md +++ b/Early Returns/Lazy Collection to the Rescue/task.md @@ -1,5 +1,3 @@ -## 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 for access. Instead, it stores a way to compute an element once it's needed somewhere. @@ -20,7 +18,7 @@ Try comparing the two approaches on your own. .flatten ``` -### Exercise +## Exercise Let's try using a lazy collection to achieve the same goal as in the previous tasks. diff --git a/Early Returns/The Problem/task.md b/Early Returns/The Problem/task.md index 71b8e6d7..39645c87 100644 --- a/Early Returns/The Problem/task.md +++ b/Early Returns/The Problem/task.md @@ -1,5 +1,3 @@ -## 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 single key, and there was no point in continuing the search in the box after one had been found. diff --git a/Early Returns/Unapply/task.md b/Early Returns/Unapply/task.md index 45ba11e2..43dd14f7 100644 --- a/Early Returns/Unapply/task.md +++ b/Early Returns/Unapply/task.md @@ -1,5 +1,3 @@ -## Unapply - Unapply methods form the basis of pattern matching. Their goal is to extract data encapsulated in objects. We can create a custom extractor object for user data validation with a suitable unapply method, for example: @@ -135,7 +133,7 @@ the `Deconstruct` trait during pattern matching: } ``` -### Exercise +## Exercise You have noticed that the first cat found with a valid fur pattern has already been adopted. Now you need to include a check in the validation to ensure that the cat is still in the shelter. diff --git a/Expressions over Statements/Nested Methods/task.md b/Expressions over Statements/Nested Methods/task.md index 3bed7218..e9640a91 100644 --- a/Expressions over Statements/Nested Methods/task.md +++ b/Expressions over Statements/Nested Methods/task.md @@ -1,5 +1,3 @@ -## Nested Methods - In Scala, it's possible to define methods within other methods. This is useful when you have a function that is only intended for one-time use. For example, you may wish to implement the factorial function using an accumulator to enhance the program's efficiency. @@ -88,6 +86,6 @@ val numberOfWhiteOrGingerKittens = .count(cat => cat.age <= 1) ``` -### Exercise +## Exercise Explore the scopes of the nested methods. Make the code in the file `NestedTask.scala` compile. diff --git a/Expressions over Statements/Pure vs Impure Functions/task.md b/Expressions over Statements/Pure vs Impure Functions/task.md index 52942bea..a141342c 100644 --- a/Expressions over Statements/Pure vs Impure Functions/task.md +++ b/Expressions over Statements/Pure vs Impure Functions/task.md @@ -1,5 +1,3 @@ -## Pure vs Impure Functions - Not all functions are created equal; some of them are better than others. A large group of such superior functions are designated as *pure*. A pure function always yields the same value if given the same inputs. @@ -47,7 +45,7 @@ def g(x: Int): Int = gPure(x, y) ``` -### Exercise +## Exercise Implement the pure function `calculateAndLogPure`, which does the same thing as `calculateAndLogImpure`, but without using a global variable. diff --git a/Expressions over Statements/Recursion/task.md b/Expressions over Statements/Recursion/task.md index f8f1b8ab..30558bdc 100644 --- a/Expressions over Statements/Recursion/task.md +++ b/Expressions over Statements/Recursion/task.md @@ -1,5 +1,3 @@ -## Recursion - *To understand recursion, one must first understand recursion* This topic should be familiar to anyone who delved into functional programming, but we would like to review it once again. @@ -119,7 +117,7 @@ The recursion in the data type points us to a *smaller* instance of the problem We then sum the values returned from the recursive calls, producing the final result. There are no `Tree`s in a `Leaf`, therefore, we know it is the base case and the problem can be solved right away. -### Exercise +## Exercise Implement a tiny calculator `eval` and a printer `prefixPrinter` for arithmetic expressions. An expression is presented as its abstract syntax tree, where leaves contain numbers, while nodes correspond to the diff --git a/Expressions over Statements/Tail Recursion/task.md b/Expressions over Statements/Tail Recursion/task.md index b5cc6834..bb104a5c 100644 --- a/Expressions over Statements/Tail Recursion/task.md +++ b/Expressions over Statements/Tail Recursion/task.md @@ -1,5 +1,3 @@ -## Tail Recursion - A common criticism of using recursion is that it is intrinsically slow. Why is that? The clue is in what is known as the call stack. @@ -93,7 +91,7 @@ It checks if your implementation is tail-recursive and triggers a compiler error We recommend using this annotation to ensure that the compiler is capable of optimizing your code, even through future changes. -### Exercise +## Exercise Implement tail-recursive functions for reversing a list and finding the sum of digits in a non-negative number. We've annotated the helper functions with `@tailrec` so that the compiler can verify this property for us. 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 6b322a42..23b88e82 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 @@ -86,7 +86,7 @@ def main() = println(s"${max("b", "aa")(using StringLengthComparator)}") // prints aa ``` -### Exercise +## Exercise Implement a `sort` function to sort an array of values based on the `Comparator` provided. Make it use a contextual parameter to avoid carrying around the immutable context. You can use any kind of sorting algorithm. diff --git a/Expressions over Statements/What is an Expression/task.md b/Expressions over Statements/What is an Expression/task.md index 8f684864..8c063feb 100644 --- a/Expressions over Statements/What is an Expression/task.md +++ b/Expressions over Statements/What is an Expression/task.md @@ -1,5 +1,3 @@ -## What is an Expression? - When programming in an imperative style, we tend to build functions out of statements. We instruct the compiler on the exact steps and the order in which they should be performed to achieve the desired result. @@ -75,7 +73,7 @@ def main(): Unit = { This way, you separate the logic of computing values from outputting them. It also makes your code more readable. -### Exercise +## Exercise Rewrite the `abs` and `concatStrings` functions as expressions to perform the same tasks as their original implementations. Implement the `sumOfAbsoluteDifferences` and `longestCommonPrefix` functions using the expression style. diff --git a/Functions as Data/anonymous_functions/task.md b/Functions as Data/anonymous_functions/task.md index 67dbd6b5..1b1b9873 100644 --- a/Functions as Data/anonymous_functions/task.md +++ b/Functions as Data/anonymous_functions/task.md @@ -1,5 +1,3 @@ -# Anonymous functions - An anonymous function is a function that, quite literally, does not have a name. It is defined only by its argument list and computations. Anonymous functions are also known as lambda functions, or simply lambdas. diff --git a/Functions as Data/filter/task.md b/Functions as Data/filter/task.md index 7811e032..8151d678 100644 --- a/Functions as Data/filter/task.md +++ b/Functions as Data/filter/task.md @@ -1,5 +1,3 @@ -# `filter` - ```def filter(pred: A => Boolean): Iterable[A]``` The `filter` method works on any Scala collection that implements `Iterable`. diff --git a/Functions as Data/find/task.md b/Functions as Data/find/task.md index 4602cad0..27facdcc 100644 --- a/Functions as Data/find/task.md +++ b/Functions as Data/find/task.md @@ -1,5 +1,3 @@ -# `find` - `def find(pred: A => Boolean): Option[A]` Imagine that instead of filtering for all black cats, we are happy with obtaining just one, no matter which. diff --git a/Functions as Data/foldLeft/task.md b/Functions as Data/foldLeft/task.md index 6826ba27..f999a19d 100644 --- a/Functions as Data/foldLeft/task.md +++ b/Functions as Data/foldLeft/task.md @@ -1,5 +1,3 @@ -# `foldLeft` - `def foldLeft[B](acc: B)(f: (B, A) => B): B` The `foldLeft` method is another method in Scala collections that can be perceived as a generalized version of `map`, but generalized differently than `flatMap`. diff --git a/Functions as Data/foreach/task.md b/Functions as Data/foreach/task.md index 32d1ad7e..08dac918 100644 --- a/Functions as Data/foreach/task.md +++ b/Functions as Data/foreach/task.md @@ -1,5 +1,3 @@ -# `foreach` - `def foreach[U](f: A => U): Unit` The `foreach` method works on any Scala collection that implements `Iterable`. diff --git a/Functions as Data/functions_returning_functions/task.md b/Functions as Data/functions_returning_functions/task.md index 5140fe70..aef1e60d 100644 --- a/Functions as Data/functions_returning_functions/task.md +++ b/Functions as Data/functions_returning_functions/task.md @@ -1,5 +1,3 @@ -# Functions Returning Functions - In Scala, it is possible to construct functions dynamically, inside functions and methods, and return them. This technique allows us to create new functions based on the arguments given to the original function and return them as the result of that function. diff --git a/Functions as Data/map/task.md b/Functions as Data/map/task.md index cccf643c..ff5898d9 100644 --- a/Functions as Data/map/task.md +++ b/Functions as Data/map/task.md @@ -1,5 +1,3 @@ -# `map` - `def map[B](f: A => B): Iterable[B]` The `map` method works on any Scala collection that implements `Iterable`. diff --git a/Functions as Data/partial_fucntion_application/task.md b/Functions as Data/partial_fucntion_application/task.md index 5cdafd98..35ccc5db 100644 --- a/Functions as Data/partial_fucntion_application/task.md +++ b/Functions as Data/partial_fucntion_application/task.md @@ -1,4 +1,3 @@ -# Partial function application Returning functions from functions is related to, but not the same as, [partial application](https://en.wikipedia.org/wiki/Partial_application). The former allows you to create functions that behave as though they have a "hidden" list of arguments provided at the moment of creation, rather than at the moment of use. Each function returns a new function that accepts the next argument until all arguments have been processed. The final function then returns the result. diff --git a/Functions as Data/passing_functions_as_arguments/task.md b/Functions as Data/passing_functions_as_arguments/task.md index addc8f8b..ebf1d60f 100644 --- a/Functions as Data/passing_functions_as_arguments/task.md +++ b/Functions as Data/passing_functions_as_arguments/task.md @@ -1,5 +1,3 @@ -# Passing functions as arguments - We can pass a named function as an argument to another function just as we would pass any other value. This is useful, for example, when we want to manipulate data in a collection. There are many methods in Scala collections classes that operate by accepting a function as an argument and applying it in some way to each element of the collection. diff --git a/Functions as Data/scala_collections_overview/task.md b/Functions as Data/scala_collections_overview/task.md index f0521b2b..cfff3722 100644 --- a/Functions as Data/scala_collections_overview/task.md +++ b/Functions as Data/scala_collections_overview/task.md @@ -1,5 +1,3 @@ -# Scala collections overview - Scala collections comprise a vast set of data structures that offer powerful and flexible ways to manipulate and organize data. The Scala collections framework is designed to be both user-friendly and expressive, enabling you to perform complex operations with concise and readable code. To achieve this, numerous methods take functions as arguments. diff --git a/Functions as Data/total_and_partial_functions/task.md b/Functions as Data/total_and_partial_functions/task.md index bf4cafe1..eea35b3f 100644 --- a/Functions as Data/total_and_partial_functions/task.md +++ b/Functions as Data/total_and_partial_functions/task.md @@ -1,4 +1,3 @@ -# Total and Partial Functions We have already discussed how a function can be categorized as pure or impure. A pure function does not produce side effects; instead, it operates solely on its arguments and produces a result. Conversely, an impure function may induce side effects and draw input from contexts other than its arguments. diff --git a/Functions as Data/what_is_a_function/task.md b/Functions as Data/what_is_a_function/task.md index 1f08a673..9fa8395d 100644 --- a/Functions as Data/what_is_a_function/task.md +++ b/Functions as Data/what_is_a_function/task.md @@ -1,5 +1,3 @@ -# What is a function? - A function is a standalone block of code that takes arguments, performs some calculations, and returns a result. It may or may not have side effects; that is, it may access the data in the program, and if the data is modifiable, the function might alter it. If it doesn't — meaning, if the function operates solely on its arguments — we state that the function is pure. diff --git a/Immutability/A View/task.md b/Immutability/A View/task.md index 91732db6..62225676 100644 --- a/Immutability/A View/task.md +++ b/Immutability/A View/task.md @@ -43,7 +43,7 @@ This avoids unnecessary calculations and hence is more efficient in this scenari To learn more about the methods of Scala View, read its [documentation](https://www.scala-lang.org/api/current/scala/collection/View.html). -### Exercise +## Exercise Consider a simplified log message: it is a comma-separated string where the first substring before the comma specifies its severity, the second substring is the numerical error code, and the last one is the message itself. diff --git a/Immutability/Berliner Pattern/task.md b/Immutability/Berliner Pattern/task.md index f1b9941f..194bb443 100644 --- a/Immutability/Berliner Pattern/task.md +++ b/Immutability/Berliner Pattern/task.md @@ -1,5 +1,3 @@ -## Berliner Pattern - In functional programming, data rarely needs to be mutable. Theoretically, it is possible to completely avoid mutability, especially in such programming languages as Haskell. However, this might feel cumbersome and unnecessarily complicated to many coders. @@ -36,7 +34,7 @@ the billion-dollar mistake caused by using `null`. We encourage you to familiarize yourself with this pattern by watching the [original video](https://www.youtube.com/watch?v=DhNw60hxCeY). -### Exercise +## Exercise We provide you with a sample implementation of an application that handles creating, modifying, and deleting users in a database. We mock the database and HTTP layers, and your task is to implement methods for processing requests following the Berliner pattern. diff --git a/Immutability/Case Class Copy/task.md b/Immutability/Case Class Copy/task.md index dc23fe20..09f6bca8 100644 --- a/Immutability/Case Class Copy/task.md +++ b/Immutability/Case Class Copy/task.md @@ -1,5 +1,3 @@ -## Case Class Copy - In Scala, case classes automatically come equipped with a few handy methods upon declaration, one of which is the `copy` method. The `copy` method is used to create a new instance of the case class, which is a copy of the original one; however, you can also modify some (or none) of the fields during the copying process. @@ -48,7 +46,7 @@ println(s"Updated user: $updatedUser") // prints out User("Jane", "Doe", Some("new.jane.doe@example.com"), Some("@newJaneDoe"), None) ``` -### Exercise +## Exercise Let's unravel the `copy` function. Implement your own function, `myCopy`, which operates identically to `copy`. diff --git a/Immutability/Comparison of View and Lazy Collection/task.md b/Immutability/Comparison of View and Lazy Collection/task.md index d2d40e09..66da555e 100644 --- a/Immutability/Comparison of View and Lazy Collection/task.md +++ b/Immutability/Comparison of View and Lazy Collection/task.md @@ -1,5 +1,3 @@ -## Comparison of View and Lazy List - Now you may be wondering why Scala has both lazy lists and views, and when to use which one. Here's a short list highlighting the key differences between these two approaches to lazy computation: diff --git a/Immutability/Lazy List/task.md b/Immutability/Lazy List/task.md index e4e26fe2..37075338 100644 --- a/Immutability/Lazy List/task.md +++ b/Immutability/Lazy List/task.md @@ -1,5 +1,3 @@ -## Lazy List - A lazy list in Scala is a collection that evaluates its elements lazily, with each element computed just once, the first time it is needed, and then stored for subsequent access. Lazy lists can be infinite, with their elements computed on-demand. Hence, if your program keeps accessing the next element @@ -46,7 +44,7 @@ In the above code: To learn more about the methods of Scala's `LazyList`, read the [documentation](https://www.scala-lang.org/api/current/scala/collection/immutable/LazyList.html). -### Exercise +## Exercise Implement a function that generates an infinite lazy list of prime numbers in ascending order. Use the Sieve of Eratosthenes algorithm. diff --git a/Immutability/Scala Collections instead of Imperative Loops/task.md b/Immutability/Scala Collections instead of Imperative Loops/task.md index 66dabb4d..a6639105 100644 --- a/Immutability/Scala Collections instead of Imperative Loops/task.md +++ b/Immutability/Scala Collections instead of Imperative Loops/task.md @@ -1,5 +1,3 @@ -## Scala Collections instead of Imperative Loops - 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. diff --git a/Immutability/The Builder Pattern/task.md b/Immutability/The Builder Pattern/task.md index 2aaf21fc..0bf44e82 100644 --- a/Immutability/The Builder Pattern/task.md +++ b/Immutability/The Builder Pattern/task.md @@ -1,5 +1,3 @@ -## The Builder Pattern - The builder pattern is a design pattern often used in object-oriented programming to provide a flexible solution for constructing complex objects. It's especially handy when an object needs to be created with numerous possible configuration options. @@ -65,6 +63,6 @@ class UserBuilder(private val firstName: String, private val lastName: String): // prints out User("John", "Doe", Some("john.doe@example.com"), Some("@johndoe"), Some("@johnDoe_insta")) ``` -### Exercise +## Exercise Implement the builder pattern for constructing a message that has optional sender, receiver, and content fields. diff --git a/Introduction/Getting to Know You/task.md b/Introduction/Getting to Know You/task.md index f223ab9d..19ef2275 100644 --- a/Introduction/Getting to Know You/task.md +++ b/Introduction/Getting to Know You/task.md @@ -1,5 +1,3 @@ -# Getting to know you - Thank you for taking our Functional Programming in Scala course! We would be happy to get to know you a bit better, so we’re asking you to fill in [this brief form](https://surveys.jetbrains.com/s3/course-introduction-functional-programming-scala). \ No newline at end of file diff --git a/Introduction/Join Our Discord Community/task.md b/Introduction/Join Our Discord Community/task.md index e469c9ed..9d197089 100644 --- a/Introduction/Join Our Discord Community/task.md +++ b/Introduction/Join Our Discord Community/task.md @@ -1,5 +1,3 @@ -# Join our Discord community - We invite you to join our course chat on Discord! It's a great space to ask questions, engage with our instructors, and connect with your fellow students. diff --git a/Monads/Either as an Alternative to Exceptions/task.md b/Monads/Either as an Alternative to Exceptions/task.md index 5802e8ae..14a52890 100644 --- a/Monads/Either as an Alternative to Exceptions/task.md +++ b/Monads/Either as an Alternative to Exceptions/task.md @@ -5,34 +5,34 @@ An instance of `Either[A, B]` can only contain a value of type `A`, or a value o 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. -Like `Option`, `Either` is a monad that means it allows chaining of successful operations. +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. -It's an arbitrary decision and everything would work the same way if we were to choose differently. -However, a useful mnemonic is that `Right` is for cases when everything went right. +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, `unit` 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. 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 -def readNumbers(x: String, y: String): Either[String, (Double, Double)] = - (x.toDoubleOption, y.toDoubleOption) match - case (Some(x), Some(y)) => Right (x, y) - case (None, Some(y)) => Left("First string is not a number") - case (Some(x), None) => Left("Second string is not a number") - case (None, None) => Left("Both strings are not numbers") +def readNumbers(x: String, y: String): Either[String, (Double, Double)] = + (x.toDoubleOption, y.toDoubleOption) match + case (Some(x), Some(y)) => Right (x, y) + case (None, Some(y)) => Left("First string is not a number") + case (Some(x), None) => Left("Second string is not a number") + case (None, None) => Left("Both strings are not numbers") def safeDiv(x: Double, y: Double): Either[String, Double] = - if (y == 0) Left("Division by zero") - else Right(x / y) + if (y == 0) Left("Division by zero") + else Right(x / y) @main def main() = - val x = readLine() - val y = readLine() - print(readNumbers(x, y).flatMap(safeDiv)) + val x = readLine() + val y = readLine() + print(readNumbers(x, y).flatMap(safeDiv)) ``` Note that we have used `String` for errors here, but we could have used a custom data type. @@ -47,7 +47,7 @@ def safeDiv(x: Double, y: Double): Either[Throwable, Double] = else Right(x / y) ``` -### Exercise +## Exercise Let's get back to our `UserService` from the previous lesson. There are three possible reasons why `getGrandchild` may fail: diff --git a/Monads/Monadic Laws/task.md b/Monads/Monadic Laws/task.md index 0dc85f96..22971a26 100644 --- a/Monads/Monadic Laws/task.md +++ b/Monads/Monadic Laws/task.md @@ -1,9 +1,9 @@ -There are multiple other monads not covered in this course. +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. -### Identity laws +## Identity Laws The first two properties are concerned with `unit`, the constructor to create monads. Identity laws mean that there is a special value that does nothing when a binary operator is applied to it. @@ -21,7 +21,7 @@ def f(value: V): Monad[V] Monad(v).flatMap(f) == f(v) ``` The right identity law states that by passing the unit method into a `flatMap` is equivalent to not doing that at all. -This reflects the idea that unit only wraps whatever value it receives and produces no additional action. +This reflects the idea that unit only wraps whatever value it receives and produces no effect. ```scala 3 val monad: Monad[_] = ... @@ -29,7 +29,7 @@ val monad: Monad[_] = ... monad.flatMap(Monad(_)) == monad ``` -### Associativity +## Associativity Associativity is a property that says that you can put parentheses in a whatever 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. @@ -68,7 +68,7 @@ for { } yield res ``` -### Do Option and Either follow the laws? +## 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. The unit of `Option` is `{ x => Some(x) }`, while `flatMap` can be implemented in the following way. @@ -116,13 +116,12 @@ 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 +## Beyond Failure -We only covered monads `Option`, `Either`, and `Try` that are very similar in a way: all of them describe failing computations. +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, non-determinism and many more. -We encourage you to explore these monads on your own. -Start with lists and see where it gets you. +They include logging, reading from a global memory, state manipulation, different flavours 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 1e4e00b0..1e3246a8 100644 --- a/Monads/Non-Determinism with Lists/task.md +++ b/Monads/Non-Determinism with Lists/task.md @@ -1,5 +1,5 @@ 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, the ability of 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. @@ -25,7 +25,7 @@ If we run `factors(4).flatMap(factors)`, we get `List(1,2,4).flatMap(factors)` w `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 be sensible 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 @@ -37,7 +37,7 @@ def factors(n: Int): Set[Int] = { // factors(6).flatMap(factors) == Set(1, 2, 4) ``` -### Exercise +## Exercise To make our model of users a little more realistic, we should take into an account that a user may have many children. This makes our `getGrandchild` function non-deterministic. diff --git a/Monads/Option as an Alternative to null/task.md b/Monads/Option as an Alternative to null/task.md index 003f1e6d..e71de606 100644 --- a/Monads/Option as an Alternative to null/task.md +++ b/Monads/Option as an Alternative to null/task.md @@ -49,15 +49,15 @@ Option(result).foreach { res => In short, `None` indicates that something went wrong, and `flatMap` allows to chain function calls which do not fail. -### Exercise +## Exercise -Let's consider users who are represented with a `User` class. -Each user has a name, an age, and possibly a child. +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 only fill in what functions these are. +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. -Use `flatMap` here, avoid pattern matching. +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 605cbcfd..9849415a 100644 --- a/Monads/Syntactic Sugar and For-Comprehensions/task.md +++ b/Monads/Syntactic Sugar and For-Comprehensions/task.md @@ -17,7 +17,8 @@ val res = client.getTeamMembers(teamId).flatMap { members => } ``` -It doesn't look pretty, there is a new nesting level for every call, and it's rather complicated to untangle the mess to understand what is happening. +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. The same code can be written more succinctly using `for/yield`: @@ -36,8 +37,8 @@ val res = for { Each line with a left arrow corresponds to a `flatMap` call, where the variable name to the left of the arrow represents the name of the variable in the lambda function. We start by binding the successful results of retrieving team members with `members`, then get user data based on the members' ids and bind it with `users`. -Note that the first line in a for-comprehension must contain the left arrow. -This is how Scala compiler understands it is a monadic action, and what type it has. +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. @@ -45,7 +46,7 @@ We also don't care about the value returned by `log` and because of that use the 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. -### Exercise +## Exercise Use for-comprehensions to implement `getGrandchild` and `getGrandchildAge` from the previous exercise. diff --git a/Pattern Matching/A Custom unapply Method/task.md b/Pattern Matching/A Custom unapply Method/task.md index 4b2725b2..cdb1ecfb 100644 --- a/Pattern Matching/A Custom unapply Method/task.md +++ b/Pattern Matching/A Custom unapply Method/task.md @@ -1,5 +1,3 @@ -# A Custom unapply Method - You can also implement a custom `unapply` method for both a regular class that lacks an automatically generated `unapply`, and for providing an additional way to destructure a case class. Here's an example of a custom `unapply` method for the `Cat` case class we defined in the previous chapter: diff --git a/Pattern Matching/Case Class/task.md b/Pattern Matching/Case Class/task.md index c0036ba9..cdcecd1d 100644 --- a/Pattern Matching/Case Class/task.md +++ b/Pattern Matching/Case Class/task.md @@ -1,4 +1,3 @@ -# Case Class In Scala, a case class is a special kind of class that comes equipped with some useful default behaviors and methods, beneficial for modeling immutable data. While Scala's compiler does place some limitations on it, it concurrently enriches it with features that diff --git a/Pattern Matching/Case Objects/task.md b/Pattern Matching/Case Objects/task.md index 95ea2dd7..1ece1fd1 100644 --- a/Pattern Matching/Case Objects/task.md +++ b/Pattern Matching/Case Objects/task.md @@ -1,6 +1,3 @@ - -# Case Objects - You might have noticed in the example of a binary tree implemented with sealed trait hierarchies that we used a *case object* to introduce the `Stump` type. In Scala, a case object is a special type of object that combines characteristics and benefits of both a case class and an object. diff --git a/Pattern Matching/Destructuring/task.md b/Pattern Matching/Destructuring/task.md index 5ece98b7..a17306f5 100644 --- a/Pattern Matching/Destructuring/task.md +++ b/Pattern Matching/Destructuring/task.md @@ -1,5 +1,3 @@ -# Destructuring - Destructuring in Scala refers to the practice of breaking down an instance of a given type into its constituent parts. You can think of it as the inversion of construction. In a constructor or an `apply` method, we use a collection of parameters to create a new instance of a given type. diff --git a/Pattern Matching/Enums/task.md b/Pattern Matching/Enums/task.md index 356806a5..cbcf457d 100644 --- a/Pattern Matching/Enums/task.md +++ b/Pattern Matching/Enums/task.md @@ -1,5 +1,3 @@ -# Enum - An enumeration (or enum) is a type that represents a finite set of distinct values. Enumerations are commonly used to limit the set of possible values for a field, thus improving code clarity and reliability. diff --git a/Pattern Matching/Pattern Matching/task.md b/Pattern Matching/Pattern Matching/task.md index 6931d2f8..4ce479ab 100644 --- a/Pattern Matching/Pattern Matching/task.md +++ b/Pattern Matching/Pattern Matching/task.md @@ -1,5 +1,3 @@ -# Pattern Matching - Pattern matching is one of the most important features in Scala. It’s so vital that we might risk saying it’s not just *a* feature of Scala, but *the* defining feature. It affects every other part of the programming language to the extent that it’s difficult to discuss any aspect of Scala diff --git a/Pattern Matching/Sealed Traits Hierarchies/task.md b/Pattern Matching/Sealed Traits Hierarchies/task.md index 1f820bb6..603bd6f9 100644 --- a/Pattern Matching/Sealed Traits Hierarchies/task.md +++ b/Pattern Matching/Sealed Traits Hierarchies/task.md @@ -1,5 +1,3 @@ -# Sealed Traits Hierarchies - Sealed traits in Scala are used to represent restricted class hierarchies, providing exhaustive type checking. When a trait is declared as sealed, it can only be extended within the same file. This restriction enables the compiler to identify all subtypes, allowing for more precise compile-time checking. diff --git a/Pattern Matching/Smart Constructors and the apply Method/task.md b/Pattern Matching/Smart Constructors and the apply Method/task.md index f8bd253e..1baa6b5c 100644 --- a/Pattern Matching/Smart Constructors and the apply Method/task.md +++ b/Pattern Matching/Smart Constructors and the apply Method/task.md @@ -1,5 +1,3 @@ -# Smart Constructors and the `apply` Method - In Scala, `apply` is a special method that can be invoked without specifying its name. ```scala 3 diff --git a/Pattern Matching/The Newtype Pattern/task.md b/Pattern Matching/The Newtype Pattern/task.md index 342e1013..f71c2ab3 100644 --- a/Pattern Matching/The Newtype Pattern/task.md +++ b/Pattern Matching/The Newtype Pattern/task.md @@ -1,5 +1,3 @@ -# The Newtype Pattern - The *newtype pattern* in Scala is a way of creating new types from existing ones that are distinct at compile time but share the same runtime representation. This approach can be useful for adding more meaning to simple types, enforcing type safety, and avoiding mistakes. From f3ec3ae7787c6fa16c07e156d26b7253b9960481 Mon Sep 17 00:00:00 2001 From: Ekaterina Verbitskaia Date: Thu, 30 May 2024 18:56:41 +0200 Subject: [PATCH 43/65] `unit` methods purged --- .../task-info.yaml | 16 ++++----- .../task.md | 5 +-- Monads/Monadic Laws/task.md | 16 ++++----- Monads/Non-Determinism with Lists/task.md | 2 +- .../Option as an Alternative to null/task.md | 33 +++++++++++++++---- 5 files changed, 47 insertions(+), 25 deletions(-) diff --git a/Monads/Either as an Alternative to Exceptions/task-info.yaml b/Monads/Either as an Alternative to Exceptions/task-info.yaml index a867a771..d069c2d3 100644 --- a/Monads/Either as an Alternative to Exceptions/task-info.yaml +++ b/Monads/Either as an Alternative to Exceptions/task-info.yaml @@ -3,17 +3,17 @@ files: - name: src/Task.scala visible: true placeholders: - - offset: 676 - length: 963 + - offset: 1043 + length: 92 placeholder_text: /* TODO */ - - offset: 676 - length: 963 + - offset: 1175 + length: 36 placeholder_text: /* TODO */ - - offset: 676 - length: 963 + - offset: 1230 + length: 52 placeholder_text: /* TODO */ - - offset: 676 - length: 963 + - offset: 1590 + length: 48 placeholder_text: /* TODO */ - name: test/TestSpec.scala visible: false diff --git a/Monads/Either as an Alternative to Exceptions/task.md b/Monads/Either as an Alternative to Exceptions/task.md index 14a52890..482a726f 100644 --- a/Monads/Either as an Alternative to Exceptions/task.md +++ b/Monads/Either as an Alternative to Exceptions/task.md @@ -9,8 +9,9 @@ Like `Option`, `Either` is a monad that means it allows chaining of succeeding c The convention is that the failure is represented with `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, `unit` 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. +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. +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. diff --git a/Monads/Monadic Laws/task.md b/Monads/Monadic Laws/task.md index 22971a26..16e8a60e 100644 --- a/Monads/Monadic Laws/task.md +++ b/Monads/Monadic Laws/task.md @@ -5,23 +5,23 @@ They are called monadic laws, namely left and right identity, and associativity. ## Identity Laws -The first two properties are concerned with `unit`, the constructor to create monads. +The first two properties are concerned with `identity`, the constructor to create monads. Identity laws mean that there is a special value that does nothing when a binary operator is applied to it. For example, `0 + x == x + 0 == x` for any possible number `x`. Such an element may not exist for some operations, or it may only work on one side of the operator. Consider subtraction, for which `x - 0 == x`, but `0 - x != x`. -As it happens, the unit is supposed to be the identity of the `flatMap` method. +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 unit 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 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`: ```scala 3 def f(value: V): Monad[V] Monad(v).flatMap(f) == f(v) ``` -The right identity law states that by passing the unit method into a `flatMap` is equivalent to not doing that at all. -This reflects the idea that unit only wraps whatever value it receives and produces no effect. +The right identity law states that by passing the `identity` method into a `flatMap` is equivalent to not doing that at all. +This reflects the idea that `identity` only wraps whatever value it receives and produces no effect. ```scala 3 val monad: Monad[_] = ... @@ -47,7 +47,7 @@ mA.flatMap( a => ) ``` -This can be refactored in the following form, using the unit of the corresponding monad. +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. ```scala 3 @@ -71,11 +71,11 @@ 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. -The unit of `Option` is `{ x => Some(x) }`, while `flatMap` can be implemented in the following way. +The `identity` of `Option` is `{ x => Some(x) }`, while `flatMap` can be implemented in the following way. ```scala 3 def flatMap[B](f: A => Option[B]): Option[B] = this match { - case Some(b) => f(b) + case Some(x) => f(x) case _ => None } ``` diff --git a/Monads/Non-Determinism with Lists/task.md b/Monads/Non-Determinism with Lists/task.md index 1e3246a8..386f1fa7 100644 --- a/Monads/Non-Determinism with Lists/task.md +++ b/Monads/Non-Determinism with Lists/task.md @@ -18,7 +18,7 @@ def factors(n: Int): List[Int] = { ``` Let's now discuss the List monad. -The unit method simply creates a singleton list with its argument inside, indicating that the computation has finished with only one value. +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)`. diff --git a/Monads/Option as an Alternative to null/task.md b/Monads/Option as an Alternative to null/task.md index e71de606..589a5186 100644 --- a/Monads/Option as an Alternative to null/task.md +++ b/Monads/Option as an Alternative to null/task.md @@ -1,10 +1,19 @@ -Monad is a powerful concept popular in functional programming. +Monad is a powerful concept widely used in functional programming. It's a design pattern capable of describing failing computations, managing state, and handling arbitrary side effects. -Unlike in Haskell, there is no specific Monad trait in the standard library of Scala. -Instead, a monad is a wrapper class that has a static method `unit` and implements `flatMap`. -The method `unit` accepts a value and creates a monad with it inside, while `flatMap` chains operations on the monad. -For a class to be a monad, it should satisfy a set of rules, called monadic laws. -We'll cover them at a later stage. +Unlike Haskell, Scala's standard library doesn't include a specific Monad trait. +Instead, a monad is a wrapper class `M[A]` that implements `flatMap`, the method for chaining several operations together. +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. +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. +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. 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. @@ -37,6 +46,18 @@ If any of the divisions fail, then the whole chain stops. div(totalVisits, numberOfUsers).flatMap { div(_, numberOfDays) } ``` +Now let's see how `identity` and `flatMap` can be implemented. +This is not exactly their implementation from the standard library, but it reflects the main idea. + +```scala 3 +def identity[A](x: A): Option[A] = Some(x) + +def flatMap[B](f: A => Option[B]): Option[B] = this match { + case Some(x) => f(x) + case _ => None +} +``` + There is one more special case in Scala: if you pass `null` as an argument to the `Option` constructor, then you receive `None`. You should avoid doing this explicitly, but when you need to call a third-party Java library, which can return `nulls`: From be12deb1d8dc6146a8e85e9591591ae461f9da41 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Thu, 6 Jun 2024 22:48:23 +0300 Subject: [PATCH 44/65] Update task.md language checked --- Early Returns/Baby Steps/task.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Early Returns/Baby Steps/task.md b/Early Returns/Baby Steps/task.md index 2e24e062..1d5a3e46 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. From da06e647dfd194baef2df7c359751e0a771aaeb4 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:38:23 +0300 Subject: [PATCH 45/65] Update task.md language checked --- Early Returns/Breaking Boundaries/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Early Returns/Breaking Boundaries/task.md b/Early Returns/Breaking Boundaries/task.md index a233158e..1b995352 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 From 890973f7a64e4aa4c0d4e3a7e2192ce793201af5 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:58:47 +0300 Subject: [PATCH 46/65] Update task.md language checked --- Early Returns/Unapply/task.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Early Returns/Unapply/task.md b/Early Returns/Unapply/task.md index 43dd14f7..42807693 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. From 799c81304d8e88de10128e762d692311e0a3cd64 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:45:15 +0300 Subject: [PATCH 47/65] Update task.md Title deleted --- .../Using Clause for Carrying an Immutable Context/task.md | 2 -- 1 file changed, 2 deletions(-) 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 23b88e82..bf81614f 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: From 5402ed3d1130aae768845fe9e7f353a9e5d574c6 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:04:22 +0300 Subject: [PATCH 48/65] Update task.md language checked --- Functions as Data/filter/task.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Functions as Data/filter/task.md b/Functions as Data/filter/task.md index 8151d678..e6e87b72 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. From cd0df9e1cfd2f292a3cb3365f3d8c82d0e468d7c Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:16:07 +0300 Subject: [PATCH 49/65] Update task.md language checked, title deleted --- Functions as Data/flatMap/task.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Functions as Data/flatMap/task.md b/Functions as Data/flatMap/task.md index 73b63d49..fcad5a33 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 From 709c442f93ecc708b31a7f7206e2a003748f30a6 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:23:29 +0300 Subject: [PATCH 50/65] Update task.md language checked --- Functions as Data/foldLeft/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Functions as Data/foldLeft/task.md b/Functions as Data/foldLeft/task.md index f999a19d..777ea088 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. From 5f0240d1881a39cccbeeaad339da158881a3bde7 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:36:14 +0300 Subject: [PATCH 51/65] Update task.md language checked --- Functions as Data/map/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Functions as Data/map/task.md b/Functions as Data/map/task.md index ff5898d9..7cf9f2ba 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. From 4230249b36b91c5d6f18e2d5abedf1a38474d06c Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:45:13 +0300 Subject: [PATCH 52/65] Update task.md language checked --- Functions as Data/passing_functions_as_arguments/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Functions as Data/passing_functions_as_arguments/task.md b/Functions as Data/passing_functions_as_arguments/task.md index ebf1d60f..fb11d2c7 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 From 3d4e2008299bcae154827df441bc2bd073256f96 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:47:34 +0300 Subject: [PATCH 53/65] Update task.md language checked --- Immutability/A View/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Immutability/A View/task.md b/Immutability/A View/task.md index 62225676..2d84df58 100644 --- a/Immutability/A View/task.md +++ b/Immutability/A View/task.md @@ -4,7 +4,7 @@ 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 From 518cad0603d33597898aa4170f2b8a2ee1b7ba3d Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:48:37 +0300 Subject: [PATCH 54/65] Update task.md title deleted --- Immutability/A View/task.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/Immutability/A View/task.md b/Immutability/A View/task.md index 2d84df58..631544bc 100644 --- a/Immutability/A View/task.md +++ b/Immutability/A View/task.md @@ -1,5 +1,3 @@ -## 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, From 535cbd65d865689cdb18f87fddeb74254381465f Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:56:09 +0300 Subject: [PATCH 55/65] Update task.md language checked --- Immutability/Berliner Pattern/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Immutability/Berliner Pattern/task.md b/Immutability/Berliner Pattern/task.md index 194bb443..9aa8fbde 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. From c6642a436a87220eb70c3378404e64e757267397 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 23:28:29 +0300 Subject: [PATCH 56/65] Update task.md title deleted --- Immutability/Lazy Val/task.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/Immutability/Lazy Val/task.md b/Immutability/Lazy Val/task.md index 2014ce28..7ed5b7e7 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. From ece6d1df09f3493c77c9c46f45e256a64ea5355d Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Mon, 10 Jun 2024 23:36:18 +0300 Subject: [PATCH 57/65] Update task.md language checked --- .../Scala Collections instead of Imperative Loops/task.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Immutability/Scala Collections instead of Imperative Loops/task.md b/Immutability/Scala Collections instead of Imperative Loops/task.md index a6639105..5c279e67 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. From fcb654a22126221deef37bb94bc2a4c3a8b7db2d Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:47:42 +0300 Subject: [PATCH 58/65] Update task.md language checked --- .../task.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Monads/Either as an Alternative to Exceptions/task.md b/Monads/Either as an Alternative to Exceptions/task.md index 482a726f..a8deffc8 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. From ffa08aa9760297992b25c6b1b94504728e6da7ac Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:15:42 +0300 Subject: [PATCH 59/65] Update task.md language checked --- Monads/Monadic Laws/task.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Monads/Monadic Laws/task.md b/Monads/Monadic Laws/task.md index 16e8a60e..a1690102 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. From 3a3f8695a4cdf1f74b353abd307205d95e09d4df Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:28:51 +0300 Subject: [PATCH 60/65] Update task.md language checked --- Monads/Non-Determinism with Lists/task.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Monads/Non-Determinism with Lists/task.md b/Monads/Non-Determinism with Lists/task.md index 386f1fa7..55d3fbed 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. From 1ecd79bd01ed4d3aa71da60ac6401ef707d6cf60 Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:53:41 +0300 Subject: [PATCH 61/65] Update task.md language checked --- .../Option as an Alternative to null/task.md | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Monads/Option as an Alternative to null/task.md b/Monads/Option as an Alternative to null/task.md index 589a5186..2d6d5433 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. From 5f16362110a623003b79b796696d80b644c2c4ed Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:07:52 +0300 Subject: [PATCH 62/65] Update task.md language checked --- .../Syntactic Sugar and For-Comprehensions/task.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Monads/Syntactic Sugar and For-Comprehensions/task.md b/Monads/Syntactic Sugar and For-Comprehensions/task.md index 9849415a..0839031c 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 From 8ab7bcc85b8aaa761708812031f5c9a7e92f3d3e Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:17:46 +0300 Subject: [PATCH 63/65] Update task.md language checked --- Monads/Use Try Instead of try-catch/task.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Monads/Use Try Instead of try-catch/task.md b/Monads/Use Try Instead of try-catch/task.md index fe04b50b..71570db2 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. From de04b4e9fb7c5c945d093a0644d038a963f3285c Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:29:26 +0300 Subject: [PATCH 64/65] Update task.md language checked --- Pattern Matching/A Custom unapply Method/task.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Pattern Matching/A Custom unapply Method/task.md b/Pattern Matching/A Custom unapply Method/task.md index cdb1ecfb..85618b48 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. From b1a2a529f4e9e01563ca74037fa6b09f2ae9ecba Mon Sep 17 00:00:00 2001 From: stephen-hero <78870893+stephen-hero@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:11:13 +0300 Subject: [PATCH 65/65] Update task.md language checked --- Pattern Matching/The Newtype Pattern/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pattern Matching/The Newtype Pattern/task.md b/Pattern Matching/The Newtype Pattern/task.md index f71c2ab3..3bde5235 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: