# Julep: Efficient Hierarchical Mutable Data

**URL:** <https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190>\
**Category:** Internals & Design\
**Tags:** mutable-structure, rfc\
**Created:** [January 25, 2025, 6:51am UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190 "2025-01-25T06:51:04Z")\
**Posts on this page:** 20\
**Page:** 2

<div class="post-metadata">

**Author:** ![StefanKarpinski](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stefankarpinski/32/24_2.png) [@StefanKarpinski](https://discourse.julialang.org/u/StefanKarpinski)\
**Post date:** [January 27, 2025, 2:09pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/22 "2025-01-27T14:09:49Z")

</div>

The key question about this proposal to me (which I don’t see addressed, but may have missed), is what happens when a reference to `p` outlives any references to `w`? In other words, the GC is doing its thing and there are no references to `w`. How do you ensure that `w` doesn’t get reclaimed? There’s a bunch of talk about lifetimes and their enforcement, but presumably it’s clear that Julia doesn’t have any lifetime enforcement mechanism (nor even a compile-time correctness checking phase, when such a mechanism would run)—objects live as long as they live and you have to handle it.

---

<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:** [January 27, 2025, 2:48pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/23 "2025-01-27T14:48:44Z")

</div>

> [@PatrickHaecker](#):
>
> Please note that for other cases the inlined mutable is more flexible than the non-`const` inlined immutable as you can use sharing for the former ones, but not for the latter ones.

For just the field, yes, but that’s what the larger mutable struct can accomplish. If we can’t share the seats or wheels, we can at least share the car. If there is no car and we really are trying to share a wheel and its changes, then `Ref` can help.

> [@PatrickHaecker](#):
>
> `part1` and `part2` could not both contain `p`

Which means that there are 2 references that can’t share one of their objects. With other instances or types with `inline` fields, that’s more references that can’t share a mutable object. That goes against mutable types’ intention and breaks the guarantee that we can assign anything to a reference. For example, say we instantiate `w1 = Whole(Part(), Part())`, a whole with 2 different parts, we’re not trying to share things there. Then we try `Whole(w1.part2, w1.part1)`; this will fail for an unprecedented reason. Granted, we have something similar with `const` disallowing reassignments; however, they do allow any assignment the first time.

---

<div class="post-metadata">

**Author:** ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)\
**Post date:** [January 27, 2025, 3:01pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/24 "2025-01-27T15:01:54Z")

</div>

> [@StefanKarpinski](#):
>
> The key question about this proposal to me (which I don’t see addressed, but may have missed), is what happens when a reference to `p` outlives any references to `w`?

My proposal above would look like

```julia
struct FieldRef{T}
owner::Any #for GC purposes only
ptr::Ptr{T}
end

```

and would rely on the hypothetical new `Core.Intrinsics.interior_pointerset(owner, ptr, value)` thing that writes `value` to `ptr`, while applying the GC write-barrier as if `value` had been written to a field of `owner`.

That would be relevant for

```julia
struct Inner
  x::Vector{Int}
end

mutable struct Owner
  part::Inner
end

reset!(ref::FieldRef{Inner}) = ref[] = Inner(Int[])

owner = Owner(Inner(Int[]))
reset!(getfieldref(owner, :inner)) #needs a GC barrier if owner is old-gen!

```

---

<div class="post-metadata">

**Author:** ![PatrickHaecker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/patrickhaecker/32/222891_2.png) [@PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker)\
**Post date:** [January 27, 2025, 4:06pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/25 "2025-01-27T16:06:09Z")

</div>

> That goes against mutable types’ intention and breaks the guarantee that we can assign anything to a reference.

Indeed, that is why `inline` would be a stronger `const`. The new rules would be:

- You can assign anything to a reference …
- … except if it’s `const` then you can only assign once during construction
- … except if it’s `inline` then you can only assign once during construction and this assignment is a newly created object

---

<div class="post-metadata">

**Author:** ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)\
**Post date:** [January 27, 2025, 9:48pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/26 "2025-01-27T21:48:54Z")

</div>

I had some time this morning to try my hand at a macro-based solution. I believe that it’s 90% there (with the remaining 90% conceptually straight-forward), and mostly in line with the original proposal.

```julia
julia> abstract type AbstractElephant end

julia> struct Elephant <: AbstractElephant
           hunger::Float64
       end

julia> @with_inline_types mutable struct Forest
           elephant::@inline(Elephant) # @inline elephant::Elephant also possible, but less
                                         # MacroTools-friendly
           normal_field::String
       end
Forest

julia> @with_inline_types mutable struct Biome
           forest::@inline(Forest)
       end
Biome

julia> f = Forest(Elephant(2), "Zoulando") # TODO: overload Base.show
Forest(2.0, "Zoulando")

julia> dump(f)
Forest
  elephant__hunger: Float64 2.0
  normal_field: String "Zoulando"

julia> f.elephant
ElephantViewFromForest(Forest(30.0, "Zoulando"))

julia> f.elephant.hunger = 30
30

julia> f.elephant.hunger
30.0

julia> b = Biome(Forest(Elephant(50), "Tasmania"))
Biome(50.0, "Tasmania")

julia> fieldnames(Biome)
(:forest __elephant__ hunger, :forest__normal_field)

```

Nested inline structs work out for construction, but field access is broken ATM. AFAICT views don’t compose, and we’d have to define `ElephantViewFromForestFromBiome`. Not very difficult, but a bit of a drag.

`@btime` looks good (no allocation) for access. `Biome(Forest(Elephant(50), "Tasmania"))` does create a needless `Forest` object. It’d need some rethinking to avoid that.

Parametric structs would be significant work.

> ****
>
> ```julia
> using MacroTools
> using MacroTools: @qq
> using Base.Meta: quot
> 
> """ Defined for types, not objects. """
> static_propertynames(::Type{T}) where T = fieldnames(T)
> 
> macro with_inline_types(def)
> # Example with respect to this
> # @with_inline_types struct Forest
> # elephant::@inline(Elephant)
> # normal_field::String
> # end
> 
> di = MacroTools.splitstructdef(def)
> di2 = copy(di)
> name = di[:name] # Forest
> inline_code = Expr[] # code to define the type views
> di2[:fields] = [] # [(normal_field, Int)]
> inlined_type_fields = Pair[] # [:elephant=>ElephatViewFromForest]
> prop_names = Symbol[] # [:elephant, :normal_field]
> constr_accessors = []
> 
> for (f, type) in di[:fields]
> push!(prop_names, f)
> if @capture(type, @inline(Texpr_))
> T = eval(Texpr) # having to `eval` is unfortunate. there are other ways worth
> # investigating, such as `getfield(@ __MODULE__ , Texpr)`, that would
> # have different trade-offs
> s_fields = [Symbol(f, "__", sub_f) for sub_f in fieldnames(T)] # [:elephant__hunger]
> view_name = Symbol(Texpr, "ViewFrom", name) # technically should also append __f
> type_view_code = quote
> struct $view_name <: $(supertype(T)) # ElephantViewFromForest <: AbstractElephant
> obj::$name
> end
> function Base.getproperty(view::$view_name, p::Symbol)
> # if p == :hunger
> # return getfield(view, :obj).elephant__hunger
> # end
> # error("Field not found")
> $([:(if p === $(quot(fieldname))
> return getfield(view, :obj).$s_fieldname
> end)
> for (fieldname, s_fieldname) in zip(static_propertynames(T), s_fields)]...)
> error("type ", $Texpr, " has no property ", p)
> end
> Base.propertynames(view::$view_name) = $(fieldnames(T))
> function Base.setproperty!(view::$view_name, p::Symbol, x)
> $([:(if p === $(quot(fieldname))
> return getfield(view, :obj).$s_fieldname = x
> end)
> for (fieldname, s_fieldname) in zip(static_propertynames(T), s_fields)]...)
> error("type ", $Texpr, " has no property ", p)
> end
> end
> for s_field in fieldnames(T)
> push!(constr_accessors, :(getfield($f, $(quot(s_field)))))
> end
> push!(inline_code, type_view_code)
> push!(inlined_type_fields, f => view_name)
> append!(di2[:fields], map(tuple, s_fields, fieldtypes(T)))
> else
> push!(constr_accessors, f)
> push!(di2[:fields], (f, type))
> end
> end
> @qq(begin # @qq to get good line numbers
> $(MacroTools.combinestructdef(di2))
> $(inline_code...)
> function Base.getproperty(obj::$name, p::Symbol)
> # if p == :elephant
> # return ElephantViewFromForest(obj)
> # end
> # return getfield(obj, p)
> $([:(if p === $(quot(fieldname))
> return $sub_type(obj)
> end)
> for (fieldname, sub_type) in inlined_type_fields]...)
> return Base.getfield(obj, p)
> end
> Base.propertynames(obj::$name) = $prop_names
> static_propertynames(::Type{$name}) = $prop_names
> function $name($(prop_names...))
> $name($(constr_accessors...))
> end
> $name
> end) |> esc
> end
> 
> ```

---

<div class="post-metadata">

**Author:** ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)\
**Post date:** [January 27, 2025, 10:27pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/27 "2025-01-27T22:27:29Z")

</div>

Your code can’t deal with

```julia
julia> @with_inline_types mutable struct BigForest
           elephant1::@inline(Elephant) # @inline elephant::Elephant also possible, but less
                                         # MacroTools-friendly
           elephant2::@inline(Elephant)
           normal_field::String
           end
julia> f=BigForest(Elephant(1.0), Elephant(2.0), ""); f.elephant1.hunger #should be 1.0
2.0

```

---

<div class="post-metadata">

**Author:** ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)\
**Post date:** [January 27, 2025, 11:10pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/28 "2025-01-27T23:10:50Z")

</div>

Right, that’s what this comment was about:

```julia
            view_name = Symbol(Texpr, "ViewFrom", name) # technically should also append __f

```

so

```julia
            view_name = Symbol(Texpr, "ViewFrom", name, "__", f)

```

then it would work out I believe, but maybe it’d need some more tweaking.

---

<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:** [January 28, 2025, 6:15am UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/29 "2025-01-28T06:15:52Z")

</div>

> [@PatrickHaecker](#):
>
> … except if it’s `inline` then you can only assign once during construction and this assignment is a newly created object

“Newly created object” is not a simple condition. In `Whole`’s inner constructor, we can at least make sure we call its `new` at some point. However, `Whole` has no idea if `Part` was instantiated within `Whole`’s constructor call, even if `Part`’s constructor is directly called; the `Part()` call is not guaranteed to call its own `new`, let alone return a `Part` instance. Even if we can infer the instantiation of `Part` within `Whole`, it’s not a sufficient condition. We have to also confirm that it won’t be initially stored elsewhere, like another conflicting `inline` field, before any one of `Whole`’s fields. As mentioned before, `x = Part(); return new(x, x)` would fail for 2 `inline` fields, but it could also fail if we instantiate something else that stores the `Part` in an `inline` field. Avoiding failure requires narrow, brittle, and opaque conditions possibly through layers of callers, and dynamic dispatch can prevent the compiler from resolving this. The compiler also can’t fall back to non-`inline` storage without overheads.

The “easy” solution to this is for `new` to force a newly created object by `copy`ing the instance before `inline`-ing the copy, much like immutable instances. We don’t have to care about whether the input was stored elsewhere or if the compiler can tell. It could be possible for the compiler to optimize away the instantiation of the original instance if it can be inferred that it occurs within the outer `mutable struct`’s constructor. But if we’re copying things around like this, immutables make more sense. However, updating fields by instantiation and reassignment does have its own issues to work out regarding object identity of nested immutable and mutable structs, it’s not as straightfoward as `a.b.c.d = e` currently meaning mutating the `d` field of `a.b.c` with no (direct) changes to `a` or `b`. For example, if any layer is mutable, we’d likely want to mutate it there rather than instantiate the whole thing and reassign, e.g. not `x = x0 = Ref(1); @reset x[] = 2; (x !== x0, x0, x)`.

---

<div class="post-metadata">

**Author:** ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)\
**Post date:** [January 28, 2025, 9:51pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/30 "2025-01-28T21:51:56Z")

</div>

I completed the (POC of the) macro-based version this morning. Now it works for nested structs:

```julia
abstract type AbstractElephant end

struct Elephant <: AbstractElephant
    hunger::Float64
end

@with_inline_types mutable struct Forest
    elephant::@inline(Elephant) # @inline elephant::Elephant also possible, but less
                                  # MacroTools-friendly
    normal_field::String
end

@with_inline_types mutable struct Biome
    forest::@inline(Forest)
end

julia> b = Biome(Forest(Elephant(50), "Tasmania"))
Biome(50.0, "Tasmania")

julia> b.forest.elephant.hunger = 33.0
33.0

julia> dump(b)
Biome
  forest __elephant__ hunger: Float64 33.0
  forest__normal_field: String "Tasmania"

julia> @btime $b.forest.elephant.hunger
  2.768 ns (0 allocations: 0 bytes)
33.0

```

I used generated functions to limit the complexity of the macro expansion. The way it’s written, it might also not be that difficult to support parametric types…

[PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker) Would that meet your needs?

I get the appeal of getting a new keyword in base to solve such a problem, but as you probably are well aware, that does not happen very often. Pushing this macro to the finishing line is more likely to work out… Happy to help if there is interest.

Full code:

> ****
>
> ```julia
> using MacroTools
> using MacroTools: @qq
> using Base.Meta: quot
> 
> """ Defined for types, not objects. """
> static_propertynames(::Type{T}) where T = fieldnames(T)
> 
> get_inlined_type_fields(x) = Val((;))
> 
> @generated function get_view_property(::Val{inl_typ_fields}, view::T, ::Val{p2}) where {T, p2, inl_typ_fields}
> # When evaluating `b.forest.elephant.hunger`, this is called twice. First time O is
> # ForestView{:forest, Biome}, and we return
> # ElephantView{:forest__elephant, Biome}(view.obj)
> # (simplified) from the first branch below while second time we return
> # view.obj.forest __elephant__ hunger
> # from the second branch. The idea being that each nested View object has, as
> # parameter, the corresponding field prefix.
> @assert length(T.parameters) == 2
> p1 = T.parameters[1]
> obj_expr = Expr(:call, :getfield, :view, quot(:obj))
> if haskey(inl_typ_fields, p2)
> sub_view = inl_typ_fields[p2]
> p12 = quot(Symbol(p1, "__", p2))
> return :($sub_view{$p12, typeof($obj_expr)}($obj_expr))
> else
> return Expr(:call, :getfield, obj_expr, quot(Symbol(p1, "__", p2)))
> end
> end
> 
> @generated function set_view_property!(::Val{inl_typ_fields}, view::T, ::Val{p2}, new_val) where {T, p2, inl_typ_fields}
> @assert length(T.parameters) == 2
> p1 = T.parameters[1]
> obj_expr = Expr(:call, :getfield, :view, quot(:obj))
> if haskey(inl_typ_fields, p2)
> # Eg. this happens for
> # b.forest.elephant = Elephant(...)
> error("TODO: must copy all fields")
> else
> # TODO: put a `convert` call to the right type
> return Expr(:call, :setfield!, obj_expr, quot(Symbol(p1, "__", p2)), :new_val)
> end
> end
> 
> macro with_inline_types(def)
> # Example with respect to this
> # @with_inline_types struct Forest
> # elephant::@inline(Elephant)
> # normal_field::String
> # end
> 
> di = MacroTools.splitstructdef(def)
> di2 = copy(di)
> name = di[:name] # Forest
> inline_code = Expr[] # code to define the type views
> di2[:fields] = [] # [(normal_field, Int)]
> inlined_type_fields = Dict() # [:elephant=>ElephantView]
> prop_names = Symbol[] # [:elephant, :normal_field]
> constr_accessors = []
> 
> for (f, type) in di[:fields]
> push!(prop_names, f)
> if @capture(type, @inline(Texpr_))
> T = eval(Texpr) # having to `eval` is unfortunate. there are other ways worth
> # investigating, such as `getfield(@ __MODULE__ , Texpr)`, that would
> # have different trade-offs
> s_fields = [Symbol(f, "__", sub_f) for sub_f in fieldnames(T)] # [:elephant__hunger]
> view_name = Symbol(Texpr, "View")
> type_view_code = quote
> struct $view_name{F, U} <: $(supertype(T)) # ElephantView <: AbstractElephant
> obj::U
> end
> Base.propertynames(view::$view_name) = $(fieldnames(T))
> Base.getproperty(view::$view_name, p::Symbol) =
> get_view_property(get_inlined_type_fields($Texpr), view, Val{p}())
> Base.setproperty!(view::$view_name, p::Symbol, x) =
> set_view_property!(get_inlined_type_fields($Texpr), view, Val{p}(), x)
> end
> for s_field in fieldnames(T)
> push!(constr_accessors, :(getfield($f, $(quot(s_field)))))
> end
> push!(inline_code, type_view_code)
> inlined_type_fields[f] = :($view_name)
> append!(di2[:fields], map(tuple, s_fields, fieldtypes(T)))
> else
> push!(constr_accessors, f)
> push!(di2[:fields], (f, type))
> end
> end
> @qq(begin # @qq to get good line numbers
> $(MacroTools.combinestructdef(di2))
> $(inline_code...)
> function Base.getproperty(obj::$name, p::Symbol)
> # if p == :elephant
> # return ElephantViewFromForest(obj)
> # end
> # return getfield(obj, p)
> $([:(if p === $(quot(fieldname))
> return $view_type{$(quot(fieldname)), $name}(obj)
> end)
> for (fieldname, view_type) in inlined_type_fields]...)
> return Base.getfield(obj, p)
> end
> Base.propertynames(obj::$name) = $prop_names
> static_propertynames(::Type{$name}) = $prop_names
> function $name($(prop_names...))
> $name($(constr_accessors...))
> end
> get_inlined_type_fields(::Type{$name}) = $(Val(NamedTuple(inlined_type_fields)))
> $name
> end) |> esc
> end
> 
> ```

