-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
proposal: function parameters with default values #484
Comments
Combination of default parameters with function pointers can get confusing. Can function pointer have default parameter value? Can it be assigned with a function of different signature? |
I would say yes, function pointers can have default values. And you can create a function pointer from a function that has default values, and you can override the default values as well. I don't know how to do function pointers in zig, but here's some code that shows how I imagine it would work:
Note that default values do not change the ABI of the function, so all of these function are ABI compatible meaning that pointers to any them are "type compatible". Actually with this feature, you could implement a wrapper for a function by simply declaring a function pointer with default values(s)
|
Default parameters sounds a bit too implicit, but your wrapper idea sounds like creating a partial function which can be made explicit enough. I was playing around with this syntax, but none of it seems particularly clear:
Would the compiler generate a new IR block/function? Or simply "hardcode" the default parameters at the call site? |
I think not having optional parameters is pretty reasonable. It makes all these considerations go away, keeps the language smaller, and it's pretty easy to do something like: fn foo_no_default(a: i32, b: i32) { ... }
fn foo_with_one_default(a : i32) { return foo_no_defaults(a, 0); }
fn foo_with_two_defaults() { return foo_no_default(0, 0); } I do this in C/C++ code and I don't miss optional parameters at all. I find this much easier to read and reason about. |
Seems reasonable. The only advantage to something like the proposed above is being able to define the partial inside another function, but then we're getting to the realm of #229. Updated above with one more syntax idea:
Another potential advantage of this approach is clean parameter type inferrence. |
@raulgrell I'm not familair with the syntax you are using in your example, I would write your example like this:
However I don't think this is a good use case, because you could call Personal Anecdote Before I started using D years ago I did alot of C#. In C# the common pattern to emulate default values was to create wrapper functions that forward the call to the real function like in andrew's example. I was never happy with this because it took alot of typing and made development a pain. Whenever I added/removed parameters to the real function, changed parameter names, or changed types, I had to modify all the wrapper functions as well. Couple this with having to document each function and you end up in this situation where you have to repeat yourself every time you want a different default value. I remember when I started using default parameter values in D all these problems magically disappeared. I didn't need to create wrapper functions, didn't need to keep track of whether they were in sync, had less documentation to maintain and all the relevant information for the function was in one place instead of spread throughout the code. It was very clear that default parameter values were superior to wrapper functions. And guess what they added to C# in version 4...default values for function parameters. This is just my personal experience. I think default arguments make programming a little less tedious which allows you to spend more time on the fun parts. I also appreciate that zig is a limited-scope langauge. I think it should stay that way so even though I think default arguments are a clear win, they may not be right for zig. Since zig doesn't support overloading it's usage would be different than with a language like D so I can't say for sure what it would feel like with/without them. |
@marler8997: My first option was awful. Looking at your adjustment, perhaps a better way to write what I proposed would be:
The reason I propose using the I too am not a fan of so much function forwarding, but at least the semantics are plenty clear, and optimizations might be more straightforward since we're using more primitive constructs. |
Couldn't you use comptime to do this as in @raulgrell's last example:
?? This allows for a sort of poor-man's currying. Since it would all be at compile time, it should not add overhead. |
fn add(a: u8, b: u8) -> u8 { a + b }
const addOne : fn(n: u8) -> u8 = add(n, 1); This doesn't make sense semantically:
|
D'oh, copied the earlier example and did not edit it :-( Here is what I meant (not sure if this compiles):
Now I do not think this will compile because n is not known at compile time. Sigh, it sounded good in my head... |
At this point we're essentially just doing I expect that if you do Semantically, what we are doing is saying - Give me the function that remains once you've provided some parameters to another function. In this case, addOne is a pointer to a function that takes a u8 parameter and returns a u8. It is assigned to the pointer of a function which takes that parameter and is equivalent to calling the add function with the second parameter equal to 1. This kinda tricks you into thinking it's assigning to the value resulting from that operation and is therefore pretty unsuitable:
This does somewhat suggest you're taking the add function and doing something to it. There are also no parens in the right side, so there is no actual suggestion of calling it right there, which is nice. What I don't like is the declaration of the n identifier, which is inconsistent with the rest of the language:
This one makes it clear that you're returning a function, and that it's related to add. It might be too implicit though, especially regarding the type of n. Unfamiliar syntax, but it's how I imagine anonymous functions/closures - we'd keep these consistent.
At this point, the only disadvantage with the wrapper functions is namespace pollution, but there is talk of allowing scoped functions which would help. I guess I'm just saying anonymous functions are a cool complement to function wrappers. |
Really what we are aiming for is currying... That is why I was trying to figure out how some interaction with comptime might work. I admit, I kind of like the syntax:
that @raulgrell has above, but that might be my Smalltalk showing :-) It is possible that the environment capture requirement is going to make this too hard for now. |
I feel this topic has gone a bit off the rails. It seems most of the discussion is about edge cases that fail to demonstrate the common reasons why default function arguments make life much easier. Consider the following: Using Function Forwarding:
Using Default Arguments:
The first thing I'd point out is that the default argument version is quicker to decipher. It's more "information dense" because it's only provides a subset of what function forwarding does. With default arguments you don't have to worry about what the author was trying to do, you know immediately that they're just trying to provide default values for some of the arguments. Now take a look at that the "function forwarding" example again but take away the function bodies.
The first thing you should notice is that you can no longer see the default values! Now if you want your documentation to indicate what the default value is you either have to put it in your comments (and make sure it stays in sync with the code) or you have implement a special case in your doc generator to handle forward functions. Also since you can't see the function bodies, you have no idea everything but foo is just a forward function. You'll have to rely on documentation/conventions and trust that the author uses them correctly to discover this. Furthermore, another issue comes up because zig does not support overloading, namely, you have to come up with a name for the forwarding function that doesn't give you any useful information. Instead of one function named The next frustrating thing about function forwarding is "refactorability". Take a look at the example again now with added documentation. Using Default Arguments:
Using Function Forwarding:
Now think about how difficult it's going to be when you want to change something about the original foo function. Anything you change is going to create N times as much work where N is the number of default arguments you want to support for a function. Every time your add/remove a parameter you're gonna curse the day you learned about function forwarding :) |
If I may interject ..
I think the edge cases matter more than the common use case. If a feature creates too many question marks for edge cases it probably means the cost is higher than the benefits. I think a lot of languages end up bloated precisely because they try to make certain parts of coding easier, and in the process creating complications else where.
If your N is large maybe it makes more sense to pack these arguments into a struct and have the struct itself have default arguments (which AFAIK was accepted), or if the language does not support default struct params you can just create a function to initialize the struct with default params. So instead of passing separate N arguments, you just initialize a struct and pass it. |
that proposal is still pending, but you can get something very close, pretty easily by using struct initialization syntax, because if you omit a field you get a compile error. so it gives you a canonical place to put default values. |
I started another issue, #491, for currying. It may not be in Zig's future path, but @marler8997 is right that this was far afield from the original proposal. |
I don't see any real actual examples here, so let me attempt to provide one: error NotFound;
fn findString(text: []const u8, target: []const u8, startOffset: usize = 0) -> %usize {
if (text.len < target.len) return error.NotFound;
{var i: usize = startOffset; while (i < text.len - target.len) : (i += 1) {
{var j: usize = 0; while (j < target.len) : (j += 1) {
if (text[i + j] != target[j]) goto next_i;
}}
return i;
next_i:
}}
return error.NotFound;
} |
And then following up my own example, here's a workaround for not having optional parameters: error NotFound;
fn findStringFromOffset(text: []const u8, target: []const u8, startOffset: usize) -> %usize {
if (text.len < startOffset) return error.NotFound;
return startOffset + %return findString(text[startOffset..], target);
}
fn findString(text: []const u8, target: []const u8) -> %usize {
if (text.len < target.len) return error.NotFound;
{var i: usize = 0; while (i < text.len - target.len) : (i += 1) {
{var j: usize = 0; while (j < target.len) : (j += 1) {
if (text[i + j] != target[j]) goto next_i;
}}
return i;
next_i:
}}
return error.NotFound;
} |
No default parameters means that one will be forced to invent more and more unique names. This is, at least for me, the most annoying part of writing code, and the one where I seldom find satisfying solution. Reasonable IDE has no problem to navigate me to the proper function in project of any size, but it is of not help with naming. To reduce this burden rules could be reconsidered to allow more characters in names, beyond the usual alphanumerics + underscore. I do not mean full Unicode (too dangerous) but characters like #, !, %, +, which have commonly understood semantic meaning. Random example: name |
I think if you really want to use arbitrary characters in symbol names, you can use the Keeping identifier grammar simple is valuable for keeping the language easy to read. If I've been very unhappy using languages where |
@thejoshwolfe: that Expressions using special characters +/- etc are not that frequent in typical code (I wouldn't be surprised if there 100x more names than math expressions), and having more readable names IMO outweighs the minor typing inconvenience with stricter expression formatting. Back to the default values: how about making defaulted value automatically
This would avoid the horror of spreading magic literals/symbols everywhere and then tearing ones hair when the would-be-better-default value has to change. |
@andrewrk I've been reading up on the various zig proposals and I noticed this one was referenced a few times. I went back to read up on this one to understand the rationale of why it was rejected but I didn't see any explanation from you. Since this is referenced in other proposals, do you mind writing up some of your thoughts and/or rationale as to why it was rejected? I think it helps the community to get on the same page if we all understand your rationale when you accept/reject proposals. |
@MasterQ32 provided rationale for not supporting default arguments here: #3721 (comment) What I'd like to see is a document explaining language decisions and their rationale, with pointers to discussion but also summaries on why certain features are or aren't supported. Keeping everyone on the same page means discussion can be optimized as we won't need to re-hash the same arguments. This helps everyone understand the reasons certain decisions are made which helps developers infer what features and designs may or may not make sense in Zig as those same reasons will be applied to all features and designs in the language. I think this would help organize the community as Zig grows and would help everyone focus on the right things. |
https://github.com/joelparkerhenderson/architecture_decision_record |
Is there a reason not to define your method to take a struct, and have your struct have default values, like this...
|
because taking a struct requires all "parameters" in that case to be named |
No description provided.
The text was updated successfully, but these errors were encountered: