DEV Community

loading...

Example: Imperative vs. Functional

miku86 profile image miku86 ・1 min read

Lately, I got a lot of questions about functional programming.

So here's a small example about the different approaches.

// list of my friends
const friends = [
  { name: "Erwin", drinks: ["beer", "coffee"] },
  { name: "Peter", drinks: ["beer"] },
  { name: "Heidi", drinks: ["water"] }
];

// what do we want to search?
const itemToSearch = "beer";

/***********************************
 * imperative approach
 */

// a place to store the results
let resultImperative = [];

// go over every friend
for (friend of friends) {
  // check if the person drinks this
  if (friend.drinks.includes(itemToSearch)) {
    // add it to the results
    resultImperative.push(friend.name);
  }
}
console.log(resultImperative); // [ 'Erwin', 'Peter' ]

/***********************************
 * functional approach
 */
const resultFunctional = friends
  // check if the person drinks this
  .filter(friend => friend.drinks.includes(itemToSearch))
  // only give me the name
  .map(friend => friend.name);
console.log(resultFunctional); // [ 'Erwin', 'Peter' ]
Enter fullscreen mode Exit fullscreen mode

I like the second approach more:

  • less complexity,
  • increased readability
  • constant level of abstraction

How do you feel about that?

Discussion (25)

pic
Editor guide
Collapse
nicholasnbg profile image
Nick Gray

And importantly (in my opinion), the functional approach practices immutability and has no side effects.

A further explanation:
The methods used in the functional approach (filter and map) all return us a new array, as opposed to editing a single array in place.

The side effect in the imperative approach is in the for loop, where we push values to an array outside the scope of the for loop block. This means the function is not pure, and breaks the rule of referential transparency.
You can google that term (or read any intro to fp blog article), but essentially this makes our code harder to reason about.

Collapse
gypsydave5 profile image
David Wickes

This means the function is not pure

What function? I see no function at all in the imperative code. The imperative code is neither pure nor impure.

Collapse
nicholasnbg profile image
Nick Gray

You can look at the block between curly brackets of the for loop as a function that is run for each iteration of the for statement.

No matter the particular semantics, the side effect point still stands

Thread Thread
gypsydave5 profile image
David Wickes • Edited

But it's still not a function. And the semantics matter here - functional programming is, at root, a paradigm for programming with functions. You can't just say that if it were a function it would have side effects; if my granny had wheels she'd be a skateboard.

We could - we should - really encapsulate both solutions in their own functions. To do so is trivial yet would be revealing: the imperative code, to the consumer of the function, is indistinguishable from the functional code.

What I'm driving at here is that your points about purity, referential transparency and dude effects are irrelevant; they are not the real difference between the two implementations.

I'd perhaps consider whether one 'reads' better than the other, at least as a starting point.

Thread Thread
impguard profile image
Kevin Wu

The point isn't about whether it's literally a function or not.

The point is, the loop breaks referential transparency, which means that you have opened up the possibility for a class of errors that misuse it.

This example is trite, but if this were a bigger codebase and a larger loop, you can imagine the potential errors I've allowed to be introduced when someone mutates something in that loop inappropriately.

One of the ideas of any style of programming is what class of errors you allow. Static typing reduce a class of errors that dynamic typing allows. Each reduction typically comes with a cost, whether it be in boilerplate or mental know-how. In this example, one can argue that reducing the class of errors that functional impurity allows is worth the cost of learning HOF and expression based programming, especially since it gets you a lot of free things like expression based testing and debugging, easier maintenance, less risky of bugs, etc.

You don't have to agree that the trade-off is worth it, but the point is much more than just "it doesn't literally use the function keyword, so the points don't matter".

Thread Thread
nicholasnbg profile image
Nick Gray • Edited

As Kevin mentions above me, this example is contrived so doesn't show the full extent of how, imperative, effectful, mutable code opens you up to a whole range of errors that can be super tricky to debug.

