DEV Community

Cover image for Practical Functional Programming in JavaScript - Control Flow
Richard Tong
Richard Tong

Posted on • Updated on

Practical Functional Programming in JavaScript - Control Flow

Usually when authors use the terms "functional programming" and "control flow" together in the same sentence, it's to say functional programming should not have control flow.

I'm using the wikipedia definition for control flow in this case

control flow (or flow of control) is the order in which individual statements, instructions or function calls of an imperative program are executed or evaluated

Control flow explicitly refers to statements, which are different from expressions. Where JavaScript is concerned, statements have semicolons and expressions do not. This is an important distinction, and means the difference between imperative and functional programming in JavaScript.

a + b;
// a, b and a + b are expressions
// a + b; is a statement
Enter fullscreen mode Exit fullscreen mode

All of the articles above avoid if statements and instead prefer language equivalents of the conditional (ternary) operator. I'm here to agree with them in technicality and diverge a bit in practice. I diverge because the conditional operator can get messy in practice; I'm here to offer a cleaner, more scalable way. More on this later on.

The conditional (also referred to as "ternary") operator takes three operands: a condition expression, an expression to evaluate on truthy condition, and an expression to evaluate on falsy condition. It's like if and else, but instead of statements (yes semicolons), you put expressions (no semicolons).

condition ? a : b // if condition, evaluate expression a, else evaluate expression b
Enter fullscreen mode Exit fullscreen mode

Purely functional languages like Haskell don't have the notion of a semicolon; they rely on syntax resembling the conditional operator

if condition then a else b
Enter fullscreen mode Exit fullscreen mode

Python also has conditional-like syntax

a if condition else b
Enter fullscreen mode Exit fullscreen mode

As you can see, the concept of a "ternary", or that which is "composed of three parts", is common across languages. It just makes a ton of sense to express a choice with three things: if some condition, do this, else do that. With JavaScript, you can do this imperatively with if, else statements or functionally with the conditional operator.

// imperative
const describeNumber = number => {
  let description = '';
  if (number < 0) {
    description = 'negative';
  } else if (number === 0) {
    description = 'zero';
  } else {
    description = 'positive';
  }
  return description;
};

// functional
const describeNumber = number =>
  number < 0 ? 'negative'
  : number === 0 ? 'zero'
  : 'positive';
Enter fullscreen mode Exit fullscreen mode

You can go pretty far with the conditional operator alone, but there will be times when something more expressive could help you solve your problems better. This is especially true for code with a lot of branching or complex data handling. For these cases, I've devised a clean and declarative way for you to express conditional flow with my functional programming library, rubico.

Consider an entrypoint to a basic node command line interface application that accepts flags. The application is very simple; all it does is print its own version and its usage.

// argv [string] => ()
const cli = argv => {
  if (argv.includes('-h') || argv.includes('--help')) {
    console.log('usage: ./cli [-h] [--help] [-v] [--version]');
  } else if (argv.includes('-v') || argv.includes('--version')) {
    console.log('v0.0.1');
  } else {
    console.log('unrecognized command');
  };
};

cli(process.argv); // runs when the cli command is run
Enter fullscreen mode Exit fullscreen mode

This is nice and familiar, but it's imperative, and you're here about functional programming, after all. Let's refactor some functionality and use the conditional operator.

// flag string => argv [string] => boolean
const hasFlag = flag => argv => argv.includes(flag);

const USAGE = 'usage: ./cli [-h] [--help] [-v] [--version]';

// argv [string] => ()
const cli = argv =>
  hasFlag('--help')(argv) || hasFlag('-h')(argv) ? console.log(USAGE)
  : hasFlag('--version')(argv) || hasFlag('-v')(argv) ? console.log('v0.0.1')
  : console.log('unrecognized command');

cli(process.argv); // runs when the cli command is run
Enter fullscreen mode Exit fullscreen mode

Now it's looking real cool, but don't you think there's a lot of argvs everywhere? It gets better with rubico.

  • switchCase - like the conditional operator, but with functions. Each function is called with the same input
  • or - like the logical or (||) operator, but with functions. Each function is called with the same input
const { or, switchCase } = require('rubico');

// flag string => argv [string] => boolean
const hasFlag = flag => argv => argv.includes(flag);

const USAGE = 'usage: ./cli [-h] [--help] [-v] [--version]';

const log = message => () => console.log(message);

// argv [string] => ()
const cli = switchCase([
  or([
    hasFlag('--help'),
    hasFlag('-h'),
  ]), log(USAGE),
  or([
    hasFlag('--version'),
    hasFlag('-v'),
  ]), log('v0.0.1'),
  log('unrecognized command'),
]);

cli(process.argv); // runs when the cli command is run
Enter fullscreen mode Exit fullscreen mode

With switchCase and higher order logical functions like or, it's like you're just typing it as you're thinking it. If the argv has the flag --help or -h, print the usage. Otherwise, if it has the flag --version or -v, print the version v0.0.1. Otherwise, print unrecognized command. I think it's an intuitive way to go about expressing logic in functional programs.

My hope is with switchCase and the logical combining functions and, or, and not, we could have a good basis to scale conditional expressions in functional JavaScript beyond the conditional (ternary) operator. If you have any thoughts about this or anything, I would love to get back to you in the comments. Thank you for reading! I'll see you next time on Practical Functional Programming in JavaScript - Error Handling

You can find the rest of the series in rubico's awesome resources

Sources:

Top comments (17)

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"]));

Enter fullscreen mode Exit fullscreen mode

Source Code for the timeInLoop func:

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

Collapse
 
Sloan, the sloth mascot
Comment deleted
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.

Collapse
 
Sloan, the sloth mascot
Comment deleted
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

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.

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
Richard Tong

switchCase benchmarks here: github.com/a-synchronous/rubico/bl...

Collapse
 
richytong profile image
Richard Tong

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
 
wulymammoth profile image
David

This is very interesting, Richard -- definitely fun exploration.

Your example begins looking very declarative (one of the awesome things about FP), and almost like SQL if one squints.

I've never gone beyond using the native functional facilities in the standard library and I used them quite a bit when I worked with JS, but curious as to whether you're familiar with Lodash's FP module: github.com/lodash/lodash/wiki/FP-G.... It definitely doesn't provide an interface that Rubico provides for handling conditions. I've never dove deep enough into Haskell to know whether what you're doing is an adaptation of an idea there

Collapse
 
richytong profile image
Richard Tong • Edited

rubico is very much a JavaScript library, and was born from my own needs as a JavaScript developer. I have tried my best to preserve the good work and idioms of more traditional fp'ers, but rubico did not come from another language or another set of practices. I would say what I'm doing here with control expressions in rubico more borrows from earlier work trying to abstract the query DSL of elasticsearch. That produced something like this

 * {
 *   $and: {
 *     fieldA: vA, // term
 *     fieldH: { $like: vH }, // match (fuzzy)
 *     fieldB: { $gte: lvB, $lte: uvB }, // range
 *     fieldI: $exists,
 *   },
 *   $or: {
 *     fieldC: [a, b], // terms
 *     path.fieldE: [a, b], // nested terms query
 *     $geo: { lat: 123, lon: 21, distance: '50mi' }, // geo_distance
 *   },
Enter fullscreen mode Exit fullscreen mode

Lodash's FP module

I wanted rubico to be less of a grab bag and focus more on absolutely necessary syntax. You could in some ways consider hasFlag as a new member of the syntax available to solve the problem of a command line interface. Also, I've left an option for a "grab bag" of sorts called rubico/x. Basically if you have a function you like (like lodash defaultsDeep), you could add it into rubico as rubico/x/defaultsDeep. Then you could import it like

const defaultsDeep = require('rubico/x/defaultsDeep')
Enter fullscreen mode Exit fullscreen mode
Collapse
 
iquardt profile image
Iven Marquardt

FP has control flow, but one that is rather determined by arithemtic laws and non-strict evaluation than the lexical position of statements/expressions. An operator can be associative, commutative, distributive. Monoid, for instance, is associative, but not commutative, which gives you a precise and predicable control flow. The entire Monad type class exists to compose statements and thus allow modelling control flows similar to those in imperative programming.

Collapse
 
richytong profile image
Richard Tong

Doesn't control flow imply statements? My understanding is pure functional programming languages do not have statements and control flow is an imperative only construct.

Collapse
 
iquardt profile image
Iven Marquardt • Edited

Consider the following computation:

readFromUser
logUserInput

You need an order for that. Another example:

// dropping the parenthesis for clarity
f g x

// adding parenthesis to reveal left associativity of function application
(f g) x

Function application establishes a left associative evaluation order. Function composition establishes a right associative one.

With monads you define evaluation order of function composition with side effects (first example).

Now if you refer to the purely functional theory (lambda calculus) you are almost right: There are different evaluation strategies and the compiler decides which one is applied.

You can even write: Loosely speaking there is no evaluation order in FP, but this is a simplification.

Collapse
 
macsikora profile image
Pragmatic Maciej

The thing is that if you compare this to simple switch then the winner is not so obvious.
dev.to/macsikora/switch-is-ok-336l