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

How are you supposed to implement the concept of interface in non OOP languages?





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.


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.


Interface or interface-adjacent features are present in numerous non-OO languages: ML modules, Haskell typeclasses, rust traits, go interfaces, …

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:

  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);
  }

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.


Non-oop languages has interfaces for a long time: Rust's traits, Haskell's typesclasses etc.

In Zig's case, you do what Rust/C++ do implicitly and create a table of function pointers

I'd do it similar to how Go does it. Zig is heavily influenced by Go to begin with.

Using function pointers like most c projects do.

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.

Basically, an automation of this technique: https://play.rust-lang.org/?version=stable&mode=debug&editio...



You can still have a full object that carries both state and function pointers.

There’s no excuse for C not to have some syntactic sugar around function pointers at this point. They’re miserable to use.

With "typeof" standardized in C23 it kinda does the job:

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.

It does. You don't need to use & on a function or * on a function pointer, this happens automatically (see https://stackoverflow.com/questions/7518815/function-pointer... ).

I suppose the : operator from lua would be useful.


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

concept interface isn't owned or invented by OOP.

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.

With the interface keyword (java/c#).

You just define the required methods and that's the interface.


Think again about fromString, that would be a static constructor method. The GP is looking at metaclasses.

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;
  }

Function pointers typically



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

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

Search: