0

I am trying to understand how compose works by recreating compose. As part of that I've created a simple calculator to take a value and based on that value return interest.

https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d#.rxqm3dqje

Essentially ultimate goal is to create a function that can do below. https://github.com/ngrx/example-app/blob/master/src/app/reducers/index.ts#L150

Nice to have: been able to pass multiple deposit values, and and may be calculate compound interest over time.

It would be good have some comments so I understand what is going on from Functional programming approach.

(()=>{
    // Generic compose function to handle anything
    const compose = (...fns) => (x) => {
        return fns.reduceRight((acc,fn)=>{
            return fn(acc);
        }, x)
    };

    const getInterest = (value) => {
        if (value < 1000) {
            return 1 / 100
        }

        if (value < 10000) {
            return 2 / 100
        }

        return 3 / 100;
    };

    const getDeposit = (value) => {
        return value;
    };

    const calculator = compose(getDeposit, getInterest)

    console.log(calculator(1000)) // Should return value of 1000 * interest rate. I currently get 0.03
})();
1
  • When you reduce from the right, acc should be the second argument of the reducer. Commented Mar 14, 2017 at 15:28

2 Answers 2

1

The issue is that you never multiply the two values: value and interest.

You should therefore pass another function into the composition, which will multiply the two previous results.

This means that this function will need to get 2 arguments, while the other two only take one. In general a function could need any number of arguments. So the composer should be able to pass enough arguments to each function. Furthermore, functions may also return more than one value -- in the form of an array. These values should be made available as arguments for the next function in the chain, while keeping any previous returned values available as well.

Another thing is that although you have implemented compose to execute the functions from right to left, the sequence of function you pass seem to suggest you expect them to execute from left to right, first getDeposit, and then getInterest, even though in your case it works both ways. Still, I would suggest to switch their positions.

So here is how you can make all that work:

(()=>{
    // Generic compose function to handle anything
    const compose = (...fns) => (...args) => {
        return fns.reduceRight((acc,fn)=>{
            // Call the function with all values we have gathered so far
            let ret = fn.apply(null, acc);
            // If function returns a non-array, turn it into an array with one value
            if (!Array.isArray(ret)) ret = [ret];
            // Queue the returned value(s) back into the accumulator, so they can
            // serve as arguments for the next function call
            acc.unshift(...ret);
            return acc;
        }, args)[0]; // only return the last inserted value
    };

    const getInterest = (value) => {
        return value < 1000 ? 0.01
            : value < 10000 ? 0.02
            : 0.03;
    };

    const multiply = (a, b) => a * b;

    const getDeposit = (value) => value;

    // Be aware the the rightmost function is executed first:
    const calculator = compose(multiply, getInterest, getDeposit);

    console.log(calculator(1000)) // Returns 20, which is 0.02 * 1000
})();

Alternative: pass along an object

The above implementation is not a pure compose implementation, since it passes not only the previous function result on to the next, but all previous functions results. This is not disturbing, and opens doors for more complex functions, but if you wanted to stick more to the original compose idea, you have a problem to solve:

As you want to have a function in the chain that only returns the rate, the next function in the chain will then only get the rate -- nothing else. With just that one piece of information it is of course not possible to calculate the result, which also needs the value as input.

You could "solve" this, by letting getInterest return an object, that not only has the rate in it, but also the value that was passed to it. You could also implement this with an array.

Here it is with an object implementation:

(()=>{
    // Straightforward implementation:
    const compose = (...fns) => (...args) => {
        return fns.reduceRight((acc,fn)=>{
            return fn(acc);
        }, args);
    };

    // Return an object with two properties: value & interest   
    const getInterest = (value) => ({
        value,
        interest: value < 1000 ? 0.01
                : value < 10000 ? 0.02
                : 0.03
    });

    // Expect an object as argument, with two properties:
    const getInterestAmount = ({value, interest}) => value * interest;

    const getDeposit = (value) => value;

    // Be aware the the rightmost function is executed first:
    const calculator = compose(getInterestAmount, getInterest, getDeposit);

    console.log(calculator(1000)) // Returns 20, which is 0.02 * 1000
})();

With this approach you can pass along objects that have many more properties, and so anything becomes possible.

Sign up to request clarification or add additional context in comments.

6 Comments

ahh that solves one part... but how can I make the getInterest reusable? for e.g. I want to substitute 'interest rates'. but keep getInterest intact in a sense that way it calculate doesn't change and not aware of the rates? jsfiddle.net/avkerto8
get value from getDeposit and getInterestRate (or any number of other methods) use these values to calculate interest/outcome?
See the updated answer. It is more generic and passes any number of results from one function to the next, maintaining a queue of values in acc.
In a pure interpretation of "compose" one function would indeed just take the result from the previous function call, not anything else. But since you wanted to have a function that returned the rate, such a strict interpretation would allow the next function in the chain to only get the rate as information, which just is not enough to do the calculation. That's why I propose to have a more extended "compose" (or actually it is a "sequence" since it executes the functions from left to right) which remembers all the previous function results and passes them all to the next call.
See the update to my answer. I hope it is useful to you.
|
0

I actually liked your simple compose function (it just only works for unary functions), and I think you can also make it work for now by making these changes:

  • rename getInterest to ...Rate, since it returns a multiplier for a value.
  • add a new getInterest function that takes a "rate getter" and a "value" in curried form: getRate => x => getRate(x) * x
  • swap the order of your calculator arguments in compose

    I think compose usually works from right to left (f => g => x => f(g(x))), and pipe works from left to right (f => g => x => g(f(x)))

(()=>{
    // Generic compose function to handle anything
    const compose = (...fns) => (x) => {
        return fns.reduceRight((acc,fn)=>{
            return fn(acc);
        }, x)
    };

    const defaultRate = (value) => {
        if (value < 1000) {
            return 1 / 100
        }

        if (value < 10000) {
            return 2 / 100
        }

        return 3 / 100;
    };

    const getInterest = getRate => x => getRate(x) * x;

    const getDeposit = x => 1000;

    const calculator = compose(getInterest(defaultRate), getDeposit);

    console.log(calculator());
    
    
})();

4 Comments

Issue with that is as I mentioned in comment for accepted answer getInterest is not that reusable? what if I want to have 2 calculators? e.g. businessInterestCalculator and personalInterestCalculator with different configurations for rates?
Ah, so that's where the variable is. Didn't get that.. It's easy to add though by asking for the getRate method in getInterest. See edit. (dv is a bit harsh, no?)
@trincot's answer is more elegant/flexible...? because now we have compose(f(g(x)), h(x))? wouldn't having to do (f(g(x)) break what we are trying to achieve with compose? when we can simply have compose(f(x), g(x), h(a, b)). sorry, I didn't down vote you...
I was downvoted as well. I think someone who had a bad day passed by :P

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.