Pattern Matching Akka Message Channel Protocols with Scala Traits

Vakindu Philliam
5 min readJun 16, 2020

Introduction to Akka Programming:

In as few words as possible, let's look at how we design Akka apps.

Akka apps are designed in 2 simple steps.

1. Create message protocols.
2. Create Actors.

Creating message protocols:

The message protocols are the communication channels that will be used to pass messages to Akka actors. These message channels are created using Scala case classes.

Case Classes:

Scala supports the notion of case classes.

Case classes are regular classes which export their constructor parameters and which provide a recursive decomposition mechanism via pattern matching.

Here is an example for a class hierarchy which consists of an abstract super class Term and three concrete case classes Var, Fun, and App.

abstract class Term

case class Var(name: String) extends Term

case class Fun(arg: String, body: Term) extends Term

case class App(f: Term, v: Term) extends Term

To facilitate the construction of case class instances, Scala does not require that the new primitive is used.

One can simply use the class
name as a function.

Here is an example:

Fun("x", Fun("y", App(Var("x"), Var("y"))))

The constructor parameters of case classes are treated as public values and can be
accessed directly.

val x = Var("x")
Console.println(x.name)

It makes only sense to define case classes if pattern matching is used to decompose data structures.

The following object defines a pretty printer function for our
lambda calculus representation:

object TermTest extends Application {

def print(term: Term): Unit = term match {

case Var(n) =>
Console.print(n)

case Fun(x, b) =>
Console.print("^" + x + ".")
print(b)

case App(f, v) =>
Console.print("(")
print(f)
Console.print(" ")
print(v)
Console.print(")")

}

def isIdentityFun(term: Term): Boolean = term match {

case Fun(x, Var(y)) if x == y => true

case _ => false
}

val id = Fun("x", Var("x"))

val t = Fun("x", Fun("y", App(Var("x"), Var("y"))))

print(t)
Console.println
Console.println(isIdentityFun(id))

Console.println(isIdentityFun(t))
}

In our example, the function print is expressed as a pattern matching statement
starting with the match keyword and consisting of sequences of case Pattern =>
Body clauses.

The program above also defines a function isIdentityFun which checks if a given term corresponds to a simple identity function.

This example uses deep patterns and guards.

After matching a pattern with a given value, the guard (defined after the keyword if) is evaluated.

If it returns true, the match succeeds; otherwise, it fails and the next pattern will be tried.

Sealed classes:

Whenever you write a pattern match, you need to make sure you have covered all of the possible cases.

Sometimes you can do this by adding a default case at the end of the match, but that only applies if there is a sensible default behavior.
What do you do if there is no default?
How can you ever feel safe
that you covered all the cases?

The solution is to make the superclass of your case classes sealed.

A sealed class cannot have any new subclasses added except the ones in the same file.
This is very useful for pattern matching, because it means you only need to worry about the subclasses you already know about.

What’s more, you get better compiler support as well. If you match against case classes that inherit from a sealed class, the compiler will flag missing combinations of patterns with a warning message.

sealed abstract class Expr

case class Var(name: String) extends Expr

case class Number(num: Double) extends Expr

case class UnOp(operator: String, arg: Expr) extends Expr

case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

Now define a pattern match where some of the possible cases are left out:

def describe(e: Expr): String = e match {

case Number(_) => "a number"
case Var(_)=> "a variable"

}

You will get a compiler warning like the following:

warning: match is not exhaustive!

missing combinationUnOp
missing combinationBinOp

Such a warning tells you that there’s a risk your code might produce a MatchError exception because some possible patterns (UnOp,BinOp) are not handled.

The warning points to a potential source of runtime faults, so it is usually a welcome help in getting your program right.

However, at times you might encounter a situation where the compiler is too picky in emitting the warning.

For instance, you might know from the context that you will only ever apply the describe method above to expressions that are either Numbers or Vars. So you know that in fact no
MatchError will be produced.

To make the warning go away, you could add a third catch-all case to the method, like this:

def describe(e: Expr): String = e match {

case Number(_) => "a number"
case Var(_) => "a variable"
case _ => throw new RuntimeException // Should not happen

}

That works, but it is not ideal.

You will probably not be very happy that you were forced to add code that will never be executed (or so you think), just to keep the compiler silent.

A more lightweight alternative is to add an @unchecked annotation to the selector expression of the match.

This is done as follows:

def describe(e: Expr): String = (e: @unchecked) match {

case Number(_) => "a number"
case Var(_)=> "a variable"

}

In general, you can add an annotation to an expression in the same way you add a type: follow the expression with a colon and the name of the annotation (preceded by an at sign).

For example, in this case you add an @unchecked annotation to the variable e, with “e: @unchecked”.

The @unchecked annotation has a special meaning for pattern matching.

If a match’s selector expression carries this annotation,
exhaustivity checking for the patterns that follow will be suppressed.

Traits:

Similar to interfaces in Java, traits are used to define object types by specifying the
signature of the supported methods.

Unlike Java, Scala allows traits to be partially implemented; i.e. it is possible to define default implementations for some methods.

In contrast to classes, traits may not have constructor parameters.

Here is an example:

trait Similarity {

def isSimilar(x: Any): Boolean
def isNotSimilar(x: Any): Boolean = !isSimilar(x)

}

This trait consists of two methods isSimilar and isNotSimilar.

While isSimilar does not provide a concrete method implementation (it is abstract in the terminology of Java), method isNotSimilar defines a concrete implementation.

Consequently, classes that integrate this trait only have to provide a concrete implementation for isSimilar.

The behavior for isNotSimilar gets inherited directly from the
trait.

Traits are typically integrated into a class (or other traits) with a mixin class composition:

class Point(xc: Int, yc: Int) extends Similarity {

var x: Int = xc
var y: Int = yc

def isSimilar(obj: Any) =
obj.isInstanceOf[Point] &&
obj.asInstanceOf[Point].x == x

}

object TraitsTest extends Application {

val p1 = new Point(2, 3)
val p2 = new Point(2, 4)
val p3 = new Point(3, 3)

Console.println(p1.isNotSimilar(p2))
Console.println(p1.isNotSimilar(p3))
Console.println(p1.isNotSimilar(2))

}

Here is the output of the program:

false
true
true

Conclusion:

In the above step we concentrated on learning how to create Akka protocols for channeling messages to Akka Actors.
The second step is to create actors that receive and process the messages.

Find the article here:
Beginner Concepts in Akka Actor programming with Scala

Thanks!

Thanks for reading this post;
Please find me on;
Twitter: Twitter.com/VakinduPhilliam
Github: Github.com/VakinduPhilliam

--

--

Vakindu Philliam

Below average chess player. Imperfect. A Work in Progress. Backend Developer. Blockchain Developer. Data Science. Christ loved me first. 1 John 4:19