Calling a more generic method with Parametric Types

Hey all,

For some context, I want to do some work with the Agents.jl package in which I want to try to keep track of the movement of the agents (so I can draw their paths and manipulate them later). The easiest way I think I can make this work is by writing a more specific agent type, with a specific move_agent! method, and then use invoke to call the more generic move_agent! methods that Agents.jl provides. The problem that I’m running into though is that I cannot get the invoke call which I think has to do with the way that parametric types work in Julia.

What I have right now is that I wrote a method with signature move_agent!(::T, ::Agents.ValidPos, ::ABM{S, T}) where {S <: ContinuousSpace, T <: AbstractTurtle}. Now I want to call the method with signature move_agent!(::A, ::Agents.ValidPos, ::ABM{<:ContinuousSpace{D}, A}) where {D, A <: AbstractAgent}. Now we have that Turtle <: AbstractTurtle <: AbstractAgent, but the problem (I think) is that ABM{ContinuousSpace{2}, AbstractTurtle} <: ABM{ContinuousSpace{2}, AbstractAgent} does not hold. This causes invoke to give the error “argument type error”. However, if I make the type in the invoke call more general it becomes ambiguous.

Is there a solution out of this problem, am I just using invoke wrong, or is what I’m trying to do just impossible given the current type system and constraints? Copying the current definition of move_agent!(::A, ::Agents.ValidPos, ::ABM{<:ContinuousSpace{D}, A}) where {D, A <: AbstractAgent} into move_agent!(::T, ::Agents.ValidPos, ::ABM{S, T}) where {S <: ContinuousSpace, T <: AbstractTurtle} will of course work but a less brittle solution would have my preference.

The code that I have at the moment only depends on Agents.jl being installed and is:

using Agents

abstract type AbstractTurtle <: AbstractAgent end

@agent Turtle ContinuousAgent{2} AbstractTurtle begin end

function Agents.move_agent!(turtle::T, pos::Agents.ValidPos, model::ABM{S, T}) where {T <: AbstractTurtle, S <: ContinuousSpace}
    start = turtle.pos
    @invoke move_agent!(turtle::AbstractAgent, pos, model::ABM{S, AbstractAgent})
    println("Turtle started at $start, ended at $(turtle.pos)")
end

model = AgentBasedModel(Turtle, ContinuousSpace((50, 50)))
agent = add_agent!(model, (1, 0))
move_agent!(agent, (1,1), model)

Julia version: 1.9.3
Agents.jl version: 5.17.1

1 Like

I think the followung may help you.

julia> Array{1, Int64} <: Array{1, Real}
false

julia> Array{1, Int64} <: Array{1, <: Real}
true

Thanks for the response! However, if I change the invoke line to move_agent!(turtle::AbstractAgent, pos::Agents.ValidPos, model::ABM{S, <: AbstractAgent}) it says the line is too ambiguous again :confused: So that doesn’t really help.

However I did not know you could do that this way. Thats neat.

This is actually really weird to me, since it still lists move_agent!(::T, ::Agents.ValidPos, ::ABM{S, T}) where {S <: ContinuousSpace, T <: AbstractTurtle} as an option, even though it should treat the first argument as an AbstractAgent, and it lists move_agent!(::A, ::Agents.ValidPos, ::ABM{<:AbstractSpace, A}) where A <: AbstractAgent as an option even though there is a more specific alternative for that.

Does this work for you?

using Agents

abstract type AbstractTurtle <: AbstractAgent end

@agent Turtle ContinuousAgent{2} AbstractTurtle begin end

function move_agent!(turtle, pos, model)
    start = turtle.pos
    Agents.move_agent!(turtle, pos, model)
    println("Turtle started at $start, ended at $(turtle.pos)")
end

model = AgentBasedModel(Turtle, ContinuousSpace((50, 50)))
agent = add_agent!(model, (1, 0))
move_agent!(agent, (1,1), model)

last line prints

Turtle started at (16.746942825197834, 38.018146848045035), ended at (1.0, 1.0)

I can think of a simpler way that doesn’t involve chaing the behavior one of the core functions of the library. Simply add a field path::Vector{NTuple{2, Int}} to your agent type. Create a new function my_move! which does two and only two things:

push!(agent.path, agent.pos)
move_agent!(...)

Instead of calling your overloaded move_agent!, call the my_move! instead.

1 Like

This would work, but like the suggestion made my @Datseris that would mean I cannot use any of the other agent move functions anymore, which means if I want to I would have to reimplement those on top of this function. And at that point I might as well just copy the code from move_agent!(::A, ::Agents.ValidPos, ::ABM{<:ContinuousSpace{D}, A}) where {D, A <: AbstractAgent}.

