Skip to content

Commit

Permalink
Complete chapter 5: Monad transformers
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Jan 16, 2024
1 parent 02a7616 commit 5b81bd2
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 0 deletions.
37 changes: 37 additions & 0 deletions src/main/scala/ch05/Autobot.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ch05

import scala.concurrent.Future
import cats.data.EitherT
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Await
import scala.concurrent.duration.*

object Autobot:

type Response[A] = EitherT[Future, String, A]

val powerLevels = Map(
"Jazz" -> 6,
"Bumblebee" -> 8,
"Hot Rod" -> 10
)

def getPowerLevel(ally: String): Response[Int] =
powerLevels.get(ally) match
case Some(avg) => EitherT.right(Future(avg))
case None => EitherT.left(Future(s"$ally unreachable"))

def canSpecialMove(ally1: String, ally2: String): Response[Boolean] =
for
lvl1 <- getPowerLevel(ally1)
lvl2 <- getPowerLevel(ally2)
yield (lvl1 + lvl2) > 15

def tacticalReport(ally1: String, ally2: String): String =
val stack: Future[Either[String, Boolean]] =
canSpecialMove(ally1, ally2).value

Await.result(stack, 1.second) match
case Left(msg) => s"Comms error: $msg"
case Right(true) => s"$ally1 and $ally2 are ready to roll out!"
case Right(false) => s"$ally1 and $ally2 need a recharge."
71 changes: 71 additions & 0 deletions src/main/scala/ch05/ch05.worksheet.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import scala.concurrent.Await
import scala.concurrent.Future
import cats.data.{EitherT, OptionT}
import cats.syntax.applicative.catsSyntaxApplicativeId
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*

type ListOption[A] = OptionT[List, A]

val result1: ListOption[Int] = OptionT(List(Option(10)))

val result2: ListOption[Int] = 32.pure[ListOption]

result1.flatMap { (x: Int) =>
result2.map { (y: Int) =>
x + y
}
}

type FutureEither[A] = EitherT[Future, String, A]

type FutureEitherOption[A] = OptionT[FutureEither, A]

val futureEitherOr: FutureEitherOption[Int] =
for {
a <- 10.pure[FutureEitherOption]
b <- 32.pure[FutureEitherOption]
} yield a + b

val intermediate = futureEitherOr.value
// intermediate: FutureEither[Option[Int]] = EitherT(
// Future(Success(Right(Some(42))))
// )

val stack = intermediate.value
// stack: Future[Either[String, Option[Int]]] = Future(Success(Right(Some(42))))

Await.result(stack, 1.second)

import cats.data.Writer

type Logged[A] = Writer[List[String], A]

// Methods generally return untransformed stacks:
def parseNumber(str: String): Logged[Option[Int]] =
util.Try(str.toInt).toOption match {
case Some(num) => Writer(List(s"Read $str"), Some(num))
case None => Writer(List(s"Failed on $str"), None)
}

// Consumers use monad transformers locally to simplify composition:
def addAll(a: String, b: String, c: String): Logged[Option[Int]] =
val result =
for
a <- OptionT(parseNumber(a))
b <- OptionT(parseNumber(b))
c <- OptionT(parseNumber(c))
yield a + b + c

result.value


// This approach doesn't force OptionT on other users' code:
val x = addAll("1", "2", "3")
// result1: Logged[Option[Int]] = WriterT(
// (List("Read 1", "Read 2", "Read 3"), Some(6))
// )
val y = addAll("1", "a", "3")
// result2: Logged[Option[Int]] = WriterT(
// (List("Read 1", "Failed on a"), None)
// )
15 changes: 15 additions & 0 deletions src/test/scala/ch05/AutobotSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ch05

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe

class AutobotSpec extends AnyFunSpec:
it("should generate a tactitcal report"):
val report1 = Autobot.tacticalReport("Jazz", "Bumblebee")
report1 `shouldBe` "Jazz and Bumblebee need a recharge."

val report2 = Autobot.tacticalReport("Bumblebee", "Hot Rod")
report2 `shouldBe` "Bumblebee and Hot Rod are ready to roll out!"

val report3 = Autobot.tacticalReport("Jazz", "Ironhide")
report3 `shouldBe` "Comms error: Ironhide unreachable"

0 comments on commit 5b81bd2

Please sign in to comment.