Naming positional arguments at call site

Isn’t mod like the exact case of a function where this is just unhelpful line noise? And that’s at the crux here: the names that package/language developers use just aren’t necessarily going to be informative to the callers. Just use units_sold! That’s what you care about.

Yes, sometimes the callers and the devs use the same language, and it’s hugely beneficial! But not always. And that separation of concerns is sensible, in my view.

11 Likes

At least for me it’s helpful, as I can’t ever remember what the order of operations are in mod functions:

mod( arg1, arg2 ) … is that dividing arg1 into arg2 or arg2 into arg1?

But when I see mod( x, y ) it triggers my memory of x divided by y. And so in every language, I always put comments around mod or rem functions to remind myself.

nothing stops the user from naming their own positional arguments with… variable names

is

sample(rng=Xoshiro(0), sampler=NUTS(), model=my_loglikelihood)

really so much better than

rng, sampler, model = Xoshiro(0), NUTS(), my_loglikelihood
sampler(rng, sampler, model)

especially given that very often one wants to reuse arguments (and something something “no magic constants?”)

4 Likes

I think there are people that would really like to know if the function mod(x, y) was changed to mod(x, z). So ideally a prompt in the ide, or package diagnostic, or (less desirable) a runtime error when running the unit test suite.

Writing leftover_units = mod( x = units_sold, y = units_per_package )

But having the compiler use leftover_units = mod( units_sold, units_per_package ) if and only if the names and positions of x and y are still consistent with when I wrote the code, could be helpful.

1 Like

why? I mean, I know never say never, and Hyrum’s law and all, but why would this possibly matter to the user?

4 Likes

I suppose there are three things going on here:

  1. Some folks here want to be able to write functions (that is, literally write — as they develop the code) without worrying about the exact order of the positional args. Just the names are enough, in any order. You’d still need to look up/know the names, but you wouldn’t need to know the order.

  2. Some folks here want to be able to read function calls as being self-documented, even if the args already match the defined positional order. Comments, variable names, etc, can all do the trick, but, yeah, none are enforced to match the defined argument names.

  3. But either way, I fear this is a PSA: Julia is not at that stage of development anymore thing. Making a change like this would drastically expand the surface area of every language and package API that’s ever existed in ways that weren’t necessarily intended.

22 Likes

Advanced LSP servers solve these problems. They can show the definition of functions/methods when you type them and show the name of parameters with inline hints.

1 Like

It’s also obvious if you already know what NUTS and Xoshiro are, but for a new user who’s used to (let’s say) BUGS, it’s probably very confusing.

I think it’s fine to have position-only arguments as an option. Sometimes, position-or-keyword arguments don’t make sense, if the keyword argument is poorly-named. However, the default should be position-or-keyword (like Python, Swift, R, and most other languages have adopted). The problem is that right now, people just default to positional arguments (because keywords are slower).

