calculations defined by pure-functions without side-effects (or ‘formulae’)
input inference: argument-passing boilerplate or ‘threading’ is inferred
modularity with parent-definition precedence
everything defined as a function - including constants and inputs
These are designed to promote flexible & reusable calculation code, and logically-structured models which are maintainable, general, and with low refactoring needs.
calculang generates pure-computation and portable Javascript modules, which are usable across application languages.
Why?
calculang is only for calculations; this forces separation of concerns: calculations (=> numbers and workings) encapsulated without application programming complexity
For calculations (=> numbers and workings) this:
facilitates frictionless transparency
facilitates specialised tooling for numbers
tools to explore, trace, interpret, visualize, …
Examples
In ‘calculang model’ below showing calculang code you can also inspect generated Javascript code using options under 🤫
Application code includes calcuvizspec visualization helper and calcudata convenience functions, and can be seen in ➤ Code blocks.
calculang code can be used in multiple applications (wider than those shown) - like a library, but also in multiple calculang models with control over calculation details due to parent-definition precedence (examples: shop-demand-curve-modular, shop-stressed-sales and savings-rec).
calculang model
Code
viewof entrypoint = Inputs.select(newMap(entrypoints_all.map((d,i) => [d[1],i])), {label:'model',value:11}) // leaving on simple-climate as it breaks if not default :o// hacky, do qs parsing?// I should view .mjs instead of .cul.js for memo case?cul_resources = {if (cul_js_cul)returnObject.values(introspection_nomemo.cul_scope_ids_to_resource).map(loc => loc.slice(2).slice(0,loc.indexOf('?') !=-1? loc.indexOf('?')-2:999)).map(d => d + ((d.slice(-3) !='.js') ?'.js':'')).map(loc => entrypoints_all[entrypoint][0]+ loc)elsereturn [...Array(Object.keys(introspection_nomemo.cul_scope_ids_to_resource).length)].map((_, i) => entrypoints_all[entrypoint][0] + entrypoints_all[entrypoint][1].slice(0,-7)+(calculang_source_nomemo.length?'-nomemo':'') +'_esm/'+'cul_scope_'+i+'.mjs')}//cul_resourcesoutput = {let o = []; cul_resources.forEach(loc => { o.push(fetch(loc).then(d => d.text())) })return o}calculang = [await output[0], cul_resources.length>1?await output[1] :'', cul_resources.length>2?await output[2] :'', cul_resources.length>3?await output[3] :'', cul_resources.length>4?await output[4] :'', cul_resources.length>5?await output[5] :'', cul_resources.length>6?await output[6] :'', cul_resources.length>7?await output[7] :''].filter(d => d !='')//calculang// "x.cul.js formulae?"fff = calculang.map(d => d.split('\n')).map(d => [d.findIndex(e => e.indexOf('inputs:'+ (options.includes('inputs') ?'BLAH':'')) !=-1) ??999,...d]).map(d => d.filter((e,i) => i <= d[0] || d[0] ==-1)).map(d => d.slice(1)).map(d => d.join('\n')).map((d,i) =>`***[${cul_resources[i].slice(entrypoints_all[entrypoint][0].length).replaceAll('-nomemo','')}](${cul_resources[i].replaceAll('-nomemo','')}) ${cul_js_cul ?`-> [js](${entrypoint_no_cul_js}_esm/cul_scope_${i}.mjs)`:``}***~~~js${d.replaceAll('export const '+ (options.includes('export consts')?'BLAH':''),'')}~~~`)md`<br/>**inputs** \`${inputs.filter(d => fns0.includes(d)).join('\`, \`')}\`${not_inputs.length?' | ~~\`':''}${not_inputs.map((d,i) =>`${d}\`~~${i != not_inputs.length-1?', ~~\`':''}`).join('')}`
savings_rec_data =calcudata({models:[model],input_domains: {year_in:_.range(0,6),actual_interest_rate_co_in: [actual_interest_rate_co_in-1,actual_interest_rate_co_in]},input_cursors: [{annual_payment_in:1000}],outputs: ['balance','interest','deposits','interest_rate']})savings_actualator.addSignalListener("y", (_, r) => { // this didn't improve perf a lot vs event listener: nearly every mousemove is a signal change, I suppose//if (observable_world == 0) return; viewof actual_interest_rate_co_in.value= r.year_in[0]; viewof actual_interest_rate_co_in.dispatchEvent(newCustomEvent('input'), {bubbles:true});}) &&1
community gallery soon ⌛ - get in touch if you want to contribute a model!
calculang dev tools 🛠️🧰 (some of)
formula-inputs matrix
Code
md`${function_inputs_table(introspection)}`// this should be better than a matrix: it should be an indented tree where it's possible to follow the logic of the compiler incl. where inputs get summarised// useful to do nomemo/memo option? (should always be the same, be aware memo functionality will change)
Code
viewof graph_nomemo = Inputs.checkbox(["nomemo"], {value: [/* todo move to this"nomemo"*/""]})g =dot`${graph_functions(graph_nomemo.length? introspection_nomemo : introspection)}`DOM.download(() =>serializeSVG(g),undefined,"Download SVG")
cul scope id graph
Code
viewof show_query_string = Inputs.checkbox(["show query string?"])viewof scope_id_graph_nomemo = Inputs.checkbox(["nomemo"], {value: ["nomemo"]})// defaults here are setup to protect you from what really happens when memo is on !dot`${scope_id_graph}`
scene.addSignalListener("ray_angle_in", (_, r) => {//if (observable_world == 0) return; viewof ray_angle_in.value=Math.round(r.ray_angle_in*100)/100; viewof ray_angle_in.dispatchEvent(newCustomEvent('input'), {bubbles:true});// I might never understand how to make this always work})
Code
level.addSignalListener("xy", (_, r) => { // this didn't improve perf a lot vs event listener: nearly every mousemove is a signal change, I suppose//if (observable_world == 0) return; viewof ray_angle_in.value=Math.round(Math.atan2((r.level_y_in[0] - player_y_in) , (r.level_x_in[0] - player_x_in))*100)/100; viewof ray_angle_in.dispatchEvent(newCustomEvent('input'), {bubbles:true});})