# \[ANN\] MapUnroll.jl: Type stable, efficient map-like iterations

**URL:** <https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762>\
**Category:** Package Announcements\
**Created:** [July 16, 2025, 3:39am UTC](https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762 "2025-07-16T03:39:10Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![Alec\_Loudenback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/alec_loudenback/32/278_2.png) [@Alec\_Loudenback](https://discourse.julialang.org/u/Alec_Loudenback)\
**Post date:** [July 16, 2025, 3:39am UTC](https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762/1 "2025-07-16T03:39:10Z")

</div>

[MapUnroll.jl](https://github.com/alecloudenback/MapUnroll.jl) will soon (end of this week) [be registered in General](https://github.com/JuliaRegistries/General/pull/134856) if all goes well.

_Copying the whole readme since it’s not very long:_

## Quickstart

To avoid issues and ensure performance with `map`s that use intermediate variables. Users should turn this pattern:

```julia
function simulate(n)
    x = 0.0

    map(1:n) do i
        x += exp(i)
        (timestep=i,state=x)
    end

end

```

Into this:

```julia
using MapUnroll

function simulate_unroll(n)
    out = UndefVector{Union{}}(n)
    x = 0.0

    @unroll 2 for i ∈ 1:n
        x += exp(i)
        out = setindex!!(out, (timestep=i,state=x), i)
    end
    out
end

```

## Explanation

This package addresses situations where you would like to map over a collection and return a concretely typed array that depends on some intermediate variables. For example:

```julia
function simulate(n)
    x = 0.

    map(1:n) do t
        x += exp(i)
        (timestep=t,state=x)
    end

end

```

In the above code, `x` is effectively a global variable with respect to the closure created to the inner `map`. This means that `x` get’s “boxed” and is wrapped in a mutable container for use within the map’s loop.

Another potential problem is that `map` does not guarantee execution in sequential order, meaning that our simulation could end up being calculated out-of-order.

An alternative is to write a `for` loop. However, the user then needs to take care to create the appropriate output container. For simple types this may work, but for complex types we would prefer that the compiler infer what the output `eltype` of our output vector should be.

`@unroll` addresses this by ‘unrolling’ the loop, or making the first couple (default `N=2`) iterations occur before the `for` loop actually begins, thus letting the compiler calculate the type of the object that will be placed into the output vector.

Then, from MicroCollections.jl (`UndefVector`) and BangBang.jl (`setindex!!`), the output container can be efficiently expanded by the compiler to return type stable and performant code. `UndefVector` and `setindex!!` are re-exported from MapUnroll.jl for convenience.

Comparing the two versions of the simulation above:

The original `simulate` does not avoid boxing the intermediate variable:

```julia-repl
julia> @code_warntype simulate(100)
...
Locals
  #15::var"#15#16"
  x::Core.Box
Body::Vector
1 ─ (x = Core.Box())
│ %2 = x::Core.Box
│ Core.setfield!(%2, :contents, 0.0)
│ %4 = Main.map::Core.Const(map)
│ %5 = Main.:(var"#15#16")::Core.Const(var"#15#16")
│ %6 = x::Core.Box
│ (#15 = %new(%5, %6))
│ %8 = #15::var"#15#16"
│ %9 = (1:n)::Core.PartialStruct(UnitRange{Int64}, Any[Core.Const(1), Int64])
│ %10 = (%4)(%8, %9)::Vector
└── return %10

```

However `simulate_unroll` avoids this and has faster performance as a result:

```julia-repl
julia> using BenchmarkTools
julia> @btime simulate(100)
  9.167 μs (407 allocations: 11.12 KiB)
julia> @btime simulate_unroll(100)
  233.583 ns (2 allocations: 1.62 KiB)

```

## Credit

The original `@unroll` macro was developed by [Mason Protter](https://github.com/MasonProtter) on the [Julia Zulip](https://julialang.zulipchat.com).

---

<div class="post-metadata">

**Author:** ![Sevi](https://avatars.discourse-cdn.com/v4/letter/s/c67d28/32.png) [@Sevi](https://discourse.julialang.org/u/Sevi)\
**Post date:** [July 16, 2025, 11:26am UTC](https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762/2 "2025-07-16T11:26:46Z")

</div>

Sounds pretty useful!

Could you briefly clarify how this differs from what `@unroll` fom Unrolled.jl does?

My understanding is that `MapUnroll.jl` is not specifically restricted to create statically unrolled loops, but essentially provides a convenience for computing arbitrary element types for the final output container. The size etc. of the result does not need to be known at compile time and also dynamic dispatch might still be an issue if e.g. looping over abstractly typed collection. Is that correct?

---

<div class="post-metadata">

**Author:** ![aplavin](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/aplavin/32/222056_2.png) [@aplavin](https://discourse.julialang.org/u/aplavin)\
**Post date:** [July 16, 2025, 1:25pm UTC](https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762/3 "2025-07-16T13:25:41Z")

</div>

> [@Alec\_Loudenback](#):
>
> ```julia
> function simulate(n)
> x = 0.0
> map(1:n) do i
> x += exp(i)
> (timestep=i,state=x)
> end
> end
> 
> ```

The recommended approach is to turn this into

```julia
function simulate(n)
    x = Ref(0.0)
    map(1:n) do i
        x[] += exp(i)
        (timestep=i,state=x[])
    end
end

```

Is MapUnroll even more performant?

---

<div class="post-metadata">

**Author:** ![Alec\_Loudenback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/alec_loudenback/32/278_2.png) [@Alec\_Loudenback](https://discourse.julialang.org/u/Alec_Loudenback)\
**Post date:** [July 16, 2025, 5:07pm UTC](https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762/4 "2025-07-16T17:07:38Z")

</div>

Yes, the point of MapUnroll is to have a convenient way to have a dynamic output container that avoids type instability or the user needing to specify what might be a fairly complex output kind.

I hadn’t seen Unrolled.jl, but it appears that package:

1. is focused on statically sized collections (e.g. tuples or static arrays) and fully unrolling them.
2. Does not do anything with dynamic arrays.
3. Requires the user to specify the type of the output container.

---

<div class="post-metadata">

**Author:** ![Alec\_Loudenback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/alec_loudenback/32/278_2.png) [@Alec\_Loudenback](https://discourse.julialang.org/u/Alec_Loudenback)\
**Post date:** [July 16, 2025, 5:10pm UTC](https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762/5 "2025-07-16T17:10:52Z")

</div>

Using `Ref` was mentioned briefly in the [associated Zulip discussion](https://julialang.zulipchat.com/#narrow/channel/274208-helpdesk-.28published.29/topic/.E2.9C.94.20map.20pattern.20and.20avoiding.20closures/with/525441431), but the point was raised that `map` does not explicitly guarantee execution order while the logic inside the `map` example above assumes sequential calculation. Therefore MapUnroll.jl has `@unroll` to force the user to use a `for` loop, which has a guaranteed order of execution.

---

<div class="post-metadata">

**Author:** ![bertschi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bertschi/32/33462_2.png) [@bertschi](https://discourse.julialang.org/u/bertschi)\
**Post date:** [July 16, 2025, 9:04pm UTC](https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762/6 "2025-07-16T21:04:42Z")

</div>

Isn’t that what fold and friends are for?

```julia
function sim(n)
    accumulate((x,i) -> (timestep = i, state = x.state + exp(i)),
               1:n;
               init = (; state = 0.0))
end

```

---

<div class="post-metadata">

**Author:** ![aplavin](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/aplavin/32/222056_2.png) [@aplavin](https://discourse.julialang.org/u/aplavin)\
**Post date:** [July 16, 2025, 10:25pm UTC](https://discourse.julialang.org/t/ann-mapunroll-jl-type-stable-efficient-map-like-iterations/130762/7 "2025-07-16T22:25:28Z")

</div>

That would be a useful piece of information to have in the package README.  
That is, the package focus isn’t in making this calculation fast (because it is already fast with `Ref` + `map`), but in forcing in-order execution for potential fancy collections where `map` is not in-order.
