DEV Community

Cover image for A Senior Developer's guide to Stream-Oriented Programming
Dario Mannu
Dario Mannu

Posted on

A Senior Developer's guide to Stream-Oriented Programming

So you're interested or curious about Stream-Oriented Programming (SP).

SP is a novel programming paradigm created to define complex applications with advanced sync+async interactions in simple terms.

An architectural mental model

There is a core mantra in this paradigm: "Everything is a Stream".

Obviously we still have strings, numbers, booleans and functions, but it's a mental model which allows us to focus on the building blocks of an application and their relationship rather than their shape and nuances.

What is a Stream?

A Reactive Stream is a high-level data construct that can optionally listen to events/messages, optionally apply some transformations and optionally emit data.

The anatomy of a reactive stream

There are various implementations of reactive streams. In the JavaScript world the most well-known, mature, battle-tested and ubiquitous implementation is RxJS.
Other implementations could be used, including Callbags, Callforwards and you can create your own, but we'll stick with RxJS in this guide for practical purposes.

N.B.: RxJS is known to be one of the most difficult JavaScript libraries to master but we'll show you how that's no longer the case in the Stream-Oriented paradigm where everything fits so well together it will become actually simpler using RxJS than not using it.

RxJS introduces a low-level, read-only primitive known as the Observable. It's on its way to become a web standard and is already available in Chrome.

The Observable is a low-level interface and you should probably never need to create one directly in your applications.

Two other types of stream will play a fundamental role in all stream-oriented development. Subject and BehaviorSubject.

Subject is the most basic implementation of a reactive stream. Data in, data out.

BehaviorSubject is a stateful stream, to extent of being the universal state machine/state manager.
Technically it works like a Subject but it has an initial/current value. This little aspect makes it able to represent any state and transition in a UI, so you'll be using this extensively.

If you come from React, this is a bit like the functional equivalent of useState, but infinitely more powerful.

Operators (the top 3)

(if you're familiar with RxJS operators, just skip this part)

Just as you have separation of concerns as a general principle in software engineering, you have an equivalent in Stream-Oriented programming, where you separate architecture from implementation.

The "architecture" of reactive streams is defined in terms of "operators", or "steps". These define the internal structure of your streams, their processing and also their flow.

Operators also wrap the business logic and the implementation details of the flow.

The anatomy of a reactive stream

map

The most basic and important operator is map: it takes the input, applies a transformation function and re-emits the result synchronously. It behaves like Array.map, but for event streams.

Here is a simple stream that doubles its input.

const doubler = new Subject<number>().pipe(
  map(x => 2*x)
);
Enter fullscreen mode Exit fullscreen mode

filter

The second most important operator is filter. Just like Array.filter, this takes a boolean function to decide synchronously whether to re-emit a given event to the next step or not.

Here is a stream that takes numbers and only lets the odd ones through:

const odds = new Subject<number>().pipe(
  filter(x => x&1)
);
Enter fullscreen mode Exit fullscreen mode

scan

This is like a "progressive" Array.reduce. It performs the same functionality, just one-by-one, re-emitting synchronously a reduced value as data comes in.

This operator will be used extensively nearly every time you need a state machine.

Following is a basic "total" stream that takes numbers in, adds them up, and emits the sum of all items received.

const total = new BehaviorSubject<number>(0).pipe(
  scan((total, newValue) => total +newValue)
);
Enter fullscreen mode Exit fullscreen mode

Here the initial value of the BehaviorSubject will be also used as the initial value of the operator, so the whole stream will emit 0 initially.

Following is a "mergeUp" stream that takes objects in and merges their key-values into a single "state" object:

const mergeUp = new BehaviorSubject<number>({}).pipe(
  scan((a, b) => ({...a, ...b}))
);
Enter fullscreen mode Exit fullscreen mode

Other operators

If you know JavaScript Promises you already know operators: Promise.all, Promise.allSettled, Promise.then, Promise.catch are all everyday examples of operators used by Promises.

Streams can have their own. Since there are many more use cases for reactive streams than promises, there are also many more operators. RxJS sports around 200 but you'll actually count on your fingers the number of actual operators you'll use on a daily basis.

Composing Streams

Streams have a key property that makes them perfectly suited to create applications: they can be composed, which means you can wrap many of them inside a larger stream which, seen from the outside, will also be a stream that in turn can be composed again, and again.

A composite reactive stream

Everything is a Stream

Back to the mantra of the paradigm. What does it mean that everything is a stream? Is that something like a "one size fits all" for programming?
Essentially... yes.

Everything stream-oriented programming is concerned about can be represented as a stream. A button, a textbox, a dialog box, a view, a page, a route, a router, an application.

This is what makes stream-oriented programming as simple as building a house with LEGO bricks. They all fit together easily because they all have the same pluggable interface.
Now, imagine using LEGO bricks if they all had slightly different sizes or shapes and you couldn't fit them together. That's object-oriented programming, or various other mixed paradigms. Every object has its own interface, its own methods with their names, their return values, or callbacks, getters and setters with various names, let alone sagas, stores, generators. You have to read the documentation of each, find the correct method to use, its parameters, its semantics. Sync methods work differently from async ones, some return promises, others return callbacks.

In Stream-Oriented programming all streams have the same interface, Stream<I,O>, so connecting them together becomes trivial.

A dialog box is a stream in stream-oriented programming

A collection of basic and advanced Dialog Boxes demonstrates how those can be treated as streams and integrated in various UI scenarios.

Solving the "glue code" problem

In a world where many objects each speak a different "language", creating an application means writing a log of "glue code" which is code that converts data emitted by one object and makes it usable in another, according to its semantics.
A large amount of glue code necessarily means a larger bug surface and more code to maintain.

Stream-Oriented programming solves this elegantly by making streams all speak the same language and letting frameworks take care of connecting them behind the scenes.

Streams and Templates

Streams on their own don't do anything. They are lazy, so they won't typically start processing any data until they get connected to the real world.

This connection is what templates are for.

A template is a declaration of where we want to connect our streams. RML is an HTML-based template language designed to connect reactive streams in HTML.
It allows you to put streams nearly anywhere and make them work with the DOM.

The simplest example of a reactive stream in action is the classic "click-counter": a button that counts how many times you clicked it.

import { BehaviorSubject, map, scan } from 'rxjs';
import { rml } from 'rimmel';

const ClickCounter = () => {
  const count = new BehaviorSubject(0).pipe(
    scan(x => x+1)
  );

  const double = count.pipe(
    scan(x => x*2)
  );

  const even = count.pipe(
    map(x => ~x&1)
  );

  return rml`
    <button onclick="${count}"> click me </button>

    count: <span>${count}</span>
    double <span>${double}</span>
    is even: <input type="checkbox" checked="${even}">
  `;
};

document.body.innerHTML = ClickCounter();
Enter fullscreen mode Exit fullscreen mode

To see this in action you can play with this running example.

The above was probably enough for a start, the rest is just larger components with more functionality, more streams and novel design patterns.

You can learn more about Rimmel.js, the first UI JavaScript library for Stream-Oriented Programming on the Github page or various posts here on dev.to

Follow for more, reach out, say hi, show us what you can do with streams before others do.


Learn More

Top comments (0)