A better anytype?

Here’s something I was messing around with a while back. It works in current Zig without any changes needed to the language.

const std = @import("std");

pub fn main() void {
    const foo = Foo{};

    // Succeeds as `Foo` is a `Printable`
    print(@TypeOf(foo), foo);

    // Fails as `Foo` is not a `Writeable`
    //write(@TypeOf(foo), foo);
}

// My custom type
const Foo = struct {
    // Sample fields that are ignored by the interface
    a: i32 = 0,
    b: void = {},
    // Implements `print` for the printable interface
    pub fn print(self: Foo) void {
        std.debug.print("Foo.print: {}\n", .{self});
    }
};

// Generic functions
fn print(comptime T: type, printer: Printable(T)) void {
    printer.print();
}
fn write(comptime T: type, writer: Writeable(T)) void {
    writer.write();
}

// Interfaces
fn Printable(comptime T: type) type {
    const interface = struct {
        print: fn (T) void,
    };
    if (conformsTo(interface, T)) return T;
    @compileError("Type " ++ @typeName(T) ++ " does not conform to the Printable interface");
}
fn Writeable(comptime T: type) type {
    const interface = struct {
        write: fn (T) void,
    };
    if (conformsTo(interface, T)) return T;
    @compileError("Type " ++ @typeName(T) ++ " does not conform to the Writeable interface");
}

/// Checks if `T` conforms to the `Interface` protocol.
fn conformsTo(comptime Interface: type, comptime T: type) bool {
    if (@typeInfo(Interface) != .@"struct") @compileError("Interface must be a struct");

    outer: for (std.meta.fields(Interface)) |field| {
        if (@hasDecl(T, field.name)) {
            if (field.type == @TypeOf(@field(T, field.name))) continue;
            return false;
        }
        for (std.meta.fields(T)) |field2| {
            if (std.meta.eql(field.name, field2.name)) {
                if (field.type == field2.type) continue :outer;
                return false;
            }
        }
        // Has neither a field nor a declaration with the given name
        return false;
    }
    return true;
}

The obvious downside is that every time you call write or print you have to pass in the type as the first argument, but otherwise I quite like it.

Sharing as I had fun playing around with this and you might too. I’m not sure if it’s a good way of doing things since I never used it in a serious project.

3 Likes

I know but the issue isn’t really about T or comptime Foo : type, it’s about having bar : anytype, especially when you have multiple of them it becomes quite hard to understand what is it, even with descriptive names, It’s not the end of the world, and it doesn’t take ages to figure it out, but it’s annoying nonetheless. Currently there is a proposal to change anytype to infer T which would mean that you can name it, which would make it more bearable.

4 Likes