diff --git a/README.md b/README.md index 6a2f9ef..551ca34 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,22 @@ Official GitHub repo: https://github.com/fpinscala/fpinscala +## Syllabus + +### Introduction to functional programming + +1. What is functional programming? + +2. Getting started with functional programming in Scala + +3. Functional data structures + +4. Handling errors without exception + +5. Strictness and laziness + +6. Purely functional state + ## Executing a main method ``` ./millw .runMain --mainClass @@ -28,16 +44,3 @@ mill mill.bsp.BSP/install ``` Then open VSCode command palette, and select `Metals: Switch build server`. - - -## References - -### ScalaCheck Generators - -* [ScalaCheck custom generator examples](https://alvinalexander.com/scala/scalacheck-custom-generator-examples/) - -* [davidallsopp/PropertyTests.scala](https://gist.github.com/davidallsopp/60d7474a1fe8dc9b1f2d) - -* [Property Based Testing: ScalaTest + ScalaCheck](https://medium.com/analytics-vidhya/property-based-testing-scalatest-scalacheck-52261a2b5c2c) - -* [Generators in Detail](https://booksites.artima.com/scalacheck/examples/html/ch06.html) \ No newline at end of file diff --git a/build.sc b/build.sc index 16a40f5..ab57223 100644 --- a/build.sc +++ b/build.sc @@ -1,10 +1,8 @@ import mill._, scalalib._, scalafmt._ -trait AdvancedScalaModule extends ScalaModule with ScalafmtModule { +trait FPModule extends ScalaModule with ScalafmtModule { // val baseDir = build.millSourcePath def scalaVersion = "3.3.1" - def scalatestVersion = "3.2.17" - def scalacheckVersion = "3.2.17.0" override def scalacOptions: T[Seq[String]] = Seq( "-encoding", "UTF-8", @@ -14,39 +12,34 @@ trait AdvancedScalaModule extends ScalaModule with ScalafmtModule { "-deprecation", "-unchecked", "-Wunused:all", - // Require then and do in control expressions - // "-new-syntax", "-rewrite", "-indent", "-source", "future", ) } -object chapter02 extends AdvancedScalaModule { - object test extends ScalaTests with TestModule.ScalaTest { - // // use `::` for scala deps, `:` for java deps - override def ivyDeps = Agg( +trait FpTestModule extends ScalaModule with TestModule.ScalaTest { + def scalatestVersion = "3.2.17" + def scalacheckVersion = "3.2.17.0" + + override def ivyDeps = Agg( ivy"org.scalactic::scalactic:$scalatestVersion", ivy"org.scalatest::scalatest:$scalatestVersion", ) - } } -object chapter03 extends AdvancedScalaModule { - object test extends ScalaTests with TestModule.ScalaTest { - override def ivyDeps = Agg( - ivy"org.scalactic::scalactic:$scalatestVersion", - ivy"org.scalatest::scalatest:$scalatestVersion", - ) - } +object chapter02 extends FPModule { + object test extends FpTestModule with ScalaTests } -object chapter04 extends AdvancedScalaModule { - object test extends ScalaTests with TestModule.ScalaTest { - override def ivyDeps = Agg( - ivy"org.scalactic::scalactic:$scalatestVersion", - ivy"org.scalatest::scalatest:$scalatestVersion", - ) - } +object chapter03 extends FPModule { + object test extends FpTestModule with ScalaTests } +object chapter04 extends FPModule { + object test extends FpTestModule with ScalaTests +} + +object chapter05 extends FPModule { + object test extends FpTestModule with ScalaTests +} diff --git a/chapter03/test/src/ListSpec.scala b/chapter03/test/src/ListSpec.scala index 544687a..37ac3d6 100644 --- a/chapter03/test/src/ListSpec.scala +++ b/chapter03/test/src/ListSpec.scala @@ -93,12 +93,19 @@ class ListSpec extends AnyFunSpec with TableDrivenPropertyChecks: it("filter should remove the elements that don't satisfy the given predicate"): filter(List(1, 2, 3), x => x % 2 == 0) shouldBe List(2) - it("flatMap should work as expected"): + it("flatMap should replace each element with a list and flatten the final list"): flatMap(List(1, 2, 3), i => List(i, i)) shouldBe List(1, 1, 2, 2, 3, 3) - it("zipWith should work as expected"): + it("zipWith should combine elements from the given lists pairwise using the given function"): zipWith( List(1, 2, 3), List(true, false, true), (x, y) => if y then x.toString() else y.toString() ) shouldBe List("1", "false", "3") + + it("zipWith should terminate with the smaller list"): + zipWith( + List(1, 2, 3), + List(true), + (x, y) => if y then x.toString() else y.toString() + ) shouldBe List("1") diff --git a/chapter05/src/LazyList.scala b/chapter05/src/LazyList.scala new file mode 100644 index 0000000..f57ea0f --- /dev/null +++ b/chapter05/src/LazyList.scala @@ -0,0 +1,225 @@ +enum LazyList[+A]: + case Empty + case Cons(h: () => A, t: () => LazyList[A]) + + import LazyList.* + + /* + Exercise 5.1: Write a function to convert a LazyList to a List, + which will force its evaluation. + */ + def toList: List[A] = + // foldRight(() => List.empty[A])((a, b) => () => a :: b())() + this match + case Empty => Nil + case Cons(h, t) => h() :: t().toList + + // stack-safe version + // def toList: List[A] = + // @tailrec + // def go(ll: LazyList[A], acc: List[A]): List[A] = + // ll match + // case Cons(h, t) => go(t(), h() :: acc) + // case Empty => acc.reverse + // go(this, Nil) + + /* + Exercise 5.2: Write a function take(n) for returning the first n elements + of a LazyList and drop(n) for skipping the first n elements of a LazyList. + */ + def take(n: Int): LazyList[A] = this match + case Empty => Empty + case Cons(h, t) => + if n == 0 then Empty + else Cons(h, () => t().take(n - 1)) + + def drop(n: Int): LazyList[A] = this match + case Cons(_, t) if n > 0 => t().drop(n - 1) + case _ => this + + /* + Exercise 5.3: Write the function takeWhile for returning all + starting elements of a LazyList that match the given predicate. + + Exercise 5.5: Use foldRight to implement takeWhile. + */ + def takeWhile(p: A => Boolean): LazyList[A] = + foldRight(empty)((a, b) => if p(a) then cons(a, b) else empty) + + // The arrow => in front of the argument type B means the function f + // takes its second argument by name and may choose not to evaluate it. + def foldRight[B](acc: => B)(f: (A, => B) => B): B = + this match + // If f doesn't evaluate its second argument, the recursion never occurs. + case Cons(h, t) => f(h(), t().foldRight(acc)(f)) + case _ => acc + + /* + Exercise 5.4: Implement forAll, which checks that all elements in a LazyList + match a given predicate. Your implementation should terminate the traversal + as soon as it encounters a nonmatching value. + */ + def forAll(p: A => Boolean): Boolean = + foldRight(true)((a, b) => p(a) && b) + + /* + Exercise 5.6: Implement headOption using foldRight. + */ + def headOption: Option[A] = + foldRight(Option.empty[A])((a, _) => Some(a)) + + /* + Exercise 5.7: Implement map, filter, append, and flatMap using foldRight. + The append method should be nonstrict in its argument. + */ + def map[B](f: A => B): LazyList[B] = + foldRight(empty)((a, b) => cons(f(a), b)) + + def filter(p: A => Boolean): LazyList[A] = + foldRight(empty)((a, b) => if p(a) then cons(a, b) else b) + + def append[A2 >: A](that: => LazyList[A2]): LazyList[A2] = + foldRight(that)((a, b) => cons(a, b)) + + def flatMap[B](f: A => LazyList[B]): LazyList[B] = + foldRight(LazyList.empty[B])(f(_).append(_)) + + def tail: LazyList[A] = drop(1) + + /* + Exercise 5.13: Use unfold to implement map, take, takeWhile, + zipWith, and zipAll. The zipAll function should continue the + traversal as long as either lazy list has more elements; it + uses Option to indicate whether each lazy list has been exhausted. + */ + def mapViaUnfold[B](f: A => B): LazyList[B] = + unfold(this)(s => s.headOption.map(a => (f(a), s.tail))) + + def takeViaUnfold(n: Int): LazyList[A] = + unfold((this, n))((s, i) => s.headOption.filter(_ => i > 0).map(a => (a, (s.tail, i - 1)))) + + def takeWhileViaUnfold(p: A => Boolean): LazyList[A] = + unfold(this)(s => s.headOption.filter(p).map(a => (a, s.tail))) + + def zipWith[B, C](that: LazyList[B], f: (A, B) => C): LazyList[C] = + unfold((this, that)): + case (Empty, _) => None + case (_, Empty) => None + case (Cons(x, xxs), Cons(y, yys)) => Some(f(x(), y()) -> (xxs() -> yys())) + + def zipAll[B, C](that: LazyList[B]): LazyList[(Option[A], Option[B])] = + unfold((this, that)): + case (Empty, Empty) => None + case (Empty, Cons(y, yys)) => Some((None -> Some(y()), Empty -> yys())) + case (Cons(x, xxs), Empty) => Some((Some(x()) -> None, xxs() -> Empty)) + case (Cons(x, xxs), Cons(y, yys)) => Some((Some(x()) -> Some(y()), xxs() -> yys())) + + /* + Exercise 5.14: Implement startsWith using functions you've written. + It should check if one LazyList is a prefix of another. + */ + // What is the expectation for prefix = Empty? + def startsWith[A](prefix: LazyList[A]): Boolean = + zipAll(prefix) + .map: + case (Some(x), Some(y)) => x == y + case (_, None) => true + case _ => false + .filter(x => !x) + .headOption + .getOrElse(true) + + /* + Exercise 5.15: Implement tails using unfold. For a given LazyList, tails + returns the LazyList of suffixes of the input sequence, starting with the + original LazyList. + */ + def tails: LazyList[LazyList[A]] = + unfold(Option(this)): + case Some(Empty) => Some(Empty, None) + case Some(xs) => Option(xs, Option(xs.tail)) + case _ => None + + /* + Exercise 5.16: Generalize tails to the function scanRight, which is like a foldRight + that returns a lazy list of the intermediate results. + --- + We can’t implement scanRight via unfold because unfold builds a lazy list from left to right. + Instead, we can use foldRight with a slight modification. We build a lazy list from right to left + where the head of the list is the accumulated value for the list suffix after the current node. + + Example: + LazyList(1, 2, 3).scanRight(0)(_ + _) + ==> (a=3, b=LazyList(0)) + ==> (a=2, b=LazyList(3, 0)) + ==> (a=1, b=LazyList(5, 3, 0)) + => LazyList(6, 5, 3, 0) + */ + def scanRight[B](z: B)(f: (A, => B) => B): LazyList[B] = + foldRight(LazyList(z)): (a, b) => + val b1 = f(a, b.headOption.get) + cons(b1, b) + +object LazyList: + def cons[A]( + hd: => A, + tl: => LazyList[A] + ): LazyList[A] = + lazy val head = hd + lazy val tail = tl + Cons(() => head, () => tail) + + def empty[A]: LazyList[A] = Empty + + def apply[A](as: A*): LazyList[A] = + if as.isEmpty then empty + else cons(as.head, apply(as.tail*)) + + /* + Exercise 5.8: Implement continually, which returns an infinite + LazyList of a given value. + */ + def continually[A](a: A): LazyList[A] = + cons(a, continually(a)) + + /* + Exercise 5.9: Write a function that generates an infinite lazy list + of integers starting from n, then n + 1, n + 2, and so on. + */ + def from(n: Int): LazyList[Int] = + cons(n, from(n + 1)) + + /* + Exercise 5.10: Write a function fibs that generates the infinite lazy + list of Fibonacci numbers. + */ + val fibs: LazyList[Int] = + def go(current: Int, next: Int): LazyList[Int] = + cons(current, go(next, current + next)) + go(0, 1) + + /* + Exercise 5.11: Write a more general LazyList-building function + called unfold. It takes an initial state and a function for + producing both the next state and the next value in the generated + lazy list. + */ + def unfold[A, S](state: S)(f: S => Option[(A, S)]): LazyList[A] = + f(state) match + case Some(x, xs) => cons(x, unfold(xs)(f)) + case _ => Empty + + /* + Exercise 5.12: Write fibs, continually, and ones in terms of unfold. + */ + val onesViaUnfold: LazyList[Int] = + unfold(0)(s => Some(1, s)) + + val fibsViaUnfold: LazyList[Int] = + unfold(List(0, 1))(s => Some(s.head, List(s(1), s.sum))) + + def fromViaUnfold(n: Int): LazyList[Int] = + unfold(n)(s => Some(s, s + 1)) + + def continuallyViaUnfold[A](a: A): LazyList[A] = + unfold(a)(s => Some(s, s)) diff --git a/chapter05/test/src/LazyListSpec.scala b/chapter05/test/src/LazyListSpec.scala new file mode 100644 index 0000000..66bb118 --- /dev/null +++ b/chapter05/test/src/LazyListSpec.scala @@ -0,0 +1,134 @@ +import LazyList.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.shouldBe + +/* +LazyList instances can't be compared because +it uses anonymous lambdas for elements. The +elements must be evaluated before comparison. + */ +class LazyListSpec extends AnyFunSpec: + describe("LazyList"): + it("toList should convert it to a Scala list"): + LazyList(1, 2, 3).toList shouldBe List(1, 2, 3) + + it("take should take the first n element"): + LazyList(1, 2, 3).take(2).toList shouldBe List(1, 2) + + it("take should return an empty list when invoked on an empty list"): + LazyList.empty[Int].take(2).toList shouldBe List.empty[Int] + + it("take should return an the same list when n is greater than the length of the list"): + LazyList(1).take(2).toList shouldBe List(1) + + it("drop should drop the first n element"): + LazyList(1, 2, 3).drop(2).toList shouldBe List(3) + + it("drop should return an empty list when all elements are dropped"): + LazyList(1, 2, 3).drop(3).toList shouldBe List.empty[Int] + + it("forAll should check if all elements match the given predicate"): + LazyList(1, 2, 3).forAll(_ % 2 == 0) shouldBe false + + it("takeWhile should return all starting elements that match the given predicate"): + LazyList(1, 2, 3).takeWhile(_ % 2 == 1).toList shouldBe List(1) + LazyList(1, 2, 3).takeWhile(_ % 2 == 0).toList shouldBe List.empty + + it("headOption should return Some(head) if the list is not empty"): + LazyList(1, 2).headOption shouldBe Some(1) + + it("headOption should return None if the list is empty"): + LazyList.empty.headOption shouldBe None + + it("map should transform a non empty list"): + LazyList(1, 2).map(_ * 2).toList shouldBe List(2, 4) + + it("map should have no effect on an empty list"): + LazyList.empty[Int].map(_ * 2).toList shouldBe List.empty + + it("filter should remove the elements that don't satisfy the given predicate"): + LazyList(1, 2, 3).filter(_ % 2 == 0).toList shouldBe List(2) + + it("filter should have no effect on an empty list"): + LazyList.empty[Int].filter(_ % 2 == 0).toList shouldBe List.empty + + it("append should append the other list at the end"): + LazyList(1, 2).append(LazyList(3, 4)).toList shouldBe List(1, 2, 3, 4) + LazyList.empty.append(LazyList(3, 4)).toList shouldBe List(3, 4) + + it("flatMap should replace each element with a list and flatten the final list"): + LazyList(1, 2).flatMap(a => LazyList(a, a)).toList shouldBe List(1, 1, 2, 2) + + it("continually should generate an infinite stream of the given value"): + continually(1).take(3).toList shouldBe List(1, 1, 1) + + it("from should generate an infinite stream starting with the given value"): + from(1).take(3).toList shouldBe List(1, 2, 3) + + it("fibs should generate an infinite stream of Fibonacci numbers"): + fibs.take(20).toList shouldBe + List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181) + + it("onesViaUnfold should generate an infinite stream of ones"): + onesViaUnfold.take(10).toList shouldBe List.fill(10)(1) + + it("fibsViaUnfold should generate an infinite stream of Fibonacci numbers"): + fibsViaUnfold.take(20).toList shouldBe + List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181) + + it("fromViaUnfold should generate an infinite stream starting with the given value"): + fromViaUnfold(1).take(3).toList shouldBe List(1, 2, 3) + + it("continuallyViaUnfold should generate an infinite stream of the given value"): + continuallyViaUnfold(1).take(3).toList shouldBe List(1, 1, 1) + + it("mapViaUnfold should transform a non empty list"): + LazyList(1, 2).mapViaUnfold(_ * 2).toList shouldBe List(2, 4) + + it("mapViaUnfold should have no effect on an empty list"): + LazyList.empty[Int].mapViaUnfold(_ * 2).toList shouldBe List.empty + + it("takeViaUnfold should take the first n element"): + LazyList(1, 2, 3).takeViaUnfold(2).toList shouldBe List(1, 2) + + it("takeViaUnfold should return an empty list when invoked on an empty list"): + LazyList.empty[Int].takeViaUnfold(2).toList shouldBe List.empty[Int] + + it("takeViaUnfold should return an the same list when n is greater than the length of the list"): + LazyList(1).takeViaUnfold(2).toList shouldBe List(1) + + it("takeWhileViaUnfold should return all starting elements that match the given predicate"): + LazyList(1, 2, 3).takeWhileViaUnfold(_ % 2 == 1).toList shouldBe List(1) + LazyList(1, 2, 3).takeWhileViaUnfold(_ % 2 == 0).toList shouldBe List.empty + + it("zipWith should combine elements from the given lists pairwise using the given function"): + LazyList(1, 2, 3) + .zipWith( + LazyList(true, false, true), + (x, y) => if y then x.toString() else y.toString() + ) + .toList shouldBe List("1", "false", "3") + + it("zipWith should terminate with the smaller list"): + LazyList(1, 2, 3) + .zipWith( + LazyList(true), + (x, y) => if y then x.toString() else y.toString() + ) + .toList shouldBe List("1") + + it("zipAll should continue as long as any one list is not empty"): + LazyList(1, 2, 3).zipAll(LazyList(true)).toList shouldBe + List(Some(1) -> Some(true), Some(2) -> None, Some(3) -> None) + + it("startsWith should check if the given list is a prefix of this list"): + LazyList(1, 2, 3).startsWith(LazyList(1, 2)) shouldBe true + LazyList(1).startsWith(LazyList(1, 2)) shouldBe false + LazyList(1, 3, 2).startsWith(LazyList(1, 2)) shouldBe false + + it("tails should return all the suffixes of the list"): + LazyList(1, 2, 3).tails.map(_.toList).toList shouldBe + List(List(1, 2, 3), List(2, 3), List(3), List()) + + it("scanRight should return a lazy list of the intermediate results"): + LazyList(1, 2, 3).scanRight(0)(_ + _).toList shouldBe List(6, 5, 3, 0)