Structures containing a function field

I would like to include a function as part of a Julia object (struct). However, when I, for example, execute

using BenchmarkTools

func(x) = 2*x^2

struct Test
    k :: Float64
    V :: Array{Float64, 1}
    fn :: Function
end

T = Test(0.5, collect(1.0:10.0), func)
@btime @. $T.V = $T.k*$T.fn($T.V)
T = Test(0.5, collect(1.0:10.0), func)
@btime @. $T.V = $T.k*$func($T.V)

I get quite a lot of allocated memory when the function from the object’s field is used in the calculations (1st calculation):

  181.873 ns (5 allocations: 112 bytes)
  9.416 ns (0 allocations: 0 bytes)

Is there any way to solve this problem? (I am using v1.7.0-beta3 because of the new Mac M1, but I It shouldn’t make any difference, I guess.)

struct Test{T}
    k :: Float64
    V :: Array{Float64, 1}
    fn :: T
end
3 Likes

Parameterize the function in the type:

julia> using BenchmarkTools

julia> func(x) = 2*x^2
func (generic function with 1 method)

julia> struct Test{F}
           k :: Float64
           V :: Array{Float64, 1}
           fn :: F
       end

julia> T = Test(0.5, collect(1.0:10.0), func)
Test{typeof(func)}(0.5, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], func)

julia> @btime @. $T.V = $T.k*$T.fn($T.V)
  12.173 ns (0 allocations: 0 bytes)

(@jling beat me)

4 Likes

just that? Wow. Thank you very much. I still have problems understanding why parameterization is so important.

because Function is an abstract type. If you need speed, you need to tell compiler to specialize according to “what function does this struct contain”.

func has type typeof(func), if your struct is parametrized by this, then T.fn is type stable (constant propagated?)

1 Like

Is it because I have to tell exactly what type of function the field is (size, number of arguments, etc.)?

not really, Julia doesn’t have typed function. each function just has its own type that’s all.

3 Likes

Just so you know, this is a bit of an anti-pattern in Julia. Julia is not object-oriented to the extent python is, and you shouldn’t store methods inside objects like this, partly due to the problems you are seeing here.

Instead of obj.fun(), define fun(obj::Object) = ...

8 Likes

The reason I want to store functions inside an object is that I am trying to program a new NN type, and I want the user to define activation functions and their derivatives when the object is created, the same way that coefficient arrays and other parameters are stored when the NN object is created. Functions that later act on the NN object are executed as function(nn :: NNObject).

And if I want to define a second object containing a function to be used in the first object, using an inner constructor, such as the following example?

using BenchmarkTools

f(x) = 2*x^2

struct Fn{F}
    fn :: F
end

struct Test
    k :: Float64
    V :: Array{Float64, 1}
    fn :: Fn
    function Test(n, k, fn)
        V = rand(n)
        fn = Fn(fn)
        return new(k, V, fn)
    end
end

T = Test(10, 0.5, f)
@btime @. $T.V = $T.k*$T.fn.fn($T.V)

I get

  460.868 ns (6 allocations: 128 bytes)
10-element Vector{Float64}:

I can see that I have to pass the function parameter in the first object, but I just don’t see how.

struct Test{F}
    k :: Float64
    V :: Array{Float64, 1}
    fn :: Fn{F}
    function Test(n, k, fn)
        V = rand(n)
        return new{typeof(fn)}(k, V, Fn(fn))
    end
end
 
  7.000 ns (0 allocations: 0 bytes)

You can either do what Jeff suggested above, or parameterize the complete type of the fn variable:

julia> struct Test{FnType}
           k :: Float64
           V :: Array{Float64, 1}
           fn :: FnType
       end
       Test(n::Int,k,fn::Function) = Test(k,rand(n),Fn(fn))
Test

julia> f(x) = 2*x^2
f (generic function with 1 method)

julia> struct Fn{F}
           fn :: F
       end

julia> Test(10,0.5,f)
Test{Fn{typeof(f)}}(0.5, [0.373040251764891, 0.3254890833516999, 0.9292136075453126, 0.15979121840237576, 0.7250485410648198, 0.6981743589031963, 0.12171709120019081, 0.5225742664754627, 0.6993845264127123, 0.5729168135399181], Fn{typeof(f)}(f))

julia> @btime @. $T.V = $T.k*$T.fn.fn($T.V)
  8.032 ns (0 allocations: 0 bytes)

The point is that a function is an abstract type, thus a struct that contains a function must be parameterized such that it is concrete for a specific type of function. In the same way, your struct Fn{F} is also an abstract type, which assumes a concrete type if F is defined. Then, for instance,

julia> Fn(sin)
Fn{typeof(sin)}(sin)

this is a concrete instance of Fn, specific for the sin function. The Test has to be instantiated to a concrete type, by informing that the function is sin, or that the complete fn is of type Fn{typeof(sin)} .

What to use depends on what is clearer for the code, and which option is more convenient for the dispatch rules that these types can participate in your code.

2 Likes

Thank you very much.