Formatting strings

pub fn get_coord_name(square: Square) ![]const u8
{
    const x: u8 = square_x(square);
    const y: u8 = square_y(square));
    var buf: [3]u8 = undefined;
    return try std.fmt.bufPrint(&buf, "{c}{d}", .{x + 'a', y + 1});
}

I am slowly getting insane of not having strings. Anyway… I think this function is horribly wrong to return a local buffer isn’t it?

I also don’t want this function to fail. Should I write bufPrint catch unreachable?

I also am not capable of reading functions in Zig which have anytype as an argument.
Like “writer”. how on earth can we discover what a “writer” is if it is anytype?


Yes, writing a format function is the easiest way to go and also the most flexible.

// as method of Square, see the link above if you want a freestanding function
pub fn format(
    self: Square,
    comptime _: []const u8,
    _: std.fmt.FormatOptions,
    writer: anytype,
) !void {
    const x: u8 = square_x(square);
    const y: u8 = square_y(square);
    try writer.print("{c}{d}", .{x + 'a', y + 1});
}

In a context where I would rather use some default string then have to handle some error I use std.fmt.allocPrint (confusingly named because it formats, doesn’t print anything) like this:

var buffer: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();

const str = std.fmt.allocPrint(allocator, "{d}", .{id}) catch "<buffer too small>";
// do something with it
fba.reset();
// allocate something else ...

I think the only way is to read the documentation, or look how it is used in a ducktyping manner. For writer normally a std.io.Writer or std.io.AnyWriter is expected or something that has these functions, but technically what ever works and has the functions that are used.

You shouldn’t use bufPrint because you would return temporary memory.
You shouldn’t use unreachable because of this unreachable - error discarding misuse you could catch and return some default string, or catch and panic.

The returning a default string only works well if you use something like a FixedBufferAllocator or an arena, because with that you don’t have to free the allocations individually, which means using something like catch "<buffer too small>" won’t error because nobody will try to call free for that string literal.

1 Like

That looks horribly complicated. All I want is:
fn get_coord(x: Square) []const u8
or something like that.
Some const return thingy without allocation.

Ok the catch I get. Don’t do unreachable.

So the weak list of Zig for me is

  1. anytype (unreadable)
  2. strings missing :slight_smile:

How would that work without allocation?

One alternative would be to pass the get_coord function (format_coord?) a buffer it can use.
If there isn’t a buffer or an allocator, I don’t see how that would be possible, except if you want to declare some buffer as a container variable and use it like a global.

Zig isn’t a garbage collected language so it can’t just create values out of nowhere.

What does that mean?
I don’t see what a string type would change here, this is mostly about having to deal with memory explicitly.

You don’t want to allocate, why exactly?

1 Like

I guess another option would be to create, use it as buffer to format into it and return a std.BoundedArray as a value.

It also has a writer method so you could use std.fmt.format(bounded_array.writer(), "{}", .{5}) for the formatting.

fn formatCoord(x: Square) std.BoundedArray(u8, 10) {
    var b: std.BoundedArray(u8, 10) = .{};
    const x: u8 = square_x(square);
    const y: u8 = square_y(square);
    std.fmt.format(b.writer(), "{c}{d}", .{x + 'a', y + 1}) catch b.clear();
    return b;
}
2 Likes

Yes for example! Thanks. I think I will do something like that.
We can give back structs and ints in Zig. Why wouldnt we be able to return a fixed array of bytes?

You can return arbitrary values, std.BoundedArray is just an array and a length.

But when you have values that contain pointers it gets more complicated, because then you need to worry about who owns what those pointers point to (what its lifetime is, whether it needs to be allocated/freed) and also whether those pointers will continue to be valid when the value is moved around and copied. If the value points to specific parts of itself and gets copied to somewhere else the pointers still point to parts in the old value.

Yes I am aware of these things quite well.
However I got lazy using C# for years at my work, where you don’t give a fluck about these things.

About the bounded array. I am missing a (simple) init function which does not fail.

var x : std.BoundedArray(u8, 8) ...;
x.init(0); // can fail

default initialization, might be changed to an empty value, with decl expressions:

so I guess in the future it might be:

var b: std.BoundedArray(u8, 10) = .empty;

Calling init(0) on a bounded array doesn’t do anything, the slice is already initialized to zero unless you set your bounding array equal to undefined.

In general I would say that Zig is just making you aware that these things can fail (memory allocation is always drawing from some finite pool, initializing the slice length in a bounded array can exceed the backing array). If you are in a situation where you are sure the call won’t fail (e.g. calling init on a bounded array where you are sure the size won’t exceed) you can use catch unreachable or otherwise assert that the error won’t occur. If you are in a situation where failure would mean that the program can’t continue (often the case when running out of memory) then you can catch @panic("...") to indicate that failure should terminate the program.

3 Likes

True, I know init should fail on a too large length. I think
std.BoundedArray(u8, 10) = .{};
should do the trick