Allow use of named-argument syntax for positional arguments?

One thing I frequently do is use one variable name in the documentation, and another one in the actual code. The docstring might say

crazyawsomevacationpic = enhance(awsomevacationpic)

but the code itself has

function enhance(img::Image)

because that’s much more convenient and readable inside the code. If positional variable names are part of the API, how will this work?

Another question regarding the update process:

Let’s say I decide to, not just change variable names, but to interchange them? I decide that, really, it makes more sense that variable x should be called y and y should be called x (I have made changes like that countless times.) That would make for a very awkward deprecation process.

It seems really bad to me. It’s ugly, verbose, intrusive, will cause bugs, and puts an extra unnecessary burden on the developer. When I use positional arguments I deliberately want to hide the names from the caller. When I want named arguments I use kwargs.

Perhaps it could be possible to allow kwargs to also accept positional input, or something like that? Then this would be opt-in behaviour. Otherwise, if a change like this were to come, I hope it will be possible for any developer to turn it off and disallow its use.

1 Like

Three years later, is there any appetite for this?

To add something - there’s very interesting prior art here with Swift. Swift encourages this pattern with one of the same core values as Julia - because code is read more often than it is written.

Swift has a language feature specifically to make this pattern both useful for library consumers, and non-painful for library authors (e.g. the pain points that @DNF raised): The name of the API function parameter (consumer sees) and the name of the bound variable inside the function (author uses) are uncoupled. Example from swift doco:

