Stream: helpdesk (published)

Topic: Convenient syntax for optionally passing kwarg?


view this post on Zulip Rasmus Henningsson (Jun 07 2024 at 10:38):

Quite often I want to optionally pass a kwarg based on some condition, like this:

if condition
    f(; a=1)
else
    f()
end

But this quickly becomes verbose if there are many other args to f (and potentially other "optional" kwargs).

The above example can be turned into a one-liner, for instance like this:

f(; (condition ? (;a=1) : (;))...)

But that doesn't read very nicely.

Another approach, which scales better to situations where f takes more arguments, would be:

kwargs = Pair{Symbol,Any}[]
condition && push!(kwargs, :a=>1)
f(; kwargs...)

But it involves creating a temporary a Vector (or Dict) with Anys. So it doesn't feel great either.

Is there a better way?

view this post on Zulip Júlio Hoffimann (Jun 07 2024 at 11:10):

I usually define the kwargs in all branches of the code, and always pass them. If a function accepts kwargs, it probably has a default that can be passed anyways:

a = condition ? 1 : default_a()

f(; a) # always forward some value

view this post on Zulip Rasmus Henningsson (Jun 07 2024 at 11:33):

Yes, that's a good solution and it often works.
But sometimes there is no default value, or I don't want to specify the default (it can be complicated or defined in a dependency). So it would be nice to be able handle these situations as well.

view this post on Zulip jar (Jun 07 2024 at 18:18):

g(a) = f(1,2,3,4,5; a)
condition ? g(1) : g(2)

view this post on Zulip Rasmus Henningsson (Jun 07 2024 at 18:46):

No, that's not what I want. That could also be written compactly as:

f(1,2,3,4,5; a=condition ? 1 : 2)

What I want is to not pass the kwarg at all in one of the cases.

view this post on Zulip jar (Jun 07 2024 at 20:26):

julia> f(;a=1) = a;

julia> f(;(rand(Bool) ? () : (:a=>2,))...)
2

julia> f(;(rand(Bool) ? () : (:a=>2,))...)
1

view this post on Zulip jar (Jun 07 2024 at 20:28):

julia> f(;(rand(Bool) ? (;) : (;a=2))...)
2

julia> f(;(rand(Bool) ? (;) : (;a=2))...)
1

view this post on Zulip aplavin (Jun 07 2024 at 22:15):

if there are many other args to f (and potentially other "optional" kwargs).

This happens indeed, and I personally prefer to fully build the kwargs namedtuple and only then pass it to the function. This makes it more convenient to have eg some keywords depending on others.

Here's a nice simple and readable way to do that:

julia> using Accessors, DataPipes

julia> kwargs = @p let
           (;a=1)
           @insert __.b = 2
           rand(Bool) ? __ : @insert __.c = __.a * 10
       end
(a = 1, b = 2, c = 10)

julia> f(;kwargs...)

view this post on Zulip Gunnar Farnebäck (Jun 08 2024 at 08:56):

I hate to be critical but to me that's neither nice, nor simple, nor readable.

For these situations I generally just resort to some variation of

kwargs = (;)
if condition
    kwargs = (; a = 1)
end
f(; kwargs...)

view this post on Zulip Daniel VandenHeuvel (Jun 08 2024 at 10:09):

I usually just use an if statement like Gunnar's as well. I personally don't have no idea how to read that Accessors syntax.. If you have kwargs in the function signature (where the other function is being called, not the f in these examples) you can also work with that object directly and delete/add keys to it.

view this post on Zulip aplavin (Jun 08 2024 at 13:37):

resort to some variation of ...

This is not type-stable though, so only suitable for pieces when performance doesn't matter.

view this post on Zulip aplavin (Jun 08 2024 at 13:41):

I personally don't have no idea how to read that Accessors syntax..

Of course, any syntax requires some familiarity to be readable and understandable :)
In this specific case, code looks reasonably nice even without Accessors:

julia> kwargs = @p let
           (;a=1)
           (;__..., b = 2)
           rand(Bool) ? __ : (;__..., c = __.a * 10)
       end

although the intent is arguably more clean with @insert. Also, it has @delete/@set/@modify for more complex usecases.

view this post on Zulip Gunnar Farnebäck (Jun 08 2024 at 14:04):

This is not type-stable though, so only suitable for pieces when performance doesn't matter.

Yes, of course. Is that any different with your proposal?

view this post on Zulip aplavin (Jun 08 2024 at 22:56):

Ehm... Surely it's type stable for reasonable conditions (not rand(Bool) :) ). I wouldn't have suggested a non-type-stable solution here without a major caveat/disclaimer.

julia> f(x, y) = @p let
                  (;a=1)
                  @insert __.b = 2
                  ndims(x) == 1 ? __ : @insert __.c = __.a * 10
                  haskey(y, :somekey) ? __ : @insert __.d = y
               end

julia> @code_warntype f([1], (mykey=1,))
<...>
Body::@NamedTuple{a::Int64, b::Int64, d::@NamedTuple{mykey::Int64}}
<...>

julia> @code_warntype f([1;;], (mykey=1, somekey=2))
<...>
Body::@NamedTuple{a::Int64, b::Int64, c::Int64}
<...>

view this post on Zulip Gunnar Farnebäck (Jun 09 2024 at 09:50):

There's some middle ground between rand() and conditions that can be determined from the input types though. Anyway I don't think it's that often it's critical because

  1. If you only have one or two conditions, kwargs will be a small union and union splitting will apply.
  2. As soon as you make the function call you have passed a function barrier.

But sure, if this happens on the way to a frequently called function, you should review what it does to performance.

view this post on Zulip Eric Hanson (Jun 10 2024 at 10:03):

I don't see how this f is helping if the user needs to pass in a namedtuple in the first place? If you had that, you would just splat it in. If you don't have that, and you need to construct a namedtuple with variable number of keys depending on a Bool, well then that's by definition type-unstable (the type of the output depends on a runtime value).


Last updated: Dec 28 2024 at 04:38 UTC