# Dealing with type stability when nothing can be returned

**URL:** https://discourse.julialang.org/t/dealing-with-type-stability-when-nothing-can-be-returned/131040
**Category:** Modelling & Simulations
**Tags:** question, performance, agentsjl
**Created:** [July 25, 2025, 4:02am UTC](https://discourse.julialang.org/t/dealing-with-type-stability-when-nothing-can-be-returned/131040 "2025-07-25T04:02:22Z")
**Posts on this page:** 2
**Page:** 1

<div class="post-metadata">

### Author: ![sea\_slug](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sea_slug/32/52907_2.png) [@sea\_slug](https://discourse.julialang.org/u/sea_slug)
#### Post date: [July 25, 2025, 4:02am UTC](https://discourse.julialang.org/t/dealing-with-type-stability-when-nothing-can-be-returned/131040/1 "2025-07-25T04:02:22Z")

</div>

I have a relatively complex Agent Based Model (ABM) that I am profiling. One function in particular (`get_shark_pos`) is dominating the runtime according to the flame graph. I am surprised because the function is very simple and not dealing with large data structures which are processed in many other parts of the model.

When I run `@code_warntype` on `get_shark_pos` I get the following yellow warnings:  
`::Union{Nothing, Vector{Tuple{Int64, Int64, Int64}}}`  
`::Union{Nothing, Tuple{Fishes, Agents.IdIteratorState}}`

I think this is because the function `nearby_agents` (used by `get_shark_pos`) can return different things depending on whether there are other agents of species `:shark` in visible range or not.

I thought creating an empty container (`shark_pos = Tuple{Int, Int, Int}[]`) to push to would help resolve this issue but it has not.

My question:

- What is a better way to set up my `get_shark_pos` function to deal with the type stability warnings?

Below is a MWE.

```julia-auto
using Agents

space = GridSpace((100, 100, 100))

@agent struct Fishes(GridAgent{3}) 
    species::Symbol
end

# agent stepping function
function fish_step!(fish::Fishes, model::StandardABM)
  shark_pos = get_shark_pos(fish, model)
  # do other stuff with shark pos
end

# define model
fish_model = StandardABM(
    Fishes,
    space;
    agent_step! = fish_step!
)

# add sharks
for n in 1:1e4
    add_agent!(fish_model, :shark)
end

# add dolphins
for n in 1:1e4
    add_agent!(fish_model, :dolphin)
end

# get the position of all :shark within range = 1
function get_shark_pos(fish::Fishes, model::StandardABM)
    near_all = nearby_agents(fish, model, 1)
    shark_pos = Tuple{Int, Int, Int}[]
    for x in near_all
        @inbounds if x.species === :shark
            push!(shark_pos, x.pos)
        end
    end
    return shark_pos
end 

# check type stability
@code_warntype get_shark_pos(fish_model[1], fish_model)

```

---

<div class="post-metadata">

### Author: ![sgaure](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sgaure/32/14779_2.png) [@sgaure](https://discourse.julialang.org/u/sgaure)
#### Post date: [July 25, 2025, 9:58am UTC](https://discourse.julialang.org/t/dealing-with-type-stability-when-nothing-can-be-returned/131040/2 "2025-07-25T09:58:53Z")

</div>

I don’t think these unions have a bad performance impact. These short unions are handled well by the compiler (3 or fewer is handled by very efficient code). Indeed, such unions with `Nothing` are created by every `for` loop.

What you can do is to preallocate the `shark_pos` array:

```julia-auto
    shark_pos = Tuple{Int, Int, Int}[]
    sizehint!(shark_pos, length(near_all))
    for x in near_all
    ... # as before

```

This may allocate more memory than needed, but will avoid reallocation inside the loop. Whether it’s better depends on the number of entries which are sharks.

Alternatively you can rewrite `get_shark_pos` as:

```julia-auto
function get_shark_pos(fish::Fishes, model::StandardABM)
    near_all = nearby_agents(fish, model, 1)
    return [x.pos for x in near_all if x.species === :shark]
end

```

I’m not sure if it will be faster.

Another possibility is to reuse a preallocated array, if that is doable in your program. I.e. keep a `Tuple{Int,Int,Int}[]` hanging around, and use a `get_shark_pos!` to fill it:

```julia-auto
function get_shark_pos!(shark_pos, fish::Fishes, model::StandardABM)
    near_all = nearby_agents(fish, model, 1)
    empty!(shark_pos)
    for x in near_all
        (x.species === :shark) && push!(shark_pos, x.pos)
    end
    return shark_pos
end

```

If you always call the `get_shark_pos!` with the same `shark_pos` vector, there will be allocations only when the vector has to increase to something larger than it has been before (because `empty!` only sets the current size, not the allocated size). Of course, if you then need to save some positions the `shark_pos` must be copied, because it will be overwritten at every call.
