The same way the OOP compilers implement them, with v-tables. Basically the compiler makes a table of function pointers so calls can be resolved (not sure resolved is the correct term) at runtime.
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.
Let's say you have an interface 'Reader' with methods 'Read' and 'Close'. In a world where no interfaces exist, every user of a different implementer of this interface would need to know exactly which 'Read' and 'Close' implementations to call, and possibly generate different code for it.
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
I'm not sure what interface means but virtual table in C++ apparently for inheritance, virtual function, and polymorphism (which is a spell or something
The rest of OOP is lipstick on arrays and arrays of arrays and "structs / records" or software defined arrays.
VTable structs. Instead of declaring `interface Foo { void bar(); }`, you do `struct Foo { this: void, bar: const fn (this: *void); }` (i.e. a struct with a function pointer field and a "this" pointer). This is how interfaces work under the hood in other languages.
Note that the code I wrote is not any particular language; it's just demonstrative.
One halfway option in Zig (though I _think_ interfaces are still being considered for addition to the language as first-class citizens) is to use the language's metaprogramming facilities.
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:
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:
I'm not sure I understand. Anytype is type-checked at compile time, not runtime, so you already have these things. The downside of anytype is that it's non-documenting, in the sense that you can't read the function signature and know what's expected.
The thing you gain is exactly that missing documentation (via @compileError and whatnot in the hypothetical library code I hand-waved away). The compiler errors can point you to the exact interface you're supposed to adhere to (as opposed to combatting errors one at a time), and by construction give you a non-out-of-date template to examine.
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.
Notice that this choice, which I agree is popular in C software, has a perf overhead. I'll illustrate:
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.
I happen to be of the opinion that Rust programs tend to heavily overuse monomorphization. It's not always so clear cut that it's worth gaining a slight amount of runtime speed in exchange for losing a massive amount of compilation speed and binary size.
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.
C syntax becomes hard to read once you nest function declarations (i.e. functions taking functions, taking functions...). But that's actually the case for most common syntaxes. Haskell type syntax is an exception, but that's a language where you're using lots of higher-order functions.
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 pointer
Rebuttal, how do you implement interfaces in OOP languages in cases where you don't yet expect the object to exist, e.g. an interface with toString and fromString methods.
I'm not going to attempt to speak for java, but at least in C#, it supports virtual static methods in interfaces, including generics.
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;
}