Implementation - interface - interface caller | all different approaches

Hi,
I come from go and functional languages.
And there is things that I feel it is not clearly addressed in most tutorial/blogs despite their good qualities. (at east for someone like me)

If you cannot afford to simply switch over multiple structs, you end up with 3 “entities”:

  1. implementation(s):

    • struct that share have a specific method/data patterns (ex: impl.next(self: Impl)
  2. an interface (VTable)

    • a reference to an struct implementation (pointer to its data and the needed methods operating on it)
    • with all the necessary mechanism to do the linking and typecasting
    • and eventually other quality of life functions that rely on the implementation’s ones (that way you can simply write it one in the interface instead of x times in each implementation)
    • ex: link_space = struct {
      pointer_to_whatever_data: *anyopaque,
      pointer_to_whatever_next_method: *fn(anyopaque)Something,
      fn link_everything( …
      }
  3. a final caller, that need to call the interface and cannot be bother about listing all possible implementations.

    • ex : fn(whatever_interface: WhateverInterface)Something {…

Is it right to think that from that point, you can freely choose :

  • to have the interface logic to be embedded into the interface caller struct
  • to have the implementation to beget the interface
    • like the std random
  • or keep all three separated ?

and then it depends on if you will switch from one implementation to another often at runtime , if multiple final callers will call the same implementation or maybe only one final caller will switch over different implementations at runtime or …

“random” example

var random_implementaton = std.Random.DefaultPrng.init(42);
const random_interface = random_implementaton.random();
need_random_interface( &random_interface );

could have been:

var rand_implementaton = std.Random.DefaultPrng.init(42);
const random_interface = RandomInterface.newFromImplementation(
   random_implementation,
);
need_random_interface( &random_interface );

?
or:

var random_interface = RandomInterface.new();
var random_implementaton = std.Random.DefaultPrng.init(42);
random_interface.linkTo(random_implementaton);
need_random_interface( &random_interface );

?
or:

var random_interface = RandomInterface.comptimeNew(.DefaultPrng);
need_random_interface( &random_interface );

?

and what would be tempted to do:

var random_implementaton = std.Random.DefaultPrng.init(42);
need_random_interface_internally.linkRandomImplementation(&random_implementaton);
_ = 
need_random_interface_internally.something_that_do_random_stuff();

?

(sorry, I probably made mistakes about what should be var/const)

Intuitively, I think it is weird that the concept of the general interface to be part of the implementation.
Generic “entities” should be aware of the different implementations not the other way around but I would expect practical use cases to defeat it

Is there legacy idioms from C/C++ that justify doing that way?
Or the expected practical usage pattern that make it de facto the right choice?
Pattern in the standard library is “standard library pattern”, different contexts involve different patterns.
Or “std lib writer had to choose one they picked that, there is no perfect solution, just deal with it” ?

Sorry if it has already been asked could not find it.

Zig in general is not big on prescribing how the code should be written. It gives you the tools to express whatever shape of (machine) code you want, and it’s on you to use them for whatever purposes.

For interfaces&implementation, there needs to be something that is aware of both implementation and interface at the same time. In Java/C++ that would be implementation/class which need to pre-declare conformance. In Rust, that would be the impl declaration. In Go, that would be the compiler, which notices that implementation is structurally compatible to the interface at the point of implicit coercion. In Zig, you can choose whatever.

implementation_instance.interface() pattern is the most pragmatic, as you’ll have to import implementaiton by name to create an instance anyway, and we can save caller one import if implementation can upcast itself to an interface.

The most important thing is that the interface is fully transparent, the vtable is public, so the caller can create one in whichever way they want.

3 Likes

Thanks, that’s exactly what I needed to know.

Instead of dictating one style, Zig let you decide whichever suits your situation the best.

I personally prefer letting the developers link the interface and implementation at the time of usage rather than hardcoding the link between them in the interface or in the implementation.

It’s like the “duck typing” of interface, i.e. as long as an implementation has methods that match the interface’s function signatures they can be linked, nothing more is required of the implementation or the interface. It’s similar to “shape matching” in TypeScript or Go.

In the blog post Zig Interface Revisited, interface and implementation are linked at usage time.

var impl_obj1 = MyImplementation1.init(...);
var impl_obj2 = MyImplementation2.init(...);
const intf1 = MyInterface.implBy(&impl_obj1);
const intf2 = MyInterface.implBy(&impl_obj2);

MyImplementation1, MyImplementation2 and MyInterface don’t know each other. It’s just that their method signatures match. The compiler will catch any mismatch at compile time.

1 Like

Instead of dictating one style, Zig let you decide whichever suits your situation the best.

Yes, and I am happy they let you do that because for example, I personally prefer the struct that consume need to interface within many things to hold the interfacing logic and I am happy zig let me do this despite the standard library doing the other way.

But then when all the standard library do one way, well you will have to accept that pattern in your head, use it anyway and it becomes, more or less, defacto, the way to do it.

It’s like the “duck typing” of interface … It’s similar to “shape matching” in TypeScript or Go.

I don’t know for you but personally, if it was more like go interface or dynamic-ish duck typing. it would have spared me dozen of hours of my life trying to picture it (I am not saying it was a waste though).

var impl_obj1 = MyImplementation1.init(...);
var impl_obj2 = MyImplementation2.init(...);
const intf1 = MyInterface.implBy(&impl_obj1);
const intf2 = MyInterface.implBy(&impl_obj2);

All the ceremonies and choices that any newbe need to go through before getting there.

For example, I am not sure that naming it virtual table (when it is actually ever fully written) help anyone that 's not from, I guess, a C/C++ background. Just think about just the function name TPtr.

Again, I am not saying that Zig has any obligation toward people that are not from a low level background, I believe it is doing well enough for people from low level ones already.