func greet(person: String, from hometown: String) -> String {
    return "Hello \(person)!  Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))

from becomes part of the function api, and hometown is free for the library author to change as she sees fit.
(see Functions — The Swift Programming Language (Swift 5.7))

They actually go all-out on this, and by default, all function arguments need to be called with their names (this was done in a major language version change, Swift 3 I think). There is special syntax for functions to allow a parameter to be used without a name (e.g. add(x,y)).
As someone who migrated a few projects to this new standard, at the time it seemed like a verbose pain in the arse. Looking back at the code now, though, it’s perfectly clear and I actually think this was a really good move.

I would love to see this allowed in Julia - as previous posters stated, I don’t care for variable ordering, it’s more to make my code easier to understand, and as a sanity check to make sure I’m calling other peoples’ libraries correctly.

2 Likes

As @StefanKarpinski wrote the very first time keyword arguments were discussed for Julia,

The main consideration here is making keyword arguments not completely impossibly complicated to understand in the presence of multiple dispatch.

For example, consider what happens if you have

foo(x::Int, y::String) = 1
foo(y::String, x::Int) = 2

and you call foo(x=3, y="bar"). Since ordering shouldn’t matter for named arguments, which method should it call?

Or what about

foo(x::Integer) = 1
foo(y::Int) = 2

If you call foo(x=1), should it call the more specific method for Int even though the argument was named differently? If not, then are you okay with foo(x=1) ≠ foo(1)?

9 Likes

To take Stefan’s first example of possible issues here:

f(x::Int, y::String="") = 2
f(y::String, x::Int=4) = 3

What should f(x=1, y="x") give?

It should give 2. If the first definition was not there, it should give the same error that f(1, "asdf") would give currently: that no compatible method for these types exist.

What I’m advocating for is that

  • dispatch continues to work based on an ordered list of types,
  • function parameters can be named at the time of call; if the name of parameter n does not match the declared name of parameter n in the API, an error is thrown.

Applying these to your edits:
foo(x=3, y="bar") should give 1 (dispatch based on order, as it is now).
foo(x=1) should dispatch against Int based on ordered list of types, but then error because it was called with x and not y.

Both of these can be resolved unambiguously and according to current dispatch rules. The second example is admittedly interesting and raises a case that library authors would have to take care with.

As long as they are still referred to and considered to be positional arguments I don’t think this is super-confusing.

2 Likes

Just landed on this (fun) discussion and figured I’d raise a couple more issues I haven’t seen so far:

  • Wouldn’t reading code be more confusing, since named-positional and keyword arguments are both allowed. I.e.
f(3, y=5)  # If I'm someone reading this, is y positional or keyword?
  • If you have named positional arguments, can you name some but not all of them?
f(3, y=4, 1)  # y is positional, so is this ok?
  • What do you do if positional and keyword methods are both defined, but they share variable names? E.g.
f(x, y=1) = x + y
f(x; y=1) = x*y

f(x, y=10) # ?

Must this throw an error now to not break existing code that does this?

1 Like

It is not clear to me what determines the ordering. Also, did you mean ordered list of arguments?

Most people here agree with you: usually more than 2–3 required positional arguments are generally recognized as code smell.

It is just that Julia has different strategies for dealing with this issue than Swift and R.

The most obvious is keyword arguments. In recent Julia versions, they can have no default value, and just error.

Arguably, the nicest for a lot of arguments that are part of some coherent group is wrapping them in a struct. This usually has no performance cost, can provide validation and errors at the call site, and reusing these arguments. For a nice example, see Optim.BFGS.

1 Like

This seems to really just address one of the points I raised: different names facing the user vs inside the code.

It’s still going to be hard to change names if I decide that I made a bad choice in the first place. Or if I just want to harmonize names across different functions. I need to come up with N times as many ‘nice’ user-facing names, increasing the likelihood of name clashes across a codebase, or inconsistent styles between different functions. Now, changing a user-facing name means changing a docstring. With this, I break code.

It makes code more verbose, which is really bad in my book, and it would look more messy without a lot of coordination across functions/modules/packages.

This seems like a feature for a different kind of language. It’s for writing big, enterprise-level software, with uniform interfaces written by coordinated teams of paid employees adhering to an enforced style guide.

My function arguments are called x or val or something like that. Why does that need to spill out into the caller code?

2 Likes

Yes, I guess as a reader you won’t know. To answer these there would need to be a solution for your third point,

This is an interesting issue that you raise! I guess if there were support for this proposal the first step would be to see how often this arises in practice. In akdor1154’s magic world I would say the behaviour in your example should be positional, requiring a ; to be interpreted as keyword, but that would be a breaking change, the effects of which could easily go unnoticed.


I mean the order of the types of the arguments as used in the user’s function call, which is how dispatch works currently (If I understand dispatch correctly).


I disagree that verbosity is universally bad. Or alternatively, that more characters means verbosity. How much/little verbosity people will put up with to increase clarity is very subjective, and is just another code style choice that authors should be able to make for themselves.

Yes, but I don’t think it’s any more difficult than just naming the functions in your API in the first place. As an anecdote, in practice, I haven’t seen any issues or anyone complain about this in Swift-land, where this is enforced and used widely.

It doesn’t, no-one is advocating to force either you or users of your library to write mean(val: myarray) or anything pointlessly verbose like that.
Under this proposal that would be possible I guess, but even if someone does that then that’s a) not harmful to you, and b) not the point of this proposal.

It’s certainly helpful in such situations! But the Swift examples I was referring to before were all on my own small codebases, that I share with nobody else, and anecdotally, I appreciated the additional clarity when reading my code for far longer than I detested the extra typing per-parameter.

1 Like

I would say that verbosity is bad in and of itself, but there is a trade-off between offering more information (can be good) and verbosity (universally bad).

Yes, that’s how it is now. But with this you would force more verbosity. Or, do you mean that this is optional?

It’s suddenly a lot more names. Instead of just naming functions, it now function plus 2-3 argument names. That’s a big change.

That’s good. I didn’t catch that. I would definitely opt very much out of this.

1 Like

Dispatch is not about order, it is a picking a matching method signature that is the narrowest.

The details of your proposal are still very unclear to me.

I don’t think you can make this optional. Once x and y are part of the API for add(x, y), a package would have to consider users who are using add(y = a, x = b). Changes that are currently innocuous (eg someone giving more descriptive variable names that show up in autogenerated docstrings) would be breaking.

