Hi All
I am teaching myself some design patterns currently I am trying to build a simple Rule Engine. I ran upon an issue where I would like to call many functions ( rule set) while passing them the same parameters. In C# I could just define an IRule interface, then List methods subscribing to the interface. Since in Julia objects and their behavior very decoupled I am not sure how to do this. I thought about storing the function in an array then looping over them, but reading this post Type Inference From a List of Functions means I need to store function references in a tuple( immutable) to loop over so I don’t loose the type info ( I don’t want dynamic dispatching on the rule set).
What would be the Julia way to have list of functions that can be called depending on a whole set of behaviors to apply to an object. I would also like the modify the list of Rules depending on what is acting on the object. See a contrived example below. I have no structs here. You could imagine this in the context of game, lets say where you want modify a players health based on various interactions with other player, world, enemys etc…
function foo(x::Int, y::Int)
return x + y
end
function foo1(x::Int, y::Int)
return x * y
end
function calc_health(x::Int, y::Int, health::Int)
func_arr = [foo, foo1]
for i in eachindex(func_arr)
health += func_arr[i](2,3)
end
return health
end
function calc_health2(x::Int, y::Int, health::Int)
func_arr = (foo, foo1)
for i in eachindex(func_arr)
health += func_arr[i](2,3)
end
return health
end
@time begin
calc_health(2,3,0)
end
@time begin
calc_health2(2,3,0)
end
@time begin
foo(2,3) + foo1(2,3)
end
This seems like functionality that can be handled better by automata or behavior trees, rule engines usually just check for the validity of the rule set (i.e. all or some of the rules have to be matched). Perhaps MLStyle may help, it supports advanced pattern matching functionality and dynamic behavior (apply function based on the type of match) can be implemented quite easily.
Thanks for the reply things were simple enough I implemented a barrier function pattern to construct the Tuple of rules that I can call from a Dict that store references to each function type. This barrier function with then passes this Tuple to correct function to loop over. Works well is quite optimized.
You’re essentially describing how DynamicGrids.jl works, given that you want to write a list of rules that are applied to some object in sequence. DynamicGrids.jl applies each rule to every cell in an array, one rule after the other. But the principle is general.
From my experience writing it, you don’t want rules to be an array of functions, but a tuple of objects that hold rule parameters and define the same common method that runs them. You then apply each rule in the tuple to the main object, using recursion instead of a loop. This will be type stable with different objects, a loop wont be.
Defining a new rule is then defining a struct and a method that dispatches on it. This is the most reusable, Julian way to do this kind of thing. Using an object, if your rule has parameters, they can be modified. If you use a function, they can’t - and your rules are problem-specific rather than generic.
@Raf Thanks I will take a look at DynamicGrids.jl. Would you mind posting a toy example? That said yes the rules were very specific and short by design. My application is pretty small so I think mostly it doesn’t matter for my use case. However, I may go rewrite in more general as you describe above. Any small contrived example would be welcome. In the mean time I will check out your pkg.
If you use functions, still make it a Tuple of functions, and use recursion. Unless you don’t care about performance.
Here’s a toy example.
function applyfuncs(obj, funcs::Tuple) =
newobj = funcs[1](obj)
applyfuncs(newobj, Base.tail(funcs))
end
applyfuncs(obj, funcs::Tuple{}) = obj
obj = ... # the object you want to update with rules
updated_obj = applyfuncs(obj, (func1, func2))
I would also make this functional, in that each function returns a new object, instead of writing to the existing object. Then everything can be immutable and fast, and you don’t have to think about state.
And with objects:
function applyrules(obj, rules::Tuple) =
newobj = runrule(obj, rule[1])
applyrules(newobj, Base.tail(rules))
end
applyrules(obj, rules::Tuple{}) = obj
struct Rule1 end
runrule(obj, rule::Rule1) = 2 * obj # Some fixed rule
struct Rule2{P}
param::P
end
runrule(obj, rule::Rule2) = obj * rule.param # A rule with a parameter
obj = ... # the object you want to update with rules
updated_obj = applyrules(obj, (Rule1(), Rule2(0.5)))
@Raf Very nice! Thanks! your example is nice and clear. I will rewrite in this manor as you describe. I will post back and let you know how it goes. I am still learning quite a bit on designing things. Cheers have a nice weekend.