This is an thought that's being going around my head in the last couple of weeks: every language is extremely flexible and can do anything. What all languages require are abstractions and reuse: libraries, functions, methods, objects ... whatever. If a language lets you build up your toolbox of abstractions, then you can do anything with very little effort, because you begin to use those abstractions.
I think Node-RED has nailed that by having subflows, flows and nodes as abstraction layers. We all start out building complex flows, then create subflows to simplify flows and eventually we create nodes to simplify subflows (if we look at it hierarchically). (another example is using function nodes where a switch or change node would do the same ...)
That NoFlo flow that you show may be complex however if it's quickly possible to abstract parts of that flow away, then NoFlo becomes useful. Remember, its difficult to create a general language that is always simple, that's why there are frameworks - abstractions for specific tasks.
Personally I have began creating flows that create flows and nodes - yet another level of abstraction. To prove the point:
[{"id":"6030f1e36ecbc405","type":"function","z":"73b4aedc.e9602","name":"generate nodes for 3d ticker wall","func":"/* How big should the display be? */\nconst nrCols = msg.nrCols;\nconst nrRows = msg.nrRows;\n\n/*\n Matrial ids: these are taken from using \"Export Nodes\" and reading the json generated.\n*/\nvar materials = [\n [\n \"17350478e877bc09\", // red\n \"e7298bcff0d93b16\", // white\n ],\n [\n \"e7298bcff0d93b16\", // white\n \"17350478e877bc09\", // red\n ],\n]\n\nvar junctionToAll = {\n \"id\": RED.util.generateId(),\n \"type\": \"junction\",\n \"x\": 0,\n \"y\": 0,\n \"wires\": [\n [\n ]\n ]\n}\n\nvar colRouter = {\n \"id\": RED.util.generateId(),\n \"type\": \"switch\",\n \"name\": \"col switch\",\n \"property\": \"col\",\n \"propertyType\": \"msg\",\n \"rules\": Array.from({ length: nrCols }, () => { return { \"t\": \"eq\", \"v\": \"1\", \"vt\": \"num\" }}),\n \"checkall\": \"false\",\n \"repair\": false,\n \"outputs\": nrCols,\n \"x\": 0,\n \"y\": -600,\n \"wires\": Array.from({ length: nrCols }, () => { return []}),\n}\n\n// set correct values\ncolRouter.rules.forEach((i, d) => i.v = (d + 1).toString())\n\nvar rowRouters = [];\nfor ( var y = 0; y < nrCols; y++) {\n var rowRouter = {\n \"id\": RED.util.generateId(),\n \"type\": \"switch\",\n \"name\": \"row switch for col \" + (y+1),\n \"property\": \"row\",\n \"propertyType\": \"msg\",\n \"rules\": Array.from({ length: nrRows }, () => { return { \"t\": \"eq\", \"v\": \"1\", \"vt\": \"num\" }}),\n \"checkall\": \"false\",\n \"repair\": false,\n \"outputs\": nrRows,\n \"x\": 200*y,\n \"y\": -500, \n \"wires\": Array.from({ length: nrRows }, () => { return []})\n }\n\n colRouter.wires[y].push(rowRouter.id)\n rowRouter.rules.forEach((i, d) => i.v = (d + 1).toString())\n rowRouters.push(rowRouter)\n}\n\nvar valueScaler = {\n \"id\": \"WILLBEPREPLACDE\",\n \"type\": \"change\",\n \"name\": \"Set Z scale\",\n \"rules\": [\n {\n \"t\": \"set\",\n \"p\": \"payload\",\n \"pt\": \"msg\",\n \"to\": \"{\\t \\\"type\\\": \\\"scale\\\",\\t \\\"relative\\\": false,\\t \\\"values\\\": {\\t \\\"x\\\":1,\\t \\\"y\\\":1,\\t \\\"z\\\":$$.payload\\t },\\t \\\"pivot\\\": {\\t \\\"x\\\": null,\\t \\\"y\\\": null,\\t \\\"z\\\": null\\t }\\t}\",\n \"tot\": \"jsonata\"\n }\n ],\n \"action\": \"\",\n \"property\": \"\",\n \"from\": \"\",\n \"to\": \"\",\n \"reg\": false,\n \"x\": 0,\n \"y\": 0,\n \"wires\": [\n [\n\n ]\n ]\n}\n\nvar baseNode = {\n \"id\": \"WILLBEREPLACED\",\n \"type\": \"box\",\n \"scene\": \"c0d1433da002f3c6\",\n \"material\": \"WILLBEREPLACED\",\n \"name_conf_a\": \"WILLBEREPLACED\",\n \"size_conf_n\": 1,\n \"height_conf_n\": \"\",\n \"width_conf_n\": \"\",\n \"depth_conf_n\": \"\",\n \"updatable_conf_b\": false,\n \"sideOrientation_conf_a\": \"1\",\n \"pos_x\": \"WILLBEREPLACED\",\n \"pos_y\": \"WILLBEREPLACD\",\n \"pos_z\": 0,\n \"scale_x\": \"1\",\n \"scale_y\": \"1\",\n \"scale_z\": \"0.25\",\n \"rot_x\": 0,\n \"rot_y\": 0,\n \"rot_z\": 0,\n \"x\": 0,\n \"y\": 0,\n \"wires\": [\n []\n ]\n}\nvar nodes = []\n\nfor (var x = 1; x < (nrCols + 1); x++) {\n for (var y = 1; y < (nrRows + 1); y++) {\n var nde = { ...baseNode };\n nde.x = 180 * x;\n nde.y = (60 * nrRows) + (50 * y);\n nde[\"name_conf_a\"] = \"pixel_\" + x + \"_x_\" + y\n nde.pos_x = x;\n nde.pos_y = y;\n nde.material = materials[x % 2][y % 2]\n nde.id = RED.util.generateId()\n\n var scaler = { ...valueScaler }\n scaler.id = RED.util.generateId()\n scaler.x = 180 * x;\n scaler.y = 50 * y;\n scaler.wires = [[nde.id]]\n\n rowRouters[x-1].wires[y-1].push(scaler.id)\n junctionToAll.wires[0].push(nde.id)\n\n nodes.push(scaler)\n nodes.push(nde)\n }\n}\n\nvar ary = [\n junctionToAll,\n colRouter\n];\n\n// @ts-ignore\nmsg.payload = nodes.concat(ary).concat(rowRouters);\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":394.1426086425781,"y":138.57132053375244,"wires":[["e522af90c2ebba47","b9e6005780e0386f"]]},{"id":"3b6cf5336219131e","type":"inject","z":"73b4aedc.e9602","name":"number of: columns and rows","props":[{"p":"nrCols","v":"11","vt":"num"},{"p":"nrRows","v":"5","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170.42828369140625,"y":86.71411323547363,"wires":[["6030f1e36ecbc405"]]},{"id":"e522af90c2ebba47","type":"debug","z":"73b4aedc.e9602","name":"debug 6","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":710.5711097717285,"y":77.99979782104492,"wires":[]},{"id":"b9e6005780e0386f","type":"json","z":"73b4aedc.e9602","name":"","property":"payload","action":"str","pretty":false,"x":625.8567047119141,"y":175.2856330871582,"wires":[["740780e56a0cf863"]]}]
This flow will create a bunch of nodes for creating a 3d ticker wall using babylonjs in Node-RED. With that flow, I say how many rows and how many columns, and the flow will give me a flows.json that I can import that will create the corresponding Node-RED nodes. It's like using eval
in JS whereby the string being evaluated was generated by the same codebase doing the eval
.
If a language can do that, then it's got the abstraction levels/layers just right