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.
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))
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
┌ 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:
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
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