Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request: distinct types #1595

Closed
emekoi opened this issue Sep 27, 2018 · 96 comments
Closed

request: distinct types #1595

emekoi opened this issue Sep 27, 2018 · 96 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@emekoi
Copy link
Contributor

emekoi commented Sep 27, 2018

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
Copy link

ghost commented Sep 27, 2018

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

I'm strongly for this 😃 👍

@nodefish
Copy link

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
Copy link

XVilka commented Sep 27, 2018

Certainly a good thing to have.

@emekoi
Copy link
Contributor Author

emekoi commented Sep 27, 2018

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
Copy link

ghost commented Sep 27, 2018

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 andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Sep 27, 2018
@andrewrk andrewrk added this to the 0.4.0 milestone Sep 27, 2018
@andrewrk
Copy link
Member

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
Copy link

nodefish commented Sep 27, 2018

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
Copy link
Contributor Author

emekoi commented Sep 27, 2018

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

@andrewrk andrewrk modified the milestones: 0.4.0, 0.5.0 Sep 28, 2018
@raulgrell
Copy link
Contributor

raulgrell commented Sep 28, 2018

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
Copy link

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
Copy link
Contributor Author

emekoi commented Sep 28, 2018

@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
Copy link

Ilariel commented Sep 28, 2018

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
Copy link
Contributor

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:

  • A library will provide and accepts objects of a type that the client isn't supposed to do anything else with. These objects function as handles.
  • The handle must have some concrete in-memory representation so that the client and library can communicate coherently.

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

@tgschultz
Copy link
Contributor

tgschultz commented Sep 29, 2018

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
Copy link
Contributor

BarabasGitHub commented Sep 29, 2018 via email

@raulgrell
Copy link
Contributor

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

@ghost
Copy link

ghost commented Sep 29, 2018

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
Copy link
Contributor

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
Copy link

Meai1 commented Oct 18, 2018

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
Copy link
Contributor

Hejsil commented Oct 18, 2018

@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
Copy link

Meai1 commented Oct 18, 2018

@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
Copy link
Contributor Author

emekoi commented Oct 18, 2018

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
Copy link
Member

andrewrk commented Oct 18, 2018

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 👎 . The counter-argument is that it Clearly Prevents Bugs 👍

Here's a question to consider: in a pure zig codebase, would there be a reason to use @OpaqueHandle?

@ghost
Copy link

ghost commented Oct 18, 2018

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)
#1595 (comment)

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
Copy link
Contributor

Hejsil commented Nov 2, 2018

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
Copy link
Member

andrewrk commented Nov 2, 2018

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

@presentfactory
Copy link

presentfactory commented Apr 12, 2024

Has there been any progress on implementing something like this? I feel like something like this would be a fairly simple thing to add to the compiler, but it'd help prevent a lot of bugs as others have noted (though I'm not a compiler dev so maybe I am misunderstanding the complexity here). Just curious because it has been over 5 years now since the original proposal.

@leecannon
Copy link
Contributor

@presentfactory one reason this has not really progressed is that it is already possible in status quo to represent distinct integer types.

Just combining enum backing type + non-exhaustive enum + @intFromEnum & @enumFromInt gets you basically the same behaviour as @distinct would have.

const Program = enum(u32) { _ };
const Shader = enum(u32) { _ };

pub fn glAttachShader(program: Program, shader: Shader) void {
    // use `@intFromEnum` to get the values
}

@presentfactory
Copy link

@leecannon I mean sure but that's kinda ugly and imo not a good solution to the issue more generally since it does not work for more complex types like structs.

Often I have something like a Vec4 which say represents a quaternion and not a position and I'd like to make a distinction there so I don't accidentally pass something intended as a rotation to a function expecting say a position or a color. This like others have said has caused me preventable bugs in the past, so a more general solution is needed.

@SuperAuguste
Copy link
Contributor

does not work for more complex types like structs

This is a non-problem for structs and unions, though. The solution for those types is to just make distinct structures for each different representation, which is what you should be doing regardless.

