I'm trying to design some code in Julia which will take a list of user-supplied functions and essentially apply some algebraic operations to them.
It appears that the return value of this list of functions will not be inferred if they are closures, leading to type-unstable code according to @code_warntype.
I tried supplying a return type with the closures but did not seem to be able to find the correct syntax.
Here is an example:
functions = Function[x -> x]
function f(u)
ret = zeros(eltype(u), length(u))
for func in functions
ret .+= func(u)
end
ret
end
Run this:
u0 = [1.0, 2.0, 3.0]
@code_warntype f(u0)
and obtain
Body::Array{Float64,1}
1 ─ %1 = (Base.arraylen)(u)::Int64
│ %2 = $(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{Float64,1}, svec(Any, Int64), :(:ccall), 2, Array{Float64,1}, :(%1), :(%1)))::Array{Float64,1}
│ %3 = invoke Base.fill!(%2::Array{Float64,1}, 0.0::Float64)::Array{Float64,1}
│ %4 = Main.functions::Any
│ %5 = (Base.iterate)(%4)::Any
│ %6 = (%5 === nothing)::Bool
│ %7 = (Base.not_int)(%6)::Bool
└── goto #4 if not %7
2 ┄ %9 = φ (#1 => %5, #3 => %15)::Any
│ %10 = (Core.getfield)(%9, 1)::Any
│ %11 = (Core.getfield)(%9, 2)::Any
│ %12 = (%10)(u)::Any
│ %13 = (Base.broadcasted)(Main.:+, %3, %12)::Any
│ (Base.materialize!)(%3, %13)
│ %15 = (Base.iterate)(%4, %11)::Any
│ %16 = (%15 === nothing)::Bool
│ %17 = (Base.not_int)(%16)::Bool
└── goto #4 if not %17
3 ─ goto #2
4 ┄ return %3
So, how do I make this code type stable?
If you want type-stability for arbitrary functions, you'll have to pass them as a tuple, which allows julia to know in advance which function will be applied at which stage.
If you inspect this with
@code_warntype
you'll see it's inferrable.fsequential!
is an example of what is sometimes called "lispy tuple programming" in which you iteratively process one argument at a time until all vararg arguments have been exhausted. It's a powerful paradigm that allows much more flexible inference than afor
-loop with an array (because it allows Julia to compile separate code for each "loop iteration"). However, it's generally only useful if the number of elements in the container is fairly small, otherwise you end up with insanely long compile times.The type parameters
F
andFs
look unnecessary, but they are designed to force Julia to specialize the code for the particular functions you pass in.