Skip to content

Commit

Permalink
Complete chapter 4
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Dec 17, 2023
1 parent 80eab21 commit 443fa7d
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 1 deletion.
10 changes: 9 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ object chapter02 extends AdvancedScalaModule {

object chapter03 extends AdvancedScalaModule {
object test extends ScalaTests with TestModule.ScalaTest {
// // use `::` for scala deps, `:` for java deps
override def ivyDeps = Agg(
ivy"org.scalactic::scalactic:$scalatestVersion",
ivy"org.scalatest::scalatest:$scalatestVersion",
)
}
}

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",
Expand Down
39 changes: 39 additions & 0 deletions chapter04/src/Either.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
enum Either[+E, +A]:
case Left(value: E)
case Right(value: A)

/*
Exercise 4.6: Implement versions of map, flatMap, orElse, and map2
on Either that operate on the Right value.
*/
def map[B](f: A => B): Either[E, B] = this match
case Right(a) => Right(f(a))
case Left(e) => Left(e)

def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] = this match
case Right(a) => f(a)
case Left(e) => Left(e)

def orElse[EE >: E, B >: A](b: => Either[EE, B]): Either[EE, B] = this match
case Right(a) => Right(a)
case Left(_) => b

def map2[EE >: E, B, C](that: Either[EE, B])(f: (A, B) => C): Either[EE, C] =
// flatMap(x => that.map(y => f(x, y)))
for
a <- this
b <- that
yield f(a, b)

object Either:
/*
Exercise 4.7: Implement sequence and traverse for Either. These should return
the first error that's encountered if there is one.
*/
def sequence[E, A](as: List[Either[E, A]]): Either[E, List[A]] =
traverse(as)(a => a)

def traverse[E, A, B](as: List[A])(f: A => Either[E, B]): Either[E, List[B]] =
as match
case x :: xs => f(x).flatMap(a => traverse(xs)(f).map(a :: _))
case _ => Right(Nil)
72 changes: 72 additions & 0 deletions chapter04/src/Option.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
enum Option[+A]:
case Some(get: A)
case None

/*
Exersie 4.1: Implement the following functions on Option.
You should be able to implement all the functions besides
map and getOrElse without resorting to pattern matching.
Try implementing flatMap, orElse, and filter in terms of
map and getOrElse.
*/
def map[B](f: A => B): Option[B] = this match
case None => None
case Some(x) => Some(f(x))

def getOrElse[B >: A](default: => B): B = this match
case None => default
case Some(x) => x

def flatMap[B](f: A => Option[B]): Option[B] =
map(f).getOrElse(None)

def orElse[B >: A](ob: => Option[B]): Option[B] =
map(Some(_)).getOrElse(ob)

def filter(f: A => Boolean): Option[A] =
// map(x => if f(x) then Some(x) else None)
// .getOrElse(None)
flatMap(x => if f(x) then this else None)

object Option:
def mean(xs: Seq[Double]): Option[Double] =
if xs.isEmpty then None
else Some(xs.sum / xs.length)

/*
Exercise 4.2: Implement the variance function in terms of flatMap.
If the mean of a sequence is m, the variance is the mean of math.pow(x - m, 2)
for each element of the sequence.
*/
def variance(xs: Seq[Double]): Option[Double] =
mean(xs).flatMap(m => mean(xs.map(x => math.pow(x - m, 2))))

