Proposal for a first-class dispatch wrapper

Most concepts in Julia have first-class object representations. However, currently dispatch is arguably not one of those things. This hasn’t been a huge issue so far, since people have sometimes found various workarounds. But I want to propose adding a sort of explicit arrow type. I think this would also be especially helpful in dealing with the new world update logic.
This also would be an answer to Helping julia with runtime dispatch/arrowtypes.
Current developments in this direction include:
GitHub - yuyichao/FunctionWrappers.jl
and
https://github.com/JuliaLang/julia/pull/18444
and
[RFC] Type stable wrapper for arbitrary callables · Issue #13984 · JuliaLang/julia · GitHub

What do I think this should look like? An arrow type would describe a mapping from the input argument types to the return type. There would not be additional runtime specialization (unlike invoke, which takes both the argument signature into account also to create a method specialization). Thus it can be used to populate a type-stable Array or type-field, even when the function argument type changes, and even if the desired return type is unknown (by using a return type of Any). In any case, it would skip the runtime dispatch. It can be used to capture the method lookup of either the current world of the dynamic scope; or it can be used to capture the method lookup of the newest world (see example below of using this to change world).

The basic definition would look like:

immutable ArrowType{ReturnType <: Type}
    argument_types::Type{T} where T <: Tuple
    lambda::MethodInstance
    worldcounter::UInt
    function ArrowType(argument_types::Type{T} where T <: Tuple,
                       newest::Bool=false)
        world = newest ? current_world_counter() : tls_world_age()
        return new(argument_types,
                   which_by_types_in_world(argument_types, world),
                   world)
    end
end

function (at::ArrowType{RT})(args...) where RT
    # this implementation is pseudo-code,
    # it would be actually implemented as an intrinsic
    # since in general I think this can be inlined and
    # optimized very effectively
    typeassert(typeof(args), at.argument_types)
    return with_world_counter=(at.worldcounter) do
        return $(Expr(:invoke, at.lambda, args...))::RT
    end
end

We could then use this logic to implement a custom dispatch system. For example, we could use this to pre-generate a type-stable dispatch table:

functions = Any[+, -, *, /]
arrows = [ ArrowType{Float64}(Tuple{typeof(f), Float64, Float64}) for f in functions ]
[ arrow(f, 1, 2) for arrow in arrows ]

This is a silly example, since we only call each function once, the dispatch cost is the same. However, I think this would have applications for writing callbacks in such varied places as Date parsers, GUI callbacks, and REPL keyboard hooks, pre-computed state transitions, and serialized code.

In these cases, there’s also interest in being able to register both a function and the world state, which is a large part of the motivation for this proposal:

function register_callback(this::SomeType, f::ANY)
    arrow = ArrowType{Any}(
        #=arguments=# Tuple{typeof(f), specialization_argument_types...},
        #=newest=# true)
    this.callback = arrow
    this.callback_func = f
    nothing
end
function do_callback(this::SomeType, args...)
    return this.callback(this.callback_func, args...)
end

In this example, the callback registration is explicitly indicating that it should use the newest world to compute the dispatch for that argument tuple. Changing the world age of the dynamic scope would normally prevent specialization of both the method lookup and return type. This definition of an arrow type would allow separating both of those concerns (by allowing the user to specify both explicitly) and thereby provide a way for the user to describe a solution for both which type-inference can use. I think this will also be a nice improvement over the current approach of calling eval to switch to the newest world:

function eval_something_newworld(args...)
  # return eval(current_module(), Expr(:body, Expr(:return, Expr(:call, QuoteNode.(args)...))))
  return ArrowType{Any}(typeof(args), true)(args...)
end

(Just FYI, attempting to use these as callback hooks from @generated function will likely result in a runtime error, unless the world-counter matches).

14 Likes

Following the discussion generated by https://github.com/JuliaLang/julia/pull/19801, I think that the world-capturing behavior of that proposal was probably sub-par (complex to implement and use). I think the following is instead a much more useful model. For those following along with that issue, this is also intended to be a very close model for how I think the cfunction itself should also behave.

# a Callback is just a callable (plus a dispatch cache)
immutable Callback{ReturnType <: Type} <: Function
    func::Any
    cache::Ref{SimpleVector}
    function Callback(func::ANY)
        return new(func, Ref(svec()))
    end
end

# allow implicit conversion of Any -> Callback{T}
# and Callback{T} -> Callback{S} where S >: T
eltype{RT}(::Type{Callback{RT}}) = RT
convert{C <: Callback}(::Type{C}, x::ANY) = C(x)
convert{C <: Callback}(::Type{C}, x::Callback) =
    if (eltype(x) <: eltype(C))
        return C(x)
    else
        throw(TypeError(:Callback, "convert", eltype(C), eltype(x)))
    end

function (at::Callback{RT})(args...) where RT
    # this implementation is pseudo-code,
    # it would be actually implemented as an intrinsic
    # since in general I think this can be inlined and
    # optimized very effectively
    push(tls-world-age = world-counter)
    # during the following dispatch,
    # the lookup result may get cached
    # as `svec(Method, argtypes...)`
    # for faster results
    local retval = at.func(args...)::RT
    pop(tls-world-age)
    return retval
end

I find this to be much simpler in all of concept, implementation, and usage. This also serves to wrap an arbitrary object in a callable, and has optimization behavior figured out by the callee (rather than needing to be declared by the user).

This makes the above examples much simpler:

# type SomeType; callback::Callback{Any}; end
register_callback(func::ANY) = (this.callback = func; nothing)
do_callback(this::SomeType, args...) = this.callback(args...)
functions = Any[+, -, *, /]
arrows = [ Callback{Float64}(f) for f in functions ]
[ arrow(1.0, 2) for arrow in arrows ] # :: Vector{Float64}
function eval_something_newworld(func, args...)
    # return eval(current_module(), Expr(:body, Expr(:return, Expr(:call, QuoteNode.(args)...))))
    return Callback{Any}(func)(args...)
end

(When called from inside a generated functions, they would still work, they’ll just fallback to acting like a normal dynamic dispatch, since for the generated functions, the newest world is the world-age in which it was defined.)

has something happened here?

I really would like to call methods directly, is there some progress with julia 1.0?

6 Likes