ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
33.26k stars 2.42k forks source link

request: distinct types #1595

Closed emekoi closed 3 weeks ago

emekoi commented 5 years ago

would it be possible to add distinct types? for example, to make it an error to pass a GLuint representing a shader as the program for glAttachShader.

ghost commented 4 years ago

(Trying to combine many of the ideas brought up in the comments here and in other issues: )

What is the opinion on the syntax and semantics below for distinct types?

1: Distinct types are stricter type aliases

// expresses intent, but there are no type safety beyond checking the base type
const ExampleOfTypeAlias = []const u8;

// expresses intent, with additional compiler type safety.
const Utf8Str = distinct([]const u8);
const AsciiStr = distinct([]const u8);

Any function taking []const u8 will also accept Utf8Str and AsciiStr, whereas any function parameter or assignment expecting Utf8Str would need an explicit cast with @as to Utf8Str from the base type. Just like type aliases, distinct can wrap any type, whether user defined or primitive. Operators accept distinct types that are "wrapping" a primitive.

2: Distinct types can have an optionally attached namespace

If present, the attached namespace becomes the "target" of unified call syntax. There are no instance members or fields allowed, only top level declarations.

const AliasedType = distinct(OrigType);

const ExtendedType = distinct(OrigType){
  fn extraMemberFunction(self: @This()) bool {
    // ...
  }
};

test "test" {
  var y : AliasedType = @as(AliasedType, OrigType.init());
  _ = y.origMemberFunction(); // point to OrigType namespace

  var x : ExtendedType = @as(ExtendedType, OrigType.init());
  _ = x.extraMemberFunction(); // point to ExtendedType namespace
  _ = OrigType.origMemberFunction(x); // necessary when distinct type has attached namespace
  _ = x.base().origMemberFunction(); // possible workaround
}

It is natural to keep utility functions and constants relevant to the distinct type in the attached namespace, instead of in a global scope or separate namespace.

Also, this gives the possibility to create member functions on primitive types. If a distinct namespace is not present, unified call syntax points directly on the base namespace. This implies there is no ambiguity on where a member function is defined.

Multiple distinct types with attached namespaces wrapping the same base type could provide different "views" on the same data, or provide context: PausedProcess RunningProcess could both be distinct types of Process.

3: Let distinct without a "target type" be equivalent to an alias of "var"

const MyGeneric = distinct(); //equivalent to "var"

const MyGeneric2 = distinct() // equivalent to "var"
{
   // .. namespace for: 
   // ... type predicate functions (useful for comptime type restraints)
   // ... functions you'd like to call with member syntax 
}

Consider fn dostuff1(num: var) vs fn dostuff1(num: Number), where Number can be a distinct type with doc comments.

4: Distinct types are dropped in expressions

Operators and many functions will expect the base type as the argument type, so in this case the distinct type information is simply ignored. It's up to the user to use the correct cast if the output value should be a distinct type. User defined functions can of course specify that the output is a distinct type, e.g fn(dist: km, time: s) kmPerHour.

const km = distinct(f64);
const kg = distinct(f64){
  fn init(num: f64) kg{
    return @as(kg,num);
  }
};

// wrong, but at least it's obviously wrong,
const x_tmp : f64 = @as(km,5.0) + kg.init(10.0);
// would also leave a trail for semantic analysis to pick up on
Tetralux commented 4 years ago

@ThomasKagan We may be blowing this a little out of proportion. I suggest we consider "distinct type" to be more like Odin's and less like those in Haskell.

In Odin it's very simple.

A distinct type inherits the ops of the backing type, if any, and can be casted between the backing type and the distinct type.

Handle :: distinct i32;

// ...

i := cast(i32) os.stdout;
assert(i == 1); // the value of stdout on Linux

h2 := cast(os.Handle) i;
assert(h2 == os.stdout); // okay, since `i32` supports '=='
Tetralux commented 4 years ago

@stolenmutex

want to guarantee that your datastructure can only be created and/or mutated within your library?

