This thread already covered why it’s a huge problem for multimethods and API, and that linked political blogpost has nothing to do with programming.
Optionally naming function arguments is a useful feature – but both IME and from other responses in this thread, its usefulness is mostly limited to a smallish set of functions.
It’s perfectly reasonable for function authors to opt-in in these cases. Even Base does that, I particularly like the range
function:
range(start, stop, length)
range(start, stop; length, step)
range(start; length, stop, step)
range(;start, length, stop, step)
Maybe, just advertising, promoting, recommending this approach would already help? For user/package functions where naming makes sense.
Or adding an even easier way to opt-in.
Maybe a way to generate the text of the method signatures so you can fill in or edit the bodies. It doesn’t seem like something a macro can always do automatically, the simplest _range
helper method pattern breaks down in the source code for range
because of the keyword-only step
.
It’s also important to note that without the methods to allow keywords, range
would be a 1-method function, the only possibility where positional-or-keyword arguments can’t run into dispatch ambiguity. Maybe the way to generate that text can warn “only do this to a unimethod and handle the dispatch in the positional helper methods”.
@mbauman – as you’ve probably noticed, I chose your post as the solution as I think that satisfactorily answers the “why it won’t happen” part of this question, and I think there are a lot of other good pieces of advice for workarounds in other places. To fully finish this off I’m happy to add some documentation in the Julia manual that summarizes your comment(s) as well as the workarounds, etc., then future questions relating to this can be answered by a simple link.
Do you happen to have any thoughts as to where in the docs I should put such a discussion? I’m thinking one of Methods, Style Guide, or FAQ, probably leaning towards the latter – but maybe not a bad idea to split between Methods and FAQ? Is there a different spot you’d recommend?
The FAQ seems like a good place for this. The Noteworthy differences from other languages page is also appropriate, specifically the Python and R sections; the disadvantage is that the sections in that page are pretty heavy info dumps, and the individual points are not linkable, so it’s not as convenient as a reference.
I think you’re taking the wrong conclusion here. You’ve decided that you don’t like some of Julia’s defaults so we must not care about good defaults. However that’s the wrong conclusion. We very much believe that bad defaults are a drag and that one should always have good defaults—it is crucially important and we have agonized over it many times. (The number of times Jeff and I have gotten heated about some feature requiring people to opt into the correct behavior is hard to even enumerate.) However I disagree that most of your list of supposed bad defaults are in fact bad. In this particular case, it is my opinion that being able to call positional arguments by name is a bad default in general but even more so in Julia. So please stop asserting that we don’t care about defaults (we do), when in fact we simply disagree with what you think the defaults should be. I can address the other defaults you disagree with if you’d like me to.
My comments were not meant to be directed at you or Jeff, and I apologize if I gave that impression. It was aimed at replies I’ve commonly gotten on both the Discourse and the Slack when I bring up that some default or another creates substantial difficulties for beginners. In this thread alone, there were many posts arguing that this choice of default doesn’t matter or isn’t a substantial cost. The most common kind of reply to this thread:
I still haven’t seen a single counterargument to the point that the package developer can always choose their API to be kwargs and forward to internal methods with positional args.
The mental model behind it being that because developers can do something, they will do it when appropriate. Whereas if defaults matter, what matters isn’t that they could do it, it’s that they don’t.
Some other highlights:
it’s really not though. I should hope that the most challenging part of designing a comfortable user interface isn’t writing a singular extra dispatch…
[this] bears on activation energy for choices we do not have to want to make nor want to have to begin any engagement with[, not here]
of all the things I’ve read someone claim will doom Julia, this is one of the silliest […] it’s also largely irrelevant. […] they’ll get over it.
all 5 of those are me lol
Yes, I think you’re the person who left the most replies against this proposal here, so I used your posts as examples.
That being said, I think I’m bowing out. I don’t think this conversation is going to be very productive.
I made a PR with a small addition to the docs
That PR seems strange to me because it’s about naming positional arguments but then compares it to Python, which doesn’t have named positional arguments either. Python has positional-or-keyword arguments, which are different because when a keyword is provided in the call, the argument is matched using the keyword, never the position e.g. foo(id=1, mass=5.6) == foo(mass=5.6, id=1)
. To be matched by position, an argument cannot have a keyword in the call.
When comparing Julia and Python in particular, it’s illustrative to compare how methods specify which arguments are which and why. This more or less just sums up what had been discussed in the thread earlier in one place.
Julia methods simply divide positional arguments from keyword arguments with ;
. Positional-or-keyword arguments are impossible because multimethods vary on the number and annotated type of positional-only arguments:
# p_ - match position
# k_ - match keyword provided in call
# calling foo(1, 2) would be ambiguous if k1 could be positional
function foo(p1; k1)
function foo(p1, p2)
# matching keywords in a call e.g. foo2(p2=Cat(), p1=Dog()) would be ambiguous
function foo2(p1::Dog, p2::Cat)
function foo2(p2::Cat, p1::Dog)
In a hypothetical language, types could be matched by keyword instead, but that would only make multimethods dispatch on keyword-only arguments, not allow positional-or-keyword arguments:
# not Julia: foo(k1=Dog(), k2=Cat()) != foo(k2=Dog(), k1=Cat())
# matching positions in a call e.g. foo(Dog(), Cat()) would be ambiguous
function foo(k1::Dog, k2::Cat)
function foo(k2::Dog, k1::Cat)
function foo(k2::Cat, k1::Dog) # overwrites the 1st method
function foo(k1::Cat, k2::Dog) # overwrites the 2nd method
The only way to resolve that positional ambiguity is force an order on k1, k2
in the methods, so types can be matched on position and keywords. But now we’re forced to name and order arguments consistently across all methods, e.g. a nonsensical greet(dog::Cat, cat::Dog)
after greet(dog::Dog, cat::Cat)
. This is a nightmare, so types were chosen to match by position, where calls are simpler to write.
Python can allow positional-or-keyword arguments because of its “unimethods,” though it must in turn disallow keyword arguments preceding positional arguments in calls and adds complexity (*args
, *
, /
) to dividing the arguments:
# pk_ - match keyword if provided in call, position otherwise
def foo1(pk1, pk2): # calling foo1(1, pk1=2) specifies pk1 twice and errors
def foo2(p1, p2, *pv, k1, k2): # if pv is empty, p1, p2 --> pk1, pk2
def foo3(pk1, pk2, *, k1, k2): # PEP 3102, v3.0+
def foo4(p1, p2, /, pk1, pk2, *, k1, k2): # PEP 570, v3.8+
Since Python already has keyword-able arguments, it doesn’t have named positional arguments because it’d look indistinguishable in calls and clash with the positional-preceding-keyword rule. Julia doesn’t have named positional arguments because the indistinguishability results in dispatch ambiguity:
function foo3(x, y)
function foo3(y; x)
# foo3(x=1, 2) calls the latter, would be ambiguous otherwise
No, but Python allows you to pass positional arguments by keyword, which I think is what most people are talking about in the context of this discussion, and certainly what people coming from Python to Julia might be expecting to find (and thus, what the FAQ discusses)
I considered that at first, but people in that issue seem to agree the goal is to document the meaning of positional arguments at the call site without defining variables in advance (which I don’t find harder to write at all). That is not why people pass arguments by keyword, it’s to pass arguments and override default values without being constrained by preceding arguments. For example, if we have a def foo(x=1, y=2, z=3): return (x,y,z)
, foo(z=5)
specifies a value for z
without specifying the preceding arguments; we can’t do that with position-only arguments in Python or Julia e.g. foo(5)
specifies x
.
Earlier in the thread someone had proposed a new syntax for naming position-only arguments and did not realize people were discussing position-or-keyword arguments instead, so that also made me lean toward concluding the distinction was also being muddled in the issue.
Exactly, this!
That seems like a weirdly strong assertion to make? I mean, yeah that is definitely one use case, but using this to easily maintain argument clarity when calling is, at least in my personal experience, just as useful in Python.
It’s what I was taught, what the Python tutorial section on keyword arguments highlights, and how it’s used in packages I’ve ran into (generally isn’t much different from how keyword-only arguments are used in Julia). Maybe my perspective is narrow, but I don’t really see how writing keyword arguments in their given position e.g. foo(p1=1, p2=2, p3=3)
is any clearer than defining variables then writing positional arguments in the same line p1=1; p2=2; p3=3; foo(p1, p2, p3)
. In practice, it’s usually clearer to use names that deviate slightly to distinguish multiple instances or to retrieve from an already named collection e.g. pow(bases[3], exponent2)
. Without the need to write arguments or override default values out of order, keyword arguments are not necessary or even optimal for clarity.
As another perspective, I mostly wrote named arguments in Python, and write keyword arguments in Julia, in the same order they are defined in the function. The benefits of writing their names are both documentation (clear which value is which argument), and reducing errors (a typo or mixup between arguments gets caught immediately). You cannot really get that with just creating and naming variables upfront.