Skip to content
This repository has been archived by the owner on Oct 5, 2021. It is now read-only.

Suggestion: Use Proxies to detect higher-order types. #57

Open
jack-williams opened this issue Apr 1, 2018 · 0 comments
Open

Suggestion: Use Proxies to detect higher-order types. #57

jack-williams opened this issue Apr 1, 2018 · 0 comments
Labels
discussion For direction and implementation discussions enhancement New feature or request help wanted Extra attention is needed

Comments

@jack-williams
Copy link

jack-williams commented Apr 1, 2018

Aim: Discover higher-order types using proxies that wrap and monitor values at run-time.

Example

function foo(f, x, y)  {
    return f(x.g,x.a) * (y ? -1 : 1);
}
let f = (g,x) => g(x*10);
foo(f, {g: x => x*2, a: 10}, true);
// The example gist will report that foo has the type:
// (((number) => number, number) => number, { g : (number) => number, a : number }, boolean) => number

It is not possible to get the type of f at the call to foo, you have to monitor f as it gets called. Proxies can be used to implement 'type catching`. The same method can be used for objects. While it is possible to traverse objects when you encounter them there are some issues: 1) they might be very large, 2) extra traversals can cause side-effects, 3) it wont catch any updates to the object inside the function. Proxies can be used to lazily monitor object access.

Background I did some research that involved using Proxies to monitor JavaScript libraries with TypeScript definition files. The are some clear similarities with what we did and TypeWiz. The main difference is that we didn't care about collecting and outputting types, only checking that the library matched the definition. The same technique could be used to collect types in general.

Implementation Here is a gist that shows the basic idea (I hacked this together, consider yourself warned!). The part that concerns proxies:

function collectType<T>(v: T, register: (t: Type) => void): T  {
    if(typeof v === "function") {
        return makeFunctionCollector(v,register);
    }
    if(typeof v === "object") {
        return makeObjectCollector(v,register);
    }
    register(typeof v as any);
    return v;
}

function makeFunctionCollector<T extends Function>(v: T, register: (t: Type) => void): T {
    const funType = makeFunctionType();
    register(funType);
    return new Proxy(v,{
        apply(target: T, thisArg: any, args: any[]) {
            const callInfo = makeCallType();
            funType.calls.push(callInfo);
            const wrappedArgs = args.map(v => collectType(v,t => callInfo.args.push(t)));
            const result = Reflect.apply(target,thisArg,wrappedArgs);
            return collectType(result, t => callInfo.ret = t);
        }
    });
}

function makeObjectCollector<T extends Object>(v: T, register: (t: Type) => void): T {
    const objType = makeObjectType();
    register(objType);
    return new Proxy(v,{
        get(target: T, prop: string, receiver: any) {
            return collectType(Reflect.get(target, prop, receiver), t => {
                if(!objType.props[prop]) {
                    objType.props[prop] = []
                }
                objType.props[prop].push(t);
            });
        },
        set(target: T, prop: string, receiver: any, val: any) {
            return Reflect.set(target, prop, receiver, collectType(val, t => {
                if(!objType.props[prop]) {
                    objType.props[prop] = []
                }
                objType.props[prop].push(t);
            }));
        }
    });
}

When we want to collect a type, if we encounter an object or function, we replace them with a proxy that recursively collects types when called or accessed. The register callback is used to store the type information as we collect it (the full gist has more details).

Issues: Proxies are known to change the semantics of programs, they have a different identity to the object they wrap: this can affect equality tests. This shouldn't be a significant problem in this context because TypeWiz is not being run in production, and the types should still be valid even if different control flow paths are taken.

Conclusion I hope this is at least interesting, even if it doesn't get taken on board. The project seems really interesting so I'll be keeping an eye out regardless.

All criticism and discussion welcome.

@urish urish added enhancement New feature or request help wanted Extra attention is needed discussion For direction and implementation discussions labels Apr 1, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
discussion For direction and implementation discussions enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants