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> 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.
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