Greg / CHUTE - Chain Functions AND Native Methods in Vanilla JavaScript
 Language: Vanilla JavaScript
Version 1: 2024-11-27
  Version: 2024-11-27
 Download: Normal (2KB)
           Minified (1KB)
   Notice: By and © Greg Abbott 2024
    Links: GitHub

Script
const chute = (()=>{
//CHUTE: Chain functions AND methods
//https://github.com/gregabbott/chute
//By + Copyright Greg Abbott 2024-11-27 (V1) + 2024-11-27 (V)
let data
let key_is=null
const error=(...x)=>{throw new Error(x)}
const is_fn=x=>x instanceof Function
const is_js_method=(o,k)=>is_fn(Object.getPrototypeOf(o)[k])
function sub_chain_reducer (data,f){
  if(is_fn(f))return f(data)
  if(f==='log'){console[f](data);return data}
  else if(typeof f==='string'){
    if(is_js_method(data,f)) return data[f]()
    else error(`Data lacks "${f}" method`)
  }
  else {error('Not a function:',f)}
  return data
}
const sub_chain=a=>a.reduce(sub_chain_reducer,data)
const call_method_of_data=(key,a)=>{
  if(is_js_method(data,key)){data=data[key](...a)}
  else error(`Data lacks "${key}" method`)
}
const if_then=({cond,fn})=>{
  const cond_is_fn=is_fn(cond)
  const good_cond=cond_is_fn||cond===false||cond===true
  if(!good_cond){
    error('give .if(arg1) bool OR test=data=>{…return bool}')
  }
  if(!is_fn(fn)){
    error('give .if(,arg2) a Fn to send data to',fn)
  }
  else if(cond_is_fn&&cond(data)===true)data=fn(data)
  else if(cond===true)data=fn(data)
}
const proxy = new Proxy(
  function target(){}, 
  {
  get(target,k){
    key_is=k
    return proxy
  },
  apply:(target,_, a)=>{
    let key = key_is
    if(key_is)key_is=null
    if(key=='log'){console[key](...a,data);}
    else if(key=='if'){if_then({cond:a[0],fn:a[1]})}
    else if(key=='tap'&&is_fn(a[0]))a[0](data)
    else if(key=='tap')error('give .tap a fn to send data to')
    else if(key===null&&a.length===0)return data
    else if(key===null||key==='and')data=sub_chain(a)
    else if(key)call_method_of_data(key,a)
    return proxy
  }
})
return (x,...fns)=>{
  data=x
  if(fns.length>0)data=sub_chain(fns)
  return proxy
}
})()
Example use
let initial_data = [1, 2, 4]
const result = chute(initial_data,/*any functions…*/push(8))
  //then swap between function and method calls as needed.
  //call any method native to the current data as normal:
  .map(double)
  //".log" calls a built in method that takes 0+ arguments.
  .log('doubled:')
  //".log" logs any arguments and the data, then resumes.
  .filter(greater_than(4))
  //".and" calls a built in method that takes 1+ functions.
  //".and(f1,f2)" sends data to f1, then f1's return to f2.
  .and(push(32),push(64))
  //nameless calls work the same way as ".and":
  (push(80,128),push(256))
  //each sub-chain uses a reducer and mimics nested calls.
    //one call "(f1,f2)" == "f2(f1(data))"
    //many calls "(f1)(f2)" == "data=f1(data);data=f2(data)"
  ('reverse')//strings call methods that need no arguments.
  //this allows unbroken sequences: "(f1,'method',f2)"
  //'.tap' calls a built in method that takes a function.
  //'.tap(f1)' sends data to f1 but ignores the result.
  .log('pre-tap')
  .tap(mutate)//.tap functions might mutate data directly.
  .log('post-tap')
  .reduce(add_up)
  //'.if' calls an inbuilt method that takes 2 arguments.
  .if(
    //a boolean
    //or a function that tests data and return a boolean.
    greater_than(64),
    //a function to send data to if the condition === true.
    halve
  )
  .toString()
  (append('.'))
  ()//an empty call returns the final value
  //from this point, method calls directly act on the data.
  .replace('.','!')

log({result})
//Demo Helper Functions
function push (...a){return l => (l.push(...a),l)}
function log (x){console.log('outer_log',x);return x}
function greater_than(n){ return x => x > n}
function double(x){return x * 2}
function halve(x){return x / 2}
function add_up(a, b){return a + b}
function append(x){return d => d+x}
function mutate (data){
  data.length=0
  data.push(1452,1386,1483,1475)
  return 'chucks this returned value'
}