-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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. |
|
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 |
Here's a use case that would benefit from this proposal: pub const id = struct {
pub const Base = enum(u64) {
none = std.math.maxInt(u64),
_,
};
pub fn base(specialized: anytype) Base {
return @enumFromInt(@intFromEnum(specialized));
}
pub fn specialize(comptime Specialization: type, base_id: Base) Specialization {
return @enumFromInt(@intFromEnum(base_id));
}
pub const Node = @Type(@typeInfo(Base));
pub const Changeset = @Type(@typeInfo(Base));
pub const ChangesetComment = @Type(@typeInfo(Base));
pub const Way = @Type(@typeInfo(Base));
pub const Relation = @Type(@typeInfo(Base));
pub const User = @Type(@typeInfo(Base));
test "id type safety" {
const a: id.Way = @enumFromInt(1);
const b: id.Base = id.base(a);
const c: id.Node = id.specialize(id.Node, b);
_ = c;
}
}; Currently, this works, but |
you could add decls by defining each enum without Type/typeInfo and adding a |
@nektro True, but |
the proposal isn't accepted yet, so having a use case for it would give more reasons for them to keep it |
@notcancername You could consider something like this: const std = @import("std");
comptime {
_ = id;
}
pub const id = struct {
fn BrandedBase(comptime brand: anytype) type {
return enum(u64) {
none = std.math.maxInt(u64),
_,
pub fn foo(self: @This()) void {
_ = self;
}
comptime {
_ = brand; // capture the parameter to make the type distinct
}
};
}
pub const Base = BrandedBase(opaque {});
pub fn base(specialized: anytype) Base {
return @enumFromInt(@intFromEnum(specialized));
}
pub fn specialize(comptime Specialization: type, base_id: Base) Specialization {
return @enumFromInt(@intFromEnum(base_id));
}
pub const Node = BrandedBase(opaque {});
pub const Changeset = BrandedBase(opaque {});
pub const ChangesetComment = BrandedBase(opaque {});
pub const Way = BrandedBase(opaque {});
pub const Relation = BrandedBase(opaque {});
pub const User = BrandedBase(opaque {});
test "id type safety" {
try std.testing.expect(Base != Node);
try std.testing.expect(Base != Changeset);
const a: id.Way = @enumFromInt(1);
a.foo();
const b: id.Base = id.base(a);
b.foo();
const c: id.Node = id.specialize(id.Node, b);
c.foo();
}
}; Edit: Changed string literal arguments to |
That's fair, @castholm . Thanks! I do think the hypothetical |
Those cover integers, but what about floating-point numbers? Both enums and packed structs’ backing types must by integers. For floating point numbers, I came up with this workaround: const kib = struct {v: f32};
const num: kib = .{.v = 1.0};
std.debug.print("{d}\n", .{num.v}); Even for integers, you have to use const kib = enum(u64) {_};
const num: kib = @enumFromInt(1);
std.debug.print("{d}\n", .{@intFromEnum(num)}); Both of these examples would be much simpler if there was a const kib = distinct f32;
const num: kib = 1.0;
std.debug.print("{d}\n", .{num}); const kib = @distinct(u64);
const num: kib = 1;
std.debug.print("{d}\n", .{num}); |
Well, of course you should have to explicitly convert between the original and "distinct" type; otherwise, it wouldn't be particularly distinct now, would it? |
My point is, I think there should be a way to make a distinct type without having to create a struct with a single field inside it. This way, you would be able to assign numerical constants to a value of this type.
By distinct I mean that a value of the backing type should not be able to be assigned to a value of the distinct type without a conversion. However, a compiletime-known constant should be able to coerce to it. Some examples: const kib = distinct f32;
const num_1: kib = 1.0; // this works
_ = num_1;
const x: f32 = 2.0;
const num_2: kib = x; // this doesn’t work
_ = num_2; Instead, there should be some kind of conversion from a primitive type to a distinct type. |
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: