Allow use of named-argument syntax for positional arguments?

proposal

#1

I’m fond of naming my arguments for readability. Is there a structural reason that one cannot use the names of arguments when the function defines they as positional? In other words, is there a technical reason I can’t do:

  f = function(the_number_I_print; how_many_times_I_print=1)
      for i in 1:how_many_times_I_print
       println(the_number_I_print)
      end
   end

# OK
f(2)

# Not ok...
f(the_number_I_print=2)

So long as one abides by things like no positional arguments after keyword arguments (as in Python)?

If that IS technically feasible, it’s something I’d love to see. I find named arguments really improve code readability and reduce the likelihood of errors.


Why not always keyword?
#2

Seems possible to do with a macro.

using MacroTools
strip_kw(any) = 
    MacroTools.@match any begin
        (keyword_ = argument_) => argument
        any_ => any
    end

strip_kws(any) = 
    MacroTools.@match any begin
        afunction_(args__) => :($afunction($(strip_kw.(args)...)))
        any_ => any
    end
   
macro named_arguments(any)
    MacroTools.prewalk(strip_kws, any)
end

f(a, b = 2, c = 3; d = 4, e = 5) = a + b + c + d + e
@named_arguments f( f(1, b = 3; d = 5), 3, c = 4; e = 6)

Although this requires ; for kw arguments and does not check if your names are correct.


#3

This can’t work in general because you can dispatch on positional arguments. Trying to simultaneously treat them as keyword arguments could introduce ambiguities because multiple methods could have arguments of the same names and types but in different orders.

For example, suppose you define f(x::Int, y::String="") = 2 and f(y::String, x::Int=4) = 3. If you could pass them as keywords, what would f(x=1, y="x") give?

You are probably thinking of languages like Python, which can have simultaneous keyword and positional arguments, but Python is in a different situation because it doesn’t have multiple dispatch.


#4

@stevengj Right! I assumed something like that.

What about allowing for named arguments for positional arguments if they respect order? In other words, for:

f = function(the_number_I_print, how_many_times_I_print)
   for i in 1:how_many_times_I_print
    println(the_number_I_print)
   end
 end

# ok
f(the_number_I_print=2, how_many_times_I_print=1)

# note ok 
f(how_many_times_I_print=1, the_number_I_print=2)

I don’t actually care much about the order flexibility, I just find that being able to label my arguments really helps me avoid errors and make code readable. I’ve come across many Julia functions with 3 or 4 positional arguments, I constantly have to go back to the docs to remember what’s what. I’d really like to be able to leave in arg names in my function calls.


#5

Allowing people to pass positional arguments by name makes the names part of the API of a function – which means you can’t change the names without risking breaking code using your function. If you want to use names to pass arguments, define functions that take keyword arguments. If the name isn’t part of the API, however, then the name that’s used in the definition shouldn’t matter and changing it shouldn’t break anything.


#6

As said by those above, just add the keyword arguments yourselves if you want it.

function f(the_number_I_print, how_many_times_I_print=1)
  for i in 1:how_many_times_I_print
    println(the_number_I_print)
  end
end

f(;the_number_I_print = 2, how_many_times_I_print=1) = f(the_number_I_print, how_many_times_I_print)

Then the following work:

f()
f(2)
f(the_number_I_print=2)
f(the_number_I_print=2, how_many_times_I_print = 4)
f(how_many_times_I_print = 5, the_number_I_print = 3)

So you can optionally use keywords, without having to.


#7

@StefanKarpinski is that such a bad thing that the var names be part of API? Doesn’t seem that problematic, and if that’s the only cost of allowing people to use names to make code more readable and help people avoid errors…


#8

@Elrod Yup! But that only works for programs I write myself – I’m mostly an applied user, so am using other people’s libraries, and many seem to have lots of positional arguments, and I always have to open the docs to remember what each position corresponds to. The ability to name arguments would help a lot with that!


#9

