Feasibility of implementing functional operators without javascript

Hey all,

I want to be able to be able to do FP like operations using VPL primitives vs having to drop into JS.

Example.

Suppose I wanted to implement a visual version of reduce (or "fold"). Suppose I wanted to multiply the accumulator by each value in an array on each reduce iteration.

In a raw function node, I can do this quite easily:

const { init, values } = msg.payload
return values.reduce((acc, it) => acc * it) , init)

Then if I sent a message of { init: 1, values: [2,2,2,3] }, the node would emit 24.

Neat. But what if I'm map/filter/reduce/flattening over a series of functions? It'd be nice to get that visualization surfaced into red, vs being buried in javascript.

Here's an implementation of a reduce function wired into a multiplication function (*):

The idea here is that reduce could be implemented in such a way where iteration values could be piped thru user processing flows--such as *--then on completion, the final value could be emitted out of a finalized output (vs the iteration output). The above works, but is a little hacky :slight_smile:. The node-red flow JSON is attached below in the post. The major bummer of this reduce design is that is not generic. It is tightly coupled to the * function.

A generic impl would look something like:

The above is known imperfect. The key intent is that the * function would no longer be coupled to the reduce implementation. Callers could use it like:

In this case, I continue to use reduce with * for consistency. However, ideally reduce users would be able to import it pipe iteration values through any number of composable nodes. Less code, more visual. Yada yada.

What do you think?

reduce/fold
[
    {
        "id": "e809845bab449b69",
        "type": "tab",
        "label": "reduce",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "a519499dfb7e0895",
        "type": "function",
        "z": "e809845bab449b69",
        "name": "reduce",
        "func": "/**\n * msg.input = [collection, init]\n */\n\nconst remaining = context.get('pending-processing')\n\nif (remaining) {\n    node.log(JSON.stringify({ acc: msg.payload, remaining }))\n    if (remaining.length) {\n        const [hd, ...tl] = remaining\n        context.set('pending-processing', tl)\n        node.send({ __reduce__: true, payload: [msg.payload, hd] })\n    } else {\n        context.set('pending-processing', null)\n        node.send({ __final__: true, payload: msg.payload })\n    }\n} else {\n    node.log(JSON.stringify(msg))\n    const [collection, init] = msg.payload\n    if (!collection.length) {\n        node.send({ __final__: true, payload: init })\n    }\n    const [hd, ...tl] = collection\n    context.set('pending-processing', tl)\n    node.send({ __reduce__: true, payload: [init, hd] })\n}\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "context.set('pending-processing', null)",
        "finalize": "context.set('pending-processing', null)",
        "libs": [],
        "x": 310,
        "y": 180,
        "wires": [
            [
                "fa3c3aa82c4f52cc",
                "6b08965615c9c3da"
            ]
        ]
    },
    {
        "id": "fa3c3aa82c4f52cc",
        "type": "function",
        "z": "e809845bab449b69",
        "name": "emit-needs-reduction",
        "func": "if (msg && msg.__reduce__) {\n    node.send(msg)\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 520,
        "y": 120,
        "wires": [
            [
                "fc6c5ed7f664f5c3"
            ]
        ]
    },
    {
        "id": "6b08965615c9c3da",
        "type": "function",
        "z": "e809845bab449b69",
        "name": "emit-final-reduction",
        "func": "if (msg && msg.__final__) {\n    node.send({ payload: msg.payload })\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 510,
        "y": 240,
        "wires": [
            [
                "fa0ecd27d0532086"
            ]
        ]
    },
    {
        "id": "fa0ecd27d0532086",
        "type": "debug",
        "z": "e809845bab449b69",
        "name": "out",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 710,
        "y": 240,
        "wires": []
    },
    {
        "id": "fc6c5ed7f664f5c3",
        "type": "function",
        "z": "e809845bab449b69",
        "name": "acc ^ x",
        "func": "// node.log(JSON.stringify(msg))\nconst [acc, curr] = msg.payload\nreturn { payload: acc * curr };",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 720,
        "y": 120,
        "wires": [
            [
                "a519499dfb7e0895"
            ]
        ]
    },
    {
        "id": "0c4bd75423f8ce53",
        "type": "inject",
        "z": "e809845bab449b69",
        "name": "init: fold-raise",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "[[2,2,2,2,2,2,2,2,3], 1]",
        "payloadType": "json",
        "x": 90,
        "y": 180,
        "wires": [
            [
                "a519499dfb7e0895"
            ]
        ]
    }
]

My first impression is that Node-RED is the wrong tool for this job :slight_smile:

