# Type instability with tuples of functions and parameters

My general application of interest requires a vector of functions of the form `f(x, y, p)`, for some known parameters `p`. I put the functions in a `Tuple` so that their types are known, and define for example the following struct:

``````struct Point{T,N}
x::T
y::T
F::Tuple{Vararg{Function,N}}
F_params::Tuple{Vararg{Union{Nothing, AbstractVector{T}}, N}}
end
``````

The `F_params` is given the type shown so that a user can provide a `Tuple` with an element for each function, and either provide a vector of parameters or no parameters at all (`nothing`). I need to have functions that take in a `Point` as input, for example:

``````function TestFnc(pt::Point{T,N}) where {T,N}
res = 0.0
for (f, p) in zip(pt.F, pt.F_params)
res = res + f(pt.x, pt.y, p)
end
return res
end
F1 = (x, y, p) -> x^2 + y^2 - p[1]
F2 = (x, y, p) -> x*y
F3 = (x, y, p) -> x + y + p[2] - p[3]*p[1]
pβ = [1.0]; pβ = nothing; pβ = [1.0, 2.0, 3.0]
x = 0.2; y = 0.7
pt = Point(x, y, (F1, F2, F3), (pβ, pβ, pβ))
``````

When I use these definitions, I get type instabilities according to `@code_warntype` (see below). How can I improve my struct so that all types are properly inferred?

``````@code_warntype TestFnc(pt)
``````
``````MethodInstance for TestFnc(::Point{Float64, 3})
from TestFnc(pt::Point{T, N}) where {T, N} in Main at c:\Users\licer\testjl.jl:7
Static Parameters
T = Float64
N = 3
Arguments
#self#::Core.Const(TestFnc)
pt::Point{Float64, 3}
Locals
@_3::Union{Nothing, Tuple{Tuple, Tuple}}
res::Any
@_5::Int64
p::Any
f::Any
Body::Any
1 β       (res = 0.0)
β   %2  = Base.getproperty(pt, :F)::Tuple{Function, Function, Function}
β   %3  = Base.getproperty(pt, :F_params)::Tuple{Union{Nothing, AbstractVector{Float64}}, Union{Nothing, AbstractVector{Float64}}, Union{Nothing, AbstractVector{Float64}}}
β   %4  = Main.zip(%2, %3)::Base.Iterators.Zip
β         (@_3 = Base.iterate(%4))
β   %6  = (@_3 === nothing)::Bool
β   %7  = Base.not_int(%6)::Bool
βββ       goto #4 if not %7
2 β %9  = @_3::Tuple{Tuple, Tuple}
β   %10 = Core.getfield(%9, 1)::Tuple
β   %11 = Base.indexed_iterate(%10, 1)::Core.PartialStruct(Tuple{Any, Int64}, Any[Any, Core.Const(2)])
β         (f = Core.getfield(%11, 1))
β         (@_5 = Core.getfield(%11, 2))
β   %14 = Base.indexed_iterate(%10, 2, @_5::Core.Const(2))::Core.PartialStruct(Tuple{Any, Int64}, Any[Any, Core.Const(3)])
β         (p = Core.getfield(%14, 1))
β   %16 = Core.getfield(%9, 2)::Tuple
β   %17 = res::Any
β   %18 = Base.getproperty(pt, :x)::Float64
β   %19 = Base.getproperty(pt, :y)::Float64
β   %20 = (f)(%18, %19, p)::Any
β         (res = %17 + %20)
β         (@_3 = Base.iterate(%4, %16))
β   %23 = (@_3 === nothing)::Bool
β   %24 = Base.not_int(%23)::Bool
βββ       goto #4 if not %24
3 β       goto #2
4 β       return res
``````
``````julia> isabstracttype(Function)
true
``````
1 Like

I donβt know what you are suggesting here, sorry.

The comment is that your type has abstractly-typed fields. Maybe this would suit your needs:

``````struct Point{T,N,FTup<:NTuple{N,Function},PTup<:NTuple{N,Union{Nothing, AbstractVector{T}}}}
x::T
y::T
F::FTup
F_params::PTup
end
``````
1 Like

Ah, I didnβt know that having abstract types was a bad thing here. Thank you. When I make this change, i still get some instances of red text with unions in `@code_warntype`. Are there any problems here, or are these unions fine despite the red text? I list below these unions it warns me of, and also the full code/output after.

