Merck / AlgebraicAgents.jl

A lightweight framework to enable hierarchical, heterogeneous dynamical systems co-integration. Batteries included!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

parameterized types using `@aagent`

slwu89 opened this issue · comments

commented

I was testing if the macros could allow us to use parameterized types for the agents when I noticed that even if there is an error when interpolating the symbols into expressions in define_agent, the name of the new agent type will still be assigned into the current module, it will just have 0 constructor methods.

using AlgebraicAgents

@aagent MyAgent{T<:AbstractString} begin
    myname::T
end

typeof(MyAgent)
methods(MyAgent)
dump(MyAgent)

It might be nice to allow users to define agents as parameterized types for when we still want each agent itself to have a fully concrete type to avoid type instability but also allow flexibility in the particular types, either for dispatch or to integrate with other packages (or for when large numbers of agents are being created programmatically). I will spend some time to think about how to do this, I'm still new to Julia metaprogramming.

commented

I tried to prototype a way to include that type information in the macro and was "partially successful". But there is a very odd issue about how the code emitted by the macro was evaluated that prevented it from being successful. I don't yet understand enough about how evaluation in Julia works to figure this out. Here's an MWE of the issue I encountered during prototyping:

using MacroTools

# crude version of aagent for prototype
macro make_type(name, fields)
    param_tnames = map(name.args[2:end]) do x
        if x isa Symbol
            return x
        else
            return x.args[1]
        end
    end # like T,L
    param_tnames_constraints = :({$(name.args[2:end]...)}) # like T<:Number,L<:Real
    tname = :($(name.args[1]){$(param_tnames...)})

    define_type(name, fields, __module__, quote
        function $(tname)() where $(param_tnames_constraints)
            m = new()
            m
        end
    end)
end

# crude version of define_agent for prototype
function define_type(name, fields, __module, constructor)
    fields = MacroTools.striplines(fields)
    quote
        let
            name = $(QuoteNode(name))
            additional_fields = $(QuoteNode(fields.args))
            additional_fieldnames = [Meta.isexpr(f, :(::)) ? f.args[1] : f for f in $(QuoteNode(fields.args))]

            expr = quote
                mutable struct $name
                    $(additional_fields...)
                    $$(QuoteNode(constructor))
                end
            end
            println("returned expr: ", expr)
            Core.eval($(__module), expr)
        end
    end
end

Calling the above results in an error:

julia> @make_type MyAgent{T<:AbstractString} begin
           myname::T
       end
returned expr: begin
    #= /Users/wusea/Desktop/misc/agentypes.jl:89 =#
    mutable struct MyAgent{T <: AbstractString}
        #= /Users/wusea/Desktop/misc/agentypes.jl:90 =#
        myname::T
        #= /Users/wusea/Desktop/misc/agentypes.jl:91 =#
        begin
            #= /Users/wusea/Desktop/misc/agentypes.jl:73 =#
            function MyAgent{T}() where {T <: AbstractString}
                #= /Users/wusea/Desktop/misc/agentypes.jl:73 =#
                #= /Users/wusea/Desktop/misc/agentypes.jl:74 =#
                m = new()
                #= /Users/wusea/Desktop/misc/agentypes.jl:75 =#
                m
            end
        end
    end
end
ERROR: syntax: invalid variable expression in "where" around /Users/wusea/Desktop/misc/agentypes.jl:89
Stacktrace:
 [1] top-level scope
   @ none:1

Now, the very weird part is that if you just copy and paste the returned code into the REPL it works just fine:

julia> mutable struct MyAgent{T <: AbstractString}
           #= /Users/wusea/Desktop/misc/agentypes.jl:90 =#
           myname::T
           #= /Users/wusea/Desktop/misc/agentypes.jl:91 =#
           begin
               #= /Users/wusea/Desktop/misc/agentypes.jl:73 =#
               function MyAgent{T}() where {T <: AbstractString}
                   #= /Users/wusea/Desktop/misc/agentypes.jl:73 =#
                   #= /Users/wusea/Desktop/misc/agentypes.jl:74 =#
                   m = new()
                   #= /Users/wusea/Desktop/misc/agentypes.jl:75 =#
                   m
               end
           end
       end

julia> alice = MyAgent{String}()
MyAgent{String}(#undef)

Got it! It seems to me that Julia applies some extra expression "normalisation" when you run the code from REPL; when you construct an expression manually and eval it, you need to be more careful.

In this case, the braces around parameter constraints are redundant. We need the following:

# in make_type
param_tnames_constraints = name.args[2:end] # like T<:Number,L<:Real`
# in define_type
function $(tname)() where $(param_tnames_constraints...)

I refined the support for parametric types in @aagent.

Consider

@aagent struct MyAgent{T <: Real, P <: Real}
    myname1::T
    myname2::P
end

a = MyAgent{Float64, Int}("myagent", 1, 2)
@test a isa MyAgent{Float64, Int}
commented

Cool! I think this issue can be closed.