Design patterns for parameterizing by functions

I often write code that is parameterized by a small set of related ‘inner’ functions (f, g,…). The functions are related in the sense that they all do different computations but they share some parameters.
I include a simplified code example below of what I have in mind.

My first instinct for this is to simply include the inner functions as arguments in whatever code I’m writing. If there is more than 1 or 2 I might wrap them in a type or tuple to reduce clutter when passing through many functions.

I also realized that you can achieve the same results using the type system and multiple dispatch. You define some ‘parameter type’ and then write methods using that type.

Are there situations where / reasons why the type-based approach is a bad idea? Is one approach better style than the other? Perhaps the type-based approach is more opaque, but it also has the advantage that it automatically ensures that all the methods used within a function call are consistent (i.e., they are generated from the same parameters). This could be achieved with the other approach, but is not automatic and would require an extra wrapper function.

module FirstModule # type-based approach

function foo end
function bar end

abstract type AbstractParameters end
# the abstract type is not really necessary
# but could be used to provide a descriptive 
# error / fallback method if appropriate

function dosomething!(y, x, p::AbstractParameters)
    for i in eachindex(x)
        y[i] = foo(x[i], p) + bar(foo(x[i], p), p) ^ 2
    end
    return y
end

export dosomething!,
       AbstractParameters
end

module SecondModule # just functions approach

function dosomething!(y, x, f, g)
    for i in eachindex(x)
        y[i] = f(x[i]) + g(f(x[i])) ^ 2 
    end 
    return y
end

export dosomething!
end

# type-based approach
struct MyParams{T} <: FirstModule.AbstractParameters
    # these parameters could be anything in general, but most likely
    # just numbers or perhaps arrays
    θ::T
end
FirstModule.foo(x, p::MyParams) = x ^ p.θ
FirstModule.bar(x, p::MyParams) = x - p.θ

# function-based approach
function makefuncs(θ)
    foo(x) = x ^ θ
    bar(x) = x - θ
    return foo, bar
end

# Check they are equivalent
x = rand(10000)

y1 = similar(x)
FirstModule.dosomething!(y1, x, MyParams(2.0))

y2 = similar(x)
SecondModule.dosomething!(y2, x, makefuncs(2.0)...)

all(y1 .== y2)

You could also define a callable struct that is the function and contains the parameters:

julia> struct Worker{T}
           θ::T
       end

julia> function dosomething!(x,f)
           x + f(x)
       end
dosomething! (generic function with 1 method)

julia> (w::Worker)(x) = w.θ * x

julia> x = Worker(2)
Worker{Int64}(2)

julia> dosomething!(1.0,x)
3.0

I don’t see any particular advantage or disadvantage of any of the approaches, that depends more on what kind of flexibility you want for your input.

1 Like

OK, thanks! Yes, that’s a nice approach too when it fits. Just wanted to check I wasn’t doing something too odd.