Actors using Scala & Akka — Part 1 : Introduction#
Since I am stuck at home due to Covid-19 pandemic. With nothing else to do, I thought the best use of time would be to continue my blog series about actors. In the last article, we saw what are actors, their properties and how these properties can be useful for writing concurrent and stateful applications. It was all theory. In this article, I will demonstrate how to write your first akka actor in scala using IntelliJ IDE. Hop on even if you don’t know scala.
Note
This tutorial uses Scala 2.13. This is not a full tutorial on Scala. To learn complete Scala, I recommend Programming in Scala book.
IDE & Project Setup#
Scala development can be done in many IDEs. We will use IntelliJ with scala plugin.
Let’s create a new scala project
If you are creating a scala project for the first time, it can take a few minutes for the project to load.
Let’s open the build.sbt
file and see what’s inside
build.sbt
is the file where we store most of build configurations such as modules, module dependencies and external dependencies, scala version, scala compiler flags, etc. Right now the file contains very minimal information as you can see in the screenshot. Since we will be using akka library for actors, let’s add that dependency. Dependencies can be added by adding an entry into libraryDependencies
collection.
Note
We are using akka version 2.6.5. You can find the latest version on github.
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.6.5"
After you add the dependency, you need to reload the project so that sbt (scala build tool) can download the new dependencies. Download usually takes 10–20 seconds depending on your bandwidth and which library you are downloading.
We will need to reload the project every time we add or modify dependencies. We are now ready to write our code!
Main.scala#
Let’s create a new package in scr/main/scala/
directory and then a Main
file
Notice the little green triangle near Main object, you can click on it to run the Main program.
object Main extends App {
println("hello scala")
}
output:
hello scala
Process finished with exit code 0
“Main” here, is an object and not a class. In scala, an object
is a “singleton instance” of a class created created for you automatically. We are now ready to write out first actor!
Running from CLI#
To compile and run program from CLI instead of IntelliJ, install sbt. SBT can be installed on macos, windows and linux. After installing sbt, run “sbt” command to enter sbt shell and then execute “run” command.
Running for the the first time, can take a while.
Our first actor : A bank account#
Our first actor will be a representing a bank account. We will begin very simple and keep adding custom requirements as we go on to demonstrate akka actor capabilities.
Protocol#
Our bank account has an account id and balance. Since account id can not change, only balance will be part of state and account id can be the “id” of the actor. The “behaviour” of actor will tree operations
- Deposit
- Withdraw
- PrintBalance
To support these operations, our actor will need to accept 3 “types” of messages. Let’s create them:
sealed trait BankAccountMessage
case class Deposit(amount: Int) extends BankAccountMessage
case class Withdraw(amount: Int) extends BankAccountMessage
case object PrintBalance extends BankAccountMessage
We will store these in BankAccountMessage.scala
file in the same package.
Traits in scala are like interfaces in other languages with slight differences. Case classes in scala are used to hold or carry data to functions. They are immutable. Since PrintBalance
does not need any variable, it can be made case object thus making it singleton. We have just defined protocol of our actor. Protocol includes what an actor can accept and what it can return. This actor does not return anything for now. Let’s now define the behaviour.
Behaviour#
The behaviour is description of what an actor does when a new message arrives. It’s easy to think of behaviour as a function. A function which takes a message as an input parameter and returns a new behaviour. Why a new behaviour? Well, state is also part of the behaviour, so if a message wants to make a change to state, then you need to return a new behaviour with modified state. We will understand this more in the demo below.
One of the many ways to create a behaviour is via Behaviors.receiveMessage
function. Since it create’s behaviour, let’s call it behaviour factory. It comes from akka.actor.typed.scaladsl.Behaviors
package. Let’s observe the signature of this behaviour factory.
It takes 1 type parameter which will enforce compile time type safety. The actor will only be able to receive given type of messages. The behaviour factory also accepts another parameter which is a function. Notice the function parameter notation in scala A=>B . I love this as it is very easy to read compared to Func<A,B>
in C# and creating a new interface in java.
Here’s the behaviour of our bank account actor
def behavior(balance: Int): Behavior[BankAccountMessage] = Behaviors.receiveMessage { message =>
message match {
case deposit:Deposit => behavior(balance + deposit.amount)
case withdraw:Withdraw(amount) => behavior(balance - withdraw.amount)
case PrintBalance =>
println(s"balance = $balance")
behavior(balance)
}
}
Some points for people who are new to scala:
- 'def' is used to define methods
- here we have used pattern matching over “message” and perform message type specific operation in each 'case'
This code, however is not idiomatic scala. We can make it more concise.
Scala allows reducing the code by allowing to remove message => message match
making it more concise.
def behavior(balance: Int): Behavior[BankAccountMessage] = Behaviors.receiveMessage {
case deposit:Deposit => behavior(balance + deposit.amount)
case withdraw:Withdraw(amount) => behavior(balance - withdraw.amount)
case PrintBalance =>
println(s"balance = $balance")
behavior(balance)
}
Scala also allows much more powerful pattern matching where we don’t have to create variables for deposit
and withdraw
. Instead, we can directly map variables to their fields.
def behavior(balance: Int): Behavior[BankAccountMessage] = Behaviors.receiveMessage {
case Deposit(amount) => behavior(balance + amount)
case Withdraw(amount) => behavior(balance - amount)
case PrintBalance =>
println(s"balance = $balance")
behavior(balance)
}
If we get Deposit
message, we simply return a new behaviour
with added amount
as the new balance
(new state). We are essentially avoiding any variable mutations here by using recursion. Similar for Withdraw
message. If we get a PrintBalance
message, we print current balance and return the same behaviour with same state. We can also use Behaviors.same
instead of behavior(balance)
.
case PrintBalance =>
println(s"balance = $balance")
Behaviors.same
Spawning actor#
Note that so far we have only described an actor using its protocol, behaviour and state. We still need to “spawn” the actor.
Spawning a new actor requires an actor system. Its like a runtime for all your actors within a JVM. In most cases you will not need more than one actor system in your entire application. Creating ActorSystem will require a guardian behaviour. With guardian behaviour, actor system creates a top level (system level) actor and you can send messages to the guardian actor which will then create child actors at user level. Akka recommends creating all business related actors at user level. This is because if a system actor crashes due an exception, entire actor system will crash destroying all other system and user actors.
In summary, a system actor is an actor spawned using actor system context and a user actor is an actor spawned using an actor context.
Actor system takes a guardian behaviour in the constructor and behaves like a system level actor. In other words, you can start sending messages to actor system and they will be handled by guardian actor.
//val is used to create "immutable" variables
//scala also supports `var` which allows changing value of the variable after it a value was assigned to it
val actorSystem = ActorSystem(guardianBehavior = ???, name = "MyBankActorSystem")
Right now, for guardianBehavior
parameter, we can pass Behaviours.empty
or behaviour(balance = 0)
or SpawnProtocol()
. SpawnProtocol()
is the recommended way because it creates hierarchy of actors and creates domain actors in user space. But get to it later and pass Behaviours.empty
for now.
ActorSystem(Behaviours.empty, name = "MyBankActorSystem")
Now using this actor system, we will to spawn an (system) actor using bank account behaviour.
val account1: ActorRef[BankAccountMessage] =
actorSystem.systemActorOf(behavior(balance = 0), "account1")
Congratulations! 🥳 🎉 you have successfully created your first actor. Notice the type of account1
. It is ActorRef[BankAccountMessage]
. An actor ref is a pointer to an actual actor and all communication with actor has to be done via this actor ref. This is like the address of an actor. The actor also has a name and this name must be unique. In this case we have used account id for actor name. If you print string representation of account1 actor ref, you can see this name.
println(account1)
output:
Actor[akka://MyBankActorSystem/system/account1#-822693343]
Sending messages#
Since this actor ref is “typed” we can only send messages of type BankAccountMessage
to it. The syntax to send a message is !
. Let’s send some messages.
account1 ! PrintBalance
account1 ! Deposit(200)
account1 ! Withdraw(50)
account1 ! PrintBalance
output:
balance = 0
balance = 150
End of Part 1#
Here’s the entire Main.scala
contents. All the project code can be found on github.
package tech.bilal
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, ActorSystem, Behavior}
object Main extends App {
def behavior(balance: Int): Behavior[BankAccountMessage] = Behaviors.receiveMessage {
case Deposit(amount) => behavior(balance + amount)
case Withdraw(amount) => behavior(balance - amount)
case PrintBalance =>
println(s"balance = $balance")
behavior(balance)
}
val actorSystem = ActorSystem(Behaviors.empty, name = "MyBankActorSystem")
val account1: ActorRef[BankAccountMessage] = actorSystem.systemActorOf(behavior(balance = 0), "account1")
println(account1)
account1 ! PrintBalance
account1 ! Deposit(200)
account1 ! Withdraw(50)
account1 ! PrintBalance
actorSystem.terminate()
}
In the next parts, we will do more things such as returning values from actors, spawn protocol, validations, async IO, failure handling, finite state machines, etc.