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

remove @Type from the language, replacing it with individual type-creating builtins #10710

Open
andrewrk opened this issue Jan 28, 2022 · 19 comments
Labels
accepted This proposal is planned. breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@andrewrk
Copy link
Member

andrewrk commented Jan 28, 2022

Motivation

As noted in #10705 (comment), here are some reasons that @Type is janky:

  • @Type(.Opaque) should be avoided in favor of opaque {}.
  • @Type(.Void) should be avoided in favor of void
  • @Type(.Type) should be avoided in favor of type
  • @Type(.Bool) should be avoided in favor of bool
  • @Type(.NoReturn) should be avoided in favor of noreturn.
  • @Type(.ComptimeFloat) should be avoided in favor of comptime_float
  • @Type(.ComptimeInt) should be avoided in favor of comptime_int
  • @Type(.Undefined) is equivalent to @TypeOf(undefined) and it is unclear which is preferred, and that is a problem.
  • @Type(.Null) is equivalent to @TypeOf(null) and it is unclear which is preferred, and that is a problem.
  • @Type(.EnumLiteral) is equivalent to @TypeOf(.foo) and I guess the @Type one is slightly better.
  • @Type( .{ .Optional = .{ .child = T } }) should be avoided in favor of ?T.
  • @Type( .{ .ErrorUnion = .{ .error_set = E, .payload = T } }) should be avoided in favor of E!T.
  • @Type(.BoundFn) should be avoided
  • @Type(.{ .Vector = .{ .len = len, .child = T }}) sucks, @Vector(len, child) is way better. and please don't suggest to reach into std.meta to use basic primitive types of the language.

Also, after living with @Type for a couple years now, I just have a feeling about it and I feel that I don't like it. I liked the old way better.

Proposed Changes

This is a reversal of #2907. This proposal is to do the following things:

  • Remove @Type
  • Add @Int
  • Add @Float
  • Add @Pointer
  • Add @Array
  • Add @Struct
  • Add @Enum
  • Add @Union

Related Issues

@andrewrk andrewrk added breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. labels Jan 28, 2022
@andrewrk andrewrk added this to the 0.10.0 milestone Jan 28, 2022
@daurnimator
Copy link
Contributor

Also, after living with @Type for a couple years now, I just have a feeling about it and I feel that I don't like it. I liked the old way better.

I've really enjoyed the change: the symmetry with @typeInfo is really nice and easy to keep a mental model of.

@marler8997
Copy link
Contributor

marler8997 commented Jan 28, 2022

EDIT 2024: My original comment here was based on not seeing any benefit to this change. I now see a benefit, namely, splitting up the API allows Zig to more accurately represent what can be "reified". With this I'm in favor of the change. Below is my original comment.


This change doesn't really seem like much of a win or loss to me since both mechanisms can be implemented in terms of the other. We could either keep @Type and implement these simpler variations in std.meta, or we could split up @Type into many builtins and implement the current @Type in std.meta if desired. Since I don't see much benefit in either case, I would tend to stick with the status quo. This being said, you're comparing how it feels to use @Type directly rather than a theoretical set of helper function in std.meta, do you think the "feel" would change if such helper functions existed?

P.S. If there really is benefit to splitting up @Type, is there also benefit to splitting up @typeInfo?

@ikskuh
Copy link
Contributor

ikskuh commented Jan 28, 2022

I really like this change, as it removes both unnecessary features from @Type, but also removes some boilerplate.

@Hejsil
Copy link
Contributor

Hejsil commented Jan 28, 2022

If we are going this way, then there could be a case for not having @Float, @Array as these types can be constructed using comptime logic alone:

/// @Float
switch (bits) {
    16 => f16,
    32 => f32,
    64 => f64,
    80 => f80,
    128 => f128,
}

/// @Array
if (m_sentinel) |sen| [size:sen]T else [size]T

@Pointer is also possible, but requires a lot of code, which could make creating pointer types at comptime slow

@InKryption
Copy link
Contributor

InKryption commented Jan 28, 2022

If we are going this way, then there could be a case for not having @Float, @Array as these types can be constructed using comptime logic alone:

/// @Float
switch (bits) {
    16 => f16,
    32 => f32,
    64 => f64,
    80 => f80,
    128 => f128,
}

/// @Array
if (m_sentinel) |sen| [size:sen]T else [size]T

@Pointer is also possible, but requires a lot of code, which could make creating pointer types at comptime slow

If we did do this, we could very well make helper functions in std.meta, in the case of pointers, floats and arrays.

@rohlem
Copy link
Contributor

rohlem commented Jan 28, 2022

I personally really like the completeness of the current interface.
The same can be provided by userland code, however that needs to be kept up-to-date with the compiler.

Putting it in std.meta means it should always be in sync, however that puts extra pressure on that interface/design over other userland code.
I don't think granting std "authority" in this way, just because it is likely to be compiler-provided, is desirable (where not strictly necessary).

With the current interface, the simplest @typeInfo -> @Type cycle implementation is always correct (and works to the extent the language doesn't impose limitations).
I think in the new design an unrelated change (f.e. introducing a new type, like promoting c_char to a distinct type) is more likely to lead to a feature gap in previously-correct code.
(EDIT: finished edits)

@InKryption
Copy link
Contributor

InKryption commented Jan 28, 2022

@rohlem

I personally really like the completeness of the current interface.

I don't think completeness for the sake of completeness is really that great; currently, as outlined in the proposal, all this completeness does is create two ways to declare a lot of types, in ways that add no utility to comptime code.

The same can be provided by userland code, however that needs to be kept up-to-date with the compiler.

That is already the case with status quo; in what way would this differ, except in reducing redundancy?

Putting it in std.meta means it should always be in sync, however that puts extra pressure on that interface/design over other userland code.
I don't think granting std "authority" in this way, just because it is likely to be compiler-provided, is desirable (where not strictly necessary).

What do you mean exactly? The capability to do these things isn't being made exclusive to std, just utility functions that would ease particular use-cases, in the same way that we have std.meta.fields; you can still do @typeInfo(T).Struct.fields, but the former is more generic, and looks nicer. Also, the proposal itself hasn't actually suggested adding any of these utility functions, only some responses have.

I think in the new design an unrelated change (f.e. introducing a new type, like promoting c_char to a distinct type) is more likely to lead to a feature gap in previously-correct code.

I'd like to ask, why would it be more likely to lead to a feature gap in previously-correct code, in such a scenario? And in what way would the example of c_char be representative of this?

@apppppppple
Copy link

I like most of this change's effects but I think it's a pretty significant downside that you can no longer do @Type(@typeInfo(T)) anymore. Obviously no one would do this in real code (unless you want to duplicate a type ig?), but I've written code before that calls @typeInfo(T), modifies a few attributes, and then calls @Type on the modified data, and it's nice that the code is agnostic to if T is a struct, enum, or anything else. This could probably be implemented in userspace, but imho it's unnecessary logic to have compared to just keeping @Type, esp. since if we don't have @Type what is the purpose of the union that @typeInfo returns? Again, the union would still be used in userspace, but it just seems natural to me to have a builtin to consume the interface that another builtin produces.

@nektro
Copy link
Contributor

nektro commented Jan 30, 2022

@Type(.Null) is equivalent to @TypeOf(null) and it is unclear which is preferred, and that is a problem.

both should be avoided in favor of proper optionals. @Type(.Null) should be zero sized if not already.

@nektro
Copy link
Contributor

nektro commented Jan 30, 2022

If we did do this, we could very well make helper functions in std.meta, in the case of pointers, floats and arrays.

given how simple their construction, i think even having std.meta functions is superfluous

@nektro
Copy link
Contributor

nektro commented Jan 30, 2022

@apppppppple @Type could easily be reimplemented in std.meta after this change

@rohlem
Copy link
Contributor

rohlem commented Feb 1, 2022

@InKryption

(long text with individual replies)

I don't think completeness for the sake of completeness is really that great

Well, okay, that sounds like a difference in preference/perspective.
An operation defined as complete covers its entire domain.
An incomplete operation has exceptions to what it covers, which need to be considered (-> known/documented). In that sense it's more complex, and less self-explanatory.
Of course, if those cases are inapplicable or heterogenous in your use case, an incomplete operation may be what you want sometimes.
Generally, with Zig's "comptime is written like runtime" and "types are values" decisions/aesthetics, I'm more inclined to appreciate completeness/uniformity. (Though that property may not be objectively comparable without the context of someone's mental model.)

[...] add[s] no utility to comptime code.

The particular use case I was thinking of is similar to what @apppppppple wrote:

  • query const t = @typeInfo(T);
  • inspect the value, maybe modify / compose a new value t2
  • create a new type from that type info, currently via const new_T = @Type(t2);

It's valid to say "no code does this in practice", or bring arguments of why code shouldn't do this in practice, but having to switch on t2 and call different builtins for different kinds of types complicates this usage scenario.

The same can be provided by userland code, however that needs to be kept up-to-date with the compiler.

That is already the case with status quo

The construct @Type(@typeInfo(T)) works in status quo, and this proposal is to remove the outer function.
While I probably won't write this exact construction, I might write a more elaborate version. Any complication to @Type similarly applies to the longer form.

The capability to do these things isn't being made exclusive to std, [...]

My point was that if I need a utility function instead of @Type, that function needs to be updated.

  • If I write and use my own package, I need to update it.
  • If I use std, chances are someone else updates it.

Keeping @Type as a compiler built-in gives me the most confidence it won't go out of sync with std.builtin.TypeInfo.
Though of course, this only solves incompatible changes within the implementation of @Type, and not in any other code.

I'd like to ask, why would it be more likely to lead to a feature gap in previously-correct code, in such a scenario? And in what way would the example of c_char be representative of this?

I was originally thinking of the scenario of adding a new kind of type (not that we need one). @Type(@typeInfo(T)) continues working, while the proposed form adds a new builtin that needs to be added to the switch statement.

The previous builtins, like @IntType, separated the struct fields into arguments, so adding or removing a field would change their signature and break old code.
If the proposed builtins instead still take a single struct as an argument, then reification code can still be agnostic to these structural changes. Example:

switch(@typeInfo(T)){
 else => unreachable,
 .Int => |int_info| {
  comptime var new_info = int_info;
  new_info.bits *= 2; // agnostic to all other fields,
  return @Int(new_info);
 }
}

I might have been too pessimistic when referencing c_char: To represent it, we need to modify std.builtin.TypeInfo.Int.
Originally I thought about adding another field, though now it seems obvious to me that extending the Signedness enum with a value neither or none would be much more intuitive, so it wouldn't apply to that case.


EDIT:
Now that I've written it all out, the compatibility concerns are in respect to language changes.
If the language is by definition designed to never be changed, then these criticisms can be ignored.

In that case my point amounts to not seeing the benefit of removing the builtin over implementing it in userland code.
And I agree with @apppppppple that having @Type consume what @typeInfo produces seems the most self-explanatory and obvious choice.
But if reducing the total code of builtin function implementations itself is a worthwhile goal, then that sufficiently justifies accepting the proposal.

@czrptr
Copy link

czrptr commented Feb 3, 2022

I think a better solution would be to keep the status quo and just add convenient wrappers for @Type in std.meta. Instead of @Pointer you'd use meta.Pointer, meta.Enum instead of @Enum and so on.

This issue can be solved by a better API which in this case could be provided in the "userspace" of the language (through code accessible to the end user). I don't see why we should complicate the language with more builtins. It doesn't seem like the simplest solution in this case.

@andrewrk andrewrk added the accepted This proposal is planned. label Mar 12, 2022
andrewrk added a commit that referenced this issue Mar 24, 2022
 * std.meta: correct use of `default_value` in reification. stage1
   accepted a wrong type for `null`.
 * Sema: after instantiating a generic function, if the return type ends
   up being a comptime-known type, then we return an error, undoing the
   generic function instantiation, and making a comptime function call
   instead.
   - We also needed to clean up the dependency graph in this case.
 * Sema: reified enums set tag_ty_inferred to false since an integer tag
   type is provided. This is a limitation of the `@Type` builtin which
   will be addressed with #10710.
 * Sema: fix resolveInferredErrorSet incorrectly calling
   ensureFuncBodyAnalyzed on generic functions.
@andrewrk andrewrk modified the milestones: 0.10.0, 0.11.0 Apr 16, 2022
@tauoverpi
Copy link
Contributor

@apppppppple @Type could easily be reimplemented in std.meta after this change

There are a few things that can't be reimplemented in the language itself at present: https://gist.github.com/tauoverpi/e8fc90d44659d86a0ac25dc7fb1081bc

But it isn't much.

@marler8997
Copy link
Contributor

marler8997 commented Aug 2, 2022

@tauverpi I think the idea was to use the newly proposed @Int, @Float, @Pointer, etc. builtins to impelement Type in userspace.

P.S. your Pointer implementation is impressive nonetheless :)

@tauoverpi
Copy link
Contributor

@marler8997 Yes, those which use @Type in the implementation would @Int, @Float, and so on where the above is what works now to reimplement @Type in stage2 (as far as I've tested).

However .ErrorSet and .Opaque (as far as I understand them) won't work with the proposal.

@whatisaphone
Copy link
Contributor

@Type(.Fn) wasn't mentioned yet. If you're doing this, could you also consider adding a @Fn builtin?

For simple cases I can imagine a workaround of @TypeOf(struct { fn f() i32 { unreachable; } }.f), but if you tried to recreate all of @Type(.Fn) in userland, the combinatorial explosion of arity, calling convention, noalias, etc would quickly get out of hand.

@Lking03x
Copy link

Lking03x commented Aug 4, 2024

What about the result of @typeInfo ?
The reciprocity it have with @Type is one of the testimonial of zig's type reflection. Removing one makes the other less "real".

@mnemnion
Copy link

mnemnion commented Dec 2, 2024

I want to suggest that the intention of this change could be accomplished by limiting the allowable set of Type union fields which can be created with @Type to the selected set. The others could provide useful compile errors explaining how to go about getting the type in question.

That keeps the @typeInfo/@Type duality, which I think is rather nice, it limits profusion of builtins, while also preventing the list of duplicated ways to get a type which motivated this issue.

I'd even offer that it's less conceptual burden for the learner: @typeInfo returns a Type, @Type takes a Type and returns a type. But you're blocked from using it pointlessly. It occurs to me that with this change, it would be possible to build a Type from parts, but then there's nothing which can be done with it. Obviously the right thing there is just to construct a Struct, say, but it feels incomplete that way.

I don't have strong feelings on the subject, it just struck me as the simplest thing which would achieve the stated objective, and I hadn't seen it discussed. It does appear to solve every bullet point in the motivation section, although not the hunch, which I can't speak to one way or the other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted This proposal is planned. breaking Implementing this issue could cause existing code to no longer compile or have different behavior. 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

15 participants