diff --git a/exercises/solutions-4/.gitignore b/exercises/solutions-4/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..cc92d252fed98e77240201d6120a9b74cb66665a --- /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 0000000000000000000000000000000000000000..1cea5243def8047a81295a8b13cb970660e0b3fc --- /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 0000000000000000000000000000000000000000..28b78174dc24ddad1a642eac71bd873b61501ab6 --- /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 0000000000000000000000000000000000000000..9865ad5b4683b009fb6b2a820e5368c86adfb891 --- /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 0000000000000000000000000000000000000000..83adafa3f1dc8a82b2f18ba573029a8f1ca4cc72 --- /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 0000000000000000000000000000000000000000..a86a8f358e5702142d139cb59d346f81bd9a36fd --- /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 0000000000000000000000000000000000000000..8ab4f787875459c64de56dbf2fbe5ab5392476e7 --- /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 0000000000000000000000000000000000000000..920c2c8d438618ce9e17a3ac980bf32af2ceeed5 --- /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") + }