Search code examples
javascriptfunctional-programmingmonadsreader-monad

Readability vs. maintainability: nested functions


What are the pros and cons of each option, considering long-term implications (increasing the number of functions / parameters, other developers taking over, etc.)?

Option 1: removes the need to pass foo and bar to each method, but will create nested functions that are hard to follow.

const myFunction = ({foo, bar}) => {
  const results = []

  const function1 = () => {
    return foo + bar;
  }

  const function2 = () => {
    return foo * bar;
  }

  const res1 = function1();
  const res2 = function2();

  results.push(res1, res2);

  return results;
}

Option 2: you pass the parameters to each function, but remove the nesting, which in my opinion makes it more readable.

const function1 = ({foo, bar}) => {
  return foo + bar;
}

const function2 = ({foo, bar}) => {
  return foo * bar;
}

const myFunction = ({foo, bar}) => {
  const results = []

  const res1 = function1({foo, bar});
  const res2 = function2({foo, bar});

  results.push(res1, res2);

  return results;
}

I would prefer to know how to improve my functional approaches here. Thank you!


Solution

  • The second approach is more idiomatic. In fact, the second approach has a name in functional programming. A function which takes in a shared static value as an input, a.k.a. an environment, is known as a reader.

    // Reader e a = e -> a
    
    // ask : Reader e e
    const ask = x => x;
    
    // pure : a -> Reader e a
    const pure = x => _ => x;
    
    // bind : Reader e a -> (a -> Reader e b) -> Reader e b
    const bind = f => g => x => g(f(x))(x);
    
    // reader : Generator (Reader e a) -> Reader e a
    const reader = gen => (function next(data) {
        const { value, done } = gen.next(data);
        return done ? value : bind(value)(next);
    }(undefined));
    
    // Environment = { foo : Number, bar : Number }
    
    // function1 : Reader Environment Number
    const function1 = reader(function* () {
        const { foo, bar } = yield ask;
        return pure(foo + bar);
    }());
    
    // function2 : Reader Environment Number
    const function2 = reader(function* () {
        const { foo, bar } = yield ask;
        return pure(foo * bar);
    }());
    
    // myFunction : Reader Environment (Array Number)
    const myFunction = reader(function* () {
        const res1 = yield function1;
        const res2 = yield function2;
        return pure([res1, res2]);
    }());
    
    // results : Array Number
    const results = myFunction({ foo: 10, bar: 20 });
    
    console.log(results);

    In the above example, we define function1, function2, and myFunction using the monadic notation. Note that myFunction doesn't explicitly take the environment as an input. It also doesn't explicitly pass the environment to function1 and function2. All of this “plumbing” is handled by the pure and bind functions. We access the environment within the monadic context using the ask monadic action.

    However, the real advantage comes when we combine the Reader monad with other monads using the ReaderT monad transformer.


    Edit: You don't have to use the monadic notation if you don't want to. You could define function1, function2, and myFunction as follows instead.

    // Reader e a = e -> a
    
    // Environment = { foo : Number, bar : Number }
    
    // function1 : Reader Environment Number
    const function1 = ({ foo, bar }) => foo + bar;
    
    // function2 : Reader Environment Number
    const function2 = ({ foo, bar }) => foo * bar;
    
    // myFunction : Reader Environment (Array Number)
    const myFunction = env => {
        const res1 = function1(env);
        const res2 = function2(env);
        return [res1, res2];
    };
    
    // results : Array Number
    const results = myFunction({ foo: 10, bar: 20 });
    
    console.log(results);

    The disadvantage is that now you're explicitly taking the environment as input and passing the environment to sub-computations. However, that's probably acceptable.


    Edit: Here's yet another way to write this without using the monadic notation, but still using ask, pure, and bind.

    // Reader e a = e -> a
    
    // ask : Reader e e
    const ask = x => x;
    
    // pure : a -> Reader e a
    const pure = x => _ => x;
    
    // bind : Reader e a -> (a -> Reader e b) -> Reader e b
    const bind = f => g => x => g(f(x))(x);
    
    // Environment = { foo : Number, bar : Number }
    
    // function1 : Reader Environment Number
    const function1 = bind(ask)(({ foo, bar }) => pure(foo + bar));
    
    // function2 : Reader Environment Number
    const function2 = bind(ask)(({ foo, bar }) => pure(foo * bar));
    
    // myFunction : Reader Environment (Array Number)
    const myFunction =
        bind(function1)(res1 =>
            bind(function2)(res2 =>
                pure([res1, res2])));
    
    // results : Array Number
    const results = myFunction({ foo: 10, bar: 20 });
    
    console.log(results);

    Note that the monadic notation using generators is just syntactic sugar for the above code.