COMMUNITY

Field access operator overloading w/ cast?

I saw the field access operator overload in Haxe 4 and wanted to try it, so I built a quick test. I ran into a ‘quirk’ where casting to a typedef on top of the abstract causes the field access overloads to disappear. I think this is probably how the type system is designed, but maybe someone can point out a way to layer a typedef on top of an abstract?

Here’s an example:

class FieldAccessTest  {
    static function main() {
        // Create a generic 'customer' and assign some values
        var customer = Item.create("customer");
        customer.firstName = "Jon";
        customer.lastName = "Doe";

        // 'type' is hidden by the field accessor (good!)
        trace(customer.type);
        
        // but we can cast and get at the internal type, neat!
        var gs:GenericStore = cast customer;
        trace(gs.type);

        // we do not have code-completion or type safety yet, so lets add a typedef to make this 'customer' look right to the compiler
        // cast to a typedef for code-completion/type safety to kick in
        var castedCustomer:Customer = cast customer;
        // dang, we loose the abstract field read/write!
        trace(castedCustomer.firstName);
    }
}

class GenericStore {
    public var type:String;
    var fields = {};

    function new(type:String) {
        this.type = type;
    }
}

@:access(GenericStore)
abstract Item(GenericStore) from (GenericStore) {
    @:op(a.b)
    public function fieldWrite<T>(name:String, value:T) Reflect.setField(this.fields, name, value);
    @:op(a.b)
    public function fieldRead<T>(name:String):T return Reflect.field(this.fields, name);

    public static function create(type:String):Item return new GenericStore(type);
}

typedef Customer = { 
    firstName:String, 
    lastName:String 
};

When you cast a variable of some type into another type that isn’t related in structure (like, Customer doesn’t extend Item or anything like that), obviously you lose every method that isn’t defined in the destination type. You’re not casting “over” an abstract, you’re casting “out” of it.

Methods aren’t bundled inside an object, they’re a property of the type. By using cast, you’re telling the compiler “treat this object like an instance of type Customer”, and since Customer doesn’t have operator overloading because it’s not an abstract, then neither do you. That’s why it’s called unsafe casting ! Try doing cast(customer, Customer) (ie the safe cast variant) and I’m pretty sure it won’t let you.

You say you want to do this because of type safety, but because of that Reflect usage you cannot possibly have type-safety here (or completion), since you’re creating fields of arbitrary type, and at runtime at that.

I’m not sure what would be a nice way of doing that. You either need to rethink the way you’re handling things, or definitively give up on type safety for that specific part of your program.

I found a relatively easy/safe solution is to create a new abstract with the fields I want to have type safety on. It ends up looking like this:

@:access(GenericStore)
abstract Customer(GenericStore) {
    public var firstName(get,set):String;
    public var lastName(get,set):String;

    function get_firstName() {
        return Reflect.field(this.fields, "firstName");
    }
    function set_firstName(val:String) {
        Reflect.setField(this.fields, "firstName", val);
        return val;
    }

    function get_lastName() {
        return Reflect.field(this.fields, "lastName");
    }
    function set_lastName(val:String) {
        Reflect.setField(this.fields, "lastName", val);
        return val;
    }
}

Now I can do var castedCustomer = cast(customer, Customer); and it works great. The class could be built by a macro that reads in typedefs. I was hoping to see a way to layer a typedef on top of the abstract, but this works too.