What I Should Known About constants in Zig

I decided to learn how const works.
I hope it will be useful.

The last test struct with reference types really caused me a lot of confusion on a real project.

Part 1:

/// What I Should Known About constants in Zig
///
/// Part 1, scalars.
///
/// I decided to write this program because, coming from High Level programming
/// Languages like Python and Go, I was very confused with the actual behavior.
///
/// Another purpose is to improve how const works in the Zig Language Reference.
///
/// I also added documents from Wikipedia and Mozilla, and some questions.
///
///
/// Notes from https://en.wikipedia.org/wiki/Constant_(computer_programming)
///
/// There are various specific realizations of the general notion of a constant,
/// with subtle distinctions that are often overlooked.
///
/// The most significant are: compile-time (statically valued) constants,
/// run-time (dynamically valued) constants, immutable objects, and constant
/// types (const).
///
///
/// Question 1:
///   const applies to all of the bytes that the identifier immediately addresses.
///   What is the meaning, for scalar values?
///
/// Question 2:
///   Is Zig const the same as the one in C, C++ and JavaScript?
///
/// Question 3:
///   However, unlike in other languages, in the C family of languages the const
///   is part of the type, not part of the object.
///   Does this apply to Zig?
///
///
/// Note from Javascript
///
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
///
/// The const declaration creates an immutable reference to a value.
/// It does not mean the value it holds is immutable — just that the variable
/// identifier cannot be reassigned.
///
/// For instance, in the case where the content is an object, this means the
/// object's contents (e.g., its properties) can be altered.
///
/// You should understand const declarations as "create a variable whose identity
/// remains constant", not "whose value remains constant" — or, "create
/// immutable bindings", not "immutable values".
const std = @import("std");
const Allocator = std.mem.Allocator;
const testing = std.testing;

fn runTimeConstant(value: i32) void {
    // run-time constant (dynamically valued)
    const b: i32 = value * 3;
    b = 3;

    // const.zig:36:9: error: cannot assign to constant
    // b = 3;
    // ^
}

test "compile-time constant" {
    // compile-time constant (statically valued)
    // stored in the global constant data section
    const a = 100;
    a = 10;

    // const.zig:22:9: error: cannot assign to constant
    // a = 10;
    // ^

    //_ = a;
}

test "run-time constant" {
    runTimeConstant(100);
}

test "immutable object" {
    // immutable objects
    // object that has a primitive type and scalar values?
    //
    // Actually, create immutable bindings, not immutable values.
    const b: i32 = 7;
    b = 5;

    // const.zig:28:5: error: cannot assign to constant
    // b = 5;
    // ^
}

test "xxx" {
    // Adapted from https://en.wikipedia.org/wiki/Const_(computer_programming)#D
    var c: [5]i32 = .{ 1, 2, 3, 4, 5 }; // d is mutable.
    const d: [5]i32 = c; // x is a const view of mutable data.
    c[1] = 10;

    try std.testing.expectEqual(c[1], 10);

    d[1] = 100;
    // const.zig:27:8: error: cannot assign to constant
    //   d[1] = 100;
    //   ~~~^~~

    _ = &c;
    _ = &d;
}

test "array" {
    // Example from JavaScript
    // Not available in the Zig Language Reference.
    // Please, add it.
    //
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
    const array: [5]i32 = .{ 1, 2, 3, 4, 5 };

    array[1] = 5;
    // const.zig:48:10: error: cannot assign to constant
    //  array[1] = 5;
    //  ~~~~~^~~
}

test "struct with primitive types" {
    const example = struct {
        x: i32,
    };

    // Fail because the x field has a primitive type/immutable?
    const a = example{ .x = 7 };
    a.x = 5;

    // const.zig:93:6: error: cannot assign to constant
    // a.x = 5;
    // ~^~
}

Part 2

/// What I Should Known About constants in Zig
///
/// Part 2, references.
const std = @import("std");
const testing = std.testing;

test "struct with reference types" {
    const Step = struct {
        power: u32,
    };

    const Workout = struct {
        steps: []Step,
    };

    const step = Step{ .power = 200 };

    // Convert an array to a slice
    var array = [_]Step{step};
    const slice = array[0..array.len];

    const wko = Workout{ .steps = slice };
    // Mutate the data is allowed.
    wko.steps[0].power = 250;

    try testing.expectEqual(wko.steps[0].power, 250);

    // Mutate the data is not allowed.
    // Not sure why.
    //
    // Both `wko` indentifier and `power` identifiers access the bytes that the
    // identifiers immediately addresses?
    const power = wko.steps[0].power;
    power = 300;

    // const-reference.zig:60:5: error: cannot assign to constant
    // power = 300;
    // ^~~~~

    try testing.expectEqual(power, 300);
}

