AotDS, captain log 9: Immer onto something?

November 1st, 2021
reduxaotdsupdux

AotDS, captain log 9: Immer onto something?

close-up of an Imperial Destroyer miniature

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?