Skip to content

Instantly share code, notes, and snippets.

@freakboy3742
Created March 7, 2018 01:20
Show Gist options
  • Save freakboy3742/d98c513e92a8badcb632d38581ae54b1 to your computer and use it in GitHub Desktop.
Save freakboy3742/d98c513e92a8badcb632d38581ae54b1 to your computer and use it in GitHub Desktop.
Desperately Seeking JSusan...
class PyType {
constructor(name, bases, dict) {
this.name = name
this.bases = bases
this.dict = dict
}
__str__() {
return this.name
}
__call__(...args) {
console.log(`Create new <${this.name}> with ${args.length} args...`)
return `New <${this.name}> object created with ${args.length} args`
}
__getattribute__(attr) {
return this[attr]
}
__setattr__(attr, value) {
this[attr] = value
}
}
let descriptor = {
get: function(obj, attr) {
if (attr === Symbol.toPrimitive) {
console.log('get as str', obj)
console.log('__str__', obj.__str__)
return obj.__str__.bind(obj)
} else {
console.log(`Getting ${attr}`)
return obj.__getattribute__(attr);
}
},
set: function(obj, attr, value) {
console.log(`Setting ${attr} to ${value}`)
obj.__setattr__(attr, value);
return true;
},
apply: function(obj, that, args) {
console.log(`Calling object with ${args.length} arguments`)
return obj.__call__.apply(that, args)
},
construct: function(obj, args) {
console.log(`Constructing object with ${args.length} arguments`)
throw "Python objects are constructed by invocation"
}
}
export function tester() {
let mt = new PyType('MyType', ['base1', 'base2'], {'attr1': 1, 'attr2': 2})
let MyType = new Proxy(mt, descriptor)
console.log('mt = ', MyType)
console.log('mt.name = ', MyType.name)
console.log('mt.bases = ', MyType.bases)
console.log('mt.dict = ', MyType.dict)
console.log('mt.__str__() = ', MyType.__str__())
console.log('mt.__call__() = ', MyType.__call__())
// This line fails:
// Uncaught TypeError: MyType is not a function
// at Object.tester (test.js:69)
let obj = MyType(1, 2, 3, 4)
}
@freakboy3742
Copy link
Author

The question: What do I need to do to make MyType callable?

It appears that Proxy.apply is only available if the underlying object is already callable. I've seen any number of solutions to this problem, but they all seem to fail (a) because Babel has protections to specifically prevent the language quirk they're relying on, or (b) the attributes the PyType object added in the constructor aren't preserved.

I'm not bound to using ES6 class notation if that's the sticking point; if this result can be achieved using old-school prototype inheritance (or any other collection of hacks) I'm fine with that - as long as the functionality is preserved, and subclasses of PyType can be defined in ES6 notation.

Bonus question: How do I make line 58 print something actually useful in the Chrome console, rather than "Proxy(...)" - ideally, the result of invoking __str__?

@callahad
Copy link

callahad commented Mar 7, 2018

Bonus question: How do I make line 58 print something actually useful in the Chrome console, rather than "Proxy(...)" - ideally, the result of invoking str?

There isn't really a concept of __repr__ in browser DevTools; each browser implements its own formatters for various data types. However, you can supply a browser add-on that implements a Custom Formatter (or, for example, this one for immutable.js data structure). Unfortunately, that API is not yet supported in Firefox.

@funkybob
Copy link

funkybob commented Mar 7, 2018

Have you looked at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/apply ?

As to the bonus point, couldn't you intercept toString ?

@callahad
Copy link

callahad commented Mar 7, 2018

toString is a good idea, but it only gets called when the object is actually coerced into a string, not when logged directly. (E.g., the difference between console.log('mt = ', mt) and console.log('mt = ' + mt))... Still, might indeed be useful as a partial solution.

@swenson
Copy link

swenson commented Mar 7, 2018

I made some progress by extending Function twice-removed:

class Callable extends Function {
  constructor() {
    super('...args', 'return this.__call__(...args)');
    return this.bind(this)
  }
}


class PyType extends Callable {
    constructor() {
      super()
    }

    __str__() {
        return this.nname
    }

