Implement your own Redux in Java. Part 5: Async Middleware
javareduxfunctional programmingIn this series of blog posts I'm implementing my own Redux in Java to use with JavaFX.
However, the purpose is not to create a production-ready library (because there is already ReduxFX available) but instead to learn by re-implementing it.
In the previous part I've upgraded the middleware API to match the original one from Redux.js. This was a prerequisite to implement asynchronous middlewares. And this will be the topic for this part.
Topics in this blog series (maybe I will adjust this in the process of writing):
- Part 1: Basic Redux functionality
- Part 2: Simple FXML Views
- Part 3: First approach on Middlewares
- Part 4: Improved Middleware API
- Part 5: Async behavior with Middlewares
- Part 6: Other examples for Middlewares
- Store-enhancers
- Dev-Tools with time-travel-debugging
- advanced FXML views
- declarative JavaFX Views
- ReduxFX - an actual library
Predictability and async operations
One of the main advantages of redux is that state changes become predictable and that you can define them with functions and immutable data instead of in-place-editing of objects. However, to archive this predictability you have to add some restrictions to the system. One is that actions are processed one after the other and not in parallel. If you dispatch an action it's guaranteed that this action is processed by the redux store before the next action is processed. In other words: The "dispatch" method of the store is a blocking operation. It blocks until the reducer was applied and the new state is available.
If you think about this restriction for a moment it makes totally sense: Imagine you would process two actions in parallel. You would spawn two threads, each one with the task to invoke the reducer with the previous state and the action to calculate the new state. Now you would have two "new" states. Which of them would you use? What if the reducer result of one action depends on the other action? The outcome wouldn't be predictable anymore. It's a good thing that redux has this restriction and we don't want to change this behavior.
But then, where do we put side effects like network requests or operations to persist data in the database or filesystem? The only place for this would be the UI component. You could spawn a new thread to request data from a network resource and then invoke the dispatch method of the store afterwards. However, this put's logic into the UI that doesn't belong in the UI.
The handling of side-effects is one of the big questions in the functional programming community and there are multiple approaches available in functional programming. If you are interested in this topic I recommend you the paper "Tackling the Awkward Squad" by Simon Peyton Jones , one of the developers of the purely functional programming language Haskell. The paper describes the problem of side-effects in more detail and also shows the history of ideas that were developed for Haskell over time before they found their current solution. However, Redux takes another approach.
Middlewares to the rescue
The starting point that redux provides for this problem is the middleware API. We can develop middlewares that enable us to trigger async operations and other side-effects. In the redux.js community there were many different middlewares with different ideas and approaches developed. There are debates on which of them is "the best" but in the end you can choose whatever you like and what style of development you prefer. In this article I will re-implement one of the simplest middlewares for this problem: The "thunk middleware". But in future articles I will show other examples of middlewares for async operations and other problems.
Thunk middleware
I'm not entirely sure where the naming "thunk" comes from but I know the term from texts about non-strict semantics and lazy evaluation in Haskell. While languages like Java or JavaScript (and almost every other actively used programming language) are using so called "strict semantics" and "eager evaluation", Haskell uses "lazy evaluation". This means that the evaluation of an expression can be delayed to a later point in time when the value of the expression is actually needed (or if the value isn't needed at all, the evaluation can be skipped entirely). In the meantime the program "works" with these not-jet evaluated expressions, which are being called "thunks".
I think the naming of the "thunk middleware" comes from a similar direction. Instead of dispatching a plain object as action you dispatch a function. The thunk middleware will detect this function and invoke it. The function itself is called "thunk". It takes two arguments: The "dispatch" function and the "getState" function. This is similar to the API of middlewares itself. It enables the thunk function to dispatch new actions at a later point in time, for example when a request is finished. Notice that the thunk "function" is not a pure function in the sense of functional programming because it doesn't return any value.
Let's start with an interface for our thunks:
public interface Thunk<S> extends BiConsumer<DispatchFunction, Supplier<S>> {
@Override
void accept(DispatchFunction dispatch, Supplier<S> getState);
}
An action-creator that uses thunks could look like this:
public static Thunk<TodoState> loadTodoItems() {
return (dispatch, getState) -> {
dispatch.accept(new LoadItemsStartedAction());
new Thread(() -> {
try {
List<TodoItem> items = todoBackendService.loadItems();
dispatch.accept(new LoadItemsSuccessAction(items));
} catch(Exception e) {
dispatch.accept(new LoadItemsFailedAction(e));
}
}).start();
};
}
In this example I assume that there is a "todoBackendService" which provides a method loadItems
that loads all
existing todo-items from the Database or a webservice. This method is a blocking method, meaning that the control-flow
will stop at the method invocation until the result is available. For this reason I'm spawning a new thread to prevent
the whole app from blocking. But before this I'm dispatching a normal action LoadItemsStartedAction
as a signal that
the loading was triggered.
If the loading was successful I'm dispatching a new LoadItemsSuccessAction
that takes the actual items as payload.
Otherwise I'm dispatching a LoadItemsFailedAction
that takes the exception object as payload. This way we can now
react to these two possible outcomes in the reducer. In the success case we would add the items to our local state which
would trigger a refresh of the UI. In the failure case we could add an error message to the local state that the UI can
show to the user.
The reducer code could look like this and should be quite easy to understand:
public class TodoReducer implements Reducer<TodoState> {
@Override
public TodoState reduce(TodoState oldState, Action action) {
if (action instanceof LoadItemsStartedAction) {
return oldState.withLoadingState(true);
}
if (action instanceof LoadItemsSuccessAction) {
LoadItemsSuccessAction loadItemsSuccessAction = (LoadItemsSuccessAction);
List<TodoItem> items = loadItemsSuccessAction.getItems();
return oldState.withLoadingState(false).withItems(items);
}
if (action instanceof LoadItemsFailedAction) {
LoadItemsFailedAction loadItemsFailedAction = (LoadItemsFailedAction);
Exception reason = loadItemsFailedAction.getReason();
return oldState.withLoadingState(false)
.withItems(Collections.emptyList())
.withErrorReason(reason);
}
return oldState;
}
}
This way of structuring the logic has some advantages and disadvantages. The good thing is that it is relatively easy to understand and the concept is not that complicated. A disadvantage is that it's not easy to cancel already triggered async actions and the "thunks" can become quite complex for bigger use-cases. I think the "thunk approach" is OK for simple use-cases but there are also other interesting approaches that I will cover in one of the future posts in this series.
But first let's see how the thunk-middleware itself is implemented:
public class ThunkMiddleware<S> implements Middleware<S> {
@Override
public Function<DispatchFunction, DispatchFunction> apply(DispatchFunction dispatch,
Supplier<S> getState) {
return next -> action -> {
if(action instanceof Thunk) {
Thunk<S> thunk = (Thunk<S>) action;
thunk.accept(dispatch, getState);
} else {
next.accept(action);
}
};
}
}
Similar to the ListMiddleware
from the previous article we are checking if the incoming action is a thunk. If so, we
are invoking it by passing the dispatch
and getState
functions. Otherwise we hand over the action to the next
middleware.
However, there is still a problem with this code. The action-creator is dispatching the action on a new thread. Starting
from there also the rest of the redux code is running on this new thread. This leads to the situation that also the
subscriber in the UI controller is invoked on this thread and this is an actual problem because in JavaFX updates to the
scene-graph may only be done on the JavaFX application thread. To fix this we would need to surround the subscriber code
in a Platform.runLater
call. However, this approach has several downsides:
- it's easy to miss by developers and the problem can be hard to notice
- we introduce a dependency of the UI to the implementation of the action-creator. A change to the action-creator code results in the need for a code-change in the UI
Ideally, we take the responsibility of thread-handling from application developer and move this task to the redux library itself. One solution would be to add extra code to our middleware like this:
public class ThunkMiddleware<S> implements Middleware<S> {
@Override
public Function<DispatchFunction, DispatchFunction> apply(DispatchFunction dispatch,
Supplier<S> getState) {
return next -> action -> {
if(action instanceof Thunk) {
Thunk<S> thunk = (Thunk<S>) action;
DispatchFunction fxThreadDispatch = someAction -> { if (Platform.isFxApplicationThread()) { dispatch.accept(someAction); } else { Platform.runLater(() -> dispatch.accept(someAction)); } };
thunk.accept(fxThreadDispatch, getState); } else {
next.accept(action);
}
};
}
}
Here we are creating a new dispatch function that checks whether it is invoked on the JavaFX platform thread. In this
case the actual dispatch function is directly invoked. Otherwise it's wrapped by Platform.runLater
. This solution
works as expected but still has a possible disadvantage: It's now the responsibility of middleware authors to check for
correct thread handling. Other async middlewares have to repeat this check in their code-base and might also miss it at
first. An alternative is to move this check to the core classes of the redux library, precisely to the Store class.
However, this is debatable too, because one could argue that it's not to much to request from middleware authors to
implement threading correctly. In the original Redux.js library there is no such problem because JavaScript doesn't
support this kind of multithreading in browsers so there is not really a good knowledge base on this topic that we can
profit from. For now I'm moving the checking code to the Store class but maybe in the future I will write in some more
detail on threading with redux.
The code in the Store class can be adjusted like this:
Before:
public void dispatch(Object action) {
this.dispatch.accept(action);
}
After:
public void dispatch(Object action) {
if (Platform.isFxApplicationThread()) {
this.dispatch.accept(action);
} else {
Platform.runLater(() -> this.dispatch.accept(action));
}
}
Now we are checking the correct thread in the Store and can therefore leave the middleware free from thread-checking.
And this is all we have to do to support async operations and side-effects in redux. As I've said it's debatable if the thunk-middleware is the best way to implement side-effects in your application. A downside is that now you have important business logic in two places: In the reducer and in the thunk action-creators. The question of how to structure your code and where to put which logic is an ongoing discussion in the redux community and in my opinion there is not one definitive answer to this question. There are multiple different approaches available that compete with each other when it comes to simplicity, expressiveness and feature-richness.
In the next blog post I will show some more examples of middlewares, some of them being actually useful and some are not. But as the overall goal of this blog series is to understand the concepts of Redux and not to develop the "perfect" solution, I think it's ok to also look on not-so-useful examples. I let the reader decide which one he or she likes most.
You can find the code for redux example in this github repository. This commit is pointing directly to the state of this blog post.