How to mark a function argument for specialization on types while still staying as a generic argument?

This is a follow up question which cristallized from this other discourse topic

Problem

I am struggeling with improving performance of my package ExtensibleEffects.jl because Julia is not specializing a function on types in certain cases. See the docs.

For me it feels, this case should not avoid specialization, but anyway, in current Julia this is what happens. Hence I am looking for a way to tell the compiler it should specialize on types, however same time I want to keep my generic function.

I.e. the recommendation from the docs, to just use myfunc(::Type{T}) where T = ... in order to trigger specialization does not work for me, because I need my function myfunc to stay generic.
What other way exist to mark a function for type-specialization?

Example

Here my concrete minimized example which fails to infer for the type input, but runs smoothly for a normal input. It is adapted from ExtensibleEffects. ChainedFunctions essentially represents function composition without composing functions, but just by storing functions. As the error already happens without composition, the example feels incomplete, but please take it for granted, that this is useful for ExtensibleEffects.jl.

struct ChainedFunctions{Fs}
    functions::Fs
    ChainedFunctions(functions...) = new{typeof(functions)}(functions)
end

myconvert(T, value) = T(value)

function myconvert_wrapper(type, value)
    continuation = ChainedFunctions(x -> myconvert(type, x))
    first_func = Base.first(continuation.functions)
    first_func(value)
end

and here the test which partly works, and partly fails.

using Test
tostring(a) = "$a"
@inferred myconvert_wrapper(tostring, 1)
# "1"

@inferred myconvert_wrapper(Symbol, 1)
# ERROR: return type Symbol does not match inferred return type Any
1 Like

The following seems to work, but it is ugly. Furthermore, I don’t understand why this works while yours does not. Maybe someone can improve this?

function myconvert_wrapper(t::Union{T,Type{T}}, value) where T
    if isa(t,Type)
        continuation = ChainedFunctions(x -> myconvert(T, x))
    else
        continuation = ChainedFunctions(x -> myconvert(t, x))
    end
    first_func = Base.first(continuation.functions)
    first_func(value)
end

Hi Stephen, thank you very much

this way I kind of found as well. It works, because you duplicate the decisive code part. The if isa(t, Type) works like a subfunction with a clause for Type{T} where T} and another normal clause. And the creation of the closure ChainedFunctions(x -> myconvert(T, x)) is the core thing which fails if not specialized.

I am looking for a solution which does not duplicate the code (and does not use a macro for hidden code duplication either).

why? the point of multi-dispatch is to allow you to write specialized methods. You’re kind of asking people to do it by not doing it.

Also, this example seems to be contrived, in the sense that myconvert is designed to deliberately to mix value and Type together, which is something you should avoid irl.

If this is not a XY problem, my suggestion is for you to additionally define this:

julia> function myconvert_wrapper(::Type{T}, value) where T
           continuation = ChainedFunctions(x -> myconvert(T, x))
           first_func = Base.first(continuation.functions)
           first_func(value)
       end
1 Like

you can also re-organize your code btw:

julia> F(::Type{T}) where T = x -> myconvert(T, x)

julia> F(t) = x -> myconvert(t, x)

julia> function myconvert_wrapper(t, value)
           continuation = ChainedFunctions(F(t))
           first_func = Base.first(continuation.functions)
           first_func(value)
       end

julia> using Test

julia> @inferred myconvert_wrapper(tostring, 1)
"1"

julia> @inferred myconvert_wrapper(Symbol, 1)
Symbol("1")

I don’t think mixing a function and a type is necessarily discouraged. In particular, the first argument of function get! in Base for Dict is Union{Function,Type}. As far as I can tell, get! specializes properly for both functions and types.

1 Like

that one is just a callable, again, if you specialize on it, you specialize on it.

I’m saying what we’re doing here to the argument seems unnatural.

Also, the type stability of get!() comes from cheating:

it’s stable because we always convert to the type of the values of dict

2 Likes

I appreciate sharing my perspective that some argument can be a type or a function or a value, and that generic code should not need to case-distinguish these.

With the current implementation of Julia this is unfortunately necessary for writing closures, as type-specialization does not trigger (does not trigger correctly?). For me writing closures is generic programming.

Thank you all for your input. I will file a feature request/bug report to julialang respectively.

Here is the feature request: Feature Request: Have a way to specialize on Type without making the argument restricted to Types only · Issue #45266 · JuliaLang/julia · GitHub