Programming at this level is often considered an obscure art with little practical value, but it need not be so.
Conrad Parker (about Type-Level programming in Haskell)
There are a lot of domains where events are coming in stream-like way. The best one example are services based on http protocol.
The languages like Java with imperative programming legacy background are providing a sort of abstraction over stream nature of Http services - where stream is converted to branch-like structure and each leaf is handling in-variants for possible inputs. It's became popular to design systems in RMI way.
Regular handler for HTTP or RPC service is looking like:
Of course there is a hidden complexity behind this - like Filter, Handlers etc.
While Reactive and Functional Programming gain some popularity, streaming DSL's gain popularity in solving Flow handling complexity.
Those flows are well self explaining and easier to maintain/change in future.
The most of primitive domain problems can be easy solved via stream of events from Input to to Sink, like:
In ~> Step1 ~> Step2 ~> StepN ~> Sink.
Even if your DSL isn't sexy enough to look like math diagram, you can use some tools to visualise flow in nice ways (for example: travesty)
Of course there is always extra complexity like filtering, conditioning, throttling, backpressure etc. Stream like data handling is looking natural and mostly befit the functional programming, where every flow step is a function that accept In type and returns Out type:
type Step1 = In1 => Out1
Piping the step functions is looking natural solution. Lest assume concrete input types: ShipmentsRequest, TrackRequest, PricingRequest and Ping:
The simplest way to handle those types is operate with Any Type:
Using Any makes possible to pipe (compose) the functions. Unfortunately compiler doesn't help us with any extra checks to make piping safe.
We can use best practices and operated with the marker trait:
Now compiler helps us to find if we have forgotten to handle Ping message:
It's still just a compiler warning - and it's not always possible to escalate it to the error - if you are not allowed to change compiler arguments. It's not always the case that all incoming message could extend some base trait.
There is other way to present input types - via union type. For example Haskell support declarations like this:
In Haskell it's evolved into Data Kinds language extension. Scala is planning to support this on language level as well in Dotty compiler as Union Types.
There is a fork of Scala - Typelevel Scala that aims to bring it early than Dotty. We will use Shapeless library for type level programming solution.
In Scala we can define our input type as a chain of embedded Either types:
Shapeless library implements some syntax sugar for that solution, here is the same code expressed via the Shapeless:
Than Step 1 can be defined as a Poly1 function determined for all unions (coproducts) types from In1:
As a result type of that step is ShipmentsResponse or TrackResponse or PricingResponse or Pong. The handling for all the cases is checked during the compilation time.
The full example for akka streams can look like: