Defining functions with multiple methods inside a function

I am writing a data type that is, essentially, a wrapper for a function, which will be accessed as a functor, but the arguments for the outside struct constructor determine the nature of the function.

(As an irrelevant aside, the goal is to be able to populate an array with “data generating process” data structures describing stochastic processes. The array of types will support estimation of parameters (via indirect inference / simulated method of moments and other methods); it will support calibration of subsets of parameters; and it will support counterfactual simulations, where exogenous variables are set to choice values. Frequently, when people estimate this sort of model, they put parameters and variables in arrays with no way to keep track of which position represent which variable/parameter except through mental effort. Requiring mental effort to keep track of this sort of thing invites serious and undetectable semantic errors).

The struct looks like:

struct DataGeneratingProcess <: AbstractDataGeneratingProcess
    LHS_varname::Union{Symbol, Vector{Symbol}}
    RHS_varnames::Vector{Symbol}
    parameter_names::Vector{Symbol}
    func::Function
    func_print::String
end

The outer constructor below is a work-in progress and I can’t get it to do what I want it to do.
The function _f( . ) need to have a method that accept a pair of NamedTuple’s, where the type signature of the NamedTuple’s depend on arguments of the constructor. Then I want to add other methods to this function. Ignoring the wisdom of this general approach, how do I execute on this?

I have tried many different ways to execute on this, but I either can’t add methods to the function or I can’t metaprogram the type signature of the function.

function DataGeneratingProcess(lhs::Union{Symbol, Vector{Symbol}}, rhs::Vector{Symbol}, parameter_names::Vector{Symbol}, eqn::String)
  

    #rhs = sort(rhs; rev=true)
    #parameter_names = sort(parameter_names; rev=true)

    RhsType = _vec_to_nt(rhs)
    ParmType = _vec_to_nt(parameter_names)
    parsed_eqn = split(eqn, "=")[2] #TODO: better parsing than relying on spaces.

    for v in rhs
        parsed_eqn = replace(parsed_eqn, " " * string(v) * " " => " inner_rhs[:$v] ")
    end

    for v in parameter_names
        parsed_eqn = replace(parsed_eqn, " " * string(v) * " " => " inner_params[:$v] ")
    end

    #_f(inner_rhs::Union{Tuple, AbstractArray}, inner_params::Union{Tuple, AbstractArray}) = _f(zip(rhs, inner_rhs), zip(parameter_names, inner_params))        

    # let RhsType=RhsType, ParmType=ParmType, parsed_eqn=parsed_eqn, parameter_names, rhs
    #     func_string = """
    #         function _f(inner_rhs::$RhsType, inner_params::$ParmType)        
    #             return $parsed_eqn
    #         end
    #     """
    #     print(func_string)

    #     func_string |> Meta.parse |> Meta.eval
    # 
    # end
    func_string = """
            global function _f(inner_rhs::$RhsType, inner_params::$ParmType)        
                return $parsed_eqn
            end
        """

    #print(func_string)
    func_string |> Meta.parse |> Meta.eval

    function _f(rhs_var::AbstractVector{N} where N <: Number, param_vals::AbstractVector{N} where N <: Number)
        return rhs_var
    end

    function _f()
        return nothing
    end

   

    return DataGeneratingProcess(lhs, rhs, parameter_names, _f, eqn)

end

The below should run but does not. I can either get a single method function generated through metaprogramming that can evaluate on the named tuple or I can get a function with multiple methods that can evaluate on the pair of arrays but not one that can run on the NamedTuple generated type-signature.

dgp1 = DataGeneratingProcess(:y, [:x, :w], [:β, :γ],"y = β * x + γ * w ")

dgp1.func((x=1,w=1), (β=1, γ=3))

dgp1.func([1,2], [1, 2])

I didn’t read through your whole code, but two things to keep in mind

  1. Don’t use Meta.parse and don’t use eval. If you are using those, something is wrong.
  2. Julia is not object oriented. Rather, store your information in structs and define methods which take in those structs, but the structs themselves should generally not be callable.

Remember that functions are first order in Julia and can be passed around very easily. You probably want to be passing a function around rather than a string which contains an operation.

2 Likes
  1. Don’t use Meta.parse and don’t use eval . If you are using those, something is wrong.

That’s usually good advice but I am pretty sure I need metaprogramming for what I want to do.

  1. Julia is not object oriented
    I am aware of that and I generally separate data from functions. But this use case does not want generic functions (see below).

Long run goal is use syntax similar to @formual(y ~ x + w).

Something like:

@parameters β, γ, δ, α, σ
@variables y, x, w, ϵ, 
structural_model = [@dgp(y = x*β + w*γ + σ*ϵ), 
                    @dgp(x = z*δ + α*ϵ), 
                    @shock(ϵ ~ Normal(0,1))]
parameter_values = ParameterValues((β=1.0, γ=1.3, δ=.5, α=2.4, σ=.2))

N=100
df = DataFrame(w=rand(N, Normal(0,1)), z=rand(N, Normal(0,1)))

simulated_df = sim(structural_model, parameter_values, df)

where simulated_df was simulated according to the @dgp expressions plugging in data from the dataframe and parameter values.

A syntax that would allow this would be extremely helpful – it would allow shorter, more readable, easier to debug code.

What you want might need metaprogramming, but eval and Meta.parse are not metaprogramming. Metaprogramming operates on already-parsed Julia expressions, not strings.

Take a look at StatsModels.jl and @formula. It doesn’t use eval or Meta.parse.

2 Likes

Thanks for the tip!