Why would it be constant?

Hello, I’m going through Ziglings to try to learn this language. I thought I would just start learning with advent of code, but it’s broken my brain hard enough that I needed something a little more guided.

I’m on 47 and 48_methods2 trying to understand the pointer/reference syntax.

First question, I took Elephants from 48 and instead of pointers wanted to pass in the struct to see if it cloned the object.

const std = @import("std");
                                      
const Elephant = struct {                                               
    letter: u8,                       
    tail: ?*Elephant = null,          
    visited: bool = false,            
        
    // New Elephant methods!    
    pub fn getTail(self: Elephant) Elephant {    
        return self.tail.?; // Remember, this means "orelse unreachable"
    }    
        
    pub fn hasTail(self: Elephant) bool {    
        return (self.tail != null);    
    }    
    
    pub fn visit(self: Elephant) void {    
        self.visited = true;    
    }    
    
    pub fn print(self: Elephant) void {    
        // Prints elephant letter and [v]isited    
        const v: u8 = if (self.visited) 'v' else ' ';    
        std.debug.print("{u}{u} ", .{ self.letter, v });    
    }    
};    
    
pub fn main() void {    
    var e = Elephant{ .letter = 'A' };    
    e.visit();    
    std.debug.print("eleelements {any}\n", .{e});    
}

But instead I get a bizarre constant error

elephants.zig:18:13: error: cannot assign to constant
        self.visited = true;
        ~~~~^~~~~~~~

What did I do? What made it constant?

Second question, in 47, I can’t understand this line

