10

Are there objective, supportable software-engineering arguments for or against modifying the values of by-value parameters in the body of a function?

A recurring spat (mostly in good fun) on my team is whether or not parameters passed by value should be modified. A couple members of the team are adamant that parameters should be never assigned to, so that the value originally passed to the function can always be interrogated. I disagree and hold that parameters are nothing more than local variables initialized by the syntax of calling the method; if the original value of a by-value parameter is important than a local variable can be declared to explicitly store this value. I am not confident that either of us has very good support for our position.

Is this a non-resolvable religious conflict, or are there good, objective software engineering reasons in either direction?

Note: The question of principle remains regardless of the implementation details of the particular language. In JavaScript, for example, where the arguments list is always dynamic, parameters can be regarded as syntactic sugar for local variable initialization from the arguments object. Even so, one could treat parameter-declared identifiers as "special" because they still capture the passing of information from the caller to the callee.

Joshua Honig
  • 339
  • 1
  • 7
  • It is still language-specific; arguing that it is language-agnostic because "it is so, from my (favorite language)'s point of view" is a form of invalid argument. A second reason why it is language-specific is because a question that does not have universal answer for all languages are subject to each language's cultures and norms; in this case, different language-specific norms result in different answers to this question. – rwong Apr 16 '18 at 15:13
  • The principle that your team members are trying to teach you is "one variable, one purpose." Unfortunately, you're not going to find definitive proof either way. For what it's worth, I don't remember ever co-opting a parameter variable for some other purpose, nor do I remember ever considering it. It never occurred to me that it might a good idea, and I still don't think it is. – Robert Harvey Apr 16 '18 at 16:13
  • I thought this must have been asked before, but the only dupe I could find is [this older SO question](https://stackoverflow.com/questions/2108804/modifying-arguments-passed-by-value-inside-a-function-and-using-them-as-local). – Doc Brown Apr 16 '18 at 20:13
  • 3
    It is considered less error prone for a language to prohibit argument mutation, just as functional language designers consider “let” assignments to be less error prone. I doubt anyone has proven this assumption using a study. – Frank Hileman Apr 16 '18 at 22:10

3 Answers3

15

I disagree and hold that parameters are nothing more than local variables initialized by the syntax of calling the method

I adopt a third position: parameters are just like local variables: both should be treated as immutable by default. So variables are assigned once and then only read from, not altered. In the case of loops, for each (x in ...), the variable is immutable within the context of each iteration. The reasons for this are:

  1. it makes the code easier to "execute in my head",
  2. it allows more descriptive names for the variables.

Faced with a method with a bunch of variables that are assigned and then do not change, I can focus on reading the code, rather than trying to remember the current value of each of those variables.

Faced with that same method, this time with less variables, but that change value all the time, I now have to remember the current state of those variables, as well as working at what the code is doing.

In my experience, the former is far easier on my poor brain. Other, cleverer, folk than me may not have this problem, but it helps me.

As for the other point:

double Foo(double r)
{
    r = r * r;
    r = Math.Pi * r;
    return r;
}

versus

double Foo(int r)
{
    var rSquared = r * r;
    var area = Math.Pi * rSquared;
    return area;
} 

Contrived examples, but to my mind it's much clearer as to what's going on in the second example due to the variable names: they convey far more information to the reader than that mass of r's do in the first example.

David Arno
  • 38,972
  • 9
  • 88
  • 121
  • "local variables .... should be treated as immutable by default." - What? – whatsisname Apr 16 '18 at 15:18
  • 1
    It's pretty hard to run `for` loops with immutable variables. Encapsulating the variable in the function isn't sufficient for you? If you're having trouble following your variables in a mere function, perhaps your functions are too long. – Robert Harvey Apr 16 '18 at 15:23
  • @RobertHarvey `for (const auto thing : things)` is a for loop, and the variable it introduces is (locally) immutable. `for i, thing in enumerate(things):` also doesn't mutate any locals – Caleth Apr 16 '18 at 16:14
  • Of course, local variables should not be immutable, how would you assign any value to them? But you should consider a static single assignment form whenever possible. In a loop that means there is only one place in the code where it is assigned a value, even if it happens dynamically each time through the loop. Another valid exception is an accumulator variable that gets initialized outside the loop and updated within. For variables that get assigned different values in if-then-else constructs use your judgement. A conditional expression operator such as C's ?: might be more readable. – Hans-Martin Mosner Apr 16 '18 at 16:17
  • @Caleth Well, if you want to copy the elements, which might not be a good idea (or even possible). And in your example the problem is actually stretching the simple computation of the return-value out over multiple lines. – Deduplicator Apr 16 '18 at 16:18
  • @Hans-MartinMosner by “immutable variables”, I mean assigned once and not altered. I’ve updated the answer to clarify that. – David Arno Apr 16 '18 at 16:26
  • 3
    This is IMHO in general a very good mental model. However, one thing I would like to add: often it is acceptable to initialize a variable in more than one step and treat it as immutable afterwards. This is not against the general strategy presented here, just against following this always literally. – Doc Brown Apr 16 '18 at 19:58
  • 3
    Wow, reading these comments, I can see that many people on here have obviously never studied the functional paradigm. – Joshua Jones Apr 17 '18 at 03:36
  • @JoshuaJones: no, many people (like me) here simply don't **prefer** to stick 100% to the functional paradigm. Otherwise, I would use Haskell. – Doc Brown Apr 17 '18 at 09:35
  • @DocBrown Apologies. I was not referring to you. Other comments express shock and awe at the idea of immutable local variables. To give my “two cents”, all variables should be treated as immutable unless there is a very good reason to mutate them. One such example is a volatile boolean variable shared between two threads used to indicate termination of an infinite loop. It is difficult to make a service/daemon without at least one mutable variable in an imperative language. It is also more difficult to express loops with counters in languages without tail call elimination, but not impossible. – Joshua Jones Apr 17 '18 at 10:20
4

Is this a non-resolvable religious conflict, or are there good, objective software engineering reasons in either direction?

That's one thing I really like about (well written) C++: you can see what to expect. const string& x1 is a constant reference, string x2 is a copy and const string x3 is a constant copy. I know what will happen in the function. x2 will be changed, because if it weren't, there would be no reason to make it non-const.

So if you have a language that allows it, declare what you are about to do with the parameters. That should be the best choice for everybody involved.

If your language does not allow this, I'm afraid there is no silver bullet solution. Both is possible. The only guideline is the principle of least surprise. Take one approach and go with it throughout your code base, don't mix it.

nvoigt
  • 7,271
  • 3
  • 22
  • 26
  • Another good thing is that `int f(const T x)` and `int f(T x)` are only different in the function-body. – Deduplicator Apr 16 '18 at 16:20
  • 3
    A C++ function with a parameter like `const int x` just to make clear x is not changed inside? Looks like a very strange style to me. – Doc Brown Apr 16 '18 at 20:04
  • @DocBrown I'm not judging either style, I'm just saying that someone who thinks parameters should not be changed can make the compiler guarantee it and somebody that thinks changing them is fine can do so as well. Both intentions can be clearly communicated through the language itself and that is a great advantage over a language that cannot guarantee it either way and where you have to rely on arbitrary guidelines and hope everybody read them and conforms to them. – nvoigt Apr 17 '18 at 07:45
  • @nvoigt: if someone prefers to modify a ("by-value") function parameter, he would simply strip the `const` away from the parameter list if it hinders him. So I don't think this answers the question. – Doc Brown Apr 17 '18 at 09:39
  • @DocBrown Then it's no longer const. It's not important whether it's const or not, the important thing is that the language includes a way to *communicate* that to the developer without a doubt. My answer is *it does not matter, as long as you communicate it clearly*, which C++ makes easy, others may not and you need conventions. – nvoigt Apr 17 '18 at 10:05
1

Yeah, it's a "religious conflict", except that God doesn't read programs (as far as I know), so it's downgraded to a readability conflict that becomes a matter of personal judgement. And I've certainly done it, and seen it done, both ways. For example,

void propagate(OBJECT *object, double t0, double dt) {
  double t = t0; /* local copy of t0 to avoid changing its value */
  while ( whatever) {
    timestep_the_object(...);
    t += dt; }
  return; }

So here, just because of the argument name t0, I'd likely choose not to change its value. But suppose instead, we merely changed that argument name to tNow, or something like that. Then I'd much-more-than-likely forgot about a local copy and just write tNow += dt; for that last statement.

But contrariwise, suppose I knew that the client for whom the program was written is about to hire some new programmers with very little experience, who'd be reading and working on that code. Then I'd likely worry about confusing them with pass-by-value versus -reference, while their minds are 100% busy trying to digest all the business logic. So in this kind of case I'd always declare a local copy of any argument that will be changed, regardless of whether or not it's more or less readable to me.

Answers to these kinds of style questions emerge with experience, and rarely have a canonical religious answer. Anybody who thinks there's one-and-only-one answer (and that he knows it:) probably needs some more experience.

John Forkosh
  • 821
  • 5
  • 11