I tried to create a declarative format framework through Zig

Zig is a language that requires flexibility. While using Zig, I was fascinated by its powerful features and did some in-depth research into what it could do, and found that it was possible to freely define and implement complex generic types at the programming level. And after many attempts to extend that feature into a tool that could be used practically, I was able to reach a conclusion.

Zig’s powerful compile-time generics are ‘an airplane can fly and turn into a jet or a spaceship, but to do so, you have to climb on top of the airplane and manually hammer and weld it.’

I thought about why it came to that. The reason was Zig’s exclusive embrace of abstraction. I know that Zig is not an object-oriented programming language. However, Zig’s compile-time generics imply some degree of object-orientation. In order for this feature to truly be powerful, an open attitude toward abstraction is needed. Zig tries to be simple and explicit. However, attempts to be explicit are counterproductive and counter-abstract. Think about it carefully. Abstracting something complex makes it simple. However, abstracting it is not explicit. Zig aims to be abstract and semi-abstract.

I want to be able to do more with Zig. However, I also want to eliminate the inconveniences that Zig is used for and that occur when collaborating on projects. After thinking about this, I think that if we restrict all the problems that occur while writing code or collaborating, the functionality will inevitably be weakened. Currently, Zig cannot prevent all problems, but I think it is a form that places restrictions on the grammar according to Zig’s design philosophy to partially solve important problems. I think Zig’s design is excellent. And if it maintains its appearance and embraces object-orientation and abstraction, I think Zig will become irreplaceable.

lib.zig:

const std = @import("std");
const Button = @import("./button.zig").Button;
const Text = @import("./text.zig").Text;

pub fn main() !void {
    const b = Button(Text(void)) {
        .child = .{
            .inside = .{
                .child = .{
                    .text = "button1",
                },
            },
        },
    };
    try b.build();
}

widget.zig:

pub fn Widget(comptime T: type) type {
    return struct {
        child: T = T {},

        const Self = @This();

        pub fn build(self: Self) !void {
            try self.child.build();
        }
    };
}

button.zig:

const Widget = @import("./widget.zig").Widget;

pub fn Button(comptime T: type) type {
    return Widget(struct {
        inside: T = T {},

        const Self = @This();

        pub fn build(self: Self) !void {
            _ = self;
        }
    });
}

text.zig:

const Widget = @import("./widget.zig").Widget;

pub fn Text(comptime T: type) type {
    return Widget(struct {
        inside: T = T {},
        text: []const u8 = "",

        const Self = @This();
    });
}

explicitness and counter-abstract are not mutually exclusive, the allocator abstractions is a great example of this, and Andrew is working on an IO abstraction as well.
both are very explicit, both are abstractions.

The only thing I can extract from this post is that you like OOP, while it does promote abstraction, abstraction != OOP.

I get why so many people like OOP, it meshes well with how humans think, and interfaces are mind-blowing when first learning about them.

OOP certainly has its uses, but it also has disadvantages, the biggest is its inefficiency. The most efficient way to do anything is to do it directly, at least in terms of resource usage and performance. Abstractions are the antitheses of that, but maybe that’s ok, really depends on what you are doing.

zig provides you the abillity to do OOP, but it doesnt make it too easy, after all zig values efficiency.

lasty regarding the code you provided, you shouldn’t assume that a given type can be default initialised.

4 Likes

Thanks for the great feedback. I like Zig’s explicit yet abstract design. I think the expression was a bit weird because I use a translator.

It would be great if we could get object-orientation without sacrificing performance, but if we can’t, I wondered if it would be better to leave object-orientation as a user choice rather than blocking it from the language itself, even if it costs a small amount. I’m not a great coder, but I’m brave enough to post it.

zig doesnt stop you from doing OOP, it just adds some friction to deter you from using it too much, zig wont block it from the language

2 Likes

I agree. However, if you look at my code, you will see that it is a code that imitates inheritance and polymorphism. It is possible to some extent, but as you said, it is not an easy way. This is because it supports polymorphism that is determined at compile time and boilerplate occurs for member access, which is the point that this code is trying to make, and I know that encapsulation is not supported yet. That is why I dared to think, “Why use another language?” if OOP features are added within the range that Zig can allow while maintaining the appearance of Zig. Of course, since the creators of the Zig language know Zig better, if there is a serious reason why they cannot accept this suggestion, I respect their will. After all, I also understand that Zig’s powerful features and performance create its main value. I wish Zig a bright future together!

I think the main reason why Zig does not have syntax support OOP-style polymorphism is readability: No one wants to read code with 5 levels of inheritance just to multiply two numbers (yes, I have seen code like that)).

By deciding not to have OOP, we will never see libraries like this in the Zig ecosystem, and I think that is far more important than having to write a bit of boilerplate if you actually want to use polymorphism.
And from my experience it is rather rare that you actually need polymorphism, I have a total of 14 vtable/function pointer interfaces in my 40000 line game, and most of these are for future modding support, not something your average application has to care about.
And for the GUI components I actually just use tagged unions, which are actually quite convenient thanks to inline else.

It would be great if we could get object-orientation without sacrificing performance

Also since you mentioned performance: any form of runtime polymorphism is slow.
The fact that Zig doesn’t have polymorphism encourages considering alternatives, like data-oriented design or tagged unions or the comptime polymorphism you used here.

6 Likes

From what I have read on Ziggit thus far, “friction” seems to be a significant feature in the Zig language. You have the freedom to do proper OOP, or even functional programming with higher-order functions, but none of it is seamless.

Rather than embrace every possible programming paradigm and become a kitchen sink language, Zig has chosen to have “sensible defaults” that steer you in the “right” direction while giving you enough wiggle room to paint outside the lines if you need/want to.

In short, perfecting Zig does not mean making everything easy.

5 Likes

So, rather than a language that opens up possibilities without any restrictions, that it’s a language that makes possibilities possible but restrains them from going too far. I understood perfectly.

5 Likes