Parameter passing

The following is just kinda thinking out loud, do not pay much attention.

Ok, we have 4 variants for parameter passing:

  • I): fn f(arg: const T), copy value and do not allow to change the copy.

This is what Zig is currently doing, but… with some peculiarities:

  • const modifier is implicit, not good
  • compiler may decide to pass by reference anyway, also not good, despite the semantic is preserved

_

  • II) fn f(arg: T), copy value and allow to change the copy

Are there any cases at all when changing the copy can be a footgun?.. It (the copy) does not exist after returning from a function, what can go wrong?..

  • III) fn f(arg: *T), pass a reference and allow to mutate the pointee.
  • IV) fn f(arg: *const T), pass a reference and do not allow to mutate the pointee.

Variants III and IV are absolutely clear. Variants I/II (with implicit const or even *const) are not, 'arg: T' may sometimes mean 'arg: const T' and sometimes 'arg: *const T'.

One thing that I did like with the way it was working is that it sort of makes the easy thing the pass as const.
So you would have fn foo(arg: T), but then when you change the function to mutate arg then you get a compile error and need to update the signature.

This way I could trust the function signature a bit more. Though this would probably be served better by having the compiler be angry the same way as for var and const.
So if you take something as arg: *T but don’t mutate it, the compiler would complain and tell you to make it const.

But I think with functions it happens fairly regularly that they are part of an api that just requires mutability even when you don’t use it, that’s why I am not really for such a change, for me a function signature is part of library design and the types including const-ness should be picked by the author based on what makes sense for the api.

I guess otherwise you would be required to use _ = &arg; to discard the error about it.

This doesn’t really make sense because zig doesn’t have mutable value parameters anyway.

fn notAllowed(arg: i32) void {
    arg = 5;
}

pub fn main() !void {
    notAllowed(5);
}
temp.zig:2:5: error: cannot assign to constant
    arg = 5;
    ^~~

Language Reference - Pass by value Parameters

Primitive types such as Integers and Floats passed as parameters are copied, and then the copy is available in the function body. This is called “passing by value”. Copying a primitive type is essentially free and typically involves nothing more than setting a register.

Structs, unions, and arrays can sometimes be more efficiently passed as a reference, since a copy could be arbitrarily expensive depending on the size. When these types are passed as parameters, Zig may choose to copy and pass by value, or pass by reference, whichever way Zig decides will be faster. This is made possible, in part, by the fact that parameters are immutable.

I kind of like that function parameters are immutable, even if it just prevents people from doing weird code golf tricks of using them as local variables and mutating them a bunch.

yep

not because of this.
it does not make sense at all.
i mean

real example (C):

