Naming positional arguments at call site

What happens if you write venus(x = 1,100) then? You have exactly the same typo causing exactly the same issue as before.

2 Likes

I’m sorry if this has been suggested before, but seeing the thread it doesn’t seem so.

What is wrong with this approach, in which you (as user/coder) can put whatever names in your code as long as the order matches:

julia> f(a, b, c, d) = a.r * (b ^ 2 + c * d)
f (generic function with 1 method)

julia> struct A
       r::Int
       end

julia> const a::A = A(8)
A(8)

julia> const b::Int = 3
3

julia> const d::Int = 7
7

julia> const c::Int = 2
2

julia> @btime f($a, $b, $c, $d)
  1.999 ns (0 allocations: 0 bytes)
184

julia> const t = (a=a, b=b, more_descriptive_name=c, super_cool_name=d)
(a = A(8), b = 3, c = 2, d = 7)

julia> @btime f($t...)
  2.000 ns (0 allocations: 0 bytes)
184

There seems to be no speed penalty, and the end user knows the name of each argument in case something needs to be modified.

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.

1 Like

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)
6 Likes

Why would it stop you if the naming is optional? How could one disambiguate between a typo and using one named and one unnamed input?

As far as I understood, it was important that this should be optional, since it is extremely controversial in the first place.

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

2 Likes

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.

3 Likes

A lot of the discussion here seems to be satisfied with a macro called something like “stripkw”

@stripkw myfun(rng = Xoshiro(), lefttable = a, righttable = b, filtercol = :foo; actual_keyword_arg=:bar)

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?

If not, something like:

@stripkwcall(myfun,(rng=Xoshiro(),lefttable=a, righttable=b,filtercol=:foo),(actual_keyword_arg = :bar))

should work, it’d just construct a call from the two named tuples, stripping the keywords from the first one…

A little ugly, but not as ugly as trying to keep comments correct through time.

  1. It’s substantially less convenient.
  2. 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

1 Like

Yes, you would expect that, but then you mistype, and you run into exactly the same issue you were claiming this approach would solve.

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.

24 Likes

From the link above, I think this PR looks nice.

3 Likes

I think this is easy:

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.

1 Like

There’s a registered package that does this and apparently handles those caveats for you. It was a result from that previous discussion that Steven linked above:

10 Likes

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.

1 Like

What a pity, had just missed the 100’s post :grinning:

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.