1 Like

There would have to be a way for me to make sure that no-one can call my functions using named arguments.

Another thing, @akdor1154 , what if I want to extend a pre-existing function (written by someone else) for a new type I defined. Would I have to expose the same argument names?

2 Likes

I think we are talking about the same thing … if I write “method signature” (as it means now, e.g. f(String, Int)) instead of “the order of the types in the user’s function call”, does that make sense?

Hypothetically under this proposal, it would be an error to call add(y=a, x=b) as the order of the names don’t match the API of add(x, y), so this situation would not occur.


That’s not what I meant by optional - I meant callers could optionally write
DNFsFunction(samples, 1.5)
or DNFsFunction(data=samples, k=1.5),
just like you’d be able to choose whether to write
convert(Array{UInt8}, data)
or convert(to=Array{UInt8}, data)
in your own code.


I don’t see why that would be required in principal; it might annoy users if you give arguments different labels when externally they appear to serve the same purpose as the pre-existing function, though.

1 Like

But can I write my function such that it is impossible for others to call it with argument names? Because I really want to prevent that.

1 Like

What I have in mind is:

  1. choose a method implementation via existing dispatch rules
  2. if the user has provided argument labels, and they don’t match the argument labels in the API, error.

Is there ambiguity in here that I have missed?

1 Like

I guess that’s an open question!
Back to the Swift implementation, they actually do have syntax for this:

func rgb(_ red: Float, _ green: Float, _ blue: Float) { }

So they decided that there were valid use cases for API authors to disallow names.

3 Likes

In that case callers would have to know the type of their variables, and call the function with different names depending on which type they were using. That would be bad. The way out would be to not use named arguments in generic code.

But if I want my code to compose well with other peoples code, and they are using named arguments, then I pretty much have to accept the names they are using.

That’s good, but also adds verbosity compared to now.

I think that external methods that can be arbitrarily extended by others, makes an awkward fit with this proposal. It’s different when the methods belong to a class, and are under full control of the class author.

Yes, that’s a good point. I was more considering cases like in DataFrames where they overload stuff in Base but with slightly different argument sets, and where users know they are calling these APIs with a dataframe.
For generic-type overloading, under this proposal, you should use the same names as the pre-existing method or you’d cause angst for users. I think this would have to be a “best practice” thing though, there are cases where it would be harmful or plain impossible to enforce it.

My understanding of your proposed rules is the following:

  1. function argument names (when applicable) and values are collected. eg a call f(x = 1, y = 3.0) would have (:x, :y) and Tuple{Int, Float64}.
  2. the Tuple of values is matched against function signatures using the current dispatch semantics, eg here f(x::Int, z::Float64) would be a match for the purposes of dispatch.
  3. then names are checked for the selected method?

Is this correct?

If yes, note that Julia can do this already with a bit of extra syntax. Just make a macro @namedcall f(x = 1, y = 3.0) that captures the relevant information, eg as a NamedTuple, and a @namedmethod f(x::Int, y::Int) that defines f on a relevant subtype of this NamedTuple, inserting an @assert about the names (you can of course use shorter/more clever names, I just made these up).

This would make a nice package and allow people to experiment with your idea. If it gains widespread use, arguing for a change of the standard semantics would be much easier.

3 Likes

I’m quite fond of concise function calls like

divrem(a, b)
reshape(1:12, 4, 3)
rand(1:9, 10, 20)

I think conciseness often improves readability rather than reduces it. There’s a reason mathematical notation is very concise—because it makes abstract concepts easier to read and understand at a glance.

Compare this notation

μ = Σx / n

to this notation

mean = sum of elements in array / count of elements in array

Once one is familiar with mathematical notation, the first example gets the point across much more quickly than the second example. And if you wrote a more complicated equation in words it would be nearly illegible.

A lot of Julia users have a mathematical background and are used to the concise notation of mathematics, so making Julia code more verbose is going to be a hard sell.

5 Likes