I would say Snap! is the VPL vehicle for this sort of computer science, as it already has most constructs of this sort built-in or easily accessible by importing a library

It might be interesting (to a few people) to try to do it but it'd really be just doing it for the sake of doing it :slight_smile:

subflows :man_facepalming:

Super works :slight_smile:

That's where I'm at. I'm not actually shipping anything on this, I just want to do some feasibility assessments.

Thx for the recc

Can you share this flow please? I'm just curious and might have some suggestions for you.

Sure thing:

[
    {
        "id": "e03926201dbef10b",
        "type": "subflow",
        "name": "reduce",
        "info": "",
        "category": "",
        "in": [
            {
                "x": 200,
                "y": 200,
                "wires": [
                    {
                        "id": "c703fa696129c8da"
                    }
                ]
            }
        ],
        "out": [
            {
                "x": 720,
                "y": 140,
                "wires": [
                    {
                        "id": "b56d2695f1458c94",
                        "port": 0
                    }
                ]
            },
            {
                "x": 720,
                "y": 260,
                "wires": [
                    {
                        "id": "f9050bab453bb9ae",
                        "port": 0
                    }
                ]
            }
        ],
        "env": [],
        "meta": {},
        "color": "#DDAA99"
    },
    {
        "id": "c703fa696129c8da",
        "type": "function",
        "z": "e03926201dbef10b",
        "name": "reduce",
        "func": "/**\n * msg.input = [collection, init]\n */\n\nconst remaining = context.get('pending-processing')\n\nif (remaining) {\n    node.log(JSON.stringify({ acc: msg.payload, remaining }))\n    if (remaining.length) {\n        const [hd, ...tl] = remaining\n        context.set('pending-processing', tl)\n        node.send({ __reduce__: true, payload: { acc: msg.payload.acc, curr: hd } })\n    } else {\n        context.set('pending-processing', null)\n        node.send({ __final__: true, payload: msg.payload.acc })\n    }\n} else {\n    node.log(JSON.stringify(msg))\n    const [collection, init] = msg.payload\n    if (!collection.length) {\n        node.send({ __final__: true, payload: init })\n    }\n    const [hd, ...tl] = collection\n    context.set('pending-processing', tl)\n    node.send({ __reduce__: true, payload: { acc: init, curr: hd } })\n}\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "context.set('pending-processing', null)",
        "finalize": "context.set('pending-processing', null)",
        "libs": [],
        "x": 350,
        "y": 200,
        "wires": [
            [
                "b56d2695f1458c94",
                "f9050bab453bb9ae"
            ]
        ]
    },
    {
        "id": "b56d2695f1458c94",
        "type": "function",
        "z": "e03926201dbef10b",
        "name": "emit-needs-reduction",
        "func": "if (msg && msg.__reduce__) {\n    node.send(msg)\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 560,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "f9050bab453bb9ae",
        "type": "function",
        "z": "e03926201dbef10b",
        "name": "emit-final-reduction",
        "func": "if (msg && msg.__final__) {\n    node.send({ payload: msg.payload })\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 550,
        "y": 260,
        "wires": [
            []
        ]
    },
    {
        "id": "3b9a9d02857dd4a7",
        "type": "tab",
        "label": "sum",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "45a4784b28a7e141",
        "type": "subflow:e03926201dbef10b",
        "z": "3b9a9d02857dd4a7",
        "name": "",
        "x": 370,
        "y": 260,
        "wires": [
            [
                "aa8f05cf3715a38f"
            ],
            [
                "54575abe13e508af"
            ]
        ]
    },
    {
        "id": "0a665e3f31bf6c9b",
        "type": "inject",
        "z": "3b9a9d02857dd4a7",
        "name": "[[1,2,3],1]",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "[[1,2,3],1]",
        "payloadType": "json",
        "x": 180,
        "y": 260,
        "wires": [
            [
                "45a4784b28a7e141"
            ]
        ]
    },
    {
        "id": "aa8f05cf3715a38f",
        "type": "function",
        "z": "3b9a9d02857dd4a7",
        "name": "*",
        "func": "const { acc, curr } = msg.payload\nmsg.payload.acc = acc * curr\nreturn msg",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 510,
        "y": 160,
        "wires": [
            [
                "25037cbf35327d0a"
            ]
        ]
    },
    {
        "id": "54575abe13e508af",
        "type": "debug",
        "z": "3b9a9d02857dd4a7",
        "name": "out",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 650,
        "y": 260,
        "wires": []
    },
    {
        "id": "25037cbf35327d0a",
        "type": "function",
        "z": "3b9a9d02857dd4a7",
        "name": "+ 1",
        "func": "const { acc } = msg.payload\nmsg.payload.acc = acc + 1\nreturn msg",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 650,
        "y": 160,
        "wires": [
            [
                "45a4784b28a7e141"
            ]
        ]
    }
]

