Three options:
- Zero length field:
pub fn ObjectProtocol(comptime T: type) type {
return extern struct {
pub inline fn incref(m: *@This()) void {
const self: *T = @alignCast(@fieldParentPtr("as_object", m));
// Use the _ variant since it should be non-null
c._Py_IncRef(@ptrCast(self));
}
pub inline fn decref(self: *@This()) void {
const self: *T = @alignCast(@fieldParentPtr("as_object", m));
// Use the _ variant since it should be non-null
c._Py_DecRef(@ptrCast(self));
}
// etc
};
}
pub const Object = extern struct {
// The underlying python structure
impl: c.PyObject,
// Use the object protocol
as_object: ObjectProtocol(@This()) = .{};
};
pub const Tuple = extern struct {
// The underlying python structure
impl: c.PyTupleObject,
// Tuple uses the object protocol
as_object: ObjectProtocol(@This()) = .{};
// Tuple uses the SequenceProtocol
as_sequence: SequenceProtocol(@This()) = .{};
// Some more tuple specific fns here...
};
Talk about the advantages of this approach: it provides a more explicit namespace for the functions of mixins, and if you need such a layer of abstraction, this is helpful, and better than the previous approach.
However, the essence of this approach is virtual inheritance, so it also has the issues of virtual inheritance. If the purpose of the mixin is merely code reuse, then the extra abstraction brought by this approach may be harmful, as it is âthe demand for code reuse intruding into abstractionâ.
In addition, if we regard it as an âabstract interfaceâ rather than simply âcode reuse,â we must face the fact that this interface fragilely relies on specific field names as CPOs, causing namespace pollution.
Side note: Iâm really looking forward to a âType-as-Keyâ symbol system; it would solve the fragility of string-based CPOs and make refactoring safer.
- Sacrifice the simplicity of the call site
pub fn ObjectProtocol(comptime T: type) type {
return struct {
pub inline fn incref(self: *T) void {
// Use the _ variant since it should be non-null
c._Py_IncRef(@ptrCast(self));
}
pub inline fn decref(self: *T) void {
// Use the _ variant since it should be non-null
c._Py_DecRef(@ptrCast(self));
}
// etc
};
}
pub const Object = extern struct {
// The underlying python structure
impl: c.PyObject,
// Use the object protocol
pub const object_ops = ObjectProtocol(@This());
};
pub const Tuple = extern struct {
// The underlying python structure
impl: c.PyTupleObject,
// Tuple uses the object protocol
pub const object_ops = ObjectProtocol(@This());
// Tuple uses the SequenceProtocol
pub const sequence_ops = SequenceProtocol(@This());
// Some more tuple specific fns here...
};
The drawbacks of this approach are obvious: we cannot use syntactic sugar when calling, we have to write it like this:
var obj: Object = .{ .impl = foo() };
Object.object_ops.incref(&obj);
Nevertheless, I still believe this is a more robust abstraction than the zero-length field scheme. Although the call site looks a bit ugly, it is more sound.
Edit: A variant:
pub fn ObjectProtocol(comptime T: type) type {
const allowed_types = .{Object, Tuple};
comptime {
var is_valid = false;
for (allowed_types) |ValidType| {
if (T == ValidType) is_valid = true;
}
if (!is_valid) {
@compileError(@typeName(T) ++ " is not allowed to use ObjectProtocol");
}
}
return struct {
pub inline fn incref(self: *T) void {
// Use the _ variant since it should be non-null
c._Py_IncRef(@ptrCast(self));
}
pub inline fn decref(self: *T) void {
// Use the _ variant since it should be non-null
c._Py_DecRef(@ptrCast(self));
}
// etc
};
}
pub const Object = extern struct {
// The underlying python structure
impl: c.PyObject,
};
pub const Tuple = extern struct {
// The underlying python structure
impl: c.PyTupleObject,
};
Use it like this:
var obj: Object = .{ .impl = foo() };
ObjectProtocol(Object).incref(&obj);
- Manual forwarding
pub fn ObjectProtocol(comptime T: type) type {
return struct {
pub inline fn incref(self: *T) void {
// Use the _ variant since it should be non-null
c._Py_IncRef(@ptrCast(self));
}
pub inline fn decref(self: *T) void {
// Use the _ variant since it should be non-null
c._Py_DecRef(@ptrCast(self));
}
// etc
};
}
pub const Object = extern struct {
// The underlying python structure
impl: c.PyObject,
// Use the object protocol
pub const incref = ObjectProtocol(@This()).incref;
pub const decref = ObjectProtocol(@This()).decref;
};
pub const Tuple = extern struct {
// The underlying python structure
impl: c.PyTupleObject,
// Tuple uses the object protocol
pub const incref = ObjectProtocol(@This()).incref;
pub const decref = ObjectProtocol(@This()).decref;
// Tuple uses the SequenceProtocol
pub const foo = SequenceProtocol(@This()).foo;
// Some more tuple specific fns here...
};
This approach is the most consistent with the original abstraction: if our goal is merely to avoid code repeat without introducing unnecessary layers of abstraction, this is the only reasonable approach.
If you need to manually introduce hundreds of methods using this approach, compared to the practice of using namespace, writing it this way can be frustrating. However, if we adhere to the principle of âreader first, writer second,â we will find that although this approach involves more boilerplate when writing code, it is very reader-friendly and makes it easy to locate the declaration positions of all mixin functions.
A minor readability flaw is that this approach introduces functions in a form different from conventional function declarations, which may cause declaration search schemes based on fn methodname to fail. This is an issue that readers need to get used to.
For writers, a potential problem is that copy-paste errors may easily occur, e.g. pub const decref = ObjectProtocol(@This()).incref;
Currently, there is no good solution to this problem. I am looking forward to a simple reuse solution when declaring symbols, such aspub const decref = ObjectProtocol(@This()).@currentSymbol();. However, such designs may lack semantic orthogonality and do not have enough motivation to incorporate language.
Fortunately, we can add unit tests to catch this kind of error:
test "ObjectProtocol mapping integrity" {
const Proto = ObjectProtocol(Object);
const expected_fields = @typeInfo(Proto).@"struct".decls;
inline for (expected_fields) |decl| {
if (!@hasDecl(Object, decl.name)) {
@panic("Object miss the protocal function: " ++ decl.name);
}
if (@field(Object, decl.name) != @field(Proto, decl.name)) {
@panic("Object forwarding error: " ++ decl.name ++ " forward to wrong impl");
}
}
}