Julep: Efficient Hierarchical Mutable Data

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
3 Likes