Let me known if same definitions are incorrect and if
some one can add good examples in the Zig Reference.

Thanks

1 Like

I think there’s two sources of confusion here:

  1. Comparing Zig with object oriented languages,
  2. The difference between const for declarations, and const for pointers.

I think you shouldn’t look at Python or JavaScript to understand how Zig works.

Declarations

The const keyword, when introducing a declaration, means that its value is never mutated. That’s it, no trick. Try to replace any old_value, or new_value, or Type here and it won’t compile:

test {
    const declaration: Type = old_value;
    declaration = new_value;
}

What you might have come across that’s confusing is a pointer that does this:

test {
    const pointer: *Type = &old_value;
    pointer.* = new_value;
}

And if old_value itself isn’t declared as const, then it works. But you notice how you’re doing pointer.* = and not pointer =? This is because you’re not mutating the pointer, but its pointee. The address that the pointer represent is still the same, only the bytes at the address are changed.

This is the same for slices, that can be called “fat pointers” they’re a pointer to multiple elements of the same type and the number of these elements. If the slice is declared with const, then you can’t modify the address it points to, nor the number of elements. But you can still modify its elements:

test {
    const slice: []T = ...;
    slice[index] = new_element;
}

Notic that it’s still not slice = but slice[index] =.

So why doesn’t it work with arrays? Because despite sharing syntax with slices, they’re much different, they’re not pointers, they’re the values they hold themselves. So modifying an array’s item is modifying the array itself, unlike slices.

Pointers

You might have encountered “var pointers”, “var slices”, “const pointers” or “const slices”. This doesn’t refer to the pointer itself, but its pointee or rather the capacity of the pointer to modify its pointee. This one is part of the type, not the declaration:

*T // This is a var pointer
*const T // This is a const pointer
[]T // This is a var slice
[]const T // This is a const slice

You can have:

  • a var pointer that’s declared var (var p: *T =): you can modify where the pointer points at, or the bytes that are where it points at.
  • a var pointer that’s declared const (const p: *T =): you can’t modify where the pointer points at, but you can modify the bytes that are where it points at.
  • a const pointer that’s declared var (var p: *const T =): you can modify where the pointer points at, but not the bytes that are where it points at.
  • a const pointer thak’s declared const (const p: *const T =): you can’t modify anything with this.

Note that a var pointer can only points to the bytes of a declaration that’s var itself. Otherwise you wouldn’t be able to guarantee that a const declaration isn’t modified via a var pointer.

Does this clear anything up for you?

5 Likes

One more thing to note, as that seems to be one of your confusion points - there are no reference types in Zig. As opposed to some higher-level languages, all types in Zig are value types, compound or not. So the struct with primitive types fails because you assigned the value of type example to const a, which means you cannot change it afterwards, unless you change it to var a. It has nothing to do with the type of the .x member being primitive – it would work the same if it were another struct.

The only way you get a “reference” in Zig is through pointers.

3 Likes

Yes, it helped me a lot. I learned my self how things works, but your post was more precise.

Only some corrections:

  • 1 I write this post to help programmers using with high level language to learn how const works in Zig , not the other way.

  • 2 As for reference type, I used incorrectly (from C++). Thanks for the clarification.
    Sorry, this was reported by @spiffyk, thanks.

I hope that someone will add these explanation to the Zig
Language Reference, or on a different document on Documentation - The Zig Programming Language.

I think the Zig documentation has people already familiar with low-level concepts in mind. It would be valuable to go more into details for people coming from Python-level languages, or people that are new to programming, but maybe somewhere else. The Doc category here on ziggit maybe?

This site also has docs written by the community Docs
though I think you need to be more active before you can contribute, someone else can do it for you.

However, what you have written is already covered by the language reference, which you linked, the docs here are a supplement not a replacement. And are often more advanced/niche

I don’t remember a section only for const in the Language Reference. There are single examples, but they are scattered.

At least for me, I found it hard to learn this way.