From 9b285ed120e2131f37604914f1f1674f007f6881 Mon Sep 17 00:00:00 2001 From: Matt Bovel <matthieu@bovel.net> Date: Wed, 11 May 2022 14:06:13 +0200 Subject: [PATCH] Add solutions of exercises 4 --- exercises/solutions-4/.gitignore | 8 +++ exercises/solutions-4/.scalafmt.conf | 4 ++ exercises/solutions-4/Readme.md | 4 ++ exercises/solutions-4/build.sbt | 22 ++++++ exercises/solutions-4/project/plugins.sbt | 1 + .../solutions-4/src/main/scala/Problem1.scala | 28 ++++++++ .../solutions-4/src/main/scala/Problem2.scala | 72 +++++++++++++++++++ .../src/test/scala/Problem1Test.scala | 52 ++++++++++++++ 8 files changed, 191 insertions(+) create mode 100644 exercises/solutions-4/.gitignore create mode 100644 exercises/solutions-4/.scalafmt.conf create mode 100644 exercises/solutions-4/Readme.md create mode 100644 exercises/solutions-4/build.sbt create mode 100644 exercises/solutions-4/project/plugins.sbt create mode 100644 exercises/solutions-4/src/main/scala/Problem1.scala create mode 100644 exercises/solutions-4/src/main/scala/Problem2.scala create mode 100644 exercises/solutions-4/src/test/scala/Problem1Test.scala diff --git a/exercises/solutions-4/.gitignore b/exercises/solutions-4/.gitignore new file mode 100644 index 0000000..cc92d25 --- /dev/null +++ b/exercises/solutions-4/.gitignore @@ -0,0 +1,8 @@ +.vscode +.metals +.bloop +.bsp +target +metals.sbt +build.properties +project/project diff --git a/exercises/solutions-4/.scalafmt.conf b/exercises/solutions-4/.scalafmt.conf new file mode 100644 index 0000000..1cea524 --- /dev/null +++ b/exercises/solutions-4/.scalafmt.conf @@ -0,0 +1,4 @@ +version = "3.4.0" +runner.dialect = scala3 +rewrite.scala3.convertToNewSyntax = true +rewrite.scala3.removeOptionalBraces = true diff --git a/exercises/solutions-4/Readme.md b/exercises/solutions-4/Readme.md new file mode 100644 index 0000000..28b7817 --- /dev/null +++ b/exercises/solutions-4/Readme.md @@ -0,0 +1,4 @@ +# Exercise Session 4, Solutions + +- Problem 1: [Problem1.scala](src/main/scala/Problem1.scala) and [Problem1Test.scala](src/test/scala/Problem1Test.scala) +- Problem 2: [Problem2.scala](src/main/scala/Problem2.scala) diff --git a/exercises/solutions-4/build.sbt b/exercises/solutions-4/build.sbt new file mode 100644 index 0000000..9865ad5 --- /dev/null +++ b/exercises/solutions-4/build.sbt @@ -0,0 +1,22 @@ +val scala3Version = "3.1.2" +val akkaVersion = "2.6.19" + +lazy val root = project + .in(file(".")) + .settings( + name := "code", + version := "0.1.0-SNAPSHOT", + scalaVersion := scala3Version, + fork := true, + javaOptions ++= Seq( + "-Dakka.loglevel=Debug", + "-Dakka.actor.debug.receive=on" + ), + libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-actor" % akkaVersion, + "com.typesafe.akka" %% "akka-testkit" % akkaVersion, + "junit" % "junit" % "4.13" % Test, + "com.github.sbt" % "junit-interface" % "0.13.3" % Test + ), + Test / testOptions += Tests.Argument(TestFrameworks.JUnit) + ) diff --git a/exercises/solutions-4/project/plugins.sbt b/exercises/solutions-4/project/plugins.sbt new file mode 100644 index 0000000..83adafa --- /dev/null +++ b/exercises/solutions-4/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") diff --git a/exercises/solutions-4/src/main/scala/Problem1.scala b/exercises/solutions-4/src/main/scala/Problem1.scala new file mode 100644 index 0000000..a86a8f3 --- /dev/null +++ b/exercises/solutions-4/src/main/scala/Problem1.scala @@ -0,0 +1,28 @@ +import scala.util.{Try, Success, Failure} +import scala.concurrent.ExecutionContext +import java.util.concurrent.Future +import java.util.concurrent.atomic.AtomicReference + +// Test using `sbt "testOnly Problem1Test"` +// See tests in Problem1Test.scala + +trait MyFuture[+T]: + def onComplete(callback: Try[T] => Unit): Unit + +extension [T](self: MyFuture[T]) + def map[S](f: T => S): MyFuture[S] = + new MyFuture: + def onComplete(callback: Try[S] => Unit): Unit = + self.onComplete { + case Success(v) => callback(Success(f(v))) + case Failure(e) => callback(Failure(e)) + } + def filter(p: T => Boolean): MyFuture[T] = + new MyFuture: + def onComplete(callback: Try[T] => Unit): Unit = + self.onComplete { + case Success(v) => + if p(v) then callback(Success(v)) + else callback(Failure(new NoSuchElementException())) + case Failure(e) => callback(Failure(e)) + } diff --git a/exercises/solutions-4/src/main/scala/Problem2.scala b/exercises/solutions-4/src/main/scala/Problem2.scala new file mode 100644 index 0000000..8ab4f78 --- /dev/null +++ b/exercises/solutions-4/src/main/scala/Problem2.scala @@ -0,0 +1,72 @@ +import akka.actor.{Actor, Props, ActorSystem, ActorRef, ActorLogging} +import akka.testkit.{TestKit, ImplicitSender} +import akka.event.LoggingReceive + +// Run using `sbt "runMain problem2"` + +/** Type of messages exchanged between our Actors. + * + * Note: enumerations are the Scala 3 idiomatic syntax to define algebraic data + * types (ADTs). The code below is desugared to something equivalent to: + * + * ``` + * trait Message + * case class Request(computation: () => Unit) extends Message + * object Ready extends Message + * ``` + * + * which is the syntax used in the lecture videos. + * + * Read also: + * - Translation of Enums and ADTs: + * https://docs.scala-lang.org/scala3/reference/enums/desugarEnums.html + * - Enums slides from CS210: + * https://gitlab.epfl.ch/lamp/cs210/-/blob/master/slides/progfun1-4-4.pdf + */ +enum Message: + case Request(computation: () => Unit) + case Ready +import Message.* + +class Coordinator extends Actor: + var availableWorkers: List[ActorRef] = Nil + var pendingRequests: List[Request] = Nil + + override def receive = LoggingReceive { + case Ready => + if pendingRequests.isEmpty then + availableWorkers = availableWorkers :+ sender() + else + val request = pendingRequests.head + pendingRequests = pendingRequests.tail + sender() ! request + case request: Request => + availableWorkers match + case worker :: rest => + worker ! request + availableWorkers = rest + case Nil => + pendingRequests = pendingRequests :+ request + } + +class Worker(coordinator: ActorRef) extends Actor: + coordinator ! Ready + + override def receive: Receive = LoggingReceive { case Request(f) => + f() + coordinator ! Ready + } + +@main def problem2 = new TestKit(ActorSystem("coordinator-workers")) + with ImplicitSender: + try + val coordinator = system.actorOf(Props(Coordinator()), "coordinator") + val workers = Seq.tabulate(4)(i => + system.actorOf(Props(Worker(coordinator)), f"worker$i") + ) + + // Now, clients should be able to send requests to the coordinator… + coordinator ! Request(() => println(3 + 5)) + coordinator ! Request(() => println(67 * 3)) + // And so on... + finally shutdown(system) diff --git a/exercises/solutions-4/src/test/scala/Problem1Test.scala b/exercises/solutions-4/src/test/scala/Problem1Test.scala new file mode 100644 index 0000000..920c2c8 --- /dev/null +++ b/exercises/solutions-4/src/test/scala/Problem1Test.scala @@ -0,0 +1,52 @@ +import scala.util.{Try, Success, Failure} +import org.junit.Test +import org.junit.Assert.{assertEquals, fail} + +class Problem1Test: + object MyFuture: + final def successful[T](value: T): MyFuture[T] = + new MyFuture: + def onComplete(callback: Try[T] => Unit): Unit = + callback(Success(value)) + + final def failed[T](error: Error): MyFuture[T] = + new MyFuture: + def onComplete(callback: Try[T] => Unit): Unit = + callback(Failure(error)) + + @Test + def mapWorksWithSuccess() = + MyFuture.successful(3).map(_ + 1).onComplete { + case Success(value) => assertEquals(4, value) + case _ => fail("Expected result to be a Success") + } + + @Test + def mapWorksWithFailure() = + val error = new Error("Some error") + MyFuture.failed[Int](error).map(_ + 1).onComplete { + case Failure(actualError) => assertEquals(error, actualError) + case _ => fail("Expected result to be a Failure") + } + + @Test + def filterWorksWithSuccessNotFilteredOut() = + MyFuture.successful(3).filter(_ == 3).onComplete { + case Success(value) => assertEquals(3, value) + case _ => fail("Expected result to be a Failure") + } + + @Test + def filterWorksWithSuccessFilteredOut() = + MyFuture.successful(3).filter(_ == 4).onComplete { + case Failure(actualError: NoSuchElementException) => () + case _ => fail("Expected result to be a NoSuchElementException exception") + } + + @Test + def filterWorksWithFailure() = + val error = new Error("Some error") + MyFuture.failed[Int](error).filter(_ == 3).onComplete { + case Failure(actualError) => assertEquals(error, actualError) + case _ => fail("Expected result to be a Failure") + } -- GitLab