Something Useless - Redux Implemented in Elixir

I got a chance to teach Redux recently and what a time! For something so simple, it sure can get complicated. If you take a look at my previous articles you might notice a technique I use to get my head around concepts. Bring that bad boy on to my own turf.

Giphy

A naïve implementation of Redux using JavaScript might look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const initializerAction = {type: "@@INIT"}
const createStore = (reducer, state=undefined) => {
state = reducer(state, initializerAction)
  let subscribers = []
  return {
    getState() {
      return state
    },
    subscribe(subscriber){
      subscribers.push(subscriber)
      return () => {
        subscribers = subscribers.filter(s => s !== subscriber)
      }
    },
    dispatch(action){
      state = reducer(state, action)
      subscribers.forEach(s => s())
    }
  }
}

const countReducer = (state=0, action) => {
  switch(action.type){
    case "INCREMENT":
      return state + 1
    default:
      return state
  }
}
const store = createStore(countReducer)
const removeSubscriber = store.subscribe(() => console.log("I got called", store.getState()))
store.dispatch({type: "INCREMENT"})
// Should log "I got called: 1"

removeSubscriber()
store.dispatch({type: "INCREMENT"})
// Should log nothing"

Cool. So we can build out a store that calls a single reducer. The store can notify subscribers of changes. We’ll work on implementing this in Elixir, and we’ll even handle the case with combined reducers. Along the way, we’ll see where the JavaScript implementation falters, blocking execution of independent reducers and how to get around that in Elixir.

Let’s dream

The code of my dreams looks something like:

1
2
3
4
5
6
7
8
9
10
11
12
{:ok, store} = Store.start_link(CountReducer)

subscriber_ref = Store.subscribe(store, fn (state) ->
    IO.puts "I Got called: #{state}"
end)

Store.dispatch(store, %{type: "INCREMENT"})
# Should log "I got called: 1"

Store.remove_subscriber(store, subscriber_ref)
Store.dispatch(store, %{type: "INCREMENT"})
# Should log nothing"

We swapped out a few things. This is Elixir so we’re passing in a reference to the Store pid on every invocation. We also changed how we unsubscribe from a store, returning a reference value instead of a function that magically calls in the right scope. Also, we pass the current state to the subscribers on every call. This looks great, but how do we build it?

Enter the GenServer

I love the Erlang lords for creating the GenServer. It works for EVERYTHING. Let’s start by implementing the get_state function on a Store process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defmodule Store do
  @initializer_action %{type: "@@INIT"}

  def start_link(reducer, initial_state \\ nil) do
    GenServer.start_link(__MODULE__, [reducer, initial_state])
  end

  def get_state(store) do
    GenServer.call(store, {:get_state})
  end

  def init([reducer, initial_state]) do
    store_state = reducer.reduce(initial_state, @initializer_action)
    {:ok, %{reducer: reducer, store_state: store_state}}
  end

  def handle_call({:get_state}, _from, state) do
    {:reply, Map.get(state, :store_state), state}
  end
end

The reducer will be a module with a reduce/2 function. Reducers have a few responsibilities. * They provide default values for state on initialization * They return a new version of state based on an action being passed * They ignore messages it doesn’t care for

The Reducers don’t care what the state is, all they do is get state, and an action, then respond with new state. Here’s a sample reducer:

1
2
3
4
5
6
7
8
9
10
defmodule Reducer do
  @callback reduce(any, %{type: any}) :: any
end

defmodule CountReducer do
  @behaviour Reducer
  def reduce(nil, action), do: reduce(0, action)
  def reduce(state, %{type: "INCREMENT"}), do: state + 1
  def reduce(state, _), do: state
end

We first create a behaviour to ensure anyone that plays the Reducer role knows how to do the job. We need it to have a reduce/2 function to support the Store module’s expectation. The rest is just some good old fashioned PATTERN MATCHING. The first reduce/2 deals a new reducer call. If we get a nil state, then just call it again with a 0. We’re then matching on a map key, or falling down to an unhandled case where we return the old state.

Time to run it

Throw all of that in a file named e_dux.exs a open up iex.

1
2
3
4
c "e_dux.exs"

{:ok, store} = Store.start_link(CountReducer)
Store.get_state(store) # => 0

Not too exciting. Let’s add the ability to dispatch an action. Back in our Store:

