Unstable type with dispatch by Val{:X}() with 5 functions

When i use following code i have no problems with types

using LinearAlgebra, BlockDiagonals
function f1(x::Vector{T}, ::Int, ::Val{:A}) where T
    Diagonal(x)
end
function f1(x::Vector{T}, i::Int, ::Val{:B}) where T
    Diagonal(x .* i)
end
function f1(x::Vector{T}, ::Int, ::Val{:C}) where T
    Diagonal(x)
end
function f1(x::Vector{T}, ::Int, ::Val{:D}) where T
    Diagonal(x)
end
function f2(x1, x2, a, b)
    x = Vector{Matrix}(undef, 2)
    x[1] = f1(x1, 2, Val{a}())
    x[2] = f1(x2, 3, Val{b}())
    BlockDiagonal(x)
end
@code_warntype f2([1,2,3], [2,3,4], :A, 

@code_warntype:

#self#::Core.Compiler.Const(f2, false)
  x1::Array{Int64,1}
  x2::Array{Int64,1}
  a::Symbol
  b::Symbol
  x::Array{Array{T,2} where T,1}

Body::Union{}
1 ─ %1 = Core.apply_type(Main.Vector, Main.Matrix)::Core.Compiler.Const(Array{Array{T,2} where T,1}, false)
β”‚        (x = (%1)(Main.undef, 2))
β”‚   %3 = Core.apply_type(Main.Val, a)::Type{Val{_A}} where _A
β”‚   %4 = (%3)()::Val{_A} where _A
β”‚   %5 = Main.f1(x1, 2, %4)::Diagonal{Int64,Array{Int64,1}}
β”‚        Base.setindex!(x, %5, 1)
β”‚   %7 = Core.apply_type(Main.Val, b)::Type{Val{_A}} where _A
β”‚   %8 = (%7)()::Val{_A} where _A
β”‚   %9 = Main.f1(x2, 3, %8)::Diagonal{Int64,Array{Int64,1}}
β”‚        Base.setindex!(x, %9, 2)
β”‚        Main.BlockDiagonal(x)
└──      Core.Compiler.Const(:(return %11), false)

But when I add one more function…

using LinearAlgebra, BlockDiagonals
function f1(x::Vector{T}, ::Int, ::Val{:A}) where T
    Diagonal(x)
end
function f1(x::Vector{T}, i::Int, ::Val{:B}) where T
    Diagonal(x .* i)
end
function f1(x::Vector{T}, ::Int, ::Val{:C}) where T
    Diagonal(x)
end
function f1(x::Vector{T}, ::Int, ::Val{:D}) where T
    Diagonal(x)
end
function f1(x::Vector{T}, ::Int, ::Val{:E}) where T
    Diagonal(x)
end
function f2(x1, x2, a, b)
    x = Vector{Matrix}(undef, 2)
    x[1] = f1(x1, 2, Val{a}())
    x[2] = f1(x2, 3, Val{b}())
    BlockDiagonal(x)
end
@code_warntype f2([1,2,3], [2,3,4], :A, :B)

I nave this: Main.f1(x2, 3, %8)::Any

Variables
  #self#::Core.Compiler.Const(f2, false)
  x1::Array{Int64,1}
  x2::Array{Int64,1}
  a::Symbol
  b::Symbol
  x::Array{Array{T,2} where T,1}

Body::Union{}
1 ─ %1 = Core.apply_type(Main.Vector, Main.Matrix)::Core.Compiler.Const(Array{Array{T,2} where T,1}, false)
β”‚        (x = (%1)(Main.undef, 2))
β”‚   %3 = Core.apply_type(Main.Val, a)::Type{Val{_A}} where _A
β”‚   %4 = (%3)()::Val{_A} where _A
β”‚   %5 = Main.f1(x1, 2, %4)::Any
β”‚        Base.setindex!(x, %5, 1)
β”‚   %7 = Core.apply_type(Main.Val, b)::Type{Val{_A}} where _A
β”‚   %8 = (%7)()::Val{_A} where _A
β”‚   %9 = Main.f1(x2, 3, %8)::Any
β”‚        Base.setindex!(x, %9, 2)
β”‚        Main.BlockDiagonal(x)
└──      Core.Compiler.Const(:(return %11), false)

How I can make type stable code with dispatch by Val() and more than 4 functions?

1 Like

Why do you want to use dispatch for this? This is a classic abuse of dispatch, i.e. construct type based on runtime value. You should just use a branch instead.

4 Likes

Hello!
I want to apply different functions based on variable value. I think, that Val() is an instrument for this.

But this is not main quastion. @code_warntype for 4 functions give that: Main.f1(x2, 3, %8)::Diagonal{Int64,Array{Int64,1}} but for 5 give me that: Main.f1(x2, 3, %8)::Any. Why?

No it’s not. Branch is.

But it is the main question. There are limitations in inference for cases like this since it’s not meant (and also impossible) to handle calling arbitrary function using runtime constructed types. You’ve also basically already lost when you wrote Val{a}().

2 Likes

The blog post on invalidations also explains what is going on here. Basically, you’re hitting a limit on inference.

I second the recommendation to use branches (if/elseif/else) whenever behavior depends on runtime values.

5 Likes

Thanks all!

So I should use if/elseif/else for this to avoid invalidations.

But it is very attractive to use dispatch. Because I can just make a new method and it will work. And I don’t need to rewrite if/elseif/else tree. Is it really no case to do dispatch by value and without invalidations?

This is not about invalidations, just that the blogpost about it describes this phenomena.

1 Like

To be clear, you can use Julia dynamically like this, and it can be fun and useful in some cases. It just won’t be super fast. But the main problem is not really that, I think. It is the missed opportunity to learn how to get the best out of Julia, to write really Julian code, which can take quite a while.

2 Likes

I would only recommend using dispatch here if:

  1. You need/want to allow users to provide new implementations of f1 for their own Val types. And allowing them to pass function instead is, for some reason, a worse solution.
  2. You can use convert or function barriers to reduce the effect of the type instability over the functions that call f1.
  3. If you think the users will add methods for f1, be aware this may lead to invalidations which, depending on the case, may be better or worse than taking a function parameter and disabling specialization on it.
3 Likes

@pablosanjose , @Henrique_Becker and all, thanks for the clarification. The main goal for dynamic style was an easy way to add user-specific methods. And I think that I will use if/elseif/else tree. Multiple dispatches seem a very easy but not optimal solution.