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.
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.
Why do you want to add bloat ?
GLuint or GLint should be u32 or i32 for the user, transparent, they are same size, or else it is just bloat and making stuff hard to read or understand
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
@assetDistance(@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.
(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?
// 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.
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.
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.
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
@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 '=='
@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.
_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.
@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.
@user00e00
... Any function taking
[]const u8will also acceptUtf8StrandAsciiStr, whereas any function parameter or assignment expectingUtf8Strwould need an explicit cast with@astoUtf8Strfrom 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.
A simple cast from
[]u8toUtf8Stris 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.
@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
@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.
Most helpful comment
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);