for (&aliens) |*alien| { 

Why do I have to iterate over a reference to the array, when you don’t have to do that with strings, or []u8? And what does *alien mean? My C brain tells me that it’s a pointer, but I didn’t think you could change what you were capturing, you just captured whatever was being iterated over.

I’ve tried changing it to this:

for (aliens) |alien| {   
    heat_ray.zap(&alien);

But now I get a compile error that I’m not mutating the aliens array???

So, on the one hand, if I pass in the struct, no pointers, it’s a constant, and I need it to be mutable. But on the other, I can pass in a reference to a method that mutates and now I’m not mutating.

Please help.

In the visit method self needs to have type *Elephant otherwise it is constant / treated as a immutable value, which means you can’t modify fields.

In Zig parameters are constant/immutable, even pointers, but with pointers you can modify the thing they point to (unless the pointer type itself forbids that).

& on an array, results in a pointer to an array, because arrays have a comptime known size this pointer also has an associated length, this pointer with length information can be automatically coerced to a slice by Zig, if the place where it is used accepts a slice, this is what’s happing here. So &aliens ends up being a slice to the corresponding array, basically a view into the memory that is owned by aliens.

The |*alien| is a payload capture that declares to capture the pointer to the payload, in combination with the previous this means that *alien will be a pointer to elements of the array, because the slice that was created through type coercion also indexes into the array.

Now if aliens is declared as a const the resulting *alien pointer will still be a pointer to a constant value (which would mean that we can only read the value it points to, but not modify that value), so if you want to modify it declare aliens as a var.

I am guessing (has been a while since I have done ziglings) that the zap method wants to destroy or modify alien so to do that you would pass alien to it. (which is already a pointer, because you declare it with *alien in the payload to get a pointer)

Hope this helps and welcome to Ziggit!

Let me know if anything is unclear, or you also could try to explain with your own words, so that we can figure out where you might have a misconception of what is going on.

5 Likes

Thanks for replying!

The &aliens coercing to a slice helps a lot.

You’re correct, zap takes a non-const pointer

const HeatRay = struct {
  pub fn zap(self: HeatRay, alien: *Alien) void {}
}
...
var aliens = [_]Alien{
    Alien.hatch(2),

I probably could have provided that in the first post. :grinning:

~Is there a way to do that capture “manually”? Or is that just the syntax for doing this? It feels wrong to me that I need to get a pointer to a thing I already have in scope mutably if I actually need to mutate it. Is that actually what it’s doing under the hood?~

Okay, it took me forever, but I finally worked it out in my head why you would have to do it this way. If you’re making a new variable (or capture) to reference an original variable, you obviously would have to have a reference to the original variable. Forgive me, I write jenkins pipelines and python for a day job.

So, is this just for bookkeeping in the english readable version of this language? Does this get optimized out in debug or release builds?

The documentation makes it look like that’s what it really is doing under the hood.

test "for reference" {
    var items = [_]i32{ 3, 4, 2 };

    // Iterate over the slice by reference by
    // specifying that the capture value is a pointer.
    for (&items) |*value| {
        value.* += 1;
    }

    try expect(items[0] == 4);
    try expect(items[1] == 5);
    try expect(items[2] == 3);
}

Maybe these are just contrived examples for simplicity. There’s real use cases where this syntax would be useful, but if I have an array of pointers, do I have to do the dereferencing manually?

It looks like yes.

pub fn main() !void {
    var aliens = [_]Alien{ Alien.hatch(3), Alien.hatch(4), Alien.hatch(2), };
    var list = [_]*Alien{ &aliens[0], &aliens[1], &aliens[2], };

    for (&list) |*alien| {
        std.debug.print("{d}\n", .{alien.*.health});
    }
}

So the new question is, does this actually push a new variable on the stack just for bookkeeping in this human written language? Do release builds optimize this out?

I think one thing that might be unusual, coming from a python perspective, is having to handle pointer vs value explicitly.

If the Alien has a update method that expects a *Alien as first parameter, you could call it like this:

var alien = Alien.init();
alien.update();

Here Zig will automatically pass the pointer to the alien, that is one way to use pointers without having to explicitly convert to pointer.

To get a mutable capture that works with a for loop, I think you need a slice.
Alternatively you can write a range-for loop and then use the index to access the array elements. (Performance wise those are pretty close and often result in the same assembly, but you can compare in specific situations)

for(&aliens) |*a| a.update();
for(0..aliens.len) |i| aliens[i].update();

I think in a lot of cases the former is just a shorter version to write the code, however sometimes using explicit indices is better/more flexible for specific algorithms (Can make it easier for the programmer to specify certain algorithms). Also the compiler can emit code that looks quite different, so you can check it by using tools like Compiler Explorer.

In your example you over-complicate things, when your list only contains pointers there is no need to capture a pointer to those pointers as a payload (unless you want to point them somewhere else by modifying the pointers, instead of modifying the thing they point at):

pub fn main() !void {
    var aliens = [_]Alien{ Alien.hatch(3), Alien.hatch(4), Alien.hatch(2), };
    const list = [_]*Alien{ &aliens[0], &aliens[1], &aliens[2], };

    for (list) |alien| {
        std.debug.print("{d}\n", .{alien.health});
    }
}

Zig’s dot-syntax automatically dereferences one level of pointers that is why there is no manual derefence needed in alien.health, this also allows you to call methods using pointers.

The for loop with the array being coerced to a slice, is more a description for us humans for what happens, the compiler doesn’t really have to emit those as separate steps and just has to output code in the end that still gives the same results. Also all of that is more description of syntactic constructs and their semantics, but what the compiler generates is another thing.

The compiler is free to output a lot of different things, I think best would be to use Compiler Explorer and look at a bunch of examples to get a bit of experience of the sort of code to expect from the compiler and then when you try to optimize a specific thing you can take a rough look to see whether it “looks reasonable”, I think that hand wavy measurement can be done with little experience just by looking at a few different examples and getting a feeling for what seems normal and is good enough to spot whether things appear to work correctly.

Then later you can learn more in depth things, if you want to optimize some specific code.

1 Like

Thanks! That’s gotten me over that hump. Thanks for pointing me at Compiler Explorer.

1 Like