Hi @cdaringe

I thought you might be using context. While that in itself is not a problem you need to consider the async nature of node/node-red & what happens if messages travel in quick succession.

I touched up your flow and subflow to use purely the msg object & avoid context. NOTE: I also moved the "looping" inside of the subflow (but that was purely for demonstration purposes - your flow design was fine also).

One other point, as there is now no internal state, you could actually use link call nodes & have a regular Flow TAB with your "subroutines" on - I think this is more manageable than subflows.

Anyhow, enough waffle, here is my modifications & a demonstration of the problem using context...

chrome_2v6BVdCULK

[{"id":"6e90e73e20b56586","type":"subflow","name":"reduce-mod","info":"","category":"","in":[{"x":120,"y":180,"wires":[{"id":"79a672384cc020eb"}]}],"out":[{"x":420,"y":180,"wires":[{"id":"79a672384cc020eb","port":1}]}],"env":[],"meta":{},"color":"#DDAA99"},{"id":"79a672384cc020eb","type":"function","z":"6e90e73e20b56586","name":"reduce","func":"\n/**\n * msg.input = [collection, init]\n */\n\nif (!msg.__remaining__) {\n    msg.original_payload = RED.util.cloneMessage(msg.payload)\n    const [collection, init] = msg.payload\n    if (!collection.length) {\n        msg.payload = init\n        delete msg.operations\n        node.send([null, msg]) //final\n    }\n    const [hd, ...tl] = collection\n    msg.__remaining__ = tl\n    msg.payload = { acc: init, curr: hd }\n    node.send([msg, null]) //reduce\n} else {\n    if ( msg.__remaining__.length) {\n        const [hd, ...tl] =  msg.__remaining__\n        msg.__remaining__ = tl\n        msg.payload = { acc: msg.payload.acc, curr: hd }\n        node.send([msg, null]) //reduce\n    } else {\n        delete msg.__remaining__\n        delete msg.operations\n        msg.payload = msg.payload.acc\n        node.send([null, msg]) //final\n    }\n}\n\n","outputs":2,"noerr":0,"initialize":"context.set('pending-processing', null)","finalize":"context.set('pending-processing', null)","libs":[],"x":270,"y":180,"wires":[["1b23e962c7016539"],[]]},{"id":"1b23e962c7016539","type":"function","z":"6e90e73e20b56586","name":"perform operations","func":"let ops = msg.operations || []\nfor (let index = 0; index < ops.length; index++) {\n    const op = ops[index];\n    op && op(msg)\n}\nreturn msg\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":290,"y":100,"wires":[["79a672384cc020eb"]]},{"id":"e03926201dbef10b","type":"subflow","name":"reduce","info":"","category":"","in":[{"x":200,"y":200,"wires":[{"id":"c703fa696129c8da"}]}],"out":[{"x":720,"y":140,"wires":[{"id":"b56d2695f1458c94","port":0}]},{"x":720,"y":260,"wires":[{"id":"f9050bab453bb9ae","port":0}]}],"env":[],"meta":{},"color":"#DDAA99"},{"id":"c703fa696129c8da","type":"function","z":"e03926201dbef10b","name":"reduce","func":"/**\n * msg.input = [collection, init]\n */\n\nconst remaining = context.get('pending-processing')\n\nif (remaining) {\n    node.log(JSON.stringify({ acc: msg.payload, remaining }))\n    if (remaining.length) {\n        const [hd, ...tl] = remaining\n        context.set('pending-processing', tl)\n        node.send({ __reduce__: true, payload: { acc: msg.payload.acc, curr: hd } })\n    } else {\n        context.set('pending-processing', null)\n        node.send({ __final__: true, payload: msg.payload.acc })\n    }\n} else {\n    node.log(JSON.stringify(msg))\n    const [collection, init] = msg.payload\n    if (!collection.length) {\n        node.send({ __final__: true, payload: init })\n    }\n    const [hd, ...tl] = collection\n    context.set('pending-processing', tl)\n    node.send({ __reduce__: true, payload: { acc: init, curr: hd } })\n}\n","outputs":1,"noerr":0,"initialize":"context.set('pending-processing', null)","finalize":"context.set('pending-processing', null)","libs":[],"x":350,"y":200,"wires":[["b56d2695f1458c94","f9050bab453bb9ae"]]},{"id":"b56d2695f1458c94","type":"function","z":"e03926201dbef10b","name":"emit-needs-reduction","func":"if (msg && msg.__reduce__) {\n    node.send(msg)\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":140,"wires":[[]]},{"id":"f9050bab453bb9ae","type":"function","z":"e03926201dbef10b","name":"emit-final-reduction","func":"if (msg && msg.__final__) {\n    node.send({ payload: msg.payload })\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":260,"wires":[[]]},{"id":"45a4784b28a7e141","type":"subflow:e03926201dbef10b","z":"3b9a9d02857dd4a7","name":"","x":250,"y":320,"wires":[["aa8f05cf3715a38f"],["54575abe13e508af"]]},{"id":"aa8f05cf3715a38f","type":"function","z":"3b9a9d02857dd4a7","name":"*","func":"const { acc, curr } = msg.payload\nmsg.payload.acc = acc * curr\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":290,"y":240,"wires":[["25037cbf35327d0a"]]},{"id":"54575abe13e508af","type":"debug","z":"3b9a9d02857dd4a7","name":"out","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":630,"y":320,"wires":[]},{"id":"25037cbf35327d0a","type":"function","z":"3b9a9d02857dd4a7","name":"+ 1","func":"const { acc } = msg.payload\nmsg.payload.acc = acc + 1\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":240,"wires":[["45a4784b28a7e141"]]},{"id":"6e9fbdfba22561d9","type":"debug","z":"3b9a9d02857dd4a7","name":"out-mod","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":640,"y":400,"wires":[]},{"id":"5c1f55fe66011405","type":"function","z":"3b9a9d02857dd4a7","name":"Operations","func":"msg.operations = [\n    function op1(msg){\n        const { acc, curr } = msg.payload\n        msg.payload.acc = acc * curr\n        return msg\n    },\n    function op2(msg){\n        const { acc } = msg.payload\n        msg.payload.acc = acc + 1\n        return msg\n    },\n]\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":400,"wires":[["0897765c010121b0"]]},{"id":"0897765c010121b0","type":"subflow:6e90e73e20b56586","z":"3b9a9d02857dd4a7","name":"","x":450,"y":400,"wires":[["6e9fbdfba22561d9"]]},{"id":"e7aff0bd83b37c28","type":"link in","z":"3b9a9d02857dd4a7","name":"link in 1","links":["1b90fc3a4414c55d","37669cdbbb8ca3e4"],"x":135,"y":320,"wires":[["45a4784b28a7e141"]]},{"id":"8b739a88bd214e37","type":"link in","z":"3b9a9d02857dd4a7","name":"link in 2","links":["1b90fc3a4414c55d","37669cdbbb8ca3e4"],"x":135,"y":400,"wires":[["5c1f55fe66011405"]]},{"id":"3e826180a230c270","type":"inject","z":"3b9a9d02857dd4a7","name":"[[1,2,3],1]","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[[1,2,3],1]","payloadType":"json","x":200,"y":80,"wires":[["37669cdbbb8ca3e4"]]},{"id":"37669cdbbb8ca3e4","type":"link out","z":"3b9a9d02857dd4a7","name":"link out 3","mode":"link","links":["e7aff0bd83b37c28","8b739a88bd214e37"],"x":615,"y":80,"wires":[]},{"id":"1b8623323d558cfb","type":"inject","z":"3b9a9d02857dd4a7","name":"[[5,5],5]","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[[5,5],5]","payloadType":"json","x":190,"y":120,"wires":[["37669cdbbb8ca3e4"]]},{"id":"0c46c4820db096f6","type":"inject","z":"3b9a9d02857dd4a7","name":"fast data","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":155,"y":160,"wires":[["fe5041e3ceeee977"]],"l":false},{"id":"fe5041e3ceeee977","type":"function","z":"3b9a9d02857dd4a7","name":"3 in a row [[1,2,3],1]  [[5,5],5]  [[1,2,3],100]","func":"node.send({payload: [[1,2,3],1]});\nnode.send({payload: [[5,5],5]});\nnode.send({payload: [[3,4,5],100]});\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":160,"wires":[["37669cdbbb8ca3e4"]]}]

