parameterized types using `@aagent`
slwu89 opened this issue · comments
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.
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}
Cool! I think this issue can be closed.