    __call__(...args) {
        print(this)
        this.nname = args[0]
        this.bases = args[1]
        this.dict = args[2]
        print(`Create new <${this.nname}> with ${args.length} args...`)
        return `New <${this.nname}> object created with ${args.length} args`
    }

    __getattribute__(attr) {
        return this[attr]
    }

    __setattr__(attr, value) {
        this[attr] = value
    }
}

Now it calls the __call__, but this is undefined within __call__ because Function is weird:

TypeError: Cannot set property 'nname' of undefined
    at __call__ (test.js:46:20)
    at Object.apply (test.js:19:29)
    at tester (test.js:78:15)

@swenson
Copy link

swenson commented Mar 7, 2018

(I got some of this info from https://stackoverflow.com/a/40878674 )

@raphamorim
Copy link

This is the simplest solution with the least amount of functionality, but it's guaranteed to work almost anywhere. Create a new function and clone an object's properties onto it.

class PyType {
    constructor(name, bases, dict) {
        this.name = name
        this.bases = bases
        this.dict = dict
    }

    __str__() {
        return this.name
    }

    __call__(...args) {
        console.log(`Create new <${this.name}> with ${args.length} args...`)
        return `New <${this.name}> object created with ${args.length} args`
    }

    __getattribute__(attr) {
        return this[attr]
    }

    __setattr__(attr, value) {
        this[attr] = value
    }

}

let descriptor = {
    get: function(obj, attr) {
        if (attr === Symbol.toPrimitive) {
            console.log('get as str', obj)
            console.log('__str__', obj.__str__)
            return obj.__str__.bind(obj)
        } else {
            console.log(`Getting ${attr}`)
            return obj.__getattribute__(attr);
        }
    },
    set: function(obj, attr, value) {
        console.log(`Setting ${attr} to ${value}`)
        obj.__setattr__(attr, value);
        return true;
    },
    apply: function(obj, that, args) {
        console.log(`Calling object with ${args.length} arguments`)
        return obj.__call__.apply(that, args)
    },
    construct: function(obj, args) {
        console.log(`Constructing object with ${args.length} arguments`)
        throw "Python objects are constructed by invocation"
    }
}

const ObjectCallable_handler = {
    get: function get(self, key) {
        if (self.hasOwnProperty(key)) {
            return self[key];
        } else { return self.__inherit__[key]; }
    },
    apply: function apply(self, thisValue, args) {
        return (self.__call__ || self.__inherit__.__call__).apply(self, args);
    }
};

function ObjectCallable(cls) {
    var p = new Proxy(function() { }, ObjectCallable_handler);
    p.__inherit__ = cls;
    return p;
}

export function tester() {
    let mt = new PyType('MyType', ['base1', 'base2'], {'attr1': 1, 'attr2': 2})
    let MyType = new Proxy(ObjectCallable(mt), descriptor)

    console.log('mt = ', MyType)
    console.log('mt.name = ', MyType.name)
    console.log('mt.bases = ', MyType.bases)
    console.log('mt.dict = ', MyType.dict)
    console.log('mt.__str__() = ', MyType.__str__())
    console.log('mt.__call__() = ', MyType.__call__())

    // This line fails:
    // Uncaught TypeError: MyType is not a function
    //   at Object.tester (test.js:69)
    let obj = MyType(1, 2, 3, 4)
}

@raphamorim
Copy link

raphamorim commented Mar 7, 2018

However, this approach will only answer your first question about callable classes. Probably you will get another error, but now with name reference. So here's a example about valid approach for deal with it:

var proxyAccount = new Proxy (ObjectCallable(new PyType('MyType', ['base1', 'base2'], {'attr1': 1, 'attr2': 2})) , {
    get: function (target, name, receiver) {
        console.log('get called for field: ', name);
        if(!target.hasOwnProperty(name))
        {
           // do I have to store `name` into global variable, to reference it in `apply trap?
           // methodName = name
            return new Proxy(target[name],this);
        }
        return Reflect.get(target, name, receiver);
    },
    apply: (target,receiver,args) => {
        console.log('methodName: ',  'I need methodName here. how do I get  -withdraw- here?');
        return Reflect.apply(target, receiver, args);
    }
});

proxyAccount(); 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment