DEV Community

Cover image for Should function arguments be reassignable or mutable?

Should function arguments be reassignable or mutable?

edA‑qa mort‑ora‑y on December 31, 2017

Working on a defect in Leaf I had a question: should function arguments be reassignable within a function? Are they just like local variables, or s...
Collapse
 
lambdude profile image
Ian Johnson • Edited

Personally, I prefer the functional style. I like it because it sets you up for parallelism without a lot of code changes. I also like that it's declarative, rather than imperative. I'm also wary of mutability in general.

I'm starting to think that non-reassignable and read-only should be the default. If I want a mutable argument, I can mark it.

In Rust, they have the mut keyword. Things are immutable by default and you have to explicitly mark something mutable.

Equivalent signatures from Rust are as follows:

fn calc(values: Vec<f64>) -> f64;
fn calc(mut values: Vec<f64>) -> f64;

And taking them by reference:

fn calc(&values: &Vec<f64>) -> f64;
fn calc(&mut values: &Vec<f64>) -> f64;

Another situation that gives me pause is argument sanitization.

For sanitization, you could use variable shadowing inside blocks to explicitly define the scope of the shadowed variable:

float calc( float a, float b ) {
    // a = 1.0
    // b = -2.0
    if (b < 0) {
        a = -a; // a = -1.0
        b = -b; // b = 2.0
    }
    // a = 1.0
    // b = -2.0
    ...
}

calc(1.0, -2.0);

Or, even better, you could use pattern matching:

fn calc(a: f64, b: f64) -> f64 {
  match (a, b) {
    (a, b) if b < 0 => {
       do_something(-a, -b)
    },
    (a, b) => {
       do_something(a, b)
    }
  }
}

With pattern matching and guard clauses, you could make all of your validation clean and have it cover all of the variants without having lots of nested constructs (like loops and conditions).

Pattern matching is super-powerful. Here's a fizzbuzz example using match:

fn main() {
  for i in 1..101 {
    match (i % 3, i % 5) {
      (0, 0) => println!("FizzBuzz"),
      (0, _) => println!("Fizz"),
      (_, 0) => println!("Buzz"),
      (_, _) => println!("{}", i),
    }
  }
}
Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

Thanks for pointing out Rust's immutable by deault. It makes me more comfortable taking the same approach. I handle references and shared values a bit differently, but it appears to be an orthogoanl concept.

Yes, variable shadowing is definitely an option. The one danger it opens is last-shadowed variables, where something early uses the original name, and something later the new name, but both in the same scope.

Pattern matching looks like a clean approach. I don't always like creating separate functions, but I could always use a local function definition, or combine it with lambdas in the simple cases.

Collapse
 
bgadrian profile image
Adrian B.G.

I like the languages where you can specify, and communicate your intention (using pointers, or in your example mutable == I will modify your value). If I had to choose I will clearly put default as read only, the side effects are the root cause of many bugs, and is not intuitive in most of the cases ( a function effect is the return result, not modifying the Input data).

Collapse
 
martinhaeusler profile image
Martin Häusler • Edited

Function parameters should never be re-assignable from within the function. It's just unclean. If the language allows you to assign a default value in case of the absence of a parameter, that's fine, but once the function context opens and the first statement is placed, they should be locked down tight. Don't give people needless room for error.

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

I think I've been thoroughly convinced this is the right direction.

It still leaves open how to handle argument sanitization, or prep-work. But I think that can be handled by convention, either by hiding, or different variable names.

Collapse
 
hrmny profile image
Leah

I prefer rust's approach of immutability by default, which also applies to function arguments, you have to add mut to be able to change it in the function

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

I think I'll be going this way as well now -- I don't have the immutable feature yet, on my infinite todo list.

As to normal variables, it'll be easy to introduce, since declarations now use var implying variable, thus mutable, and functions use defn implying a literal function. I can also add a let which denotes an immutable value.

Collapse
 
rhymes profile image
rhymes • Edited

I agree that function arguments should be immutable, it's the safer option but it's true there's no definitive consensus on this.

Languages that have objects and "pass by reference" allow you to modify a given object as argument and also with that they occupy less memory.

If you have a complex object in memory you can make a function setTheseRelatedFields(object) which might or not return anything but which operates on the given object by address instead of making a copy for the function.

The other side of coin is that sometimes it leads to unexpected consequences (side effects).

I usually have a "semi functional" approach even in languages that are not functional, the code tends to be easier to test and read.

So, if I were to design functions I would make the arguments read only BUT with an option to mutate the passed variable (which is indeed a label standing in for something, be it an integer or a hash).

If the argument is a "simple" value reassign it leads to an error, if the argument is an array changing one of the items leads to an error, the same for hashes and so on.

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

I wonder how much the lack of consensus rests on history though. For languages in the C family it'd be kind of shocking if a new one had differnet semantics.

But I don't want history holding me back. It does seems the consensus on a "safe default" would be immutable and not-rebindable. It's only a default of course, and there must be a way to mark arguments as mutable.

Collapse
 
namirsab profile image
Namir • Edited

IMO,
Short answer: no,
Long answer: noooooooooooooooooooooooooooooooooooo,

I only do this on recursive functions in the internal implementation, as an output argument. And I document this as an exception so everybody knows that's not the default way to go, but it was necessary in that case.

EDIT:
UPS, after reading the post in detail, I saw you were asking about how to make the syntax easier in your language.

I'd just say default is not mutable, and if it mutates, i'd either mark it with out or mutable as you said. I always think of mutable arguments as output arguments, a way to give a function an "empty" box (with a "particular shape", if it's not a primitive value, like an instance of a class) that the function is gonna fill for me.

Collapse
 
codemouse92 profile image
Jason C. McDonald

At work, one of our teams is designing a language for some specific use cases, and one of our top goals is "safety" - many of the users in-house will actually be graphics designers and content developers, neither of whom have extensive programming experience. I've brought this article up to the team for consideration, but I think I agree with your assessment: immutable-by-default helps prevent some rather nasty logic errors.

Collapse
 
michelemauro profile image
michelemauro

In languages that manage memory for you (i.e. Java, or in general garbage collected runtimes), even if imperative, it's better to leave parameters as they are: the compiler may want to do some optimizations on their passing, and won't be able to if you modify them.
If you have a functional language, those optimizations are probably the norm, and you won't be able to do it at all (thus, you have one less problem to worry about).
If you have a runtime where you manage the memory directly (C, C++, or in embedded/IoT situations) there may be some circumstances where you're better off mutating your parameters; but you should be able to recognize them and use them correctly.

Otherwise, is much better to leave your parameters alone: they make the code much easier to reason about.

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

Part of my motivation for the question was a defect in the Leaf compiler (my language). The fix is relatively simple, but it does imply an efficiency lost for calling functions.

As you say, if I limit what can be done with arguments by default, I gain a lot of flexibility in the compiler.