1
2
3
4
5
6
7
8
9
10
11
12
defmodule Store do
  # Existing code...

  def dispatch(store, action) do
    GenServer.cast(store, {:dispatch, action})
  end

  # Existing code...
  def handle_cast({:dispatch, action}, %{reducer: reducer, store_state: store_state} = state) do
    store_state = apply(reducer, :reduce, [store_state, action])
    {:noreply, Map.put(state, :store_state, store_state)}
  end

Here we cast asynchronously using GenServer.cast/2. This strategy makes it so our dispatch code won’t block our calling code. Imagine if a reducer took a long time to finish. Or better yet, imagine if we have to run through several reducers? This not blocking takes advantage of Elixir’s real power: Concurrency. Try this out by loading the file like before and run this:

1
2
3
4
5
6
c "e_dux.exs"

{:ok, store} = Store.start_link(CountReducer)
Store.get_state(store) # => 0
Store.dispatch(store, %{type: "INCREMENT"})
Store.get_state(store) # => 1

WOOT! Got some INCREMENT action! Let’s move on to adding subscribers and notifying them.

I’d like to subscribe to your newsletter

Back to our dream code:

1
2
3
4
5
6
7
8
9
10
11
12
{:ok, store} = Store.start_link(CountReducer)

subscriber_ref = Store.subscribe(store, fn (state) ->
    IO.puts "I Got called: #{state}"
end)

Store.dispatch(store, %{type: "INCREMENT"})
# Should log "I got called: 1"

Store.remove_subscriber(store, subscriber_ref)
Store.dispatch(store, %{type: "INCREMENT"})
# Should log nothing"

We want a dispatch to notify our existing subscribers after we’ve updated our state. We’ll also have our store track its subscribers. We’ll be updating init/1, as well as adding subscribe/2 and remove_subscriber/2

init/1

1
2
3
4
def init([reducer, initial_state]) do
  store_state = apply(reducer, :reduce, [initial_state, @initializer_action])
  {:ok, %{reducer: reducer, store_state: store_state, subscribers: %{}}} # add new subscribers map to state
end

Here we add a new key to our state map: subscribers. We’ll be taking advantage of this being a map when we delete subscribers in remove_subscriber/2

subscribe/2 remove_subscriber/2 and call backs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def subscribe(store, subscriber) do
  GenServer.call(store, {:subscribe, subscriber})
end

def remove_subscriber(store, ref) do
  GenServer.cast(store, {:remove_subscriber, ref})
end

def handle_call({:subscribe, subscriber}, _from, %{subscribers: subscribers} = state) do
  ref = make_ref()
  {:reply, ref, put_in(state, [:subscribers, ref], subscriber)}
end

def handle_cast({:remove_subscriber, ref}, %{subscribers: subscribers} = state) do
  subscribers = Map.delete(subscribers, ref)
  {:noreply, Map.put(state, :subscribers, subscribers)}
end

Our subscribe/2 function will add our subscriber to our map, storing a ref as the key. Since a ref is a unique value, it’s a great way to well… reference stuff. We’ll return that ref to our caller. remove_subscriber/2 takes a store pid and a ref. This updates our subscriber map. This is done in a cast since we don’t care about the return value.

dispatch/2

1
2
3
4
5
def handle_cast({:dispatch, action}, %{reducer: reducer, store_state: store_state, subscribers: subscribers} = state) do
  store_state = reducer.reduce(store_state, action)
  for {_ref, sub} <- subscribers, do: sub.(store_state) # notify those subscribers
  {:noreply, Map.put(state, :store_state, store_state)}
end

FINALLY we update handle_cast/2 for our dispatch/2 function to call every subscriber.

With this code, our dreams are ALIVE!

1
2
3
4
5
6
7
8
9
10
11
12
{:ok, store} = Store.start_link(CountReducer)

subscriber_ref = Store.subscribe(store, fn (state) ->
    IO.puts "I Got called: #{state}"
end)

Store.dispatch(store, %{type: "INCREMENT"})
# Should log "I got called: 1"

Store.remove_subscriber(store, subscriber_ref)
Store.dispatch(store, %{type: "INCREMENT"})
# Should log nothing"

Combined Reducers

So Redux lets you break down complex logic into multiple reducers. Something like:

1
2
3
4
5
6
7
8
9
const rootReducer = combineReducers({
  count: countReducer,
  square: squareReducer
})

const store = createStore(rootReducer)
store.getState() // {count: 0, square: 2} // default to 2 so we can get some good squaring action
store.dispatch({type: "INCREMENT"})
store.getState() // {count: 1, square: 4}

