Making Redux in Rust
Tags: Rust, Programming, Software Development, Technology, Data Science

Making Redux in Rust

Recreating one of GitHub’s most popular libraries in Rust

Photo by Felix Brendler on Unsplash

Rust is an amazing language, boasting speeds comparable to C, and abstraction comparable to high-level languages like C# or TypeScript. It piqued my interest a few years back — but that’s as far as I went with it (merely interested). I have a pretty solid (and fanatically functional) JavaScript background, but I have always felt like I was a front-end dev only by default. There is such high availability of classic web technology guidance at the tips of my googling fingers! I have done a bit of programmerly soul-searching of late in an attempt to find my niche and really build something. And since Rust can literally go anywhere (?) what better language to learn?

Thus, I’m investing my further learning in Rust. I can still boast some great JS skills that will likely be in demand for years to come, but I see Rust as a major player in the near future.

Anyway. My assumption is you, my reader, are in a similar boat. You’ve come from JS and love web technologies; you have heard about Rust and are hungry for more. Well, I’ve got you covered, because today I’m going to write something relatable to most JS devs — Redux in Rust!

If you’re not familiar with Redux, that’s OK too. Redux is a simple state management machine that

holds some state, updates that state based on pure functions known as reducers.

That’s the most basic version, anyway. It’s an implementation of a messaging system popularized by the people at Facebook known as a Flux. Redux is probably the simplest implementation of a Flux. We’ll do more in our example than just hold state and update it, though. We will introduce the ability to handle side effects apart from the pure functional state reducer and provide a way to back-trace the state history, too.

Side note: A pure function is one that doesn’t perform any side effects. That is to say, if you give it the same input, it will always produce the same output and will never modify outside state.

First, we’ll need some state to manipulate:

Start basic.

That’s quite nearly the simplest state we could be managing. This should suffice for our examples, though. Since we want our state changes to be immutable steps, we should probably also derive some traits here:

Add some free functionality with derive .

Copy and Clone let us pass our struct around by value on the stack. Default lets us initialize our type without a new function. PartialEq allows us to compare our struct with == and != . We’ll enjoy these traits’ features when we are implementing our state store.

The next thing we need to do is define a wrapper for our state. This wrapper is what is known as the Store. You can think of the Store as a unique reference to the state it contains. There will only ever exist one current copy of your state within the Store.

Defining a simple state reducing store.

…and that got complicated quickly. This requires some explanation. Store is a generic type with a few fields.

It wraps up our state, T , which can be any type at instantiation. It takes a trait object as its reducer field. What this field allows us to do is pass a function as the value for reducer . The Box is merely a pointer to a heap-allocated item, which our function must be since its size cannot be known at compile time. We expect this function to take the form Fn(T, Msg) -> T .

But hold on, what’s Msg ?

This will hold our message types.

For right now, that’s Msg . If you’re familiar with JavaScript’s Redux library, this is what you’d normally call an action. Msg will eventually expand to cover all the possible actions for our application — or at least all the actions that we will perform for this one piece of state. Now that we’ve got some of the pieces, let’s get some implementation going. We will be implementing functionality for the Store struct:

Constructing a store with idiomatic new.

We have added some trait bounds to our impl block. where designates that the following type parameters must implement the traits provided. The traits that must be implemented on our data structures are listed in addition to format. Remember the traits we derived on our state struct, MyState ? These trait bounds allow us to leverage the traits that we’ve supplied to our state so that the compiler will know that what we’re doing has prerequisites and is OK so long as those PRs are met. This lets us do things like call the generic parameter T ’s default() function — the compiler knows T must implement the Default trait. Let’s kind of ignore the reducer parameter of our constructor for now. We’ve already discussed that heap-allocated trait object situation a moment ago, and it will make more sense as we move forward and utilize these fields than it will to discuss exactly how it works in memory.

Let’s add some more functionality. At the core of the Flux pattern is the concept of a dispatcher. Though I could (and in a more advanced scenario, probably would) make the dispatcher its own structure, I’ll opt in favor of the Redux route and simply make dispatch a method of the Store.

The only way to update our state is via dispatch.

That’s a stupid-simple and ultimately naive solution, but let’s run with it for now — it should compile anyhow. The dispatch function takes a Msg type object. All we do is take the current state and reassign it to the result of our reducer function, passing in the current state and the dispatched Msg parameter. OK, but what about the reducer? We still haven’t tried to construct a Store yet, but when we do, we will need a reducer according to our new signature. First let’s define an action that represents a request to change the state:

