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 Fi
s 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.