Skip to content

Commit 357f597

Browse files
committed
Unroll the recursive function into iteration
This avoids a StackOverflow error, and also turns out to be faster still, and lets us maintain the ability for map to return a list of a different eltype than the original, added in #27. Before: ```julia julia> @Btime DataStructures.map(x->x*2, $(list((1:1000)...))); 29.705 μs (1745 allocations: 42.89 KiB) ``` After: ```julia julia> @Btime DataStructures.map(x->x*2, $(list((1:1000)...))); 13.139 μs (1011 allocations: 47.65 KiB) ``` I'm not sure how there are fewer allocations.. But I can see how it'd be faster, with no recursive function calls.
1 parent fa11333 commit 357f597

File tree

1 file changed

+14
-2
lines changed

1 file changed

+14
-2
lines changed

src/list.jl

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,20 @@ end
7070
Base.map(f::Base.Callable, l::Nil) = l
7171

7272
function Base.map(f::Base.Callable, l::Cons{T}) where T
73-
rest = Base.map(f, l.tail)
74-
return cons(f(l.head), rest)
73+
# Tail recursion manually unrolled into iterative loop with local stack to avoid
74+
# StackOverflow. The recursive logic is:
75+
# map(f::Base.Callable, l::Cons) = cons(f(head(l)), map(f, tail(l)))
76+
# Recursion to iteration instructions: https://stackoverflow.com/a/159777/751061
77+
stack = Vector{T}()
78+
while !(tail(l) isa Nil)
79+
push!(stack, head(l))
80+
l::Cons{T} = tail(l)::Cons{T}
81+
end
82+
l2 = list(f(head(l))) # Note this might have a different eltype than T
83+
for i in reverse(1:length(stack))
84+
l2 = cons(f(stack[i]), l2)
85+
end
86+
return l2
7587
end
7688

7789
function Base.filter(f::Function, l::LinkedList{T}) where T

0 commit comments

Comments
 (0)