# Would attribute inference be possible?

**URL:** <https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723>\
**Category:** General Usage\
**Created:** [November 25, 2023, 9:45pm UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723 "2023-11-25T21:45:39Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![Tarny\_GG\_Channie](https://avatars.discourse-cdn.com/v4/letter/t/3bc359/32.png) [@Tarny\_GG\_Channie](https://discourse.julialang.org/u/Tarny_GG_Channie)\
**Post date:** [November 25, 2023, 9:45pm UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/1 "2023-11-25T21:45:39Z")

</div>

```julia
@attributeInferredType Student end

function main()
    s1 = Student()
    s2 = Student()
    s1.age = 19
    s2.age = 20
    s1.grade = 3.7
    #Here, you have two instances of students, one with only attribute age,
    #And another one with grade.
end

```

How would this be used?  
There could be many scenarios. Here is an example.  
Let’s say I have a game with 2 mods, one for unit experience and one for morale.

```julia
function initialize_experiences(units)
    for unit in units
        unit.experience = 0
    end
end
function initialize_morale(units)
    for unit in units
        unit.morale = get_max_morale(unit.typeid) #This is actually a global dictionary lookup.
    end
end

```

Now, depending on what mods got loaded, the attributes would change!  
Yes, you could use a dictionary but that’s inefficient.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [November 26, 2023, 1:59am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/2 "2023-11-26T01:59:47Z")

</div>

> [@Tarny\_GG\_Channie](#):
>
> ```julia
> s1.age = 19
> s2.age = 20
> s1.grade = 3.7
> #Here, you have two instances of students, one with only attribute age,
> #And another one with grade.
> 
> ```

You can pull off ad-hoc “attributes” (fields?) by overloading `Base.setproperty!` and storing things in a `Dict{String, Any}`. But no, it’s not possible for the compiler to infer the types of these ad-hoc fields. The compiler only sees the `Student` type and the typed fields it has in its definition, and that can’t change at runtime with the ad-hoc fields. If you only have a fixed set of fields, you could have the type definition specify them all but store a sentinel value or `nothing` when a field is “irrelevant”. This is of course impractical if the set is large.

---

<div class="post-metadata">

**Author:** ![mrufsvold](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mrufsvold/32/31600_2.png) [@mrufsvold](https://discourse.julialang.org/u/mrufsvold)\
**Post date:** [November 26, 2023, 3:46am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/3 "2023-11-26T03:46:24Z")

</div>

I think the repo is quiet for now, but you might look at @uniment 's WIP for some inspiration.

> **[GitHub - uniment/Objects.jl: Fast, Efficient, and Easy Objects for Julia](https://github.com/uniment/Objects.jl)**
>
> Fast, Efficient, and Easy Objects for Julia. Contribute to uniment/Objects.jl development by creating an account on GitHub.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [November 26, 2023, 3:57am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/4 "2023-11-26T03:57:55Z")

</div>

Tradeoffs still apply there:

> [@](#):
>
> Three subtypes of object are provided by storage technique: `Dynamic`, `Static`, and `Mutable`. Their external behaviors are identical, except for property-setting flexibility and performance.
> 
> - Dynamic: maximum flexibility—properties can be added or changed at any time, arbitrarily.
> - Static: maximum performance—after construction, properties cannot be changed.
> - Mutable: happy medium—properties can be changed at any time, but their types cannot change and new ones cannot be added.
> 
> …  
> `Dynamic` is very easy and casual to use, but unfortunately type instability causes lower performance. Good for hacking and playing, throwing stuff together before worrying about formalizing, but not so good for efficient runtime.

---

<div class="post-metadata">

**Author:** ![Tarny\_GG\_Channie](https://avatars.discourse-cdn.com/v4/letter/t/3bc359/32.png) [@Tarny\_GG\_Channie](https://discourse.julialang.org/u/Tarny_GG_Channie)\
**Post date:** [November 26, 2023, 4:43am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/5 "2023-11-26T04:43:34Z")

</div>

Yes, but here’s the thing.

Could a compiler powerful enough to infer the attributes of each object be implemented?

I believe the answer is yes.

In the example above, the compiler just needs to scan, create “Unit” as an abstract type, and then assign s1 and s2 to be type “Unit1” and “Unit2” respectively. Next, you trace over the object lifetime and figure out what have been assigned.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [November 26, 2023, 6:29am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/6 "2023-11-26T06:29:07Z")

</div>

> [@Tarny\_GG\_Channie](#):
>
> In the example above, the compiler just needs to scan, create “Unit” as an abstract type, and then assign s1 and s2 to be type “Unit1” and “Unit2” respectively. Next, you trace over the object lifetime and figure out what have been assigned.

I don’t understand, nowhere in your example do you mention a `Unit` type, and `s1` and `s2` seem like instances of `Student`. `s1` and `s2`’s types can diverge in separate function calls. However something like

> [@Tarny\_GG\_Channie](#):
>
> ```julia
> function main()
> s1 = Student()
> s2 = Student()
> 
> ```

forces `s1` and `s2` to be instances of the same return type of `Student()` at runtime, and that can’t change no matter what fields you assign afterward. Julia compiles per call signature, so it can’t adapt the return type of `Student()` to subsequent code, either. Maybe a tracing JIT can do it, though I’ve never heard of a compiler that “undoes” dynamism like this because this becomes unstable if field assignments become conditional e.g. `if s1.age < 18 s1.grade = 3.8 end`

---

<div class="post-metadata">

**Author:** ![Tarny\_GG\_Channie](https://avatars.discourse-cdn.com/v4/letter/t/3bc359/32.png) [@Tarny\_GG\_Channie](https://discourse.julialang.org/u/Tarny_GG_Channie)\
**Post date:** [November 26, 2023, 7:29am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/7 "2023-11-26T07:29:19Z")

</div>

Oops… you’re right… it’s Student type.

Well, maybe not now… but my idea stands, to have dynamic attributes and then optimize away the dynamism by tracing.

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [November 26, 2023, 9:41am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/8 "2023-11-26T09:41:12Z")

</div>

This sort of thing is pretty easy to do today if you make the types immutable. I’m using Accessors.jl here for convenience, but it’d be possible without too.

```julia
using Accessors
using Accessors.ConstructionBase

struct Student{K, V}
    data::NamedTuple{K, V}
end
Student(;kwargs...) = Student(NamedTuple(kwargs))
getdata(s::Student) = getfield(s, :data)

Base.propertynames(s::Student) = propertynames(getdata(s))
Base.getproperty(s::Student, prop::Symbol) = getproperty(getdata(s), prop)
ConstructionBase.setproperties(s::Student, props::NamedTuple) = Student(merge(getdata(s), props))

```

```julia
let s1 = Student(), s2 = Student()
    s1 = @set s1.age = 19
    s2 = @set s2.age = 20
    s1 = @set s1.grade = 3.7
    propertynames(s1)
    propertynames(s2)
    @info("", propertynames(s1), propertynames(s2), typeof(s1), typeof(s2))
end

```

```julia
 ┌ Info: 
 │ propertynames(s1) = (:age, :grade)
 │ propertynames(s2) = (:age,)
 │ typeof(s1) = Student{(:age, :grade), Tuple{Int64, Float64}}
 └ typeof(s2) = Student{(:age,), Tuple{Int64}}

```

Everything can be statically inferred here, no dynamism needed:

```julia
code_warntype(()) do
    s1 = Student()
    s2 = Student()
    s1 = @set s1.age = 19
    s2 = @set s2.age = 20
    s1 = @set s1.grade = 3.7
    s1, s2
end

```

```julia
MethodInstance for (::var"#7#8")()
  from (::var"#7#8")() @ Main In[10]:2
Arguments
  #self#::Core.Const(var"#7#8"())
Locals
  s2::Union{Student{(), Tuple{}}, Student{(:age,), Tuple{Int64}}}
  s1::Union{Student{(), Tuple{}}, Student{(:age,), Tuple{Int64}}, Student{(:age, :grade), Tuple{Int64, Float64}}}
Body::Tuple{Student{(:age, :grade), Tuple{Int64, Float64}}, Student{(:age,), Tuple{Int64}}}
1 ─ (s1 = Main.Student())
│ (s2 = Main.Student())
│ %3 = s1::Core.Const(Student{(), Tuple{}}(NamedTuple()))
│ %4 = Core.apply_type(PropertyLens, :age)::Core.Const(PropertyLens{:age})
│ %5 = (%4)()::Core.Const((@optic _.age))
│ %6 = (Accessors.opticcompose)(%5)::Core.Const((@optic _.age))
│ %7 = (identity)(%6)::Core.Const((@optic _.age))
│ (s1 = (Accessors.set)(%3, %7, 19))
│ %9 = s2::Core.Const(Student{(), Tuple{}}(NamedTuple()))
│ %10 = Core.apply_type(PropertyLens, :age)::Core.Const(PropertyLens{:age})
│ %11 = (%10)()::Core.Const((@optic _.age))
│ %12 = (Accessors.opticcompose)(%11)::Core.Const((@optic _.age))
│ %13 = (identity)(%12)::Core.Const((@optic _.age))
│ (s2 = (Accessors.set)(%9, %13, 20))
│ %15 = s1::Core.Const(Student{(:age,), Tuple{Int64}}((age = 19,)))
│ %16 = Core.apply_type(PropertyLens, :grade)::Core.Const(PropertyLens{:grade})
│ %17 = (%16)()::Core.Const((@optic _.grade))
│ %18 = (Accessors.opticcompose)(%17)::Core.Const((@optic _.grade))
│ %19 = (identity)(%18)::Core.Const((@optic _.grade))
│ (s1 = (Accessors.set)(%15, %19, 3.7))
│ %21 = Core.tuple(s1::Core.Const(Student{(:age, :grade), Tuple{Int64, Float64}}((age = 19, grade = 3.7))), s2::Core.Const(Student{(:age,), Tuple{Int64}}((age = 20,))))::Core.Const((Student{(:age, :grade), Tuple{Int64, Float64}}((age = 19, grade = 3.7)), Student{(:age,), Tuple{Int64}}((age = 20,))))
└── return %21

```

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [November 26, 2023, 10:03am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/9 "2023-11-26T10:03:26Z")

</div>

> [@Mason](#):
>
> `s1::Union{Student{(), Tuple{}}, Student{(:age,), Tuple{Int64}}, Student{(:age, :grade), Tuple{Int64, Float64}}}`

But as `@set` reassigns a variable with a new type, the union of inferred types increases in size. I think it’s ~4 before it falls back to `Any`. The call-wise compiler can’t retroactively apply and keep only the last one, as Tarny\_GG\_Channie is suggesting.

---

<div class="post-metadata">

**Author:** ![Tarny\_GG\_Channie](https://avatars.discourse-cdn.com/v4/letter/t/3bc359/32.png) [@Tarny\_GG\_Channie](https://discourse.julialang.org/u/Tarny_GG_Channie)\
**Post date:** [November 26, 2023, 10:15am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/10 "2023-11-26T10:15:40Z")

</div>

Genius! It works in this case! However, I’m still in doubt when complicated stuff happens like in the “unit” example. Also, this was a case where everything turned out to be constant, which might have some benefit to type inference. I’d still have to say it’s a good work.

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [November 26, 2023, 10:17am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/11 "2023-11-26T10:17:15Z")

</div>

> [@Benny](#):
>
> > [@Mason](#):
> >
> > `s1::Union{Student{(), Tuple{}}, Student{(:age,), Tuple{Int64}}, Student{(:age, :grade), Tuple{Int64, Float64}}}`
> 
> But as `@set` reassigns a variable with a new type, the union of inferred types increases in size.

That’s just the binding `s1`, it doesn’t affect the compiled code at all. If you look at the actually typed code, every line is concretely inferred. Notice that none of the SSA slots are labelled `s1`, they’re all `%1`, `%2`, …, etc.

It’s maybe more clear if I show the `code_typed`:

```julia
code_typed(()) do
    s1 = Student()
    s2 = Student()
    s1 = @set s1.age = 19
    s2 = @set s2.age = 20
    s1 = @set s1.grade = 3.7
    s1, s2
end

```

```julia
  CodeInfo(
 1 ─ return (Student{(:age, :grade), Tuple{Int64, Float64}}((age = 19, grade = 3.7)), Student{(:age,), Tuple{Int64}}((age = 20,)))
 ) => Tuple{Student{(:age, :grade), Tuple{Int64, Float64}}, Student{(:age,), Tuple{Int64}}}

```

So we see the compiler fully understands the body of the code here.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [November 26, 2023, 11:08am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/12 "2023-11-26T11:08:10Z")

</div>

Now that I look at the `code_warntype` printout more closely, the types of `s1` and `s2` between each reassignment are indeed concretely inferred. The compiler is only uncertain if you make the reassignments conditional, like in if statements and for loops:

```julia
julia> maybe::Bool = true
true

julia> code_warntype(()) do
           s1 = Student()
           s2 = Student()
           if maybe s1 = @set s1.age = 19 end
           if maybe s2 = @set s2.age = 20 end
           if maybe s1 = @set s1.grade = 3.7 end
           s1, s2
       end
MethodInstance for (::var"#13#14")()
  from (::var"#13#14")() in Main at REPL[22]:2
Arguments
  #self#::Core.Const(var"#13#14"())
Locals
  s2::Union{Student{(), Tuple{}}, Student{(:age,), Tuple{Int64}}}
  s1::Student
Body::Tuple{Student, Union{Student{(), Tuple{}}, Student{(:age,), Tuple{Int64}}}}
...

```

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [November 26, 2023, 11:59am UTC](https://discourse.julialang.org/t/would-attribute-inference-be-possible/106723/13 "2023-11-26T11:59:47Z")

</div>

> [@Tarny\_GG\_Channie](#):
>
> It works in this case! However, I’m still in doubt when complicated stuff happens like in the “unit” example

It depends. The unit example isn’t very fleshed out. You’d just need to understand with this approach that every time you add a new property to your struct, its type changes. You then need to have a good mental model of what sorts of things the compiler is able to reason about, and what sorts of things it’s not able to reason about, and adjust accordingly.

For instance, your function stub `initialize_experiences` would encounter problems because e.g. a `Vector{Unit{(), Tuple{}}}` has no fields `experience`, so we would need to allocate a whole new vector if we encountered it. However, we can do a “mutate or widen” strategy where we only reallocate the vector when necessary.

E.g.:

> **Some initial setup:**
>
> ```julia
> using Accessors
> using Accessors.ConstructionBase
> 
> function propertytype end
> function propertytypes end
> macro nt_struct(_Name::Symbol)
> Name = esc(_Name)
> mod = @ __MODULE__ ()
> quote
> struct $Name{K, V}
> data::NamedTuple{K, V}
> end
> $Name(;kwargs...) = $Name(NamedTuple(kwargs))
> 
> getdata(s::$Name) = getfield(s, :data)
> Base.propertynames(s::$Name) = propertynames(getdata(s))
> Base.getproperty(s::$Name, prop::Symbol) = getproperty(getdata(s), prop)
> ConstructionBase.setproperties(s::$Name, props::NamedTuple) = $Name(merge(getdata(s), props))
> $mod.propertytype(::$Name{K,V}, s::Symbol) where {K,V} = $propertytype($Name{K, V}, s)
> $mod.propertytype(::Type{$Name{K,V}}, s::Symbol) where {K,V} = fieldtype(NamedTuple{K, V}, s)
> $mod.propertytypes(::$Name{K,V}) where {K,V} = $propertytypes($Name{K, V}, s)
> $mod.propertytypes(::Type{$Name{K,V}}) where {K,V} = fieldtypes(NamedTuple{K, V})
> end
> end
> 
> ```

and then you can do things like

```julia
@nt_struct Unit
function initialize_experience!!(units::Vector{Unit{names, types}}) where {names, types}
    if :experience ∈ names
        units_out = units
    else
        UnitType = Unit{(names..., :experience), Tuple{propertytypes(eltype(units))..., Int}}
        units_out = Vector{UnitType}(undef, length(units))
    end
    for (i, unit) ∈ enumerate(units)
        units_out[i] = @set unit.experience = 0
    end
    units_out
end

```

This sort of strategy forms the backbone of packages like Transducers.jl, via a package called BangBang.jl, though it doesn’t have great support for this use-case. Maybe we should add a `map!!` function to BangBang which does this.

The idea here is that throughout the loop, we check if it’s possible to do

```julia
setindex!(units, @set(unit.experience = 0), i)

```

if `units` can accommodate that type, then we are able to do a simple `setindex!`, if not, then we need to re-allocate `units` with a wider type that has the `:experience` field. calling it something like `map!!`.

This is all type stable and inferrable. No tracing JIT, or other speculative/dubious modifications to the language necessary. Sure, it’s more work than using a regular mutable type with a fixed known amount of fields, but hey, sometimes you want to be able to change things around like this.