For example, both types below are distinct:

const Rgba = struct {r: f32, g: f32, b: f32, a: f32};
const Vec4 = struct {x: f32, y: f32, z: f32, w: f32};

If I have

pub fn myFunc(color: Rgba) void { ... }

calling myFunc(Vec4{ ... }) is not permissible.

@presentfactory
Copy link

presentfactory commented Apr 12, 2024

@SuperAuguste It is a problem though, I don't want to have to re-type the struct redundantly every time like that, that's just WET and bad practice.

Also the point is that while all distinct types a Color, Position and Quaternion are all a Vec4, meaning they can still use the base Vec4 functions for linear algebra operations. With the approach you propose you'd have to duplicate all the functions across all these structs, or pass them to non-member functions taking anytype which is just bad.

There's simply no way around this, distinct types are needed and that's that. The assumption that all you need is aliasing on type assignment is incorrect and there needs to be a mechanism to control this behavior. It'd be like saying all you need in a programming language is references, obviously this is untrue, copies are needed sometimes.

@Beyley
Copy link

Beyley commented Apr 12, 2024

@SuperAuguste It is a problem though, I don't want to have to re-type the struct redundantly every time like that, that's just WET and bad practice.

Its not re-typing the same struct though, RGBA should have its fields be r, g, b, a and Vec4 should have its fields be x, y, z, w these are not only distinct in the fact they represent different data, but they also should have different field naems, and also multiplying colours is not always the same as multiplying vectors. You can also use usingnamespace here with some comptime to dedupe the member functions aswell

@presentfactory
Copy link

@Beyley In this isolated case sure it has different member names but that's irrelevant, usually people implement it with a normal vector type because colors are fundamentally vectors. They fundamentally have the same primitive operations too because they are again, vectors. I do not understand why people are trying to poke holes in this, it'd be incredibly useful to have this feature and it does not matter if it does not cover every single conceivable use case.

Also yes I'm sure there's many ways to hack this like the enum method for integers but I do not want ugly hacky things to do what should be a trivial operation in the compiler. usingnamespace is not meant for this nor would anyone find that method intuitive or easy to understand, same with the enum method for integers.

@presentfactory
Copy link

presentfactory commented Apr 13, 2024

