Introduction to Reactive Programming

Introduction to Reactive Programming

Reactive Programming is a kind of Event driven programming.
It’s a declarative paradigm, meaning you declare “what” not “how”.

Reactive Streams

A reactive stream emits values over a period of time:

The arrow represents time. If you go further right, you go further in time.
The circle, cube etc. represent events. A stream emits those events as time goes on.
For example, this stream emitted the numbers 2, 10 and 4.
In code, this would look something like this:

local numberStream = Reactive.new()

numberStream:emit(2)
numberStream:emit(10)
numberStream:emit(4)

The :emit method is essentially the analog to dispatching events.

Operators

The power of reactive streams come from the operators on them.

Map

If you tried functional programming, you probably have heard of “mapping”. You can map a stream too:

Mapping simply means taking some value and turning it into some other value. Map (and all other operators) return new streams, meaning you can access both the original and new stream. In the example above it turns every emitted value into a string and emits it in the new stream.
In code this is expressed as follows:

local numberStream = Reactive.new()
local doubledStream = numberStream:map(function(value)
    return tostring(value)
end)

numberStream:emit(8)
numberStream:emit(3)
numberStream:emit(6)

Filter

Another important operator is filter:

Filter takes a function that takes the emitted value and returns either true or false. If it returns true, the item is emitted in the filtered stream. If it returns false, the item is not emitted in the filtered stream. In the above example, the function checks wether the emitted number is even. If it’s even, it returns true, which means it gets emitted. If it’s odd (aka not even), the function returns false and the item is not emitted.

The code for it looks like this:

local numberStream = Reactive.new()
local evenStream = numberStream:map(function(value)
    return value % 2 == 0
end)

numberStream:emit(8)
numberStream:emit(3)
numberStream:emit(6)

Of course there are other operators, like fold, debounce, buffer etc. You may recognize some from functional programming, others are completely new.

onEvent

Up to now, we created new streams without ever reacting to them.
The onEvent method (sometimes also called onNext or forEach) is the reactive analog to :Connect-ing a callback to an event.
Or, just as the name forEach suggests, it’s iterating over a array of time. You can imagine time as the index and events as the values.
In code, it looks like this:

local numberStream = Reactive.new()
numberStream:onEvent(function(value) 
    print("numberStream emitted: ", value)
end)

numberStream:emit(8) -- prints: "numberStream emitted: 8"
numberStream:emit(3) -- prints: "numberStream emitted: 3"
numberStream:emit(6) -- prints: "numberStream emitted: 6"

Example: Detecting double clicks

First of all, lets think about how we would do it imperatively (either procedurally or with OOP).
We would detect a mouse click, then check if there was a mouse click x seconds before. That doesn’t sound that bad, but look at the code for detecting a double click:

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local mouse = player:GetMouse()

local duration = 0.5 -- If the user clicks twice in 0.5 seconds, it's a double click
local lastClick -- This is mutable state. Mutable state is bad.
-- Trigger this when there is a double click
local doubleClickEvent = Event.new()

mouse.Button1Down:Connect(function()
    if not lastClick then
        lastClick = time()
        return
    end

    if time() - lastClick >= duration then
        doubleClickEvent.trigger()
    end

    lastClick = time()
end)

…Thats some ugly code. Generally, we want to avoid using mutable state and checking wether something exists already or not. Now imagine writing code to detect triple clicks. Writing imperative code just isn’t extensible as the reactive equivalent.

The reactive version looks way cleaner:

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local mouse = player:GetMouse()

-- create a reactive stream from a RBXScriptSignal
local clickStream = Reactive.fromRBXScriptSignal(mouse.Button1Down)
local doubleClickStream = clickStream
    :buffer(0.5)
    :map(function(clickList) return #clistList end)
    :filter(function(clicks) return clicks >= 2  end)

Isn’t that way cleaner?
Here we see a new method: buffer. Buffer takes a duration in seconds, and only every x seconds it can emit a event. it “buffers” (puts all events that happen in x seconds into a list) the emitted events.
Here is a visualization:

The weird ovals around the items in the new stream represent arrays. Every 0.5 seconds, it emits all the clicks that we captured in those 0.5 seconds as a list of clicks.

After buffering, we map the clicks we gathered in those 0.5 seconds to their length. After that we filter for when the user clicked 2 times or more in 0.5 seconds.
Here is a visualization that:

As you can see, the reactive code looks pretty clean, but it’s also extensible. we can easily modify the code to detect triple clicks:

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local mouse = player:GetMouse()

-- create a reactive stream from a RBXScriptSignal
local clickStream = Reactive.fromRBXScriptSignal(mouse.Button1Down)
local multipleClickStream = clickStream
    :buffer(0.5)
    :map(function(clickList) return #clistList end)
local doubleClickStream = multipleClickStream:filter(function(clicks)
    return clicks >= 2
end)
local tripleClickStream = multipleClickStream:filter(function(clicks)
    return clicks >= 3
end)

Now imagine doing that in the imperative code. It would be way harder and require way more rewriting. The reactive approach is easy to extend and once you get to know it better, it’s way easier to read.

10 Likes

Where’s the library you used to achieve that?

1 Like

I have developed my own library to use in games. It only has a few functions that i found important. I just implement needed functionality as i go.
Here are two libraries:

Adapting RxLua to roblox

For a full feature set there is RxLua, which has a lot more functionality and has additional "onEvent"s like onError and onComplete (those are mostly useful for fetching async data for websites, as that is the main use case of reactive programming).
You would have to create custom functions for turning roblox Events etc. into reactives (which is quite simple actually, create a stream and emit each time a roblox event emits).

(I don’t recommend this) The library i made and use

I would recommend the above approach but i have my (barebones) reactive library in a github repository. It does include Signals additionally (aka variables with a :onEvent function that fires everytime the variable changes), but filtering, buffering and a lot of other functions are pretty much meaningless for signals.

If you look into my library, you will notice code smell (shouldn’t affect performance, just makes looking at it unpleasant), but again, i plan to refactor and update it as i need new functionality for my own need.

For signals, i have made a small library that i have in a github repository. I do plan to make a article on it Soon™. But basically, reactive streams are good for events and signals are good for variables that you want to depend on other variables. There aren’t many signal libraries in lua though.

2 Likes