Fixing the Piping/Chaining Issue

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)). Index 1 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 by Fix{(1,)}(f, (x,)) which isa Fix{<:Any, (1,)}. It is presumed that such an object would be created by f(x, _...).
  • FixLast(f,x) is created by Fix{(-1,)}(f, (x,)) which isa Fix{<:Any, (-1,)}. It is presumed that such an object would be created by f(_..., x).
  • In many locations where Base.Fix2 is used, people will probably use f(_, x), which will create a Fix{<:Any, (2,)} object. When calling a function with two arguments, the fact that Fix{<:Any, (2,)} behaves as Fix{<: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.