I am trying to define a struct that contains the keys to a set of variables vars and a function with a signature that corresponds to those variables.

struct MyType
f::Function
vars::Vector{Int}
end

A few things concern me about how to optimize this struct and constrain the function signature.
Is there a way to constrain the signature of f? I would be so happy to be able to something such as:

struct MyType{T<:Real}
f::Function{x::T...}
vars::Vector{Int}
end

I know that Function is an abstract type, and basically, I would like to ensure that at least a method of f has the correct signature.

Outside the signature matter (that can be ignored in the worst case), is there an optimal way to store a function in a struct?

struct MyType{F<:Function, A}
f::F
vars::A
function MyType{F,A}(f, vars) where {F<:Function,A}
applicable(f, vars...) || throw(ArgumentError("Function has no method with signature $(typeof.(vars))"))
return new(f, vars)
end
end
MyType(f::F, vars::A) where {F<:Function,A} = MyType{F,A}(f, vars)

EDIT: This makes all fields concrete, and implements a check that the variables vars... are a valid signature of some method of f (I assume that’s what you’re asking?)

Parametrizing the struct with the type of the function itself will give you fast calls. If you call the function several times, this may be good overall for performance.

For checking the signature the only way I know of is to check it in the constructor with methods(). That has the drawback of checking at struct creation, not at the call.

Please note that, as with any parametric data type, any function you create that take a MyType{F, A} will probably be recompiled and specialized for each call with MyType of a different F and A. You may want to check in your use case how the extra specializations triggered by MyType{F, A} compare to the lack of specializations of just MyType (i.e., if the overhead is worth it). Or go by a third path an keep using MyType{F, A} but annotate some method parameters with @nospecialize.

Functions are first class objects in Julia, so it might make more sense to use a closure, a higher-order function, or a callable struct, depending on your use case. Can you elaborate on why you need a function stored in a struct?

If you need to be able to make concrete collections of your type while having each struct containing different functions and being type stable, you may want to look into FunctionWrappers.jl

I am writing a solver with variables and constraints (predicates over the variables) given by the user.
Those constraints are scalable predicates over a vector/tuple of variables.

Ideally, I want the user to have a comprehensible error on the signature type of each predicate in case the signature is incorrect. And I would prefer it to be checked within the Constraint constructor.

However, I am opened to any suggestions. I mainly focus on having a working and user-friendly solver at the moment, and I will focus on efficiency after that.

If your predicates are not completely arbitrary but can instead be parametrized in some way, you might want to consider wrapping the parameters in a callable type instead of wrapping a function. So if your constraints are, e.g. all of the form min[i] <= r[i] <= max[i], something like

struct Constraint{N,T}
min::NTuple{N,T}
max::NTuple{N,T}
end
(c::Constraint{N})(r::NTuple{N}) where {N} = all(c.min .<= r .<= c.max)

would save you needing a new function f(min, max) = all(r -> min .<= r .<= max) of different type for different values of min and max. I don’t know if this is of any use in your case. I only mention because I recently had this situation and this approach allowed me to remove all allocations that were killing my performance. FunctionWrappers is another interesting approach that is even more general, as @WschW says.