Odd behavior with closures and let block (inside script)

Hi,

I try to hide some state in a let block, as in Does Julia Need a C++ style `static` keyword? - #4 by antoine-levitt (In particular, I don’t want it in the module scope.)

Here are 3 variants of a counter given in script-form – the first uses an Int; the 2nd and 3rd hide the Int inside a mutable struct:

#!/usr/bin/env julia

module Minex1
    let
        i::Int64 = 0

        global get_i() = i
        global inc_i() = ( i += 1 )
    end
end

module Minex2
    let
        mutable struct S  i::Int64  end
        s::S = S(0)

        global get_i() = s.i
        global inc_i() = ( s.i += 1 )
    end
end

module Minex3
    let
        mutable struct S  i::Int64  end
        s::S = S(0)

        global get_i() = ( tmp="$(s.i)";  s.i )
        global inc_i() = ( s.i += 1 )
    end
end

function main()
    print(Minex1.get_i());  Minex1.inc_i();  println(Minex1.get_i())
    print(Minex2.get_i());  Minex2.inc_i();  println(Minex2.get_i())
    print(Minex3.get_i());  Minex3.inc_i();  println(Minex3.get_i())
end
if abspath(PROGRAM_FILE) == @__FILE__
    main()  
end

Variant Minex1 (1st line in main) gives “01” as expected.
Minex2 weirdly yields “00” – this I don’t get.
Minex3 yields “01” again!

On top – in the REPL, all 3 lines of main output “01”!

Am a bit lost; any help/idea greatly appreciated!

Why not, isn’t that expected from the definition of get_i() inside Minex2?

Apart from that, since you are modifying the definition of global functions multiple times, confusing things certainly can happen, dependent on the order of evaluation of the code. Best is to avoid that.

Are you actually using such a pattern or is this an exercise?

ps: I think it is more clear if you print like this:

function main()
    @show Minex1.get_i(),  Minex1.inc_i(),  Minex1.get_i()
    @show Minex2.get_i(),  Minex2.inc_i(),  Minex2.get_i()
    @show Minex3.get_i(),  Minex3.inc_i(),  Minex3.get_i()
end

main()

resulting in:

(Minex1.get_i(), Minex1.inc_i(), Minex1.get_i()) = (0, 1, 1)
(Minex2.get_i(), Minex2.inc_i(), Minex2.get_i()) = (0, 1, 1)
(Minex3.get_i(), Minex3.inc_i(), Minex3.get_i()) = (0, 1, 1)

(seems consistent to me at first sight)

Well, REPL yields the expected 01, ie, the increase works. When executing as script, however, it does not – except when, in case 3, I use a string interpolation…

I think the functions being “global” here just puts them from the let block inside the enclosing Module, so they should not clash.

(And yes, I would like to implement the pattern suggested in the link I mentioned at the beginning.)

I don´t see any difference in the REPL behavir vs script, by the way (not with your original version or the other show function I used there).

Thanks for taking time… here is my output from bash; I tested on Windows WSL and on macOS:

DEV_julia_let_block/$ julia --version
julia version 1.9.2
DEV_julia_let_block/$ cat test.jl
#!/usr/bin/env julia

module Minex1
    let
        i::Int64 = 0

        global get_i() = i
        global inc_i() = ( i += 1 )
    end
end

module Minex2
    let
        mutable struct S  i::Int64  end
        s::S = S(0)

        global get_i() = s.i
        global inc_i() = ( s.i += 1 )
    end
end

module Minex3
    let
        mutable struct S  i::Int64  end
        s::S = S(0)

        global get_i() = ( tmp="$(s.i)";  s.i )
        global inc_i() = ( s.i += 1 )
    end
end

function main()
    print(Minex1.get_i());  Minex1.inc_i();  println(Minex1.get_i())
    print(Minex2.get_i());  Minex2.inc_i();  println(Minex2.get_i())
    print(Minex3.get_i());  Minex3.inc_i();  println(Minex3.get_i())
end
if abspath(PROGRAM_FILE) == @__FILE__
    main()
end


DEV_julia_let_block/$ julia test.jl
01
00
01

Uhm, that’s not what I get:

~/Downloads% julia test2.jl 
01
01
01

Could you please tell me your Julia version?

It is 1.10.3, and indeed, the result differs from 1.9.2:

~/Downloads% julia +release --startup-file=no test2.jl 
01
01
01
~/Downloads% julia +1.9.2 --startup-file=no test2.jl
01
00
01
1 Like

I believe there is a bug on all 1.9.x versions causing this. the behavior seems fine on all versions <1.9.0 and >= 1.10.0

1 Like

All that said, since this is “new to Julia” section, maybe it might be interesting to think about using functors, if you want to have a state-dependent function that can be updated:

julia> mutable struct A
           i::Int
       end

julia> (a::A)(x) = x * a.i 

julia> a = A(1)
A(1)

julia> a(5)
5

julia> a.i += 1
2

julia> a
A(2)

julia> a(5)
10

(you can make a struct a callable object, that carries its state and behaves as a function).

1 Like