AotDS, captain log 9: Immer onto something?
AotDS, captain log 9: Immer onto something?
Good gracious, it’s been quite a while since I’ve touched this pet project, hasn’t it? Well, I’m back. And, predictively, the first thing I did when returning was to decide to fall down the refactoring hole. And oh boy did I fall deep.
First I returned to the aotds-battle
sub-project — the one implementing the
game rules. That game engine is basically a Redux store using my own
Updux framework to cut on the
boilerplate. From there I decided to do a full rewrite of Updux
to iron out
the Typescript kinks of the original version. Well, that’s a way to phrase it
that implies that I have some functional brain rudder. What actually have
happened is closer to an initial “Screw it, I’m going to rewrite it without
those bloody Typescript generics! JavaScript all the way, baby!”, followed a
few days after by a “Come back, Typescript. All is forgiven, let’s start anew.
Between you and I, we can make it work. pleeease”
Anyway, the short of it is that I’m well on my way to have a shiny new functional rewrite of Updux. The big difference with the previous version is that I’m slacking off on the full-blown immutability pedantry, and stop trying to make Typescript types perfect for all convoluted cases. Instead, I’m going for making it easy to declare all Typescript needs when the Updux object is created, and have the declarations of mutations come after to leverage type checking and the such.
For example, here is what the current dux (how I call one Updux instance, inspired by ducks) for a ship, aka bogey looks like:
import u from 'updeep';
import { Updux } from 'updux';
import drive from './drive';
import weaponry from './weaponry';
import navigation from './navigation';
type Orders = {
navigation?: {
thrust: number;
bank: number;
turn: number;
};
};
export const dux = new Updux({
subduxes: { drive, weaponry, navigation },
initial: {
name: '',
orders: {} as Orders,
},
actions: {
setOrders: (bogeyId: string, orders: Orders) => ({ bogeyId, orders }),
},
});
dux.setMutation('setOrders',
({ orders }) => u({ orders: u.constant(orders) })
);
In that code snippet, when we create the dux, we define its local initial state, the local actions it’s using, and its subduxes (i.e., internal duxes). The cool part is that Updux will merge the local bits with the bits of the subduxes so that, for example, when the full state of the dux is the merge of the local state plus the state of all subduxes. The wicked cool part is that — thanks to way too hairy generic gymnastics — those are available to Typescript.
Wanna check out the type of the store state? Sure.
Want to check out the action generators, along their signatures? No problemo.
The most useful part is that those definitions are also available when we are adding mutations and the like to the dux:
Trouble in paradise
There’s a wrinkle and a half in there, though.
The half-wrinkle is how updeep’s succinctness can make it a tad hermetic. Part of me loves the way
dux.setMutation('increment', (incr) => state => {
return {
...state,
counter: state.counter + incr,
}
});
can be shortened to
dux.setMutation('increment', (incr) => u({ counter: fp.add(incr) }) );
but another part acknowledge that for more complex mutations, that could become detrimental cleverness.
The other wrinkle, though, is more problematic. If you look at the previous
screenshot, you’ll see that final as any
. That’s because updeep
’s
Typescript signatures tries very hard to predict the update results based on
the arguments (I would know, I wrote them), but because Typescript is only
compile-time and updeep
doesn’t expect the output type to equal the input
type, it can only go so far.
But… there is another immutable library, Immer that is specifically made to keep the input and output types in sync. The previous version of Updux was already playing nicely with it, and with the new version, it ain’t any harder…
dux.setImmerMutation('setOrders', (draft, { orders }) => {
draft.orders = orders;
});
It’s not much longer than the updeep
version, and it’s quite more readable.
And we still have all the Typescript typing goodness too:
Bottom-line, Immer might play a bigger role in the next version of Updux than I would have expected. Who wouldn’ve thunk it?