Closed emekoi closed 1 month ago
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:
They do not change the runtime code, but they can prevent a lot of errors at compile time.
Sounds like a job for comptime
.
Certainly a good thing to have.
how would comptime provide this feature? what i mean is we could do something like const ShaderProgram = distinct u32;
and it would be an compiler time error to pass a plain u32
as a ShaderProgram
and vice versa.
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. The big downside is that setting and getting the member is always boilerplate and does discourage the use of such a typesafe feature.
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:
const ShaderProgram = @distinct(u32);
how would comptime provide this feature?
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 @distinct
is a better than distinct
.
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?
// Pass a block like in @cImport()
const ShaderProgram = @distinct(u32, {
pub fn bind() void { ... }
pub fn unbind() void { ... }
});
or an alternative way with minimal changes to syntax that is consistent with enum semantics of 'underlying type'.
const ShaderProgram = struct(u32) {
pub fn bind(sp: ShaderProgram) void { ... }
pub fn unbind(sp: ShaderProgram) void { ... }
};
EDIT: Just a bit further - this could allow for explicit UFCS. The blocks below would be equivalent:
ShaderProgram(0).bind();
ShaderProgram(0).unbind();
var sp: = ShaderProgram(0);
sp.bind();
sp.unbind();
ShaderProgram.bind(0);
ShaderProgram.unbind(0);
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? for example:
const ShaderProgram = @distinct(u32); // produces a Distinct struct
const Distinct = struct {
cont base_type = // ...
value: base_type
};
and when an operator is invoked on the type the compiler can basically insert an @intCast(ShaderProgram.base_type, value.value)
or the equivalent.
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:
var first : = ShaderProgram(1);
var second : = ShaderProgram(2);
//This should be an error with all math operators
var nonsense : ShaderProgram = first *insert any operator here* second;
void glAttachShader(GLuint program, GLuint shader);
I'm not familiar with the gl api, but I assume that program
and shader
are effectively opaque types. Despite being integers, it would not make sense to do any arithmetic on them, right? They're more like fd's in posix.
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 @OpaqueType()
, and then use single-item pointers to the type as the handle.
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 usize
. This is not always appropriate. Sometimes the handle type must be c_int
instead, such as with posix fd's, and c_int
and usize
often have different size. You have to use the correct handle type, so a pointer to an opaque type is not appropriate with these handle types.
A new builtin @OpaqueHandle(comptime T: type) type
.
const H = @OpaqueHandle(T);
const G = @OpaqueHandle(T);
var t = somethingNormal();
var h = getH();
var h2 = getAnotherH();
var g = getG();
assert(H != T);
- You get a different type than you passed in.assert(G != H);
- Similar to @OpaqueType()
, each time you call it, you get a different type.assert(@sizeOf(H) == @sizeOf(T) and @alignOf(H) == @alignOf(T));
- Same in-memory representation.H
is guaranteed to behave identically to T
in the extern
calling convention. This includes when it is part of a larger type, such as a field in an extern struct.h = t; t = h; h = g; // all errors
- The handle types don't implicitly cast to or from any other type.if (h != h2) { h = h2; }
- Handles can be copied and equality-compared.h + 1, h + h2, h < h2 // all errors
- Whether T
supported arithmetic or not, the handle types do not support any kind of arithmetic.t = @bitcast(T, h);
- If you really need to get at the underlying representation, I think @bitcast()
should be the way to do that. Or maybe we should add special builtins for this, idk.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 @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()
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.
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 https://github.com/ziglang/zig/issues/1595#issuecomment-425615618, or mute the thread https://github.com/notifications/unsubscribe-auth/AL-sGuZxTFMx5aawVhueT-oWDPD9kwDpks5ufvrKgaJpZM4W79q6 .
Another use case: const Vec2 = [2]f32;
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. https://golang.org/ref/spec#Type_declarations
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 WS_GROUP
etc constants as well as http://wiki.libsdl.org/SDL_WindowFlags could be represented in Zig as this new bitflags type, and that would also lead to beating C at its own game. The proposal ended up being pretty complicated, so I haven't posted it anywhere yet.
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?
pub fn foo(myparam : u32 or []const u8) {
}
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. I'm usually always against adding any kind of type shenanigans but this is actually something I use and need all the time in languages like Typescript.
@Meai1 I'm not sure how this solves the issue. We're talking about allowing two names A
and B
, to have the same underlying type (usize
or something else) but disallow implicit casts between them.
I think what you're proposing fits with #1268.
@Hejsil Because I think that what is described in this issue is just a tiny subset of the general problem/solution of "type refinement": https://flow.org/en/docs/lang/refinements/
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 B
is a distinct A
, that are the same type but A
cannot implicitly cast to B
and vice versa. this means calling a function with the signature fn foo(bar: A) void
with an argument that is of type B
is an error, despite the fact types A
and B
are identical.
I think @thejoshwolfe's proposal is promising. One modification though:
t = @bitcast(T, h);
- If you really need to get at the underlying representation, I think@bitcast()
should be the way to do that. Or maybe we should add special builtins for this, idk.
Following with the established pattern, opaque handles would have their own casting functions, unique to opaque handles. @fromOpaqueHandle(x)
to get the value, @toOpaqueHandle(T, x)
to get the opaque handle.
The strongest argument against this I can think of is that it is More Language :-1: . The counter-argument is that it Clearly Prevents Bugs :+1:
Here's a question to consider: in a pure zig codebase, would there be a reason to use @OpaqueHandle
?
Whether T supported arithmetic or not, the handle types do not support any kind of arithmetic.
I think this is a bad idea, because its very verbose so people will not use it (enough) https://github.com/ziglang/zig/issues/1595#issuecomment-425632219
Here's a question to consider: in a pure zig codebase, would there be a reason to use @OpaqueHandle?
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());
}
Here's a question to consider: in a pure zig codebase, would there be a reason to use @OpaqueHandle?
I'm pretty sure I'd never use this.
comptime hack_around_comptime_cache: comptime_int
this could be a type
and then you pass @OpaqueType()
rather than 0, 1, etc.
@andrewrk Aaah, nice!
A distinct integer type seem like it could be done via an empty non-exhaustive enum (#2524).
The example from earlier using a non-exhaustive enum:
const ShaderProgram = enum(u32) {
_,
pub fn bind(sp: ShaderProgram) void { ... }
pub fn unbind(sp: ShaderProgram) void { ... }
};
You could get to the underlying integer via @enumToInt
and @intToEnum
.
The enum doesn't have to be empty either, it would let you specify "special" values if your distinct integer type has them.
how is this bloat? having distinct types goes along one of the zens of zig: "communicate intent precisely". this is a proposal for the type system to help catch errors at compile-time.
Here's one of the usecases in my emulator.
[]u8
:
[]u16
:
Using raw values, it was a struggle to correctly distinguish registers and memory data, until I wrapped everything in a struct. Granted this was C so even mixing up pointers and arrays was a nightmare.
Being able to add methods in Zig was actually quite powerful so I'd love to see this concept embraced instead of worked around.
@emekoi one of the classic instances of this is to use types to catch unit errors. I.e. one type is kilometers and another is miles. You can't add them or do any arithmetic with them. I use struct wrappers in C but then you lose the operators :-(
Vague idea: what if we allow explicit type coercion via postfix operator?
const Kilometer = @distinct(u32);
var x = Kilometer(1000);
const distance = Kilometer(120);
x.* += 3 * distance.*; // shouldn't be `.*` but I'm not sure what else is good
Things this solves:
@fengb interesting idea. Not sure that overloading .* is helpful...
Perhaps you could make this a more explicit thing. Zig has a number of things that are intentionally limited to keep some sanity, like errors. Perhaps that idea could be applied here. Take your @distinct
function. That returns a type that acts like the wrapped type but has no implicit coercion allowed and all operators etc. only work against the same type.
const Km = @distinct(u32);
const Mi = @distinct(u32);
var k = Km(10);
var m = Mi(2);
var nope = k + m; // blows up with a compiler error, uncoerceable types.
var k2 = Km(15);
var yep = k + k2; // works because types are the same.
That would have saved at least one space probe.
Sorry, I didn't mean we should overload .*
but rather it should be a postfix operator that behaves like .*
. I couldn't think of a good symbol but here's some silly ideas:
x.~ += 3 * distance.~;
x.$ += 3 * distance.$;
x.% += 3 * distance.%;
x.| += 3 * distance.|;
x.# += 3 * distance.#;
I'm suggesting a postfix operator so we can get similar assignment semantics to pointer and nullable assignment.
The big caveat with operator overloading is which ones make sense. Adding km is fine, but multiplying should yield a new unit, division should yield a unitless scalar, and bitwise are all a giant question mark. Which of these should be a part of the type? Having an explicit "unwrap" operator sidesteps these questions by making it explicit but not too annoying.
Automatically extending operators onto distinct types seems problematic. What if @distinct
took a whtelist of operators?
fengb, where does the type checking happen in your examples? It seems like this code would still pass:
some_kilometers.* += 3 * some_hours.*;
No type checking. The idea is make the type unwrapping explicit so the language doesn't need to handle type checks and operator overloading.
With that said, I still think that type checking is still fundamentally broken for non-addition non-subtraction. 3 * kilometer
is correct but structurally unsound, while kilometer * kilometer
is non-sensical unless we have complex types.
@dbandstra Hmm, that is a good point. If you unwrap like that, then it is unitless and will work with any incorrect pairings.
@fengb Good point about the operators. Only cases where the input and output units should be the the same work. Perhaps that is still useful?
Without either being able to define/override operators, or perhaps whitelisting (interesting idea, @bb010g!) them, I am not sure how you'd make this work using common notation. You could easily make it work by embedding the underlying values in structs and adding some struct-namespaced functions. Then you get things like:
var total_kms = first_dist.add(second_dist);
var dist = a_rate.mult(a_time);
Not pretty, but it does work. I do that in C sometimes when it is really important to keep track of units.
Note that the proposal in #2953 seems to get pretty close to this. I like the use of blah.value to expose the underlying value without the type. Simple and easy but not too error prone. Hopefully.
I think we're having two distinct requests here (#2953 and #2524) which both are valid requests.
exhaustive enums are non-arithmetic types with an integer representation (like OpenGL objects or enumeration values where you can't know all possible values)
the wrapped primitives are arithmetic types that won't automatically convert into any other type. these are more the "custom unit" approach to solve the apples and oranges problem.
I think it's attractive to allow the user to specify methods on any user-defined type, so both type classes should allow this.
After some thinking, I'm leaning towards the usage pattern of distinct types presented below.
(+ - * /)
on a database key type.x[m] /y[s] = z[m/s]
0.000..1 * 100..00.0
might have more rounding errors than 1[nano] * 1[Mega]
.Usage example:
const x = @distinct( NUMBER , NUMBERGROUP_SUBTYPE);
Where:
NUMBER
is any builtin number type: u8, f64, i32
,NUMBERGROUP_SUBTYPE
is an instance of a MY_NUMBERGROUP
struct that implements (or embeds) a built in NumberGroup
interface that the compiler recognizes and can use for compile time checks. NumberGroups could be user provided or included in std or some other library.NumberGroup
interface has a mandatory field NumberGroupType
that is a built-in enum, that can take values like the following: Measure_f64, Measure_i32, Key_u32, ...
and is used by the compiler to decide things like handling of compound types, allowed operands, and so on.x
is a primitive type, but the compiler knows it also belongs to the specified "group", so that every time x is referenced, compile time checks can be performed: "Is this operation valid?", "Does the result of this operation match the desired type?", "Can these operands be used here?"// "phys_..." variables are instances of the struct PhysicalUnit
const x = @distinct(0.13,phys_Second);
const y = @distinct(20.3,phys_Meter);
const z = y / z; // same as @distinct(20.3/0.13,phys_MeterPrSecond);
// below expressions become compile errors
_ = z >> 2;
_ = z & 5;
// usage in function. assume workaround for treating instance `phys_Newton` as a type
fn calculateAcceleration(ball: Ball, force: phys_Newton, dir: Vec3) phys_MeterPrSecond2{
// ... use fields like ball.mass : phys_Kilogram, etc in calculations
//
}
Example with another NumberGroup that is handled a bit differently from PhysicalUnit
above with respect to which operators are allowed.
// "db_..." variables are instances of the struct MyDbKeys (enum setting = .Key_u32)
const x = @distinct(4,db_Person);
const y = @distinct(2,db_Task);
// all the below expressions become compile errors
_ = x + y;
_ = x / y;
_ = x + x
_ = x == y;
_ = x > y;
// while these expressions are allowed
const x2 = @distinct(5,db_Person);
_ x2 == x; // returns false
As for how this could be implemented with something like enum NumberGroupType
and interface NumberGroup
, see this gist and #3002 (and 3002 related gist )
Non-exhaustive enums #2524 can probably solve a distinct integer type use case, however there are still floats and pointers which could potentially benefit from this.
A few ideas that come to mind:
@Distinct(f32)
@Distinct(u8) != @Distinct(u8)
const Mile = @Distinct(f64);
const Km = @Distinct(f64);
fn getLength() Mile;
const length: Km = getLength(); // compile error
fn getDistance() Km;
fn setDistance(value: Km) void;
setDistance(getDistance() + 0.3); // error: explicit cast required for a distinct type
@as
setDistance(@as(Km, 1.5)); // ok
setDistance(@as(Km, getDistance() + 0.3)); // ok
Edit: there is a slight problem that @as
looses its symmetry:
const a = @as(u8, 11);
same as const a: u8 = 11;
This is also probably in the scope of #2457 - c types and apis could be annotated as 'distinct'.
operators can be used on such types however resulting values lose their 'distinctness'
I've been doing thought experiments on how distinct types could be preserved or transformed in expressions, e.g to have the compiler help you keep track of units when writing math, but it gets too complex. The approach of simply dropping "distinctness" like suggested above is better.
Just one addition. I think the compiler should forbid inferring types from expressions involving distinct types.
const a = @as(Meter,10.0);
const b = @as(Mile,5.0);
// compile error:
// distinct types lost in expression
const x = a + b;
const x1 : f64 = @as(f64, a + b); // Ok
const x2 : f64 = a + b; // Ok
const x3 = @as(Meter, a + a); // Ok
const x4 : Meter = @as(Meter, a + b); // Accepted by compiler, but not correct.
// Could perhaps be caught by some kind of static analysis in a large code base
// Distinct types, expression analysis
// ...
// "Meter = Meter + f64" 3 occurrences
// "SqrMeter = pow2(Meter)" 5 occurrences
// "Meter = Meter + Meter" 100 occurrences
// "Meter = Meter + Mile" 1 occurrence
// ...
I am also wondering if it should be possible to create groups of distinct types. Used to create functions that are "generic" for one group of distinct types, but not for others.
// config could be a built-in enum
// if any kind of configuration is needed for a group of distinct types
const SiUnits = @DistinctGroup(f64, config);
const Meter = @Distinct(f64,SiUnits);
const Second= @Distinct(f64,SiUnits);
const My_f64 = @Distinct(f64,null);
fn SI_multiply(a: SiUnits, b: SiUnits) SiUnits{
....
}
// compile error. My_f64 not part of SiUnits distinct type group
_ = SI_multiply(@as(My_f64, 2.0), @as(My_f64, 4.0));
A distinct integer type seem like it could be done via an empty non-exhaustive enum (#2524).
It can't work for floating point distinct types for now; but perhaps they could be solved by allowing an enum
with floating point type? (this should be a new proposal: it could be useful for situations where you want to keep your enum in a floating point register; as well as when you have a floating point number with known "special" values)
Alternate proposal, that doesn't require a builtin. Note: I've used OpenGL for approximately six hours in my life for experimentation purposes; this is entirely based on other code in this thread.
Instead of
// Pass a block like in @cImport()
const ShaderProgram = @distinct(u32, {
pub fn bind() void { ... }
pub fn unbind() void { ... }
});
99% sure we can already do this:
fn ShaderProgram(_id: u32) type {
return struct {
const id: u32 = _id;
fn bind() void {
// do something with id
}
};
}
const a = ShaderProgram(0);
a.bind();
a.unbind();
const underlying = a.id;
and since the returned type is identical for any given input IIRC, that means this example also works
ShaderProgram(0).bind();
ShaderProgram(0).unbind();
@pixelherodev the problem is that id
is returned by an OpenGL function and thus is runtime known.
Also the concept isn't to distinct several values of u32
, but distinct several types of u32
:
const Program = @distinct(u32);
const Shader = @distinct(u32);
pub extern "OpenGL" glCreateShader(type: ShaderType) : Shader; // returns GLuint
pub extern "OpenGL" glCreateProgram() : Program; // returns GLuint
As you can see, the distinct type is required to non-confuse the return type of those functions. But as @daurnimator already said: Non-exhaustive enums solve this kind of problem class (by providing a non-arithmetic integer class with user-defined operators even)
Another way you might use @distinct(Type)
is to bypass function memoization:
fn MakeInstanceCountedType(comptime T: type) type {
return struct {
var count: usize = 0; // counts the number of instances of this type
// ...
};
}
// Broken: because Type1.count and Type2.count are the same because of function memoization
const Type1 = MakeInstanceCountedType(u64);
const Type2 = MakeInstanceCountedType(u64);
// Works as expected
const Type1 = @Distinct(MakeInstanceCountedType(u64));
const Type2 = @Distinct(MakeInstanceCountedType(u64));
Another way to solve this problem would be with a @noMemoization()
tag inside the function:
fn MakeInstanceCountedType(comptime T: type) type {
@noMemoization();
// ...
}
This would be preferable in this example since its a bug to use MakeInstanceCountedType
without @Distinct
. A @noMemoization
tag would also allow you to create distinct struct
, enum
and union
types, however it wouldn't work for the other primitive types.
@momumi I dont know what a use case for this, but you can move @Distinct
inside MakeInstanceCountedType
so you would not forget to call it.
fn MakeInstanceCountedType(comptime T: type) type {
return @Distinct(struct {
var count: usize = 0; // counts the number of instances of this type
// ...
});
}
@Rocknest That won't work because because zig memoizes comptime function calls. For example if you had
fn MyType(comptime T: type) type {
return @Distinct(T);
}
const a : MyType(i32) = 1;
const b : MyType(i32) = 2;
The first time you call MyType(i32)
it will run normally and cache the return value. Then subsequent calls of the function with the same arguments will use the cached return value. So a
and b
would get given the same type. Zig does this on purpose because most of the time you do want @TypeOf(a) == @TypeOf(b)
.
However, here's a working example in current zig which shows how memoization can have unintended effects: https://gist.github.com/momumi/72c7bf7d6e089f6d766b398500e7c4ff
Because of function memoization this program outputs:
number of A variables: 5
number of B variables: 5
we made 3 A
variables and 2 B
variables, but memoization causes A.count
and B.count
to be the same value which results in the wrong output.
@momumi Oh right! I forgot how confusing comptime stuff can be. As i remember there is a hack with @OpaqueType
to prevent caching.
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
.