Skip to content

Commit

Permalink
Complete chapters 11 and 12
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Dec 21, 2023
1 parent 100109a commit 9328997
Show file tree
Hide file tree
Showing 17 changed files with 769 additions and 42 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <module>.runMain --mainClass <fully-qualified main class>
Expand All @@ -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`

Expand All @@ -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:
Expand Down
27 changes: 26 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
)
}
}
12 changes: 10 additions & 2 deletions chapter10/src/Foldable.scala
Original file line number Diff line number Diff line change
@@ -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)
/*
Expand Down
3 changes: 2 additions & 1 deletion chapter10/src/Monoid.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
28 changes: 15 additions & 13 deletions chapter10/test/src/MonoidLaws.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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))
}
}
56 changes: 32 additions & 24 deletions chapter10/test/src/MonoidLawsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]]
16 changes: 16 additions & 0 deletions chapter11/src/Id.scala
Original file line number Diff line number Diff line change
@@ -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)
106 changes: 106 additions & 0 deletions chapter11/src/Monad.scala
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 9328997

Please sign in to comment.