Since the release of Proposal #32099 - remove @TypeOf and anytype; introduce |T| syntax - ziglang/zig - Codeberg.org, I have been following it closely and I am looking forward to its acceptance. It is an important proposal to improve the user experience of Zig type system. I have some ideas about this proposal, but we need to review Zig Zen first. All of my arguments are based on it.
* Communicate intent precisely.
* Edge cases matter.
* Favor reading code over writing code.
* Only one obvious way to do things.
* Runtime crashes are better than bugs.
* Compile errors are better than runtime crashes.
* Incremental improvements.
* Avoid local maximums.
* Reduce the amount one must remember.
* Focus on code rather than style.
* Resource allocation may fail; resource deallocation must succeed.
* Memory is a resource.
* Together we serve the users.
Introduce |T| syntax
Firstly, starting from the most fundamental aspect, which is the core content of #32099, it introduces the syntax of |T| to replace @TypeOf and anytype. Its meaning is as follows:
Zig itself has variable capture syntax:
if(optional) |opt| {...}
if(error_union) |v| {...} else |err| {...}
for(list) |capture| {...}
The syntax of |T| is similar, but it is placed in the expected type position:
const a: u32 = b;
const c: |T| = a; // T is inferred to be u32
Peer Type Resolution
When using |T| instead of @TypeOf, it introduces a problem that @TypeOf itself can perform peer type resolution(as mentioned in the issue #32099), but |T| cannot do similar things.
The solution I proposed is to introduce a new built-in function specifically for peer type resolution, called @PeerType (...) (signed as fn @PeerType(type, ...) type, which allows accepting several types as parameters).
For example, in issue #32099:
pub fn clamp(val: anytype, lower: anytype, upper: anytype) @TypeOf(val, lower, upper) {
const T = @TypeOf(val, lower, upper);
// ...
}
![]()
pub fn clamp(val: |V|, lower: |L|, upper: |U|) @PeerType(val, lower, upper) {
const T = @PeerType(V, L, U);
// ...
}
It is based on zig zen’s Communicate intent precisely. @TypeOf itself does not contain any semantic information about peer type parsing, which is confusing for those who are not familiar with @TypeOf.
The expressions are evaluated, however they are guaranteed to have no runtime side-effects:
This feature is the second issue of replacing @TypeOf with |T|, which is a feature of @TypeOf and only it has this feature. In my opinion, it can be expressed through _: |T|=expr;To achieve. Assigning the value of an expression to _ means that the value of the expression is not important, so there is no need to generate runtime code for it. But if developers expect the side effects of expressions, they should use const result: |T|=expr; _ = result;.This method explicitly tells the compiler that I need to evaluate it.
_: |T| = expr;
// equivalent to
const T = @TypeOf(expr);
//----
const result: |T| = expr;
_ = result;
// equivalent to
const T = @TypeOf(result);
const result = expr;
_ = result;
This pattern is designed for the form of _: |T|, which means that even if it is in the function parameters:
fn TypeOf(_: |T|) type {
return T;
}
Because _ has already told you that the runtime value of the expression here is not important.
Type Deconstruction
This is what I most expect, because for type systems, pattern matching is more readable than the @typeInfo code. I first argue why pattern matching is more readable compared to reflective code:
For example, if you expect a pointer type and use the current code to write:
pub fn expectPointerType(expr: anytype) !void {
const info = @typeInfo(expr);
if (info != .Pointer) {
@compileError("Expected pointer type");
}
}
The caller usually only sees the function signature pub fn expectPointerType(expr: anytype) !void, Unless he is interested in jumping to the definition. But the function parameters tell me that you can accept any type, but this is obviously unreasonable! If you could directly tell me the pattern of the type you expect in the parameters, that would be great:
pub fn expectPointerType(expr: *|T|) !void {
...
}
The caller can see pub fn expectPointerType(expr: *|T|) !void, It expects a pointer type, which is obvious. Not only pointers, but also arrays and slices should be possible. For example, the API of the allocator, The parameters of the create and destroy functions expect pointers, while alloc and free expect slices. If written on the parameters, it will not confuse people.
(zig zen Favor reading code over writing code.)
Secondly, there are not many built-in compound types in the language (such as []T,?T, etc.), and using pattern matching syntax can actually reduce mental burden.
Deconstruction of Generic Functions (Maybe)
This is also the feature I am looking forward to, but there is an obvious problem. Zig’s generic functions have a many to one relationship, which means that you can only determine a specific type based on function parameters, but cannot determine the function and parameters that generate it based on a single type.
My idea for this issue is that since Zig already has a caching mechanism for evaluating generic functions, for the mapping table of (function, parameter list)=>generic type, if it is only a one-to-one mapping, then it has successfully inferred. If it is a many to one mapping, then it should report an error. In most cases, we use generic functions to adapt them to different types. For Rust or C++, if the generic parameters are different, they must not be the same type. And for many to one mapping relationships, the power to handle them specifically lies with the developers.
const a: std.ArrayList(u8) = undefined;
_: std.ArrayList(|T|) = a; // T is inferred as u8
pub fn generic(comptime N: usize) type {
return struct {
pub const T = if (N > 10) i32 else u8;
};
}
const T = generic(5);
const a: std.ArrayList(T) = undefined;
_: std.ArrayList(|T|) = a; // OK, there is only one possibility, T.
const T2 = generic(6);
const b: std.ArrayList(T) = undefined;
_: std.ArrayList(|T|) = b; // Error, there are two possibilities: T and T2
In addition, to ensure code readability, generic function deconstruction should not allow nesting.
const a: std.ArrayList(std.ArrayList(u8)) = undefined;
// Error
_: std.ArrayList(std.ArrayList(|T|)) = a;
// OK
_: std.ArrayList(|U|) = a;
_: std.ArrayList(|T|) = @as(U, undefined);
There is also |_| used for discarding values, which is similar to anytype.
I AM REALLY LOOKING FORWARD TO IT!