Idiom to preinitialize function parameters

Coming from a language with curried function arguments, when I try to precompute some parameters for a function that is to be used in a hot loop, I end up with code like this:

precompute = length
function setup(input)
  param = precompute(input)
  compute(x) = (x+param; nothing)
  return compute
end
mykernel = setup("complicated input that takes time to initialize")
for i in 1:1000
  mykernel(i)
end

So parameters are often stored in closures. The code reads a bit cumbersome and error messages are riddled with a lot of cryptic function types. Is there a better way to do this in terms of readability and/or perfomance? I’ve read the advice to pass parameters around, possibly aided by macros in Parameters.jl ? How would you do it?

So I guess with explicit parameter passing it would look more like

function compute(param, x)
  x+param
  nothing
end
function setup_param(input)
  return length(input)
end
mykernel2(x) = compute(param, x)

with flat, rather than nested, functions definitions. When parameters become too many, I could pack them into named tuples, and use @unpack from UnPack.jl to take out just the ones I need inside the hot loop. Is that better?

I’m not sure this will solve your problem but I have two answers in mind.

  1. One can define function arguments using previous function arguments.
function f(x, y=exp(x), z=x*y)
    return x + y + z
end
  1. It is a frequent pattern in Julia to define two versions of a function: one in-place and one out-of-place. Typically this may look like:
function mul!(result, A, b)
    # multiply A by b and store in result
end

function *(A, b)
    result = # preallocate
    mul!(result, A, b)
    return result
end

Does this help?

  1. seems like a clever way to do initialization but I don’t quite see how to apply it here. If i do
input = 4
for i in 1:1000
  f(input; z=i)
end

then y will get computed automatically, but in every iteration, no?

  1. is something that I have become aware of; typically the ‘kernel’ is the inplace function, for speed. But the non-varying input of the kernel still needs setting up first, which is what I was concerned about.

I don’t think Parameters.jl is really needed here. You can just use NamedTuples and destructuring syntax.

For example:

function setup(input)
    (; p1=exp(1-input), p2=rand())
end
function f(x, params)
    (;p1, p2) = params
    x * p1 - p2
end

let params = setup(10.0)
    for i in 1:1000
        f(x, params)
    end
end

or of course you can just use keyword arguments like so:

function f(x; p1, p2)
    x * p1 - p2
end

let params = setup(10.0)
    for i in 1:1000
        f(x; params...)
    end
end

Why not create a struct that contains the precomputed parameters and use it as a functor?

# struct to hold precomputed parameters
struct MyKernel{T}
    param::T
end

# setup method to create the kernel
function setup(input, precompute)
    # precompute stuff...
    param = precompute(input)
    return MyKernel{typeof(param)}(param)
end

# kernel computation
function (kernel::MyKernel{T})(x::S) where {T, S}
    return (x + kernel.param, nothing)
end

Then your code would look like this:

mykernel = MyKernel("complicated input...")

for i in 1:1000
    mykernel(i)
end

I’m not sure if this is more/less efficient than a closure, but it is type stable and (for me) easier to reason about where the parameters are coming from.

Yep, there’s nothing wrong with this approach. It’s basically the closure one but with better stacktraces and a bit more boiler-plate.