Brace yourselves - I am about to commit code to my package that calls eval
to redefine a @generated
function so that it can observe global state (that will normally not change after the redefinition).
I actually think this is the best way to proceed, but all of these techniques are usually signs of bad design, so I want to see if anyone has any other suggestions (see question at bottom).
Background
The context is POMDPs.jl. This is a complex package since it has two user classes: problem writers, who define methods for functions in the interface, and solver writers who use functions from the interface. In addition, there are really two interfaces: the explicit interface that deals with probability distributions, and the generative interface that deals with only simulated samples. See Concepts and Architecture · POMDPs.jl for more info.
I am trying to allow other packages that extend the interface to “hook into” the automatically synthesized functions of the generative interface. My example is constrained MDPs/POMDPs, in which, in addition to the primary reward, there is another cost (denoted with c) that must be constrained.
The gen
function can sample a tuple of various random variables from the model, for example, a reward, :r
, observation, :o
, or next step, :sp
. This issue has more information: https://github.com/JuliaPOMDP/POMDPs.jl/issues/240, and docs that explain the new functionality can be found here: https://github.com/JuliaPOMDP/POMDPs.jl/blob/genvarmin/docs/src/generative.md
Cartoon Version
Here is a cartoon version that has the main features of what I am implementing:
module POMDPs
export gen
# in reality genvar_registry contains a lot more than just Functions
genvar_registry = Dict{Symbol, Function}()
gendef = quote
@generated function gen(::Val{symboltuple}, m, s, a) where symboltuple
# in reality, genvar_registry and symboltuple are used to create something more complicated.
func = genvar_registry[first(symboltuple)](m)
return quote
return ($(func)(m, s, a),)
end
end
end
function add_genvar(name, func)
genvar_registry[name] = func
eval(gendef)
end
end
module ConstrainedPOMDPs
import Main.POMDPs
function cost end
POMDPs.add_genvar(:c, M->cost)
end
using Main.POMDPs
using Main.ConstrainedPOMDPs
struct MyPOMDP end
ConstrainedPOMDPs.cost(::MyPOMDP, s, a) = abs(s) + abs(a)
@show gen(Val((:c,)), MyPOMDP(), 1, 2)
Real Version
The real version of gen
is implemented here: https://github.com/JuliaPOMDP/POMDPs.jl/blob/genvarmin/src/gen_impl.jl (currently it does not have the eval
part).
Question
Is there a better design to accomplish this that avoids the eval
?