DEV Community

loading...

Discussion on: Practical Functional Programming in JavaScript - Control Flow

Collapse
functional_js profile image
Functional Javascript • Edited

Great stuff Richard.
And great community input David.

Congrats on the Rubico library also. Keep at 'er.

I notice you have a bunch of posts on dev.to on your functional approach, so over the next little while I'll take a gander through them.

I've only read this post of yours so far, so I'm not commenting on your full body of work, but let me compare with my architectural viewpoint of a functional approach to software architecture.

As far as how things are implemented I care about these attribute, in this order...

  • natural language documentation
  • security
  • robustness
  • performance
  • code readability

More can be said on how those attributes interplay, but the actual implementation that gets chosen is the one that wins out on that prioritized attribute set.

So from your examples above, none of those won out. :)
I'd use the switch statement, as it uses 10 to 20 times less cycles.

After running each func 1e+6 (1 million) times....

Func Time Iter
cliSwitch 24.255ms 1e6
cliSwitchIncludes 46.474ms 1e6
cliIf 224.728ms 1e6
cliTernary 242.144ms 1e6
cliRubico 413.755ms 1e6
/**
@func
test switching through a set of possible flags
- input by the user from the cli

@param {string[]} args - contains the passed-in flag
*/
const cliSwitch = args => {
  const s = args[2];
   switch (s) {
    case "-h":
    case "--help":
      l('usage: ./cli [-h] [--help] [-v] [--version]');
      break;
    case "-v":
    case "--version":
      l('v0.0.1');
      break;
    default:
      l('unrecognized command');
  }
};

//@tests
timeInLoop("cliSwitch", 1e6, () => cliSwitch(["", "", "-nomatch"]));

Source Code for the timeInLoop func:

gist.github.com/funfunction/91b587...

Collapse
richytong profile image
Richard Tong Author • Edited

Thank you for these benchmarks. I will take care to document/address in rubico. Also, if you're curious, here is a large part of the switchCase implementation (rest of switchCase is just user error throwing)

// (fns [any], x any, i number) => fnix any
// arraySwitchCase([condition, f, g], x) // condition(x) ? f(x) : g(x)
const arraySwitchCase = (fns, x, i = 0) => {
  if (i === fns.length - 1) return fns[i](x)
  const ok = fns[i](x)
  return isPromise(ok)
    ? ok.then(res => res ? fns[i + 1](x) : arraySwitchCase(fns, x, i + 2))
    : ok ? fns[i + 1](x) : arraySwitchCase(fns, x, i + 2)
}

switchCase will forever lose to vanilla JavaScript syntax in performance because of the built in Promise handling. I built in Promise handling to all rubico functions for the sake of being able to do practical stuff but not have to worry so much about the async boilerplate. I thought it would be more Mathsy that way.

Collapse
functional_js profile image
Functional Javascript • Edited

Hey Richard,

That isPromise check should be fast.
I think your bottleneck there will be the recursion idiom.
The iterative idiom is always faster and more robust.
Any recursion can be replaced with a simple loop.
(I never use recursion, it always fails my robustness and performance tests)

However

However your non-recursive "or" func is slow verses the baseline, and so is its utility, "arrayOr", even when I pass in non-promises (which should make it take the fastest path).

const arrayOr = (fns, x) => {
  const promises = []
  for (let i = 0; i < fns.length; i++) {
    const point = fns[i](x)
    if (isPromise(point)) promises.push(point)
    else if (point) return (promises.length > 0
      ? Promise.all(promises).then(() => true)
      : true)
  }
  return (promises.length > 0
    ? Promise.all(promises).then(res => res.some(x => x))
    : false)
}

//@tests
timeInLoop("arrayOr", 1e6, () => arrayOr([() => 1, () => 2, () => 3], 0)) //48.216ms for 1e6

isPromise

Btw, as a little aside, I perf-compared your isPromise implementation with mine.
Now, my isPromise looks more "proper"; and is actually more robust in my robustness test (not shown here), however yours is magnificently faster, by almost 10x :-) ...

const isPromise = v => v instanceof Promise;

const isPromise2 = x => x && typeof x.then === "function"

