Making the same type unique

I have had this problem for a while where I need a struct or an enum to have the same “shape”, but I need their types to be unique. Using a function to generate the type is nice since all of them will have the same “shape”, but the problem is that their type will be the same.

Here is a very contrived example:

pub fn MakeNonExhaustiveEnum(comptime T: type, comptime val: comptime_int) type {
    ok: {
        switch (@typeInfo(T)) {
            .int => |info| if (info.signedness == .unsigned) break :ok,
            else => {},
        }

        @compileError("Unexpected type was given: " ++ @typeName(T) ++ ", expected an unsiged integer.");
    }

    return enum(T) {
        _,

        const Self = @This();
        const _unique_val = val;

        pub inline fn make(int: T) Self {
            return @enumFromInt(int);
        }

        pub inline fn value(@"enum": Self) T {
            return @intFromEnum(@"enum");
        }
    };
}

pub const EntityType = MakeNonExhaustiveEnum(u32, 1);
pub const GenerationType = MakeNonExhaustiveEnum(u32, 2);
// pub const GenerationType = @UniqueType(MakeNonExhaustiveEnum(u32));

test "Must be unique" {
    try std.testing.expect(EntityType != GenerationType);
}

This works but however I hate the extra book keeping it adds, is there a better way to do this? Thank you!

IMHO this is a little feature gap in Zig, assigning a type to another just creates a type alias (similar to C typedef):

const Bla = struct { x: i32, y: i32, z: i32 };
const Blub = Bla;

This makes an item of type Bla assignable to type Blub, but this creates two distinct types:

const Bla = struct { x: i32, y: i32, z: i32 };
const Blub = struct { x: i32, y: i32, z: i32 };

IMHO a language needs both: structural types (where types with different name but same shape are assignable to each other) and distinct types (where types with different names but the same shape are not assignable to each other - even if the new type has been created as a ‘copy’ of the old type: const Blub = Bla;).

The C-derived convention where each type is unique but can be ‘aliased’ with a different name is somehow the worst of both worlds.

There’s a related and closed proposal here, but this would IMHO only go half of the way (because it doesn’t solve the structural typing half of the problem):

3 Likes

You may still not like it, but I will set the registration statement as a compile-time string, which feels better than a magic number registration value.

1 Like

Instead of comptime_int you can use an opaque{}.

const std = @import("std");

pub fn MakeNonExhaustiveEnum(comptime T: type, comptime Unique: type) type {
    ok: {
        switch (@typeInfo(T)) {
            .int => |info| if (info.signedness == .unsigned) break :ok,
            else => {},
        }

        @compileError("Unexpected type was given: " ++ @typeName(T) ++ ", expected an unsiged integer.");
    }

    return enum(T) {
        _,

        const Self = @This();
        const _unique = Unique;

        pub inline fn make(int: T) Self {
            return @enumFromInt(int);
        }

        pub inline fn value(@"enum": Self) T {
            return @intFromEnum(@"enum");
        }
    };
}

pub const EntityType = MakeNonExhaustiveEnum(u32, opaque {});
pub const GenerationType = MakeNonExhaustiveEnum(u32, opaque {});

test "Must be unique" {
    try std.testing.expect(EntityType != GenerationType);
}
3 Likes

Ah thank you! This is probably the best it gets currently.

1 Like