getify / monio

The most powerful IO monad implementation in JS, possibly in any language!
http://monio.run
MIT License
1.05k stars 58 forks source link

Feature: `useState(..)` #22

Open getify opened 2 years ago

getify commented 2 years ago

Taking inspiration from Hooks (i.e., React), a state preserving mechanism like useState(..) is proposed.

You could use it in do-routines like this:

IO.do(component).run({});

function *component(viewContext) {
    var [ id, updateID ] = yield useState(() => Math.floor(Math.random() * 1000));

    console.log(`random ID: ${id}`);

    id = yield updateID(42);

    console.log(`fixed ID: ${id}`);
}

There are some unresolved issues with such a mechanism:

Here's a candidate first implementation:

function useState(initialVal) {
    return IO((viewContext = {}) => {
        var { state = {}, } = viewContext;
        viewContext.state = state;
        var { nextSlotIdx = 0, slots = [], } = viewContext.state;
        state.nextSlotIdx = nextSlotIdx;
        state.slots = slots;
        var curSlotIdx = state.nextSlotIdx++;

        if (!(curSlotIdx in slots)) {
            if (typeof initialVal == "function") {
                initialVal = initialVal();
            }
            slots[curSlotIdx] = initialVal;
        }
        return [ slots[curSlotIdx], function update(nextVal){
            return IO(({ state: { slots, }, }) => {
                if (typeof nextVal == "function") {
                    nextVal = nextVal(slots[curSlotIdx]);
                }
                return (slots[curSlotIdx] = nextVal);
            });
        }];
    });
}
getify commented 2 years ago

Here's an alternate approach that I think addresses some of the above limitations:

doWithState(component).run({});

function *component({ useState, }) {
    var [ id, updateID ] = yield useState(() => Math.floor(Math.random() * 1000));

    console.log(`random ID: ${id}`);

    id = yield updateID(42);

    console.log(`fixed ID: ${id}`);
}

And the implementation of doWithState(..):

function doWithState(gen,...args) {
    var state = { nextSlotIdx: 0, slots: [], };

    return IO(env => {
        state.nextSlotIdx = 0;
        var context = { ...env, useState, };
        return IO.do(gen,...args).run(context);
    });

    // ********************************************

    function useState(initialVal) {
        return IO(env => {
            var slots = state.slots;
            var curSlotIdx = state.nextSlotIdx++;

            if (!(curSlotIdx in slots)) {
                if (typeof initialVal == "function") {
                    initialVal = initialVal();
                }
                slots[curSlotIdx] = initialVal;
            }
            return [
                slots[curSlotIdx],

                function update(nextVal){
                    return IO(() => {
                        if (typeof nextVal == "function") {
                            nextVal = nextVal(slots[curSlotIdx]);
                        }
                        return (slots[curSlotIdx] = nextVal);
                    });
                },
            ];
        });
    }
}
getify commented 2 years ago

A further refinement to the doWithState(..) approach, that seems to solve all the remaining issues as listed earlier... provides a useState(..) for accessing private state via numerically indexed slots, and useSharedState for accessing shared state via named slots, both of which are still fully self-contained in the provided IO reader-env object:

doWithState(component).run({});

function *component({ useState, useSharedState, }) {
    var [ id, updateID ] = yield useState(() => Math.floor(Math.random() * 1000));
    var [ greeting, updateGreeting ] = yield useSharedState("greeting","Hello World!");

    console.log(`random ID: ${id}`);

    id = yield updateID(42);

    console.log(`fixed ID: ${id}`);

    console.log(greeting);
}

And this implementation:

const privateStateKeys = new WeakMap();
const SHARED_STATE = Symbol("shared-state");

function doWithState(gen,...args) {
    return withState(IO.do(gen,...args),gen);
}

function doXWithState(gen,deps,...args) {
    return withState(IOx.do(gen,deps,args),gen);
}

function withState(io,gen) {
    if (!privateStateKeys.has(gen)) {
        privateStateKeys.set(gen,Symbol(`private-state:${gen.name || gen.toString()}`));
    }
    const PRIVATE_STATE = privateStateKeys.get(gen);

    return IO(env => {
        var {
            [PRIVATE_STATE]: privateState = { nextSlotIdx: 0, slots: [], },
            [SHARED_STATE]: sharedState = {},
        } = env;
        env[PRIVATE_STATE] = privateState;
        env[SHARED_STATE] = sharedState;

        privateState.nextSlotIdx = 0;
        return io.run({
            ...env,
            useState,
            useSharedState,
        });
    });

    // ********************************************

    function useState(initialVal) {
        return IO(({ [PRIVATE_STATE]: privateState, }) => {
            return accessState(
                privateState.slots,
                privateState.nextSlotIdx++,
                initialVal,
                env => env[PRIVATE_STATE].slots
            );
        });
    }
}

function useSharedState(stateName,initialVal) {
    return IO(({ [SHARED_STATE]: sharedState, }) => {
        return accessState(
            sharedState,
            stateName,
            initialVal,
            env => env[SHARED_STATE]
        );
    });
}

function accessState(stateStore,prop,initialVal,getStateStore) {
    if (!(prop in stateStore)) {
        if (typeof initialVal == "function") {
            initialVal = initialVal();
        }
        stateStore[prop] = initialVal;
    }
    return [
        stateStore[prop],

        function update(nextVal){
            return IO(env => {
                var stateStore = getStateStore(env);
                if (typeof nextVal == "function") {
                    nextVal = nextVal(stateStore[prop]);
                }
                return (stateStore[prop] = nextVal);
            });
        }
    ];
}