Keyword arguments without a keyword?

I feel like I have seen this proposal before, but I can’t find it now and I don’t remember the resolution, so sorry if this is repetitive.

I think it would be very nice if single word arguments passed to a function after a semi-colon were interpreted as keyword arguments where the variable name is the keyword and the value is the value. For example:

f( ; kw) = kw

kw = 2
f(kw=kw) #returns 2
f(; kw) #throws a syntax error, but should it?

a = 2
f(a=a) #throws a MethodError
f(; a) #throws a syntax error, but maybe it should throw a MethodError?

Given that the current behavior is to throw an error, adding this would not break anything. It would be a big QOL improvement to avoid typing x=x, y=y, z=z over and over.

I’ve been using a macro get this syntax, but its not very elegant because only way I figured to make it work was to put all the arguments in a Dict{Symbol, <:Any} and then splat it after the semi-colon.

I’m curious to hear thoughts on whether or not this is a good idea

3 Likes

This is not a good idea. This would suddenly mean that the behavior of non-macro code would depend on variable symbols, rather than just the value of the variables themselves. One of the virtues of macros is that they make it very explicit when this can happen.

Note that if what you are looking to do is forward keyword arguments from one function to another (a common use case for sure) one can do

f(;kwargs...) = g(;kwargs...)

Admittedly I have sometimes found myself worrying about whether there are performance implications to this, but there usually don’t seem to be.

3 Likes

The reason I don’t do that is because different keywords go to different functions. I suppose I could make all my functions take an arbitrary amount of keywords and just ignore anything not specified, but that seems wrong.

I see your point about macros, but I don’t really agree. All code depends on variable symbols. when you type f(x=x) which variable is named x definitely matters.

The point is that only the value of x matters in this case. You could have called it absolutely anything and it would not matter. Take any piece of code without macros, and transform it by mapping all symbols to new symbols, and it still works.

I guess it depends on how independent you view functions. If you change the symbol in all functions at the same time, the proposal still works. It only breaks if you change the symbols in one function but not another.

Things will also break if you change the name of a function but don’t change the references in other functions. Clearly the symbol, and not just the value, matters when it comes to functions, and given that functions are treated just like every other type I don’t see why they should be so different.

One thing why not to do so is potential WTFs with macros. Say:

f(; kw) = kw

kw = 2
f(; kw) # supposedly works

@some_macro let kw = 42
    f(; kw) # error due to name mangling by the macro
end

If you want that behavior in your code and can get it with a macro, that’s perfectly fine and is a julian solution, I guess. You can use a named tuple instead of a Dict and splat pairs(x), that’d be more lightweight.

2 Likes

I didn’t fully understand the conversation, but have you seen this https://github.com/mauro3/Parameters.jl/blob/master/README.md, it might be a relevant package.

One could get around that by doing the expansion of f(; kw) to f(; kw=kw) before doing macro expansion. Then the macro would rewrite it to f(; kw=var”##3#kw”) or something.

1 Like

I really like this idea, and I would probably use it a lot if it got implemented. I’ll even go as far as saying that with this feature I’d use keyword arguments a lot more than I do now.

1 Like

Relevant issue: https://github.com/JuliaLang/julia/issues/29333. The syntax would also naturally extend to named tuples so that (; a, b) would be syntax for (a = a, b = b).

6 Likes

This can be implemented in a package:

using MacroTools: @capture, splitarg

macro pun(ex)
    ret = if @capture ex (f_(args__; kw__))
        kw = map(kw) do kwarg
            key, typ, splat, val = splitarg(kwarg)
            if splat
                kwarg
            elseif val == nothing
                :($key = $key)
            else
                kwarg
            end
        end
        :(
            $(f)($(args...); $(kw...))
        )
    else
        ex
    end
    esc(ret)
end

function f(args...;kw...)
    println("="^80)
    @show args
    @show kw
end
a = 1 
b = 2
cs = (d=1, e=2)

@pun f(; a=3)
@pun f(a,b; a, b, c=3)
@pun f(a,b; a, b, cs...)
13 Likes

This macro actually works great with no extra cost (which my previous did have). Thanks!

I’ll be using this until the feature is added, or forever if its not

2 Likes

I think I would like to put this macro into a package. I just can’t come up with a good package+macro name.

I would call it KeywordTools or KwargTools or something like that. I personally like to call the macro @pkw standing for pass-keyword. Maybe @passkwarg.

1 Like

Since this feature exists in other languages, I wonder if it has a name. In some languages like Rust it is restricted to constructors and goes under the name field init syntax.

But that name does not really make sense for arbitrary function calls.

How about calling them “eponymous keyword arguments”?

4 Likes

Not really a short syntax (especially for small number of arguments), but you can use NamedTupleTools.@namedtuple (or EponymTuples.jl) to do:

julia> a = 1;

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

julia> f(; (@namedtuple a)...)
(a = 1,)
1 Like

That sounds like a good name. If nothing else comes up, I think I will go with
EponymKeywordSyntax.jl and @eponym.

2 Likes

I added the NamedTuple syntax the Stefan mentioned above. I’m new to using MacroTools so there’s likely a better way, but this works

edited to mimic NamedTupleTools.@namedtuple and use MacroTools better

macro eponym(ex)
    ret = if @capture ex (f_(args__; kw__))
       kw = map(kw) do kwarg
           key, typ, splat, val = splitarg(kwarg)
           if splat
               kwarg
           elseif val == nothing
               :($key = $key)
           else
               kwarg
           end
       end
       :(
           $(f)($(args...); $(kw...))
       )
   elseif @capture ex ((; kw__))
       kw = map(key -> :($key = $key), kw)
       Expr(:tuple, Expr(:parameters, kw...))
   else
       ex
    end
    esc(ret)
end
1 Like

The potential issue with this one is that its probably a little (tiny) bit slower.

julia> @macroexpand @eponym testf(;x,y,z)
:(testf(; x = x, y = y, z = z))

julia> @macroexpand testf(; @namedtuple(x,y,z)...)
:(testf(; ($(Expr(:parameters, :(x = x), :(y = y), :(z = z))),)...))

so it looks to me like there would be a little extra work at runtime because it still has do the tuple making and splatting. Hopefully this is optimized out by the compiler, but I don’t know for sure.

The time saved (if any) would be a fraction of a nanosecond, so it would only make a difference for a function in an inner loop that’s potentially called several billion times, but sometimes I need to write code like that so I think about optimizing these things.

1 Like