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.