Hmm… I think it has to do with local scope, but I agree this is surprising.
Let me simplify the example.
julia> let
function build_matrix()
A = zeros(2,2)
A[1,2] = A[2,1] = 1
return A
end
A = build_matrix()
B = build_matrix()
A === B
end
true
What is happening is that the binding A
refers to A
within the local scope of the let
block. The second time we call build_matrix
it reassigns A
.
julia> let
function build_matrix()
A = zeros(2,2)
@info "build_matrix: " pointer(A)
A[1,2] = A[2,1] = 1
return A
end
A = build_matrix()
@info "A = build_matrix()" pointer(A)
B = build_matrix()
@info "B = build_matrix()" pointer(A) pointer(B)
A === B
end
┌ Info: build_matrix:
â”” pointer(A) = Ptr{Float64} @0x00007fd15df7a080
┌ Info: A = build_matrix()
â”” pointer(A) = Ptr{Float64} @0x00007fd15df7a080
┌ Info: build_matrix:
â”” pointer(A) = Ptr{Float64} @0x00007fd15df7b340
┌ Info: B = build_matrix()
│ pointer(A) = Ptr{Float64} @0x00007fd15df7b340
â”” pointer(B) = Ptr{Float64} @0x00007fd15df7b340
true
This applies to @testset
since the macro creates a local scope using let
.
julia> @macroexpand @testset begin
function build_matrix()
A = zeros(2, 2)
A[1, 2] = A[2, 1] = 1
return A
end
A = build_matrix()
B = build_matrix()
@test A === B
println(A === B)
end
quote
# ...
let
#= /cache/build/builder-amdci4-6/julialang/julia-release-1-dot-10/usr/share/julia/stdlib/v1.10/Test/src/Test.jl:1577 =#
begin
#= REPL[26]:2 =#
function build_matrix()
#= REPL[26]:2 =#
#= REPL[26]:3 =#
A = zeros(2, 2)
#= REPL[26]:4 =#
A[1, 2] = (A[2, 1] = 1)
#= REPL[26]:5 =#
return A
end
#= REPL[26]:8 =#
A = build_matrix()
#= REPL[26]:9 =#
B = build_matrix()
#= REPL[26]:10 =#
Conversely, if we define build_matrix
in global scope, then we will get false
.
julia> function build_matrix()
A = zeros(2, 2)
A[1, 2] = A[2, 1] = 1
return A
end
build_matrix (generic function with 1 method)
julia> @testset begin
A = build_matrix()
B = build_matrix()
@test A === B
end
test set: Test Failed at REPL[31]:4
Expression: A === B
Evaluated: [0.0 1.0; 1.0 0.0] === [0.0 1.0; 1.0 0.0]