I still believe the for loop is an example of side effects regardless of if is strictly a function itself. If you draw a circle around the block inside the curly braces, you can clearly see that it has to reach outside that circle to mutate an array, it is changing the world around it.

And yes, if you encapsulate both versions in their own function, to their consumers they do the same thing. Even more reason for FP approach IMO, if you knew functions were written in a immutable side effect free fashion,you wouldn't have to look at the implementation (especially if static types are present) to double check it's not wiping your hard drive or sending your data to some foreign API.
(Exaggerated but the point stands)

Thread Thread
gypsydave5 profile image
David Wickes

Whew - some fine responses!

And yes, if you encapsulate both versions in their own function, to their consumers they do the same thing.

Let's do this!

here's the one with imperative guts:

function imp(friends, itemToSearch) {
  let resultImperative = [];
  for (friend of friends) {
    if (friend.drinks.includes(itemToSearch)) {
      resultImperative.push(friend.name);
    }
  }

  return resultImperative
}

and here's one with functional guts

function fun(friends, itemToSearch) {
  return friends
    .filter(friend => friend.drinks.includes(itemToSearch))
    .map(friend => friend.name);
}

Now we've got some structure into this (just to keep Dijkstra happy) we can perform a fair comparison.

Both of these functions are 'pure'.1 They are both stateless, returning the same value for the same set of arguments. They do not mutate state - they have no effect upon any value outside of the function.

to their consumers they do the same thing

They don't only 'do' the same thing, they are indistinguishable. Other than the imperative one is faster. You do the maths.

The point is, the loop breaks referential transparency, which means that you have opened up the possibility for a class of errors that misuse it.

The loop does break referential transparency. But the solution as a whole doesn't. Do you know what else breaks referential transparency:

  .filter(friend => friend.drinks.includes(itemToSearch))

Yup, that there lambda don't get itemToSearch as an argument... I guess it's not functional. But the solution as a whole is.

Anyway, obligatory xkcd comic:

White Hat questions Cueball's faith in functional programming. Cueball responds saying, "Tail recursion is its own reward."

Functional programming combines the flexibility and power of abstract mathematics with the intuitive clarity of abstract mathematics.


  1. schoolofhaskell.com/school/startin... 

Thread Thread
nicholasnbg profile image
Nick Gray

Nice, thanks for the great discussion, and I agree with your points, as always nothing is black and white, both paradigms have benfits, and yes FP can be daunting, I'm very lucky to have landed my dev job at a company with a strong FP community so have a lot of people around me to learn from.

One question, in regards to the filter function not being referentially transparent, could you make it so like this?:

function fun(friends, itemToSearch) {
  const friendFilter = (friend) => friend.drinks.includes(itemToSearch)
  return friends
    .filter(friend => friendFilter(friend))
    .map(friend => friend.name);
}
Thread Thread
gypsydave5 profile image
David Wickes • Edited

Classy! But It still involves a non-referentially transparent function.

const friendFilter = (friend) => friend.drinks.includes(itemToSearch)

itemToSearch is still closed over by the lambda (i.e. it's not one of its arguments).

Before I get into this - I really wouldn't do this in real life! It's fine that the lambda closes over itemToSearch. I'm only doing this because it's fun and I enjoy FP :D.

But since you asked... one way of handling this - if you really, really only wanted pure functions everywhere - would be to pass itemToSearch as an argument and return the friendFilter function, essentially currying the function:

function fun(friends, itemToSearch) {
  const friendFilter = (item) => (friend) => friend.drinks.includes(item)
  const filterItem = friendFilter(itemToSearch)

  return friends
    .filter(friend => filterItem(friend))
    .map(friend => friend.name);
}

Lambda and currying make a great way to add data into your functions as and when its needed.

small refactor:

function fun(friends, itemToSearch) {
  const friendFilter = (item) => (friend) => friend.drinks.includes(item)
  const predicate = friendFilter(itemToSearch)

  return friends
    .filter(predicate)
    .map(friend => friend.name);
}

stupid why-oh-why refactor:

const fun = (friends, itemToSearch) => friends
    .filter((item => friend => friend.drinks.includes(item))(itemToSearch))
    .map(friend => friend.name);

Ridiculous, please-make-it-stop refactor

const fun = (friends, itemToSearch) => 
  (predicate => friends.filter(predicate).map(friend => friend.name))
  ((item => friend => friend.drinks.includes(item))(itemToSearch))

If you enjoy this madness, might I recommend reading some books about the Scheme programming language.

Thread Thread
nicholasnbg profile image
Nick Gray

Haha nice, I actually work with Scala day to day and a lot of code ends up looking similar to the second last example (with types).

Thanks again mate :)

