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/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 new file mode 100644 index 00000000..8a5b151b --- /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, "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 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/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 new file mode 100644 index 00000000..8d76587e --- /dev/null +++ b/Early Returns/Baby Steps/src/Task.scala @@ -0,0 +1,77 @@ +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 new file mode 100644 index 00000000..5a4470f2 --- /dev/null +++ b/Early Returns/Baby Steps/task-info.yaml @@ -0,0 +1,38 @@ +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 new file mode 100644 index 00000000..1d5a3e46 --- /dev/null +++ b/Early Returns/Baby Steps/task.md @@ -0,0 +1,128 @@ +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. +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 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 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 the `UserData` class. +Following this step, we run *validation* to check if the email is correct. +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: + 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. + */ + 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. + */ + 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 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 + /** + * Imperative approach that uses unidiomatic `return`. + */ + 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 the `userData` is valid. +However, this necessitates calling `complexConversion` twice, as `find` returns the original identifier rather +than the `userData`. + +```scala 3 + /** + * Naive functional approach: calls `complexConversion` twice on the selected ID. + */ + 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 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`. + */ + def findFirstValidUser3(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case userId if complexValidation(complexConversion(userId)) => complexConversion(userId) + } + +``` + +## 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 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. + +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 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`, which works in an imperative fashion. +* `functionalFindFirstValidCat`, utilizing a functional style. +* `collectFirstFindFirstValidCat`, using the `collectFirst` method. + +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. diff --git a/Early Returns/Baby Steps/test/TestSpec.scala b/Early Returns/Baby Steps/test/TestSpec.scala new file mode 100644 index 00000000..387d1392 --- /dev/null +++ b/Early Returns/Baby Steps/test/TestSpec.scala @@ -0,0 +1,36 @@ +import org.scalatest.funsuite.AnyFunSuite +import Task._ +import Database._ + +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/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/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 new file mode 100644 index 00000000..96e5b20e --- /dev/null +++ b/Early Returns/Breaking Boundaries/src/Task.scala @@ -0,0 +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 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)) + + /** + * 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 findFirstValidCat(catIds: Seq[CatId]): Option[Cat] = + boundary: + 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 new file mode 100644 index 00000000..998f33c3 --- /dev/null +++ b/Early Returns/Breaking Boundaries/task-info.yaml @@ -0,0 +1,22 @@ +type: edu +files: + - 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 new file mode 100644 index 00000000..1b995352 --- /dev/null +++ b/Early Returns/Breaking Boundaries/task.md @@ -0,0 +1,36 @@ +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, and in such cases, one can add labels to `break` calls. +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 + +Finally, let's use boundaries to achieve the same result. + +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. diff --git a/Early Returns/Breaking Boundaries/test/TestSpec.scala b/Early Returns/Breaking Boundaries/test/TestSpec.scala new file mode 100644 index 00000000..7b7350bb --- /dev/null +++ b/Early Returns/Breaking Boundaries/test/TestSpec.scala @@ -0,0 +1,27 @@ +import org.scalatest.funsuite.AnyFunSuite +import Task._ +import Database._ + +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/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/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 new file mode 100644 index 00000000..a0fecaa2 --- /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, "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 `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 findFirstValidCat(userIds: Seq[UserId]): Option[UserData] = + userIds + .iterator + .map(safeComplexConversion) + .find(_.exists(complexValidation)) + .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 63d3f9a5..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,3 +1,57 @@ -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 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 4ef9637a..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,8 +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 new file mode 100644 index 00000000..a57270ff --- /dev/null +++ b/Early Returns/Lazy Collection to the Rescue/task.md @@ -0,0 +1,26 @@ +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. +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 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 + def findFirstValidUser9(userIds: Seq[UserId]): Option[UserData] = + userIds + .iterator + .map(safeComplexConversion) + .find(_.exists(complexValidation)) + .flatten +``` + +## Exercise + +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. 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/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..8e1a993b --- /dev/null +++ b/Early Returns/The Problem/src/Main.scala @@ -0,0 +1 @@ +object Main \ 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..39645c87 100644 --- a/Early Returns/The Problem/task.md +++ b/Early Returns/The Problem/task.md @@ -1,23 +1,21 @@ -## 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. -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. - -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 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 something 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 +35,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. -If there was no such element, then `null` is returned after all the elements of the collection are explored in vain. +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 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 +57,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. -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. +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/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 new file mode 100644 index 00000000..dc49cdd3 --- /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, "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 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/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 new file mode 100644 index 00000000..4f2fecda --- /dev/null +++ b/Early Returns/Unapply/src/Task.scala @@ -0,0 +1,115 @@ +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 new file mode 100644 index 00000000..b13977e8 --- /dev/null +++ b/Early Returns/Unapply/task-info.yaml @@ -0,0 +1,47 @@ +type: edu +files: + - 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 new file mode 100644 index 00000000..42807693 --- /dev/null +++ b/Early Returns/Unapply/task.md @@ -0,0 +1,150 @@ +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: + 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. + */ + def findFirstValidUser4(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser(user) => user + } +``` + +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. +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 + /** + * 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 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 + /** + * Partiality of `safeComplexConversion` trickles into the search function. + */ + 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 could be valid. +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 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. + +```scala 3 + 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) + + 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 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 that calls two abstract methods, `convert` and `validate`, which 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 during pattern matching: + +```scala 3 + def findFirstValidUser8(userIds: Seq[UserId]): Option[UserData] = + userIds.collectFirst { + case ValidUser8(user) => user + } +``` + +## 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. + +* 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. The validation of fur characteristics should not be run on cats who have been adopted. + +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 the `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 new file mode 100644 index 00000000..8f675c89 --- /dev/null +++ b/Early Returns/Unapply/test/TestSpec.scala @@ -0,0 +1,31 @@ +import org.scalatest.funsuite.AnyFunSuite +import Task._ +import Database._ + +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) + } +} 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/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 4938b117..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. @@ -25,13 +23,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. @@ -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 global variable. +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 9127a62b..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. @@ -7,7 +5,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 +35,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 +48,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 +62,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 +88,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 +## 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. 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..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: @@ -86,7 +84,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 1d8831fd..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. @@ -57,7 +55,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,10 +70,10 @@ 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 +## 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 ae71bf1c..1b1b9873 100644 --- a/Functions as Data/anonymous_functions/task.md +++ b/Functions as Data/anonymous_functions/task.md @@ -1,7 +1,5 @@ -# 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 +19,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 +30,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 +42,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. diff --git a/Functions as Data/filter/task.md b/Functions as Data/filter/task.md index 454fa542..e6e87b72 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`. @@ -20,7 +18,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) @@ -39,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,6 +46,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 that possess at least one of the characteristics above. Your task is to implement the necessary functions and then apply the filter. diff --git a/Functions as Data/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/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 diff --git a/Functions as Data/foldLeft/task.md b/Functions as Data/foldLeft/task.md index 515921f3..777ea088 100644 --- a/Functions as Data/foldLeft/task.md +++ b/Functions as Data/foldLeft/task.md @@ -1,24 +1,22 @@ -# `foldLeft` - `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 +34,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 +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", 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. diff --git a/Functions as Data/foreach/task.md b/Functions as Data/foreach/task.md index 96d14d6a..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`. @@ -8,7 +6,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. diff --git a/Functions as Data/functions_returning_functions/task.md b/Functions as Data/functions_returning_functions/task.md index 4cda4c90..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. @@ -17,7 +15,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 +28,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 +47,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. diff --git a/Functions as Data/map/task.md b/Functions as Data/map/task.md index 1c5a7536..7cf9f2ba 100644 --- a/Functions as Data/map/task.md +++ b/Functions as Data/map/task.md @@ -1,13 +1,11 @@ -# `map` - `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. +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. ```scala // We define the Color enum @@ -46,6 +44,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`. diff --git a/Functions as Data/partial_fucntion_application/task.md b/Functions as Data/partial_fucntion_application/task.md index 97adb5fe..35ccc5db 100644 --- a/Functions as Data/partial_fucntion_application/task.md +++ b/Functions as Data/partial_fucntion_application/task.md @@ -1,11 +1,10 @@ -# 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 +23,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. diff --git a/Functions as Data/passing_functions_as_arguments/task.md b/Functions as Data/passing_functions_as_arguments/task.md index ababb5df..fb11d2c7 100644 --- a/Functions as Data/passing_functions_as_arguments/task.md +++ b/Functions as Data/passing_functions_as_arguments/task.md @@ -1,9 +1,7 @@ -# 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. -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 +23,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 +40,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. diff --git a/Functions as Data/scala_collections_overview/task.md b/Functions as Data/scala_collections_overview/task.md index 3899fd59..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. @@ -10,11 +8,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. diff --git a/Functions as Data/total_and_partial_functions/task.md b/Functions as Data/total_and_partial_functions/task.md index e7856a11..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. @@ -58,15 +57,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 +73,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 diff --git a/Functions as Data/what_is_a_function/task.md b/Functions as Data/what_is_a_function/task.md index 34ddb05c..9fa8395d 100644 --- a/Functions as Data/what_is_a_function/task.md +++ b/Functions as Data/what_is_a_function/task.md @@ -1,12 +1,10 @@ -# 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 +24,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 +66,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. diff --git a/Immutability/A View/task.md b/Immutability/A View/task.md index 4413d66c..631544bc 100644 --- a/Immutability/A View/task.md +++ b/Immutability/A View/task.md @@ -1,16 +1,14 @@ -## 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 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 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,14 +36,14 @@ 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). -### 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. -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. diff --git a/Immutability/Berliner Pattern/task.md b/Immutability/Berliner Pattern/task.md index eaaf2a23..9aa8fbde 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. @@ -7,45 +5,45 @@ 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. +* 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 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`. 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 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. diff --git a/Immutability/Case Class Copy/task.md b/Immutability/Case Class Copy/task.md index 9cb36a64..09f6bca8 100644 --- a/Immutability/Case Class Copy/task.md +++ b/Immutability/Case Class Copy/task.md @@ -1,25 +1,23 @@ -## 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. @@ -48,9 +46,9 @@ 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` 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. diff --git a/Immutability/Comparison of View and Lazy Collection/task.md b/Immutability/Comparison of View and Lazy Collection/task.md index f9d239c9..66da555e 100644 --- a/Immutability/Comparison of View and Lazy Collection/task.md +++ b/Immutability/Comparison of View and Lazy Collection/task.md @@ -1,19 +1,17 @@ -## 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. diff --git a/Immutability/Lazy List/task.md b/Immutability/Lazy List/task.md index 6cb1f651..37075338 100644 --- a/Immutability/Lazy List/task.md +++ b/Immutability/Lazy List/task.md @@ -1,16 +1,14 @@ -## 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 +33,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 +## 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. diff --git a/Immutability/Lazy Val/task.md b/Immutability/Lazy Val/task.md index 771849f9..7ed5b7e7 100644 --- a/Immutability/Lazy Val/task.md +++ b/Immutability/Lazy Val/task.md @@ -1,21 +1,19 @@ -## 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. -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 +41,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. diff --git a/Immutability/Scala Collections instead of Imperative Loops/task.md b/Immutability/Scala Collections instead of Imperative Loops/task.md index 2daa92a3..5c279e67 100644 --- a/Immutability/Scala Collections instead of Imperative Loops/task.md +++ b/Immutability/Scala Collections instead of Imperative Loops/task.md @@ -1,20 +1,18 @@ -## 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. +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 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. -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. diff --git a/Immutability/The Builder Pattern/task.md b/Immutability/The Builder Pattern/task.md index 75152421..0bf44e82 100644 --- a/Immutability/The Builder Pattern/task.md +++ b/Immutability/The Builder Pattern/task.md @@ -1,28 +1,26 @@ -## 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. @@ -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/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..149de3f1 --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/src/Task.scala @@ -0,0 +1,95 @@ +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) + + 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 new file mode 100644 index 00000000..d069c2d3 --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/task-info.yaml @@ -0,0 +1,21 @@ +type: edu +files: + - name: src/Task.scala + visible: true + placeholders: + - offset: 1043 + length: 92 + placeholder_text: /* TODO */ + - offset: 1175 + length: 36 + placeholder_text: /* TODO */ + - offset: 1230 + length: 52 + placeholder_text: /* TODO */ + - offset: 1590 + length: 48 + placeholder_text: /* TODO */ + - 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..a8deffc8 --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/task.md @@ -0,0 +1,66 @@ +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. +This is achieved by `Either` having two subclasses: `Left[A]` and `Right[B]`. +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 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 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. +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) +``` + +## 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` can 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 new file mode 100644 index 00000000..3cca92a3 --- /dev/null +++ b/Monads/Either as an Alternative to Exceptions/test/TestSpec.scala @@ -0,0 +1,40 @@ +import org.scalatest.funsuite.AnyFunSuite +import 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/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..a1690102 --- /dev/null +++ b/Monads/Monadic Laws/task.md @@ -0,0 +1,133 @@ +There are multiple monads not covered in this course. +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 + +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 `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 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] + +Monad(v).flatMap(f) == f(v) +``` +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[_] = ... + +monad.flatMap(Monad(_)) == monad +``` + +## Associativity + +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. +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 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 +mA.flatMap( a => + mB.flatMap( b => + doSomething(a, b) + ) +) +``` + +This can be refactored in the following form, using the `identity` of the corresponding monad. +Here we parenthesize the chaining of the first two 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 adhere to them. +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(x) => f(x) + 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 same 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 capable of describing failures and non-determinism. +There are many other *computational effects* that can be expressed via monads. +These include logging, reading from global memory, state manipulation, different flavors of non-determinism, and many more. +We encourage you to explore these monads on your own. +Once you feel comfortable with the basics, take a look at the [scalaz](https://scalaz.github.io/7/) and [cats](https://typelevel.org/cats/) libraries. + + + + + + + + diff --git a/Monads/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/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..55d3fbed --- /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, 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 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 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 +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 `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)`. + +`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. + +```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 multiple children. +This makes our `getGrandchild` function non-deterministic. +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. 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/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..d1cc1152 --- /dev/null +++ b/Monads/Option as an Alternative to null/src/Task.scala @@ -0,0 +1,82 @@ +object Task: + class User(val name: String, val age: Int, val child: Option[User]) + + 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) + + /** + * 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) + + /** + * 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 new file mode 100644 index 00000000..05caa757 --- /dev/null +++ b/Monads/Option as an Alternative to null/task-info.yaml @@ -0,0 +1,18 @@ +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 + 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..2d6d5433 --- /dev/null +++ b/Monads/Option as an Alternative to null/task.md @@ -0,0 +1,84 @@ +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 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 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. +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, 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 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 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: + +```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 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. + +```scala 3 +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`: + +```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 chaining function calls that do not fail. + +## Exercise + +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 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. +Use `flatMap` here and 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 new file mode 100644 index 00000000..59195688 --- /dev/null +++ b/Monads/Option as an Alternative to null/test/TestSpec.scala @@ -0,0 +1,26 @@ +import org.scalatest.funsuite.AnyFunSuite +import 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/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..cb11042e --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/src/Task.scala @@ -0,0 +1,99 @@ +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 new file mode 100644 index 00000000..f5f03821 --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/task-info.yaml @@ -0,0 +1,27 @@ +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 + 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..0839031c --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/task.md @@ -0,0 +1,55 @@ +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: + +```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, 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`: + +```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 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 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. + +## 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 new file mode 100644 index 00000000..3cca92a3 --- /dev/null +++ b/Monads/Syntactic Sugar and For-Comprehensions/test/TestSpec.scala @@ -0,0 +1,40 @@ +import org.scalatest.funsuite.AnyFunSuite +import 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/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..971c0b14 --- /dev/null +++ b/Monads/Use Try Instead of try-catch/src/Task.scala @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..8993dd57 --- /dev/null +++ b/Monads/Use Try Instead of try-catch/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/Use Try Instead of try-catch/task.md b/Monads/Use Try Instead of try-catch/task.md new file mode 100644 index 00000000..71570db2 --- /dev/null +++ b/Monads/Use Try Instead of try-catch/task.md @@ -0,0 +1,52 @@ +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 code: + +```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 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: + +```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` 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. + + +```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..55123e0d --- /dev/null +++ b/Monads/lesson-info.yaml @@ -0,0 +1,7 @@ +content: + - Option as an Alternative to null + - Either as an Alternative to Exceptions + - Use Try Instead of try-catch + - Syntactic Sugar and For-Comprehensions + - Non-Determinism with Lists + - Monadic Laws diff --git a/Pattern Matching/A Custom unapply Method/task.md b/Pattern Matching/A Custom unapply Method/task.md index 4b2725b2..85618b48 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: @@ -47,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: @@ -89,7 +87,7 @@ could prove valuable, depending on the situation. Given that each component in the RGB range can only be between `0` and `255`, it only uses 8 bits. The 4 components of the RGB representation fit neatly into a 32-bit integer, which allows for better memory usage. Many color operations can be performed directly using bitwise operations on this integer representation. -However, sometimes it's more convenient to access each components as a number, +However, sometimes it's more convenient to access each component as a number, and this is where the custom `unapply` method may come in handy. Implement the `unapply` method for the int-based RGB representation. diff --git a/Pattern Matching/Case Class/task.md b/Pattern Matching/Case Class/task.md index b373b81c..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 @@ -9,15 +8,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 +27,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) diff --git a/Pattern Matching/Case Objects/task.md b/Pattern Matching/Case Objects/task.md index 593f6646..1ece1fd1 100644 --- a/Pattern Matching/Case Objects/task.md +++ b/Pattern Matching/Case Objects/task.md @@ -1,16 +1,13 @@ - -# 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 +21,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 +49,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. diff --git a/Pattern Matching/Destructuring/task.md b/Pattern Matching/Destructuring/task.md index 6c0609fa..a17306f5 100644 --- a/Pattern Matching/Destructuring/task.md +++ b/Pattern Matching/Destructuring/task.md @@ -1,20 +1,18 @@ -# 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. +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 +22,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 +51,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 +70,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 +94,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. diff --git a/Pattern Matching/Enums/task.md b/Pattern Matching/Enums/task.md index e5b794f2..cbcf457d 100644 --- a/Pattern Matching/Enums/task.md +++ b/Pattern Matching/Enums/task.md @@ -1,16 +1,14 @@ -# 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 +19,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 +48,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 +57,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. diff --git a/Pattern Matching/Pattern Matching/task.md b/Pattern Matching/Pattern Matching/task.md index c45794ed..4ce479ab 100644 --- a/Pattern Matching/Pattern Matching/task.md +++ b/Pattern Matching/Pattern Matching/task.md @@ -1,11 +1,9 @@ -# 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. diff --git a/Pattern Matching/Sealed Traits Hierarchies/task.md b/Pattern Matching/Sealed Traits Hierarchies/task.md index 06494655..603bd6f9 100644 --- a/Pattern Matching/Sealed Traits Hierarchies/task.md +++ b/Pattern Matching/Sealed Traits Hierarchies/task.md @@ -1,13 +1,11 @@ -# 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 +37,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. 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..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 @@ -10,12 +8,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 +35,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 +49,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 +61,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]`. diff --git a/Pattern Matching/The Newtype Pattern/task.md b/Pattern Matching/The Newtype Pattern/task.md index 3e936e58..3bde5235 100644 --- a/Pattern Matching/The Newtype Pattern/task.md +++ b/Pattern Matching/The Newtype Pattern/task.md @@ -1,26 +1,24 @@ -# 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 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 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: ```scala 3 @@ -36,8 +34,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 +59,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. 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. diff --git a/course-info.yaml b/course-info.yaml index 83b5dbb2..8173ec7e 100644 --- a/course-info.yaml +++ b/course-info.yaml @@ -17,6 +17,8 @@ content: - Pattern Matching - Immutability - Expressions over Statements + - Early Returns + - Monads - Conclusion environment_settings: jvm_language_level: JDK_17