//@tests
const aFalse = [undefined, e => e, {}, { then: {}}, {then: e => e}];
//const aTrue = [new Promise(resolve => { }), new Promise(e => e)];
timeInLoop("isPromise", 1e6, () => isPromise(aFalse)); //93.021ms
timeInLoop("isPromise2", 1e6, () => isPromise2(aFalse)); //10.004ms

prioritized attribute set

So using my "prioritized attribute set" criteria explained in my first post, if I can modify yours enough to be as robust as mine (should be easy to do with very little performance hit) I will swap the slow for the fast.

Thread Thread
richytong profile image
Richard Tong Author

I welcome your contribution. I do not doubt that the current recursive implementation would lose to an iterative implementation. All I request is documentation via issues and then PRs. Also, I'll need to create a benchmarks directory with some examples - I'll do that today.

Collapse
richytong profile image
Richard Tong Author

could you elaborate on natural language documentation?

Collapse
functional_js profile image
Functional Javascript

Hey Richard,

I just posted an article to elaborate on that...
dev.to/functional_js/squeezing-out...

Collapse
richytong profile image
Richard Tong Author

Wanted to share some interesting benchmarks. I've adopted your timeInLoop function, by the way. Thank you for that.

  switchCase
    switchCase([isOdd, () => 'odd', () => 'even']); timeInLoop
20ms
      ✓ switchCase
150ms
      ✓ conditional operator cond ? a : b (151ms)
147ms
      ✓ if else statement (150ms)
147ms
      ✓ switch case statement (147ms)

It's looking like rubico's switchCase beats out regular syntax! I feel like this has something to do with the functions being in the scope already.

Collapse
functional_js profile image
Functional Javascript

Nice!

Feel free to post the source code and I'll give it a shot myself.

And yes, as I had mentioned in the Tips section of the Post I linked to above, each test must be run separately, otherwise the compiler may optimize some code by learning from the other code.

Thread Thread
richytong profile image
Collapse
functional_js profile image
Functional Javascript

I think the problem with your code might be that your second "switchCase" is running with a lamba inside of a lamba, so the actual code you want to test does not get hit.
Correct me if I'm wrong.

Here are my results...
It includes the test of the buggy one.

Note:
I ran each "timeInLoop" separately, and about 5 times each, and reported the lowest score.

import { or, switchCase } from "rubico";
import timeInLoop from "./timeInLoop";

const isOdd = x => x % 2 === 1;


//@perftests

//isOdd: 1e+6: 10.013ms
timeInLoop("isOdd", 1e6, () => isOdd(4));



// isOdd_ternary: 1e+6: 9.726ms
timeInLoop("isOdd_ternary", 1e6, () => {
  isOdd(4) ? 1 : 0
});


// isOdd_ifElse: 1e+6: 9.846ms
timeInLoop("isOdd_ifElse", 1e6, () => {
  if (isOdd(4)) return 'odd'
  else return 'even'
});


// isOdd_switch: 1e+6: 9.776ms
timeInLoop("isOdd_switch", 1e6, () => {
  switch (isOdd(4)) {
    case true: return 'odd'
    default: return 'even'
  }
});



//isOdd_rubicoSwitchCase: 1e+6: 152.762ms
timeInLoop("isOdd_rubicoSwitchCase", 1e6, () => {
  switchCase([() => isOdd(4), () => 'odd', () => 'even'])
});



//@BUG: a nested lambda, the code to be perftested never executes
// isOdd_rubicoSwitchCaseExtraLambda: 1e+6: 10.667ms
timeInLoop("isOdd_rubicoSwitchCaseExtraLambda", 1e6, () => {
  () => switchCase([isOdd, () => 'odd', () => 'even'])
});
Thread Thread
richytong profile image
Richard Tong Author

I looked into it a bit, turns out the differences we were seeing were due to mocha. I was using it to organize the benchmarks, but I see now that I should probably get closer to the ground. I'll also revise rubico's timeInLoop to model yours more closely.

Thread Thread
functional_js profile image
Functional Javascript

Great.
Keep up the good work, and let me know how it progresses and if you come up with more ideas.

Forem Open with the Forem app