JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.66k stars 5.48k forks source link

Dispatch on a type being anywhere in a signature #39878

Open MasonProtter opened 3 years ago

MasonProtter commented 3 years ago

Okay, so this is a bit of a weird feature request, but there are some circumstances, for example when writing a CAS, where you want to dispatch on whether some type T occurs anywhere in an args list. Currently, the only real way to do this is with a trait like mechanism, e.g.

struct T end
struct HasT end
struct DoesntHaveT end

has_T(::Tup) where {Tup <: Tuple} = T ∈ Tup.parameters ? HasT() : DoesntHaveT();

f(args...) = f(has_T(args), args...)
f(::HasT, args...) = 1;
f(::DoesntHaveT, args...) = 2;
julia> f(1, 2, 3, T(), 4)
1

julia> f(1, 2, 3, 4)
2

I think it would be quite useful if there was a built-in dispatch mechanism for this. Perhaps a syntax like

f(args::Tup) where {Tup <: Tuple, T ∈ Tup} = ...

or even

f(args_left..., ::T, args_right...) = ...

where args_right is allowed to have instances of T in it?

I get that this is probably too complicated and niche a thing to put in the dispatch system, but I figured I should bring it up in case anyone else and thoughts on this and I didn't see an issue for it.

JeffBezanson commented 3 years ago

Very fancy, but definitely not weird at all! This has certainly come up before, and would be really cool. I think it does make it even easier to write lots of ambiguities, but that's never really stopped us before.

JeffBezanson commented 3 years ago

I think the first interesting thing that comes up is that we don't get a way to represent the intersection of these "for free". I.e. let All{A} == Tuple{Vararg{A}}, and Any{A} be the new thing, then

All{A} ∪ All{B} = Union{All{A}, All{B}}  # since we have union types anyway
All{A} ∩ All{B} = All{A ∩ B}  # can compute it recursively
Any{A} ∪ Any{B} = Any{A ∪ B}
Any{A} ∩ Any{B} = uhoh!

So we might have to add explicit intersection types at that point too...

oxinabox commented 3 years ago

TensorFlow.jl wanted this for example. (If any argument is a TensorFlow.Tensor then all other arguments need to be a converted to TensorFlow.constants). As a work around we just unrolled 3-5 arguments and handled each. Could probably make a macro for it

MasonProtter commented 3 years ago

Are intersection types something that we'd want in the type system? I certainly think I'd want it, but I can imagine that it'd be unwanted because it's yet another avenue for writing non-terminating type-level manipulations.

alok commented 3 years ago

@MasonProtter do the non terminations matter in practice though, GHC triggers plenty of opportunities for them and seems to do fine with a timeout.

MasonProtter commented 3 years ago

As I said, I think I want it. I was more wondering if the language devs could explain if there's some terrible downside to this.

vtjnash commented 3 years ago

I recall GHC uses only the much more limited HM-type-inference. Non-terminations do occur relatively frequently in Julia already at the inference level, but don't happen in the type system. It seems unlikely that explicit intersection type representations would make this non-terminating, since it is already very similar to existing expressions. (Specifically, I think it is already quite close to being expressed by the constraints implied by repeated TypeVars such as typeintersect(Tuple{T, T} where T, Tuple{A, B} where {A, B}))

stevengj commented 3 years ago

Currently, the only real way to do this is with a trait like mechanism, e.g.

As discussed in #42455, you can't in general accomplish this in Julia right now if you are extending someone else's method, without resorting to type piracy.

42455 suggests a simpler syntax for this analogous to the Vararg type, which doesn't require parser changes. Your example above would become:

f(args::VarargOne{T,Any,N}) where {N} = ...

(The VarargOne name can be bikeshedded, of course. We could even just call it Vararg{Any,N,T}, for that matter, where Vararg{S,N,T} would denote a tuple of N elements of type S, at least one of which must be of type T, but I'm not sure if adding an optional parameter to Vararg would be considered a breaking change or too punny.)

stevengj commented 3 years ago

Regarding intersections, you could have

VarargSome{S,N,(T1,)} ∩ VarargSome{S,N,(T2,)} = VarargSome{S,N,(T1,T2)}

if you allowed VarargSome{S,N,(T...)} to be parameterized by a tuple of types T, each of which must appear in the tuple at least once.

MasonProtter commented 3 years ago

Yes, this seems like a great way to get the thing that I wanted (it seems we also have very similar use cases).

The name can be bikeshed, but I do like that this doesn’t require any funny parser changes or anything like that.

ericphanson commented 1 year ago

Just to say this would be useful for Convex.jl, which currently pirates hcat, vcat, and hvcat to support when at least one argument is a Convex AbstractExpr. It is a much worse api to require users use a function like e.g. Convex.hcat because that doesn't hook into the special [A B] hcat syntax (and similarly for vcat and hvcat). (But it seems we can downgrade our piracy to the point the package will at least precompile on 1.10, so maybe there isn't urgency).

vtjnash commented 1 year ago

That PR seems right for the current state of affairs for vcat (waiting on https://github.com/JuliaLang/julia/issues/2326). You are currently permitted to add your own types to hcat, as long as they do not subtype Number or AbstractArray; that is not piracy. For example, LinearAlgebra demonstrates how to add AbstractQ and UniformScaling to the set of joinable objects.

ericphanson commented 1 year ago

You are currently permitted to add your own types to hcat, as long as they do not subtype Number or AbstractArray; that is not piracy

We have aliases

const Value = Union{Number,AbstractArray}
const AbstractExprOrValue = Union{AbstractExpr,Value}

so I think Base.hcat(args::AbstractExprOrValue...) violates that rule, right?

LinearAlgebra demonstrates how to add AbstractQ and UniformScaling to the set of joinable objects

Are you saying using these methods? https://github.com/JuliaLang/julia/blob/f0a28e9a45a34f1c524b3cf02cbbac1351f1da81/stdlib/LinearAlgebra/src/abstractq.jl#L133-L142

Maybe that can work. If there is any AbstractExpr in the arguments, we have to promote the whole thing to an AbstractExpr (which is a symbolic object, not really an actual array), so we need some way to ensure Convex's methods win out.

edit: it looks like currently only the first object matters for cat_similar, so this wouldn't work as-is. If Base had some promotion mechanism to promote among the cat_similar types for each argument, maybe that could work.

vtjnash commented 1 year ago

Not those, it defines new vcat methods very similar to that PR, which is a super type of AbstractArray