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: `doRender(..)` #21

Open getify opened 2 years ago

getify commented 2 years ago

This is a fancy, reactive string builder. It's used to define "components" as do-routines that yield string(s) which are all concated together to produce a chunk of HTML, which can then be shoved into the DOM. Since it's based on IOx, it can be set as dependent on other IOs/IOxs as inputs and reactively re-rendered on each update.

Example for how to use the utility:

// this could be an IOx which can have its value updated
// over time if desired 
var id = IO.of(42);

doRender(whatever,[ id ])

// this step is just console.log()ing, but it could instead
// be pushing the HTML into a DOM element
.map(v => console.log("html",v))

.run({});

// here's a "component" -- you `yield` out string(s), or any
// monad that produces a string, and it concats them all.
//
// the nice thing is, other than the string concating, this
// is a full do-routine that can be async, do any monad/IO
// operations, etc, whatever you need to define your
// component's markup
function *whatever(context,id) {
    yield `<div id="${id}">`;
    for (let i = 0; i < 3; i++) {
        yield `<span>${i}</span>`;
    }
    yield `</div>`;
}

And here's the candidate first implementation of doRender(..):

function doRender(gen,deps = [],...args) {
    return IOx.do(runner,deps);

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

    async function *runner(context,...depsVals) {
        var htmlIO = IO.of("");

        var it = gen(context,...depsVals,...args);
        var res;
        while (true) {
            res = it.next(res);
            if (isPromise(res)) {
                res = await res;
            }
            else {
                let done = res.done;

                if (isMonad(res.value)) {
                    if (IO.is(res.value)) {
                        res = res.value.chain(concatIfString);
                    }
                    else if (Nothing.is(res.value)) {
                        return IO.of("");
                    }
                    else if (Either.is(res.value) || Maybe.is(res.value)) {
                        res = res.value.fold(IO.of,concatIfString);
                    }
                    else {
                        res = res.value.fold(concatIfString);
                    }
                }
                else {
                    res = concatIfString(res.value);
                }

                res = yield res;
                if (done) {
                    return htmlIO.run(context);
                }
            }
        }

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

        function concatIfString(v) {
            if (typeof v == "string") {
                htmlIO = htmlIO.concat(IO.of(v));
                return IO.of();
            }
            else {
                return IO.of(v);
            }
        }
    }
}
getify commented 2 years ago

OK, so at @DrBoolean's sage observation, this can be generalized to support any monoid (not just strings) without too much complexity:

var id = IO.of(42);

doBuilder(whatever,v => (typeof v == "string"),[ id ])
.map(v => console.log("html",v))
.run({});

function *whatever(context,id) {
    yield `<div id="${id}">`;
    for (let i = 0; i < 3; i++) {
        yield `<span>${i}</span>`;
    }
    yield `</div>`;
}

And the implementation of doBuilder(..):

function doBuilder(
    doRoutine,
    isMonoid = v => (v && typeof v.concat == "function"),
    deps = [],
    ...args
) {
    return IOx.do(runner,deps);

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

    async function *runner(context,...depsVals) {
        var concatIO = IO.of("");

        var it = doRoutine(context,...depsVals,...args);
        var res;
        while (true) {
            res = it.next(res);
            if (isPromise(res)) {
                res = await res;
            }
            else {
                let done = res.done;

                if (isMonad(res.value)) {
                    if (IO.is(res.value)) {
                        res = res.value.chain(concatIfMonoid);
                    }
                    else if (Nothing.is(res.value)) {
                        return IO.of("");
                    }
                    else if (Either.is(res.value) || Maybe.is(res.value)) {
                        res = res.value.fold(IO.of,concatIfMonoid);
                    }
                    else {
                        res = res.value.fold(concatIfMonoid);
                    }
                }
                else {
                    res = concatIfMonoid(res.value);
                }

                res = yield res;
                if (done) {
                    return concatIO.run(context);
                }
            }
        }

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

        function concatIfMonoid(v) {
            if (isMonoid(v)) {
                concatIO = concatIO.concat(IO.of(v));
                return IO.of();
            }
            else {
                return IO.of(v);
            }
        }
    }
}
getify commented 2 years ago

Next revision/abstraction:

var id = IO.of(42);

strBuilderX(whatever,[ id ])
.map(v => console.log("html",v))
.run({});

function *whatever(context,id) {
    yield `<div id="${id}">`;
    for (let i = 0; i < 3; i++) {
        yield `<span>${i}</span>`;
    }
    yield `</div>`;
}

And the updated implementation:

function strBuilder(doRoutine,...args) {
    var runner = buildRunner(doRoutine,v => (v && typeof v == "string"),...args);
    return IO.do(runner);
}

function strBuilderX(doRoutine,deps = [],...args) {
    var runner = buildRunner(doRoutine,v => (v && typeof v == "string"),...args);
    return IOx.do(runner,deps);
}

function buildRunner(doRoutine,isMonoid = v => (v && typeof v.concat == "function"),...args) {
    return async function *runner(context,...depsVals){
        var concatIO = IO.of("");

        var it = doRoutine(context,...depsVals,...args);
        var res;
        while (true) {
            res = it.next(res);
            if (isPromise(res)) {
                res = await res;
            }
            else {
                let done = res.done;

                if (isMonad(res.value)) {
                    if (IO.is(res.value)) {
                        res = res.value.chain(concatIfMonoid);
                    }
                    else if (Nothing.is(res.value)) {
                        return IO.of("");
                    }
                    else if (Either.is(res.value) || Maybe.is(res.value)) {
                        res = res.value.fold(IO.of,concatIfMonoid);
                    }
                    else {
                        res = res.value.fold(concatIfMonoid);
                    }
                }
                else {
                    res = concatIfMonoid(res.value);
                }

                res = yield res;
                if (done) {
                    return concatIO.run(context);
                }
            }
        }

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

        function concatIfMonoid(v) {
            if (isMonoid(v)) {
                concatIO = concatIO.concat(IO.of(v));
                return IO.of();
            }
            else {
                return IO.of(v);
            }
        }
    };
}
getify commented 2 years ago
else if (Nothing.is(res.value)) {
   return IO.of("");
}

This line needs to be generalized from using "" as the empty monoid value to having such a value parameterized to buildRunner(..).