Ignore{N} similar to Fix{N}

I am occasionally finding something like this useful:

struct Ignore{N,F}
    f::F
    Ignore{N}(f::F) where {N,F} = new{N,F}(f) # no checks etc, just an MWE
end

function (f::Ignore{N})(args::Vararg{Any,M}) where {N,M}
    f.f(args[begin:begin+(N-2)]..., args[begin+N:end]...)
end

julia> Ignore{3}((x...,) -> x)(1:5...)
(1, 2, 4, 5)

so I am wondering if it would make sense to add this to Base to complement Fix.

This is interesting. Since I rarely use argument spreading, may you provide a more practical example to demonstrate how it could be used?

I think the main reason for Base.Fix is that functions can have specialized methods for the case where one argument is fixed, and hence some things can be precomputed. Otherwise, an anonymous function would work just as well.

I’m not sure why this would be true for Ignore?

I believe Base.Fix{N} is used to eliminate closures (not sure if that’s still a thing in Julia though), or to facilitate constant propagation. However, the theoretical use cases of Ignore{N} doesn’t involve those, so this might not be too useful?

As I recall, the more specific reason for Base.Fix is so that we don’t create, compile, and carry N distinct anonymous functions for N different places in the code where we would write (e.g.) x -> x < y. Instead, we only have one Base.Fix2{typeof(<), typeof(y)}) function for each typeof(y).

julia> (x -> x < 0) === (x -> x < 0)
false

julia> <(0) === <(0)
true

The Base.Fix is itself a closure, so it’s not that it ā€œpreventsā€ a closure per-se. And it’s worse for constant propagation, not better. For example:

julia> code_llvm(x -> x < 0, (UInt,); debuginfo=:none) # const-prop the 0
; Function Signature: var"#29"(UInt64)
define i8 @"julia_#29_5695"(i64 zeroext %"x::UInt64") #0 {
top:
  ret i8 0
}

julia> code_llvm(<(0), (UInt,); debuginfo=:none) # does not const-prop because it's a generic closure
; Function Signature: (::Base.Fix{2, typeof(Base.:(<)), Int64})(UInt64)
define i8 @julia_Fix_5289(ptr nocapture noundef nonnull readonly align 8 dereferenceable(8) %"f::Fix", i64 zeroext %"arg::UInt64") #0 {
top:
  %"f::Fix.unbox" = load i64, ptr %"f::Fix", align 8
  %0 = icmp sgt i64 %"f::Fix.unbox", -1
  %1 = icmp ugt i64 %"f::Fix.unbox", %"arg::UInt64"
  %2 = and i1 %0, %1
  %3 = zext i1 %2 to i8
  ret i8 %3
}

Still, in many cases the constant propagation would be of little/no value and the code reuse of Base.Fix is pure up-side.

In fact, it was initially introduced to replace exactly two cases that already had specialized implementations EqualsTo and OccursIn of partial evaluation:

But, digging deeper, the motivation for EqualsTo was indeed (in part) to ā€œcut down the number of function typesā€:

Putting aside Ignore’s specific merits momentarily, I think there’s too much in Base as-is, so I prefer new things to live in packages if the rest of Base doesn’t use it. If that ever changes, the package can just be deprecated and suggest the Base version.

Thanks for all the answers. Just to clarify the motivation: I consider Ignore{N}, which is (semantically) equivalent to a (a, _b, c) -> f(a, c) closure, a natural complement to Fix{N}, which does (a, c) -> f(a, fixed_b, c).

But other than that, there is indeed no strong reason to do this. The main motivation for Fix{1|2} was indeed specialization. Though occasionally, the knowledge that an argument is not used could simplify a reduction, that I think is pretty rare to warrant a special case.

That said, Fix{N} branched into its own mini-language to eliminate closures used solely for partial application. I am not aware of anyone specializing on, say, Fix{3} of a method. Introducing Ignore{N} could be viewed as compounding a design mistake further.

Yes, I heard that argument, but AFAIK it is only costly when the closure is created at top-level. Within a function it should be fine.

I wonder if it would make sense to have the language could just recognize semantically equivalent closures as equivalent, eg (a, b) -> a > b vs (x, y) -> x > y. It would not have to be perfect and could stop the analysis above some expression complexity.

I believe treating semantically equivalent closures as equivalent just adds unnecessary complexities to both the compiler and the user. The handling of scopes and shadowing, no matter how good, could introduce many confusions and potential glitches. The added complexity of the compiler would outweigh the relatively trivial cost of an extra closure.