---

<div class="post-metadata">

**Author:** ![PatrickHaecker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/patrickhaecker/32/222891_2.png) [@PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker)\
**Post date:** [January 29, 2025, 6:54am UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/31 "2025-01-29T06:54:47Z")

</div>

> Avoiding failure requires narrow, brittle, and opaque conditions possibly through layers of callers, and dynamic dispatch can prevent the compiler from resolving this.

The general case is hard. Although it would be nice to support all cases, I think this is not necessary, as `inline` creates some kind of static context rendering Julia’s more dynamic parts incompatible. In the end the semantics only make sense to me if exactly one `new` call of `Part` happens, if the reference of this call is returned and if an `inline Part` does not have a (direct or indirect) `Part` field (a non-inlined one might work, but I am not yet convinced that it should be supported).

If this can be inferred (could the effect system help here?), the call is valid, otherwise it is invalid. And as the `new` call needs to act differently and we need to hand the memory address to it, all involved functions on the stack between the inner constructor of `Whole` and the inner constructor of `Part` need another method with an implicit parameter transporting this information.

> But if we’re copying things around like this, immutables make more sense.

I agree. Copying sounds compelling in the beginning, but it needs a strong story about object identity to really make sense with mutables. This might mean that nothing can observe the originally created object which is the source object of the copy and that probably brings us back to the `new` approach.

