note: This is not a syntax proposal, the colour of the shed can be decided later if this is actually accepted.
Where I want to use this my game stuff/ecs.
Given two separate types,
const Player = struct { x: u16, y: u16, z: u16, hp: u16 };
const Mob = struct { x: u16, y: u16, z: u16, hp: u16, type: enum { a, b, whatever }};
To work with them structurally we use anytype
,
fn lenFromOrigin(v: anytype) u16 {
comptime assert(
// this is only here to show what the types are
@TypeOf(v) == Player or
@TypeOf(v) == Mob
);
return @sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
Which doesn't communicate anything about the expected shape.
Under this proposal the function would become:
fn lenFromOrigin(v: shape struct { x: u16, y: u16, z: u16 }) u16 {
comptime assert(
// this didn't change much
@TypeOf(@fromShape(v)) == Player or
@TypeOf(@fromShape(v)) == Mob
);
return @sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
Where shape narrows the view we have of a type passed but doesn't change the underlying type thus:
test {
var player: Player = dontCareInitPlayer(); // implementation is whatever, don't care
var mob: Mob = dontCareInitMob(); // ditto
const plen: u16 = lenFromOrigin(player);
const mlen: u16 = lenFromOrigin(Mob);
_ = plen;
_ = mlen;
}
Works as you expect with anytype
.
You cannot construct a shape thus the following is nonsense:
const v: shape struct { x: u32 } = .{ .x = 5 };
const u: shape union { x: u32 } = .{ .x = 5 };
const e: shape enum { a, b, c } = .a;
as shapes only reppresent the structure of an expected type and thus do not themselves have a runtime representation.
fn offset(v: shape struct { x: u32 }) usize {
return @offsetOf(@TypeOf(@fromShape(v)), "x");
}
Gives the offset of the field x
in whatever type was passed to offset
the same way as anytype
allows as shape is just a narrowed view of the
original type passed.
Since zig generics are not bijective shapes do not try to be:
fn ArrayListShape(comptime T: type) type { // or viewtype
return shape struct {
items: []T,
capacity: usize,
pub fn append(@This(), Allocator, T) error{OutOfMemory}!void;
};
}
Specifies what an ArrayList
looks like in this future where that
Managed
types have been removed. Thus std.io
could use:
pub inline fn readUntilDelimiterArrayList(
self: Self,
array_list: ArrayListShape(u8),
delimiter: u8,
max_size: usize,
} (NoEofError || Allocator.Error || error{StreamTooLong})!void {
// ...
// implementation remains the same
}
If it didn't care that the type is specifically ArrayList(u8)
.
Since it only ever uses a subset of ArrayList(u8)
it could even
use a subset of the definition of ArrayListShape
such that a user
could pass their own ArrayList
implementation. (note: I just
picked the first example I remembered that uses ArrayList as a
parameter, not suggestiong this interface should be made anytype
generic).
Shapes use structural equivalence thus compare equal even if order differs.
const SA = shape sturct { a: u32, b: bool };
const SB = shape struct { b: bool, a: u32 };
comptime assert(SA == SB);
as a consequence @typeInfo(SA).@"shape".fields
would need to be
sorted by field name along with .decls
so there's no observable
difference wrt to field order observable at comptime.
I assume it would be something like this:
/// Type of the child which this shape represents.
/// This is only set when another type has coerced
/// to this shape as to not lose track of what the
/// original type is. Otherwise if a shape is inspected
/// directly via `@typeInfo(shape struct {})` then
/// this field is `null`.
child: ?type,
/// Definition of a shape where entries are sorted
/// by name of `mem.order(..)`.
shape: union(enum) {
@"opaque": []const Declaration,
@"struct": struct {
decls: []const Declaration,
fields: []const StructField,
},
@"union": struct {
decls: []const Declaration,
fields: []const UnionField,
},
@"enum": struct {
decls: []const Declaration,
fields: []const [:0]const u8,
},
}
Shapes are views and cannot have their own functions thus none of the functions declared within a shape have a function body as it doesn't make any sense (shaped only describe what other types look like).
Someone may argue that shapes should allow methods too to be just like haskell typeclasses or rust traits but that's rabbit hole I don't want to explore as things would explode in complexity and shapes would no longer compare equal (something something HOTT is never making it into zig anyway).
Any type which satisfies the shape will coerce to a shape type thus
ArrayList(T)
coerces to what is effectively shape ArrayList(T)
(and any subtype of that) implicitly however going in the other
direction requires @fromShape(value)
which recovers the underlying
type ArrayList(T)
. That is to say, shapes only ever wrap types,
they don't represent distinct types themselves. This is also why
@fromShape(..)
operates on values and not types and in the
following case:
fn foo(x: shape struct {}) void {
_ = @fromShape(x).stuff;
}
is equivalent to:
fn foo(x: anytype) void {
comptime assert(@typeInfo(@TypeOf(x)) == .@"struct");
_ = x.stuff;
}
as we've discarded the shape/view and instead operated directly
on the concrete type which the user passed to foo(..)
. That is
to say, shape ..
is anytype
with a check equivalent to
hand-rolled @typeInfo(x)
with comptime assert(..)
while moving
this to the type signature to better communicate expectations.
One can now write shape agnostic code which is the point of the
proposal. It makes anytype
generics less of a pain which might
as a consequence result in more of it (or less, who knows).
- this is not ziglang/zig#21066
- based on https://elm-lang.org/docs/records
- I stole this ziglang/zig#17198 (comment)
- It's not this either ziglang/zig#20819 as this proposal only constrains what
anytype
can do and lives withanytype
(not replacing it) - This answers a, b, and c of ziglang/zig#1268
- not ziglang/zig#3620
- unlike ziglang/zig#9272 you cannot add new members to the namespace the shape describes
Whatever, even things not in this set, it doesn't matter if this proposal is rejected.
anytype sturct { .. }
shape struct { .. }
view struct { .. }
viewtype struct { .. }
interface struct { .. }
generic struct { .. }
ducktype struct { .. }
whatever struct { .. }
hiandrewimentionedthisoverircbackduringzig_0_5_0daysrelatedtoelmtype struct { .. }
What do you think about user level implementation?