Hacker News new | past | comments | ask | show | jobs | submit login
In Zig, what's a writer? (openmymind.net)
140 points by todsacerdoti 9 days ago | hide | past | favorite | 73 comments





I've been using zig for a week now. When I see a construct like writer I find myself looking for complexity that is just not there. I'm pleasantly surprised by the zig language design decisions. Initially I thought allocators were a pain but now I hardly notice them after I set up the struct init and deinit. And the myriad of zig pointer and slice types make me appreciate how C has overloaded the use of certain constructs in non-intuitive ways that we've just come to accept over the decades. I'm more impressed with zig than the last half dozen languages I've picked up - and the C/C++ interop at the language and compiler level is just the icing on the cake.

It seems to me that the problem the author writes about is bigger than the writer abstraction. To me it feels like the lack of interfaces* and inheritance in the Zig language hinders creating meaningful abstractions because you always have to use composition.

Alternatively you could argue that this just isn't the right approach within the boundaries of the Zig language: don't try to abstract, just provide a meaningful implementation (in the standard library) and re-use (using composition).

Note:

*) Yes, I know, you can create interfaces but this introduces lots of boilerplate code where to cast/forward pointers to their correct type/implementation. For an example, the author refers to another article: https://www.openmymind.net/Zig-Interfaces/.


As you note, within the Zig philosophy, I don't think this is a problem, I think it's an intentional feature.

There's no reason a feature can't be a problem though. Many would point out that inheritance is both.

Interfaces solve most of the problems of inheritance with none of the problems. Some people would point out that you can implement interfaces yourself, but that isn't a real solution since either people won't bother or there will end up being a library everyone uses to do it.

I think it's a useful abstraction and leaving it out will slow the adoption of Zig.


This and the everything is public in a struct is wild. The “document it” or use _ to mean “don't touch this” for a user is crazy IMO.

everything is public in a struct is great, otherwise sane debug printing would be impossible. there are other languages where nothing is private and operability (debugging on the fly when shit is falling down around you in prod) is unparalleled.

Rust has sane debug printing and doesn’t have to allow everything to be public. Opaque types are a thing in C for these reasons. You don’t want users of your code to depend on how it works internally.

This is exactly Zig's strength, not its problem. The flexibility/lack of interfaces allows you to choose the correct abstraction for the given task. In C++, every writer is `anytype`, in Java every writer is `AnyWriter`, in Rust every writer is `GenericWriter`. They all have tradeoffs but "fits better due to language design" shouldn't be one of the tradeoffs considered.

> in Rust every writer is `GenericWriter`.

I may be misunderstanding the article - but it looks like GenericWriter in zig still has dynamic dispatch overheads at runtime in all cases. Rust traits are more like “anytype” - since they get monomorphized by the compiler and have no runtime overhead at all. But unlike zig’s anytype, traits have excellent documentation (since they’re explicit, not implicit interfaces). Rust can also implicitly create an “AnyWriter” style object if you don’t want monomorphization via &dyn Trait. But you often don’t need to, because you can store trait objects in other structs just fine. - Though admittedly, you can do the same in zig via comptime structs.

There are a lot of things I admire about zig. But for interfaces like Writer, rust’s trait system seems like the better tool. I wish zig would copy rust’s trait system into the language.


No, GenericWriter takes a function at compile time and it gives you a GenericWriter struct that calls that function (at compile time), no function pointers needed.

There's definitely overhead with the GenericWriter, seeing as it uses the AnyWriter for every call except `write` (1)

    genericWriter        - 31987.66ns per iterations
    appendSlice          - 20112.35ns per iterations
    appendSliceOptimized - 12996.49ns per iterations

`appendSliceOptimized` is implemented using knowledge of the underlying writer, the way that say an interface implementation in Go would be able to. It's a big part of the reason that reading a file in Zig line-by-line can be so much slower than in other languages (2)

(1) https://gist.github.com/karlseguin/1d189f683797b0ee00cdb8186...

(2) https://github.com/ziglang/zig/issues/17985