---

<div class="post-metadata">

**Author:** ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)\
**Post date:** [January 29, 2025, 9:45am UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/32 "2025-01-29T09:45:09Z")

</div>

Nice. However,

```julia
julia> f=BigForest(Elephant(1.0), Elephant(2.0), ""); f.elephant2.hunger
2.0

julia> typeof(f.elephant2)
ElephantView{:elephant2, BigForest}

julia> typeof(f.elephant2) == typeof(f.elephant1)
false

```

which brings up what I noted upthread:

> [@foobar\_lv2](#):
>
> The thing that makes this a new language feature is the desire that `typeof(fieldref(w::Whole, :part1)) === typeof(fieldref(w, :part2))`, i.e. that the type does not imply the offsets from the parent object.

I think this would really require new (albeit simple) core intrinsics relating to GC write barriers and a `very_unsafe_pointer_to_objref(typ::DataType, p::Ptr) = unsafe_pointer_to_objref(p)::typ` that assumes the type instead of checking (so UB instead of exception if it’s wrong, unless `check-bounds=yes`, or some compiler flag for the julia runtime is set).

---

<div class="post-metadata">

**Author:** ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)\
**Post date:** [January 29, 2025, 7:59pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/33 "2025-01-29T19:59:16Z")

</div>

> [@foobar\_lv2](#):
>
> The thing that makes this a new language feature is the desire that `typeof(fieldref(w::Whole, :part1)) === typeof(fieldref(w, :part2))`, i.e. that the type does not imply the offsets from the parent object.

I’m not sure I get why this is a desideratum. Both are `AbstractElephants`, just like array views are `AbstractArray`s. It’d be problematic for performance if you wanted to, say, put `elephant1` and `elephant2` inside a `Vector`, but given the context of the post (wants to share lifetime of parent object with child) that seems unlikely.

If you wanted a C-style inlined array, as in

```julia
struct Forest
    @inlined trees::Tree[4]
end

```

you could implement a `TreeArrayView` where the index is not part of the type.

That said, I can see some of the “Why does this have to be so complicated in Julia” point…

---

<div class="post-metadata">

**Author:** ![PatrickHaecker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/patrickhaecker/32/222891_2.png) [@PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker)\
**Post date:** [January 30, 2025, 9:46am UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/34 "2025-01-30T09:46:34Z")

</div>

> I had some time this morning to try my hand at a macro-based solution.

This is indeed impressive macro work!

I used

```julia
mutable struct Part
    a::Float32
end

@with_inline_types mutable struct Whole 
    b::Int32
    part::@inline(Part) 
end

w = Whole(Int32(42), Part(42.0f0))

```

to stay compatible with the rest of the discussion (but great that the solution is tested with nested structs)

Note that I have only a limited understanding of the macro implementation, so please correct points I haven’t evaluated correctly, so I can correct the list.

These are the pros and cons I see :

pros:

- clearly defined constructor semantics
- seems to have ideal memory behavior (only one alloc, clear GC story)
- field access works with nested structs
- preserves user defined `getproperty` for `Whole`:  
`Base.getproperty(w::Whole, s::Symbol) = s === :c ? true : getfield(w, s)`  
`julia> w.c`  
`true`
- Preserves object identity (although I do not understand why)  
`w1 = Whole(Int32(1), Part(1.0))`  
`w2 = Whole(Int32(1), Part(1.0))`  
`julia> w1.part === w1.part !== w2.part === w2.part`  
`true`
- methods involving `Part` can be called (using a `Union` or subtyping):

cons:

- constructor no longer supports type conversion (`Whole(42, Part(42.0)` does not work)
- definition syntax more complicated as macro needed both before struct definition and inside field definition
- returns “wrong” fieldnames:  
`julia> Whole |> fieldnames`  
`(:b, :part__a)`
- returns “wrong” type for substructs: `w.part |> typeof`  
`julia> w.part`  
`PartView{:part, Whole}(Whole(42, 42.0f0))`
- `Part`’s methods needs to accept a `Union` or an abstract type (then `Part` needs to use that, too) to work
- Does not support getting the memory address  
`julia> w1.part |> pointer_from_objref`  
`ERROR: pointer_from_objref cannot be used on immutable objects`
- Returns [different types for multiple fields of the same type](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/32)

~~So while it does work for the simplest of cases, it seems to be too limited (but I might be using it incorrectly).~~

**Edit** : corrections due to additional input (see below)  
**Edit 2** : added `foobar_lv2`’s limitation

---

<div class="post-metadata">

**Author:** ![JonasWickman](https://avatars.discourse-cdn.com/v4/letter/j/9de0a6/32.png) [@JonasWickman](https://discourse.julialang.org/u/JonasWickman)\
**Post date:** [January 30, 2025, 2:48pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/35 "2025-01-30T14:48:51Z")

</div>

> [@PatrickHaecker](#):
>
> - returns wrong type for substructs: `w.part |> typeof`  
> `julia> w.part`  
> `PartView{:part, Whole}(Whole(42, 42.0f0))`
> - therefore, no methods involving `Part` can be called:  
> `inc(p::Part) = p.a += 1`  
> `julia> w.part |> inc`  
> ERROR: MethodError: no method matching inc(::PartView{:part, Whole})`

This seems very similar to how views work on vectors:

```julia-auto
f(v::Vector) = sum(v)

v = collect(1:10)
vv = view(v, 2:3)

f(v) # 55
f(vv) # ERROR: MethodError: no method matching f(::SubArray{Int64, 1, Vector{Int64}, Tuple{UnitRange{Int64}}, true})

```

For views, the solution is typically to use `AbstractVector` rather than `Vector` for dispatch:

```julia-auto
f(v::AbstractVector) = sum(v)

```

Perhaps `Part` and `PartView` could similarly both be made to subtype `AbstractPart`. I know nothing about macros though, so no idea how to accomplish this.

---

<div class="post-metadata">

**Author:** ![PatrickHaecker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/patrickhaecker/32/222891_2.png) [@PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker)\
**Post date:** [January 30, 2025, 5:17pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/36 "2025-01-30T17:17:45Z")

</div>

> For views, the solution is typically to use `AbstractVector` rather than `Vector` for dispatch

Ahh, thanks a lot. @cstjean used an `AbstractElephant` and with that calling methods works:

```julia
abstract type AbstractPart end
mutable struct Part <: AbstractPart
    a::Float32
end
inc(p::AbstractPart) = p.a += 1

julia> w.part |> inc
43.0f0

```

So

> therefore, no methods involving `Part` can be called

becomes a “pro” point that

- methods involving `Part` can be called

However, `cons` gets the point

- `Part` must be changed in general to support method calls

The original idea was that `Part` can be used unmodified.

Nevertheless, with the ability to call methods, this gets much more useful and should now cover all the simple cases. It’s still not on the level of having something like this supported by the language itself. But it already fills a niche where in simple cases the performance with modifications should be superior to what is currently possible with immutable structs. I wonder whether a package might make sense until we (hopefully) will have language support for such a feature.

---

<div class="post-metadata">

**Author:** ![Janis\_Erdmanis](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/janis_erdmanis/32/10869_2.png) [@Janis\_Erdmanis](https://discourse.julialang.org/u/Janis_Erdmanis)\
**Post date:** [January 30, 2025, 8:29pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/37 "2025-01-30T20:29:13Z")

</div>

In your specific case, it may be cleaner to define `Union` type directly. It’s a matter of personal preference, I would personally define a macro for each inlined type like:

```julia
@Part == Union{Part, PartView}

```

that can be used in the function signatures so that they only need to be changed slightly. For example,

```julia
f(x::@Part) = 5

```

so one simply would need to add `@` in front of the types that support views.

---

<div class="post-metadata">

**Author:** ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)\
**Post date:** [January 30, 2025, 8:49pm UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/38 "2025-01-30T20:49:06Z")

</div>

> [@Janis\_Erdmanis](#):
>
> I would personally define a macro for each inlined type like:
> 
> ```julia
> @Part == Union{Part, PartView}
> 
> ```

Just a nitpick, but in this case using a macro is not necessary at all, just define a constant, like `const UPart = Union{Part, PartView}`.

---

<div class="post-metadata">

**Author:** ![PatrickHaecker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/patrickhaecker/32/222891_2.png) [@PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker)\
**Post date:** [February 2, 2025, 7:58am UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/39 "2025-02-02T07:58:40Z")

</div>

> [@foobar\_lv2](#):
>
> My proposal above would look like
> 
> ```julia
> struct FieldRef{T}
> owner::Any #for GC purposes only
> ptr::Ptr{T}
> end
> 
> ```

I am not sure, whether I understand the proposal correctly. Is your idea to only create a `FieldRef` object on demand when accessing `part` or to store that `FieldRef` permanently while `Whole` is alive?

Wouldn’t your proposal mean that we need the size of two pointers per inlined field when the purpose was to get rid of the (permanently stored) single unnecessary pointer (for coupled lifetimes)?

Assuming the `FieldRef` is created on demand, can’t we somehow put the memory offset into the type information similar (leaving the recursive discussion out for the moment) to your suggested `FieldRef{Whole, Part, :part1}` or even `FieldRef{Whole, Part, Val{:part1}}` so that `FieldRef` only has the pointer. Of course, the GC then would need to special case these objects, but we would be down at the minimum memory requirement.

---

<div class="post-metadata">

**Author:** ![PatrickHaecker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/patrickhaecker/32/222891_2.png) [@PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker)\
**Post date:** [February 3, 2025, 6:29am UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/40 "2025-02-03T06:29:01Z")

</div>

I wonder whether we can reverse [the approach from @foobar\_lv2](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/24) and mix in [ideas from @cstjean](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/26).

In `foobar_lv2`’s approach we have the `FieldRef` which needs the new `interior_pointerset` to apply the GC write-barrier on `Whole` although `Part` is accessed. So we effectively access `Part` and link to `Whole`. This is necessary, for example, if a method creates `Whole` (together with `Part`) but only returns a reference to `Part`. Then `Whole` is no longer on the stack (or a global) and the GC would otherwise delete `Whole` (including `Part`) while `Part` is still accessible.

In `cstjean`’s approach all fields of the substructs are merged into a superstruct. This elegantly solves the lifetime issue, but [brings a couple of problems](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/34) with it.

In this third idea we could automatically create a new type for the inlined `Part` field. Let’s call it `Part{Whole, :part}` and the created object `pw`. Importantly `pw` would have _the same memory address as `Whole`_ (similar to `cstjean`’s approach). So we would have `pw` as an alias of `w` (the object of `Whole`) in terms of memory addresses. For that to work, `Part{Whole, :part}` would only have the fields of `Part` defined and would contain gaps where `Whole` has other fields not part of `Part`.

Additionally, we would (at least conceptually) automatically define all methods involving the type `Part{Whole, :part}` to call the corresponding method with `Part` (with object `p`) where `p`’s address is effectively calculated by `pointer_from_objref(w) + fieldoffset(Whole, :part)`.

The advantage compared to `foobar_lv2`’s approach would be that no explicit GC write barrier is needed, as the write barrier would be implicit as `pw` is on the stack and therefore the GC would think that `w` is on the stack (as it is aliased).

The disadvantage is that we either modify the stack (two calls instead of one) or we would need some special handling of this call (which could again be a GC write barrier and some kind of `fieldref` call) and we would need to see whether these kind of types would really need to be created or could be handled implicitly.

I am not convinced, that this idea is better than `foobar_lv2`’s. But as it looks like another possible implementation, I wanted to bring it into the discussion.

---

<div class="post-metadata">

**Author:** ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)\
**Post date:** [February 3, 2025, 6:31am UTC](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190/41 "2025-02-03T06:31:45Z")

</div>

> [@PatrickHaecker](#):
>
> Does not support getting the memory address  
> `julia> w1.part |> pointer_from_objref`  
> `ERROR: pointer_from_objref cannot be used on immutable objects`

I’m not quite up-to-date with the C FFI, but couldn’t we write a method of `pointer_from_objref(::ElephantView)` that returns, a pointer to `C.elephant`? AFAICT `Forest` has the same layout as the corresponding C struct.

> [@PatrickHaecker](#):
>
> But it already fills a niche where in simple cases the performance with modifications should be superior to what is currently possible with immutable structs. I wonder whether a package might make sense until we (hopefully) will have language support for such a feature.

I wouldn’t start a package like this by myself, but if you want to start from the above code (or some other implementation), I’m down to co-maintain. Alternatively, I could put it inside of QuickTypes.jl, where it’s somewhat thematically relevant.

[Previous page](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190.md?page=1)

[Next page](https://discourse.julialang.org/t/julep-efficient-hierarchical-mutable-data/125190.md?page=3)