Hope some of that is useful to you.

And here it is using LINK CALL...

chrome_esXMvRoTGN

[{"id":"02adb77ebb69e62d","type":"link call","z":"3b9a9d02857dd4a7","name":"","links":["299eee6d0c3972bd"],"linkType":"static","timeout":"30","x":530,"y":520,"wires":[["eeaf838d16bc8fb8"]]},{"id":"065cb43a50fd1a18","type":"function","z":"3b9a9d02857dd4a7","name":"Operations","func":"msg.operations = [\n    function op1(msg){\n        const { acc, curr } = msg.payload\n        msg.payload.acc = acc * curr\n        return msg\n    },\n    function op2(msg){\n        const { acc } = msg.payload\n        msg.payload.acc = acc + 1\n        return msg\n    },\n]\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":520,"wires":[["02adb77ebb69e62d"]]},{"id":"eeaf838d16bc8fb8","type":"debug","z":"3b9a9d02857dd4a7","name":"out-link-call","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":730,"y":520,"wires":[]},{"id":"3f21c0b5cdadf02f","type":"inject","z":"3b9a9d02857dd4a7","name":"[[1,2,3],1]","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[[1,2,3],1]","payloadType":"json","x":140,"y":500,"wires":[["065cb43a50fd1a18"]]},{"id":"68357e278106dd6a","type":"inject","z":"3b9a9d02857dd4a7","name":"[[5,5],5]","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[[5,5],5]","payloadType":"json","x":130,"y":540,"wires":[["065cb43a50fd1a18"]]},{"id":"91bb7b7ad4deab38","type":"group","z":"3b9a9d02857dd4a7","name":"Reducer","style":{"fill":"#ffffbf","label":true,"color":"#000000"},"nodes":["0523dd7cda780875","c47d901584d2aad5","299eee6d0c3972bd","55ba7aa9a6717650"],"x":284,"y":599,"w":352,"h":142},{"id":"0523dd7cda780875","type":"function","z":"3b9a9d02857dd4a7","g":"91bb7b7ad4deab38","name":"reduce","func":"\n/**\n * msg.input = [collection, init]\n */\n\nif (!msg.__remaining__) {\n    msg.original_payload = RED.util.cloneMessage(msg.payload)\n    const [collection, init] = msg.payload\n    if (!collection.length) {\n        msg.payload = init\n        delete msg.operations\n        updateStatus(msg.original_payload, msg.payload)\n        node.send([null, msg]) //final\n    }\n    const [hd, ...tl] = collection\n    msg.__remaining__ = tl\n    msg.payload = { acc: init, curr: hd }\n    node.send([msg, null]) //reduce\n} else {\n    if ( msg.__remaining__.length) {\n        const [hd, ...tl] =  msg.__remaining__\n        msg.__remaining__ = tl\n        msg.payload = { acc: msg.payload.acc, curr: hd }\n        node.send([msg, null]) //reduce\n    } else {\n        delete msg.__remaining__\n        delete msg.operations\n        msg.payload = msg.payload.acc\n        updateStatus(msg.original_payload, msg.payload)\n        node.send([null, msg]) //final\n    }\n}\n\nfunction updateStatus(orig, result) {\n    orig = JSON.stringify(orig, null, 0);\n    node.status({fill:\"green\",shape:\"dot\",text:`${orig} => ${result}`});\n}","outputs":2,"noerr":0,"initialize":"context.set('pending-processing', null)","finalize":"context.set('pending-processing', null)","libs":[],"x":470,"y":700,"wires":[["c47d901584d2aad5"],["55ba7aa9a6717650"]]},{"id":"c47d901584d2aad5","type":"function","z":"3b9a9d02857dd4a7","g":"91bb7b7ad4deab38","name":"perform operations","func":"let ops = msg.operations || []\nfor (let index = 0; index < ops.length; index++) {\n    const op = ops[index];\n    op && op(msg)\n}\nreturn msg\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":640,"wires":[["0523dd7cda780875"]]},{"id":"299eee6d0c3972bd","type":"link in","z":"3b9a9d02857dd4a7","g":"91bb7b7ad4deab38","name":"reduce-subroutine","links":[],"x":325,"y":700,"wires":[["0523dd7cda780875"]]},{"id":"55ba7aa9a6717650","type":"link out","z":"3b9a9d02857dd4a7","g":"91bb7b7ad4deab38","name":"link out 4","mode":"return","links":[],"x":595,"y":700,"wires":[]}]
2 Likes

And, of course, if this was generally useful to people, you could easily convert that into a simple node.

Hey Steve, thx for sharing those flows. I appreciate you taking the time to spiff them up!

I have some additional thoughts and ideas I want to noodle through, but am having a difficult time expressing them at the moment. Perhaps I'll write 'em up, and get some feedback from ya later. Something something message purity, something something binding .operations reference as substitute for params input, something about iteration msgs flowing thru operations nodes vs programmatic operations execution, something something complete. It's no doubt great stuff! But really, node-red has node input constraints that inhibit me from expressing my design in the visual way that I want to, I think. Not a problem, ...just wrestlin thru it :slight_smile:

Much appreciated!