int modbus_rtu__fill_subframe(struct data_src *ds, struct modbus_cmd_descr *mbcd,...
...
for (k = 0; k < ds->ncmd_dacq_sqn; k++, cmd++, mbcd++) {
                                               ^^^^^^

pointee remains untouched, we just forward the pointer which is on stack.
absolutely safe.

I think implicit immutable/const for parameters is fine because there is no var variant.

I think if it was variable implicitly that would be worse.

If you have this in zig:

fn add(a: i32, b: i32) i32 {
   ...
}

You can easily expect from the function signature that it wont modify a or b and will return the result.

Even if mutating a or b didn’t affect the call site values/registers it would still be awkward to have mutation there because in zig other mutation requires var, so it would be strange to also have mutation in parameters without var.

Overall I don’t really understand what you are arguing for and what your goal is, it seems like bike-shedding to me that mostly argues to be more c-like which doesn’t really seem like a worthy goal to me. I think immutable parameters are an improvement over C.

I think having to do:

var ptr = mbcd;
...

before you can change the pointer locally is actually a good thing because it clearly separates the incoming parameters from how their values are used locally in calculations.

I don’t see it as a matter of safety, I think it makes the code easier to read you just know that these values can’t change, which means that you can skip to the latter half of function and still assume that they have the same value, without having to scan over the code before and see whether any code has modified it as a local mutable copy.

Because all the cases where there were modifications had to declare their own var for it with a different name. So the reason why I like it, is that it causes less cognitive burden on the programmer to know which variables are being mutated and which ones aren’t.

1 Like

oh! this reminded me incredibly funny case
we were studying Pascal
(you know, procedure argument var modifier means “pass by reference”)
once I saw a code written by a classmate
where he mutated procedure parameter which was not var
i quizzically asked - “how is it possible? it can not work at all, it’s not var!!!, you can not do nothing with it!!!”
but you can :rofl:

I think explicit indication of mutability is better in any case.
I take it back, saying that “immutable arguments does not make sense at all” was really stupid, your add example shows it explicitly.

No, I meant that mutable args can make sense:

void count_down(int counter) {
    for (;counter; counter--)
        printf("%d\n", counter);
}

what is wrong here? nothing.

That is because c doesn’t have a syntactic distinction between var and const declared variables.

Zig has such a distinction, when you have such a distinction on the syntax level then allowing modification makes it syntactically inconsistent with needing var for local variables.

Or said another way, this would be ugly IMHO:

fn countDown(var counter: i32) {
    while(counter > 0) : (counter -= 1) {
       std.debug.print("{d}\n", .{counter});
    }
}
void count_down(const int counter) {
    for (;counter; counter--)
        printf("%d\n", counter);
}
$ gcc ccc.c 
ccc.c: In function ‘count_down’:
ccc.c:5:27: error: decrement of read-only parameter ‘counter’
    5 |     for (;counter; counter--)
      |                           ^~

mutable by default (implicitly), but can be made immutable explicitly.
if you really want an improvment over C, make everything explicit. :slight_smile:

Zig has 2 types of variables one is var and the other is const.

In c there is just one and the type is different.
This whole comparison with C doesn’t make sense, Zig is a different language and thus makes different language choices.

If you want to argue for adding a language feature you need to bring an argument based on the choices that Zig has already done and or argue for change certain decision based on what value that would bring to the language.

All I have seen from you so far is that you have a preference that is different from what Zig currently does, but Zigs current choice seems sensible to me.

There is no reason to add a const to parameters if there aren’t mutable parameters and I also see no reason to add mutable parameters.

I’m not sure I understand why you would want to take a mutable argument but then never mutate it; for a future when you might want to?

This may be a case of library code vs application code. The reason why I would find it nice is that I’ve been working on large c++ codebases where sometimes things are const refereces and sometimes they are not. If they are not doesn’t mean they are actually mutated anywhere in the function. It would be nice if it was explicit.

With that said; how much protection does const give you on a pointer?
If I take a *const Foo where Foo contains a *Bar, would I then be able to mutate Bar?

1 Like

For example when you use function pointers, if somewhere in your api it takes a function pointer which then is used as a callback later, the callback function may be allowed to do modification, but that doesn’t mean that it is required to modify anything. For example the api might be designed to support modification, but you just use it to print something to console without making changes.

You could provide 2 different apis one that is mutable and another that is immutable, but sometimes it makes sense to just have the mutable option when it is manily used for modification.

Yes you can mutate it. Zig doesn’t have const-ness that magically gets added by accessing a nested value through a const pointer. I think that is too much type-system complexity for a language like Zig.

I would be more likely to expect something like that in a language like Rust or Haskell. (Although I don’t have a deep understanding of either)

1 Like

I’ve already said somewhere.

  • mutable variable is a sort of tautology
  • immutable variable is a sort of oxymoron

Come on, programming languages calling these things variables is just common usage of the term within programming languages all over the place.

LaynesLaw:

Layne’s Law of Debate:

  • Every debate is over the definition of a word. Or
  • Every debate eventually degenerates into debating the definition of a word.
    Or
  • Once a debate degenerates into debating the definition of a word, the debate is debatably over.
2 Likes

Also I can easily write a madness like this:

fn madd(a: i32, b: i32) i32 {
    const aa = a + 1;
    const bb = b + 1;
    return aa + bb;
}

yep.

if a subroutine result (returning value) does depend on it’s parameters then obviously it should not mutate them => “function” in mathematical sense => functional programming => everything is immutable => garbage collection.

otherwise do what you want with copies on stack, if you have a pointer to pointer, feel free to realloc it and so on => procedural programming => manual memory management => no hair-raising things like “monads” for i/o, you just have read and write.

That is your opinion, but I don’t know how that relates to zig.

Sure you can say you want to completely redesign the language, personally I am not so interested in that. (Semantics of the language are still evolving and once those are close to becoming finalized when Zig is near 1.0, the syntax could still get an overhaul or redesign if that seems beneficial)

Zig doesn’t need a “function” in mathematical sense, it has imperative procedural things and if you want to, you can write pure functions however there currently isn’t any type checker or keyword that requires you to separate between pure or non-pure code.

It seems you are arguing for having fn for pure functions and proc for procedures or something like that.

I can imagine programs where it would be useful to be able to distinguish between the two, but also a lot of other programs where I don’t really care and Zig’s focus is on imperative code.

I also find it useful to have both const and var within one “body of a unit of code”. (to avoid calling it a function :upside_down_face:)

What are we even discussing here, random nitpicks?

I don’t like that we are hopping around here from one topic to the next, with seemingly no goal, except expressing random opinions and preferences.

If you don’t like the languages syntax you might as well create your new syntax redesign for it, otherwise I don’t know what the point of this is.

I am not really interested in something that is just about complaining, without actually doing something that leads towards changing something that results in some actual useful new value or feature.

If this is just “programmers philosophy circle”, then I am out of here and instead working on my projects.

We are discussing whether a mutability modifier of a function parameter should be explicit/mandatory or not, regardless of a language.

No-no-no, I am toooooo dumb to write a compiler. :expressionless:

remember old style C function declaration?

Actually there are only two (kinda pure) “programming languages”:

  • LISP, everything is in prefix notation, lot of parens
  • FORT, everything is in postfix notation

both are difficult to read and write, but relatively easy to write an interpeter/compiler/VM.

Everything else is a prefix/infix/postfix mix, hence all these “syntax argy-bargy”, so to say.

1 Like