Request: "type functions" for parametric dispatch

Wouldn’t it be great to have “type functions” in the dispatch of our methdos?

Suppose we have a boolean function which determines a property of a type

struct data{C} end
property(::Type{data{C}}) where C = C > 0

This tells us if C>0 on the parametric type, similar to data{1}<:data, so it’s a “type function”

julia> property(data{1})
true

julia> property(data{0})
false

Now, since this can techincally already be computed at compile time, why not also make this function usable as a “type dispatch filter” function, for example

julia> positive(::T) where property(T) where T<:data = "dispatch on positive C only"
ERROR: syntax: invalid variable expression in "where"

Since property(T) is a “type function” it returns Bool on Type inputs, which means it is not much differnt from T<:data which is also a “type function” returning a Bool.

Therefore, it would make sense to be able to define more “type functions” for specific types, other than only <: for example.

This has been previously discussed

However, I’d like to have another fresh look at this:

Note however that A <: B relations form a hierarchy, while properties would not.

In any case, you can use a trait:

_dosomething(is_positive::Val{true}, value) = ...

dosomething(value) = _dosomething(is_positive_trait(value), value)

is_positive_trait(::Data{C}) where {C} = Val{C > 0}()

You are encouraged to dispatch on computed (hopefully constant) results, i.e. traits but this should not be part of the dispatch itself since there’s a strong push against making the dispatch Turing complete, i.e. unpredictable (undecidable). IIRC, this doesn’t only cover allow running arbitrary code as part of dispatch but maybe some other more complex forms as well.

That said, more integrated syntax without changing the semantics is certainly possible if the distinction is made clear (e.g. your where property(T), spelled differently, could be made a check in the function body, if the syntax makes it clear that this is what’s happening)

4 Likes

Can you elaborate here please. What do you mean by dispatching on traits but it shouldn’t be part of dispatch?

And what covers running arbitrary code and what complex forms?

Dispatch is to decide which method to call. In particular, in julia, it needs to decide whether a function is applicable and also which one is the most specific method. The requirement to decide on specifity means that the dispatch rule cannot be turing complete. For example, it’ll be impossible to tell, in general, if f(::T) where property1(T) is more specific than f(::T) where property2(T) in the cases where both matches. (The only safe solution is then to always throw an error in ambiguious case like this, which is what C++ does).

As shown in the example, this directly rules out running arbitrary code (no matter how pure it is) during dispatch. Extending the set of operation allowed is possible, as was done in 0.6, but callling arbitrary black box function is not.

The ability for the dispatch code to fully “understand” the dispatch rule also means that the compiler can do that too, even with partial information. This enables type inference to infer code even without full type information and in general gives inference an easier time doing its job… As an example, that’s why the following code can be fully inferred even though the input type to f is unknown.

julia> f(x) = 1
f (generic function with 1 method)

julia> f(x::Integer) = 2
f (generic function with 2 methods)

julia> function g(x)
           f(x[])
       end
g (generic function with 1 method)

julia> @code_warntype g(Ref{Integer}(1))
Body::Int64
2 1 ─     (Base.getfield)(x, :x)                                                     │╻╷ getindex
  └──     return 2                                                                   │

Note that the requirement I listed above only applies when you want to make garantee about certain aspect of the system, i.e. the decidability of the dispatch (and making sure you can make prediction based on partial type that is correct). It does not mean that you can’t run arbirary code and feed the result of that into dispatch. Since the code you put in there could be arbitrary and there’s in general no guarantee that the compiler can know what the input value is at compile time, you may not get guaranteed static dispatch at compile time, but the code should always run fine whether or not it’s optimized (and should be optimized most of the time if it’s well written and well used).

Traits is basically doing exactly this with tools around it for convinience and you can see this in the example from @Tamas_Papp above. The actual dispatch happens at _dosomething (presumably you can define one with Val{false}) and you can feed whatever you want as the first argument based on arbitrary computation you did in is_positive_trait.

6 Likes

Oh, forgot about this one. The obvious example I can think of and that is asked the most (AFAICT) is something like.

f(::T{K}) where T where K

Jeff had some argument about why this makes the dispatch undecidable since this effectively introduces function calls.

1 Like