diff --git a/src/main/scala/ch05/Autobot.scala b/src/main/scala/ch05/Autobot.scala new file mode 100644 index 0000000..13386e4 --- /dev/null +++ b/src/main/scala/ch05/Autobot.scala @@ -0,0 +1,63 @@ +package ch05 + +import scala.concurrent.Future +import cats.data.EitherT +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Await +import scala.concurrent.duration.* + +/* +5.4 Exercise: Monads: Transform and Roll Out + +The Autobots, well-known robots in disguise, frequently send messages +during battle requesting the power levels of their team mates. +This helps them coordinate strategies and launch devastating attacks. + +Transmissions take time in Earth’s viscous atmosphere, and messages are +occasionally lost due to satellite malfunction or sabotage by pesky Decepticons8. + +Optimus Prime is getting tired of the nested for comprehensions in his neural matrix. +Help him by rewriting Response using a monad transformer. + */ +object Autobot: + + type Response[A] = EitherT[Future, String, A] + + val powerLevels = Map( + "Jazz" -> 6, + "Bumblebee" -> 8, + "Hot Rod" -> 10 + ) + + /* + Implement getPowerLevel to retrieve data from a set of imaginary allies. + If an Autobot isn’t in the powerLevels map, return an error message reporting + that they were unreachable. Include the name in the message for good effect. + */ + 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")) + + /* + Two autobots can perform a special move if their combined power level is greater than 15. + If either ally is unavailable, fail with an appropriate error message. + */ + def canSpecialMove(ally1: String, ally2: String): Response[Boolean] = + for + lvl1 <- getPowerLevel(ally1) + lvl2 <- getPowerLevel(ally2) + yield (lvl1 + lvl2) > 15 + + /* + Write a method tacticalReport that takes two ally names and prints + a message saying whether they can perform a special move. + */ + 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." diff --git a/src/main/scala/ch05/ch05.worksheet.sc b/src/main/scala/ch05/ch05.worksheet.sc new file mode 100644 index 0000000..6ab6f82 --- /dev/null +++ b/src/main/scala/ch05/ch05.worksheet.sc @@ -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) +// ) \ No newline at end of file diff --git a/src/test/scala/ch05/AutobotSpec.scala b/src/test/scala/ch05/AutobotSpec.scala new file mode 100644 index 0000000..5514606 --- /dev/null +++ b/src/test/scala/ch05/AutobotSpec.scala @@ -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"