In Zig or any other C like language without "interfaces", you would implement the V-table by hand, which is a common idiom in Zig.
I'm a bit confused about when you would construct this table and how one would use it
typedef struct Writer {
int (*method_one)(const char*, int);
//...
} Writer;
void takes_a_writer(const Writer *obj) {
// somewhere
obj->method_one(buff, buff_size);
// ...
}
// a method implementation
int method_one_impl(const char *buff, int n) {
//...
}
// somewhere else
Writer w;
w.method_one = method_one_impl;
//...
takes_a_writer(&w);
It isn't the best example, but should do the job giving you an overview.In order to make this mechanism generic, you can instead say, "every implementer of the 'Reader' interface has a pointer as its first field, and that pointer leads to an array of two elements: the first element is a pointer to a 'Read' method, the second to a 'Close' method."
This way, the user of a Reader knows nothing of the internals of each implementation, other than how to find its methods
const std = @import("std");
// base struct
const Animal = struct {
// points to the derived struct
ctx: *anyopaque,
// points to the vtable of the concrete type
vtable: *const VTable,
// the vtable interface derived struct must implement
const VTable = struct {
make_noise: *const fn (ctx: *anyopaque, loudness: u32) anyerror!void,
};
// call the derived struct's implementation
pub fn make_noise(animal: Animal, loudness: u32) anyerror!void {
return animal.vtable.make_noise(animal.ctx, loudness);
}
};
const Dog = struct {
// extra data
weight: u32,
// implement the interface
const vtable = Animal.VTable{
.make_noise = &make_noise,
};
pub fn make_noise(ctx: *anyopaque, loudness: u32) anyerror!void {
const dog: *Dog = @alignCast(@ptrCast(ctx));
std.debug.print("woof {} {}\n", .{ dog.weight, loudness });
}
// helper to convert to the base struct
pub fn _animal(self: *Dog) Animal {
return Animal{
.ctx = @ptrCast(self),
.vtable = &vtable,
};
}
};
const Cat = struct {
weight: u32,
const vtable = Animal.VTable{
.make_noise = &make_noise,
};
pub fn _animal(self: *Cat) Animal {
return Animal{
.ctx = @ptrCast(self),
.vtable = &vtable,
};
}
pub fn make_noise(ctx: *anyopaque, loudness: u32) anyerror!void {
const cat: *Cat = @alignCast(@ptrCast(ctx));
std.debug.print("meow {} {}\n", .{ cat.weight, loudness });
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
// list of base structs
var animal_list = std.ArrayList(Animal).init(alloc);
defer {
for (animal_list.items) |animal| {
if (animal.vtable == &Dog.vtable) {
const dog: *Dog = @alignCast(@ptrCast(animal.ctx));
alloc.destroy(dog);
} else if (animal.vtable == &Cat.vtable) {
const cat: *Cat = @alignCast(@ptrCast(animal.ctx));
alloc.destroy(cat);
}
}
animal_list.deinit();
}
for (0..20) |i| {
if (i % 2 == 0) {
var dog = try alloc.create(Dog);
dog.* = Dog{ .weight = @intCast(i) };
try animal_list.append(dog._animal());
} else {
var cat = try alloc.create(Cat);
cat.* = Cat{ .weight = @intCast(i) };
try animal_list.append(cat._animal());
}
}
// meows and woofs here
for (animal_list.items) |animal| {
try animal.make_noise(10);
}
}
ive written a couple and still find them mindbendy const Animal = union(enum) {
cat: Cat,
dog: Dog,
pub fn make_noise(self: Animal) void {
switch (self) {
inline else => |impl| impl.make_noise(),
}
}
};The rest of OOP is lipstick on arrays and arrays of arrays and "structs / records" or software defined arrays.
In my opinion.
Note that the code I wrote is not any particular language; it's just demonstrative.
Somebody, once, in the userspace of the language, needs to write a utility that reads a type and produces an interface checker for that type, so that you're able to write code like the following:
const IDog = Checker(TemplateDogType);
Then you can use that when defining a function expecting to conform to some interface: fn bark(_dog: anytype) void {
const dog: IDog(_dog) = _dog;
dog.bark();
}
You can easily get nice compiler errors, no runtime overhead, and all the usual sorts of things you expect from a simple interface system. It's just more verbose.Limiting access to non-interface methods without runtime overhead would be a bit more cumbersome I think. Off the top of my head, the following API is possible though:
fn bark(dog: anytype) void {
IDog.bark(&dog);
}It's not perfect since it's all in the userspace of the language (it'd be nicer to be able to express an interface type in the function signature), but it solves the problem you mentioned completely.
Imagine there are two functions with the same signature dog_noise and goose_noise, and goose_noise needs to set up a Honk Apparatus but dog_noise does not, it can easily Bark without prior setup.
Now suppose we want to use our own make_noise_six_times function, but we're going to pass in a function to say which noise. make_noise_six_times(dog_noise) and make_noise_six_times(goose_noise)
With this function pointer approach, make_noise_six_times has no idea about the Honk Apparatus, it will just call into goose_noise six times, each time setting up and tearing down a Honk Apparatus. At runtime these are likely CALL instructions.
However, in a language like Rust that's going to be mono-morphized, make_noise_six_times(dog_noise) and make_noise_six_times(goose_noise) end up generating two implementations which get optimised, there's a good chance the noise sub-functions are inlined - so no function calls - and the creation of the Honk Apparatus may get hoisted out of the loop for the make_noise_six_times(goose_noise) implementation, even though it's across function boundaries, so long as that obeys the "As if" rule.
The reduction in overhead can be dramatic - if your inner functions are tiny the call overhead might dwarf what they actually do, so the inlining makes the whole program orders of magnitude faster in this case. This is very noticeable for e.g. sorting, since the comparison function is executed so often in a hot loop, if that's a C function call it's so much more expensive than if it's a single inlined CPU instruction.
What I'd love is a language which is able to compile 'impl TraitName' into dynamic dispatch in debug mode and only monomorphize it in release mode.
Basically, an automation of this technique: https://play.rust-lang.org/?version=stable&mode=debug&editio...
int(* fnptr_name)(int param) becomes typeof(int(int param))* fnptr_name
There is a recent proposal to update standard headers to this style: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3450.txt
-void (*signal(int sig, void (*func)(int)))(int);
+typeof(void (int)) *signal(int sig, typeof(void (int)) *func);
Much better honestly. The original took me a few reads to understand, until I noticed the outer (int) is part of the return type.I suppose the : operator from lua would be useful.
For the most common use cases C syntax is quite ergonomic once you've learned the principle.
// declare func as normal function
void func(ARGUMENTS);
// declare (*func) as normal function: func is a pointer to a normal function
void (*func)(ARGUMENTS);
// can also declare a type name instead of a function
typedef void functype(ARGUMENTS);
// and then use that to declare function-pointer
functype *func;
// can also declare a function pointer type directly (I don't normally do this)
typedef void (*funcptrtype)(ARGUMENTS);
funcptrtype func; // declare function pointerYou just define the required methods and that's the interface.
So, as I stated, you would use the interface keyword.
public interface IFromString<T>
{
static abstract T FromString(string value);
}
public struct MyHasFromString : IFromString<MyHasFromString>
{
public string Text = "";
public MyHasFromString(string value)
{
Text = value;
}
public static MyHasFromString FromString(string value)
=> return new MyHasFromString(value);
public override string ToString() => Text;
}