An actual action.

We’ve added a variant to our Msg type. Now we have something to work with in our state reducer:

A basic reducer function.

It’s a really simple reducer to accompany our really simple state structure. A reducer typically behaves this way: It matches the message received, then returns a brand new state object based on the data in that received message. Our current messages are so simple that they don’t have any payload data, they are merely flags. Our reducer only needs to match a single case for now; in all other cases, we pass back the original state. Now we can try out our machine:

A store that does…nothing. Yet.

Great…but, uh…what can I do with it? Nothing! I can’t log my state, look at my state, or do practically anything at all except send out my increment messages into the proverbial void, hoping that they’re producing correct state objects!

This leads us to another big part of the Flux pattern: isolating side effects. A side effect in the computer science world is practically everything you want to do with an application. Logging in? Side-effecting. Saving data? Side-effecting. Web request? Side-effecting again. The only thing that isn’t side-effecting is a pure function — one that, given the same input, will produce the same output every time forever. Side effects are necessary bits of chaos that must be controlled!

Since side effects aren’t part of our pure state reducer and may change variables outside of the supplied parameters, it’s best to isolate them from pure code so that you can change or remove them easily. So, to handle these side effects, I’ve opted to take the route of supplying a hook for middleware-functions to perform side effects as a part of the dispatch mechanism. First we must add a vector of pointers to functions that take the form Fn(Msg) -> Msg .

Adding a new field to our Store.

I used a vector of functions because I want to allow for the use of multiple middleware functions on the Store. This will allow us to compose our side affecting functions later. Now we need to add some implementation to utilize our new field:

Middleware-capable.

There are two new functions to add to our impl block for Store. use_middleware registers a function to our middleware pipeline. middleware_strategy is a helper that we will call within dispatch (which I’ll describe momentarily). Since we can’t mutably borrow self again inside our first for loop, our strategy gathers the results (which are of type Msg ) in a Vec and then iterates through those, allowing us to borrow self again to dispatch each Msg . Now we just need to plug this middleware hook into our dispatch function:

Noop-terminating dispatcher.

I’ve added a helper function, is_empty , which takes a message and determines whether it is the Noop variant of Msg . I’ll explain why it’s important in a series of steps:

When our dispatch fires, we immediately start the middleware loop.

fires, we immediately start the middleware loop. Each middleware returns a new message and then dispatches that message.

dispatch fires and we start the middleware loop.

fires and we start the middleware loop. And now we’re looping infinitely.

So to circumvent the infinite looping, we must have some kind of non-propagating action that will always terminate the middleware loop by not triggering a new dispatch . That’s what our Noop variant will be most useful for here. Let’s write a middleware to log messages received by the Store.

Finally, some insight.

That should now log our Increment message twice. All right! We’re doing something “useful” with our Flux implementation. Notice how we must return the Noop message in logger_mw to stop the dispatch process.

Now that we understand how middlewares work, let’s write one that would be more useful in real life. I’m going to introduce a conditional message splitter by changing a few things. First we should think of the original dispatch message as a request, which could yield success or failure:

Splitting messages, part 1.

Now we dispatch the TryIncrement message, and we will have a middleware handle the condition. Let’s write our splitter middleware now:

Splitting messages, part 2.

We’ve introduced some arbitrary rules — if we call upon our state to Increment more than ten times, the Increment should fail. Now if we plug that in with use_middleware , it should split the messages. And if we update our logger:

Logging our new messages.

…then it should also tell us what message gets dispatched. If we call it more than ten times, it should log IncrementFailed messages instead,

Before we call this experiment done, we should implement a way to see the state’s history. Let’s add some pieces:

A few things are happening here:

We’ve added fields to represent the history as a Vec of the provided type T . We modify our non-terminal dispatch branch to include capturing the previous state and storing it in our history Vec . Finally, this is the reason we added PartialEq to our trait bounds — it allows us to compare the previous and next states using a simple != operator. Based on the result of that comparison, we can either determine the states were equal and not add to the state change history, or we determine that the states were inequal, subsequently adding the previous state to the history. We also need a function to retrieve the history. bactrace looks back as many steps as requested, returning a Vec containing up to steps state changes, provided enough changes have occurred.

I think that just about wraps it all up. We’ve learned how a Redux/Flux implementation works to split pure and impure portions of code. We learned how to make modular function middlewares to perform side effects. We implemented simple logging and state back-tracing. And we did it all in Rust!

Here’s a link to the Rust Playground.

Until the next episode of my Rust journey, FP on folks!