Tuple with similarly-typed elements: How to call function on arbitrary element?

I have a tuple with similarly-typed elements. (Every element in all_leds is a struct Pin(X), where X is a comptime value.) And I’d like to call a specific function (pin.toggle()) on element j. How do I do that, idiomatically?

The cleanest way I found is the following. But I’m probably missing a simpler alternative?

            forTupleElementDo(all_leds, j, void, struct {
                fn callable(pin: anytype) void {
                    pin.toggle();
                }
            }.callable);

where

/// For the given `tuple` element `j`, call the given function `f` with return type `R`.
fn forTupleElementDo(tuple: anytype, j: usize, comptime R: type, f: fn (anytype) R) R {
    inline for (0..tuple.len) |i| {
        if (i == j) {
            f(@field(tuple, std.fmt.comptimePrint("{d}", .{i})));
        }
    }
}

Thanks!

Welcome @marnix.klooster!

I would avoid the functional-style programming.

Use index access. When your index is only known at runtime, use a switch to convert the index to a comptime known index and then use index access too:

const std = @import("std");

fn Pin(comptime T: type) type {
    return struct {
        const Self = @This();
        data: T,
        set: bool,

        const init: Self = .{
            .data = 0,
            .set = false,
        };

        pub fn toggle(self: *Self) void {
            self.set = !self.set;
        }
    };
}

pub fn main() !void {
    const Tuple = struct { Pin(u16), Pin(u32), Pin(i32) };
    var tuple: Tuple = .{ Pin(u16).init, Pin(u32).init, Pin(i32).init };

    // with comptime known index
    tuple[0].toggle();
    std.debug.print("{any}\n", .{tuple});

    var index: u2 = 0;
    if (true) index = 2;
    // with runtime known index
    switch (index) {
        inline else => |i| if (i < tuple.len) {
            tuple[i].toggle();
        } else {
            @panic("invalid index");
        },
    }
    std.debug.print("{any}\n", .{tuple});
}

A shorter version of this:

is this:

fn forTupleElementDo(tuple: anytype, j: usize, f: fn (anytype) void) void {
    inline for (0..tuple.len) |i| {
        if (i == j) f(tuple[i]);
    }
}
2 Likes

@Sze Thanks for showing me that the tuple[i] syntax does work for a comptime-known i! Obvious in retrospect, and the compiler must have suggested this to me, and actually used in a code example in the manual at https://ziglang.org/documentation/0.13.0/#Tuples, but I’d missed it.

And switch-with-inline else works perfectly fine as well for a runtime-known j, especially in my special case where the tuple length is a power of 2 (namely 8, so that the index range is the range of an u3):

switch (@as(u3, @intCast(j))) {
    inline else => |i| all_leds[i].toggle(),
}

does exactly the same as all the code from my original post.j

And more generally one can use

switch (j) {
    inline 0...all_leds.len - 1 => |i| all_leds[i].toggle(),
    else => @panic("led index too large"),
}

or your version, perhaps in the following slightly less verbose form:

switch (j) {
    inline else => |i| {
        if (i >= all_leds.len) @panic("led index too large");
        all_leds[i].toggle();
    },
}

Thanks again!

1 Like