Why can't we modify args data in func()?

I’d like to switch h and w when w is bigger than h in the followings.

fn sample(h: u32, w: u32) u32 {
    var tmp: u32 = 0;
    if( w > h){
         tmp = w;
         w = h;
         h = tmp;
     }
     |
}

Zig code can’t modify args of func(). So I have to set new variables every time.
I think it is quite troublesome.

why doesn’t Zig allow that? Will it be improved in the future?

Thanks,
Mikio

1 Like

Hi, the reason is that arguments in a function are const by default, this is an intentional design decision in Zig. There are a lot of reasons why, having const parameters forces you to be explicit about what you are doing in a function, In the case of a very long function you also don’t need to second guess whether the parameter you are about to use has been modified somewhere, I think it also allows for optimization that are typically not available in language where parameters can be changed or aliased.

If you are coming from a language like C, it’s a bit frustrating at first, but once you get used to it, it actually make a lot of sense and it’s a good pattern in my opinion, if you want to modify something, you have to pass a pointer to it.

7 Likes

In language reference section: Functions - Pass by value 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.”

7 Likes

If the args are pointers to the values, you can modify them throgh the pointers:

fn sample(w: *u32, h: *u32) void {
    if (w.* > h.*) {
        const tmp = w.*;
        w.* = h.*;
        h.* = tmp;
    }
}

var w: u32 = 42;
var h: u32 = 41;
sample(&w, &h);
3 Likes

You could also std.mem.swap the variables for convenience. Under the hood this switches pointers.

2 Likes

Seems to me like you could just use @min and @max to get the lower and upper bound:

fn sample(h: u32, w: u32) u32 {
    const l = @min(w, h);
    const h = @max(w, h);
    // ...
}

If these are width and height and you are switching them, then they aren’t really used like width and height in the algorithm and more like bounds, so having to give them new names seems appropriate.

In this particular case, I think if expression with destructuring and new variables is a nice solution:

const std = @import("std");

fn sample(h: u32, w: u32) u32 {
    const l, const u = if (w > h) .{ h, w } else .{ w, h };
    return u - l;
}

pub fn main() !void {
    std.debug.print("sample(20, 10) = {}\n", .{sample(20, 10)});
    std.debug.print("sample(10, 20) = {}\n", .{sample(10, 20)});
}

But something like the following doesn’t work, because of aliasing (it doesn’t introduce a temporary variable and the assignment to a and b are two separate steps), so if you use destructuring+if to swap make sure that the destination variables are independent from the ones in the if:

pub fn main() !void {
    var a: u32 = 20;
    var b: u32 = 10;

    std.debug.print("a = {}  b = {}\n", .{ a, b });
    a, b = if (a > b) .{ b, a } else .{ a, b }; // FIXME this doesn't work, assign to new/other variables
    std.debug.print("a = {}  b = {}\n", .{ a, b });
}

Output:

a = 20  b = 10
a = 10  b = 10

So technically this isn’t a variable swap but declaring two new variables which may be initialized in a swapped way, despite these caveats — used carefully, it is a good solution.

1 Like

Thanks for a reply,
I got it, though I thought the answer.
I used to check how to pass args over to the callee function. Then I think all args are owned by the callee.
So I wonder why Zig is on so strict spec as it is hard for me to code.
My projects were all limited by memory. So I don’t want to prepare new variable for args inside the callee function instead modifying args.

I checked how zig code deploys to assemble code.

fn sample(ar1: u32) void {
    _ = ar1;
}
pub fn main() !void {
    var arg1: u32 = 1;
    sample(arg1);
}

then the followings are codes:

0000000000000000 <_sample.main>:
       0:	55                   	push   %rbp
       1:	48 89 e5           	mov    %rsp,%rbp
     |
0000000000000020 <_sample.sample>:
      20:	55                   	push   %rbp
      21:	48 89 e5             	mov    %rsp,%rbp
      |
