How can I determine what particular struct a type is?

Essentially, I want to be able to pass data to a function, and have it be one of any number of structs I’ve defined, and let the function decide how to handle the data depending on what kind of struct it is.

For example, if I have a function that takes a type, along with a value of that type:

pub fn handler(comptime T: type, data: T) {
  std.debug.assert(@typeInfo(T) == .Struct);

  // ... switch on T struct type?
}

// at call site
const d1: MyCustomStruct = .{ ... };
handler(MyCustomStruct, d1);

// or
const d2: AnotherCustomStruct = .{ ... };
handler(AnotherCustomStruct, d2);

// or
const d3: TestStruct = .{ ... };
handler(TestStruct, d3);

How can handler() know which struct type was passed to it?

I believe you can do exactly as you state in the comment

switch (T) {
    MyCustomStruct => ...,
    AnotherCustomStruct => ...,
    TestStruct => ...,
    else => @compileError("Unsupported struct type " ++ @typeName(T)),
}
4 Likes

Interesting, that makes sense. Though that checks against the type T, rather than the actual data. I guess within each switch case the data parameter would have to be manually cast to the specific type? Or is there a way to capture the data in the indicated type as part of the switch?

It provide action-set struct owning the vtbl (virtual method table).
Defined structs provide a method returned this struct.

For example…

fn ActionSet(comptime Receiver: type) type {
    return struct {
        some_action: ?(*const fn (self: Receiver) void) = null,
        ....,
    };
}

const MyCustomStruct = struct {
    pub fn getActionSet(self: MyCustomStruct) ActionSet {
        return .{
            .some_action = &custom_action,
            ...,
        };
    }

    fn custom_action(self: MyCustomStruct) void {
        // ....
    }
};

const AnotherCustomStruct = struct {
    pub fn getActionSet(self: AnotherCustomStruct) ActionSet {
        return .{
            .some_action = &another_action,
            ...,
        };
    }

    fn another_action(self: AnotherCustomStruct) void {
        // ....
    }
};

const TestStruct = struct {
    pub fn getActionSet(self: TestStruct) ActionSet {
        return .{
            .some_action = &test_action,
            ...,
        };
    }

    fn test_action(self: TestStruct) void {
        // ....
    }
};

pub fn handler(comptime T: type, data: T) {
    std.debug.assert(@typeInfo(T) == .Struct);
    
    // get action set from data
    var action_set = data.getActionSet();
    // call action#1
    if (action_set.some_action) |action| {
        action(data);
    }

    // call action#2 ...
}

This is a bit more idiomatic:

pub fn handler(data: anytype) void {
    switch (@TypeOf(data)) {
    // ...
    }
}
3 Likes

Remember that your data parameter is already known to be of type T, so you just access its fields and methods directly within each switch prong. The compiler will not compile if you try to access a field or method that doesn’t exist for the type; that’s what’s commonly referred to as compile-time duck-typing in Zig.

3 Likes

For those a little slow like me, the whole example based on @chung-leong’s answer:

const std = @import("std");

const A = struct {
  foo: u8,
};

const B = struct {
  bar: []const u8,
};

fn handler(data: anytype) void {
  switch (@TypeOf(data)) {
    A => std.debug.print("{}\n", .{ data.foo }),
    B => std.debug.print("{s}\n", .{ data.bar }),
    else => unreachable(),
  }
}

pub fn main() void {
  const a: A = .{ .foo = 42 };
  handler(a);
}
5 Likes