As a way of demonstrating how to write code with Akka, Scala, and functional programming (FP), I started creating a new project this weekend. I named it Aleka, because it may eventually be like Amazon’s Echo/Alexa, written with Akka (and Scala).
(I suppose a better name might be “Ekko,” after Echo, but I have a niece named Aleka, so unless she objects, this works for me.)
Background
If you know me, you know that I got stranded in Canada for a while in the winter of 2010, and began writing an application I named Sarah, which was based on “SARAH” in the tv series Eureka. Sarah is basically a crude/preliminary Mac version of Alexa, and uses Mac speech recognition and text-to-speech capabilities to work like Alexa.
The goal of this series of tutorials is to rebuild a lot of the Sarah functionality using Scala, Akka, and functional programming (FP).
See the third video on the Sarah page (“Sarah - Version 2”) to see how one of the latest versions of Sarah works on a Mac.
Assumptions
In this post I assume that you are at least moderately comfortable with Scala and SBT, and have at least have a “Hello, World” knowledge of Akka — preferably at least a little more than that. As a result, I don’t go into great detail when discussing the code that follows.
1) Create an SBT project
The first step in the process is to create a Simple Build Tool (SBT) project. A simple way to do this is with my sbtmkdirs
shell script, but use whatever tool you are comfortable with. Afterwards you should have a standard SBT directory structure, and a build.sbt file like this:
name := "Aleka"
version := "0.1"
scalaVersion := "2.11.7"
resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.4.9"
scalacOptions += "-deprecation"
The last line is optional, but all of the others are required.
2) Create a Scala app
The next step is to create a Scala app to kick off the application. I created mine as a Scala class named Aleka1.scala, in a directory named com/alvinalexander/aleka (under the src/main/scala directory):
package com.alvinalexander.aleka
import akka.actor.Actor
import akka.actor.ActorSystem
import akka.actor.Props
import akka.event.Logging
object Aleka1 extends App {
// an actor needs an ActorSystem
val system = ActorSystem("AlekaSystem")
// create the actors in our play
val mouth = system.actorOf(Props[Mouth], name = "mouth")
val brain = system.actorOf(Props[Brain], name = "brain")
// send the brain two messages
brain ! Hello
brain ! "hello"
Thread.sleep(500)
// shut down the system
system.terminate
}
As shown, the code does the following things:
- It creates a Scala app with
object Aleka1 extends App
. - It creates an
ActorSystem
. - It creates two
ActorRef
instances:mouth
, andbrain
. You’ll see how these are used in a few moments. - Two messages are sent to the
brain
. One is a case object namedBrain
. (There’s no way for you to know yet that this is a case object; stand by.) The second message is the string literal"hello"
. - After a short nap, the actor system is shut down with
system.terminate
.
3) Messages
It’s generally a bad idea to use strings as messages between actors, so I created a few messages as case objects, and put them in a file named Messages.scala, in the same directory as Aleka1.scala:
package com.alvinalexander.aleka
/**
* this file is a container for all of the messages that can
* be sent in the application.
*/
case object Hello
case object SayHello
As you saw in Aleka1.scala, the Hello
message is sent to the brain
with this line of code:
brain ! Hello
4) The brain
Next, create a file named Brain.scala in the same directory. It should have these contents:
package com.alvinalexander.aleka
import akka.actor.Actor
import akka.event.Logging
import akka.actor.Props
import akka.actor.ActorRef
import akka.actor.ActorLogging
class Brain extends Actor with ActorLogging {
// actors we communicate with
val mouth = context.actorSelection("../mouth")
// handle messages sent to us
def receive = {
case Hello =>
log.info("Brain got a Hello message")
mouth ! SayHello
case unknown =>
log.info("Brain got an unknown message: " + unknown)
}
}
This code creates an Akka Actor
named Brain
. Over time this Brain
will learn how to respond to many different messages, but right now the only thing it can respond to is the Hello
message (i.e., the Hello
case object). Any message that is sent to Brain
that is not a Hello
will be handled with the unknown
case.
A few other notes:
- Extending
ActorLogging
is what gives this class alog
reference. This is really convenient for logging/debugging. Brain
communicates with another actor, which it looks up with the name"../mouth"
. You’ll see theMouth
class in just a few moments.- Akka actors respond to messages in a
receive
block, as shown here. - When
Brain
receives aHello
message, it logs that, and then forwards aSayHello
message to themouth
actor reference.
5) The mouth
The last piece of the puzzle for this application is an actor named Mouth
. Rather than speaking to you, Mouth
will print its output to the command line.
package com.alvinalexander.aleka
import akka.actor.Actor
import akka.event.Logging
class Mouth extends Actor {
val log = Logging(context.system, this)
def receive = {
case SayHello =>
log.info("Mouth got a SayHello message")
println("Hello back at you.")
case unknown =>
log.info("Mouth got an unknown message: " + unknown)
println("Mouth: I don't know what that was.")
}
}
Notes:
Mouth
intentionally uses a different way to create alog
variable. (I wanted to show both approaches.)- When
Mouth
receives aSayHello
message, it logs a message saying that it received the message, then prints a message to STDOUT usingprintln
. Mouth
also has anunknown
case, in the event that it’s sent other messages that it doesn’t currently know how to handle.
6) The project directory structure
As a quick review, your project directory structure should now look like this, as shown with the tree
command:
$ tree .
.
├── build.sbt
├── lib
├── project
├── src
│ ├── main
│ │ ├── java
│ │ ├── resources
│ │ └── scala
│ │ └── com
│ │ └── alvinalexander
│ │ └── aleka
│ │ ├── Aleka1.scala
│ │ ├── Brain.scala
│ │ ├── Messages.scala
│ │ └── Mouth.scala
│ └── test
│ ├── java
│ ├── resources
│ └── scala
└── target
7) Running the application
As a quick review of what should happen, when you type sbt run
, Aleka1
(which extends the App
trait) will kick off the application. The important thing it does is send these two messages to its brain
actor reference:
brain ! Hello
brain ! "hello"
The rest of the application responds to those two messages. When you run the application, the important output you’ll see should look like this:
$ sbt run
[info] Loading global plugins from /Users/al/.sbt/0.13/plugins
[info] Done updating.
[info] Compiling 4 Scala sources ...
[info] Running com.alvinalexander.aleka.Aleka1
[INFO] [09/04/2016 18:49:15.592] [AlekaSystem-akka.actor.default-dispatcher-3] [akka://AlekaSystem/user/brain] Brain got a Hello message
Hello back at you.
[INFO] [09/04/2016 18:49:15.594] [AlekaSystem-akka.actor.default-dispatcher-6] [akka://AlekaSystem/user/mouth] Mouth got a SayHello message
[INFO] [09/04/2016 18:49:15.594] [AlekaSystem-akka.actor.default-dispatcher-3] [akka://AlekaSystem/user/brain] Brain got an unknown message: hello
[success] Total time: 4 s
The source code
You can get the source code for this project on Github at this URL:
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
What’s next
In this first step, the code is hardwired to send two messages to the Brain
. In the next version of this project, we’ll add the following functionality:
- Aleka will accept your input from the command line.
- I’ll show how to turn off Akka logging, so you only need to see those extra messages when you’re debugging your code.