How to enforce function signature type on a struct

Hi julia community!

I am working on a program synthesis library for julia. We represent the algorithms as iterators that enumerate the search space of possible programs in a certain order. Algorithms might need certain configuration to run. For instance, genetic algorithms need a cross_over function and a mutate! function.

What would be a good way to enforce the signature of the cross_over function for instance?
The function should have a certain signature and certain return type. If a user provides the wrong function an error should be thrown before the algorithm even starts.

If it is not possible to provide the exact signature type for the function, what would be a better way to design the code? I remember reading a post where someone said that storing functions in a struct is not the best idea.

Base.@kwdef struct GeneticSearchIterator <: ExpressionIterator
    fitness::Function
    cross_over::Function # cross over two parents
    mutation!::Function # mutates a node
    mutation_probability::Float64
    evaluation_function::Function
end
1 Like

The answer is that you can’t, because a “function” in Julia is just a name given to a collection of “methods”, which are the actual implementations for each combination of input types.
The type of a function f is just typeof(f), and it doesn’t give you any clues on what the function eats or spits out, precisely because 2 methods of f might behave differently.
Just think of all the implementations available for addition, which you can list by running methods(+).

As a result, it is impossible to do what you want statically, i.e. based only on the type of the cross_over function.
You can however do it dynamically when you call cross_over, simply by checking that the outputs are what you expect, and throwing an error if they are not.

Side note: since each function has a unique type, the way you wrote your struct means it will be very inefficient because the compiler cannot recognize the functions it contains.
In other words, you used a container with a field of abstract type.
It’s a common mistake, and here is how to fix it: encode the type of each function in the type parameters of the struct.
The documentation should probably mention this special case.

Base.@kwdef struct GeneticSearchIterator{F1,F2,F3,F4} <: ExpressionIterator
    fitness::F1
    cross_over::F2 # cross over two parents
    mutation!::F3 # mutates a node
    mutation_probability::Float64
    evaluation_function::F4
end

Note that the Fis could be forced to subtype Function, but this would be counterproductive, since there are other things than functions which behave in the same way, that is, which you can call.

6 Likes

You might find it valuable to go over Julia Functions In Excruciating Detail | by Emma Boudreau

Especially if you are interested in function/method introspection at runtime.

1 Like

Thank you for your quick message. I know understand that julia does not param types as part of the signature.

I also read a bit and understood that having abstract types in structs is bad for performance.

I don’t really know how to take your code example further.
How do I create a GeneticSearchIterator now? Do I just but Function in place for each F1,F2, F3, F4?

Just do this:

GeneticSearchIterator(
  fitness=x -> x + 1,
  cross_over=x -> x - 1,
  (mutation!)=xs -> xs[1] = xs[1] + 1,
  mutation_probability=0.05,
  evaluation_function=() -> 0.5)

The default constructor for a parametric struct works in exactly the same way, it will automatically infer the types of the arguments you give it so that you don’t need to specify them.

Ok thank you very much. I did not know that.