diff --git a/README.md b/README.md index b943311..2bd4757 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ Official GitHub repo: https://github.com/fpinscala/fpinscala 10. Monoids +11. Monads + +12. Applicative and traversable functors + ## Executing a main method ``` ./millw .runMain --mainClass @@ -36,7 +40,7 @@ Official GitHub repo: https://github.com/fpinscala/fpinscala ## VSCode -* Open command palette: `Ctrl+Shift+P` +* Open command palette: `Ctrl + Shift + P` * Open another window: `File > New Window > Open Recent/Open Folder` @@ -46,6 +50,10 @@ Official GitHub repo: https://github.com/fpinscala/fpinscala * Add `"workbench.editor.enablePreview": false` to `settings.json` and save it. +* To see the methods in a file: `Cmd + Shift + P` + +* To search a file by name: `Cmd + P` + ## Mill Install a BSP connection file: diff --git a/build.sc b/build.sc index bd5b2ac..959c4a1 100644 --- a/build.sc +++ b/build.sc @@ -4,7 +4,7 @@ trait FPModule extends ScalaModule with ScalafmtModule { // val baseDir = build.millSourcePath def scalaVersion = "3.3.1" - override def scalacOptions: T[Seq[String]] = Seq( + def scalacOptions: T[Seq[String]] = Seq( "-encoding", "UTF-8", "-feature", "-Werror", @@ -55,3 +55,28 @@ object chapter10 extends FPModule { ) } } + +object chapter11 extends FPModule { + def moduleDeps = Seq(chapter06) + def scalacOptions = super.scalacOptions() ++ Agg( + "-Ykind-projector:underscores" + ) + + object test extends FpTestModule with ScalaTests { + def ivyDeps = super.ivyDeps() ++ Agg( + ivy"org.scalatestplus::scalacheck-1-17:$scalacheckVersion" + ) + } +} + +object chapter12 extends FPModule { + def moduleDeps = Seq(chapter06, chapter10) + def scalacOptions = super.scalacOptions() ++ Agg( + "-Ykind-projector:underscores" + ) + object test extends FpTestModule with ScalaTests { + def ivyDeps = super.ivyDeps() ++ Agg( + ivy"org.scalatestplus::scalacheck-1-17:$scalacheckVersion" + ) + } +} diff --git a/chapter10/src/Foldable.scala b/chapter10/src/Foldable.scala index be897d5..10f568b 100644 --- a/chapter10/src/Foldable.scala +++ b/chapter10/src/Foldable.scala @@ -1,9 +1,17 @@ trait Foldable[F[_]]: + import Monoid.dual + import MonoidInstances.endoMonoid + extension [A](as: F[A]) - def foldRight[B](acc: B)(f: (A, B) => B): B - def foldLeft[B](acc: B)(f: (B, A) => B): B + def foldRight[B](acc: B)(f: (A, B) => B): B = + as.foldMap(f.curried)(using dual(endoMonoid[B]))(acc) + + def foldLeft[B](acc: B)(f: (B, A) => B): B = + as.foldMap(a => b => f(b, a))(using endoMonoid[B])(acc) + def foldMap[B](f: A => B)(using m: Monoid[B]): B = foldLeft(m.empty)((acc, x) => m.combine(acc, f(x))) + def combineAll(using m: Monoid[A]): A = as.foldLeft(m.empty)(m.combine) /* diff --git a/chapter10/src/Monoid.scala b/chapter10/src/Monoid.scala index e0b95d6..ab86d7d 100644 --- a/chapter10/src/Monoid.scala +++ b/chapter10/src/Monoid.scala @@ -147,4 +147,5 @@ object MonoidInstances: val empty: A => B = _ => mb.empty object MonoidSyntax: - extension [T](a: T)(using m: Monoid[T]) def |+|(b: T): T = m.combine(a, b) + // 'a' is the thing on which the method |+| is invoked. + extension [A](a: A)(using m: Monoid[A]) infix def |+|(b: A): A = m.combine(a, b) diff --git a/chapter10/test/src/MonoidLaws.scala b/chapter10/test/src/MonoidLaws.scala index 3da30ad..7b92599 100644 --- a/chapter10/test/src/MonoidLaws.scala +++ b/chapter10/test/src/MonoidLaws.scala @@ -1,9 +1,9 @@ -import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers.shouldBe import MonoidSyntax.* import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import org.scalacheck.Arbitrary +import org.scalatest.funspec.AnyFunSpecLike /* Multiple context bounds are separated by ':', like @@ -13,19 +13,21 @@ Multiple using are separated by ',', like (using instanceM: Monoid[A], arbA: Arbitrary[A]). If one using parameter is named, all should be named? */ -trait MonoidLaws { this: AnyFunSpec & ScalaCheckPropertyChecks => - def associativityLaw[A: Arbitrary: Monoid]: Unit = - it("should satisfy the associativity law"): - forAll { (x: A, y: A, z: A) => - ((x |+| y) |+| z) shouldBe (x |+| (y |+| z)) +trait MonoidLaws extends AnyFunSpecLike with ScalaCheckPropertyChecks: + def leftIdentity[A: Arbitrary: Monoid]: Unit = + it("should satisfy the left identity law"): + forAll { (x: A) => + (summon[Monoid[A]].empty |+| x) shouldBe x } - def identityLaw[A: Arbitrary: Monoid]: Unit = - val instanceM = summon[Monoid[A]] - - it("should satisfy the identity law"): + def rightIdentity[A: Arbitrary: Monoid]: Unit = + it("should satisfy the right identity law"): forAll { (x: A) => - (x |+| instanceM.empty) shouldBe x - (instanceM.empty |+| x) shouldBe x + (x |+| summon[Monoid[A]].empty) shouldBe x + } + + def associativity[A: Arbitrary: Monoid]: Unit = + it("should satisfy the associativity law"): + forAll { (x: A, y: A, z: A) => + ((x |+| y) |+| z) shouldBe (x |+| (y |+| z)) } -} diff --git a/chapter10/test/src/MonoidLawsSpec.scala b/chapter10/test/src/MonoidLawsSpec.scala index 5fac93f..7ca904e 100644 --- a/chapter10/test/src/MonoidLawsSpec.scala +++ b/chapter10/test/src/MonoidLawsSpec.scala @@ -10,41 +10,45 @@ class MonoidLawsSpec extends AnyFunSpec with ScalaCheckPropertyChecks with Monoi describe("Integers form a Monoid under addition"): import MonoidInstances.intAddition - associativityLaw[Int] - identityLaw[Int] + leftIdentity[Int] + rightIdentity[Int] + associativity[Int] describe("Integers form a Monoid under multiplication"): given Monoid[Int] = MonoidInstances.intMultiplication - associativityLaw[Int] - identityLaw[Int] + leftIdentity[Int] + rightIdentity[Int] + associativity[Int] describe("Booleans form a Monoid under disjunction (OR)"): given Monoid[Boolean] = MonoidInstances.booleanOr - associativityLaw[Boolean] - identityLaw[Boolean] + leftIdentity[Boolean] + rightIdentity[Boolean] + associativity[Boolean] describe("Booleans form a Monoid under conjunction (AND)"): given Monoid[Boolean] = MonoidInstances.booleanAnd - associativityLaw[Boolean] - identityLaw[Boolean] + leftIdentity[Boolean] + rightIdentity[Boolean] + associativity[Boolean] - describe("Options form a Monoid"): + describe("Option forms a Monoid"): given Monoid[Option[Int]] = MonoidInstances.optionMonoid - associativityLaw[Option[Int]] - identityLaw[Option[Int]] + leftIdentity[Option[Int]] + rightIdentity[Option[Int]] + associativity[Option[Int]] import MonoidInstances.WC val wcGen: Gen[WC] = - def genStringN(n: Int) = Gen - .containerOfN[List, Char](n, Gen.asciiPrintableChar) - .flatMap(_.mkString) - val genString = Gen.choose(0, 10).flatMap(genStringN) - val genStub = genString.map(WC.Stub(_)) + val genString = Gen + .choose(0, 10) + .flatMap(Gen.stringOfN(_, Gen.asciiPrintableChar)) + val genStub = genString.map(WC.Stub(_)) val genPart = for lStub <- genString words <- Gen.choose(0, 10) @@ -59,24 +63,28 @@ class MonoidLawsSpec extends AnyFunSpec with ScalaCheckPropertyChecks with Monoi given Arbitrary[WC] = Arbitrary(wcGen) given Monoid[WC] = MonoidInstances.wcMonoid - associativityLaw[WC] - identityLaw[WC] + leftIdentity[WC] + rightIdentity[WC] + associativity[WC] describe("Tuple2s form a Monoid under integer addition"): import MonoidInstances.{productMonoid, intAddition} - associativityLaw[(Int, Int)] - identityLaw[(Int, Int)] + leftIdentity[(Int, Int)] + rightIdentity[(Int, Int)] + associativity[(Int, Int)] describe("Tuple2s form a Monoid under integer multiplication"): import MonoidInstances.productMonoid given Monoid[Int] = MonoidInstances.intMultiplication - associativityLaw[(Int, Int)] - identityLaw[(Int, Int)] + leftIdentity[(Int, Int)] + rightIdentity[(Int, Int)] + associativity[(Int, Int)] describe("Maps form a Monoid on their values"): import MonoidInstances.{mapMergeMonoid, intAddition} - associativityLaw[Map[String, Int]] - identityLaw[Map[String, Int]] + leftIdentity[Map[String, Int]] + rightIdentity[Map[String, Int]] + associativity[Map[String, Int]] diff --git a/chapter11/src/Id.scala b/chapter11/src/Id.scala new file mode 100644 index 0000000..64fc03f --- /dev/null +++ b/chapter11/src/Id.scala @@ -0,0 +1,16 @@ +/* +Exercise 11.17: Implement map and flatMap as methods on this class, +and give an implementation for Monad[Id]. + */ +case class Id[+A](value: A): + def map[B](f: A => B): Id[B] = + Id(f(value)) + def flatMap[B](f: A => Id[B]): Id[B] = + f(value) + +object Id: + given idMonad: Monad[Id] with + def unit[A](a: => A) = Id(a) + extension [A](fa: Id[A]) + override def flatMap[B](f: A => Id[B]) = + fa.flatMap(f) diff --git a/chapter11/src/Monad.scala b/chapter11/src/Monad.scala new file mode 100644 index 0000000..d996bf5 --- /dev/null +++ b/chapter11/src/Monad.scala @@ -0,0 +1,106 @@ +trait Functor[F[_]]: + extension [A](fa: F[A]) def map[B](f: A => B): F[B] + +trait Monad[F[_]] extends Functor[F]: + def unit[A](a: => A): F[A] + + extension [A](fa: F[A]) + /* + Exercise 11.8: Implement flatMap in terms of compose. + */ + def flatMap[B](f: A => F[B]): F[B] + // compose returns Any => F[B]. + // Commenting out to prevent cyclic defintion. + // compose(const(fa), f)(const) + + /* + Exercise 11.13: Implement either flatMap or compose in terms of join and map. + */ + // join(fa.map(f)) + + def map[B](f: A => B): F[B] = + flatMap(f.andThen(unit)) + + def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] = + fa.flatMap(a => fb.map(b => f(a, b))) + + def product[B](fb: F[B]): F[(A, B)] = + fa.map2(fb)((_, _)) + + def const[A](fa: F[A])(x: Any): F[A] = fa + /* + Exercise 11.3: Implement sequence and traverse on Monad[F]. + */ + def sequence[A](fas: List[F[A]]): F[List[A]] = + traverse(fas)(identity) + + def traverse[A, B](as: List[A])(f: A => F[B]): F[List[B]] = + as.foldRight(unit(List.empty[B]))((a, b) => f(a).map2(b)(_ :: _)) + + /* + Exercise 11.4: Implement replicateM. + */ + def replicateM[A](n: Int, fa: F[A]): F[List[A]] = + sequence(List.fill(n)(fa)) + + /* + Exercise 11.5: Implement the function filterM-it's a bit like filter, + except instead of a function from A => Boolean, we have an A => F[Boolean]. + */ + def filterM[A](as: List[A])(f: A => F[Boolean]): F[List[A]] = + def takeOrDrop(a: A)(keep: Boolean, acc: List[A]): List[A] = + if keep then a :: acc else acc + + val z = unit(List.empty[A]) + as.foldRight(z)((a, b) => f(a).map2(b)(takeOrDrop(a))) + + /* + Exercise 11.7: Implement the Kleisli composition function compose. + */ + def compose[A, B, C](f: A => F[B], g: B => F[C]): A => F[C] = + f.andThen(_.flatMap(g)) + + /* + Exercise 11.12: Implement join in terms of flatMap. + */ + def join[A](ffa: F[F[A]]): F[A] = + ffa.flatMap(identity) + +object MonadInstances: + /* + Exercise 11.1: Write monad instances for Option, List, LazyList, Par, and Parser. + */ + given optionMonad: Monad[Option] with + def unit[A](a: => A): Option[A] = Some(a) + extension [A](fa: Option[A]) + def flatMap[B](f: A => Option[B]): Option[B] = + fa match + case Some(a) => f(a) + case _ => None + + given listMonad: Monad[List] with + def unit[A](a: => A): List[A] = List(a) + extension [A](fa: List[A]) + def flatMap[B](f: A => List[B]): List[B] = + fa match + case Nil => Nil + case x :: xs => f(x) ++ xs.flatMap(f) + + /* + Exercise 11.2: Implement a State monad. + */ + // _ in type constructor requires kind-projector:underscores plugin enabled. + // Alternatively, Monad[[x] =>> State[S, x]] + given stateMonad[S]: Monad[State[S, _]] with + def unit[A](a: => A): State[S, A] = State(s => (a, s)) + extension [A](st: State[S, A]) + def flatMap[B](f: A => State[S, B]): State[S, B] = + State.flatMap(st)(f) + + given eitherMonad[E]: Monad[Either[E, _]] with + def unit[A](a: => A): Either[E, A] = Right(a) + extension [A](e: Either[E, A]) def flatMap[B](f: A => Either[E, B]): Either[E, B] = e.flatMap(f) + +object MonadSyntax: + // 'fA' is the thing on which the method >>= is invoked. + extension [F[_], A](fA: F[A])(using mA: Monad[F]) infix def >>=[B](f: A => F[B]): F[B] = mA.flatMap(fA)(f) diff --git a/chapter11/test/src/MonadLaws.scala b/chapter11/test/src/MonadLaws.scala new file mode 100644 index 0000000..a51ca8b --- /dev/null +++ b/chapter11/test/src/MonadLaws.scala @@ -0,0 +1,69 @@ +import org.scalacheck.Arbitrary + +import MonadSyntax.* +import org.scalactic.Equality +import scala.language.adhocExtensions +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import org.scalatest.funspec.AnyFunSpecLike +import org.scalatest.matchers.should.Matchers.shouldBe + +trait MonadLaws[F[_]](desc: String) extends ScalaCheckPropertyChecks with AnyFunSpecLike: + // Monad[F].unit(x).flatMap(f) === f(x) + def leftIdentity[A, B](using + Monad[F], + Arbitrary[A], + Arbitrary[A => F[B]], + Equality[F[B]] + ): Unit = + it("should satisfy the left identity law"): + forAll { (a: A, f: A => F[B]) => + val lhs = summon[Monad[F]].unit(a) >>= f + val rhs = f(a) + + lhs shouldBe rhs + } + + // m.flatMap(Monad[F].unit) === m + def rightIdentity[A](using + Monad[F], + Arbitrary[F[A]], + Equality[F[A]] + ): Unit = + it("should satisfy the right identity law"): + forAll { (fa: F[A]) => + val lhs = fa >>= summon[Monad[F]].unit + val rhs = fa + + lhs shouldBe rhs + } + + // m.flatMap(f).flatMap(g) === m.flatMap { x => f(x).flatMap(g) } + def associativity[A, B, C](using + Monad[F], + Arbitrary[F[A]], + Arbitrary[A => F[B]], + Arbitrary[B => F[C]], + Equality[F[C]] + ): Unit = + it("should satisfy the right associativity law"): + forAll { (fa: F[A], f: A => F[B], g: B => F[C]) => + val lhs = fa >>= f >>= g + val rhs = fa >>= (a => f(a) >>= (g)) + + lhs shouldBe rhs + } + + def checkAll[A, B, C](using + Monad[F], + Arbitrary[A], + Arbitrary[F[A]], + Arbitrary[A => F[B]], + Arbitrary[B => F[C]], + Equality[F[A]], + Equality[F[B]], + Equality[F[C]] + ) = + describe(s"$desc"): + leftIdentity[A, B] + rightIdentity[A] + associativity[A, B, C] diff --git a/chapter11/test/src/MonadLawsSpec.scala b/chapter11/test/src/MonadLawsSpec.scala new file mode 100644 index 0000000..738d161 --- /dev/null +++ b/chapter11/test/src/MonadLawsSpec.scala @@ -0,0 +1,20 @@ +import org.scalacheck.Arbitrary +import org.scalacheck.Gen + +class OptionMonadLawsSpec extends MonadLaws[Option]("Option monad"): + import MonadInstances.optionMonad + + checkAll[Int, Int, Int] + +class ListMonadLawsSpec extends MonadLaws[List]("List monad"): + import MonadInstances.listMonad + + checkAll[Int, Int, Int] + +class IdMonadSpec extends MonadLaws[Id]("Id monad"): + import Id.idMonad + + val genInt = Gen.choose(-100, 100) + given Arbitrary[Id[Int]] = Arbitrary(genInt.flatMap(Id(_))) + + checkAll[Int, Int, Int] diff --git a/chapter11/test/src/MonadSpec.scala b/chapter11/test/src/MonadSpec.scala new file mode 100644 index 0000000..77b19e8 --- /dev/null +++ b/chapter11/test/src/MonadSpec.scala @@ -0,0 +1,65 @@ +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.shouldBe +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import org.scalacheck.Gen +import org.scalatest.AppendedClues.convertToClueful + +class MonadSpec extends AnyFunSpec with ScalaCheckDrivenPropertyChecks: + val genInt = Gen.choose(-100, 100) + val genOptionInt = Gen.option(genInt) + val genEitherint = Gen.either(genInt, genInt) + def genList[A](g: Gen[A]) = Gen + .choose(0, 100) + .flatMap(Gen.listOfN(_, g)) + + describe("Monad"): + it("sequence should return None if there's a None in the input list"): + forAll(genList(genOptionInt)) { xs => + MonadInstances.optionMonad + .sequence(xs) match + case Some(ys) => ys shouldBe xs.map(_.get) + case _ => xs.exists(_.isEmpty) shouldBe true + } + + it("sequence should return first Left in the input list"): + forAll(genList(genEitherint)) { xs => + MonadInstances + .eitherMonad[Int] + .sequence(xs) match + case Left(x) => xs.find(_.isLeft).get + case Right(ys) => ys shouldBe xs.flatMap(_.toSeq) + } + + it("traverse should replace the None values in the input list"): + forAll(genList(genOptionInt)) { xs => + MonadInstances.optionMonad + .traverse(xs)(_.orElse(Option(1))) match + case Some(ys) => ys shouldBe xs.map(_.getOrElse(1)) + case _ => fail("shouldn't be here") + } + + it("traverse should return the Left values in the input list"): + forAll(genList(genEitherint)) { xs => + MonadInstances + .eitherMonad[Int] + .traverse(xs)(_.orElse(Right(1))) match + case Right(ys) => ys shouldBe xs.map(_.getOrElse(1)) + case _ => fail("shouldn't be here") + } + + it("replicateM should return None if the input is None or else n copies of the value"): + forAll(genOptionInt.flatMap(x => Gen.choose(1, 10).map(n => (n, x)))) { (n, x) => + MonadInstances.optionMonad + .replicateM(n, x) match + case Some(xs) => xs shouldBe List.fill(n)(x.get) + case _ => x shouldBe None + + } + + it("filterM should return an empty list when looking for an odd number in all evens"): + forAll(genList(genInt)) { xs => + MonadInstances.optionMonad + .filterM(xs.map(_ * 2))(x => Some(x % 2 == 1)) match + case Some(ys) => ys.isEmpty `withClue` (s", actual = $ys") + case _ => fail("shouldn't be here") + } diff --git a/chapter12/src/Applicative.scala b/chapter12/src/Applicative.scala new file mode 100644 index 0000000..c14e9fa --- /dev/null +++ b/chapter12/src/Applicative.scala @@ -0,0 +1,115 @@ +trait Functor[F[_]]: + extension [A](fa: F[A]) def map[B](f: A => B): F[B] + +trait Applicative[F[_]] extends Functor[F]: + // primitive combinators + def unit[A](a: => A): F[A] + /* + Exercise 12.2: Define in terms of map2. + */ + def apply[A, B](fab: F[A => B])(fa: F[A]): F[B] = + fab.map2(fa)((f, a) => f(a)) + + def assoc[A, B, C](p: (A, (B, C))): ((A, B), C) = + p match + case (a, (b, c)) => ((a, b), c) + + /* + Exercise 12.8: We can take the product of two applicative functors. + Implement this function on the Applicative trait. + */ + def product[G[_]](G: Applicative[G]): Applicative[[x] =>> (F[x], G[x])] = + val self = this + new: + def unit[A](a: => A) = (self.unit(a), G.unit(a)) + override def apply[A, B](fs: (F[A => B], G[A => B]))(p: (F[A], G[A])) = + (self.apply(fs(0))(p(0)), G.apply(fs(1))(p(1))) + + /* + Exercise 12.9: Applicative functors compose! If F[_] and G[_] are applicative functors, + then so is F[G[_]]. Implement this function on the Applicative trait. + */ + def compose[G[_]](G: Applicative[G]): Applicative[[x] =>> F[G[x]]] = + val self = this + new: + def unit[A](a: => A) = self.unit(G.unit(a)) + extension [A](fga: F[G[A]]) + override def map2[B, C](fgb: F[G[B]])(f: (A, B) => C) = + self.map2(fga)(fgb)(G.map2(_)(_)(f)) + + // derived combinators + extension [A](fa: F[A]) + /* + Exercise 12.2: Define in terms of apply and unit. + */ + def map[B](f: A => B): F[B] = + // fa.map2(unit(()))((a, _) => f(a)) + apply(unit(f))(fa) + + /* + Exercise 12.2: Define in terms of apply and map. + */ + def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] = + apply(apply(unit(f.curried))(fa))(fb) + + def product[B](fb: F[B]): F[(A, B)] = + fa.map2(fb)((_, _)) + + /* + Exercise 12.1: Implement sequence, traverse, replicateM, and product + using only map2 and unit or methods implemented in terms of them. + */ + def sequence[A](fas: List[F[A]]): F[List[A]] = + traverse(fas)(identity) + + def traverse[A, B](as: List[A])(f: A => F[B]): F[List[B]] = + as.foldRight(unit(List.empty[B]))((a, b) => f(a).map2(b)(_ :: _)) + + def replicateM[A](n: Int, fa: F[A]): F[List[A]] = + sequence(List.fill(n)(fa)) + + /* + Exercise 12.12: Implement sequence over a Map. + */ + def sequenceMap[K, V](ofv: Map[K, F[V]]): F[Map[K, V]] = + ofv.foldLeft(unit(Map.empty[K, V])): + case (acc, (k, fv)) => + acc.map2(fv)((m, v) => m + (k -> v)) + +object ApplicativeInstances: + + /* + Exercise 12.6: Write an Applicative instance for Validated that accumulates errors in Invalid. + */ + import Validated.* + + given validatedApplicative[E: Semigroup]: Applicative[Validated[E, _]] with + def unit[A](a: => A) = Valid(a) + + extension [A](fa: Validated[E, A]) + override def map2[B, C](fb: Validated[E, B])(f: (A, B) => C) = + (fa, fb) match + case (Valid(a), Valid(b)) => Valid(f(a, b)) + case (Invalid(e1), Invalid(e2)) => + Invalid(summon[Semigroup[E]].combine(e1, e2)) + case (Invalid(e), _) => Invalid(e) + case (_, Invalid(e)) => Invalid(e) + + given OptionApplicative: Applicative[Option] with + def unit[A](a: => A) = Some(a) + + extension [A](fa: Option[A]) + override def map2[B, C](fb: Option[B])(f: (A, B) => C) = + (fa, fb) match + case (Some(a), Some(b)) => Some(f(a, b)) + case (None, _) => None + case (_, None) => None + + type Const[A, B] = A + + given monoidApplicative[M](using + m: Monoid[M] + ): Applicative[Const[M, _]] with + def unit[A](a: => A): M = m.empty + override def apply[A, B](m1: M)(m2: M): M = + m.combine(m1, m2) diff --git a/chapter12/src/Monad.scala b/chapter12/src/Monad.scala new file mode 100644 index 0000000..74d6e16 --- /dev/null +++ b/chapter12/src/Monad.scala @@ -0,0 +1,37 @@ +trait Monad[F[_]] extends Applicative[F]: + extension [A](fa: F[A]) + def flatMap[B](f: A => F[B]): F[B] = + fa.map(f).join + + override def map[B](f: A => B): F[B] = + fa.flatMap(a => unit(f(a))) + + override def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] = + fa.flatMap(a => fb.map(b => f(a, b))) + + extension [A](ffa: F[F[A]]) def join: F[A] = ffa.flatMap(identity) + + def compose[A, B, C](f: A => F[B], g: B => F[C]): A => F[C] = + a => f(a).flatMap(g) + +object MonadInstances: + /* + Exercise 12.5: Write a Monad instance for Either. + */ + given eitherMonad[E]: Monad[Either[E, _]] with + def unit[A](a: => A): Either[E, A] = Right(a) + extension [A](eea: Either[E, A]) + override def flatMap[B](f: A => Either[E, B]) = eea match + case Right(a) => f(a) + case Left(b) => Left(b) + + given stateMonad[S]: Monad[State[S, _]] with + def unit[A](a: => A): State[S, A] = State(s => (a, s)) + extension [A](st: State[S, A]) + override def flatMap[B](f: A => State[S, B]): State[S, B] = + State.flatMap(st)(f) + + type Id[A] = A + given idMonad: Monad[Id] with + def unit[A](a: => A) = a + extension [A](a: A) override def flatMap[B](f: A => B): B = f(a) diff --git a/chapter12/src/Semigroup.scala b/chapter12/src/Semigroup.scala new file mode 100644 index 0000000..370b828 --- /dev/null +++ b/chapter12/src/Semigroup.scala @@ -0,0 +1,21 @@ +trait Semigroup[A]: + def combine(a1: A, a2: A): A + +trait Monoid[A] extends Semigroup[A]: + def empty: A + +enum Validated[+E, +A]: + case Valid(get: A) + case Invalid(error: E) + +case class NonEmptyList[+A](head: A, tail: List[A]): + def toList: List[A] = head :: tail + +object NonEmptyList: + def apply[A](head: A, tail: A*): NonEmptyList[A] = + NonEmptyList(head, tail.toList) + +object SemigroupInstances: + given nelSemigroup[A]: Semigroup[NonEmptyList[A]] with + def combine(x: NonEmptyList[A], y: NonEmptyList[A]) = + NonEmptyList(x.head, x.tail ++ (y.head :: y.tail)) diff --git a/chapter12/src/Traverse.scala b/chapter12/src/Traverse.scala new file mode 100644 index 0000000..4c8c62c --- /dev/null +++ b/chapter12/src/Traverse.scala @@ -0,0 +1,96 @@ +import ApplicativeInstances.{Const, monoidApplicative} +import MonadInstances.{Id, stateMonad, idMonad} + +trait Traverse[F[_]] extends Functor[F], Foldable[F]: + extension [A](fa: F[A]) + def traverse[G[_]: Applicative, B](f: A => G[B]): G[F[B]] = + fa.map(f).sequence + + /* + Exercise 12.14: Implement map in terms of traverse. + */ + def map[B](f: A => B): F[B] = + fa.traverse[Id, B](f)(using idMonad) + + override def foldMap[B: Monoid](f: A => B): B = + fa.traverse[Const[B, _], Nothing](f) + + def mapAccum[S, B](s: S)(f: (A, S) => (B, S)): (F[B], S) = + fa.traverse(a => + for + s1 <- State.get[S] + (b, s2) = f(a, s1) + _ <- State.set(s2) + yield b + ).run(s) + + override def toList: List[A] = + fa.mapAccum(List[A]())((a, s) => ((), a :: s))(1).reverse + + def zipWithIndex: F[(A, Int)] = + fa.mapAccum(0)((a, s) => ((a, s), s + 1))(0) + + /* + Exercise 12.16: Write a function to reverse any traversable functor. + */ + def reverse: F[A] = + fa.mapAccum(fa.toList.reverse)((_, as) => (as.head, as.tail))(0) + + /* + Exercise 12.17: Use mapAccum to give a default implementation of foldLeft. + */ + override def foldLeft[B](acc: B)(f: (B, A) => B): B = + fa.mapAccum(acc)((a, b) => ((), f(b, a)))(1) + + extension [G[_]: Applicative, A](fga: F[G[A]]) + def sequence: G[F[A]] = + fga.traverse(ga => ga) + + /* + Exercise 12.19: Implement the composition of two Traverse instances. + */ + def compose[G[_]: Traverse]: Traverse[[x] =>> F[G[x]]] = + val self = this + new: + extension [A](fga: F[G[A]]) + override def traverse[H[_]: Applicative, B](f: A => H[B]): H[F[G[B]]] = + self.traverse(fga)(ga => ga.traverse(f)) + +/* +Exercise 12.13: Write Traverse instances for List, Option, Tree, and [x] =>> Map[K, x]. + */ +object TraverseInstances: + + given listTraverse: Traverse[List] with + extension [A](as: List[A]) + override def traverse[G[_]: Applicative, B](f: A => G[B]): G[List[B]] = + val g = summon[Applicative[G]] + as.foldRight(g.unit(List[B]()))((a, acc) => f(a).map2(acc)(_ :: _)) + + given optionTraverse: Traverse[Option] with + extension [A](oa: Option[A]) + override def traverse[G[_]: Applicative, B]( + f: A => G[B] + ): G[Option[B]] = + oa match + case Some(a) => f(a).map(Some(_)) + case None => summon[Applicative[G]].unit(None) + + case class Tree[+A](head: A, tail: List[Tree[A]]) + /* + We immediately invoke f with the head value, which gives us a G[B]. + We then traverse the tail list of subtree and recursively traverse + each of them, which gives us a G[List[B]]. We use map2 to combine + the head G[B] and subtree G[List[B]] into a single G[Tree[B]] + */ + given treeTraverse: Traverse[Tree] = new: + extension [A](ta: Tree[A]) + override def traverse[G[_]: Applicative, B](f: A => G[B]): G[Tree[B]] = + f(ta.head).map2(ta.tail.traverse(a => a.traverse(f)))(Tree(_, _)) + + given mapTraverse[K]: Traverse[Map[K, _]] with + extension [A](m: Map[K, A]) + override def traverse[G[_]: Applicative, B](f: A => G[B]): G[Map[K, B]] = + m.foldLeft(summon[Applicative[G]].unit(Map.empty[K, B])): + case (acc, (k, a)) => + acc.map2(f(a))((m, b) => m + (k -> b)) diff --git a/chapter12/test/src/ApplicativeLaws.scala b/chapter12/test/src/ApplicativeLaws.scala new file mode 100644 index 0000000..b01a22a --- /dev/null +++ b/chapter12/test/src/ApplicativeLaws.scala @@ -0,0 +1,104 @@ +import org.scalacheck.Arbitrary +import org.scalactic.Equality +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import org.scalatest.funspec.AnyFunSpecLike +import org.scalatest.matchers.should.Matchers.shouldBe + +trait ApplicativeLaws[F[_]](desc: String) extends ScalaCheckPropertyChecks with AnyFunSpecLike: + def functorIdentity[A](using + Applicative[F], + Arbitrary[A], + Arbitrary[F[A]], + Equality[F[A]] + ): Unit = + it("should satisfy the Functor identity law"): + forAll { (fa: F[A]) => + fa.map(identity) shouldBe fa + } + + def functorComposition[A, B, C](using + Applicative[F], + Arbitrary[F[A]], + Arbitrary[A => B], + Arbitrary[B => C], + Equality[F[C]] + ): Unit = + it("should satisfy the Functor composition law"): + forAll { (fa: F[A], f: A => B, g: B => C) => + fa.map(f).map(g) shouldBe fa.map(f.andThen(g)) + } + + def leftIdentity[A](using + Applicative[F], + Arbitrary[A], + Arbitrary[F[A]], + Equality[F[A]] + ): Unit = + it("should satisfy the left identity law"): + forAll { (fa: F[A]) => + val af = summon[Applicative[F]] + af.unit(()).map2(fa)((_, a) => a) shouldBe fa + } + + def rightIdentity[A](using + Applicative[F], + Arbitrary[A], + Arbitrary[F[A]], + Equality[F[A]] + ): Unit = + it("should satisfy the right identity law"): + forAll { (fa: F[A]) => + val af = summon[Applicative[F]] + fa.map2(af.unit(()))((a, _) => a) shouldBe fa + } + + def associativity[A, B, C](using + Applicative[F], + Arbitrary[F[A]], + Arbitrary[F[B]], + Arbitrary[F[C]], + Equality[F[A]], + Equality[F[B]], + Equality[F[C]] + ): Unit = + it("should satisfy the associativity law"): + forAll { (fa: F[A], fb: F[B], fc: F[C]) => + val af = summon[Applicative[F]] + + val lhs: F[((A, B), C)] = fa.product(fb).product(fc) + val rhs: F[((A, B), C)] = fa.product(fb.product(fc)).map(af.assoc) + + lhs shouldBe rhs + } + + def naturality[A, B, C](using + Applicative[F], + Arbitrary[F[A]], + Arbitrary[F[B]], + Arbitrary[A => B], + Arbitrary[B => C] + ): Unit = + it("should satisfy the naturality law"): + forAll { (fa: F[A], fb: F[B], f: A => B, g: B => C) => + fa.map2(fb)((a, b) => (f(a), g(b))) == fa.map(f).product(fb.map(g)) + } + + def checkAll[A, B, C](using + Applicative[F], + Arbitrary[A], + Arbitrary[F[A]], + Arbitrary[F[B]], + Arbitrary[F[C]], + Arbitrary[A => B], + Arbitrary[B => C], + Equality[F[A]], + Equality[F[B]], + Equality[F[C]] + ) = + describe(s"$desc"): + functorIdentity[A] + functorComposition[A, B, C] + leftIdentity[A] + rightIdentity[A] + associativity[A, B, C] + naturality[A, B, C] diff --git a/chapter12/test/src/ApplicativeLawsSpec.scala b/chapter12/test/src/ApplicativeLawsSpec.scala new file mode 100644 index 0000000..828bc34 --- /dev/null +++ b/chapter12/test/src/ApplicativeLawsSpec.scala @@ -0,0 +1,26 @@ +import org.scalacheck.{Arbitrary, Gen} +class OptionApplicativeLawsSpec extends ApplicativeLaws[Option]("Option applicative"): + import ApplicativeInstances.OptionApplicative + + checkAll[Int, Int, Int] + +class ValidApplicativeLawsSpec extends ApplicativeLaws[Validated[NonEmptyList[String], _]]("Validated applicative"): + import ApplicativeInstances.validatedApplicative + import SemigroupInstances.nelSemigroup + import Validated.* + + val genNonNegativeInt = Gen.choose(0, 10) + val genPostiveInt = Gen.choose(1, 100) + def genStrN(n: Int): Gen[String] = Gen.stringOfN(n, Gen.asciiPrintableChar) + def genNel(n: Int): Gen[List[String]] = Gen.listOfN(n, genNonNegativeInt.flatMap(genStrN)) + val genNEL: Gen[NonEmptyList[String]] = genPostiveInt + .flatMap(n => genNel(n).flatMap(xs => NonEmptyList(xs.head, xs.tail))) + val genValidated = Gen.oneOf( + genNEL.flatMap(Invalid(_)), + genNonNegativeInt.flatMap(Valid(_)) + ) + + given arbValidated: Arbitrary[Validated[NonEmptyList[String], Int]] = + Arbitrary(genValidated) + + checkAll[Int, Int, Int]