I would expect it to stop me from doing that. Same with venus(y=2, 1.23). And if I loaded a new package and venus(a=1, b=2; x::Real) appeared, and it couldn’t figure out whether to call (x::Real) or (a, b; x::Real) when I used (x = 1.100) I would expect it to stop me as well because I likely made a mistake.
In reality I would hope to type half the function name, hit tab autofill <venus(x =, read the documentation for x to confirm and enter the value, and then repeat through the argument list in order, as the names and documentation are inferred from the types.
If the main idea is that it makes code more readable and self-documenting, then just use @baggepinnen@adienes’s solution above of splitting variables into different lines, and then use descriptive names.
Using the canonical example from above if
sample(Xoshiro(0), NUTS(), model)
isn’t clear or self-documenting enough, then simply add variables
rand_gen = Xoshiro(0) # pick a random number generator
nuts = NUTS() # use the NUTS sampler
model = some_likelihood # use a log likelihood model
sample(rand_gen, nuts, model) # do some sampling
In this case, by splitting variables out, the code is even more self-documenting and clear. I am having a very hard time understanding how adding new syntax to the language is better than the above option that is already currently possible.
Heck, you could even just do this
#sample(random number generator, sampler, loglikelihood)
sample(Xoshiro(0), NUTS(), model)
I would expect to either fill out all of the names exactly in the order they appear in the documentation/definition that I’m being shown in the ide, or I would expect to fill out none of the names and default back to c-like positional calls now.
If I write code like sample(rand_gen, nuts, model) I would do it in rust, and that is not as a criticism of the language, it’s because I feel more comfortable with the rust analyzer watching out for issues, and in interactive use I feel more comfortable with rstudio supporting me.
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 fact that you want to write sampler(rand_gen=rand_gen, nuts=nuts, model=model) is an issue you should take up with the designer/exporter of sampler, not a language feature
And, again, even as a user of the library, you can just define your own
function sample(;rand_gen; nuts, model, kwargs...))
return Turing.sample(rand_gen, nuts, model; kwargs)
end
I maybe originally forgot that you don’t even need to make the sample wrapper a new method of the original Turing.sample, so this isn’t even type piracy. You’re totally fine creating a wrapper like that for your local project.
Comming from R to Julia, I also felt initially the need for keyword arguments. But actually, the IDE I’m ussing (VSCode) does a good job of showing me the arguments I need and their order, and I have long switched to positional arguments. Besides the multipledispatch advantage, the code is “cleaner” also.
The macro would strip the keywords from arguments that come before the semicolon before calling?
I’m not sure if this is possible because I don’t know if the parser implies the existence of the semicolon as soon as you start doing keyword args and then complains about 2 semicolons?
The fact that it’s substantially less convenient means that for years, developers haven’t been doing this. (This is substantially compounded by the substantial performance penalties for keyword arguments in older Julia versions). This means a large body of code doesn’t allow keyword arguments for no reason other than a poor choice of default (despite the costs this imposes on users).
sample is a particular example; the pattern of using positional arguments when you really should be using keyword arguments is pervasive in Julia.
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…
the pattern of using positional arguments when you really should be using keyword arguments is pervasive in Julia.
I have yet to see a problematic example without a trivial (existing!) solution
In fact, even in Python I consider it bad style to use this feature! when a parameter is defined positionally, I always call it positionally, and vice-versa for being defined as a named param. when reviewing my coworker’s PRs, if this rule is not followed I will ask for either the callsite or the signature to be changed
The problem here isn’t the motivation for the feature — it’s a fine feature for a language to have and as noted some languages use it to good effect. Folks are totally justified to like a feature like this, especially if they’re used to it. Conversely, folks are totally justified to not like such a feature; kwargs and positional args are both supported in the language and developers get to choose which makes sense for their APIs. Arguing about the motivation is going to be as productive as arguing about a favorite color.
The problem is that PSA: Julia is not at that stage of development anymore. This isn’t a simple feature to turn on; it has huge ramifications to the API surface area (and thus maintenance) of all positional-argument functions ever written. It would also have some very confused (and breaking) semantics in the presence of multiple dispatch and existing kwargs.
Instead of arguing about the motivations, I think it’d be more beneficial to look towards how we could gain some of the ergonomic advantages from the user/caller-side.
julia> macro stripkw(ex)
Meta.isexpr(ex, :call) || throw("@stripkw expects a function call") # LoadError
for i in 2:length(ex.args)
Meta.isexpr(ex.args[i], :parameters) && continue # real keywords
Meta.isexpr(ex.args[i], :kw) || continue # ordinary arguments
ex.args[i] = ex.args[i].args[2]
end
esc(ex)
end;
julia> @macroexpand @stripkw sum(f = abs, xs; dims = 1)
:(sum(abs, xs; dims = 1))
julia> @macroexpand @stripkw myfun(rng = Xoshiro(), lefttable = a, righttable = b, filtercol = :foo; actual_keyword_arg=:bar)
:(myfun(Xoshiro(), a, b, :foo; actual_keyword_arg = :bar))
But what it doesn’t do is complain if, at the call site, you use the wrong names, or the wrong order. So this is equivalent to writing comments.
A more elaborate macro could call which at runtime, when the types of the arguments are known, and throw an error if the names it is removing do not match those of the definition. This would catch errors, and also, of course, mean that your code breaks when the package changes a name (or, for instance, adds a more specialised fast path something like sum(::typeof(abs), xs::Array) without touching the old method).
What you really want is, of course, for the package author to somewhere specify which names are canonical. Which can be done, in one line, by adding an all-keyword method.
Well a keyword is just a name (which can be well-chosen and helpful though). With “concept”, I mean some more abstract part/functionality/concern of a program.
E.g., sampler can denote a concept which usually consists of a data structure and some associated interface of methods/operations you can meaningfully call on it, i.e., much more than just its name.
Further, I was referring to functions with many – say 10 or more – keyword arguments. Those are either never testable exhaustively due to combinatorial explosion or the keywords belong to independent concepts which could also be stated and tested separately. Then, instead of dumping all arguments into one function, I would prefer smaller ones with clear separation of concerns and correspondingly much fewer arguments.
In any case, if a function has few, i.e., at most about three or so, arguments, I don’t see much need for keyword arguments which do add considerable syntactic noise. There is a good reason why mathematical notation is rather terse with mostly unary and binary operators as less clutter makes it easier to identify reusable abstractions and reason about your formula/code. When I seem to need (too) many arguments, I try to pause and rethink my design. Often it can be composed from simpler concepts, i.e., instead of a monolithic (training) loop, iterate single steps and decide on termination, logging etc. later which is quite easy to achieve with iterators and higher-order functions.
Have you ever looked at or tried Smalltalk? It not only forces keywords for all methods with more than two arguments, but also has a very readable and clever syntax for keyword-methods.
You’d think it would work that way, but no. This is exactly the broader point I was making about poor defaults (and the way people tend to ignore the damage this causes). There’s an overwhelming amount of evidence from behavioral science that when given a “default” option, most people will stick with it even when it’s inappropriate. The point is to select a default that’s appropriate, or at least not hugely inappropriate, for most cases. Joint keyword/positional arguments are a good default because they’re never a huge problem.