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).