-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
dispatch behavior of optional arguments #7357
Comments
Unfortunately this would be extraordinarily difficult to do, odd as that may sound. Default arguments are exactly equivalent to arguments; the system makes no distinction, so treating default arguments specially is not natural. A check could be done at definition time, but that only works for constant types. For example |
it can certainly lead to some odd behaviors: julia> f(::Any) = 1
f (generic function with 1 method)
julia> f(x::Int="HI") = 2
f (generic function with 3 methods)
julia> f()
1 |
Yes, that's a nice piece of obfuscated code :) I think it's ultimately a good thing that default arguments are not a special feature, and just reduce to dispatch. You don't need to learn extra rules about how default arguments behave. In this example, it doesn't matter which definition contains |
I see, it's indeed good that default arguments are handled like others. Though I disagree with your statement:
It may be artificial, but it would really make sense (if easy to implement): there's no point in defining a default argument in a method which only applies to another one (apart to obfuscate code). In general, many checks are artificial: why doesn't There are still the two issues that in the first case a line number appears and does not correspond to anything; and that in the second case the function name is weird. |
I agree that it's good that default arguments are just a shorthand for defining the appropriate additional methods. But it's pretty hard to view this example as anything but problematic. Currently, we have f(x::Int="HI") = 2 meaning f(x::Int) = 2
f() = f("HI") How about making it mean this instead: f(x::Int) = 2
f() = f(convert(Int,"HI")) It's not a compile-time error, but at least it ensures that either the expected method gets called or you get a runtime conversion error, which seems much better than the current behavior. If the default is already of the expected type, then the conversion compiles down to a no-op. If we include type-checking and linting into base, enabled via |
I can imagine a case like
You might want to extend this function outside the library as follows
Note that it's perfectly valid to write
The default argument form is just a rewrite of that, so why should it be disallowed? The
If |
Because approximately nobody is going to understand how such a declaration is going to work? :-)
This is not "stuffing more behaviors", on the contrary this is limiting the complexity of the feature to make it more "simple". I don't think calling |
I don't think the convert business is as bad as the current behavior. If we can ensure that f(x::Int) = 2
f() = let x = "HI"
isa(x,Int) ? x : convert(Int,x)
end |
I would be ok with the type assert as well but it does seem a little finicky. |
The issue with abstract types is that a later definition can be more specific, and "intercept" your default argument. There are really 4 options here:
This is one of those "easy" vs. "simple" things. I'm not sure which of these is easiest, but I think (4) is objectively simplest. I believe the current rewrite that default arguments do is obvious, and is what everybody would write in the absence of the feature. The alternatives involve generating more code, forcing you to avoid the feature if you don't want the extra code. More stuff happening behind your back is not simpler. |
By the way, I think my (3) above is highly defensible. However I don't like it that much. If you write |
Yeah, that's why simply asserting sounds like a better solution to me. |
The good thing about asserting is that it's "what we do now but with more errors". So there are no silent behavior changes. |
The bad part about simply asserting, is that it is "what we do now but with more errors". And it still doesn't necessarily call the function that it "looks" connected to. I'm actually in favor of the current behavior, and just fixing the backtraces for inline functions, so it is more clear what is happening. |
Nullables make a strong case for implicit conversion since you may well want to do this sort of thing: f(x::Int, y::Nullable{Int}=nothing) = ... and be able to call the function as |
+1 for conversion+assertion. It makes sense to me that function f(x)
y::Int = z
return f(x, y)
end
f(x, y::Int) = ... which calls Note that we should also do the same conversion+assertion for keyword arguments, for the same reasons. Currently, |
I find it surprising that a default value is not currently coerced to the type of the argument it is bound to. The idea that a person can define a more 'refined' version of the method later and dispatch sends me off to the new version is quite unexpected. If the user wants this behavior they could specify a type further up the tree.
I don't think that helps with the type instability of
Should be an error |
It seems pretty unanimous at this point that conversion to the declared argument type is the right thing to do for both optional positional arguments and keyword arguments, although @JeffBezanson has been conspicuously silent on the matter. |
I guess my main question is how to lower I wouldn't call this unanimous --- both @nalimilan and @vtjnash were not in favor of converting (earlier in this thread). |
There is an asymmetry: we dispatch on the types of positional arguments, but not on the types of keyword arguments. That makes it much more natural to convert keyword arguments than positional arguments. |
To clarify: I'm all for automatic conversion of keyword arguments, but I'm more reserved about optional positional arguments. Automatic conversion to |
I also can't think of time when I've really thought I needed auto-conversion for positional arguments. It's always popped up in cases of keyword arguments only. |
The total number of methods remains the same, so it's hard to imagine it being that bad. |
There is one extra method (and function) per definition. See #7357 (comment) |
Is this realistically still happening in 0.6? |
@JeffBezanson did some experiments with this and found that the rename approach is too expensive since it increases the number of methods generated by a very large amount (+10%). There may be other ways to do it (e.g. using invoke), but this isn't happening in 0.6. |
My current thinking is that the simplicity of the existing approach is a good thing, and we should just keep it. |
Actually, I'm offering to play around with some of the changes to make this possible, and delay deciding on which way to go with this. So the plan is to make it possible to pass a Method to |
True, we haven't tried the |
Just wanted to support the current approach, as most people here expressed a preference for an alternative. I understand it can be perceived as surprising, but IMHO this should just request a warning in the docs. I can't help but finding the current behavior as correct:
The first definition is the one specifying the API, the most general one (hence it makes some sense to specify the default there). If the second one is defined, now or later, possibly in another file, it means generally (always?) that this specialization should be better for I still need to see a use case for the proposed change (with
The worst part is if the improved method (the last one) is added later in time, then one must review all existing methods with defaults to check that the correct implementation is selected. To conclude, in the case presented above here for example, if the "somebody" refers to the author of another package "somewhere else", then this somebody is commiting type piracy, so according to current Julia "good practices", this should not happen. |
It's arguable that in cases where the default value is of the given argument type the current behavior is acceptable, but in cases where the default is not of the given type are really clearly wrong. |
Yes, |
The |
For a motivating example of default argument behavior, consider: Lines 29 to 33 in d989d3e
(which is missing some coverage of correct methods). I would be nicer to be able to write those with default keywords (and more complete coverage of the set of possible method dispatch signatures), and expect that dispatch will work out as desired: SubString{T}(s::AbstractString, i::Int, j ::Int) where {T} = SubString{T}(s, i, j)
SubString{T}(s::SubString, i::Int, j::Int) where {T} = SubString{T}(s.string, s.offset + i, s.offset + j)
SubString{T}(s::T, i::Int, j::Int) where {T <: SubString} = SubString{T}(s, i, j)
SubString{T}(s::AbstractString, i::Integer=start(s), j::Integer=endof(s)) where {T} = SubString{T}(s, Int(i), Int(j))
SubString(s::AbstractString, i::Int, j::Int) = SubString{typeof(s)}(s, i, j)
SubString(s::SubString, i::Int, j::Int) = typeof(S)(s.string, s.offset + i, s.offset + j)
SubString(s::AbstractString, i::Integer=start(s), j::Integer=endof(s)) = SubString(s, Int(i), Int(j)) |
I suspect we should just punt on this for 1.0 unless someone (@vtjnash maybe) has some evidence that we can do something about this without a performance hit. |
@vtjnash 's example argues for keeping the current behavior, which I'm also in favor of. "Obviously wrong" default arg values like |
Closing since at this point, we aren't going to change behavior. |
Since empty arrays
[]
are of typeVector{None}
, it will sometimes happen that people do mistakes like this:At least, in the second case, the function name is a bit weird. In the first case, the line number doesn't correspond to anything interesting. If it's possible, it would be great to print a more explicit message, like "default argument value is of an incorrect type".
The text was updated successfully, but these errors were encountered: