DEV Community

loading...

Discussion on: OOP vs functional programming

Collapse
peerreynders profile image
peerreynders • Edited

Functional programming is primarily about composing functions to transform a value.

So I might write the "functional-style" version as:

;(function(){
  // functional.js

  /*
      Functional Core
   */
  // Result module
  // Either {ok}  - contains the successful value
  // Or     {err} - contains the error message
  const makeOk = value => ({ok: value});
  const makeErr = message => ({err: message});

  // Higher order functions (HOFs) for Result values
  const andThen = fn =>
    rs => {
      const {ok} = rs;
      if (ok === undefined) return rs;

      return fn(ok);
    };
  const map = fn =>
    rs => {
      const {ok} = rs;
      if (ok === undefined) return rs;

      return makeOk(fn(ok));
    };

  // Validations
  const validateRequired = message =>
        value => value.trim().length > 0 ? makeOk(value) : makeErr(message);

  const digits = new RegExp('\\s*\\d+\\s*');
  const validateInteger = message =>
        value => digits.test(value) ? makeOk(value) : makeErr(message);

  const validateBetween = (message, lower, upper) =>
        value => lower <= value && value <= upper ? makeOk(value) : makeErr(message);

  // JavaScript isn't functional so iteration (+ mutation)
  // is more natural than recursion.
  function factorial(n) {
    let temp = [1, n];

    // iterate until terminating condition
    while(temp[1] > 1) {
      temp = stepFactorial(temp);
    }
    return temp[0];
  }

  const stepFactorial = ([acc, n]) => [acc * n, --n];

  // Partially apply the functions and
  // "compose" the resulting single argument functions
  // to transform the input value
  const composition = [
    validateRequired('Please provide a value'),
    andThen(validateInteger('Please provide a integer')),
    map(n => Number.parseInt(n, 10)),
    andThen(validateBetween('Please specify an integer between 0 and 18', 0, 18)),
    map(factorial)
  ];

  function pipe(fns, value) {
    let result = value;
    for(let i = 0; i < fns.length; ++i) {
      result = fns[i](result);
    }
    return result;
  }

  // transform the string to an {ok/err} result
  const transform = value => pipe(composition, value);

  /*
    Imperative Shell
  */
  function whenDomUpdated(fn) {
    requestAnimationFrame(
      () => requestAnimationFrame(fn)
    );
  }

  function calculateListener(event) {
    const inputEl = event.target.querySelector('input');
    const {ok, err} = transform(inputEl.value);

    if (err === undefined) {
      resultEl.innerText = ok.toString();

    } else {
      resultEl.innerText = '';
      whenDomUpdated(() => {
        alert(err);
      });
    }

    event.preventDefault();
  }

  // Initialization Script
  const formEl = document.querySelector('#factorial-form');
  const resultEl = document.querySelector('#factorial-result');
  formEl.addEventListener('submit', calculateListener);

}());

Edit: It's useful to remember Master Qc Na's lessons:

  • Objects are merely a poor man's closures
  • Closures are a poor man's object

Using a closure

function validateBetween(message, lower, upper) {
  return function(value) {
    // As part of its closure the returned function has access
    // to the `message`, `lower`, `upper` argument values
    return lower <= value && value <= upper ? makeOk(value) : makeErr(message);
  };
}

const validate = validateBetween('Please specify an integer between 0 and 18', 0, 18);

console.log(validate(18)); // {ok: 18}
console.log(validate(19)); // {err: 'Please specify an integer between 0 and 18'}

versus using an object

class ValidateBetween{
  constructor(message, lower, upper){
    // store argument values in object properties
    this.message = message;
    this.lower = lower;
    this.upper = upper;
  }

  execute(value) {
    // Stored `message`, `lower`, `upper` properties
    // are accessed via `this`
    return this.lower <= value && value <= this.upper ?
           makeOk(value) :
           makeErr(this.message);
  };
}

const validate = new ValidateBetween('Please specify an integer between 0 and 18', 0, 18);

console.log(validate.execute(18)); // {ok: 18}
console.log(validate.execute(19)); // {err: 'Please specify an integer between 0 and 18'}

composition and pipe could be replaced with:

  function composeFns() {
    const fn0 = validateRequired('Please provide a value');
    const fn1 = andThen(validateInteger('Please provide a integer'));
    const fn2 = map(n => Number.parseInt(n, 10));
    const fn3 = andThen(validateBetween('Please specify an integer between 0 and 18', 0, 18));
    const fn4 = map(factorial);

    return value => fn4(fn3(fn2(fn1(fn0(value)))));
  }

  // transform the string to an {ok/err} result
  const transform = composeFns();

or

  // transform the string to an {ok/err} result
  // use an IIFE to initialize function closure
  const transform = (() => {
    const fn0 = validateRequired('Please provide a value');
    const fn1 = andThen(validateInteger('Please provide a integer'));
    const fn2 = map(n => Number.parseInt(n, 10));
    const fn3 = andThen(validateBetween('Please specify an integer between 0 and 18', 0, 18));
    const fn4 = map(factorial);

    return value => fn4(fn3(fn2(fn1(fn0(value)))));
  })();

IIFE (Immediately Invoked Function Expression)