The is "Expando objects" in Julia?

I played with the “expando object” idea and found a tiny way (≈30 lines) to get a Python-like instance experience in Julia: dynamic attributes + dynamic “methods” with obj.f(args...) syntax, by storing everything in an EasyConfig.Config (nested dict) and auto-binding self via getproperty.

using EasyConfig

@kwdef mutable struct DynObj
    cfg::Config = Config()
end

_wrap(c::Config) = DynObj(c)
_wrap(x) = x

function Base.getproperty(o::DynObj, name::Symbol)
    name === :cfg && return getfield(o, :cfg)
    c = getfield(o, :cfg)

    if haskey(c, name)
        v = c[name]
        return if v isa Function
            (args...) -> v(o, args...)
        else
            _wrap(v)
        end
    else  # create intermediate nodes to allow o.a.b.c = 1
        child = Config()
        c[name] = child
        return DynObj(child)
    end
end

function Base.setproperty!(o::DynObj, name::Symbol, v)
    name === :cfg && throw(ArgumentError("field :cfg is reserved"))
    getfield(o, :cfg)[name] = v isa DynObj ? v.cfg : v
    return v
end

Base.propertynames(o::DynObj; private=false) = private ? (:cfg, keys(o.cfg)...) : (keys(o.cfg)...,)
Base.delete!(o::DynObj, k::Symbol) = (delete!(o.cfg, k); o)

Quick demo:

o = DynObj()
o.x = 1
o.msg = "hello"

o.add = (self, y) -> self.x + y
o.greet = self -> "greet: $(self.msg)"

o.add(5)     # 6
o.greet()    # "greet: hello"

o.sub.name = "child"
o.sub.say = self -> "I am $(self.name)"
o.sub.say()  # "I am child"

Caveats:

  • performance is not the goal here: this is fully dynamic (Dict + Any + closure binding), so it’s mostly for experimentation / prototyping.
  • reading a missing property creates an empty node (nice for o.a.b.c = ..., but can hide typos).
  • “methods” here are just function values stored in a dict; the auto-binding returns a closure, so it bypasses Julia’s normal multiple dispatch / method tables and may allocate.

Ref: ANN: EasyConfig.jl: An easy-to-write JSON-ish data structure