You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bad type reduction between two generic functions #17863
Expected behavior:
Both g1 and g2 should have the same type <A, B>(f: (a: A) => B): () => B | undefined (<A, B>(f: (a: A) => B): () => B) | (<A, B>(f: (a: A) => B): () => B | undefined).
Edit:
Note, if unification is being used this should be an error, as B has to be equal to B | undefined. On the other hand, the signature of f2 is in some sense "more generic" than f1's, so it could be the answer. Indeed, the assignability relations work as expected:
consth1: typeoff1=f2;// Error as expectedconsth2: typeoff2=f1;// it's fine
Edit 2:
Upon further investigation, it seems that unification is the only sensible approach. I.e. both f1 and f2 should be assignable to one another for reduction to take place. If either one is not, producing a union is the only safe option. See #17863 (comment)
The issue here is that we still use type parameter erasure when comparing signatures for identity. Once A and B are replaced with any (causing undefined to be subsumed and disappear), the signatures appear identical.
in my opinion function signatures should be banned from being manipulated like being merged in unions, making it right is a can of worms and honestly it's not at all intuitive, figuring out variance compared to that is a walk in the park
no seriously, for a start, try to come up with a simple matchParameters function as follows:
consttypes=['never','any','void','primitive','object','array','tuple','union','intersection','signature','generic'];types.map(one=>types.map(another=>console.log(matchParameters(one,another)));functionmatchParameters(one: string,another: string): string{// please put your bright ideas right here}
when done, try matchResultTypes, matchingOnlyOneParameterWhileAnotherOneIsMissing, matchParameterAgainsMultipleOverloads
I do agree that the current reduction logic is incorrect, but function unions appear naturally when you choose and return one function between two available options. I stumble upon them every now and then while working with higher order functions that return a generic "default" handler.
I've hit this specific problem while using my parser combinator library. A simplified version is as follows:
// A parser is function that takes an input stream and returns either an error,// or a tuple of the parsed value and the rest of the streaminterfaceParser<E,T>{(input: string): [T,string]|E}// Combines parsers `p1` and `p2`, such that they are sequentially invoked and their// results are glued together by `f`. If `p1` fails, neither `p2` nor `f` is invoked.declarefunctioncombine<E1,E2,A,B,C>(p1: Parser<E1,A>,p2: Parser<E2,B>,f: (a: A,b: B)=>C): Parser<E1|E2,C>;// A simple look-ahead combinator. If `p1` fails, the result is `Parser<never, undefined>`,// or else it acts like `combine`.declarefunctiontrai<E1,E2,A,B,C>(p1: Parser<E1,A>,p2: Parser<E2,B>,f: (a: A,b: B)=>C): Parser<E2,C|undefined>;// Bad: `... => Parser<E1 | E2, C>` <-- what about `undefined` ?consttest1=Math.random() ? combine : trai;// Bad: `... => Parser<E2, C | undefined>` <-- what about `E1` ?consttest2=Math.random() ? trai : combine;
I do agree it would have been best if no reduction were performed at all, as both of the above inferences are wrong for different reasons.
PS: If unifications had been used, we would have known that when a reduction had been found, it would have been correct.
Aleksey-Bykov My initial post before editing suggested that the snippet should produce an error. After that I had a thought that "assignability" may be the answer, but as you've discovered that's a treacherous path.
Now I'm completely convinced the OP's snippet should either be an error or produce a union. In both cases the type of g1 and g2 should be the same. Unfortunately, however, they are two distinct reduction results as of now.
The obvious solution is to use unification (in the Hindley-Milner sense). If there is no unifier, we can either issue an error or produce a union.
if it is a practical question you should wrap your functions in Either, if it is a theoretical question, i am afraid there is no reliable answer other than showing an error
There seem to be two code paths handling functions / consts with function signature. The code below works safely.
declareconstc1: <A,B>(f: (a: A)=>B)=>()=>B;declareconstc2: <A,B>(f: (a: A)=>B)=>()=>B|undefined;// r1 and r2 have the same type:// (<A, B>(f: (a: A) => B): () => B) | (<A, B>(f: (a: A) => B): () => B | undefined)declareconstr1: typeofc1|typeofc2;declareconstr2: typeofc2|typeofc1;
Is it expected that consts and functions behave differently?
Activity
ahejlsberg commentedon Aug 18, 2017
The issue here is that we still use type parameter erasure when comparing signatures for identity. Once
AandBare replaced withany(causingundefinedto be subsumed and disappear), the signatures appear identical.zpdDG4gta8XKpMCd commentedon Aug 28, 2017
in my opinion function signatures should be banned from being manipulated like being merged in unions, making it right is a can of worms and honestly it's not at all intuitive, figuring out variance compared to that is a walk in the park
zpdDG4gta8XKpMCd commentedon Aug 28, 2017
no seriously, for a start, try to come up with a simple
matchParametersfunction as follows:when done, try
matchResultTypes,matchingOnlyOneParameterWhileAnotherOneIsMissing,matchParameterAgainsMultipleOverloadsgcnew commentedon Aug 28, 2017
I'm not sure I'm following you.
I do agree that the current reduction logic is incorrect, but function unions appear naturally when you choose and return one function between two available options. I stumble upon them every now and then while working with higher order functions that return a generic "default" handler.
I've hit this specific problem while using my parser combinator library. A simplified version is as follows:
I do agree it would have been best if no reduction were performed at all, as both of the above inferences are wrong for different reasons.
PS: If unifications had been used, we would have known that when a reduction had been found, it would have been correct.
zpdDG4gta8XKpMCd commentedon Aug 28, 2017
my point is that there is no such thing (it seems) as a unified signature produced out of the given ones
except those few trivial cases (all given sigratures are same or subtypes of the original)
this is why i don't understand how the problem you are facing can be possibly resolved in the language
it can be easily solved by wrapping your result functions and discriminating the wrappers prior to calling
gcnew commentedon Aug 28, 2017
Aleksey-Bykov My initial post before editing suggested that the snippet should produce an error. After that I had a thought that "assignability" may be the answer, but as you've discovered that's a treacherous path.
Now I'm completely convinced the OP's snippet should either be an error or produce a union. In both cases the type of
g1andg2should be the same. Unfortunately, however, they are two distinct reduction results as of now.The obvious solution is to use unification (in the Hindley-Milner sense). If there is no unifier, we can either issue an error or produce a union.
zpdDG4gta8XKpMCd commentedon Aug 28, 2017
if it is a practical question you should wrap your functions in
Either, if it is a theoretical question, i am afraid there is no reliable answer other than showing an errorgcnew commentedon Aug 28, 2017
There seem to be two code paths handling functions / consts with function signature. The code below works safely.
Is it expected that consts and functions behave differently?
RyanCavanaugh commentedon Sep 18, 2019
This has been fixed; the unions in the OP are no longer reduced.