DEV Community

Cover image for Mutation is ok
Pragmatic Maciej
Pragmatic Maciej

Posted on

Mutation is ok

The phrase - "mutation" started to have almost negative connotation in our programming community. It's like something wrong to mutate. As if we mutate we are not writing our beloved functional code anymore. Is mutation so evil? Or maybe some misuses are? Let's dive in.

Statements

Functional programming is commonly about programming with using expressions only, and expression is something which evaluates to a value, therefore it has no side effects. But what if a function locally uses imperative statements, what can go wrong?

// expression based
const userName(u: User)  => u.secured ? "No access" : u.name;

// statement based
function userName(u: User) {
  if (u.secured) {
    return "No access";
  } else {
    return u.name;
  }
}
Enter fullscreen mode Exit fullscreen mode

Ok so probably most of you don't see issues with both options, even though in the second I have used statements. We can then use statements in functional programming. I hope we agree at this point.

Note I am deliberately using arrow function for expression based as arrow function is an expression, where function declaration is a statement.

Local mutation

// declarative / expression based
const removeInactive (users: User[]) => 
  users.filter(user => user.active)

// imperative / statement based
function removeInactive (users: User[]) {
  let newUsers = []
  for (u in users) {
    if (u.active) {
      newUsers.push(u)
    }
  }
  return newUsers;
}
Enter fullscreen mode Exit fullscreen mode

Now the code is more controversial. Declarative code is short, has no variables, it is also more readable for anyone having fp basics. The imperative one is longer, has variables and has local mutation.

For sure I would pick the first option if somebody would ask me - which code is better for you. But, if somebody has written the second, then does it create any problems for our code-base?

Looking from helicopter view on how functions behave, both are

  • referential transparency (for the same input gives the same output)
  • have no side effects

Looks like from the interface perspective these functions are equivalent, both functions are pure mathematical functions. If some developer would write imperatively such function, and put it into some library, nobody would notice, and even nobody would care. And that is the thing. What is inside this function is - implementation details.

Note Still if we consider comparison of local complexity there is a clear winner.

Reduce it

Many say that reduce can be overused, and many times when we use reduce code is just over-complicated. In my experience I have never seen reduce as a problem, but if we start to use it as a hammer, it can start to be a problem.

// reduce version - declarative
const intoCSV = (users: User[]) => 
   users.reduce((acc, user) => {
     const prefix = acc.length === 0 ? "" : ",";
     return acc + prefix + user.name;
  }
  , "");

// for..of version - imperative
function intoCSV (users: User[]) {
  let csv = "";
  for (const user of users) {
    const prefix = csv.length === 0 ? "" : ",";
    csv = csv + prefix + user.name; 
  }
  return csv;
}

Enter fullscreen mode Exit fullscreen mode

In terms of input -> output both versions of intoCSV are again the same. These are pure functions even though inside of the second there are statements and variables. But readability argument is not so obvious as in previous examples. The reduce version is not far better. I would say there is no clear winner here.

Copy or not to copy

// reduce version - declarative
const intoUsersById = (users: User[]) => 
   users.reduce((acc, user) => ({...acc, [user.id]: user })
  , {} as { [k: number]: User });

// for..of version - imperative
function intoUsersById (users: User[]) {
  let byId: { [k: number]: User } = {};
  for (const user of users) {
    byId[user.id] = user;
  }
  return byId;
}
Enter fullscreen mode Exit fullscreen mode

Next example shows another issue with the declarative version. This is also common, overusing copying of the structure. In the example we make a shallow copy of our final object during every "iteration". This has a real impact on the performance. Of course not as we should be very afraid, but if our collection is processed by node.js/deno we should worry. Some more thoughts about this aspect you can find in my previous article Data mutation in functional JS.

Still you should not be worried to make a mutation here. Its local not shared variable, nobody can use it until you will be done. Mutation is allowed and preferable in this case.

Note my point is here not against reduce. I like and use reduce, but we need to understand the trade-off. Also we can make mutation and use reduce.

Why are people saying mutation is wrong?

First of all people are saying many things, and not all of them are correct ­čśë. Second of all, we have currently hype for FP, the hype is so strong that some people just go into dark corners of the paradigm, and claim FP supremacy even in places where there are no arguments to prove it. And I am also fan of FP, but I also follow common sense.

And yes if we work with expression based language like Haskell, Elm, PureScript, then we write only expressions and pure functions, but this is exactly how these languages were designed.

In multi-paradigm languages like TypeScript, JavaScript, Java, C# and so on, we should understand that language is not made for some concepts, and also that there are statements and mutations. If we know when it's safe to use that, everything should be ok.

But when mutation is really wrong?

Everything which does not belong to the function should not be mutated. By "belong" I mean something created inside the body of the function. In other words, we can mutate our local variables, but we should avoid mutation of external state and input arguments. If we follow the rule then mutation should not bite us.

And this concept is commonly known, even Rust language made from this its core concept. Take a look at borrowing.

Summary

Imperative core, functional shell.. wait what? Yes, so common architecture pattern is "Functional core, imperative shell", and it is about putting side effects to the border. I am starting some mini-series about exactly making such imperative shell here. But what we are doing in this article is reverse of that, we use micro-mutations in order to produce some data inside pure functions. And don't be afraid to do so, until outside the function is referential transparent everything is good.

If you like this article and want read more from me, follow me on dev.to and twitter.

Discussion (15)

Collapse
kristijanfistrek profile image
KristijanFištrek

Can't agree more! We should think logically about our software logic and not follow some patterns blindly. ­čśî

Collapse
miketalbot profile image
Mike Talbot

Nice syntax and a feeling of "doing the right thing" lead to very inefficient code when it comes to immutability. This is a very nice article - I took a look at the costs of immutability in tight loops and code myself (which is another lens on this subject):

Collapse
phlash909 profile image
Phil Ashby

My take on this - if the 'feels right' / 'elegant' solution isn't performant, then the language may have a problem - one of the reasons I like Python, the 'right way' is usually pretty quick too!

Collapse
miketalbot profile image
Mike Talbot

There is certainly a lot more sugar in JavaScript these days, and it's not always good for you ;)

Collapse
slifin profile image
Adrian Smith

Most modern languages with immutability implement it with persistent data structures, ClojureScript for example, deep efficient immutability is the default

The closest thing you have to persistent data structures in JS is ImmutableJS, though honestly once you've cobbled together rambdajs, react, ImmutableJS, Google Clojure Compiler you should just use ClojureScript

Collapse
greenroommate profile image
Haris Secic

OO is OK.
FP is OK.
Java is OK.
Kotlin is OK.
Everything is OK if it gets the job done heheh.
Just saw your series and made some suggestions

Collapse
adnanbabakan profile image
Adnan Babakan (he/him)

Java is literally not OK. xD
(just kidding)

Collapse
greenroommate profile image
Haris Secic

There's always one xD

Collapse
phlash909 profile image
Phil Ashby

Couple of thoughts while reading this excellent article Maciej:

  • Functional core, imperative shell: is a whole-application design ethos that you are not suggesting we break (thanks!), which differs in scale from your examples here of perfectly legitimate mutation within a method/function, however...
  • Internal mutation falls apart if others can attempt to extend/get-inside a thing, this largely impacts next-scale up entities such as classes or libraries that expose the state.
  • Ensuring that referential transparency is maintained is harder to prove when mutating anything, neat/efficient it may be, but proving it's safe for missile guidance may be harder :)
Collapse
macsikora profile image
Pragmatic Maciej Author

Hi Phil,
Thanks for the comment.

Internal mutation falls apart if others can attempt to extend/get-inside

Extending function is not a common practice in FP. We more should strive for small replaceable units. Instead of adding new use cases to the unit we should compose it with another. Until we keep them small even imperative code should not be any issue.

Ensuring that referential transparency is maintained is harder to prove

Again the same counter argument from my side. Until we keep functions small with one responsibility, nothing like that should happen.

Collapse
richytong profile image
Richard Tong

Be wary of people who tell you you are just plain "wrong". Usually they just wish you were wrong. I fully endorse your views in this article - they are telling of someone who likes functional programming and wants to get sh*t done.

Collapse
martinmuzatko profile image
Martin Muzatko • Edited

Everything which does not belong to the function should not be mutated.

