Zig NEWS

Cover image for Switching Strings with a Var
David Vanderson
David Vanderson

Posted on

Switching Strings with a Var

I recently wanted to change which static string a var pointed to.

const std = @import("std");

pub fn main() !void {
    var str = "first";
    if (true) str = "second";
    std.debug.print("str: {s}\n", .{str});
}
Enter fullscreen mode Exit fullscreen mode

I got this error:

est.zig:5:21: error: expected type '*const [5:0]u8', found '*const [6:0]u8'
    if (true) str = "second";
                    ^~~~~~~~
test.zig:5:21: note: pointer type child 'u8' cannot cast into pointer type child '[5:0]u8'
referenced by:
    callMain: /home/dvanderson/apps/zig-linux-x86_64-0.11.0/lib/std/start.zig:574:32
    initEventLoopAndCallMain: /home/dvanderson/apps/zig-linux-x86_64-0.11.0/lib/std/start.zig:508:34
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
Enter fullscreen mode Exit fullscreen mode

Zig inferred the type of str to be a pointer to a const array (length 5, zero terminated) of u8. That is the type of string literals in zig.

When trying to assign "second", the types don't match because the length of the arrays is different.

The solution is to specify the type of str to be a slice:

const std = @import("std");

pub fn main() !void {
    // type can also be [:0]const u8
    var str: []const u8 = "first";
    if (true) str = "second";
    std.debug.print("str: {s}\n", .{str});
}
Enter fullscreen mode Exit fullscreen mode

This works because pointers to arrays coerce to slices. Each assignment coerces to the same str slice type.

I've run into this a few times, so hope that writing it up can help people that find themselves in this situation.

Latest comments (5)

Collapse
 
nairou profile image
Nairou

Couldn't you also use a literal switch statement for this, instead of an array?

const std = @import("std");

pub fn main() !void {
    const condition = true;
    var str = switch (condition) {
        true => "second",
        else => "first"
    };
    std.debug.print("str: {s}\n", .{str});
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
david_vanderson profile image
David Vanderson

Yep! And usually that's what I do if the condition is simple. Like Loris said (I hope I'm saying this correctly), the type of the switch statement uses peer type resolution on "second" and "first", which resolves to a slice type.

The original formulation is useful if the conditions for which string to pick are larger and spread-out. Usually for me I want to set str to a default string, and then later in the function change it if needed.

Collapse
 
kristoff profile image
Loris Cro

The article doesn't use arrays, but slices instead. The difference is critical and what you're implicitly doing in your code is defining str to be a slice, so it's another way of relying on the same principle explained in the original article.

Collapse
 
kristoff profile image
Loris Cro

Short and sweet, thanks for sharing!

As a bit of trivia: a few versions ago (0.5 maybe?) Zig didn't have this, so it's a relatively recent introduction that I believe was aimed at making sure that we don't lose statically known type information, which could be leveraged to do more things at comptime (vs having to do them at runtime).

Looking at a string literal as a pointer to an array means that you know the length at comptime, which is information that you lose when representing it with a slice.

Another related trick that Zig does is this:

const buffer = "arbitrarily long string";

const offset: usize = askUser(); // runtime known
const window_size = 5; // comptime known

const window = buffer[offset..][0..window_size];
Enter fullscreen mode Exit fullscreen mode

In this case the type of window is *const [window_size] u8 because even though the offset at which we begin slicing is runtime known, the size of that slice is fixed, and thus representable with an array pointer.

Of course the downside of this approach is that sometimes the compiler will default to types that are less general than what you might expect, but the upside is that you don't lose any type information when doing those transformations, without requiring the compiler to implement an optimization pass dedicated to deducing this stuff (which then wouldn't even be usable in your comptime code).

Collapse
 
david_vanderson profile image
David Vanderson

Thanks for the context!

I love how zig has managed to pull off some great features in ways I didn't expect.

Like you say - slicing a known length is one of them. There was lots of ideas about how to syntactically do that, but directly recognizing buf[offset..][0..len] is beautiful.
github.com/ziglang/zig/issues/15482

Same thing with multi-object for loops. So many options for how to special case the index, but once for(aa, bb, 0..) |a,b,i| was suggested it now feels natural.
github.com/ziglang/zig/issues/7257...