Thinking on it some more I do actually think the difficulty in "inheriting" behavior with distinct types like I propose is determining what say the return value of something is. I think though as long as things are annotated it is useful still, and when you do not want this sort of behavior some sort of function to make a total copy of the type instead would be good too (as really there's type different types of behavior here one might want). So something like:

const Position = @inherit(Vec3);
const Direction = @inherit(Vec3);

fn addPositionDirection(p: Position, d: Direction) Position {
  // Fine, p/d can call fn add(a: Vec3, b: Vec3) Vec3 as they can coerce to Vec3,
  // and the returned Vec3 can coerce back to a Position to return from this function
  return p.add(d);
}

var p: Position = ...;
var d: Direction = ...;

const v = p.add(d); // Fine, returns a Vec3
const p2: Position = p.add(d); // Fine, returns a Vec3 but coerces back to a Position
const p3 = addPositionDirection(p, d); // Fine
const p4 = addPositionDirection(d, p); // Error

And then for when this behavior is not desired (more useful for things like handles where you actually don't want them to be compatible with their base type):

const Handle = @clone(u32);

var h1: Handle = ...;
var h2: Handle = ...;

const h3 = h1 + h2; // Fine, the addition operator conceptually is a member of this type and is cloned with it, calling fn add(a: Handle, b: Handle) Handle essentially, resulting in another Handle
const h4 = h1 + 5; // Error, even though Handle is cloned from an integer it's not able to coerce like this

The issue though with cloning things like this however as you lose the ability to do any operations on the type really with say normal integers. This is especially a problem with primitive types like this as you cannot actually add new behavior to them (as they aren't really structs you can add new methods to unlike a user-defined type). To solve that there would probably need to be some sort of cast operator I think to allow for explicit casting between compatible clones of the type (rather than the implicit coercion of the inheritance based method).

Something like this:

const h4 = h1 + @similarCast(5); // Casts 5 to a Handle to allow it to be added
const bar = bars[@similarCast(h1)]; // Casts the Handle to a usize to allow for indexing with it

With user defined types you could probably just do this via some sort of anytype "conversion constructor" I guess which gets cloned into each instance and allows for conversions between them:

const Vec = struct {
  x: f32, y: f32,

  fn new(other: anytype) Self {
    return .{ other.x, other.y };
  }
};

const Position = @clone(Vec);
const Direction = @clone(Vec);

var p: Position = ...;
var d: Direction = ...;

// Does the same thing as what the inheriting sort of distinct types would, just a lot more verbosely, and again this only works for user defined types where this sort of anytype thing can be added
const p = Position.new(Vec.new(p) + Vec.new(d));

Overall it is a pretty tricky problem as there are multiple ways of making distinct types like this and multiple ways of solving the issues with each approach...but hopefully this bit of rambling is useful in figuring out what Zig should do. Might also be worth looking at some other languages that do this, I don't know of any myself but Nim seems to with its own method where it clones the type but without any of the methods/fields for some reason in favor of having to explicitly borrow them, and relying on explicit casts to go between similar types: https://nim-by-example.github.io/types/distinct/

@andrewrk
Copy link
Member

andrewrk commented Jul 25, 2024

This use case is adequately addressed by non-exhaustive enums and packed structs.

@andrewrk andrewrk closed this as not planned Won't fix, can't repro, duplicate, stale Jul 25, 2024
@uyha
Copy link

uyha commented Oct 20, 2024

This use case is adequately addressed by exhaustive enums and packed structs.

Just want to clarify here, you mean non-exhaustive enums instead exhaustive, right?

@cztomsik
Copy link

const Program = enum(u32) { _ };
const Shader = enum(u32) { _ };

FYI this is also what @andrewrk blogged about recently
https://ziglang.org/devlog/2024/#2024-11-04

@notcancername
Copy link
Contributor

notcancername commented Feb 19, 2025

Here's a use case that would benefit from this proposal:

pub const id = struct {
    pub const Base = enum(u64) {
        none = std.math.maxInt(u64),
        _,
    };

    pub fn base(specialized: anytype) Base {
        return @enumFromInt(@intFromEnum(specialized));
    }

    pub fn specialize(comptime Specialization: type, base_id: Base) Specialization {
        return @enumFromInt(@intFromEnum(base_id));
    }

    pub const Node = @Type(@typeInfo(Base));
    pub const Changeset = @Type(@typeInfo(Base));
    pub const ChangesetComment = @Type(@typeInfo(Base));
    pub const Way = @Type(@typeInfo(Base));
    pub const Relation = @Type(@typeInfo(Base));

    pub const User = @Type(@typeInfo(Base));

    test "id type safety" {
        const a: id.Way = @enumFromInt(1);
        const b: id.Base = id.base(a);
        const c: id.Node = id.specialize(id.Node, b);
        _ = c;
    }
};

Currently, this works, but Base can't have declarations because @Type can't reify decls, this round-trip can't replace an @distinct builtin, which is unfortunate.

@nektro
Copy link
Contributor

nektro commented Feb 19, 2025

you could add decls by defining each enum without Type/typeInfo and adding a usingnamespace mixin

@notcancername
Copy link
Contributor

@nektro True, but usingnamespace is due to be removed anyway.

@nektro
Copy link
Contributor

nektro commented Feb 19, 2025

the proposal isn't accepted yet, so having a use case for it would give more reasons for them to keep it

@castholm
Copy link
Contributor

castholm commented Feb 19, 2025

@notcancername You could consider something like this:

const std = @import("std");

comptime {
    _ = id;
}

pub const id = struct {
    fn BrandedBase(comptime brand: anytype) type {
        return enum(u64) {
            none = std.math.maxInt(u64),
            _,

            pub fn foo(self: @This()) void {
                _ = self;
            }

            comptime {
                _ = brand; // capture the parameter to make the type distinct
            }
        };
    }

    pub const Base = BrandedBase(opaque {});

    pub fn base(specialized: anytype) Base {
        return @enumFromInt(@intFromEnum(specialized));
    }

    pub fn specialize(comptime Specialization: type, base_id: Base) Specialization {
        return @enumFromInt(@intFromEnum(base_id));
    }

    pub const Node = BrandedBase(opaque {});
    pub const Changeset = BrandedBase(opaque {});
    pub const ChangesetComment = BrandedBase(opaque {});
    pub const Way = BrandedBase(opaque {});
    pub const Relation = BrandedBase(opaque {});

    pub const User = BrandedBase(opaque {});

    test "id type safety" {
        try std.testing.expect(Base != Node);
        try std.testing.expect(Base != Changeset);

        const a: id.Way = @enumFromInt(1);
        a.foo();
        const b: id.Base = id.base(a);
        b.foo();
        const c: id.Node = id.specialize(id.Node, b);
        c.foo();
    }
};

Edit: Changed string literal arguments to opaque {}.

@notcancername
Copy link
Contributor

That's fair, @castholm . Thanks! I do think the hypothetical @distinct approach is more readable, but it's nice to see that it's possible in userspace :)

@amarz45
Copy link

amarz45 commented Feb 21, 2025

This use case is adequately addressed by non-exhaustive enums and packed structs.

Those cover integers, but what about floating-point numbers? Both enums and packed structs’ backing types must by integers. For floating point numbers, I came up with this workaround:

const kib = struct {v: f32};
const num: kib = .{.v = 1.0};
std.debug.print("{d}\n", .{num.v});

Even for integers, you have to use @enumFromInt and @intFromEnum everywhere, so I think it’s even worse than the workaround I showed above.

const kib = enum(u64) {_};
const num: kib = @enumFromInt(1);
std.debug.print("{d}\n", .{@intFromEnum(num)});

Both of these examples would be much simpler if there was a distinct keyword or even an @distinct builtin:

const kib = distinct f32;
const num: kib = 1.0;
std.debug.print("{d}\n", .{num});
const kib = @distinct(u64);
const num: kib = 1;
std.debug.print("{d}\n", .{num});

@mlugg
Copy link
Member

mlugg commented Feb 21, 2025

what about floating-point numbers?

extern or packed structs. You've figured out how to do this with a normal struct already; extern or packed will give you a few more guarantees which make it equivalent to true distinct types. I don't understand why you don't think packed structs work; the backing type is irrelevant.

Even for integers, you have to use @enumFromInt and @intFromEnum everywhere

Well, of course you should have to explicitly convert between the original and "distinct" type; otherwise, it wouldn't be particularly distinct now, would it?

@amarz45
Copy link

amarz45 commented Feb 21, 2025

extern or packed structs. You've figured out how to do this with a normal struct already; extern or packed will give you a few more guarantees which make it equivalent to true distinct types. I don't understand why you don't think packed structs work; the backing type is irrelevant.

My point is, I think there should be a way to make a distinct type without having to create a struct with a single field inside it. This way, you would be able to assign numerical constants to a value of this type.

Well, of course you should have to explicitly convert between the original and "distinct" type; otherwise, it wouldn't be particularly distinct now, would it?

By distinct I mean that a value of the backing type should not be able to be assigned to a value of the distinct type without a conversion. However, a compiletime-known constant should be able to coerce to it. Some examples:

const kib = distinct f32;

const num_1: kib = 1.0; // this works
_ = num_1;

const x: f32 = 2.0;
const num_2: kib = x; // this doesn’t work
_ = num_2;

Instead, there should be some kind of conversion from a primitive type to a distinct type.

@ziglang ziglang locked as resolved and limited conversation to collaborators Feb 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests