Wow @generated
is like having a superpower in your back pocket. Thanks @CameronBieganek for that!
Also: its performance is great. I think I was mistaken previously.
Third-pass Code for a typed arbitrary-index partial application functor
struct Fix{F,fixinds,V<:Tuple,KW<:NamedTuple}
f::F
fixvals::V
fixkwargs::KW
Fix{F,fixinds,V,KW}(f,fixvals,fixkwargs) where {F,fixinds,V,KW} = begin
orderok(a, b) = a < b || (a > 0 && b < 0) # not a perfect test ... just want args ordered left to right
length(fixinds) > 1 && @assert all(orderok.(fixinds[begin:end-1], fixinds[begin+1:end]))
new{F,fixinds,V,KW}(f,fixvals,fixkwargs)
end
end
Fix{fixinds}(f, fixvals; fixkwargs...) where {fixinds} =
Fix{typeof(f), fixinds, typeof(fixvals), typeof((; fixkwargs...))}(f, fixvals, (; fixkwargs...))
@generated (f::Fix{F,fixinds,V,KW})(args...; kwargs...) where {F,fixinds,V,KW} = begin
combined_args = Vector{Expr}(undef, length(fixinds)+length(args))
args_i = fixed_args_i = 1
for i ∈ eachindex(combined_args)
if any(==(fixinds[fixed_args_i]), (i, i-length(combined_args)-1))
combined_args[i] = :(f.fixvals[$fixed_args_i])
fixed_args_i = clamp(fixed_args_i+1, eachindex(fixinds))
else
combined_args[i] = :(args[$args_i])
args_i += 1
end
end
:(f.f($(combined_args...); kwargs..., f.fixkwargs...))
end
Performance:
julia> @btime Fix{(1,2,-3,-1)}((args...;kwargs...)->(args...,(;kwargs...)), (:a,:b,:getting_close,:END), (z=5))(:y, 1, 2; k=2)
1.000 ns (0 allocations: 0 bytes)
(:a, :b, :y, 1, :getting_close, 2, :END, (k = 2, z = 5))
Notes:
- call
Fix{fixinds::Tuple}(f, fixvals::Tuple; fixkwargs...)
to construct functor
–fixinds
is a tuple of indices starting from left (e.g.(1, 2, 3)
), and any indices counting from the right are negative (e.g.,(1, 2, 3, -3, -2, -1)
). Index1
is left-most argument,-1
is right-most. - Fixed keyword arguments override called keyword arguments. Not sure if this is the right decision.
- There is no check that the number of arguments or keyword arguments fit a profile; the combined argument list simply grows with number of arguments passed during call, with new arguments filling in the middle between the arguments with positive indices and the arguments with negative indices.
- This could use some more road testing, for sure
-
FixFirst(f,x)
is created byFix{(1,)}(f, (x,))
whichisa Fix{<:Any, (1,)}
. It is presumed that such an object would be created byf(x, _...)
. -
FixLast(f,x)
is created byFix{(-1,)}(f, (x,))
whichisa Fix{<:Any, (-1,)}
. It is presumed that such an object would be created byf(_..., x)
. - In many locations where
Base.Fix2
is used, people will probably usef(_, x)
, which will create aFix{<:Any, (2,)}
object. When calling a function with two arguments, the fact thatFix{<:Any, (2,)}
behaves asFix{<:Any,(-1,)}
means the type signature of a partial function which does the intended task is not unique. For the people who care about the types of the object, not sure if this matters.