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 Any
s. So it doesn't feel great either.
Is there a better way?
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
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.
g(a) = f(1,2,3,4,5; a)
condition ? g(1) : g(2)
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.
julia> f(;a=1) = a;
julia> f(;(rand(Bool) ? () : (:a=>2,))...)
2
julia> f(;(rand(Bool) ? () : (:a=>2,))...)
1
julia> f(;(rand(Bool) ? (;) : (;a=2))...)
2
julia> f(;(rand(Bool) ? (;) : (;a=2))...)
1
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...)
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...)
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.
resort to some variation of ...
This is not type-stable though, so only suitable for pieces when performance doesn't matter.
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.
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?
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}
<...>
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
kwargs
will be a small union and union splitting will apply.But sure, if this happens on the way to a frequently called function, you should review what it does to performance.
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