“most” is doing a whole lot of heavy lifting there. there’s definitely a decent chunk that have made that choice (e.g. the ones you listed + Kotlin, Fortran, Scala, F#, probably a few more), but I wouldn’t be so bold as to present this as a universal/“obvious” design choice

In any case, the omission of this type of parameter certainly doesn’t make Julia an outlier

2 Likes

Is this true? Certainly Julia 0.6 or something had slow keywords, but I think that (in almost all cases?) this was fixed long ago.

All the examples here show no cost for me. As do simple tests… including ones which rely on constant propagation:

julia> fun(; x, y) = fun(x, y); fun(x, y) = /(x, y);  # very simple fast function

julia> @btime fun(x=$1, y=$2); @btime fun($1, $2);
  min 2.500 ns, mean 2.610 ns (0 allocations)
  min 2.458 ns, mean 2.583 ns (0 allocations)

julia> tup(n) = sum(ntuple(i -> i^2, n)); tup(; n) = tup(n);

julia> @btime tup(5); @btime tup($5); @btime tup(n = 5); @btime tup(n = $5);
  min 0.875 ns, mean 0.957 ns (0 allocations)
  min 15.071 ns, mean 15.269 ns (0 allocations)
  min 0.834 ns, mean 0.957 ns (0 allocations)
  min 15.071 ns, mean 15.306 ns (0 allocations)

julia> @code_warntype tup(5)  # Tuple{Vararg{Int64}}, without constant propagation!
5 Likes

No, it’s definitely the multimethod aspect that makes position-only arguments much more useful, as I explained earlier about KeywordCalls.jl.

I’m not sure if function overloading counts as multimethods but it’s similar to compile-time method dispatch so I checked if any language had both function overloading and position-or-keyword arguments. I ran into Kotlin, and when I adapted the ambiguous method dispatch example I wrote earlier, it just caused a Overload resolution ambiguity. The positional arguments calls worked just fine.

I cannot imagine why “x divided by y” would be more natural than “y divided by x”.

And what if the developer decided to go with opposite choice of your preferred mnemonic?

And what if different people wrote different methods of the same function, sometimes going with x/y and sometimes y/x and sometimes alpha/beta?

5 Likes

It’s perhaps not more natural, the pedantic approach would have been, perhaps, to name the arguments to mod to be dividend and divisor, but the mental picture I’m reminded of when I see mod( x, y ) are the various lists of elementary operators:

Julia

Python

Irrational, I know, but it’s an example of why I like to write my code in such a manner. I find that “future-Greg” curses “past-Greg” a lot less when I do.

The choices x and y are prototypical examples of names devoid of any intrinsic meaning.

That list is a perfect example of why this whole idea is misguided, not just in practice, but in principle.

6 Likes

The rule-of-thumb for API functions is this:

Public methods should have no more than three positional arguments, with three discouraged unless it really makes sense, e.g. mapreduce(f, op, x) or fit(model, X, y).

…Granted, not all package developers have heard of this rule-of-thumb. I don’t have a reference, but I have seen something like it mentioned on Discourse.

And, of course, it is just a rule-of-thumb. It is subjective, and there are exceptions.

1 Like

A similar rule should apply for keyword arguments. Imho, Python/R tend to overuse keyword arguments tending towards one-stop kitchen-sink functions with many, many keyword arguments due to the perceived readability of them. Yet, eventually the number of concepts you have to hold in your head trumps the perceived simplicity of surface syntax.
Especially with keyword arguments acting like configuration switches this leads to excessive branching inside a single function. Further, different parts of keyword options are often meaningful together, but independent of other groups of arguments without any visual cues about that. Personally, I would prefer smaller functions with clear separation of concerns, i.e.,

  • put options that belong together into dedicated structs and pass these – naturally giving you named arguments for free
  • separate different aspects of the functionality into designated functions, e.g., instead of an iterations argument return an iterator and let the caller choose the desired number of iterations or pass in a function/functor for handling termination conditions etc.
  • use a fluent interface building up the final configuration in small logical steps, e.g., compare reading CSV in Spark and Pandas.
7 Likes

I don’t really care about variables named x and z. On the other hand, I’d certainly prefer it if these names were exposed, so that developers could give them better names than x and y, like mod(dividend, modulus).

The main point is that with keywords you don’t have to hold those concepts in your head, unlike with positional arguments (where you have to remember the concept associated with each argument).

1 Like

Because positional arguments imply that the difference between the function which is eventually called can determined by a period or comma, which happen to be next to each other on the keyboard.

# Mariner I
function venus(x::Real)
    println("Success")
end
function venus(x::Real, y::Real)
    println("Failure")
end
> venus(1.100)
Success
> venus(1,100)
Failure

I don’t see how using named arguments venus(x = 1.100) , when used in the same order/type as the definition breaks anything, other than my own code when the package developer changes the name. Unordered named arguments, yes I can see how there could be ambiguity when mixing types, names, and orders, but that can be addressed with ide tools or conflict management rules like R uses.

I guess the safest way is to use a struct as the first argument, but that’s single dispatch R s3 classes - circa 1992.