The is "Expando objects" in Julia?

In C# is possible to have a dynamic object with fields defined at runtime using expando objects. Example:

dynamic contact = new ExpandoObject();
contact.Name = “Patrick Hines”;
contact.Phone = “206-555-0144”;

As you can see, the contact object is dynamic, in the sense we can set fields in the object, without declaring previously the structure of the object. I understand in Julia we can have a parameter/variable without declared type, as in:

function setProp1(obj) obj.prop1=1 end

But, as i understand it, the obj object must be created from a type with a field prop1. If prop1 is not declared somewhere as a field, setProp1 can’t set dynamically the field prop1. If i want full dynamism, and i want not to worry for performance, there is a way to create “expando objects” in Julia?

Use Dict.

6 Likes

No, this isn’t possible with Julia types. The closest thing would be a dictionary from symbols to values, like:

contact = Dict{Symbol, String}()
contact[:Name] = "Patrick"
contact[:Phone] = "123-456"

If you really, really, really want to access the fields with dot notation, you could write a macro that transforms:

@expando contact.Name = "Patrick"

into the appropriate dictionary syntax:

contact[:Name] = "Patrick"

but this will make your code harder to follow (and won’t improve performance).

3 Likes

Also, I can’t find much information about benchmarking ExpandoObject, but this blog suggests that their performance is similar to a dictionary anyway. That suggests that you won’t lose performance by switching to using a Dict in julia (except to the extent that Julia and C# may implement their dictionaries differently).

Thanks for the detailed answer. Yes, i’m thinking in dot notation. And sorry for my English, i’m thinking in Dict semantic using dot notation, where i’m not worried by the performance.

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