Hacker News new | past | comments | ask | show | jobs | submit login

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.






do you have an example of what that would look like ?

I'm a bit confused about when you would construct this table and how one would use it


Basically you have something like

    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.

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


somethign like this I think. i only dabble in zig/systems stuff so there might be better/more idiomatic ways to write parts

  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

You can just used tagged enums and the inline else syntax, like this:

  const Animal = union(enum) {
      cat: Cat,
      dog: Dog,

      pub fn make_noise(self: Animal) void {
          switch (self) {
              inline else => |impl| impl.make_noise(),
          }
      }
  };

iirc there's multiple idioms that are used in different cases. i recall a nice github that laid them all out with use cases but I can't find it

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.

In my opinion.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: