Hi everyone, I am happy to announce Signals.jl, a fast, dynamic, functional reactive programming framework for Julia.
Inspiration for this package comes , obviously, from
Reactive which I use extensively… who’s pitfalls (from my point of view) led me to try out my own implementation, this turned out to be a very hard task indeed because:
a) Reactive is already abstract simple and fast, it was hard to compete with its performance
b) It is not trivial at all to offer the full range of Reactive programming while still maintaining a simple data structure
c) I wanted to allow both pushing into the signal graph as well as pulling
d) I wanted to give the user easy control over async/nom-async opeartions
e) tiny seemingly equivalent mutations of the code , caused dramatic changes in compiled code and performance
But after some long nights, hours of frustration and a few “eureka” moments, I prevailed and came from the other side with a deeper knowledge of Julia.
Signals.jl, while offering the same functionality as Reactive is different on some key factors
Dynamic: Signals are not typed, you can push an integer then float64 and then a string and it blends nicely with julia’s Multiple Dispatch
Push-Pull: you can either push a value into a Signal and propagate changes along the Signal graph , or you can set a value without any propagation and only pull the necessary changes from any other signal.
Syntax: Syntax is somewhat simplified , square brackets to set or query a value, round brackets to pull or push
a value (see documentation for more examples)
Signal graph: Signals.jl does not maintain any internal data structure other than the signals themselves
Eventloop: the event-loop in Signals.jl is dirt simple and handles world-age issues gracefully by restarting itself. As long as you don’t create Signals programatically you should encounter just a couple of event loop restarts.
Performance: Signals.jl is between 2X to 4X faster than Reactive on my machine, on various benchmark’s I made.
“Strict” and “Soft” pushes: If you push into a signal several times before the event-loop processes the Signal graph then only the last update will be considered, this is called a “soft” push and is the defualt behaviour. If you depend on the fact that every push will run independently , then you can change the behaviour of individual nodes to have a “strict” policy for push.
Non-Signal inputs: There is no restriction in Signals.jl as for the type of inputs arguments that go into a Signal.
just that input arguments which are Signals themselves get replaced by their value before performing the Signal action
Signals are pull-based, pulls are a synchronic operation, it starts and and it completes now.
Pulls are minimal: if a Signal has valid data stored in it , then no action takes place when pulling its value.
Pushes are achieved by running down the Signal graph and enqueuing pulls on terminal nodes.
Puling is a 2 step operation , this gives the signal graph an opportunity to re-validate itself and thus implement
operators such as drop-repeats or filtering.
Signal action is typed on its arguments , somehow this gave the best performance boost for code generation.
Thats it for now, read the docs , give it a spin , point out more optimisation and I will be happy to integrate it.
Moreover, for those of you who care … the code was written such that it would be simple to follow the logic behind the implementation. try mutating the code such as making it type stable instead of dynamic , mutable instead of struct etc, and see for yourself how the compiled code is changing.
for conveniance there are benchmarks as part of
Pkg.test("Signals") so it is easy to see impact of changes.