This is why I avoided count=0
in favor of count=Ref(0)
—so that instead of reassigning the identifier, we merely setindex!
a new value into its x
field to avoid any performance penalty.
The same applies for globals: working with a constant Ref
is faster than reassigning a type-annotated variable. To illustrate (Julia 1.9.0-alpha1):
julia> c1::Int = 0
0
julia> const c2 = Ref(0)
Base.RefValue{Int64}(0)
julia> function f1() global c1 += 1 end
f1 (generic function with 1 method)
julia> function f2() global c2[] += 1 end
f2 (generic function with 1 method)
julia> @btime f1()
11.300 ns (1 allocation: 16 bytes)
500503
julia> @btime f2()
4.800 ns (0 allocations: 0 bytes)
500503
What’s a mild surprise for me, is that part of me thinks the compiler should be able to tell through static analysis that the captured value never changes type. Instead, it sees the function make an assignment and it gives up.
The other mild surprise, is that when I think of a capture, I think of a functor whose struct contains its captured values (which can be accessed and manipulated externally, although that’s not official API). For example:
julia> const foo = let i = Ref(0); f() = (i[] += 1; i[]) end
(::var"#f#1"{Base.RefValue{Int64}}) (generic function with 1 method)
julia> ((foo() for _=1:5)...,)
(1, 2, 3, 4, 5)
julia> foo.i[] = 10;
julia> ((foo() for _=1:5)...,)
(11, 12, 13, 14, 15)
By contrast, these global functions are singleton.
julia> let i = Ref(0); global bar() = (i[] += 1; i[]) end
bar (generic function with 1 method)
julia> ((bar() for _=1:5)...,)
(1, 2, 3, 4, 5)
julia> bar.i[] = 10
ERROR: type #bar has no field i
Are these the only state-storing objects in the language whose state is truly private?