Have a const State = @OpaqueType(); and hand out *State from your library, and take-in to your library. Then, have your library cast it to a *InternalStruct or whatever that actually has the state.

I'm not sure if that's the best way to do it, but that is a way that you could use.

BarabasGitHub commented 4 years ago

You want to guarantee that your datastructure can only be created and/or mutated within your library.

I haven't read the whole thread (again), but for me this is not a purpose. If you really want that you can indeed just create an @OpaqueType(). And I'm not even sure if you should want that. If your client wants to mess around in your datastructure... why not let him? His fault if he makes it crash.

For me it would be more about not accidentally supplying a function with a wrong quantity. For example in physics you don't want to use a length as a mass or a temperature as a time. Distinct types could help with that, without having to make a new struct.

I'm not sure if it's worth adding to the language, but I can definitely see it being helpful in some cases.

BarabasGitHub commented 4 years ago

@stolenmutex

I'm not arguing the official API should be to let others poke around in your internals. It's just an option that in my opinion is good to have available sometimes. One of the things that annoys the hell out of me with Object Oriented design is that if the specific feature you want isn't available in the public API there's no other option than to rewrite the whole damn thing yourself.

In your example the pilot doesn't have to control everything, but he has the option to get access to it if he needs to (because issues, plane is on fire, automatic control went crazy, etc.). And they generally do. They fly on auto-pilot, but in case of emergency they can switch to manual.

jakwings commented 4 years ago

@user00e00

... Any function taking []const u8 will also accept Utf8Str and AsciiStr, whereas any function parameter or assignment expecting Utf8Str would need an explicit cast with @as to Utf8Str from the base type. ...

A simple cast from []u8 to Utf8Str is deemed to hide bugs. Proper initilization is necessary for unsafe source. This feature is not suited for any conversion that need validation.

4: Distinct types are dropped in expressions

I think this should only be allowed between aliases. Otherwise they are not so "distinct", they simply share the same base while having their own namespaced methods. I don't find this very useful. There should always be some check between conversion.

ghost commented 4 years ago

A simple cast from []u8 to Utf8Str is deemed to hide bugs. Proper initilization is necessary for unsafe source. This feature is not suited for any conversion that need validation.

I agree. I think the best remedy would be to limit casts to a typedef distinct, while always allowing casts back to the base type.

const str = "hello";
const str2: Utf8Str = str; // (typedef) compile error. coercion not accepted
const str3: Utf8Str = @as(Utf8Str,str); // typedef compile error. cast to distinct type outside self scope is not supported 
const str4 : Utf8Str = Utf8Str.init(str); // accepted. function in typedef scope does validation.

4: Distinct types are dropped in expressions

I think this should only be allowed between aliases. Otherwise they are not so "distinct", they simply share the same base while having their own namespaced methods. I don't find this very useful. There should always be some check between conversion.

It would be possible to make a typedef distinct that preserves its distinctness through expressions too, but then you might want to force all operands to have the same typedef id for example, which can in turn be cumbersome.

If you have a preference for how distinct types should work, feel free to present it, and I'll see if I can "map" that behavior to a typedef coercion behavior.

jakwings commented 4 years ago

@user00e00 Ah... I see. We are talking about different kinds of distinct types. My primary use case is to differentiate the use of data, so different int/float/... don't mess around and this is convenient for refactoring. For example:

fn customize(comptime S: type, comptime T: type) type {
    return struct {
        pub const Text = S;
        pub const PostId = @distinct(T);
        pub const CommentId = @distinct(T);
    };
}

// type1: Bytes/AsciiStr/Utf8Str/Big5Str/...
// type2: u32/u64/HashBytes/HashStr...
usingnamespace customize([]const u8, u32);

var a = @as(PostId, 1);  // ok: PostId <- comptime_int
var b: CommentId = 1;    // ok: CommentId <- comptime_int

a += 1;  // ok: PostId + @as(PostId, comptime_int)

