Blogoloquy: model Types correctly and write less code, using akka-streams

Nirmalya Sengupta
6 min readDec 23, 2020

A small note: I had published this article, a couple of years back, on Blogspot. Now that I decided to move to Medium, I thought of bringing it into Medium and give it fresh life, as it were. If you have read this already, you may want to skip it.

One of my most productive days was throwing away 1000 lines of code.

— Ken Thompson

Keep your codebase smaller, wherever possible: Generations of greats in the world of computer science and programming have been reminding us about this little yet undeniable truth. A smaller codebase lends itself to simplicity, elegance, and readability — properties that the stakeholders love to have at their disposal. Usually, such a codebase is amenable to easier maintainability, and extensibility. Who doesn’t want to own such a codebase?

If we have to write programs that are simple to understand, we have to model the problem using appropriate and descriptive terms. It is my considered view that a well-modeled (i.e., its crux is understood) problem, leads to compact and resilient code. It is much easier to reason about. The verbosity of the language and accidental complexities of the technologies involved may bring in necessary planks and poles, but to an alert pair of eyes, the theme remains easily discernible. A beautiful codebase is a smaller codebase.

The case at hand

Allow me to take you through a greatly abridged version of a recent assignment I have been associated with. Shorn of all detailed functionality and quirks, what happens is this:

  • A player begins a session, chooses a game (quiz) — from several available — to play
  • The player answers questions one at a time
  • The Server, at the end of the game (all questions attempted), calculates and declares the total she has scored

The server updates / informs other important functional units, of a player’s accomplishments. Any number of players can play games simultaneously. The server has to keep up with this load. Those layers of HTTP, Websockets, Configuration, Database, Timers, Queues, and what have you: let’s ignore all of them here.

Focus on the interaction

On a closer look, what does the server do, when a player contacts it? Assume that the player has been duly authenticated already. Then, her interaction has two distinct parts:

  • Interaction 1
(START A ROUND)
  • Interaction 2
Interaction 2 (PLAY A ROUND)
(PLAY A ROUND)

Let us try and describe the interactions in a manner that is terse, yet conveys the right meaning.

Modeling the interaction is the key

For every piece of data that reaches it, the Server keeps on transforming it through one or more steps (viz., help from other components or services may be summoned; inquire with a DB, for example) till it emits the resultant, final piece of data that is sent to the client. If we can identify each step, and identify what it transforms and to what, we can easily model how the whole server works! An aggregation of these steps is what leads to what models the Server.

An obvious first question is how do we identify these pieces? In the world of OO and Functional Programming, where I manage to reside (and so far, have not been evicted), it is quite natural to identify these pieces by their Types! Every step takes in a Type and gives rise to the same or different Type. Given this tenet, how can we represent the way the Server responds to the player?

The Type-borne behaviour

The diagram below elucidates the scheme of things. The rightmost box shows the transformations that are happening inside the Server.

In terms of Types, one way to describe the flow ‘Start A Round’ (above) is:

Let’s elaborate: stay with me

The logic of confirming the correctness of sessionID passed by the player (viz. is it existing and valid, or not), is encased in the transformer named sessionExistenceChecker. Because the server stipulates that every message reaching its shores, must have a valid sessionID, every message has to pass through sessionExistenceChecker. The important observation is this: sessionExistenceChecker understands SessionCarrier only. Therefore, in order to be recognized by the checker, every message must also be a SessionCarrier. In OO terms, every message entering sessionExistenceChecker must be subtype (IS-A) of SessionCarrier.

The other transformer is named guessNumberPreparator. Its job is to choose a number for the player and to associate that with a Round Identifier. The key understanding is that it can transform only what the preceding transformer — sessionExistenceChecker — produces. Thus, it has but got to be capable of consuming either of IncorrectSessionIDProvided and StartARound.

Summarising:

The key understanding, again, is that these are not values but Types! The actual (runtime) objects moving in and out of the checker may carry anything, but they must conform to these Types.

That’s it. We have the blueprint of the Server’s implementation of Interaction[1], available. Translating this into code — when implemented using Akka Streams — we get this:

val serverSays =
Source.single(StartARound(“A123”)) // A123 is a session id
.via(sessionExistenceChecker)
.via(guessNumberPreparator)

Now to the aforementioned Interaction[2]: when Player makes a guess, and the server gives her points for guessing correctly.

Recall that the Server ends the game after 3 rounds. The way error is handled is the same as that in the previous flow (StartARound). Also, the transformer SessionExistenceChecker is reused here. I am not detailing the flow of types and transformers for this flow, for space’s sake. The code segment that implements this flow is:

val serverSays =
Source
.single(GuessSubmittedByPlayer(
sessionID,roundID, guessedNumber)
)
)
.via(sessionExistenceChecker)
.via(roundCorrectnessChecker)
.via(guessedNumberVerifier)
.via(pointsAssigner)
.via(scoreBoardUpdater)
.via(currentScorePreparator)
.via(gameTerminationDecider)
.via(nextGuessGenerator)

That’s what our Server does, to implement Interaction[2]. That’s all there is to it, really!

An Akka-streams based implementation brings in many other benefits, However, the aim of this blog is not to explore and discuss, many and very useful aspects of Akka Streams. A number of blogs already exist which do the job very, very well (Colin Breck’s are here, my personal favourite), not to mention Akka Stream’s own site and numerous discussions on StackOverFlow. Therefore, I will rather bring your attention to other aspects of this approach of modeling:

  • Type is self-documenting: The constraints are obvious. If I want to know what do I need to gather before I can ask sessionExistenceChecker to flag me off OK, I have to look no further than the type it expects. Moreover, it is quite easy — in many cases straightforward — to translate this model into code.
  • Compiler helps to reduce defect: If we can model the pathway of processing of any message as a series of transformations, then the translation of the same in code becomes decidedly easier. if I am unmindful and pass a message which is not session-id-checkable, the compiler will block my progress with a grim message. A defect will be forestalled much before the code is readied for testing. That’s a substantial gain. Type-Driven Development, did you say?
  • Code is small: If the model is clear, the code is small and crisp. The code does what the model depicts; nothing more, nothing less. No code exists, that has no reason to exist. Brevity matters.

Let me know what you think of such an approach.

All accompanying code resides here, on github.

(I work as a Principal at Swanspeed Consulting)

--

--

Nirmalya Sengupta

Philomath, professional programmer, looking for tech opportunities to help make stuff that works, Indian but Citizen of the World, loves History and Geography