Thread Thread
miku86 profile image
miku86 Author • Edited

Thanks for sharing all your insights (all of you), I will have a closer look at them.

Collapse
macsikora profile image
Maciej Sikora • Edited

Point you mention are arbitrally subjective and many will argue. FP has just different principles, expressions over statements, values over variables. If something is readable or not depends more from familiarity. And this kinda argument is brought more by against FP devs who's say that loops are more readable then HOF. That is why I don't like such kind of arguments against both sides.

Collapse
aminmansuri profile image
hidden_dude

There's a bit of a terminology issue here with the word "imperative". There's a lot of inconsistency associated with this term. Check out this Wikipedia article:

en.wikipedia.org/wiki/Imperative_p...

I think what you're trying to express is the contrast between PROCEDURAL and functional.

Imperative programing has to do with viewing the computer as a Von Neumann model, where coding is based on mutating memory addresses. In that sense, Functional can be considered as distinct because, rather than mutating memory, in a pure functional language (like OCaml) you never change memory, but rather you create a new value based on another.

But in another sense, many "functional" languages are still very much imperative, because coding is about running a sequence of statements in order, and memory mutation is still allowed.

I think what you're comparing here is a structured programming / procedural approach vs a functional one. In the former you continuously mutate the same memory addresses, and in the second you use a more "pure" transformational approach without secondary effects (that presumably has advantages, especially in the face of concurrency and mathematical purity).

I rather think of the term "imperative" as opposed to "declarative". Declarative languages like SQL, Prolog, and Haskell don't necessarily execute "in order" or in a fixed way. In fact, we're encouraged to not even think about how its implemented.

In an imperative language such as Javascript or Java or Lisp or Clojure, the steps the system will take are obvious an transparent to the programmer (for better or for worse). But in a declarative language like SQL we state the problem and let the solver come up with the solution. The solution may be computed in any number of different ways.

So, I believe that a more consistent nomenclature is:

Imperative vs Declarative

Procedural vs OO vs Functional

Different dimensions discussing different things. But as I said, these terms are used a bit inconsistently.

Collapse
aminmansuri profile image
hidden_dude • Edited

In a practical sense, Functional programming "names your loops".. For example:


new_list = []
for elem in old_list :
.....new_list.append(xfer(elem))

In FP we call this MAPCAR, MAP or COLLECT.

REDUCE is a generalization of sums. REMOVE-IF-NOT, FILTER or SELECT is a generalization of filtering loops.

By thinking at a higher level of operations code is supposed to look more self-explanatory, and programmers are encouraged to think more globally. This is particularly useful for solving complex graph problems basing yourself on DFS and other algorithms, or for parallel programming as in OpenMPI's SCATTER, GATHER and COLLECT. And set theory gives us things such as UNION, INTERSECTION and DIFFERENCE.

Instead of focusing on the minor mechanics of a transformational operation, we can compose higher level transformations and express them more succinctly.

Collapse
johnkazer profile image
John Kazer

I like a functional approach because I prefer the way of thinking and sense of security of immutability and pure functions. It has taken some effort to learn the basics but was worth it. Unexpectedly I find I just feel happier solving problems functionally and declaratively, even recursion feels better than a for loop which I never thought I'd say. Moving to virtual DOM helped too!

