diff --git a/exercises/solutions-5/.gitignore b/exercises/solutions-5/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..cc92d252fed98e77240201d6120a9b74cb66665a --- /dev/null +++ b/exercises/solutions-5/.gitignore @@ -0,0 +1,8 @@ +.vscode +.metals +.bloop +.bsp +target +metals.sbt +build.properties +project/project diff --git a/exercises/solutions-5/.scalafmt.conf b/exercises/solutions-5/.scalafmt.conf new file mode 100644 index 0000000000000000000000000000000000000000..1cea5243def8047a81295a8b13cb970660e0b3fc --- /dev/null +++ b/exercises/solutions-5/.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-5/Readme.md b/exercises/solutions-5/Readme.md new file mode 100644 index 0000000000000000000000000000000000000000..a1885584f2c5daf2e923d7c4be20932038d31848 --- /dev/null +++ b/exercises/solutions-5/Readme.md @@ -0,0 +1,35 @@ +# Exercise Session 5, Solutions + +## Problem 1: Message Processing Semantics + +### Problem 1.1 + +For the `Client1` actor, the only possible output is `0`. The reason is that messages between two actors are guaranteed to be received in the order they were sent. + +You can try the code yourself by running: +``` +sbt "runMain problem1_1" +``` + +### Problem 1.2 + +1. For the `Client2` actor, either `0` or `1` can be printed. There are no restrictions on the order in which messages are processed in this case. It might be the case that the `Memory` actor receives the `Write` message from the `Client2` first, or the `Read` message from the `Proxy` first. + +2. The order in which the messages are sent by the `Client2` doesn't change the possible behaviours of the system. + +3. In the case both messages are sent through the `Proxy`, then the only possible output is `0`, since in this case the messages between the `Client2` and `Proxy`, as well as between `Proxy` and `Memory`, are guaranteed to be handled in the same order they were sent. + +You can try the code yourself by running: +``` +sbt "runMain problem1_2" +``` + +## Problem 2 + +The solution is in `Problem2.scala`. + +You can run it using: + +``` +sbt "runMain problem2 10" +``` diff --git a/exercises/solutions-5/build.sbt b/exercises/solutions-5/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..0aed4e0a5e70090991d53bc8cc333cba5385ac8e --- /dev/null +++ b/exercises/solutions-5/build.sbt @@ -0,0 +1,25 @@ +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=Info", + "-Dakka.actor.allow-java-serialization=on" + ), + libraryDependencies ++= Seq( + "org.scalameta" %% "munit" % "1.0.0-M3" % Test, + "com.typesafe.akka" %% "akka-actor" % akkaVersion, + "com.typesafe.akka" %% "akka-testkit" % akkaVersion, + "com.typesafe.akka" %% "akka-persistence" % akkaVersion, + // SLF4J backend + // See https://doc.akka.io/docs/akka/current/typed/logging.html#slf4j-backend + "ch.qos.logback" % "logback-classic" % "1.2.11" + ), + Test / testOptions += Tests.Argument(TestFrameworks.JUnit) + ) diff --git a/exercises/solutions-5/project/plugins.sbt b/exercises/solutions-5/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..83adafa3f1dc8a82b2f18ba573029a8f1ca4cc72 --- /dev/null +++ b/exercises/solutions-5/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") diff --git a/exercises/solutions-5/src/main/scala/Problem1.scala b/exercises/solutions-5/src/main/scala/Problem1.scala new file mode 100644 index 0000000000000000000000000000000000000000..2b055846cbba0a02e3e58ae92f8621eed4b110f2 --- /dev/null +++ b/exercises/solutions-5/src/main/scala/Problem1.scala @@ -0,0 +1,56 @@ +import scala.util.{Try, Success, Failure} +import scala.concurrent.ExecutionContext +import java.util.concurrent.Future +import java.util.concurrent.atomic.AtomicReference +import akka.actor.* +import akka.testkit.* +import akka.event.LoggingReceive + +enum Protocol: + case Write(value: Int) + case Read(requester: ActorRef) +import Protocol.* + +enum Responses: + case Answer(value: Int) +import Responses.* + + +class Memory extends Actor: + var value = 0 + + override def receive: Receive = { + case Write(newValue) => value = newValue + case Read(requester) => requester ! Answer(value) + } + +class Client(memory: ActorRef) extends Actor: + override def receive: Receive = { case Answer(value) => + println(value) + } + +class MyProxy(memory: ActorRef) extends Actor: + override def receive: Receive = { case message => + memory ! message + } + +@main def problem1_1 = + for _ <- 1 to 1000 do + val system = ActorSystem("example") + try + val memory = system.actorOf(Props(Memory())) + val client = system.actorOf(Props(Client(memory))) + memory ! Read(client) + memory ! Write(1) + finally system.terminate() + +@main def problem1_2 = + for _ <- 1 to 1000 do + val system = ActorSystem("example") + try + val memory = system.actorOf(Props(Memory())) + val proxy = system.actorOf(Props(MyProxy(memory))) + val client = system.actorOf(Props(Client(memory))) + proxy ! Read(client) + memory ! Write(1) + finally system.terminate() diff --git a/exercises/solutions-5/src/main/scala/Problem2.scala b/exercises/solutions-5/src/main/scala/Problem2.scala new file mode 100644 index 0000000000000000000000000000000000000000..d198ffef1c4c89b6e909319145cd2f7f0c10a852 --- /dev/null +++ b/exercises/solutions-5/src/main/scala/Problem2.scala @@ -0,0 +1,86 @@ +import akka.actor.* +import akka.testkit.TestKit + +object Soldier: + // The different messages that can be sent between the actors: + enum Protocol: + + // The recipient should die. + case Death + + // The recipient should update its next reference. + case Next(next: ActorRef) + + // The recipient should act. + case Act + +class Soldier(number: Int) extends Actor: + import Soldier.* + import Protocol.* + + def receive: Receive = behavior(None, None, false) + + def behavior( + next: Option[ActorRef], + killer: Option[ActorRef], + mustAct: Boolean + ): Receive = { + + case Death => + next match + case Some(myNext) => + sender() ! Next(myNext) + myNext ! Act + println("Soldier " + number + " dies.") + self ! PoisonPill + + case None => + context.become( + behavior(next = None, killer = Some(sender()), mustAct = mustAct) + ) + + case Next(newNext) => + if newNext == self then println("Soldier " + number + " is last !") + else if !killer.isEmpty then + killer.get ! Next(newNext) + newNext ! Act + println("Soldier " + number + " dies.") + self ! PoisonPill + else if mustAct then + newNext ! Death + context.become(behavior(next = None, killer = None, mustAct = false)) + else + context.become( + behavior(next = Some(newNext), killer = None, mustAct = false) + ) + + case Act => + next match + case Some(myNext) => + myNext ! Death + context.become( + behavior(next = None, killer = killer, mustAct = false) + ) + + case None => + context.become(behavior(next = None, killer = killer, mustAct = true)) + } + +@main def problem2(n: Int) = new TestKit(ActorSystem()): + import Soldier.* + import Soldier.Protocol.* + + // Initialization + require(n >= 1) + + // Creation of the actors. + val actors = Seq.tabulate(n)( + (i: Int) => system.actorOf(Props(classOf[Soldier], i), "Soldier" + i) + ) + + // Inform all actors of the next actor in the circle. + for i <- 0 to (n - 2) do actors(i) ! Next(actors(i + 1)) + actors(n - 1) ! Next(actors(0)) + + // Inform the first actor to start acting. + actors(0) ! Act