Home > Back-end >  Chain methods and call function one by one
Chain methods and call function one by one

Time:12-02

so basically I need to implement a lazy evaluation. .add() takes a function as parameter and any other arbitrary arguments. The function that is passed as an argument is run later (when evaluate is called) with other arguments(if any) as parameters to that function.

Basically my issue stands when i run .evaluate() which takes an array as parameter, the functions that were passed to add() as parameters are not called one at a time and returning a result and have it as a parameter for the next.

class Lazy {
  constructor() {
    this.functions = [];
    this.counter = 0;
    this.resultedArray = [];
  }

  add(func, ...args) {
    if (args.length > 0) {
      this.counter  ;
      const argsArrayNumeric = args.filter((item) => typeof item === "number");

      this.functions.push({
        func: func,
        args: args,
      });
    } else {
      if (func.length <= args.length   1 || func.length === args.length) {
        this.counter  ;
        this.functions.push({ func: func });
      } else {
        console.log("Missing parameters for "   func.name   " function");
      }
    }
    return this;
  }

  evaluate(array) {
    if (this.counter > 0) {
      let result;

      this.functions.map((obj, index) => {
        array.map((item, index) => {
          console.log(obj);
          if (obj.func && obj.args) {
            result = obj.func(item, ...obj.args);
          } else {
            result = obj.func(item);
          }
          this.resultedArray.push(result);
        });
      });
      console.log(this.resultedArray);
    } else {
      console.log(array);
    }
  }
}

const s = new Lazy();

const timesTwo = (a) => {
  return a * 2;
};
const plus = (a, b) => {
  return a   b;
};

s.add(timesTwo).add(plus, 1).evaluate([1, 2, 3]); 
//correct result is [3,5,7] but i have [ 2, 4, 6, 2, 3, 4 ]
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

There are a few problems in evaluate:

  • push will be executed too many times when you have more than one function.
  • Don't use map when you don't really map. .map() returns a result.
  • From the expected output (in the original version of the question) it seems you need to apply the functions from right to left, not from left to right.
  • this.counter does not really play a role. The length of the this.functions array should be all you need.
  • the result should not be printed in the method, but returned. It is the caller's responsibility to print it or do something else with it.

All this can be dealt with using reduce or reduceRight (depending on the expected order) like this:

evaluate(array) {
    return this.functions.reduceRight((result, obj) => 
        result.map((item) => obj.func(item, ...(obj.args || [])))
    , array);
}

And in the main program, print the return value:

console.log(s.add(plus, 1).add(timesTwo).evaluate([1, 2, 3])); 

The add method has also some strange logic, like:

  • When the first if condition is false, then the else block kicks in with args.length == 0. It is then strange to see conditions on args.length... it really is 0!
  • If the first condition in func.length <= args.length 1 || func.length === args.length is false, then surely the second will always be false also. It should not need to be there.
  • argsArrayNumeric is never used.

All in all, it seems the code could be reduced to this snippet:

class Lazy {
    constructor() {
        this.functions = [];
    }

    add(func, ...args) {
        if (func.length > args.length   1) {
            throw ValueError(`Missing parameters for ${func.name} function`);
        }
        this.functions.push({ func, args });
        return this;
    }

    evaluate(array) {
        return this.functions.reduceRight((result, obj) => 
            result.map((item) => obj.func(item, ...(obj.args || [])))
        , array);
    }
}

const timesTwo = (a) => a * 2;
const plus = (a, b) => a   b;

const s = new Lazy();
console.log(s.add(plus, 1).add(timesTwo).evaluate([1, 2, 3])); // [3, 5, 7]
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

I think you're working too hard at this. I believe this does almost the same thing (except it doesn't report arity errors; that's easy enough to include but doesn't add anything to the discussion):

class Lazy  {
  constructor () {
    this .fns = []
  }
  add (fn, ...args) {
    this .fns .push ({fn, args});
    return this
  }
  evaluate (xs) {
    return xs .map (
      x => this .fns .reduce ((a, {fn, args}) => fn (...args, a), x)
    )
  }
}

const timesTwo = (a) => a * 2
const plus = (a, b) => a   b

const s = new Lazy () .add (timesTwo) .add (plus, 1)

console .log (s .evaluate ([1, 2, 3]))
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

But I don't actually see much of a reason for the class here. A plain function will do much the same:

const lazy = (fns = [], laze = {
  add: (fn, ...args) => lazy (fns .concat ({fn, args})),
  evaluate: (xs) => xs .map (
    x => fns .reduce ((a, {fn, args}) => fn (...args, a), x)
  )
}) => laze

const timesTwo = (a) => a * 2
const plus = (a, b) => a   b

const s = lazy () .add (timesTwo) .add (plus, 1)

console .log (s .evaluate ([1, 2, 3]))
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

This is slightly different in that s is not mutated on each add, but a new object is returned. While we could change that and mutate and return the original object easily enough, the functional programming purist in me would actually consider moving more in that direction. In fact, all we're maintaining here is a list of {fn, args} objects. It might make the most sense to take that on directly, like this:

const add = (lazy, fn, ...args) => lazy .concat ({fn, args})
const evaluate = (lazy, xs) => xs .map (
  x => lazy .reduce ((a, {fn, args}) => fn (...args, a), x)
)

const timesTwo = (a) => a * 2
const plus = (a, b) => a   b

const q = []
const r = add (q, timesTwo)
const s = add (r, plus, 1)

// or just
// const s = add (add ([], timesTwo), plus, 1)

console .log (evaluate (s, [1, 2, 3]))
<iframe name="sif5" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

In this version, we don't need a Lazy constructor or a lazy factory function, as our data structure is a plain array. While this is the format I prefer, any of these should do something similar to what you're trying.

Update

Based on comments, I include one more step on my journey, between the first and second snippets above:

const lazy = () => {
  const fns = []
  const laze = {
    add: (fn, ...args) => fns .push ({fn, args}) && laze,
    evaluate: (xs) => xs .map (
      x => fns .reduce((a, {fn, args}) => fn (...args, a), x)
    )
  }
  return laze
}

const timesTwo = (a) => a * 2
const plus = (a, b) => a   b

const s = lazy () .add (timesTwo) .add (plus, 1)

console .log (s .evaluate ([1, 2, 3]))
<iframe name="sif6" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

This version may be more familiar than the second snippet. It stores fns in a closure, hiding implementation details in a way we often don't with JS classes. But that list is still mutable through the add method of the object we return. Further ones proceed down the road to immutability.

  • Related