Best practice for passing parameters to functions passed as parameters

Hello!

I want to write a general function with several subroutines. I would like this method to be able to use different functions as subroutines, by taking them as parameters, as shown in the toy example below.

function myfunction(input::AbstractMatrix,
                    preprocess::Function,
                    update!::Function)
    A = preprocess(input)

    # Some common code
    # [...]

    for i in 1:100
        # Some common code
        # [...]
        updater!(A)
    end

    # Some common code
    # [...]

    return A
end

My motivation is that a big part of the code will be common and only the preprocess and the updater will be specific.

My question is: how can I pass parameters to preprocess and updater!, if the different options for these subroutines take different parameters? For example I have one preprocesser that takes no parameter, another that takes one Integer, another that takes a matrix, etc.

I would like to know what is the best practice in this case, in terms of “standard Julia style”, performance, and readibility.

The options I see are:

  1. Duplicate the code, write specific versions of myfunction and do not pass functions as parameters
  2. Have a big structure holding all possible parameters for all differents preprocessers, pass it as a parameter in myfunction and in A = preprocess(input, param_struct) and make each preprocesser use only the ones it needs.
  3. Do some kind of homemade curryfication, where I specialize a preprocesser with its parameters before passing it to myfunction.

All these options seem unsatisfying to me, so maybe I’m asking the wrong question and I should see the problem another way. Anyway I would be very thankful for any help, comment, or advice.

Thanks for reading, and please ask if something is not clear!

Nicolas

I guess the key question is where those parameters are coming from and how you want to manage them. From your description it seems like you could just do

function myfunction(input::AbstractMatrix,
                    preprocess::Function,
                    preprocess_args::Tuple,
                    updater!::Function,
                    updater_args::Tuple
                    )
    A = preprocess(input, preprocess_args...)

    # Some common code
    # [...]

    for i in 1:100
        # Some common code
        # [...]
        updater!(A, updater_args...)
    end

    # Some common code
    # [...]

    return A
end

but I might be missing something.

Maybe you could use keyword arguments Functions · The Julia Language

The differences to optional arguments is that the order doesn’t matter but only the keywords. Notices that one uses ; instead of , to capture keyword argument lists.
E.g.

function myfunction(input::AbstractMatrix,
                    preprocess::Function,
                    updater!::Function;
                    args...
                    )
    A = preprocess(input; args...)

    # Some common code
    # [...]

    for i in 1:100
        # Some common code
        # [...]
        updater!(A; args...)
    end

    # Some common code
    # [...]

    return A
end

For the user calling your function it is enough to write

myfunction(input, preprocess, updater!, alpha = 10, beta = 4 )

Do some kind of homemade curryfication, where I specialize a preprocesser with its parameters before passing it to myfunction.

Why are you disparaging the approach of enforcing that the user-provided function have a specific arrow type (e.g. (Int, Int) -> Float64))? This seems like very normal behavior for higher-order functions.

1 Like

Thanks a lot for your answers. Both solutions from Gunnar and Steffen would work in my case. I include below a toy MWE for future reference, and also in case I’m doing something wrong you can maybe comment it. I am not very experienced in Julia.

With Gunnar’s solution:

function myfunction(input::AbstractMatrix,
                    preprocess::Function,
                    preprocess_args::Tuple)
    A = preprocess(input, preprocess_args...)
    return A
end

function nonneg(A)
    B = copy(A)
    B[B.<0] .= 0
    return B
end

function addx(A, x)
    return A .+ x
end

a = randn(2,3)
display(a)
a1 = myfunction(a, nonneg, ())
display(a1)
a2 = myfunction(a, addx, (5,))
display(a2)

With Steffen’s:

function myfunction(input::AbstractMatrix,
                    preprocess::Function;
                    args...)
    A = preprocess(input; args...)
    return A
end

function nonneg(A)
    B = copy(A)
    B[B.<0] .= 0
    return B
end

function addx(A; x)
    return A .+ x
end

a = randn(2,3)
display(a)
a1 = myfunction(a, nonneg)
display(a1)
a2 = myfunction(a, addx, x=5)
display(a2)

I think I will use the second solution as the syntax will be clearer in my case.

About jonhmyleswhite’s comment: it would probably be cleaner the way your propose it, but in my case I really need functions that take different number and types of parameters. Maybe my use case is not a good one for higher-order programming (I have no experience at all) but I thought this could help me have less duplicate code.

For the record, this is how a currying approach could look:

function myfunction(input::AbstractMatrix,
                    preprocess::Function)
    A = preprocess(input)
    return A
end

function nonneg(A)
    B = copy(A)
    B[B.<0] .= 0
    return B
end

function addx(A, x)
    return A .+ x
end

a = randn(2,3)
a1 = myfunction(a, nonneg)
a2 = myfunction(a, x -> addx(x, 5))
2 Likes