/*
Exercise 4.3: Write a generic function map2 that combines two Option values
using a binary function. If either Option is None, then the return value is too.
*/
def map2[A, B, C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
a.flatMap(x => b.map(y => f(x, y)))

/*
Exercise 4.4: Write a function sequence that combines a list of Options into
one Option containing a list of all the Some values on the original list.
If the original list contains None even once, the result of the function
should be None; otherwise, the result should be Some, with a list of all
the values.
*/
def sequence[A](as: List[Option[A]]): Option[List[A]] =
traverse(as)(a => a)

/*
Exercise 4.5: Sometimes we'll want to map over a list using a function
that might fail, returning None if applying it to any element of the
list returns None.
Implement this function. It's straightforward to do using map and sequence,
but try for a more efficient implementation that only looks at the list once.
In fact, implement sequence in in terms of traverse.
*/
def traverse[A, B](as: List[A])(f: A => Option[B]): Option[List[B]] =
as match
case x :: xs => f(x).flatMap(a => traverse(xs)(f).map(a :: _))
case _ => Some(Nil)
41 changes: 41 additions & 0 deletions chapter04/test/src/EitherSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Either.*
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe

class EitherSpec extends AnyFunSpec:
describe("Either"):
it("map should transform a Right"):
Right(2).map(_ + 2) shouldBe Right(4)

it("map should have no effect on a Left"):
(Left(2): Either[Int, Int]).map(_ + 2) shouldBe Left(2)

it("flatMap should transform a Right"):
Right(2).flatMap(x => Right(x + 2)) shouldBe Right(4)

it("flatMap should have no effect on a None"):
(Left(2): Either[Int, Int]).flatMap(x => Right(x + 2)) shouldBe Left(2)

it("orElse should return the first Either if it's a Right"):
Right(2).orElse(???) shouldBe Right(2)

it("orElse should return the second Either when invoked on a Left"):
(Left(2): Either[Int, Int]).orElse { Right(3) } shouldBe Right(3)
(Left(2): Either[Int, Int]).orElse { Left(3) } shouldBe Left(3)

it("map2 should combine two Either values"):
Right(2).map2(Right(3))(_ * _) shouldBe Right(6)

it("map2 should return Left if any one of the arguments is a Left"):
val l: Either[Int, Int] = Left(3)
val r: Either[Int, Int] = Right(2)

r.map2(l)(_ * _) shouldBe Left(3)
l.map2(r)(_ * _) shouldBe Left(3)
l.map2(l)(_ * _) shouldBe Left(3)

it("sequence should collect all the Right values"):
sequence(List(Right(1), Right(2), Right(3))) shouldBe Right(List(1, 2, 3))

it("sequence should stop at first Left"):
sequence(List(Right(1), Left(2), Right(3))) shouldBe Left(2)
49 changes: 49 additions & 0 deletions chapter04/test/src/OptionSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Option.*
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe

class OptionSpec extends AnyFunSpec:
describe("Option"):
it("map should transform a Some"):
Some(2).map(_ + 2) shouldBe Some(4)

it("map should have no effect on a None"):
Some(2).filter(_ % 2 == 1).map(_ + 2) shouldBe None

it("getOrElse should return the value inside a Some"):
Some(2).getOrElse(???) shouldBe 2

it("getOrElse should return the default value when invoked on a None"):
Some(2).filter(_ % 2 == 1).getOrElse { 3 } shouldBe 3

it("filter should keep the value if it satisfies the given predicate"):
Some(2).filter(_ % 2 == 0) shouldBe Some(2)

it("filter should convert Some to None if the value doesn't satisfy the given predicate"):
Some(2).filter(_ % 2 == 1) shouldBe None

it("flatMap should transform a Some"):
Some(2).flatMap(x => Some(x + 2)) shouldBe Some(4)

it("flatMap should have no effect on a None"):
Some(2).filter(_ % 2 == 1).flatMap(x => Some(x + 2)) shouldBe None

it("orElse should return the first Option if it's a Some"):
Some(2).orElse(???) shouldBe Some(2)

it("orElse should return the second Option when invoked on a None"):
Some(2).filter(_ % 2 == 1).orElse { Some(3) } shouldBe Some(3)

it("map2 should combine two Some values"):
map2(Some(2), Some(3))(_ * _) shouldBe Some(6)

it("map2 should return None if any one of the arguments is a None"):
map2(Some(2), None: Option[Int])(_ * _) shouldBe None
map2(None: Option[Int], Some(2))(_ * _) shouldBe None
map2(None: Option[Int], None: Option[Int])(_ * _) shouldBe None

it("sequence should collect all the Some values"):
sequence(List(Some(1), Some(2), Some(3))) shouldBe Some(List(1, 2, 3))

it("sequence should stop at first None"):
sequence(List(Some(1), None, Some(2))) shouldBe None

0 comments on commit 443fa7d

Please sign in to comment.