New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: disallow intersection between function types #7494

Closed
danielearwicker opened this Issue Mar 12, 2016 · 5 comments

Comments

Projects
None yet
2 participants
@danielearwicker

The TS compiler steers clear of trying to generate code to resolve overloads at runtime, instead requiring the user to define one "catch all" implementation that contains whatever custom resolution logic is needed.

Yet for some generic function:

function merge<A, B>(a: A, b: B): A & B ...

The implication is that possibly both A and B are functions (unless there is a way to declare that one of them must not be a function?)

Which further implies that merge will be responsible for returning a single function that can dispatch to a or b depending on how it is called, without prior knowledge of the structure of the types A and B - the very same overload resolution problem that the compiler wisely refuses to touch with a barge pole!

An intersection between two function types implies a merging of two functions with different signatures into one function with a generally compatible signature. In effect this means forming an automatic overload-resolving function out of any two functions. This is a Hard Problem® when we have no idea what the two functions might be, as is the case when dealing with generic type parameters that happen to be functions. Shouldn't the compiler treat intersection between two function types as an error until such time as automatic overload resolution (somehow) becomes generically solvable at runtime?

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Mar 14, 2016

Member

It's somewhat counterintuitive, but we don't actually know when we produce an intersection type that this is the case.

Because we support infinitely-expanding self-referential types, the compiler is very carefully written to never greedily and recursively expand the members of an object (because for many types, this expansion never finishes). So when the type A & B comes into existence, we don't get its properties until we need to. Later when we get the properties of e.g. (A&B).x), we don't get its properties until we need to. And so on.

So even if this behavior was desirable (and it's arguable that it's not), it's not possible to implement it in a way that wouldn't be prohibitively expensive -- to detect this, we'd have to greedily expand all types until we hit some depth limit, and then stop.

Member

RyanCavanaugh commented Mar 14, 2016

It's somewhat counterintuitive, but we don't actually know when we produce an intersection type that this is the case.

Because we support infinitely-expanding self-referential types, the compiler is very carefully written to never greedily and recursively expand the members of an object (because for many types, this expansion never finishes). So when the type A & B comes into existence, we don't get its properties until we need to. Later when we get the properties of e.g. (A&B).x), we don't get its properties until we need to. And so on.

So even if this behavior was desirable (and it's arguable that it's not), it's not possible to implement it in a way that wouldn't be prohibitively expensive -- to detect this, we'd have to greedily expand all types until we hit some depth limit, and then stop.

@danielearwicker

This comment has been minimized.

Show comment
Hide comment
@danielearwicker

danielearwicker Mar 14, 2016

The problem only arises if the function is called, and at that point you are already doing the necessary resolving. Why not check it at the call site?

Anyway, if you have time it would be interesting/educational to hear the other arguments against this, besides compile time cost.

The problem only arises if the function is called, and at that point you are already doing the necessary resolving. Why not check it at the call site?

Anyway, if you have time it would be interesting/educational to hear the other arguments against this, besides compile time cost.

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Mar 14, 2016

Member

The problem only arises if the function is called,

This presumes that the merge definition never captures outer type parameters, which is not the case (e.g. by being inside a generic type declaration).

Consider something like this:

class Foo<T, U> {
  blah: {
    getThing(): T & U;
  }
}
var x = new Foo<() => void, () => number>();
// We only create the "invalid" type if this line exists
var y = x.blah.getThing();

Re: arguments against, you could easily imagine a function like this existing:

function merge<A extends () => number, B extends () => number>(a: A, b: B): A & B {
  function result() {
    return a() + b();
  }
  Object.assign(result, a);
  Object.assign(result, b);
  return <A&B>result;
}

function alpha() {
  return 0;
}
namespace alpha {
  export const A = 1;
}
function beta() {
  return 1;
}
namespace beta {
  export const B = 1;
}
var delta = merge(alpha, beta);
var y = delta.A + delta.B + delta();
// delta.A, delta.B, delta() are all OK
Member

RyanCavanaugh commented Mar 14, 2016

The problem only arises if the function is called,

This presumes that the merge definition never captures outer type parameters, which is not the case (e.g. by being inside a generic type declaration).

Consider something like this:

class Foo<T, U> {
  blah: {
    getThing(): T & U;
  }
}
var x = new Foo<() => void, () => number>();
// We only create the "invalid" type if this line exists
var y = x.blah.getThing();

Re: arguments against, you could easily imagine a function like this existing:

function merge<A extends () => number, B extends () => number>(a: A, b: B): A & B {
  function result() {
    return a() + b();
  }
  Object.assign(result, a);
  Object.assign(result, b);
  return <A&B>result;
}

function alpha() {
  return 0;
}
namespace alpha {
  export const A = 1;
}
function beta() {
  return 1;
}
namespace beta {
  export const B = 1;
}
var delta = merge(alpha, beta);
var y = delta.A + delta.B + delta();
// delta.A, delta.B, delta() are all OK
@danielearwicker

This comment has been minimized.

Show comment
Hide comment
@danielearwicker

danielearwicker Mar 15, 2016

Re: first example, that's exactly what I'm saying: it's the attempt to call that is the problem. That's where the error would be issued. If that line doesn't exist, there's no problem.

In the second example, the function signature is specified by the constraints, making it trivial and not the problem I'm talking about: the general case, where the function signatures are not constrained and it is impossible to implement a correspondingly general merge at runtime.

Re: first example, that's exactly what I'm saying: it's the attempt to call that is the problem. That's where the error would be issued. If that line doesn't exist, there's no problem.

In the second example, the function signature is specified by the constraints, making it trivial and not the problem I'm talking about: the general case, where the function signatures are not constrained and it is impossible to implement a correspondingly general merge at runtime.

@danielearwicker

This comment has been minimized.

Show comment
Hide comment
@danielearwicker

danielearwicker Mar 15, 2016

And extending 2nd example further, it actually has the same problem in that the constraints specify the minimum capabilities of the function types. Adding this to end of your snippet:

interface GetSetNum {
    (v: number): void;
    (): number;
}

const getset: GetSetNum = null; // obtain an overloaded func from somewhere

var f = merge(alpha, getset);

f(5); // lets me use merged function as a setter!

And extending 2nd example further, it actually has the same problem in that the constraints specify the minimum capabilities of the function types. Adding this to end of your snippet:

interface GetSetNum {
    (v: number): void;
    (): number;
}

const getset: GetSetNum = null; // obtain an overloaded func from somewhere

var f = merge(alpha, getset);

f(5); // lets me use merged function as a setter!

@Microsoft Microsoft locked and limited conversation to collaborators Jun 19, 2018

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