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

Proposal: "embed" keyword. #9911

Closed
ghost opened this issue Oct 6, 2021 · 5 comments
Closed

Proposal: "embed" keyword. #9911

ghost opened this issue Oct 6, 2021 · 5 comments
Milestone

Comments

@ghost
Copy link

ghost commented Oct 6, 2021

Motivation

While (recreationally) trying to write a linear algebra library, i encountered a problem, where i was forced to repeat a lot of code, if i want to leave some structs' layout for user to decide (to mantain compatibility with multiple rendering APIs).

So here's possible solution!

embed keyword would be used to acces fields of a field struct through it's container.

Besides just data layout of a struct, this allows to cover most of OOP's strengths, without any of the drawbacks.

Syntax

test "syntax" {
    
    const SubStruct = struct { v: usize };

    const Struct = struct {
        embed SubStruct;
        fn useSubStruct(self: *@This()) void {
            self.v += 1;
        }
    };

    var s: Struct = .{ .v = 123 };
    s.useSubStruct();

}

Might be useful to allow naming embedded structs:

test "named embedded struct" {

    const SubStruct = struct { v: usize };

    const Struct = struct {
        embed sub_struct: SubStruct;
    };

    // struct literals can also possibly use this name
    var s: Struct = .{ .sub_struct = .{ .v = 123 } };

}

Also might be useful to be able to provide default values:

const Struct = struct {
    embed SubStruct{ .v = 3 };
};

Advantages

Data layouts

This example is pretty silly, but that type of pattern becomes infinetely more useful with matrices and quaternions.

test "use case[0]: data layouts" {
    const Vec3DataLayout = enum {
        xyz, zyx
    };

    const Vec3Data = struct {
        fn Vec3Data(comptime T: type, layout: Vec3DataLayout) type {
            return switch (layout) {
                .xyz => extern struct { x: T, y: T, z: T, const DataT = T; },
                .zyx => extern struct { z: T, y: T, x: T, const DataT = T; },
            };
        }
    }.Vec3Data;

    const Vec3 = struct {
        embed data: Vec3Data(f32, .xyz),
        fn scale(vec: *const @This(), scalar: @This().DataT) @This() {
            return .{
                .x = vec.x * scalar,
                .y = vec.y * scalar,
                .z = vec.z * scalar
            };
        }
    };
}

"Inheritance"

Embedding a struct can be used to extend a struct behaviour, without the mess of virtual methods.

const Pixel = struct { r: u8, g: u8, b: u8 };
const Rect = struct { x: f32, y: f32, w: f32, h: f32 };

extern fn loadTexture(path: []const u8) [][]const Pixel;
extern fn calculateFrames() []Rect;
extern fn draw(pixels: [][]const Pixel, rect: Rect) void;

const Sprite = struct {
    texture: [][]const Pixel,
    rect: Rect,
    fn drawSprite(self: *@This()) void {
        draw(self.texture, self.rect);
    }
};

const AnimatedSprite = struct {
    embed Sprite;
    frames: []Rect,
    current_frame: usize,
    fn advanceFrame(self: *@This()) void {
        self.current_frame = @mod(self.current_frame + 1, frames.len);
        self.rect = self.frames[self.current_frame];
    }
};

test "use case[1]: \"inheritance\"" {
    var animated_sprite: AnimatedSprite = .{
        .texture = loadTexture("texture.png"),
        .frames = calculateFrames(),
        .current_frame = 0,
        .rect = .frames[0]
    };

    while (true) {
        animated_sprite.drawSprite();
        animated_sprite.advanceFrame();
    }
}

"Interfaces"

This use case only serves making sure that fields are present and of a specified type. One can guarantee it with testing, but this is much more self-documenting.

const std = @import("std");

const Interface = struct {
    value1: u32,
    value2: i32
};

const Impl1 = struct {
    embed Interface;
    impl_value_float: f32
};

const Impl2 = struct {
    embed Interface;
    impl_value_byte: u8
};

