NOTE: in case it's not clear, in my thinking out loud and sharing my failures along with questions and answers, my goal is to provide some useful anecdotal guidelines for both myself and anyone else who's looking for help. It can be especially hard to find info on FRP if you haven't (yet) gotten into Haskell since lots of the google searches and things on the space closest to Sodium tend to land on Reactive Banana and whatnot, and learning to think in terms of Sodium is still very much a process of discovery, for me at least. Hope this helps!
OK, there was one more piece - I had my data wrong. I thought I had a {Stickers: Array<Cell<Sticker>>}
or Cell<{Stickers: Array<Sticker>}>
but in the end neither of those were right!! (which I think is what was causing lots of my back-and-forth problems. Rather, what I really have, is a Cell<{Stickers: Array<Cell<Sticker>>}>
In other words - I need to allow both for the overall state object to change, and the inner contents (e.g. if it gets changed from outside - like an image finishing loading)
So I think that's the final key - and I just about had it working, but there were some weird quirks. I don't think the quirks were in the FRP architecture itself though, I think I had a bug in my merge function. So why "had" and not "have"? Well - as I was working on that, I realized it's a waste of time
If I'm already using custom logic per stream source, then there's really no benefit to going through all this headache to manually control the snapshot/loop stuff. The main benefits I wanted were:
- Ability to feed those streams into FRP elsewhere for responding (e.g. pushing to history, etc.)
- Ability to gate some heavy processing from firing.
I think #2 is just a premature optimization... I should be far more concerned with the code that runs when a stream fires rather than a simple accumulation (e.g. on tick)
And #1 is solved by just using collect
instead of accum
The output is the event data of the stream that fires, plus the updated state.
Granted I need to use an enum
and consider streams firing simultaneously - so it doesn't feel like anything more than a basic reducer map thing - but in this case it makes sense (and obviously the complexity of it and managing the timing and all his managed under the hood by Sodium). As a bonus - I can now just configure a generic dictionary to gather all the config (event type, mapping function, and mapped stream)
I haven't tested this in depth yet, just ported over the very beginnings of it, but so far so good!
This isn't really anything outside the norm now... just a basic vanilla usage of collect. That's comforting, hehe.
Here's a code snippet with lots of typescript definition and one function in:
//config pt 1 - add in new enums
enum ACCUM_TYPE {
STICKER_ADD = "stickerAdd",
//e.g. INIT_TOUCH = "initTouch"
}
//generic utility helpers
type ACCUM_MAPPER<A> = (data:A) => (state:StickersState) => StickersState;
const makeAccumConfig = <A>(type:ACCUM_TYPE) => (stream:Stream<A>) => (mapper: ACCUM_MAPPER<A>)
: {
type:ACCUM_TYPE;
stream:Stream<{data: A, type: ACCUM_TYPE}>;
mapper: ACCUM_MAPPER<A>;
} => ({
type,
stream: stream.map(data => ({
data,
type,
})),
mapper
})
//main
const startStickers = (): Stickers => {
const initState = makeInitState(); //defined elsewhere
const sStickerAdd = new StreamSink<StickerSource>(); //sent from elsewhere
//Config goes here - simply add in new accumulators ;)
const accumConfig = [
makeAccumConfig(ACCUM_TYPE.STICKER_ADD) (sStickerAdd) (appendSticker)
]
//Nothing to touch below here :)
const accumulators = new Map<ACCUM_TYPE, ACCUM_MAPPER<any>>();
accumConfig.forEach(({type, mapper}) => accumulators.set(type, mapper));
const streams:Stream<{data: any, type: ACCUM_TYPE}> = accumConfig.reduce((sources:Stream<any>, source) =>
sources === null
? source.stream
: sources.orElse(source.stream), null)
accumulators.set(ACCUM_TYPE.STICKER_ADD, appendSticker);
//sState also contains the event that caused the state change
//can be used for pushing to history etc.
const sState = streams.collect(initState, (accEvt, state) => {
const updatedState = accumulators.get(accEvt.type) (accEvt.data) (state);
return new Tuple2({
evt: accEvt,
state: updatedState
}, updatedState)
})
return sState.map(update => update.state).hold(initState), //gets listened and rendered on tick
//real-world usage would also send more data like the streams for sending
}