-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow struct method extensions #1170
Comments
Is it feasible to do this now via some two-step comptime function?
(Please excuse this rambling comment, I elaborated on a few ideas and it's a little bigger than I intended now!) comptime or @struct approach?What about solving this with a comptime namespace, maybe via macro, or some kind of syntactic sugar, which would desugar or elaborate to a single struct namespace? Or, maybe a simpler approach to multi-file namespace extension could work. I'm imagining something a little like this:
Here's another way it could be done.Allow a block to be named and elaborated in multiple files one way or another. A call to
|
What's the motivation for this? Why not just |
@thejoshwolfe I think it's about OOP-ness of the language. The only popular language I know to allow this is C# with its partial classes. My guess is that implementing this sort of thing involves potential slowdowns (vtable?) that aren't worth the benefits. |
To resolve the inconsistency and tension between utility methods and packaged methods. The example here illustrates that we have v1.dot(v2) but then cross(v1, v2) (or more probable: vec3_cross(v1, v2) ) So essentially when you want to extend the operations on a struct (that is also tightly coupled to the struct itself) you need to use a function, whereas the struct author can use a struct method. I want to argue that this is inconsistent and confusing. As I see it the struct methods are merely namespaced functions with slightly easier discovery than a free function. In particular I feel that this will make the standard library much more useful and reusable. |
@isaachier No, this is all resolved at compile time. |
OK my fear here is that importing a different set of packages effectively gives you a different class. In order to use someone else's code with such a struct, you must determine which files you need. I believe a similar issue exists with Rust traits. |
@isaachier I suggest that these functions act like any other function and need to be explicitly imported into the namespace. Consequently, added methods would be local to the context into which they are added, but would not pollute the outer context. So if you use two libraries, one which internally uses the function Vec3.foo(), and another which internally uses Vec3.bar(), you would see none of them unless they are public AND you explicitly pull them into your current context. |
One more note: since all of this essentally is syntactic sugar, you could have libraries A and B, both defining Vec3.cross - and if you want import them into your scope with different names e.g. Vec3.cross_a and Vec3.cross_b |
Switching from a closed-for-modification to open-for-modification assumption inhibits some optimizations that you don't get back until you turn on LTO, and don't get back at all with shared libraries. Feature requests for a |
@bnoordhuis How do you mean?
Would be similar to defining:
The compiler would simply expand Vec3 struct v1, So basically, when the compiler comes across v1.<some_method>(v2):
When the compiler comes across fn .( *x, ... ):
Hope that this makes things clearer: this is not related to vtables or OO. There is no dispatch on the first argument. |
I assumed your proposal implies access to non-public methods and properties of the struct. That blocks off most closed-world optimizations. If that isn't the proposal, there is no performance issue, just the philosophical issue of whether it aligns with zig's design goals. |
@bnoordhuis Only public methods and properties of the struct should be allowed (exactly what you would get from a free function). Allowing non-public method/property access would be a very, very bad idea – not just for performance reasons. For a direct example, today File has the method Let's say we wanted a "closeIfOpen" method, that only performed the close if the file was open. With this proposal we could write this as:
We can imagine convenience functions for reading an entire file as a string or data e.g. " |
The proposal exists because stdlib is perceived as untouchable. It "solves" the problem by making the situation messier. What would be ideal solution?
This assumes that stdlib is properly organized: good filenames, not a collection of tiny files. Once there was related wish: to be able to replace certain functions (in regular code, outside mock testing context). IMO it has the same solution. |
@PavelVozenilek no, not really. Other usecases:
|
I'm not sure it's valuable to be able to define a struct over several files. We'd only need a way to be able to call user defined functions on a struct with method call syntax. It might also be useful to define variables in that struct's namespace. We probably don't want to be able to add fields. We also don't want to affect method resolution all over the place - we should be able to affect only a single file or a clearly defined import chain. I think
I could import Vec3 from A more general solution is the following:
Where |
I am worried that the proposals of @lerno and @raulgrell do nothing but add unwanted complexity to Zig.
Please elaborate why one would ever need to do this. If the programmer doesn't like a struct, they roll their own. Otherwise we have people writing horrible spaghetti code all over the place, adding weird extensions to structs in the std, or something like that.
This, very much so. Just because structs have local namespaced functions, doesn't mean we should start abusing it by adding superfluous features.
I'm afraid this still has the same problem. Allowing a programmer to just easily change APIs of libraries they can't touch encourages bad programming. A language should not encourage bad programming. |
this would cover it #1214 you just make a new struct If zig has interfaces and all functions ( |
Not as elegant as extending, but a working solution. Also, embedded structs is something I'm all for. |
It is really convenient, and often helpful to readability, to define the methods of a type across multiple files in a module. Since the type is simply acting as a namespace for its methods and contained values, isn't the real question namespace extension? I.e., while it is useful to define a new namespace to contain extensions to an existing namespace, and that should be supported cleanly (yay Go embedded structs), it isn't the use-case of this issue, is it? Unless you're really proposing that if a type wants to be defined across multiple files, it should be composed out of a few structs, one from each of those files? |
not sure I agree, I think I don't for me this issue would be an edge use case and embedded struct would be good enough |
Using multiple files gives a lot more flexibility. Extensibility suffers otherwise. |
You can still make new functions in another file for the same struct. They'll just not be in the same namespace. (As long as you don't need anything private.) |
This is a common issue in Java:
As opposed to:
|
You can just pass string as an argument rather than defining the method on the string class/ Struct. |
It's actually one reason why I don't like methods on classes and such. I prefer to have free functions. But that does get harder with duck typing and such and becomes pretty much impossible if you don't have function overloading. |
@BarabasGitHub |
@monouser7dig sure, I don't disagree with namespaces, but how do you make a generic function if you don't have methods on a struct to call and no function overloading? (I'm not saying we should have function overloading. I'm wondering how to solve that issue if your functions aren't on the object itself.) |
This. But it's way too hard to change this anymore in Zig. :/
Wrong. Andrew says this: #1239 (comment) |
@monouser7dig For me personally at least this is a problem. If you have everything as free functions then everything is fine, we have:
And then you can add your function
Discoverability is fine, because you assume all functions relating to string shares a common prefix. But if you then decide to allow "functions on structs" you kill that uniformity. Basically it comes down to naming. If the one designing struct Foo uses struct functions for all calls, then what should I use as naming standard? Ok, so I pick one, like stringisValid( ... ), but then I use another library, which uses isStringEmail( ... ) or stringUtilsIsEmail( ... ). Consistency and discoverability suffers. Sure, I can start renaming every function, but wouldn't it to simplify consistency? @karlguy The comment on #1239 is not relevant. Let's say we create a generic 3D vector: Vector3. We then create a few methods to it, with conversion etc. We're on OS X, so we want to use both Metal and (the now deprecated) OpenGL. Consequently, we want a vector3.toGL() and vector3.toMetal() for each conversion (let's assume they are different). Now I obviously don't want to include both the GL and Metal code in my Vector3 source file, because I want to localize the actual implementation to the subsystems that actually do the rendering. If there is no way to borrow the namespace, then obviously I'd have to use vector3ToGl(...) and vector3ToMetal(...), but I want to demonstrate that even if I own the struct itself, it's not obvious that I want to place all its functions in the same file. |
I think there are multiple, differing use-cases being discussed here.
I am talking about C++ style definition of names in a namespace across multiple files in a single folder. For example, "read" and "write" might be implemented on a "file" struct in two files,
I definitely do not favor extending a pre-existing / imported namespace with new methods (
type MyString string // or struct { internal string } or whatever
func (m MyString) Reverse() string { return /* do stuff */ } Note that in Rust you can add new methods to existing types as traits, and I think that is also a separate conversation. For example, you could define a "Buffer" method, which operates on any implementation of a "Read" trait. Then, by importing the definition of your It's weird and complicated to use, but arguably powerful. However, this is dealing with generics, and I think it's a totally separate issue. I personally would probably not favor it -- just wrap the generic type in a struct defining your extension method. |
From my perspective it's been a powerful tool. What's a bit weird for me here is that this is a "library user" functionality. It's a way for me as user of a library to make it fit seamlessly with the rest of my code. And if I don't like it I don't need to use it. The idea is still that this would have to be explicitly imported, so if I use some lib that someone else wrote I would not get their struct extensions unless I wanted to. That this freedom is so frowned upon here is a bit disheartening, so I'm just going to quietly take a step back here. |
@lerno Why do structs even have function namespaces in the first place? It incentivises people to not use functions like vector3ToGl(). A struct is nothing but a data structure, and forcing people to decide how or where they put their interfacing functions is a waste of time (for example this github issue). The freedom here kills inconsistancy, and violates zig zen bullet 4. I want a solution to this issue as much as you do, and extending namespaces across files just feels like the wrong way to do it. And of course my opinion here is going to be unpopular, but I see no way out of this hole otherwise. EDIT: If no one tries to kill me for this idea in a bit, I'm going to try to open a github issue on this. |
Well, I still want to be allowed to define a namespace across multiple files in the same definition module. 😛 Also @lerno, you might be right that it's fine as a library user, but what happens when a library author starts extending string? |
There already is namespace extension. Look at the use of index.zig files throughout the std directories. |
You can do this in Zig as well right? Unless I misundertand. You can write a generic function that calls Read on the object that you pass in. |
@lerno may I keep the issue open until I have fully evaluated it? Admittedly my attention has been elsewhere during this discussion. |
@BarabasGitHub, not exactly: http://xion.io/post/code/rust-extension-traits.html
impl<T> OptionMutExt<T> for Option<T> {
fn replace(&mut self, val: T) -> Option<T> {
self.replace_with(move || val)
}
// ...
}
use ext::rust::OptionMutExt; // assuming you put it in ext/rust.rs
// ...somewhere...
let mut opt: Option<u32> = ...;
match opt.replace(42) {
Some(x) => debug!("Option had a value of {} before replacement", x),
None => assert_eq!(None, opt),
} Not that I'm in love with this syntax, but it shows how a method set can be extended without actually touching the method set of a particular concrete type. Maybe something to think about if Zig adds a static interface concept. |
Here's what this comes down to for me:
We have a fundamental disagreement here. I find this superior, as it's completely clear where all the identifiers are coming from. I'm strongly in favor of keeping the language simple, and allowing patterns such as the above code to emerge. |
Struct method extensions with typedef #5132 would give flexibility as to where method syntax can be used, without creating the problem that is brought up here. The method is either in the base struct, or in the typedef. There are only these two possibilities, so methods cannot be scattered around in different scopes and files. const Td_u32 = typedef(u32, .Alias) {
fn method(self: @This()) @This() {
return (self + 1);
}
fn init(value : u32) @This() {
return @as(@This(),value);
}
}
test "" {
const my_u32 = Td_u32.init(0); // access top level declaration of Td_u32 namespace
_ = my_u32.methodCall(); // works.
// typedef namespace contained a method,
// irrespective of base type being a primitive
} Or to take the example that was presented, and how it would look with typedef. string.toUpper();
string.toLower();
string.split(":");
// MyString is typedef wrapping the type of string
const mystring : MyString = string; // coerce to the typedef
mystring.splitOnCharacter( ':') // is a function in MyString
mystring.isValidAsEmail(); // is a function in MyString
|
I think this proposal's concept is more like the extend function in Kotlin. with this feature, we can have powerful builder type API like:
without this feature, we have to
Rust doesn't have this feature, so people made a lot of ugly(I think) XXXExt type just want to run that function after a dot. @andrewrk Hence I highly suggest to reopen it. |
I would like to suggest that struct methods could be defined outside of the struct and even in different source files.
For example, assume we have a Vec3 struct, defined like this:
The proposal is that one could define extensions in other files like this:
One would obviously need to discuss how (if) this should be supported for generic structs.
This is not UFCS ( discussed in #148 ), but much more narrow change that might be a better fit for Zig.
The text was updated successfully, but these errors were encountered: