Markus Sailor
Markus Sailor

Reputation: 21

D Metaprogramming like Python descriptors?

I'am trying to translate a part of my framework from Python to D and being struggling with one detail: But first I have to expand the scope. The component is like an ORM. My users define classes, which instances will be persistent in a database. The user-API should be as simple as possible. For example

class Person:
  identity _id,
  string name,
  date birthdate,
  ...

class Car:
  indentity _id,
  string ident,
  int power,
  Person owner
  ...

myCar = Car.load(ident="DEAD-BEAF")
print myCar.power
print myCar.owner.name

The load-function loads the instance data from the database. But the loading of the owner of the car should be deferred until the instance is used, because most of the application works on cars, the owners are uses rarely.

In Python I can implement this behaviour using the Descriptor-Protocol. I have a set of "field"-classes, which are descriptors. For example

class StringField(object):

    def __get__(self,obj,objtype):
        # get the value from obj

    def __set__(self,obj,value):                    
        # set value in obj

The EntityClass has an appropriate metaclass, which wires the needed connections. The user defines in Python:

class Person(EntityClass):      
    name = StringField()
    birthdate = DateField()
    ...

class Car(EntityClass):
  ident = StringField()
  power = IntField()
  owner = RelationField(Person)
  ...

and uses the classes:

myCar = Car.load(ident="DEAD-BEAF")
print myCar.power      (#1)
print myCar.owner.name (#2)

Under the hood the call to myCar.power is expanded to

Car.power.__get__(myCar,Car)

If I load a car from the database, I only load the owner-Id. If one uses the owner

theowner = myCar.owner

I can load deferred the Person instance from the database

class RelationField(object):

    def __get__(self,obj,objtype):
        if not instance in obj.cache:
            load instance
            add instance to obj.cache
        return instance from obj.cache

Translating the ORM to D I have tried different implementations. For simple basetypes it is very simple to use User Defined Attributes (UDA) in conjuction with templates and the unified call syntax:

struct Persistent {};

save(T)(ref T obj)
  {
  ...
  }

T load(T)(...) 

class Person
  {
  @Persistent string name;
  @Persistent Date birthday;
  ...
  }

class Car
  {      
  @Persistent string ident;
  @Persistent int power;
  @Persistent Person owner; //???
  ...
  }

auto myCar = load!Car(...);
writeln(myCar.power);
writeln(myCar.owner.name)

This API is as simple as the Python-API, but I have no idea how to implement the deferred loading of owner. What I need is to replace the owner-member by a property function, but I do not known how to do this using compile time meta programming. So how gan this be done? Or is there an idiomatic way to do?

Upvotes: 2

Views: 184

Answers (1)

Tamas
Tamas

Reputation: 3442

You can use opDispatch. The property name is available compile time, so you can handle them with a generic code, or you can also create specialisations, like the code below does, using if after the template line.

struct Person {
    int id;
    string name;
}

Person[int] person_by_id;

struct Car {
    int id;
    int power;
    int owner_id;

    // https://dlang.org/spec/operatoroverloading.html#dispatch
    template opDispatch(string property)
    if (property == "owner")
    {
        @property ref Person opDispatch() {
            return person_by_id[owner_id];
        }
    }
}

void main() {
    auto p = Person(1, "Joe");
    person_by_id[p.id] = p;
    auto c = Car(123, 900, 1);

    assert(c.owner.name == "Joe");
    c.owner.name = "Average Joe";

    assert(person_by_id[1].name == "Average Joe");
}

The opDispatch template can be generated by a mixin template so the user should write something like this:

struct Car {
    int id;
    int power;
    int owner_id;
    mixin addMyHandlers!Car;
}

Here the addMyHandler mixin template should generate the opDispatch function above, using introspection of the passed-in struct. If you prefer to keep the struct nice and clean, you can generate functions outside the struct, and take advantage of the Uniform Function Call Syntax (UFCS):

struct Car {
    int id;
    int power;
    int owner_id;
}

@property ref Person owner(const ref Car car) {
    return person_by_id[car.owner_id];
}

This owner function can also be generated using a mixin template, using introspection on Car to investigate what to generate. (Similarly to your python code.)

Upvotes: 2

Related Questions