16 ms·
In Zig, what's a writer?
- benob 2y agoHow are you supposed to implement the concept of interface in non OOP languages?
- virexene 2y agoIn Zig's case, you do what Rust/C++ do implicitly and create a table of function pointers
- masklinn 2y agoInterface or interface-adjacent features are present in numerous non-OO languages: ML modules, Haskell typeclasses, rust traits, go interfaces, …
- feverzsj 2y agoUsing function pointers like most c projects do.
- jimbob45 2y agoThere’s no excuse for C not to have some syntactic sugar around function pointers at this point. They’re miserable to use.
- ajb 2y agoIt 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-automatic-dereferencing https://stackoverflow.com/questions/7518815/function-pointer... ). I suppose the : operator from lua would be useful.
- fuhsnn 2y agoWith "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 https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3450.txt
- debugnik 2y ago-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.
- jstimpfle 2y agoC 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
- tialaramex 2y agoNotice 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.
- krab 2y agoYou can still have a full object that carries both state and function pointers.
- wavemode 2y agoI 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&edition=2021&gist=1055a2f0583c4c01e1386508bb506ee4 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- GuestHNUser 2y agoFunction pointers typically
- unclad5968 2y agoThe 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.
- bsaul 2y agodo 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
- genewitch 2y agoI'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.
- frabert 2y agoLet'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
- pjmlp 2y agoBasically 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.
- Arch485 2y agoVTable 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.
- donatj 2y agoI'd do it similar to how Go does it. Zig is heavily influenced by Go to begin with.
- aiono 2y agoNon-oop languages has interfaces for a long time: Rust's traits, Haskell's typesclasses etc.
- mrkeen 2y agoRebuttal, 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.
- caspper69 2y agoWith the interface keyword (java/c#). You just define the required methods and that's the interface.
- layer8 2y agoThink again about fromString, that would be a static constructor method. The GP is looking at metaclasses.
- caspper69 2y agoI'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; }
- deleted 2y ago[deleted]
- humanfromearth9 2y ago[flagged]
- hansvm 2y agoOne 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); }
- cgh 2y agoI'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.
- hansvm 2y agoThe 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.
- feelamee 2y agoconcept interface isn't owned or invented by OOP.
- schobers 2y agoIt 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/ https://www.openmymind.net/Zig-Interfaces/.
- PaulRobinson 2y agoAs you note, within the Zig philosophy, I don't think this is a problem, I think it's an intentional feature.
- Turskarama 2y agoThere'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.
- metaltyphoon 2y agoThis 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.
- throwawaymaths 2y agoeverything 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.
- ziguratatur 2y agoI'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.
- cfiggers 2y agoIn 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.
- geetee 2y agoThis 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.
- jmull 2y agoI 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.)
- latch 2y agoA 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.
- jmull 2y ago> 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.
- serbuvlad 2y agoThis 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.
- jstimpfle 2y agoAnd 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.
- int_19h 2y agoComptime 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.)
- pjmlp 2y agoMore 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.
- sweeter 2y agoI love Zig, but anytype and the typed GenericWriters are a horrible mess. It is such a pain in the ass.
- throwawaymaths 2y agoAllocators 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.
- sweeter 2y agoThat 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
- nilslice 2y agoI 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 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.
- jvillasante 2y agoIt 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!