handlers/transactions (statee inspiration)
Proposal 1: reinstate tags
A function called tags that just generates a type from a name and a list of cases. This is bringing back to life an old function that I've since realised is an essential primitive.
const tags = (typeName, caseNames) => {
return {
name: typeName,
...Object.fromEntries(
caseNames.map(
caseName => [caseName, value => ({ type: typeName, case: caseName, value })]
)
)
}
}
const State = tags('LoginState', ['Idle', 'Loading', 'ResetSent', 'Error'])
State.Error('Hi')
// { type: 'LoginState', case: 'Error', value: 'Hi' }
State.Loading()
// { type: 'LoginState', case: 'Loading' }
Proposal 2: transactions
It will probably not be in core, but it's a compelling idea, and could be in a parallel library like
stags/transaction
.
A function that accepts any stag, and gives you statee like behaviour, heaps of helper methods and on and on, and it mutates! And when you exit the handle you get back a normal stag value and stag type.
This let's you work with a local mutable model for component state and a immutable model when you route away from a component.
// generates a type
const State = tags('LoginState', ['Idle', 'Loading', 'ResetSent', 'Error'])
// generates all kind of helpers and has an internal store
// uses proxies and stuff
const state = transaction(x => x.Idle(), State)
state.isIdle // true
state.isIdle() // true
state.isLoading // false
state.setLoading()
state.isLoading // true
state.Error = "Oh no!"
state.Error // => "Oh no"
state.assertError( x => x == 'Unknown User' )
// => false
state.Error // => "Unknown User"
state.assertError( x => x == 'Unknown User' )
// => true
state.Loading = true
state.assertError( x => x == 'Unknown User' )
// => false
doSomething()
.then( () => state.ResetSent = true )
.catch( state.setError )
Internally all the state is tracked and modelled as a normal stag.
And it supports any stag.
// [Type, normalStagValue]
const [Type, value] = state.commit()
// back to immutability
setState({ loginState: () => value })
This gets us into immer territory, all the assignments need not actually mutate, they could generate patch functions that you can apply to your global store after you've finished.
So its sort of like working within a database transaction, you make all these modifications, but then you can cancel or abort them if there was an error. But unlike immer or database transactions, we can generate a lot of behaviors and type checking for free because we have the existing stags specification.
I imagine common usage would define the type and the handle in one expression.
const state = transaction( tags('LoginState',
['Idle'
, 'Loading'
, 'ResetSent'
, 'Error'
]
))
And then you've basically got statee if you squint...
const statee = compose(transaction, tags)
const state = statee('LoginState',
[ 'Idle'
, 'ResetSent'
, 'Loading'
, 'Error'
]
)
state.Error = "Oh no!"
Background
I think there's 2 really compelling ergonomic aspects to statee
Reference / Handle
Statee has a handle on state, you don't really have this immutability + type thing
You define a thing, it sort of has a type, but it's initialized with default values immediately and don't need a type State
and an instance state
. It's just the same object.
The fact you have a handle or a reference is also handy because you have assignment/getter/setters/you name it
That's always been beyond the purview of stags, stags is all about creating values. Where you store those values, and how you do that, is not its job. But that doesn't mean you can't have an opinionated layer that does the equivalent.
Tags > Tagged
I used to have a thing called nTags
, it generated a stag type from a list of cases. I got rid of it when I was cleaning up in favour of tagged
.
tagged
had a lot of overlap with nTags
, and I couldn't model certain behaviours with nTags
that I could with tagged
. I had a lot of code I wanted to migrate from union-type and sum-type. So I needed something with property existence checking.
But now that I've done that migration, I've had time to notice I never really reach for tagged
, it's annoying to not have unkeyed values. #25 (closed) and #18 (closed).
I got rid of nTags
because it seemed so trivial to implement and I was in a migration mindset from existing libraries instead of thinking about my actual use-cases or what was suited to the library.