Presently, I have a typed functor that could be appropriate for placeholder partial application:
struct Fix{F,NA,NKW,I,V,KW}
f::F # function
nargs::NA # number of args; `nothing` for unlimited (varargs)
nkwargs::NKW # number of kwargs; `nothing` for unlimited (varkwargs)
fixargs::V # fixed arg values
# ::I is tuple of fixed arg indices
fixkwargs::KW # fixed kwargs (keys & values)
end
Fix(f, nargs::Union{Int,Nothing}, nkwargs::Union{Int,Nothing}, fixindices::Tuple, fixargs::Tuple; fixkwargs...) = begin
length(fixindices) > 0 && @assert all(fixindices[begin:end-1] .< fixindices[begin+1:end]) "all indices in increasing order please"
length(fixkwargs) > 0 && @assert all(keys(fixkwargs)[begin:end-1] .< keys(fixkwargs)[begin+1:end]) "all keyword arguments in alphabetical order please"
Fix{typeof(f), typeof(nargs), typeof(nkwargs), fixindices, typeof(fixargs), typeof(fixkwargs)}(f, nargs, nkwargs, fixargs, fixkwargs)
end
Fix(f, nargs::Union{Int,Nothing}, nkwargs::Union{Int,Nothing}, fixindices::Tuple, fixargs::Tuple, fixkwargs) =
Fix(f, nargs, nkwargs, fixindices, fixargs; fixkwargs...)
Fix(f, fixindices::Tuple, fixargs::Tuple; fixkwargs...) =
Fix(f, nothing, nothing, fixindices, fixargs, fixkwargs)
Fix(f, fixindices::Tuple, fixargs::Tuple, fixkwargs) =
Fix(f, fixindices, fixargs; fixkwargs...)
(fix::Fix{F,NA,NKW,I,V,KW})(args...; kwargs...) where {F,NA,NKW,I,V,KW} = begin
arglen = length(args)+length(I)
argitr = ((1:arglen)...,)
fixarg_map = (I, fix.fixargs) # tuple of fixed arg indices and tuple of fixed arg values
argsI = filter(i-> i ∉ I && i-arglen-1 ∉ I, argitr) # arguments not fixed must be called
arg_map = (argsI, args) # tuple of called arg indices and tuple of called arg values
argsout = map(argitr) do i
which_map = (i ∈ I || i-arglen-1 ∈ I) ? fixarg_map : arg_map
which_map[2][findfirst(i-arglen-1 ∈ I ? ==(i-arglen-1) : ==(i), which_map[1])]
end
isnothing(fix.nargs) || @assert length(argsout) == fix.nargs
kwargs = (; kwargs..., fix.fixkwargs...)
isnothing(fix.nkwargs) || @assert length(kwargs) == fix.nkwargs
fix.f(argsout...; kwargs...)
end
To construct and call this object is easy:
f = (a, b, c, d) -> (a, b, c, d)
g = Fix(f, (1,), (:a,)) # funcname, fixed arg indices, fixed arg values
g(:b, :c, :d)
h = Fix(f, (1,2), (:a,:b))
h(:c, :d)
i = Fix(f, (1,2,3), (:a,:b,:c))
i(:d)
j = Fix(f, (1,2,3,4), (:a,:b,:c,:d))
j()
Negative indices denote distance from the end of the argument list (-1
for the end, -2
for next-to-end, etc.)
k = Fix(f, (-1,1), (:d,:a))
k(:b, :c)
Keyword arguments are allowed, and additional arguments can be inserted to specify that the called function has a fixed number of arguments (instead of varargs).
Problem
At the moment it seems to work fine, except: when fixing zero or one arguments it is type stable, but with two or more arguments fixed the functor’s call to map
loses type stability. I’m struggling to figure out why.
Example:
julia> @btime Fix((a,b,c,d)->(a,b,c,d), (1,), (1,))(2, 3, 4)
1.000 ns (0 allocations: 0 bytes)
(1, 2, 3, 4)
julia> @btime Fix((a,b,c,d)->(a,b,c,d), (1,2), (1,2))(3, 4)
778.505 ns (17 allocations: 640 bytes)
(1, 2, 3, 4)