An introduction to functional programming

Functional programming is a very ambiguous buzzword. But it actually makes a great deal of sense once you understand it. I love functional programming and prefer it to object-oriented and logic programming, so in this post I will try to make it clearer.

What is functional programming?

Functional programming is a style of programming that aims to make programming more suited to the human programmers rather than the machine. It prioritizes programmer time over machine time, which is not to say that functional programs have to be inefficient, but they are generally less efficient than imperative programs. It is a declarative style of programming, which means you say more about what should be done rather than how it should be done. Functional programming emphasizes the ruthless elimination of unnecessary cognitive load. It does this through a combination of several key ideas:

  • Immutable (constant and unchanging) data instead of mutable (changeable and transient) objects
  • Building the bulk of your program out of first-class strictly input-to-output functions (first-class meaning that you are able to assign functions to variables, store them in data structures, and pass them around as arguments etc.) instead of methods or procedures.
  • Distinguishing between code that performs an action and code that simply makes decisions or transforms input data to output data. We isolate and minimise the action-performing parts of our code, putting it all in one place (though of course not all in one function).

Why would you want to do it?

These ideas are all intended to make programs easier to understand, maintain, refactor and test. They enable you to reason locally, rather than globally:

  • Immutable data means you never need to keep track of what time you are looking at a piece of data or worry about another piece of code changing it out from under you. It is a much saner foundation for reasoning. It is also maximally thread-safe.
  • Strictly input-to-output functions are much simpler and more predictable, because they always return the same output for the same input and are not involved with time. You only need to think about the function's input and its body, not about the code it is interacting with. For example, this is a strictly input-to-output function:

    function inc (x) {
      return x + 1;
    }
    

    while this is not:

    function mutatingInc () {
      y++;
    }
    

    The first one is easier to understand because its behaviour doesn't change across time - it only depends on the arguments you pass and the body. The second one depends on the state of the variable y, so it matters when you call it and what other code could affect y too.

  • First-class functions give programmers the ability to manipulate functions just like they do data: there's less of a distinction between them that you must keep in mind. There isn't one rule for functions and another rule for data structures - for most purposes, functions are data. For example, storing functions in a data structure:

    const inc = (x) => x + 1;
    const dec = (x) => x - 1;
    const toString = (x) => String(x);
    const funs = [inc, dec, toString];
    
    for (let fun of funs) {
      console.log(fun(42));
    }
    

    or passing them as arguments:

    [1, 2, 3].map(inc);
    
  • Actions are, of course, why we run programs most of the time and functional programs are no different. We don't totally eliminate actions (or effects) in functional programs - we still have to store things on disk and send network traffic - but we recognise that code that effects change in the external world or other parts of the program is much harder to understand, so we try to minimize it. Using as few effects as possible and all in the same place. Code that performs an action (or has an effect) necessarily requires you to keep in mind the state of the external world and the time the code is being run, which is why functional programs aim to minimize and isolate it. For example:

    function parse (s) {
      return JSON.parse(s);
    }
    
    return stringify (o) {
      return JSON.stringify(o, null, 2);
    }
    
    function main () {
      const input = fs.readFileSync("input.json");
      const output = stringify(parse(input));
      console.log(output);
    }
    

    In this case, main is the only part of the program which performs actions by interacting with the external world. parse and stringify are both strictly input-to-output.

References

  1. Programming Paradigms and the Procedural Paradox.
  2. Composable parts.
  3. Reasoning about code.
  4. Global mutable state.
  5. How to Switch from the Imperative Mindset.
  6. Objects Should Be Immutable.
  7. Decoupling decisions from effects.
  8. Functional architecture is Ports and Adapters.
  9. Simple Made Easy.
  10. Hammock Driven Development.
  11. Design, Composition and Performance.
  12. Effective Programs.
  13. Denotational Design: from meanings to programs.
  14. A Theory of Functional Programming.