Nice! It'd be fun to see a comparison in rust if anyone is keen. - Both code and resulting performance.

I was curious, so I ran your zig version myself and ported it to rust[1].

I think you forgot to run your benchmark in release mode. In debug mode, I get similar results to you. But in release mode, it runs ~5x faster than you reported:

    genericWriter        - 4035.47ns per iterations
    appendSlice          - 4026.41ns per iterations
    appendSliceOptimized - 2884.84ns per iterations
I bet the first two implementations are emitting identical code. But appendSliceOptimized is clearly much more efficient.

For some reason, rust is about twice as fast as zig in this test:

    generic_writer          time:   [1.8812 µs 1.8837 µs 1.8870 µs]
    append_slice            time:   [2.0019 µs 2.0065 µs 2.0126 µs]
    append_slice_raw        time:   [1.6932 µs 1.7041 µs 1.7182 µs]
(1µs = 1000ns)

[1] https://gist.github.com/josephg/e2dd6e7baf0a764a21bd724f8a2e...

Tests running on linux 6.8.0, CPU: Ryzen 7950x. zig v0.13 / rust v1.84.1.

Zig command: zig build --release=fast -Dtarget=native run

Rust command: RUSTFLAGS='-C target-cpu=native' cargo run --release -- --bench


If you read the end of that interfaces article, you'll see a low-boilerplate way to get interfaces using tagged enums. There's no pointer casting etc. involved.

That relies on whoever's defining the Writer knowing every implementation of Writer in existence though, right? Same reason why Rust has both enums and trait objects; it's the expression problem.

A person who is inundated with boilerplate code doesn't (yet) understand the power of code generation.

The proper technique is to "define the system", then "generate the code". Yes, it is very non-trivial and difficult, but all interesting information processing systems are always like that :-)


I think a lot of the confusion is just that it's not what people are used to.

e.g., an anytime parameter is checked by the compiler, just a like a parameter of an interface type in a language with interfaces. In either case, if you try to pass a non-conforming type you get a compiler error.

Now, with an interface, you get a symbol name you can hang your hat on. That can really help when you're in the "I'm not exactly sure what I'm doing, I'm trying to figure out this API" phase of development. API documentation and design need to compensate. But you need to be past that phase before you're writing actually useful code anyway, so this isn't that big a deal. Zig's overall simplicity really mitigates this too. You're probably going to be spending less overall time in the confusion stage.

(re "writer: anytype" the article says, "You either have to go through the source code and see how writer is used, or let the compiler tell you which function is expected." which is true, but also true of writer: IWriter. It also leaves off the option of reading the docs. doc comments are a first-class construct in zig, so they are pretty accessible.)


A couple comments have mentioned that whatever issue my post raises, are minor compared to the simplicity of the language. But the current implementation of GenericWriter + AnyWriter (with some performance pitfalls) seems more, not less, complicated. Also, neither of these, nor anytype, lend themselves to _simple_ documentation, so that seems like another strike against the simplicity-argument.

As for anytype specifically, in simple cases where it's being used in a single function, you can quickly figure out what it needs.

But in non trivial cases, the parameter can be passed all over the place, including into different packages. For example `std.json.stringify`. Not only does its own usage of `out_stream: anytype` take more than a glance, it passes it into any custom `jsonStringify` function. So you don't just need to know what `std.json.stringify` needs, but also any what any custom serialization needs.


> But in non trivial cases, the parameter can be passed all over the place

Like pretty much any parameter in any language? That's abstraction.

Re jsonStringfy, that's a way for a type to control how its own instances are serialized as JSON. std.json.stringify doesn't depend on the details of a jsonStringfy implementation (nor a caller of std.json.stringify). A type being able to implement a method to customize how instances of the type are serialized is a common/typical feature of a lot of JSON serializers.


> Like pretty much any parameter in any language? That's abstraction.

It’s crappy abstraction. Why make every user of the interface figure out how to use it properly? And guess what functions are required and which are always available?

