I happened across this little javascript library called Redux. The link I followed touted it as a silver bullet "backend" library along with React. Now, I despise all these modern javascript fad libraries. So, like a hauty teenager intent on getting outraged by reading facebook posts, I decided to have a closer look...
The Redux website liberally mentions "facebook" <hate>
as an inspiration, and CPQS <fad>
. The React <more fad, more hate>
library is it's biggest consumer. Ooo, I hated it all already. I was getting really angry and worked up, just as intended!
But then I start to notice words like, "immutability", "pure functions", and "single source of truth" and my interest begins to perk up. I read "time-traveling state machine" and I'm intrguied. I then see the teeny tiny 9 method public API and I'm hooked!
I immediately skim read the entire 7 page introdution. I get it
, I understand it
, but most importantly I WANT IT!
So here's Redux in a nutshell as I see it:
Redux is a methodology for representing your domain model with immutable data. This state can then only be updated by applying small incremental actions. These actions themselves are also immutable and idempotent (pure functions) - which makes your entire system completely deterministic to point where you can even roll your domain back in time!
Yes, Redux is just Event Sourcing at heart, but it's also more than that. It's not just an term or an idea, it's also a framework and methodology for implementing it.
Anyway, I was later soaking in a hot bath and found myself pondering over the design implications of implementing a Redux application and realised I'd already built it! "Eureka!"
The StackHub web application, with its strict adherence to update actions on the domain objects backed by a caching mechanism... is Redux! I just hadn't generalised the code or abstracted out a library. It may be hard coded in the source, but the design ideas were all there!
I was excited again. So much so I felt the need to rewrite Redux in Fantom. I figured it should be easy as Fantom is type safe and has immutability baked in to its core - it lends itself to Redux far more than javascript ever will.
I quickly bashed out a fully functioning and complete bare bones implementation of Redux... in about 25 lines of Fantom code - which runs a faithful representation of the basic Redux Todo example. And here it is:
// ---- Redux ----class Store { private Obj? rootState const Reducer rootReducer StoreListener[] listeners := StoreListener[,] new make(Obj? rootState, Reducer rootReducer) { this.rootState = rootState this.rootReducer = rootReducer } Obj? getState() { rootState } Void dispatch(Action action) { rootState = rootReducer.reduce(rootState, action).toImmutable listeners.each { it.actionDispatched(this, action) } } |->| subscribe(StoreListener listener) { listeners.add(listener) return |->| { listeners.remove(listener) } } } const mixin StoreListener { abstract Void actionDispatched(Store store, Action action) } const mixin Action { } const mixin Reducer { abstract Obj? reduce(Obj? state, Action action) }
Next I'll walk you through the Todo example. It's great for understanding how a Redux application works and how it's all wired together.
The idea is to create a domain model that lets you create a Todo list, mark them complete, and update some view data. But all using immutable state.
I believe the following type safe Fantom implementation of the Todo application is a lot easier to follow and understand than the corresponding javascript one.
So here goes:
1. Define your domain model
You need to first define what data your application will hold. Note how all the classes are const
and immutable (thank you Fantom!). This is the key to a deterministic Redux application.
// ---- State ----const class AppRootState { const Str filter := "off" const Todo[] todos := Todo[,] new make(|This|? f := null) { f?.call(this) } override Str toStr() { "${filter} - ${todos}" } } const class Todo { const Str text const Bool isComplete new make(|This| f) { f(this) } override Str toStr() { (isComplete ? "COMPLETE" : "TODO") + ": ${text}" } }
2. Define your actions
Actions are write operations that will be performed on your domain. Javascript Redux uses strings to identify unique actions. Here in Fantom land we can use type safe classes instead.
The Action
mixin is just used as a marker interface.
// ---- Actions ----const class AddTodoAction : Action { const Str text new make(Str text) { this.text = text } } const class ToggleTodoAction : Action { const Int todoId new make(Int todoId) { this.todoId = todoId } } const class SetVisibilityFilterAction : Action { const Str filter new make(Str filter) { this.filter = filter } }
3. Write your reducers
This is the actual implementation of how you update the immutable domain model.
You tend to write a Reducer
for each part of the domain model that needs updating. So we have three, one for the root object, one for the view state, and one for updating the Todo
entity.
// ---- Reducers ----const class AppRootReducer : Reducer { override Obj? reduce(Obj? state, Action action) { appState := (AppRootState) state return AppRootState { it.filter = VisibilityReducer().reduce(appState.filter, action) it.todos = TodoReducer().reduce(appState.todos, action) } } } const class VisibilityReducer : Reducer { override Obj? reduce(Obj? state, Action action) { filter := (Str) state if (action is SetVisibilityFilterAction) filter = ((SetVisibilityFilterAction) action).filter return filter } } const class TodoReducer : Reducer { override Obj? reduce(Obj? state, Action action) { todos := (Todo[]) state if (action is AddTodoAction) { addTodoAction := (AddTodoAction) action newTodo := Todo { it.text = addTodoAction.text it.isComplete = false } todos = todos.rw todos.add(newTodo) } if (action is ToggleTodoAction) { toggleTodoAction := (ToggleTodoAction) action oldTodo := todos[toggleTodoAction.todoId] newTodo := Todo { it.text = oldTodo.text it.isComplete = !oldTodo.isComplete } todos = todos.rw todos[toggleTodoAction.todoId] = newTodo } return todos } }
Yes, the word Reducer is a terrible, non-descriptive word. The Redux documentation contains paragraphs of text that attempt to justify the use of the word "reduce", which just tells me even they know it's stoopid name!
4. (Optional) Write some listeners
Listeners receive callback events when an action is dispatched / invoked. The example uses them to print out the state of the domain after each action.
// ---- Listeners ----const class EchoListner : StoreListener { override Void actionDispatched(Store store, Action action) { echo("${action.typeof.name} -> ${store.getState}") } }
And here is the main example that invokes all of the above. You can see how, line-for-line, it is identical the javascript version.
const class TodoExample { static Void main(Str[] args) { store := Store(AppRootState(), AppRootReducer()) echo(store.getState) unsubscribe := store.subscribe(EchoListner()) store.dispatch(AddTodoAction("Learn about actions")) store.dispatch(AddTodoAction("Learn about reducers")) store.dispatch(AddTodoAction("Learn about store")) store.dispatch(ToggleTodoAction(0)) store.dispatch(ToggleTodoAction(1)) store.dispatch(SetVisibilityFilterAction("showCompleted")) unsubscribe() echo(store.getState) } }
Which when run, prints out:
off - [,] AddTodoAction -> off - [TODO: Learn about actions] AddTodoAction -> off - [TODO: Learn about actions, TODO: Learn about reducers] AddTodoAction -> off - [TODO: Learn about actions, TODO: Learn about reducers, TODO: Learn about store] ToggleTodoAction -> off - [COMPLETE: Learn about actions, TODO: Learn about reducers, TODO: Learn about store] ToggleTodoAction -> off - [COMPLETE: Learn about actions, COMPLETE: Learn about reducers, TODO: Learn about store] SetVisibilityFilterAction -> showCompleted - [COMPLETE: Learn about actions, COMPLETE: Learn about reducers, TODO: Learn about store] showCompleted - [COMPLETE: Learn about actions, COMPLETE: Learn about reducers, TODO: Learn about store]
Admittedly, as the above 100 line example shows, most of the work is done by your application. You have to write the meat of Redux yourself. So the real challenge is yet to come; to see how much boilerplate code can be cut out.
And I already have ideas for that...
Stay tuned!