You can always do foo(#=frob=# 7, #=blarg=# 8) etcetera to add comments to arguments, or foo(7, 8) # foo(frob, blarg).


#10

Another trick to “document” positional arguments, which I often use:

forb, blarg = 7, 8
foo(forb, blarg)

Also, some editors (Juno, VSCode, …) will query help for things under the mouse pointer, thus it gets trivial to look up the docs.


#11

That’s a nice trick, thanks!


#12

It’s fine for names to be part of an API. What is not ok is for names to unintentionally be part of the API, which is what allowing any argument to be passed by name – whether the function author intended it or not – would lead to. This is not hypothetical, that’s what’s happened in Python and R where you can call positional arguments by name. One could make the case that library authors should always have to think carefully about what they call their argument, but that seems to be directly opposed to the general premise of positional arguments.


#13

I think the problem there is more with APIs where there are a lot of positional parameters. I dislike those as well and would encourage people not to create such APIs. Either use keyword arguments or have an options type that bundles up a lot of parameters into a single value.


#14

@StefanKarpinski I think that’s a little unfair to positional arguments! They do a lot more than just parse arguments by position – they can also be mandatory. Moreover, I’m not opposed to the use of unnamed positional arguments in some cases, but I think there are cases where it makes sense to name them.

I’d also like to make a point about the defensive programming implications of allowing named arguments. While @mauro3 's suggestion is great for readability, it doesn’t help prevent mistakes. One concern I always have is when a function accepts multiple Real positional arguments – if I get them mixed up, I may just get the wrong answer with no errors! If users can pass names for the positional arguments, then getting confused on order will raise an exception, protecting our valuable research from unintentional errors.

Offering this kind of protection also seems consistent with Julia’s philosophy – it’s similar in spirit to type-checks in a function signature – it’s basically for error avoidance, not performance.

For example:

function f(num_to_print, times_to_print)
      for i in 1:times_to_print
            println(num_to_print)
      end
end

# Good readability, but no error checking!
num_to_print, times_to_print = 1, 4
f(times_to_print, num_to_print)

Obviously in this example we can see things are wrong, but one of the things that makes numerical computing unique is we often don’t know the right answer, so when things are wrong, we don’t necessarily know. If I flipped parameter seeds into a numerical optimizer, I might have no idea!

That’s also a reason for making them optional – in some cases this is just super unlikely to happen. For example, I work with LightGraphs a lot, and the first argument is almost all the graph object one is working with. I don’t foresee myself ever naming that argument. But in cases where a function then also takes several (positional) parameters of the same type, I would definitely use the names.

I don’t disagree. But as a user, I don’t always have control over that – seems like bad engineer to make that the only place this kind of problem can be addressed (single point of failure!). Why not a completely optional safeguard?

[Edit: fixed typo]


#15

What do you suggest as an upgrade path when library authors decide to change the names they use for positional arguments?


#16

Fair question – What’s the norm for when keyword arguments change? Do they just layer on extra keywords for a while?

EDIT: better question: what’s the normal upgrade path for positional arguments currently?

I guess I’m having trouble thinking of a situation where you need to change the keyword but not the actual variable (which may be lack of imagination – I’m an applied user more than a developer, as you can probably infer from my reasoning here). If you’re changing what a positional argument does, then you’re breaking API no matter what, right? So stuff’s gonna break. The only place this introduces a new problem is where you want to change the keyword but not change the behavior. Is that common?


#17

It’s an extremely awkward and annoying process where the method definition in question retains the old keyword name with a nothing (or other sentinel) default value, and if a different value is passed, it manually calls depwarn to tell the user to change their code to use the new keyword argument, then it assigns the new keyword argument the appropriate value and continues on its way. I don’t think we want to encourage more of this. By comparison, deprecating positional methods is essentially trivial and doesn’t touch the main code base at all – you just add a deprecation method with the deprecated signature.

If you’re changing what a positional argument does, then you’re breaking API no matter what, right? So stuff’s gonna break. The only place this introduces a new problem is where you want to change the keyword but not change the behavior. Is that common?

Yes, if you change the behavior, that’s a breaking change anyway, so that’s not really relevant to this issue. But naming things is hard and people change their minds about naming fairly often. Currently, changing a positional argument name is guaranteed not to affect the caller in any way, so it happens all the time. Used the name s for an I/O stream and now regret it and want the I/O argument for all functions to be name io instead? No problem, just change it.

If we allowed and encouraged users to pass names that would have to match as a form of defensive programming, that would prevent changing argument names in libraries without going through a deprecation process. We’re already doing too much deprecation. So while I could imagine doing that at some point in the far future when the standard library is very mature and solidified and when common packages are too, at this point in the history of the language, I just don’t think it’s a good tradeoff.


#18

I want to be able to change variable names in my code (which I do a lot) without having to consider code breakage and possible bugs all over the place, or needing to go through a lot of annoying deprecation nonsense.

This would be a spectacularly unwelcome change for me.


#19

That’s very reasonable. Would you mind if I tabled this for now but brought up again in a year or so?


#20

Sure, that seems reasonable.