var c = a + b; // error: incompatible types

var d: u32 = a; // error: need explicit cast

var e = @distinctAs(u32, a); // ok: check base type then @bitCast(u32, a)

edit: more details

ghost commented 4 years ago

@iology I imagine const T = u32; const PostId = typedef(T, .ResourceHandle) to be the typedef + typedef config that suits the use case you have there. It doesn't need to coerce down to the base in expressions.

topolarity commented 3 years ago

Just a quick note for anyone looking, it looks like this is already possible for composite types (e.g. struct {...}), using the existing @Type and @typeInfo built-ins:

// Struct Types
const Type1 = struct {
    x: u32
};
const Type1_aliased = Type1;
const Type2 = @Type(@typeInfo(Type1));

fn func1(x: Type1) void { }
fn func2(x: Type2) void { }

pub fn main() void {
    // Struct types work as expected
    var a : Type1 = .{.x = 5};
    var b : Type1_aliased = .{.x = 5};
    var c : Type2 = .{.x = 5};

    func1(a);    // OK
    func1(b);    // OK
    // func1(c); // error: expected type 'Type1', found 'Type2'

    // func2(a); // error: expected type 'Type2', found 'Type1'
    // func2(b); // error: expected type 'Type2', found 'Type1'
    func2(c);    // OK
}

