-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
request: distinct types #1595
Comments
do you mean something like strong typedefs? https://arne-mertz.de/2016/11/stronger-types/ I'm strongly for this 😃 👍 |
From the article posted by @monouser7dig:
Sounds like a job for |
Certainly a good thing to have. |
how would comptime provide this feature? what i mean is we could do something like |
The current workaround is (like c) to use a Struct with just one member and then always pass the Struct instead of the wrapped value. |
Without yet commenting on the feature itself, if we were to do it, I would propose not changing any syntax, and instead adding a new builtin:
|
You're right, I conflated this with the "strong typedefs" described in the article posted above. They are distinct concepts after all, no pun intended. |
yeah, i think |
I'm actually quite fond of this idea. Would there be any issues if we took it further and allowed functions to be declared inside?
or an alternative way with minimal changes to syntax that is consistent with enum semantics of 'underlying type'.
EDIT: Just a bit further - this could allow for explicit UFCS. The blocks below would be equivalent:
|
Nim has such feature and it is rather clumsy. Distinct type looses all associated operations (for example, distinct array type lacked even element access by []). All these operations have to be added, and there is lot of special syntax to "clone" them from original type. Nice idea was butchered by implementation. |
@PavelVozenilek but thats because nim has operator overloading. in zig all the operators are known at compile time, so wouldn't the compiler be able to use the implementation of the distinct type's base type?
and when an operator is invoked on the type the compiler can basically insert an |
I think distinct types are useful, but I don't think they fit in Zig. There should be only one obvious way to do things if possible and reasonable. The problem with distinct types is that you most likely don't want all of the operators or methods of the underlying type. For example:
|
void glAttachShader(GLuint program, GLuint shader); I'm not familiar with the gl api, but I assume that Perhaps we can scope this down to enable specifying the in-memory representation of an otherwise opaque type. There are two features that we want at the same time:
The recommended way to do this is to make a type with const Program = @OpaqueType();
const Shader = @OpaqueType();
pub fn glAttachShader(program: *Program, shader: *Shader) void {} But this mandates that the in-memory representation of the handle is a pointer, which is equivalent to a ProposalA new builtin const H = @OpaqueHandle(T);
const G = @OpaqueHandle(T);
var t = somethingNormal();
var h = getH();
var h2 = getAnotherH();
var g = getG();
This is an exciting idea. I think this fits nicely into the Zig philosophy of beating C at its own game - Zig is preferable to C even when interfacing with C libraries. If you translate your GL and Posix apis into Zig extern function declarations with opaque handle types, then interfacing with the api gets cleaner, clearer, less error prone, etc. |
One objection I can think of to handling these as opaque types is that Consider the following constants from win32 api
and the following window creation code:
It may be desirable ensure that APIs like this use a With |
I think these are two very different use cases and they might not have the
same solution. I do like the `@opaqueHandle` it's useful as well outside
the C interfacing use case, to create handles to other things. I have
exactly this in my C++ project where the client gets handles to objects it
creates (which is just an integer in a struct).
The flags use case might be solved with a special flag type, just like
enums but supports bitwise operators or something.
Op za 29 sep. 2018 06:44 schreef tgschultz <notifications@github.com>:
… One objection I can think of to handling these as opaque types is that
@distinct(T) as envisioned originally would be useful for C-style flags,
and @OpaqueHandle(T) wouldn't because you can't use & and | with them
without verbose casting.
Consider the following constants from win32 api
pub const WS_GROUP = c_long(131072);
pub const WS_HSCROLL = c_long(1048576);
pub const WS_ICONIC = WS_MINIMIZE;
pub const WS_MAXIMIZE = c_long(16777216);
pub const WS_MAXIMIZEBOX = c_long(65536);
pub const WS_MINIMIZE = c_long(536870912);
pub const WS_MINIMIZEBOX = c_long(131072);
pub const WS_OVERLAPPED = c_long(0);
pub const WS_OVERLAPPEDWINDOW = (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
pub const WS_POPUP = c_long(-2147483648);
pub const WS_SIZEBOX = WS_THICKFRAME;
pub const WS_SYSMENU = c_long(524288);
pub const WS_TABSTOP = c_long(65536);
pub const WS_THICKFRAME = c_long(262144);
pub const WS_TILED = WS_OVERLAPPED;
pub const WS_VISIBLE = c_long(268435456);
pub const WS_VSCROLL = c_long(2097152);
and the following window creation code:
var winTest = CreateWindowExA(
0,
wcTest.lpszClassName,
c"Zig Window Test",
@intcast(c_ulong, WS_OVERLAPPED | WS_MINIMIZEBOX | WS_SYSMENU),
CW_USEDEFAULT,
CW_USEDEFAULT,
800,
600,
null,
null,
hModule,
null
) orelse exitErr("Couldn't create winTest");
It may be desirable ensure that APIs like this use a @distinct
<https://github.com/distinct>() type instead of a normal int constant, to
ensure that you do not accidentally pass something like WS_S_ASYNC, which
is completely unrelated, or a (perhaps mis-spelled) variable containing an
unrelated integer.
With @OpaqueHandle(T), the user could not use the function properly
without casting the handles in a very verbose manner. This could be
abstracted away by the API, though, by providing a varargs fn that would do
that for you. Just something to consider since this is the usecase that
sprang immediately to mind when I read the original proposal.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#1595 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AL-sGuZxTFMx5aawVhueT-oWDPD9kwDpks5ufvrKgaJpZM4W79q6>
.
|
Another use case: |
In go, the new type inherits the operations but not the methods of the old type, I think this is a good way to do it as it provides the benefit without great complexity, type system does not need to catch every possible error, but just help us. |
I agree that bitflags are a separate issue. I've partially typed up a proposal for bitflags in Zig including extern bitflags appropriate for interfacing with C. Those I think the usecase for a handle type is still valid separate from the flags case. |
Wouldn't it be great if you can say that a function can receive either type A or B in a type safe way?
I know what the critique against it is: "just make a parent struct". But that's not the point, this gets the job done so much faster without all the boilerplate of constantly writing structs and naming them and setting them up even though I don't actually need a struct. |
@Hejsil Because I think that what is described in this issue is just a tiny subset of the general problem/solution of "type refinement": edit: I guess they call it 'subtyping' when it is used to define something, in my opinion they look identical: https://flow.org/en/docs/lang/subtypes/ |
what the first article talks about are sum types which can be achieved through union types. as for subtypes i don't see how that relates to distinct types. what i meant by distinct types is that when |
I think @thejoshwolfe's proposal is promising. One modification though:
Following with the established pattern, opaque handles would have their own casting functions, unique to opaque handles. The strongest argument against this I can think of is that it is More Language 👎 . The counter-argument is that it Clearly Prevents Bugs 👍 Here's a question to consider: in a pure zig codebase, would there be a reason to use |
I think this is a bad idea, because its very verbose so people will not use it (enough)
everywhere you use an int/ float type |
If we're not gonna support the operators for the type, then we are pretty close to be able to have this in userland: const std = @import("std");
const debug = std.debug;
pub fn OpaqueHandle(comptime T: type, comptime hack_around_comptime_cache: comptime_int) type {
return packed struct.{
// We could store this variable as a @IntType(false, @sizeOf(T) * 8)
// but we lose the exact size in bits this way. If we had @sizeOfBits,
// this would work better.
____________: T,
pub fn init(v: T) @This() {
return @This().{.____________ = v};
}
pub fn cast(self: @This()) T {
return self.____________;
}
};
}
test "OpaqueHandle" {
// I know that the 0,1 things is not ideal, but really, you're not gonna have
// 10 or more of these types, so it's probably fine.
const A = OpaqueHandle(u64, 0);
const B = OpaqueHandle(u64, 1);
debug.assert(A != B);
const a = A.init(10);
const b = B.init(10);
debug.assert(a.cast() == b.cast());
}
I'm pretty sure I'd never use this. |
|
Just anecdotal evidence, but I've also seen the class of bugs this prevents in the wild many many times (in C++) |
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 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 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. |
@zzyxyzz Struct wrapping doesn't actually solve the problem when accessing through 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;
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 |
@cryptocode, |
@zzyxyzz I'll try. Using the last example: len = end - origin.val; Imagine This compiles because 1) Since struct wrapping invites the use of direct access to With distinct types, this won't compile: len = end - origin; for two reasons: 1) 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 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. |
Thanks, this makes it a bit clearer. Though I feel there's a bit of an apples-to-oranges comparison involved here. Also, since you are assuming that arithmetic is inherited, the struct-based solution would probably add some helper methods to len = pos.x.minus(origin).val; which is more ergonomic and less error-prone. |
Well The compile error "fix" would be Adding methods could help, but I wouldn't rely on people doing that given how verbose it gets in more complex expressions.
But why would you write that as 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. |
Isn't that the same with |
That's sort of my point. Given |
@zzyxyzz Yeah I get what you're saying, but the point of the example, 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?
@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) |
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. |
@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 + const Program = enum(u32) { _ };
const Shader = enum(u32) { _ };
pub fn glAttachShader(program: Program, shader: Shader) void {
// use `@intFromEnum` to get the values
} |
@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 |
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
calling |
@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 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. |
Its not re-typing the same struct though, RGBA should have its fields be |
@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. |
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 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/ |
This use case is adequately addressed by non-exhaustive enums and packed structs. |
Just want to clarify here, you mean non-exhaustive enums instead exhaustive, right? |
FYI this is also what @andrewrk blogged about recently |
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 forglAttachShader
.The text was updated successfully, but these errors were encountered: