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