I have experienced that some devs love to write huge functions that span multiple pages. Which is something that is not ok for me. It might be convenient to just add to an ever increasing scope to never juggle state from one function to another, but it quickly becomes opaque which variable is mutated at which point or re-used somewhere else along the pages of code. So the rule of "mutation is ok within the same function" depends on how absurdly you stretch the rules, or in that case the function.

If it is compact, then maybe. But most of the time I prefer a re-usable function that you can apply to a map instead of writing a for loop that collects some data.

FP has the promise of a declarative style that should make it more readable. Sometimes that is true, when you know what the words mean:

const isOddNumber = n => n % 2
const filterOddNumbers = numbers => numbers.filter(isOddNumber)
Enter fullscreen mode Exit fullscreen mode

or better:

const filterList = fn => arr => arr.filter(fn)
const isOddNumber = n => n % 2
const filterOddNumbers = filterList(isOddNumber)
Enter fullscreen mode Exit fullscreen mode

It needs some setup, but once you have the function assembled, you don't need as much boilerplate than with if and for statements EVERY TIME you use it. that is the entire point of FP. You can compact a lot of behavior into one function, give it a fitting name and re-use it as much as you want.

vs

const getOddNumbers = numbers => {
    let result = []
    for (const n of numbers) {
        if (n % 2) result.push(n)
    }
    return result
}
Enter fullscreen mode Exit fullscreen mode

Ok, to be fair, we can apply at least the isOddNumber function within the if condition.
What bugs me the most, is that all these imperative statements (if, switch, for) always come with their own boilerplate. If you work on the result of getOddNumbers you have to do a for loop once more, which adds to the boilerplate.

I know why I do FP and I know why state mutation and statements are most oftenly not ideal to express your desired computation or behavior.

Collapse
macsikora profile image
Pragmatic Maciej Author • Edited

I would just answer - it depends. We state our opinions, in contrary to you I see also problem in extending dictionary for such simple cases, in my experience creating so many one liner function can end in ton of functions which nobody will use because they just prefer to write them inline. The example which shows that is famous id function, I personally prefer to just use x => x than import id from magic utils, it is just too simple.

As having functions and composing them is a good thing, we need to take into consideration that more one liners mean also more jumping over the code. I was writing Elm for a while and I found myself in preferring using inline if expressions, or inline case of instead of constantly creating new function. As I loose eye contact with what is really going on, and having many functions force me to jump through them. And no, readable name not always is enough to say that we never want to see what is going on.

Also when I see this:

const filterList = fn => arr => arr.filter(fn)
Enter fullscreen mode Exit fullscreen mode

It is ­čÜę for me, as this is nothing more like duplicating the already existing tools, such function in my opinion has no value, and even worst introduce smth which is additional in the codebase. So for me your first implementation:

const isOddNumber = n => n % 2
const filterOddNumbers = numbers => numbers.filter(isOddNumber)
Enter fullscreen mode Exit fullscreen mode

is better, and probably I would even wrote that in that way, but this example is simplified and doesn't shows the whole picture.

I don't force anybody to use mutation, I am just saying that it is fine to mutate locally, it is fine to use "for" loops, nothing wrong with that. Of course if we just reimplement map or filter then we should think twice. But if function makes more than that, transformation for example needs reduce then reaching for loops is fully ok. I don't feel pain looking at statements, also "boilerplate" can happen with expressions, Elm is famous for having a lot of boilerplate, even though it has no statements at all.

And yes I prefer expressions over statements, but I in the same way I hate ternary expression and prefer if statement, as it is just more readable. Python has <expr1> if <conditional_expr> else <expr2> and this is awesome. But in JS, nah, ternary is really not nice.

Collapse
nosknut profile image
nosknut • Edited

Generally i choose whatever code is more readable. Readabillity over performance is generally a good rule. Unless you get a meaningful performance drop that actually impacts the application, or alredy when making it know that the code will be executed constantly many times a second, readabillity is king in my book. Generally i prefer the code i dont need to understand. If the method signature is enough, i prefer not looking too much into the implementation. If the implementation is nice clean english such as users.filter, i won't have to give the rest a detailed read to feel comfortable using or changing it. You should understand the code enough that you arent putting gibberish into your codebase of course, however the best code is the one you can understand without reading it.

Collapse
djuric profile image
┼Żarko ─Éuri─ç

FP supremacy vs. left-wing OOP