Rust traits seem like a strictly better tool here. You get exactly the same emitted code as anytype, but the expected interface is explicit and well documented. Any time a trait name appears, you can cmd+click the trait name and see what functions are parts of the trait. It’s clean and explicit.

And traits can also have associated types (eg for an error type). They can have default implementations of functions. You can store a trait object in a struct. And you can use &dyn Trait if you want dynamic dispatch instead of monomorphisation. If this article is anything to go by, Zig makes all of this stuff difficult to impossible.

Anytype only solves the problem for the compiler. Traits solve the problem for the compiler and the programmer at the same time.


> If this article is anything to go by, Zig makes all of this stuff difficult to impossible.

Well, having done it, it’s not.


Oh go on then - how do you make a zig interface that has equivalent behaviour to a rust trait? I want something that has a documented interface, default function implementations, can be stored in other structs, and allows both monomorphization and dynamic dispatch.

If the authors of the zig standard library can't do it, I assume its impossible. And hence this article talking about 3 different definitions of a "writer" appearing in the standard library, each trying to make up for shortcomings of the others. You have anytype, AnyWriter and GenericWriter. All with weird and complex tradeoffs and interactions. Not only do people need to learn 3 different interfaces, they also need to figure out how to convert from one kind of writer to another!

I talked to Rob Pike, back before Go hit version 1.0. I told him I really thought he should have not only enums, but parametric enums in Go. Of course, he disagreed. He said enums don't add enough value. Honestly I'm not sure if he'd ever used a language with parametric enums - and he didn't understand their value. Having used typescript, and now rust and swift, going back to C or Go and needing to build my own enums out of union structs and so on is awful. It feels like I'm trying to build a house using a rock and a piece of flint.

I see rust's trait system as just like that. What a pity that Zig doesn't have something similar. To me, traits seem like an obvious thing to copy into the language.


It'd be a bit gnarly, but somebody could absolutely write a utility capable of providing those features in the userspace of the language:

1. Documented Interface: Documentation would be a struct with the desired function declarations (combined with some type-checking, I spit-balled an idea for that recently [0]).

2. Default Function Implementations: The exact tool you'll use will vary since the language hasn't hit 1.0, but a `pub usingnamespace FakeInheritanceFooBarBaz(TheTrait, @This());` could have the desired effect.

3. Stored in other structs: Rust doesn't even really give you this. It has syntactic sugar to make the dynamic case easier, and it has the ability to store concrete implementations. For the latter, Zig just works. For the former, you want something that automagically wraps any trait-conforming object in a dynamic pointer-based structure. That's a weak point in the language since the metaprogramming facilities for creating functions and for creating objects with decls aren't very solid (that will get fixed by 1.0). You could make do for now by using comptime fields instead of decls in the wrapper logic that generates this thing (think `foo: fn (A) B` instead of `fn foo(A) B {}`), and that generation code would have to handle different argument counts by hand.

4. Monomorphization: You'd get that for free, since this is just ordinary comptime duck-typing.

5. Dynamic (runtime dispatch: This is really identical to the work for (3).

[0] https://news.ycombinator.com/item?id=42899354


I followed 80% of what you've said, but not all of it. I'd love to read a sketch of this in code.

How would you clean up Writer in std?


I'll try to put together a PoC a little later. I won't have time till at least this evening though, maybe later.

It's a very nontrivial amount of code to make the library that provides all of those features, even worse to do it well. Here's a PoC for one of them [0] (default implementations).

The broad theme is that other languages use keywords for traits or subclasses or whatever. In Zig, you can define libraries using the language's metapgrogramming facilities that do the same thing (or similar things).

Zig doesn't have a complete metaprogramming story yet either, which makes it harder (no custom types with custom decls, can't create a function dynamically, ...), but there are still a few options for creating something that looks like an interface.

In my mind, all the obvious solutions have something like a "template" and utilities that operate on that template. The template serves as documentation and also a hint to the LSP. Its exact semantics aren't important; they just have to match however that library decides the implementation should look.

[0] https://github.com/hmusgrave/zinter