Unfortunately, the same trick does not currently work for primitive types, like u32 (see https://godbolt.org/z/MKvcoWvhc)

nektro commented 2 years ago

adding explicitly here, since I've seen it spoken elsewhere and run into it myself in practice, is that being able to generate distinct integers would make developing while using a data oriented design paradigm a lot more type safe

alinebee commented 2 years ago

I've been trying out non-exhaustive enums as distinct types, as suggested in @daurnimator's comment from 2019. I've been using them in a game interpreter to represent various kinds of integer IDs that are parsed from game data.

The good:

The bad:

// Before const id: IDType = 123; const ids = [_]IDType{ 0, 1, 2, 3 };

// After const distinct_id = @intToEnum(DistinctIDType, 123); const distinctids = []DistinctIDType{ @intToEnum(DistinctIDType, 0), @intToEnum(DistinctIDType, 1), @intToEnum(DistinctIDType, 2), @intToEnum(DistinctIDType, 3), }; // Or (less safe and requires explicit array size) const distinct_ids_fromcast = @bitCast([4]DistinctIDType, []IDType{ 0, 1, 2, 3 });

const expectEqual = @import("std").testing.expectEqual; test "ID type ergonomics in tests" { // Before try expectEqual(123, id); // After try expectEqual(@intToEnum(DistinctIDType, 123), distinct_id); }



That boilerplate got in the way whenever I needed to create distinct values, but it could be eliminated by _letting integer literals coerce to a non-exhaustive enum type_ - the way they already do to other integer types.

---

Beyond that, I think non-exhaustive enums only address a subset of what people want distinct types for. Ultimately, _most_ values probably should be distinct, and prevented by the compiler from mixing with values that have a different semantic meaning; but often we still want some or all of the operations of the underlying type when working with values of the same distinct type. Casting from the distinct type down to the underlying type, performing the operation, and casting back is too cumbersome for most people to adopt that pattern I think.
daurnimator commented 2 years ago

@alunbestor could you write a helper function for your tests? It should work at comptime: (untested):

const DistinctIDType = enum(IDType) {
    _,
    const self = @This(),
    pub fn fromInt(a: IDType) self {
        return @intToEnum(self, a)
    }
    pub fn fromIntArray(a: anytype) [@TypeOf(a).len]DistinctIDType {
        var r: [@TypeOf(a).len]DistinctIDType = undefined;
        inline for (a) |x, i| {
            r[i] = @intToEnum(self, x);
        }
        return r;
    }
};

Which would then make your boilerplate at usage go away:

const distinct_id = DistinctIDType.fromInt(123);
const distinct_ids = DistinctIDType.fromIntArray(.{ 0, 1, 2, 3 });
Vexu commented 1 year ago

I once again spent nearly an hour looking for a bug that would have entirely been prevented by this proposal.

wooster0 commented 1 year ago

What's the problem with changing existing semantics and making distinct types the default? The existing @as() would be used for coercion and has to be done explicitly.

const Hello = u32; // distinct type; not an alias

// takes Hello, not u32
fn x(hello: Hello) void {
    _ = hello;
}

pub fn main() void {
    x(5); // ok; @as(comptime_int, 5) coerces to both u32 and Hello (or maybe we can require an explicit `@as(Hello, 5)` for this too, even for comptime_int)
    x(@as(u32, 5)); // bad; type is not Hello
    x(@as(Hello, 5)); // ok; type is Hello
    const y: u32 = 5;
    x(y); // bad; type is not Hello
    x(@as(Hello, y)); // ok; type u32 coerced to Hello explicitly
}

This basically disallows type aliases.

And then what if instead of a "distinct" keyword or builtin, we add an "alias" one? So distinct types would be the default like above and if you really need a type alias, do probably this:

alias Apple = u8;

So now the safer thing, distinct types, would be the default and the implicit and less safe thing, type aliases, would be something you have to reach for purposely using a keyword that will be seen far less than const.

And I do think using a keyword for this would be better rather than @alias(u8) or something because it makes type alias creation more limited. They'd basically only be created using exactly the syntax alias {type name} = {type};.

We could however also just not have type aliases in the languages and only have distinct types. But we should probably have type aliases.

zzyxyzz commented 1 year ago

@r00ster91 That would defeat the whole point of Zig's unified const assignment syntax, which always acts as an alias, regardless of the kind of object assigned.

And anyway, distinct types should not be encouraged as a default, IMO. They have their uses, but the gain in safety is offset by the boilerplate to convert back and forth between the new type and the underlying value, so their desirability in any particular case is far from obvious.

cryptocode commented 1 year ago

Just anecdotal evidence, but I've also seen the class of bugs this prevents in the wild many many times (in C++) The userland kludges to implement strong typedefs are less than inviting.

zzyxyzz commented 1 year ago

Let's not forget about the good ol' wrapper type solution. Intuitively, it feels like it ought to be more cumbersome than a dedicated distinct type facility, but look at this:

// A
const MyFloat = struct { val: f32 };
const x = MyFloat { .val = 5 };
const v = x.val;

// B
const MyFloat = @Distinct(f32);
const x = @as(MyFloat, 5);
const v = @as(f32, x);

// C
const MyFloat = @OpaqueHandle(f32);
const x = @toOpaqueHandle(MyFloat, 5);
const v = @fromOpaqueHandle(x);

You could call option B slightly more elegant, but the advantage is paper-thin at best. And it turns negative if we want to do more than create and pass around values. For example, adding two MyFloats would look like this, respectively:

MyFloat { .val = x.val + y.val }
@as(MyFloat, @as(f32, x) + @as(f32, y))
@toOpaqueHandle(MyFloat, @fromOpaqueHandle(x) + @fromOpaqueHandle(y))

Structs are clearly superior in this case, at least if distinct types have black box semantics. If they inherit operators (and methods?) from the underlying type, adding two values of the same distinct type would be as simple as x + y.

However... It's not at all obvious that this semantics is actually desirable. Sometimes you want to inherit operators and sometimes you don't. And even if you do, you might only want to support some of them. Adding apples to apples is good, but multiplying or xoring them probably isn't. We could try to make the necessary operations selectable in some way (see @user00e00's comment for example), but this quickly leads into too-much-complexity-for-too-little-gain territory, IMO.

In addition, some limitations would remain even with inheritance. For example, multiplying a MyFloat by 2 would still require @as(MyFloat, 2) * x instead of 2 * x. Automatic coercion can't be allowed because that would erode the distinctness of distinct types pretty badly.


TL;DR: Some of the use-cases for this proposal have now been subsumed by non-exhaustive enums. For the rest, manual struct wrapping is a surprisingly viable and flexible solution. Proper distinct types, as discussed so far, seem to be either a) equivalent b) worse or c) too complicated. As things stand, I don't think Zig needs this functionality.

cryptocode commented 1 year ago

@zzyxyzz Struct wrapping doesn't actually solve the problem when accessing through .val And people will use .val directly at use-sites.

const Meter = struct {val: u32};
const EntityID = struct {val: u64};

[...]

// compiles, but wrong parent after refactoring, should've used entity.parent = root.id()
entity.parent.val = found_parent;

// compiles, but timing-related end variable used by mistake 
// (imagine this is the midst of some complex function)
len = end - origin.val;

@distinct types would catch this class of bugs (which is not uncommon in the wild)

const Meter = @distinct(u32);
const EntityID = @distinct(u64);

[...]

// compile error
entity.parent = found_parent;

// compile error, must fix using end + cast expr to len's type
len = end - origin;

Granted, setting .val directly when re-initializing should raise red flags, but that's weak protection. Expressions like the second example would be the more common source of bugs.

zzyxyzz commented 1 year ago

@cryptocode, Could you expand this example a little bit? I don't quite understand what it's supposed to do and what error is to be prevented here.

cryptocode commented 1 year ago

@zzyxyzz I'll try. Using the last example:

len = end - origin.val;

Imagine len is u32, and is going to be used to serialize some length in meters to a file. Thus we need to go from the world of Meter to the world of u32.

This compiles because 1) end happens to be u32 as well, but it's a completely unrelated variable - for instance for timing purposes, and because 2) origin.val is accessed directly (which makes sense/is too tempting in such expressions)

Since struct wrapping invites the use of direct access to .val in expressions, you no longer have distinct types where this is done.

With distinct types, this won't compile:

len = end - origin;

for two reasons: 1) len (to be serialized to file) is of a different type, and 2) end is a different type and in fact the wrong variable. Potentially a long debug session averted :)

And that's the class of bugs distinct types catch: by introducing more types you reduce the chance for these mixups. I've seen this in monetary related apps for instance; the current discussion thread contains some other examples.

The fix (where pos.x is also Meter):

len = @as(u32, pos.x - origin);

Also, I personally don't think these casts are noisy in practice, because you'll stay in the world of the distinct type most of the time.

I find making the point in such small examples hard, but hope it makes sense.

Clearly this must be balanced with the added complexity of the compiler etc, but I do think it'll catch some otherwise hard-to-find bugs.

zzyxyzz commented 1 year ago

Thanks, this makes it a bit clearer.

Though I feel there's a bit of an apples-to-oranges comparison involved here. len = end - origin would fail to compile with structs too, while the equivalent of end - origin.val with distinct types (end - @as(u32, origin)) would fail to catch the double bug involved here as well. So what exactly is the difference?

Also, since you are assuming that arithmetic is inherited, the struct-based solution would probably add some helper methods to Meter, so that the actual solution should look something like this:

len = pos.x.minus(origin).val;

which is more ergonomic and less error-prone.

cryptocode commented 1 year ago

Well len = end - origin would fail to compile for a different reason.

The compile error "fix" would be len = end - origin.val and now you're back to the hard-to-find bug.

Adding methods could help, but I wouldn't rely on people doing that given how verbose it gets in more complex expressions.

So what exactly is the difference?