Internally all a combined reducer is is a reducer that delegates to other reducers. More naive code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const combineReducers = (reducerMap) => {
  return (state, action) => {
    return Object.keys(reducerMap).reduce((map, stateName) => {
      map[stateName] = reducerMap[stateName](state[stateName], action)
      return map
    }, {})
  }
}

const countReducer = (state=0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1
    default:
      return state
  }
}

const squareReducer = (state=2, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state * state
    default:
      return state
  }
}

const rootReducer = combineReducers({count: countReducer, square: squareReducer})
rootReducer({count: 0, square: 2}, {type: "INCREMENT"}) // {count: 1, square: 4}

Here we return a function that when called with a state and action will delegate portions of the state to specific reducers. Pretty nice! But something wicked lies in this code. When our reducer dispatches, it can only delegate to one reducer at a time, even though their state is completely isolated. This would be a perfect time for some concurrency! Let’s look at how we can handle this in Elixir.

CombineReducer Module

We’ll start out with a change to how treat individual reducers vs a Map with reducers. Above our existing init/1 function, add this:

1
2
3
4
5
6
7
8
9
10
11
12
# Multi-Reducer code
def init([reducer_map, nil]) when is_map(reducer_map), do: init([reducer_map, %{}])
def init([reducer_map, initial_state]) when is_map(reducer_map) do
  store_state = CombineReducers.reduce(reducer_map, initial_state, @initializer_action)
  {:ok, %{reducer: reducer_map, store_state: store_state, subscribers: %{}}}
end

# Solo reducer code
def init([reducer, initial_state]) do
  store_state = apply(reducer, :reduce, [initial_state, @initializer_action])
  {:ok, %{reducer: reducer, store_state: store_state, subscribers: %{}}}
end

Converting nil values to an empty map will let our reducers receive nil as their state when we call something like state[:count]. They’ll then convert that to their expected default state.

One small change to dispatch/2 to support our reducer potentially being a map instead of a module:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Multi-Reducer code
def handle_cast({:dispatch, action}, %{reducer: reducer_map, store_state: store_state, subscribers: subscribers} = state) when is_map(reducer_map) do
  store_state = CombineReducers.reduce(reducer_map, store_state, action)
  for {_ref, sub} <- subscribers, do: sub.(store_state)
  {:noreply, Map.put(state, :store_state, store_state)}
end

# Solo reducer code
def handle_cast({:dispatch, action}, %{reducer: reducer, store_state: store_state, subscribers: subscribers} = state) do
  store_state = apply(reducer, :reduce, [store_state, action])
  for {_ref, sub} <- subscribers, do: sub.(store_state)
  {:noreply, Map.put(state, :store_state, store_state)}
end

And finally, a brand new module to handle our CombineReducers logic.

1
2
3
4
5
6
7
8
9
10
11
defmodule CombineReducers do
  def reduce(reducers, state, action) do
    for {state_name, reducer} <- reducers do
      Task.async(fn () ->
        {state_name, reducer.reduce(state[state_name], action)}
      end)
    end
    |> Enum.map(&Task.await/1)
    |> Enum.into(%{})
  end
end

Ok, dopeness alert. Notice how we’re using a task to run this calculation. This means that the calculation for ALL of the reducers will take as long as the slowest one, not the culmination of all of their time. THIS IS REALLY COOL. How are you not excited?!

Let’s test it out. First create a SquareReducer:

1
2
3
4
5
6
7
8
defmodule SquareReducer do
  @behaviour Reducer
  def reduce(nil, action), do: reduce(2, action)
  def reduce(state, action), do: do_reduce(state, action)

  defp do_reduce(state, %{type: "INCREMENT"}), do: state * 1
  defp do_reduce(state, _), do: state
end

Then Run this!

1
2
3
4
{:ok, store} = Store.start_link(%{count: CountReducer, square: SquareReducer})

Store.dispatch(store, %{type: "INCREMENT"})
Store.get_state(store)

SO COOL!!!

Closing up

This bit of hacking really deepend my understanding of Redux, how combined reducers worked, and what their limitations where. I also got a chance to work on some interesting Elixir. I don’t really have a use for this pattern. It seems like it might be stepping on some of the same pain points you’d solve with GenEvent. Thanks for reading.

Want to see more of something mentioned here? Leave any ideas in the comments below.

Here’s a gist with the code. Happy Clacking.

comments powered by Disqus