Like if I’m reimplementing parts of Agents.jl anyway, might as well pick the bit that is less effort.

Ah ok now I think I’m following, for example you want to use the walk! function with your new move_agent! function and so you need some sort of overloading

ok I guess I understood what is the problem and I have a fix: simply use the dev version of Agents which should be released soon.

In that the signature is

move_agent!(agent::AbstractAgent, pos::ValidPos, model::ABM{<:ContinuousSpace})

while on 5.17 it is

move_agent!(agent::A, pos::ValidPos, model::ABM{<:ContinuousSpace{D},A}) where {D, A<:AbstractAgent}

I think that the signature in 5.17 conflicts with the one you want to use since A in that definition is a subtype of AbstractAgent, I think it is harder in that case to disambiguate it, but It’s not clear to me why (I think that A becomes the concrete version and that’s why your method can’t call it), Maybe someone more knowledgeable can give more details.

In the dev version this works:

using Agents

abstract type AbstractTurtle <: AbstractAgent end

@agent Turtle ContinuousAgent{2} AbstractTurtle begin end

function Agents.move_agent!(turtle::Turtle, pos::Agents.ValidPos, model::ABM{S}) where {S <: ContinuousSpace}
    start = turtle.pos
    invoke(Agents.move_agent!, Tuple{AbstractAgent, Agents.ValidPos, ABM}, turtle, pos, model)
    println("Turtle started at $start, ended at $(turtle.pos)")
end

model = AgentBasedModel(Turtle, ContinuousSpace((50, 50)))
agent = add_agent!(model, (1, 0))
move_agent!(agent, (1,1), model)
walk!(agent, (2.0, 2.0), model) # this uses your version

This means that we probably need to update Agents.jl using wherever possible the first type of signature :slight_smile:

But it seems weird also to me that still I can’t disambiguate those version in 5.17 since e.g.

f(x::T) where {T<:Integer} = println("First version")
g(x) = f(x)
function f(x::Int)
    println("Second Version")
    invoke(f, Tuple{Integer}, x)
end
g(1)

works returning

Second version
First Version

but instead something which seems equivalent to me in Agents.jl 5.17 doesn’t

using Agents

@agent Turtle ContinuousAgent{2} begin end

function Agents.move_agent!(turtle::Turtle, pos, model)
    start = turtle.pos
    invoke(Agents.move_agent!, Tuple{AbstractAgent, Any, Any}, turtle, pos, model)
    println("Turtle started at $start, ended at $(turtle.pos)")
end

model = AgentBasedModel(Turtle, ContinuousSpace((50, 50)))
agent = add_agent!(model, (1, 0))
move_agent!(agent, (1,1), model)

this throws

julia> move_agent!(agent, (1,1), model)
ERROR: MethodError: move_agent!(::Turtle, ::Tuple{Int64, Int64}, ::StandardABM{ContinuousSpace{2, true, Float64, typeof(Agents.no_vel_update)}, Turtle, typeof(Agents.Schedulers.fastest), Nothing, Random.TaskLocalRNG}) is ambiguous.

Candidates:
  move_agent!(agent::A, pos::Union{Int64, Tuple{Int64, Int64, Float64}, Tuple{Vararg{Int64, N}}, Tuple{Vararg{var"#s5", M}} where var"#s5"<:AbstractFloat} where {N, M}, model::AgentBasedModel{<:ContinuousSpace{D}, A}) where {D, A<:AbstractAgent}
    @ Agents ~/.julia/packages/Agents/xtlGn/src/spaces/continuous.jl:131
  move_agent!(agent::A, pos::Union{Int64, Tuple{Int64, Int64, Float64}, Tuple{Vararg{Int64, N}}, Tuple{Vararg{var"#s5", M}} where var"#s5"<:AbstractFloat} where {N, M}, model::AgentBasedModel{<:Agents.AbstractSpace, A}) where A<:AbstractAgent
    @ Agents ~/.julia/packages/Agents/xtlGn/src/core/space_interaction_API.jl:125
  move_agent!(turtle::Turtle, pos, model)
    @ Main REPL[7]:1

Possible fix, define
  move_agent!(::Turtle, ::Union{Int64, Tuple{Int64, Int64, Float64}, Tuple{Vararg{Int64, N}}, Tuple{Vararg{var"#s5", M}} where var"#s5"<:AbstractFloat} where {N, M}, ::AgentBasedModel{<:ContinuousSpace{D}, A}) where {D, A<:AbstractAgent}

which is weird

Could you sbow us the entire error please?

Ambiguity means you have two methods that are of equal specificity and Julia cannot decide which to pick.