Awesome! Nice work. I’m still astounded by what you can pull off with comptime.

Comptime is amazing. It's a new language, but I'm excited. I got my team excited too, for some fantastic benefits at work.

> Like pretty much any parameter in any language? That's abstraction.

This is only an issue in languages with duck-typing though. You're not really abstracting something if you need to go look how it's used to understand what to pass.


In my so far very limited experience, Zig is very intentionally verbose at almost every opportunity. This isn't what some people want in a language, and I get that. But because they're SO consistent about it, Zig does end up with this effect of almost "no news is good news." Like, when something seems straight-forward it's because it literally is—if they could've been more verbose about it they would've been. So you can sorta trust that, even to the point of absence of verbosity being meaningful data point.

This is basically how you would do it in C as well.

I haven't had as much time as I would have liked to play around with Zig. But, it seems like Zig is like C, but with much stronger static checks, much less UB, lots of useful syntactic sugar, an anti-global agenda (explicitly passed allocator in Zig vs strtok in C) etc. And, truthfully, I really like this.

If we can move from a C/C++/Java world to a Zig/Rust/Go world, I would view that as an absolute win.


And the anytype stuff seems pretty close to C++ templates. I wonder how different comptime really is as compared to templates + constexpr?

As for C++, my experience is that templates can help produce some things in a type safe way that you later tend to regret and understand you should have never produced in the first place :-)

Just created some custom closure producing code that takes a pointer to any function along with arguments. The arguments get stored in custom allocations and can be applied to the function at any later point. Creating the template code is quite painful and probably none of my teammates will be able to maintain it. On the flipside, closures like this might be the right API for the use case.


Comptime is pretty much C++ templates without the need for a completely separate language for metaprogramming, which is the way it should be IMO.

(The downside is that there's no template parameter inference, but I'm pretty sure that this is one of those things that Zig would avoid by design anyway.)


More like Zig is Modula-2 with an updated syntax for C folks, plus comptime.

It is the almost safe path we could have taken if the likes of Modula-2 and Object Pascal, among others, had stayed in the mainstream.


This was one of the first things I encountered while dabbling in Zig. I spent a few hours digging through examples in the standard library and googling to see how you're supposed to pass a writer.

My day job is with TypeScript, so "any" gives me some heartache, but it seems unwarranted because Zig verifies the correctness at compile time. I won't make any claims to really understand Zig yet, but it was enough to make me feel comfortable with continuing to learn it.


I find this part of Zig challenging. I like real interfaces as a language primitive, sue me!

However, I recently just swallowed my pride and try to work by convention: provide a method on the implementation type by the same name as the `anytype` argument. `T.writer()` in this case, `T.allocator()` in others, etc.

Previously trying to battle this, I went about as far as the language would let me attempt to automate the type validation: https://github.com/nilslice/zig-interface

This at least allows you to leverage composition to combine "interfaces" - but the caller still faces the `anytype` issue.

I'd like to re-write this (with admitted of help from Claude!) to follow the stdlib pattern used in things like `std.mem.Allocator`, but I reach for this too infrequently to dedicate the time.


I love Zig, but anytype and the typed GenericWriters are a horrible mess. It is such a pain in the ass.

Allocators were a mess too before they cleaned it up. It's entirely possible writers will go a similar way (though the type erasure does scare some people). I can't claim to know what's in the team's heads, but it seems the language is very much more stable compared to before, but the stdlib is generally up for grabs for quakes if a clearer way forward is found.

That is definitely one of the things I really do love about Zig. Is that even I as a semi-inexperienced programmer have had multiple pirs accepted in zig without really any friction? And I've talked to Andrew and a few other of the guys and girls multiple times and they are very receptive and very open to well thought out well planned solutions. But in regards to any writer and any type, there have been a lot of suggestions, some of which were tentatively accepted and then didn't work out in the end and some of which have been denied such as interfaces. So I'm not sure where that will end up by 1.0

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

It seems to me that Zig is a language that has been hacked together as opposed to designed carefully. What a mess around such a simple concept!



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

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

Search: