Functors and allocations

I would like define a custom type carrying parameters for a function inside it and then I would like to update the parameters of the function. The problem is that I don’t know the number of parameters a priori (it shall be user-selectable).

In the code below, I am using a “functor” for this (with an example function using two parameters…this part would also be up for the user to change). I am not sure this is the right way :slight_smile: Any tips?

Also, is there a way to avoid the allocation in “bench1”? Am I benchmarking it correctly? “bench2” is of course just updating a vector and does not allocate; that makes sense to me.

Here is the MWE:

module atest

struct foo{T<:Real}
    functionparameters::Vector{T}
end

mutable struct modelparameters
    bar::foo
    barbar::Vector{Float64}
end

function (f::foo)(x)
    f.functionparameters[1]*(x-1)^f.functionparameters[2]
end

end

a = atest.modelparameters(atest.foo([1e-6, 1]), [1.0, 2.0])
bench1 = @benchmark $a.bar = $(atest.foo)($[2e-6, 2])
bench2 = @benchmark $a.barbar = $[2.0, 3.0]

bench1:

BenchmarkTools.Trial:
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     7.708 ns (0.00% GC)
  median time:      8.609 ns (0.00% GC)
  mean time:        11.344 ns (4.81% GC)
  maximum time:     2.183 μs (98.23% GC)
  --------------
  samples:          10000
  evals/sample:     999

bench2:

BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     2.400 ns (0.00% GC)
  median time:      2.800 ns (0.00% GC)
  mean time:        3.023 ns (0.00% GC)
  maximum time:     36.300 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

edit: made the MWE hopefully clearer.

foo is an abstract type.
To avoid the allocation, set the type of modelparameters.bar to foo{Float64} or parameterize the modelparameters type.

4 Likes

Note that you can just use a closure for this. That way the user can pass any parameters they want and you don’t need to know the number or types of these parameters, and you don’t need to require the user to use a special “functor” type.

For example:

# ≈ ∫ₐᵇf(x)dx
function myintegral(f, a, b, N=100)
    x = range(a,b, length=N+1)[1:end-1]
    return sum(f, x) * step(x)  # N-point left Riemann sum
end

# call myintegral, wrapping some parameters into a closure:
someparameter1 = 7
someparameter2 = π
approx_integral = myintegral(x -> cos(someparameter2 * x - someparameter1), 0, 1, 10^4)

# compute the error:
exact_integral = (sin(someparameter2 * 1 - someparameter1) + sin(someparameter1)) / someparameter2
@show approx_integral, exact_integral, abs(approx_integral - exact_integral) / abs(exact_integral)
3 Likes

Thanks! This has helped. For others looking this up in the future, here is the non-allocating version of this:

module atest

struct foo{T<:Real}
    functionparameters::Vector{T}
end

mutable struct modelparameters{T}
    bar::foo{T}
    barbar::Vector{Float64}
end

function (f::foo)(x)
    f.functionparameters[1]*(x-1)^f.functionparameters[2]
end

end

a = atest.modelparameters(atest.foo([1e-6, 1]), [1.0, 2.0])
bench1 = @benchmark $a.bar = $(atest.foo)($[2e-6, 2])
bench2 = @benchmark $a.barbar = $[2.0, 3.0]

Thanks for your answer. I did originally use this approach, but as my code got more complex and I needed to pass around the user-defined function throughout the program (for example, it (and several other things) ends up getting passed as a parameter to a differential equation model), I ended up storing it in a struct for easier reference. This worked fine.

However, when I started doing things like minimizing an objective function by varying the parameters inside the closure, it started to become messy, because the type of the closure that I wanted to store changed when I re-defined it after the optimizer changed the parameters (I was still able to store it into my struct, but the field wasn’t type-specific anymore; I started disliking it…).

The above way with the functor is trying to get around this problematic. I am not sure yet, whether it is what I actually want, but I’ll find out soon enough!