Inspecting closures

question

#1

Is there a way to inspect and modify the environment of a function, like environment in R or the (admittedly ugly, but working) combination of .__code__.co_freevars, .__closure__[idx].cell_contents and ctypes.pythonapi.PyCell_Set in Python?

For example:

function f()
   x = 0
   () -> x
end

g = f()

Is there a way to modify the return value of g without redefining f and g?


#2

No.


#3

Ok, thanks.


#4

To expand a little bit, try:

g = f()
fieldnames(g)

Notice that x is a field of the (immutable) type of the closure returned by f. So it can be inspected, but not modified. Julia is not designed to permit the level of local dynamism that is possible in some other languages.


#5

You can set it if it’s boxed because the compiler can see some mutation of it, but if the compiler has decided that it’s an unmodified binding then it will be immutable. This is fairly essential to code running fast.


#6

Was immutability a semantic decision or a performance one? Would this change if such mutables could be inlined on the stack?

(Also is changing a field of an immutable on the heap “officially” supported? If it’s boxed are we guaranteed the compiler won’t make any optimizations assuming immutavilities that we might violate in doing so?)


#7

A semantic decision.

field of an immutable on the heap “officially” supported

No. They are immutable. It’s also not unofficially supported. If it is boxed, we do many compiler optimizations that would be violated by doing so.


#8

That’s what I thought, but what does Stefan mean by this, if not changing the field of a boxed immutable?

You can set it if it’s boxed because the compiler can see some mutation of it


#9

“boxed” is an overloaded term. In this case, he means that a Box instance was allocated for it.


#10

Yes, in this case, it’s literally wrapped in a mutable Core.Box type.


#11

So just to be 100% clear, you wouldn’t actually recommend trying to mutate such an immutable boxed object, would you Stefan? It seems it would be susceptible to compiler improvements (such as better inference removing the Box).

(Is it correct to assume that the case of using pointers to mutate a field of an element of an Array{T} where isbits(T) is reasonably safe (given the usual caveats with pointers)?)


#12

The box is mutable, that’s the point of the wrapper for captured variables that can change:

julia> accumulator(n=0) = (d=0) -> n += d
accumulator (generic function with 2 methods)

julia> a = accumulator()
(::#381) (generic function with 2 methods)

julia> a()
0

julia> a(1)
1

julia> a(-3)
-2

julia> a.n
Core.Box(-2)

julia> a.n.contents
-2

julia> a.n.contents = 3
3

julia> a()
3

If the captured binding is not mutable then you cannot change it. Well, not officially, but you can change it by poking around in memory, but this definitely seems like asking for trouble:

julia> adder(a) = b -> a + b
adder (generic function with 1 method)

julia> a = adder(3)
(::#385) (generic function with 1 method)

julia> a(4)
7

julia> a.a
3

julia> unsafe_load(convert(Ptr{Int}, pointer_from_objref(a)))
3

julia> unsafe_store!(convert(Ptr{Int}, pointer_from_objref(a)), 7)
Ptr{Int64} @0x0000000107c8bb00

julia> a(4)
11

Although it does happen to work in this case.