Collapse
rohansawant profile image
Rohan Sawant

This demystifies things considerably. Nice! 🔥

Collapse
wagslane profile image
Lane Wagner

Not a point on functional vs imperitive, but I prefer more familiar syntax to language specific jargon. Map, filter, etc require knowing what they mean or looking up documentation. Loops are universal

Collapse
lambdafalcon profile image
falcon

Loops are universal, and because they are, you need to read the code to understand whether the loop is searching for an element in a collection, mapping a collection to another, or reducing a collection to something.

Instead, with map/filter/reduce, the reader immediately understands what the intent of the code is.

A simple example: some "functional" code to sum up the lengths of a list of strings would read as follows:

const sum = (a, b) => a + b;
const getLength = (str) => str.length;

const strings = ['foo', 'bar', 'hello', 'world'];

const totalLength = strings
    .map(getLength)
    .reduce(sum);

Which is, in my opinion, much more understandable at a glance that the (one or two) generic for loops required to do this in imperative style.

And in any case, your argument is not really strong: there are countless functions that people know and use without the need to read the documentation every time, and this is because they remember what the functions do.
So, refusing to use map/filter/reduce in JS in 2019 is really just laziness and stubbornness against the more modern API.
Often, people who don't understand the functional array API are still using var instead of let and const.

Collapse
wagslane profile image
Lane Wagner

It's not a lack of understanding that I struggle with, it's the same problem I have with relying heavily on a framework imo. I would rather take the extra one line of code to be very explicit about what is happening, rather than rely on functions that are es6 specific. Const and let are totally different, they are keyword syntax, and provide functionality that didn't exist before.

Thread Thread
lambdafalcon profile image
falcon

The functions map, filter and reduce are definitely not es6 specific; and ES6 is anyway almost 5 years old.
They exist in almost every major language; in Java they are in the Streams API, in Python they are omnipresent, often used as list comprehensions, in Scala they are there, in Rust they are present in the standard library.

And by using them, you are extremely explicit about what is happening: if you use map, you are doing that and only that. With a for loop, instead, you might be using the for loop to map elements, or to filter them, or to reduce, and one would have to read the details to understand what exactly you are doing. This can take time, especially in larger applications.
Therefore, I argue that e.g. saying map instead of filter is more specific than writing a generic for loop that looks almost the same in the two cases.
It also results in less lines of code, and respects immutability, which in turn helps reducing the average number of bugs in the application.

Collapse
bumasoft profile image
bumasoft

I would rename this to “Imperative vs Declarative”. Functional programming can be either imperative or declarative.

Collapse
aminmansuri profile image
hidden_dude

yes.. But using MAP and REDUCE is not truly declarative in languages such as Javascript. But its OK to imagine it as such.

If you're composing "pure" functions to solve problems, then you're using a functional approach. Whether you do it in a declarative or in an imperative language.

Collapse
srebalaji profile image
Srebalaji Thirumalai

Can you also mention the time and space complexity of both methods? I think the functional approach has higher time complexity than imperative ?

Collapse
impguard profile image
Kevin Wu

In non functional languages like JS here, FP is for sure slower.

Whether that's important in your space is up to you. I've found that thinking about performance first over code quality has resulted in less performant code for other reasons.

And you can achieve asymptotically equally performant code using libraries or languages built for this style of programming. Given that functional programming isn't really taught, however, that's not the norm.

Collapse
lambdafalcon profile image
falcon

When talking about complexity we always mean asymptotic complexity.
Since the algorithms are essentially the same, time complexity is the same.

Instead, if you want to talk about performance, then yes, there is a difference.
However, developers should write code for other developers, not for the machine.
If it is easier to read, understand, and extend, it is worth the slight performance overhead.
And anyway, in most cases there is a ton of underlying framework code impacting performance already, so things like using a map instead of a for loop will not matter.