Naming positional arguments at call site

…, which is because you can’t define >1 method with the same positional arguments and different keyword arguments. Keyword arguments just can’t multimethod.

julia> bar(; a = 1.0, b = 2) = 0
bar (generic function with 1 method)

julia> bar(; b = 2, a = 1.0) = 1
bar (generic function with 1 method)

julia> bar(; b = 2, a = 1.0, c = 0) = 2
bar (generic function with 1 method)

No, NamedTuples are ordered just like the underlying Tuples, though there can be another unordered named tuple type (something more compact than a Dict I mean). But the 1 method restriction means you won’t have to worry about keyword order. Bear in mind that variable keyword arguments are taken in your provided order:

julia> bar(; kwargs...) = kwargs
bar (generic function with 1 method)

julia> bar(;a=1,b=2)
pairs(::NamedTuple) with 2 entries:
  :a => 1
  :b => 2

julia> bar(;b=2,a=1)
pairs(::NamedTuple) with 2 entries:
  :b => 2
  :a => 1
2 Likes

Exactly, and this would be easier if these tuples were just the same. Indeed they can be made equal via conversions:

julia> for nt in [(; a = 1.0, b = 2), (; b = 2, a = 1.0)]
           @show NamedTuple(pairs(Dict(keys(nt) .=> values(nt))))
       end
NamedTuple(pairs(Dict(keys(nt) .=> values(nt)))) = (a = 1.0, b = 2)
NamedTuple(pairs(Dict(keys(nt) .=> values(nt)))) = (a = 1.0, b = 2)

Thus, arguably, they do indeed denote the same (immutable) value!

Well, they are implemented like that, but would not need to be. I.e., if you have names you don’t need an order on top (or below). On the contrary, an ordered tuple could just be seen as an unordered named one mapping integer names to values.
Was just thinking that you could use named tuples to dispatch on parameters with required names:

run(model12, params::NamedTuple{(:param1, :param2)})
run(model1AB, params::NamedTuple{(:param1, :paramA, :paramB)})

but this would not work very well as run(model12, params::NamedTuple{(:param2, :param1)}) is a different method.

Maybe that can be introduced by a new calling syntax, without breaking things. Something like:

f(x = 1, y = 2) = x + y

possibly being be called with `

f(2, 3)
#or
f(*x = 2, *y = 3)

I’m short of ideas about the symbol, but some symbol might fit.

ps: @ParadaCarleton and @ParadaCarleton are the same person?

1 Like

No, they are semantically ordered like the contained Tuples. Iteration occurs in order, you can getindex or getfield them with integer indices (analogous to getfield for struct instances), both first and last are implemented, etc. Making NamedTuple unordered is a breaking change.

Assuming you intend f(*y = 3, *x = 2) to do the same thing, which is why people use keywords for positional arguments in calls in other languages, this still doesn’t address the extra layer of method ambiguities when multiple methods have the same argument names in different orders.

1 Like

No, I don´t . That should just error (unless there is a f(y::OtherType=1.0,x=2) method defined, for, of course, other types, just like it is now). I meant literally positional arguments.

In the rust-analyzer in vscode, the function call:
f(2, 3)
Would be displayed as to the user as
f(x: 2, y: 3)
And you clicked on the x: and y: links, it would go to their type documentation, definition, etc.

While I would like to see arguments treated as they are in R, if the vscode extension could treat positional arguments the same way, it could make the existing positional arguments easier to use.

3 Likes

People name positional arguments (or rather arguments that can be either position or keyworded) in function calls in other languages precisely to reorder arguments when the order is harder to remember than the names. Without that, naming would just be an extra restriction on method API and an extra chore to match the argument names. If you want the latter right now, you could just do comments f(#=x=# 2, #=y=# 3). I think it’s possible for a macro to generate that from f(x=2, y=3), though it’d be tricky to try to additionally enforce that the names even match any existing method’s arguments.

I wouldn’t want this, though, it would also introduce an easy misconception expecting another nightmarish layer to method dispatch:

f(x, y) = x+y
f(a::Int, b::Int) = a-b

@namedpositional f(x=2, y=3) 
# user is only aware of f(x, y)
# but it'll be dispatched to f(a::Int, b::Int)
# unless the macro finds f(x, y) and does invoke
2 Likes

That’s not the point that’s being raised. The issue is that if someone defines f(x) = 2x and then someone calls it like f(*x = 5), then the fact that x was used as the name is now exposed to the user as public API. If names like this are public API, then you couldn’t change this to f(y) = 2y without breaking downstream user code. I think that is a pretty legitimate worry since it would make things more fragile down the line.

16 Likes

Right, than the package developer has to on his side make the API available explicitly. For some package that can be useful (not sure how much more useful than keyword arguments), but still could be implemented in a non-breaking fashion by introducing some new syntax at the function definition level.

(I don’t think this is a major issue, by the way - I was just trying to see why this cannot be implemented as a new non-breaking feature).

That is very well-put, and it’s exactly the primary motivation for PEP 570 introducing position-only arguments in Python 3.8, as mentioned earlier. Thing is, Python did this late and is still stuck with some API names because removing keyword capability is a breaking change (adding keyword capability is not breaking e.g. pow). If a language that doesn’t need to deal with the extra factor of multimethods still moved toward position-only arguments, maybe we shouldn’t interfere with our own.

2 Likes

Note that this suggestion of naming positional arguments like keywords has come up multiple times on this forum, and usually the same sorts of arguments arise. See Allow use of named-argument syntax for positional arguments? and the other discussions linked therein.

9 Likes

For what it’s worth, my own curiosity has been satiated. In particular I’ve done quite a bit of reading of the docs today and found this to be quite informative (emphasis mine):

Keyword arguments behave quite differently from ordinary positional arguments. In particular, they do not participate in method dispatch. Methods are dispatched based only on positional arguments, with keyword arguments processed after the matching method is identified.

All I really cared about was being able to better document code by providing a name, even if it’s ignored, to the function argument – and this is accomplished, to my satisfaction, by @Benny’s suggestion of using inline comments:

function my_func( x, y; z=0 )
    return sqrt.( x.^2 .+ y.^2 .+ z.^2 )
end

my_func( #=x=# [1,2], #=y=# [1,2] )
my_func( #=x=# [1,2], #=y=# [1,2], z=[1,2] )
5 Likes

There’s a pr that adds this to the Julia LSP, Initial InlayHint provider implementation by pfitzseb · Pull Request #1077 · julia-vscode/LanguageServer.jl · GitHub.

4 Likes

Just a FYI:
the compiler does compile specialized method for every possible keyword argument type encounted.
It didn’t always, but it has for quiet a few years.

8 Likes

As someone working in that domain but also software engineering more broadly, I would caution against extrapolating interface choices of ML frameworks to the language level. Not all software engineering is writing ML models. So even if a particular syntax were the only reason to prefer a framework, I’d not want to take that as a lesson for a language. Tensorflow and PyTorch are libraries. In Julia, libraries can do very powerful syntax changes to improve the user interface if they want to. The keyword annotation is particularly easy to achieve by wrappers at the library level.

But the broader point for me seems to be that if your framework or language choice comes to syntax differences, it‘s pretty much chance of which one you are going to pick. I mostly work in a given language or framework because it allows me to do things that I would otherwise be unable or magnitudes slower to do, or because it integrates into toolchains that are given, and I believe that we‘d better focus our energy on those types of concerns.

6 Likes

One more objection would be that different methods (perhaps from different packages by different authors) may have different internal variable names, and at compile time it can be unknown which method will be called.

Still IMO it would be nice to have named positional arguments for documentation purposes. My feeling, whereas inline comments is technically OK, the readability is pretty bad, which is what may count.

my_func( #=x=# [1,2], #=y=# [1,2] )

I’d suggest two other options:

  • allow named positional arguments, followed by (mandatory in this case) semicolon, but let compiler ignore the names - that could be the job of a linter
  • or make a macro with the same effect, like
my_func( @*(x= [1,2]), @*(y=[1,2]) )

You’re overlooking the perhaps most obvious way of naming arguments:

x = [1,2]
y = [1,2]
my_func(x, y)

Another alternative is

my_func(
    [1,2], # x
    [1,2], # y
)
13 Likes

The problem with this is that quite often the variables come from other routines and, for readability, don’t have names that happen to match downstream function signatures:

# Without looking at the other code blocks, tell me what x and y represent
x, _, y= get_unit_sale_data()
leftover_units  = mod( x, y )

vs.

units_sold, price_per_unit, units_per_package = get_unit_sale_data()
leftover_units  = mod( units_sold, units_per_package )

vs.

units_sold, price_per_unit, units_per_package = get_unit_sale_data()
leftover_units = mod( #=x=# units_sold, #=y=# units_per_package )

vs.

units_sold, price_per_unit, units_per_package = get_unit_sale_data()
leftover_units = mod(
                      units_sold, #x
                      units_per_package, #y 
                    )
1 Like

Can this be addressed by a macro to rewrite the code?

# this line of pseudo code
leftover_units  = @macro mod( x = units_sold, y = units_per_package )
# expands to ...
x = units_sold
y = units_per_package
leftover_units  = mod( x, y )

The reason I raise this is that if the function is defined as mod(x, y) then the IDE can change what is displayed so that this line of code mod(x, y) will be displayed in the ide as simply mod(x, y)

Whereas mod(units_sold, units_per_package) will be displayed as mod(x: units_sold, y: _units_per_package)

It seems like you want to write the code the way the ide can display it, and then reverse the operation when running the code.

1 Like