Destroying type stabiliy by creating "chaining" closures of functions

question

#1

I’ve found myself doing things like the below out of convenience. I’ve noticed that type stability is destroyed however. Is there a better way of achieving the same thing?

julia> struct Foo{T}
       a::T
       end

julia> (foo::Foo)() = foo.a

julia> foo = Foo(1)
Foo{Int64}(1)

julia> @code_warntype foo()
Body::Int64
1 1 ─ %1 = (Base.getfield)(foo, :a)::Int64                               │╻ getproperty
  └──      return %1                                                     │ 

julia> bar() = foo()
bar (generic function with 1 method)

julia> @code_warntype bar()
Body::Any
1 1 ─ %1 = (Main.foo)()::Any                                                          │
  └──      return %1                                                                  │

julia> baz() = () -> foo() + foo()
baz (generic function with 1 method)

julia> @code_warntype baz()
Body::getfield(Main, Symbol("##3#4"))
1 1 ─ %1 = %new(Main.:(##3#4))::Core.Compiler.Const(getfield(Main, Symbol("##3#4"))(), false)
  └──      return %1    

julia> versioninfo()
Julia Version 0.7.1-pre.0
Commit 36cddc1006 (2018-08-09 00:19 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin17.7.0)
  CPU: Intel(R) Core(TM) i7-5557U CPU @ 3.10GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, broadwell)
Environment:
  JULIA_NUM_THREADS = 4

The reason I’m doing this because the functions keep a reference to the original objects. This means I can close a function over a mutable struct, then later update it, and these changes are reflected in the closure:

julia> mutable struct Foo{T}
       a::T
       end

julia> (foo::Foo)() = foo.a

julia> foo = Foo(1)
Foo{Int64}(1)

julia> bar() = foo()
bar (generic function with 1 method)

julia> bar()
1

julia> foo.a = 2
2

julia> bar()
2

#2
julia> struct Foo{T}
              a::T
         end
julia> (foo::Foo)() = foo.a
julia> const foo = Foo(1)
Foo{Int64}(1)
julia> bar() = foo()
bar (generic function with 1 method)
julia> @code_warntype bar()
Body::Int64
1 1 ─     return 1

Can’t you make foo constant? All non const globals have this problem, since you can assign anything to them at any point in the program.


#3

Thanks! I thought I couldn’t do that, but it seems I can, but now I’m not sure what const actually means.

julia> mutable struct Foo{T}
       a::T
       end

julia> (foo::Foo)() = foo.a

julia> const foo = Foo(1)
Foo{Int64}(1)

julia> bar() = foo()
bar (generic function with 1 method)

julia> @code_warntype bar()
Variables:
  #self# <optimized out>

Body:
  begin 
      return (Core.getfield)(Main.foo, :a)::Int64
  end::Int64

julia> bar()
1

julia> foo.a = 2
2

julia> bar()
2

#4

Marking a global as const tells the compiler that the variable is not going to change (neither value nor type) since constants will only be assigned once. Be aware that this only works for global scope. See here.


#5

It seems as though the scope dependent behaviour of const is quite nuanced, especially compared to the same word in c++.


#6

Is it? In the global scope, it just says that the binding cannot be changed. In local scope, it’s an error.


#7

I misspoke, I mean nuanced in general. This here is a definite gotcha:

julia> mutable struct Bar{T}
           a::T
       end

julia> struct Foo{T}
           a::T
       end

julia> const foo = Foo(Bar(1))
Foo{Bar{Int64}}(Bar{Int64}(1))

julia> foo.a.a = 2
2

But this throws:

julia> foo.a = Bar(2.0)
ERROR: MethodError: Cannot `convert` an object of type Bar{Float64} to an object of type Bar{Int64}
Closest candidates are:
  convert(::Type{T}, ::T) where T at essentials.jl:154
  Bar{Int64}(::Any) where T at REPL[1]:2
Stacktrace:
 [1] setproperty!(::Foo{Bar{Int64}}, ::Symbol, ::Bar{Float64}) at ./sysimg.jl:19
 [2] top-level scope at none:0

#8

Neither of those has anything to do with const. Const does not make a mutable object immutable, so there’s nothing wrong with modifying its field. It only requires that the binding (the label foo) is not reassigned to some other object.

The second case also has nothing to do with const, and you’ll get the same error without a const declaration. It’s simply a matter of trying to set a field with a value of the wrong type.


#9

const is regarding the constness of the binding, not the constness of the content in e.g. a container.

So if you have const Foo = ... it means you can never do global Foo = something_else().


#10

Could this explanation go in the docs? As well as @rdeits’s comment about it being separate from mutability?


#11

The docs say

Note that “constant-ness” does not extend into mutable containers; only the
association between a variable and its value is constant. If x is an array or
dictionary (for example) you can still modify, add, or remove elements.

But feel free to put up a PR with improved wording.


#12

Having thought about it, and considered the examples in this post, the explanation in the docs covers everything. I suppose I just had a mental block changing from c++ const to Julia const.

My tuppence on the subject that I expect that a lot of c++ focused developers will have raised eyebrows when the language even allows them to change something prefixed by const regardless of the warning. I for one would have better understood a word like consttype or fixedtype, or something entirely different like strong, to embody the qualities of const.


#13

Maybe compare it to the following in C++:

struct Bar {
    int a;
};

struct Foo {
    Bar &a;
};

void f(Foo const &foo) {
    foo.a.a = 2;
}

#14

I was totally wrong!


#15

Yeah, I think it just comes down to the fact that C/C++ has two kinds of const-y pointers: a const pointer and a pointer-to-const, and they do completely different things: one stops you from changing what address the pointer points to and the other stops you from modifying whatever is at that address.

Julia handles things differently: mutability is a property of the object (mutable struct vs. struct), so the only thing a const annotation can do is inform the language that a particular binding will always refer to the same value. So Julia consts behave essentially like (I think…) const pointers, and not like pointers-to-const.


#16

To follow on with the original issue here is a MWE of what I now think is going on.

Needless to say I’ve been doing this kind of thing the type unstable way.

julia> struct Foo{T}
           bar::T
       end

julia> function unstable()
           foo = Foo(1)
           foos = Vector{Foo}([foo]) # what I've been doing
           f() = foos[1].bar
           return f
       end
unstable (generic function with 1 method)

julia> function stable()
           foo = Foo(1)
           foos = Vector{Foo{typeof(foo.bar)}}([foo]) # my guess work for stability
           f() = foos[1].bar
           return f
       end
stable (generic function with 1 method)

julia> f_stable = stable()
(::getfield(Main, Symbol("#f#4")){Array{Foo{Int64},1}}) (generic function with 1 method)

julia> f_unstable = unstable()
(::getfield(Main, Symbol("#f#3")){Array{Foo,1}}) (generic function with 1 method)

julia> @code_warntype f_stable()
Body::Int64
4 1 ─ %1 = (Core.getfield)(#self#, :foos)::Array{Foo{Int64},1}           │ 
  │   %2 = (Base.arrayref)(true, %1, 1)::Foo{Int64}                      │╻ getindex
  │   %3 = (Base.getfield)(%2, :bar)::Int64                              │╻ getproperty
  └──      return %3                                                     │ 

julia> @code_warntype f_unstable()
Body::Any
4 1 ─ %1 = (Core.getfield)(#self#, :foos)::Array{Foo,1}                  │ 
  │   %2 = (Base.arrayref)(true, %1, 1)::Foo                             │╻ getindex
  │   %3 = (Base.getfield)(%2, :bar)::Any                                │╻ getproperty
  └──      return %3                                                     │ 

Is creating Vector{Foo{typeof(foo.bar)}}([foo]), from function stable(), the only want get a type stability in the returned function?


#17

Vector{Foo} needs to be able to hold Foo with any T for example [Foo(1), Foo(1.0), Foo("bar"), Foo([1,2,3])].

This is also possible

julia> function stable2()
           foo = Foo(1)
           foos = [foo] # what I've been doing
           f() = foos[1].bar
           return f
       end

#18

Great! Thank you!