But why would you write that as (end - @as(u32, origin) ? That feels like constructed to introduce the bug.

You could add the last cast because of the compile error, but at that point you're more likely to realize the real problem, right? Because you would think... "why doesn't this compile... end and origin are bother meters", and then you realize that's not the case. It's not like it's a panacea of course.

InKryption commented 1 year ago

The compile error "fix" would be len = end - origin.val and now you're back to the hard-to-find bug.

Isn't that the same with len = @as(u32, pos.x - origin); though? If you cast to the base type mistakenly, it hasn't actually solved the issue of using the data type inappropriately.

zzyxyzz commented 1 year ago

@cryptocode,

But why would you write that as (end - @as(u32, origin) ? That feels like constructed to introduce the bug.

That's sort of my point. Given len = end - origin, the compiler will error out in both cases, and report that origin of type Meter cannot be subtracted from u32. But now you are for some reason assuming that with struct wrappers, the programmer will incorrectly "fix" this by simply unwrapping the value, while with distinct types they will realize that the line is totally wrong and rewrite is correctly. Why?

cryptocode commented 1 year ago

@zzyxyzz Yeah I get what you're saying, but the point of the example, len = end - origin.val, is that it's relatively easy to end up with code like that to begin with. And that compiles with a bug. Maybe my assumption is wrong, but that's the thinking.

The original point was that with the struct approach you'll have a lot of instances of accessing the wrapped value directly, which is the same thing as not using distinct types at all, right?

len = end - origin.val If you cast to the base type mistakenly

@InKryption Right, I should've written a "fix" instead of the "fix" as the discussion derailed. The point, as mentioned above, was really that with wrapping structs you can end up with such code in first place (not just have it as a bad fix)

presentfactory commented 4 months ago

Has there been any progress on implementing something like this? I feel like something like this would be a fairly simple thing to add to the compiler, but it'd help prevent a lot of bugs as others have noted (though I'm not a compiler dev so maybe I am misunderstanding the complexity here). Just curious because it has been over 5 years now since the original proposal.

leecannon commented 4 months ago

@presentfactory one reason this has not really progressed is that it is already possible in status quo to represent distinct integer types.

Just combining enum backing type + non-exhaustive enum + @intFromEnum & @enumFromInt gets you basically the same behaviour as @distinct would have.

const Program = enum(u32) { _ };
const Shader = enum(u32) { _ };

pub fn glAttachShader(program: Program, shader: Shader) void {
    // use `@intFromEnum` to get the values
}
presentfactory commented 4 months ago

@leecannon I mean sure but that's kinda ugly and imo not a good solution to the issue more generally since it does not work for more complex types like structs.

Often I have something like a Vec4 which say represents a quaternion and not a position and I'd like to make a distinction there so I don't accidentally pass something intended as a rotation to a function expecting say a position or a color. This like others have said has caused me preventable bugs in the past, so a more general solution is needed.

SuperAuguste commented 4 months ago

does not work for more complex types like structs

This is a non-problem for structs and unions, though. The solution for those types is to just make distinct structures for each different representation, which is what you should be doing regardless.

For example, both types below are distinct:

const Rgba = struct {r: f32, g: f32, b: f32, a: f32};
const Vec4 = struct {x: f32, y: f32, z: f32, w: f32};

If I have

pub fn myFunc(color: Rgba) void { ... }

calling myFunc(Vec4{ ... }) is not permissible.

presentfactory commented 4 months ago

@SuperAuguste It is a problem though, I don't want to have to re-type the struct redundantly every time like that, that's just WET and bad practice.

Also the point is that while all distinct types a Color, Position and Quaternion are all a Vec4, meaning they can still use the base Vec4 functions for linear algebra operations. With the approach you propose you'd have to duplicate all the functions across all these structs, or pass them to non-member functions taking anytype which is just bad.

There's simply no way around this, distinct types are needed and that's that. The assumption that all you need is aliasing on type assignment is incorrect and there needs to be a mechanism to control this behavior. It'd be like saying all you need in a programming language is references, obviously this is untrue, copies are needed sometimes.

Beyley commented 4 months ago

@SuperAuguste It is a problem though, I don't want to have to re-type the struct redundantly every time like that, that's just WET and bad practice.

Its not re-typing the same struct though, RGBA should have its fields be r, g, b, a and Vec4 should have its fields be x, y, z, w these are not only distinct in the fact they represent different data, but they also should have different field naems, and also multiplying colours is not always the same as multiplying vectors. You can also use usingnamespace here with some comptime to dedupe the member functions aswell

presentfactory commented 4 months ago

@Beyley In this isolated case sure it has different member names but that's irrelevant, usually people implement it with a normal vector type because colors are fundamentally vectors. They fundamentally have the same primitive operations too because they are again, vectors. I do not understand why people are trying to poke holes in this, it'd be incredibly useful to have this feature and it does not matter if it does not cover every single conceivable use case.

Also yes I'm sure there's many ways to hack this like the enum method for integers but I do not want ugly hacky things to do what should be a trivial operation in the compiler. usingnamespace is not meant for this nor would anyone find that method intuitive or easy to understand, same with the enum method for integers.

presentfactory commented 4 months ago

Thinking on it some more I do actually think the difficulty in "inheriting" behavior with distinct types like I propose is determining what say the return value of something is. I think though as long as things are annotated it is useful still, and when you do not want this sort of behavior some sort of function to make a total copy of the type instead would be good too (as really there's type different types of behavior here one might want). So something like:

const Position = @inherit(Vec3);
const Direction = @inherit(Vec3);

fn addPositionDirection(p: Position, d: Direction) Position {
  // Fine, p/d can call fn add(a: Vec3, b: Vec3) Vec3 as they can coerce to Vec3,
  // and the returned Vec3 can coerce back to a Position to return from this function
  return p.add(d);
}

var p: Position = ...;
var d: Direction = ...;

const v = p.add(d); // Fine, returns a Vec3
const p2: Position = p.add(d); // Fine, returns a Vec3 but coerces back to a Position
const p3 = addPositionDirection(p, d); // Fine
const p4 = addPositionDirection(d, p); // Error

And then for when this behavior is not desired (more useful for things like handles where you actually don't want them to be compatible with their base type):

const Handle = @clone(u32);

var h1: Handle = ...;
var h2: Handle = ...;

const h3 = h1 + h2; // Fine, the addition operator conceptually is a member of this type and is cloned with it, calling fn add(a: Handle, b: Handle) Handle essentially, resulting in another Handle
const h4 = h1 + 5; // Error, even though Handle is cloned from an integer it's not able to coerce like this

The issue though with cloning things like this however as you lose the ability to do any operations on the type really with say normal integers. This is especially a problem with primitive types like this as you cannot actually add new behavior to them (as they aren't really structs you can add new methods to unlike a user-defined type). To solve that there would probably need to be some sort of cast operator I think to allow for explicit casting between compatible clones of the type (rather than the implicit coercion of the inheritance based method).

Something like this:

const h4 = h1 + @similarCast(5); // Casts 5 to a Handle to allow it to be added
const bar = bars[@similarCast(h1)]; // Casts the Handle to a usize to allow for indexing with it

With user defined types you could probably just do this via some sort of anytype "conversion constructor" I guess which gets cloned into each instance and allows for conversions between them:

const Vec = struct {
  x: f32, y: f32,

  fn new(other: anytype) Self {
    return .{ other.x, other.y };
  }
};

const Position = @clone(Vec);
const Direction = @clone(Vec);

var p: Position = ...;
var d: Direction = ...;

// Does the same thing as what the inheriting sort of distinct types would, just a lot more verbosely, and again this only works for user defined types where this sort of anytype thing can be added
const p = Position.new(Vec.new(p) + Vec.new(d));

Overall it is a pretty tricky problem as there are multiple ways of making distinct types like this and multiple ways of solving the issues with each approach...but hopefully this bit of rambling is useful in figuring out what Zig should do. Might also be worth looking at some other languages that do this, I don't know of any myself but Nim seems to with its own method where it clones the type but without any of the methods/fields for some reason in favor of having to explicitly borrow them, and relying on explicit casts to go between similar types: https://nim-by-example.github.io/types/distinct/

andrewrk commented 3 weeks ago

This use case is adequately addressed by exhaustive enums and packed structs.