Allow use of named-argument syntax for positional arguments?

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

This feature already exists. Here’s the syntax:

function f(; person::String, from::String)
    hometown = from
    return "Hello, $(person)! Glad you could visit from $hometown."
end

julia> f(person = "Bill", from = "Cupertino")
"Hello, Bill! Glad you could visit from Cupertino."
3 Likes

The trouble with this is that you need to know which arguments passed with key = val syntax are positional and which are keyword arguments before you can choose a method. But you don’t know that until you’ve selected a method. For example, if I see f(x = 1, y = 2, z = 3) how do I know which subset of x, y and z are positional and should be used to dispatch f and which are keywords and should be ignored during dispatch? There are 8 = 2^3 possibilities to consider and that number grows exponentially with the number of arguments.

As a higher level observation, Swift was designed to appeal to and make sense to a large existing base of Objective-C programmers and Objective-C does some very unusual stuff with keyword names and dispatch. When someone writes [obj name:argument] in Objective-C, the name is the name of the method that’s sent to obj. So the “keyword names” are actually the name of the method to invoke—any dispatch that’s done happens after looking at the set of keyword names. That’s kind of the opposite of what Julia does, where the dispatch is done ignoring keyword arguments and then keyword values are used to pass additional data to the selected method as though they’d just been assigned at the top of the method body.

More generally when it comes to APIs, if the arguments to a function need labels in order for the call’s meaning to be clear, then the API should probably be reconsidered. Either the arguments should be keywords so that the name is required, or as has been suggested, you could use a structure to hold the options.

4 Likes

I’m going to second what Stefan just said. It seems like the proposal mostly centers around wanting functions to be called with explicitly named arguments. You can already do this with keyword arguments.

1 Like

I understand the reasons given against named arguments syntax and don’t have a solution for them. However, the current situation definitely has some rough edges and inconsistencies that would not arise at all if naming any arguments was allowed. Several examples from widely used mature libraries:

# inconsistency when it would make lots of sense to allow both variants:
mean([...], dims=1)  # OK
mean([...], 1)  # error
# vs
splitdims([...], dims=1)  # error
splitdims([...], 1)  # OK
# does it read from a and write to b, or the other way?
map!(x -> x * 2, a, b)
# wouldn't it be better?
map!(x -> x * 2, src=a, dest=b)

# which function is mapping, which is reduction?
mapreduce(f, g, [...])
2 Likes

There are several ways that the API in Base could be made more consistent — this was done prior to 0.7/1.0 and it can be done again for 2.0. You may want to check if there is an existing issue for things you notice, and if not, open one and ask for a 2.0 milestone.

That said, in

the convention for ! is to modify the first argument, while

the API is consistent with the function name (f is for mapping, g is reducing).

2 Likes

This makes perfect sense, thanks for the detailed analysis. I guess you could use a different syntax for naming positional arguments but that would be pretty ugly.

This is really the crux of the issue - what is “clear” or not is both hugely subjective, and dependent on context. The current situation means we have a code-style decision relating to consumer code, but it is needing to be decided by API authors, not API consumers.

There’s also the issue that even if an API author decides she wants to force all her consumers to write akdor1154-style code with kwargs everywhere, and is OK with irritating the rest of the community in doing so, she cannot use dispatch on kwargs.

Both @CameronBieganek’s fondness of concise mathematical-style functions and @aplavin’s dislike of perceived ambiguity are perfectly valid, and I’d love to work towards a solution where both of these desires can be satisfied.

1 Like

@StefanKarpinski I’ve had this in the back of my mind for a while - a solution to the problem you raise would be as simple as requiring callers who want to give kws to positional args put a semi in, wouldn’t it? So I as a caller would have to do f(x=4, y=5; z=6) to pass x,y as named positionals. Seems reasonable to me, given everyone really has to be aware about the diff between pos and kwargs anyway.
You’d probably also need to require a trailing semi for the case of named posargs and no kwargs.

1 Like

I’ve had a shot at a macro implementation of this as per Tamas’ suggestion above: https://github.com/akdor1154/NamedPositionals.jl

@np train_model(myModel, max_iters=100, k_weight=0.4, j_weight=0.2;)

I’ve not yet published it to the registry but will do so as soon as I iron out a couple more rough edges. Please have a play! There will definitely be situations where you can break it :slight_smile: Hopefully some of them are fixable…

4 Likes

Looks great! I also agree with this:

Allowing the caller to re-order arguments: nope. Argument order is hugely important in Julia, it shouldn’t be hidden or abstracted. Hiding this is potentially a huge footgun, and would yield major WTFs from anybody who ever reads your code.

(from your readme)

Basically, this should work as just an extra check to confirm that argument names match?

1 Like

Exactly right.

2 Likes

Maybe, at least, just allow some kind of comment in the positional argument list.

That is already possible and folks regularly do so. Here’s a shoddy regex search that pulls up a bunch of hits:

https://juliahub.com/ui/RepoSearch?q=,%20%23%3D\s*\w%2B\s*%3D%23\s*\w%2B,&r=true

For example:

close(getfield(socket, :pollfd), #=readable=#true, #=writable=#false)
5 Likes

I don’t think you can do this. The kwargs allow it, but anything before the semicolon does not.

I get bitten by this all the time. I am trying to shape up.

FYI this is now published to the registry as a prerelease, version 0.1.0 (and hence can be used by ] add NamedPositionals) I’ve still got some playing around to do before a proper ANN.

2 Likes