Slicing a tuple or popping the last element

Hi! I was learning Zig for fun and couldn’t find an easy way to slice a tuple (anonymous struct). I see there is Allow slicing of tuples · Issue #4625 · ziglang/zig · GitHub but until that’s resolved, is there a hack to do this? I am actually interested in dropping the last element of the tuple so maybe there is a function for this instead?

Hey @mohamed82008, welcome to the forum!

I believe this should do what you’re looking for…

const std = @import("std");


fn SubTupleInfo(comptime T: type, comptime N: usize) struct { type: type, len: usize }{
    const fields = std.meta.fields(T);

    // you could decide to return an empty tuple here instead...
    if (comptime fields.len < N) {
        @compileError("Trim size larger than origin tuple.");
    }

    const M = fields.len - N;

    return .{ 
        .type = @Type(.{
            .Struct = .{
            .layout = .Auto,
            .fields = fields[0..M],
            .decls = &.{},
            .is_tuple = true,
            .backing_integer = null
        }}), 
        .len = M,
    };
}

pub fn trimTuple(tuple: anytype, comptime N: usize) SubTupleInfo(@TypeOf(tuple), N).type {

    const sub_info = SubTupleInfo(@TypeOf(tuple), N);

    var sub_tuple: sub_info.type = .{};

    inline for (0..sub_info.len) |i| {
        sub_tuple[i] = tuple[i];
    }
    return sub_tuple;
}


pub fn main() !void {

    const x = .{ 42, 43, 44 };

    // trim the last two elements off the end of x
    const y = trimTuple(x, 2);

    std.debug.print("\n{}\n", .{ y });
    std.debug.print("\n{}\n", .{ y[0] });
}

We’re building a new tuple with the remaining number of trimmed fields, copying over the values, and returning it.

If you want to write it like a traditional “pop” function (where the last element is removed and the returned), you could do something similar… like so…

const std = @import("std");

fn PoppedTypeInfo(comptime T: type) struct { type: type, idx: usize } {
    const fields = std.meta.fields(T);

    if (fields.len < 1) {
        @compileError("Cannot pop from empty tuple.");
    }

    const I = fields.len - 1;

    return .{ .type = fields[I].type, .idx = I };
}

pub fn popTuple(tuple: anytype) PoppedTypeInfo(@TypeOf(tuple)).type {
    const pop_info = PoppedTypeInfo(@TypeOf(tuple));

    return tuple[pop_info.idx];
}

pub fn main() !void {

    const x = .{ 42, 43, 44 };

    const y = popTuple(x);

    std.debug.print("\n{}\n", .{ y });
}

It’s important to note that this doesn’t modify the existing tuple. It makes a new, smaller tuple or it copies the last element off the back of it.

You could combine these two methods where you return a struct with the last element as a data member, and a smaller tuple as another data member - that would begin to mimic a shrinking type. Tuples are just types though - shrinking them isn’t quite like shrinking a dynamic container (which just reduces a capacity variable or moves a pointer back).

Hope that helps :slight_smile:

5 Likes

Thanks @AndrewCodeDev. That’s a fun little hack that taught me about @Type. I appreciate your help.

1 Like

Trying to extend your solution to work with a starting index > 0 gives me an error. Any idea why this doesn’t work?

fn SubTupleType(comptime T: type, comptime first: usize, comptime lastp1: usize) type {
    const fields = std.meta.fields(T);
    return @Type(.{ .Struct = .{
        .layout = .Auto,
        .fields = fields[first..@max(lastp1, first)],
        .decls = &.{},
        .is_tuple = true,
        .backing_integer = null,
    } });
}

The @Type call fails whenever first > 0 but works when first == 0. The error when first = 1 is error: tuple field 2 exceeds tuple field count. Any idea what’s happening?

Take a look at SubTuple here:

The answer to this is that tuple fields need to start with names starting from 0, so you need to rebuild the type with the field names being reindexed, so that they start from 0.

2 Likes