0000000000000040 <_main>:
      40:	55                   	push   %rbp
      41:	48 89 e5             	mov    %rsp,%rbp
      |

main() → sample.main() → sample.sample()
I saw medium func sample.main() manages to control memories.

Why Zig set args as const to transfer is:

  1. Zig can’t control args as it passes down two steps and includes memory managing.
  2. Or Zig wants to avoid breaking up memory by callee.
  3. the other

I felt inconvenient to code zig, then I’d like you only to know it.

Thanks,
Mikio

Thanks,
It is a better way to pass pointers to func().
I didn’t mean I am having an issue about args.
But you help me to find some idea, Thanks.

1 Like

Thanks,
I didn’t know std.mem.swap.
The sample code is just an example and I am not confused how to handle it.

Sorry, I didn’t mean I ask for a solution with the sample code.
But I am delighted to find “const u, const l = .{w, h};” which I wanted to do.
it doesn’t work, though.
Error: error: expected ';' after statement

Thanks for your help, anyway,
Mikio

Note this only works in Zig 0.12 dev, not on 0.11 stable.

4 Likes

Ooh, 0.12 it is. I will change it.
Thanks.

I changed 0.12, but some issues happened as the followings.

pub fn main() !void {
    var arg1: u32 = 1;
    var arg2: u32 = 2;
    //const arg1: u32, const arg2: u32 = .{ 1, 2 };
    //var arg1: u32, var arg2: u32 = .{ 1, 2 };
    sample(arg1, arg2);
}
// in case of var arg1: u32, var arg2: u32 = .{1, 2};
sample.zig:7:24: error: local variable is never mutated
    var arg1: u32, var arg2: u32 = .{ 1, 2 };
                       ^~~~

const case is OK on 0.12, and var arg1: u32 = 0; case comes to the same Error.

In 0.11.0 , var arg1: u32 = 0; works correctly.

The 0.12 version seems to inhibit variables in functions, doesn’t it? If so, we couldn’t make re-entrance functions using inside variables.

In 0.12, the compiler was improved so that it can detect if you are declaring a var that is never mutated and will produce an error like the one you’re seeing. This has two positive outcomes for your code:

  1. Your code will better match your intent.
  2. You avoid possible errors caused by the variable being modified inadvertently further down the program’s execution.

In this specific case, if your intention is to modify arg1 and arg2, you declare them with var as you did but you must pass pointers to them in the call to sample instead of the variables themselves which would prevent them from being mutated by passing in only copies. So the following should work:

var arg1: u32, var arg2: u32 = .{ 1, 2 };
sample(&arg1, &arg2);
5 Likes

I see.

If we don’t modify variables, we should set const for them.

Thanks.

2 Likes

This really grinds my gears sometimes. I have some code that creates this long call chain of methods and returns instance temporaries that are arguments to other functions. these can go really deep and they get inlined well. They are rust style iterators that can chain, are lazy, and inline to the exact same thing as boomer loops in release small and almost the same in release fast. Except when I have to break the chain to make a copy. And pass the copy down.

I know the instance is temporary and I don’t need it for longer than the function call receiving it, but it holds the iterator state and zig won’t let me use it non-const even though the life times are perfectly fine. It makes you break the nice Range.filter.map.blah.fold.other chain into multiple lines and make unnecessary copies. I wish I could just use the temp copy.

This is a perfect example of zig’s enforcement of programmer taste and opinion that hurts code generation and lowers performance for some ill-defined notion of clean code.

1 Like

why doesn’t Zig allow that? Will it be improved in the future?

If it’s allowed, then programmers will spend the next 50 years bickering over whether it’s a bad practice to mutate arguments :stuck_out_tongue:

So? It isn’t zig’s responsibility to enforce good programming practices or to enforce taste or some hand wavy notion of “clean code”. The tagline for zig isn’t “we’ll force you to write clean code”. It used to be “we’ll make it easy to get the exact machine code you want” (Kelley literally says this in a number of talks.).

1 Like