• `f::Union{var"#11#12", var"#13#14", var"#15#16"}`
• `@_3::Tuple{Tuple{Union{var"#11#12", var"#13#14", var"#15#16"}, Union{Nothing, Vector{Float64}}}, Tuple{Int64, Int64}}`
• `Core.getfield(%9, 1)::Tuple{Union{var"#11#12", var"#13#14", var"#15#16"}, Union{Nothing, Vector{Float64}}}`
• `Base.indexed_iterate(%10, 1)::Tuple{Union{var"#11#12", var"#13#14", var"#15#16"}, Int64}`
• `Base.indexed_iterate(%10, 2, @_5)::Union{Tuple{Nothing, Int64}, Tuple{Vector{Float64}, Int64}}`
Code:
``````struct Point{T,N,FTup<:NTuple{N,Function},PTup<:NTuple{N,Union{Nothing, AbstractVector{T}}}}
x::T
y::T
F::FTup
F_params::PTup
end
function TestFnc(pt::Point{T,N,FTup,PTup}) where {T,N,FTup,PTup}
res = 0.0
for (f, p) in zip(pt.F, pt.F_params)
res = res + f(pt.x, pt.y, p)
end
return res
end
F1 = (x, y, p) -> x^2 + y^2 - p[1]
F2 = (x, y, p) -> x*y
F3 = (x, y, p) -> x + y + p[2] - p[3]*p[1]
pβ = [1.0]; pβ = nothing; pβ = [1.0, 2.0, 3.0]
x = 0.2; y = 0.7
pt = Point(x, y, (F1, F2, F3), (pβ, pβ, pβ))
@code_warntype TestFnc(pt)
``````
``````MethodInstance for TestFnc(::Point{Float64, 3, Tuple{var"#11#12", var"#13#14", var"#15#16"}, Tuple{Vector{Float64}, Nothing, Vector{Float64}}})
from TestFnc(pt::Point{T, N, FTup, PTup} where {FTup<:Tuple{Vararg{Function, N}}, PTup<:Tuple{Vararg{Union{Nothing, AbstractVector{T}}, N}}}) where {T, N} in Main at c:\Users\licer\testjl.jl:7
Static Parameters
T = Float64
N = 3
Arguments
#self#::Core.Const(TestFnc)
pt::Point{Float64, 3, Tuple{var"#11#12", var"#13#14", var"#15#16"}, Tuple{Vector{Float64}, Nothing, Vector{Float64}}}
Locals
@_3::Union{Nothing, Tuple{Tuple{Union{var"#11#12", var"#13#14", var"#15#16"}, Union{Nothing, Vector{Float64}}}, Tuple{Int64, Int64}}}
res::Float64
@_5::Int64
p::Union{Nothing, Vector{Float64}}
f::Union{var"#11#12", var"#13#14", var"#15#16"}
Body::Float64
1 β       (res = 0.0)
β   %2  = Base.getproperty(pt, :F)::Core.Const((var"#11#12"(), var"#13#14"(), var"#15#16"()))
β   %3  = Base.getproperty(pt, :F_params)::Tuple{Vector{Float64}, Nothing, Vector{Float64}}
β   %4  = Main.zip(%2, %3)::Base.Iterators.Zip{Tuple{Tuple{var"#11#12", var"#13#14", var"#15#16"}, Tuple{Vector{Float64}, Nothing, Vector{Float64}}}}
β         (@_3 = Base.iterate(%4))
β   %6  = (@_3::Core.PartialStruct(Tuple{Tuple{var"#11#12", Vector{Float64}}, Tuple{Int64, Int64}}, Any[Tuple{var"#11#12", Vector{Float64}}, Core.Const((2, 2))]) === nothing)::Core.Const(false)
β   %7  = Base.not_int(%6)::Core.Const(true)
βββ       goto #4 if not %7
2 β %9  = @_3::Tuple{Tuple{Union{var"#11#12", var"#13#14", var"#15#16"}, Union{Nothing, Vector{Float64}}}, Tuple{Int64, Int64}}
β   %10 = Core.getfield(%9, 1)::Tuple{Union{var"#11#12", var"#13#14", var"#15#16"}, Union{Nothing, Vector{Float64}}}
β   %11 = Base.indexed_iterate(%10, 1)::Tuple{Union{var"#11#12", var"#13#14", var"#15#16"}, Int64}
β         (f = Core.getfield(%11, 1))
β         (@_5 = Core.getfield(%11, 2))
β   %14 = Base.indexed_iterate(%10, 2, @_5)::Union{Tuple{Nothing, Int64}, Tuple{Vector{Float64}, Int64}}
β         (p = Core.getfield(%14, 1))
β   %16 = Core.getfield(%9, 2)::Tuple{Int64, Int64}
β   %17 = res::Float64
β   %18 = Base.getproperty(pt, :x)::Float64
β   %19 = Base.getproperty(pt, :y)::Float64
β   %20 = (f)(%18, %19, p)::Float64
β         (res = %17 + %20)
β         (@_3 = Base.iterate(%4, %16))
β   %23 = (@_3 === nothing)::Bool
β   %24 = Base.not_int(%23)::Bool
βββ       goto #4 if not %24
3 β       goto #2
4 β       return res
``````

You seem to be emulating a class-based OOP approach when putting functions inside your structs. Itβs not exactly clear from your example if this is necessary.

Can you reftame your problem in terms of dispatch with external methods, or must each instance of `Point` have its own individual tuple of functions?

Basically, your code is not idiomatic for Julia, and itβs not clear if that is unavoidable.

3 Likes

The actual application Iβm considering is for some PDE code. These functions in this simple example are to resemble a user providing functions for certain parts of a PDE, and for mixed boundary conditions (for which there can be arbitrarily many, hence my `Vararg` initially). So I think for your second question itβs the latter, i.e. each instance of `Point` does need its own set of functions. So do you think the current state of that code is about as far as I might get, then?

No, most likely there is a different approach and better design. Julia is used a lot for PDEs and ODEs, so thereβs a lot of expertise.

Unfortunately, I have no experience with this.

But do you anticipate that each point on your grid will have its own unique and individual set of functions? That sounds a bit exotic to me. Is there no grouping, like interior points vs boundary points?

3 Likes

Sorry, I was a bit unclear (and I probably should have used a different name than `Point`). These functions are primarily for boundary conditions. Iβm considering for example a domain \Omega split into N separate segments (e.g. a square with a boundary condition for each side, or a triangle), each with a different boundary condition, and these functions are my `F` in my initial example and their parameters are `F_params`.

You can make it type stable / get rid of any unions if `N` is not too big by using a generated function and unrolling the loop (see below). (There is also the `Unrolled.jl` package, but it would require changing your function signature I think.)

If `N` is large, you could wrap your functions in FunctionWrappers, and store those in the function tuple. However, note this is essentially calling them via a pointer so adds a bit of overhead.

Whatever you decide, you can set `pβ = Float[]` and drop the `Union` type in the definition of `Point` so the parameter tuple has the same type for all elements.

So something like this (which Iβm sure someone else can point out a better way to do) is then type stable:

`````` struct Point{T,N,FTup,PTup<:NTuple{N,AbstractVector{T}}}
x::T
y::T
F::FTup
F_params::PTup
end

@generated function TestFnc(pt::Point{T,N,FTup,PTup}) where {T,N,FTup,PTup}
quote
res = 0.0
Base.Cartesian.@nexprs \$N i -> res = res + pt.F[i](pt.x, pt.y, pt.F_params[i])
return res
end
end
``````
1 Like

Thank you very much @isaacsas. Iβll have to think about this for quite a while since Iβve never used `@generated`, but this looks very nice. Can `Base.Cartesian.@nexprs` also be used index range is not something like `1:N`? e.g. can it be extended to work for something like

``````for j in idx
``````

where `idx` could be say `[1, 3, 5, 10, 17]` (just a vector of indices in general)? Looking at the documentation julia/cartesian.jl at 742b9abb4dd4621b667ec5bb3434b8b3602f96fd Β· JuliaLang/julia Β· GitHub doesnβt seem to suggest this is a feature.

No, `N` would need to be known at compile time via the type I believe. (But I have only used `@generated` a very few times, so am not an expert myself, and Iβve never used `@nexprs`.)

One last option is to use recursion to keep splitting off one element of the tuple. As long as `N` is not gigantic the compiler can make it type stable:

`````` struct Point{T,FTup <: Tuple}
x::T
y::T
F::FTup  # I'll make it a tuple of tuples to make the recursion easier
end

pt = Point(x, y, ((F1,pβ), (F2,pβ), (F3,pβ)))

function TestFnc(x,y,F,args...)
F[1](x,y,F[2]) + TestFnc(x,y,args...)
end

function TestFnc(x,y,F)
F[1](x,y,F[2])
end

function TestFnc(pt::Point)
TestFnc(pt.x,pt.y,pt.F...)
end
``````

I think this should also be type stable.

2 Likes

Oh yeah, there is also GitHub - tisztamo/FunctionWranglers.jl: Fast, inlined execution of arrays of functions which I havenβt used but would be another option.

1 Like

Another option is to use `ntuple` instead of a loop with `zip`:

``````function TestFnc(pt::Point{T,N,FTup,PTup}) where {T,N,FTup,PTup}
res = sum(ntuple(i -> pt.F[i](pt.x, pt.y, pt.F_params[i]), length(pt.F)))
return res
end
``````

(`ntuple` should expand into recursion for small tuple, so that is unroll-friendly as well)

1 Like