A slightly more comprehensible `Fix{N}`

I had the pleasure to fix several variables with Base.Fix. Iterating with Fix{2}(Fix{5}(Fix{1}(...)))) isn’t very …er… ergonomic.

I wrote a slightly different Fix which can take a tuple of positions as a parameter, so one can do

fun(u,v,w,x,y,z) = “$u $v $w $x $y $z”
f = Fix{(1,4,5,2)}(fun, 1, 4, 5, 2)
f(-3, -6)
g = Fix{2}(f, -6)
g(-3)

Any thoughts? I think it’s backwards compatible with the current Fix. Should I submit a PR?

Code
const _stable_typeof = Base._stable_typeof  # throw in this if running outside of Base

struct Fix{N,F,T} <: Function
    f::F
    x::T

    function Fix{N}(f::F, x...) where {N, F}
        if N isa Int
            fix = (N,)
        elseif N isa NTuple{L, Int} where L
            fix = N
        else
            throw(ArgumentError(LazyString("expected type parameter in Fix to be `Int` or a tuple of `Ints`, but got `", N, "::", typeof(N), "`")))
        end

        any(<(1), N) && throw(ArgumentError(LazyString("expected type parameter in Fix to be integers greater than 0, but got ", N)))

        length(N) == length(x) || throw(ArgumentError(LazyString("type parameter in Fix specifies $(length(N)) fixed arguments $N, but got $(length(x)): ",x)))

        new{fix, _stable_typeof(f), _stable_typeof(x)}(f, x)
    end
end

@generated function (f::Fix{N,F,T})(args...; kws...) where {N,F,T}

    callexpr = :(f.f(; kws...))
    allargs = callexpr.args

    offset = length(allargs)  # for function name and parameters

    # make room for all args
    resize!(allargs, offset+length(args)+length(N))
    for i in offset+1:length(allargs)
        allargs[i] = :undef
    end

    # fill in the fixed args
    for n in 1:length(N)
        allargs[N[n]+offset] = :(f.x[$n])
    end

    # and the others from args
    nextarg = 1
    for i in offset+1:length(allargs)
        if allargs[i] === :undef
            allargs[i] = :(args[$nextarg])
            nextarg += 1
        end
    end

    return callexpr
end
1 Like

I’m wondering if it would make sense to order the fixed arguments and to support iterated fixing of arguments. The following would then be equal (and of the same type):

Fix{(1,2)}(f, -1, -2)
Fix{(2,1)}(f, -2, -1)
Fix{1}(Fix{2}(f, -2), -1)
Fix{1}(Fix{1}(f, -1), -2)

This would make it more user-friendly to dispatch on such types.

Ordering the fixed arguments is reasonably easy, if only there were a sortperm(::Tuple). The iteration might be somewhat more involved, but probably a good idea.

Intuitively I imagine this could work recursively. Seems like there’s something like that in Curry.jl

I’m not saying it’s necessarily a good idea but maybe something to consider.

1 Like

There’s one thing I don’t understand in the Fix in Base. It calls Base._stable_typeof which is defined as

_stable_typeof(x) = typeof(x)
_stable_typeof(::Type{T}) where {T} = @isdefined(T) ? Type{T} : DataType

What does this function do? Or more specifically, for which inputs does it return DataType?

Some specific issues:

  • Currently Base.Fix2 === Base.Fix{2}. With your change you presumably want to have Base.Fix2 === Base.Fix{(2,)}, which would be breaking.

  • With your change T(args...) isa T would not hold for T === Base.Fix{1}, for example.

Potential issue (not sure): are you sure that arguments which are types could be supported in a performant manner with your proposal. Keep in mind:

julia> (Int, String)
(Int64, String)

julia> typeof(ans)
Tuple{DataType, DataType}

In general, IMO the proposal “smells bad” to me, because it complicates the implementation for no certain benefit. Furthermore, as extensively discussed already on the original Fix PR, something like this should go into a package first, instead of forcing it into Base prematurely.

1 Like

The first method is for non-Types, the second method is for Types. The branch in the second method is for handling incomplete types.

Ah, I hadn’t thought about that. That’s a good point. For arguments that are types, like Int, we have Int isa Type{Int}, so the T in Fix can be Type{Int} (as well as the non-specific DataType), but this relation does not extend to tuples, i.e. we do not have (Int,) isa Tuple{Type{Int}}, so the T in Fix has to be Tuple{DataType}, which is non-specific and probably not performant.

So, one would have to use single argument fixing for types.