ziglang / zig

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

request: distinct types #1595

Open emekoi opened 5 years ago

emekoi commented 5 years ago

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

ghost commented 5 years ago

do you mean something like strong typedefs? https://arne-mertz.de/2016/11/stronger-types/

I'm strongly for this 😃 👍

nodefish commented 5 years ago

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.

XVilka commented 5 years ago

Certainly a good thing to have.

emekoi commented 5 years ago

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.

ghost commented 5 years ago

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.

andrewrk commented 5 years ago

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);

nodefish commented 5 years ago

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.

emekoi commented 5 years ago

yeah, i think @distinct is a better than distinct.

raulgrell commented 5 years ago

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);
PavelVozenilek commented 5 years ago

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.

emekoi commented 5 years ago

@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.

Ilariel commented 5 years ago

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; 
thejoshwolfe commented 5 years ago
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.

Proposal

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();

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.

tgschultz commented 5 years ago

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.

BarabasGitHub commented 5 years ago

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 .

raulgrell commented 5 years ago

Another use case: const Vec2 = [2]f32;

ghost commented 5 years ago

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

thejoshwolfe commented 5 years ago

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.

Meai1 commented 5 years ago

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.

Hejsil commented 5 years ago

@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.

Meai1 commented 5 years ago

@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/

emekoi commented 5 years ago

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.

andrewrk commented 5 years ago

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?

ghost commented 5 years ago

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

Hejsil commented 5 years ago

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.

andrewrk commented 5 years ago

comptime hack_around_comptime_cache: comptime_int this could be a type and then you pass @OpaqueType() rather than 0, 1, etc.

Hejsil commented 5 years ago

@andrewrk Aaah, nice!

daurnimator commented 5 years ago

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.

emekoi commented 5 years ago

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.

fengb commented 5 years ago

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.

kyle-github commented 5 years ago

@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 :-(

fengb commented 4 years ago

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:

  1. No operator overloading. Postfix only coerces to and from the native type.
  2. It still allows you to use operators.
  3. Not a completely new concept since it's similar to pointer unwrapping.
kyle-github commented 4 years ago

@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.

fengb commented 4 years ago

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.

bb010g commented 4 years ago

Automatically extending operators onto distinct types seems problematic. What if @distinct took a whtelist of operators?

ghost commented 4 years ago

fengb, where does the type checking happen in your examples? It seems like this code would still pass:

some_kilometers.* += 3 * some_hours.*;
fengb commented 4 years ago

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.

kyle-github commented 4 years ago

@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.

kyle-github commented 4 years ago

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.

ikskuh commented 4 years ago

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.

ghost commented 4 years ago

Distinct types with "Number Groups"

After some thinking, I'm leaning towards the usage pattern of distinct types presented below.

Features:

Usage example:

const x = @distinct( NUMBER , NUMBERGROUP_SUBTYPE);

Where:

// "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 )

Rocknest commented 4 years ago

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:

This is also probably in the scope of #2457 - c types and apis could be annotated as 'distinct'.

ghost commented 4 years ago

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));
daurnimator commented 4 years ago

A distinct integer type seem like it could be done via an empty non-exhaustive enum (#2524).

2524 is now implemented. Could people try and give it a try for their use cases in this issue?

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)

pixelherodev commented 4 years ago

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();
ikskuh commented 4 years ago

@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)

momumi commented 4 years ago

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.

Rocknest commented 4 years ago

@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

        // ...
    });
}
momumi commented 4 years ago

@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.

Rocknest commented 4 years ago

@momumi Oh right! I forgot how confusing comptime stuff can be. As i remember there is a hack with @OpaqueType to prevent caching.