test "use case[2]: \"interfaces\"" {

    const expectInterface = struct {
        fn expectInterface(comptime T: type) !void {
            if (!std.meta.trait.hasFields(T, .{ "value1", "value2" })) 
                return error.NotAllFieldsPresent;
            // you can also test for types
        }
    }.expectInterface;

    try expectInterface(Impl1);
    try expectInterface(Impl2);
}

Theres no way for a struct to depend on itself, so trying to fork member functions based on a container type isn't really possible, so this doesn't break "no hidden control flow" rule.


I feel like with this keyword, it is absolutely necessary to ensure that embedded struct can be used by itself. So, not depending on it's container at all, BUT:

To be useful, embedded struct needs to reinterpret it's @This() in a context of a container.

@leecannon
Copy link
Contributor

leecannon commented Oct 7, 2021

Whats the difference from status quo other than needing to not name the substructs field and use it to access its fields?

test "syntax" {
    const SubStruct = struct { v: usize };

    const Struct = struct {
        sub: SubStruct,
        fn useSubStruct(self: *@This()) void {
            self.sub.v += 1;
        }
    };

    var s: Struct = .{ .sub = .{ .v = 123 } };
    s.useSubStruct();
}

Data layout

const Vec3DataLayout = enum { xyz, zyx };

fn Vec3Data(comptime T: type, layout: Vec3DataLayout) type {
    return switch (layout) {
        .xyz => extern struct {
            x: T,
            y: T,
            z: T,
            const DataT = T;
        },
        .zyx => extern struct {
            z: T,
            y: T,
            x: T,
            const DataT = T;
        },
    };
}

test "use case[0]: data layouts" {
    const impl = Vec3Data(f32, .xyz);
    const Vec3 = struct {
        data: impl,
        fn scale(vec: *const @This(), scalar: impl.DataT) @This() {
            return .{
                .data = .{
                    .x = vec.data.x * scalar,
                    .y = vec.data.y * scalar,
                    .z = vec.data.z * scalar,
                },
            };
        }
    };
    const a: Vec3 = .{ .data = .{ .x = 1, .y = 1, .z = 1 } };
    const b = a.scale(10);

    try std.testing.expectEqual(@as(impl.DataT, 10), b.data.x);
}

Inheritance

const Sprite = struct {
    texture: [][]const Pixel,
    rect: Rect,
    fn drawSprite(self: *@This()) void {
        draw(self.texture, self.rect);
    }
};

const AnimatedSprite = struct {
    sprite: Sprite,
    frames: []Rect,
    current_frame: usize,
    fn advanceFrame(self: *@This()) void {
        self.current_frame = @mod(self.current_frame + 1, self.frames.len);
        self.sprite.rect = self.frames[self.current_frame];
    }
};

test "use case[1]: \"inheritance\"" {
    const frames = calculateFrames();

    var animated_sprite: AnimatedSprite = .{
        .sprite = .{
            .texture = loadTexture("texture.png"),
            .rect = frames[0],
        },
        .frames = frames,
        .current_frame = 0,
    };

    while (true) {
        animated_sprite.sprite.drawSprite();
        animated_sprite.advanceFrame();
    }
}

@ghost
Copy link
Author

ghost commented Oct 7, 2021

Whats the difference from status quo other than needing to not name the substructs field and use it to access its fields?

From a purely functional point of view, there's no difference besides that. It's a usability feature, and an expansion of usingnamespace principle to work on data.

@InKryption
Copy link
Contributor

Unsure of whether it's a duplicate, but this does sound very similar to #985, which was rejected. So I don't really think this will be accepted.

I think I would prefer if #7311 was accepted, which solves this differently, but better (in my purely subjective, personal opinion).

@raulgrell
Copy link
Contributor

Another proposal with similar ideas is #7969, which approaches it with a kind of abstract struct concept - partial structs.

@iacore
Copy link
Contributor

iacore commented Nov 20, 2021

You can already implement this without any language-level feature.
https://ziglang.org/documentation/0.8.1/#Type

@andrewrk andrewrk added this to the 0.9.0 milestone Nov 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants