-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Function composition challenge for type system #10247
Comments
This seems really interesting and shows that some typings are not that easy, especially when it comes to pure functional programming. |
@yahiko00 thank you for the feedback. It's a good point about readability, however I didn't tried to find great syntax by now and we'd do it later if it lands. This proposal is purely for discussion and attempt to express really complicated functional concepts. I try to address the complexity of type flow when it comes to generalization. We can capture types in generic fashion but cannot catch generics themselves. |
One idea (relate to #10215) is to have the ability to describe the functional contract into two pieces: // Pseudo code !!!
function wrap<T extends Function<Args, Ret>>(fn: T): PromiseLike<Ret>; That may allow us to declare types that override either |
@unional yes, that might work if |
Yes, which is essentially what's your OP is about, right? 👍 |
However, should the function aware the generic parameters to begin with? If the argument is a function, user can always have a reason to pass in a function with generics. So it think this problem should not be tackled by "adding generic constructs" to capture it, but "allowing argument to capture generic from 'function with generics'". Just thinking out loud. EDIT: Of course, this may be the end goal and what you are describing is how to declare that explicitly. 🌷 |
I'm pretty sure, it should. So let's say we take your example: // Pseudo code !!!
//function wrap<T extends Function<Args, Ret>>(fn: T): PromiseLike<Ret>;
// I modified it to what I think you tried to express, using variadic kinds
function wrap<T extends Function<...Args, Ret>>(fn: T): (...Args) => PromiseLike<Ret>; And pass that function function identity<T>(x: T): T { return x; }
const wrappedIdentity = wrap(identity); // what would the type of wrappedIdentity?
// and even if we pass generic arguments explicitly
wrap<T, T>(identity); // what is the T here? My point is that currently we only have first level of generalization, where function can abstract from concrete types it operates with. And this level cannot generalize itself, because it operates with concrete type. I am proposing second level, where the first level's generalization can be generalized :) You may ask, would we need third level? I'm pretty sure we wouldn't, because capturing all the generic abstraction into a generic parameter would be able to capture on the same level. |
Composition and currying are the cornerstones of functional programming, and both quickly break down when used with generics -- see my linked thread listing several related issues filed at
It can be expressed pretty succinctly. My minimum reproduction might help there:
I feel you there, but I imagine most users won't have to run into this. I don't mind leaving the hard stuff here with definition writers of FP libs, so that users can just use the functions. I think for some functions the type definitions can definitely be harder to understand than just explanations. And I think that's fair, because those are aimed at filling distinct purposes: explanations are for humans, while type definitions in the first place need to enable proper inference about return types, which admittedly can be tough. Take, say, my For capturing generics, I like the idea of using a spread as in the variadic kinds proposal you mentioned, but in his So we'd have, e.g. for 1 function, 1 parameter (skipping variadics to generalize those to keep it simple), Points I'm thinking about:
What's your take there, @Igorbek, also in relation to your originally proposed notation here? Edit: added an alternative. |
See #9949 which uses I would rather that TypeScript inferred the genericity of an expression without all the extra syntax, the same way that Haskell and some other functional programming languages do. To take this example from the OP: const wrap = f => (a, b) => f(a, b);
const pair = (a, b) => [a, b]; with types const wrap: <A, B, C>(f: (a: A, b: B) => C): (a: A, b: B) => C;
const pair: <T>(a: T, b: T) => [T, T]; (ignore that When TypeScript sees TypeScript could just carry the type parameters forward without any new syntax, so instead of filling |
@jesseschalken: Yeah, I'd take that any day. AFAIK the TS team would also prefer proposals that don't add further syntax (which just keeps adding up), so yours should be preferable. Edit: fixed link |
isn't it just common sense? one can't just "resolve" unspecified type parameters by dang, i am so angry |
I hope we could get any comments on this by a member; I haven't really looked at the TS code-base much, and would have use for pointers on where one might begin in order to investigate this. This |
from my experience it takes a full time job to keep up with the TS team |
Yeah, I'd definitely take your word for that. In my attempt to type Ramda, I currently have a few fixes/features I'm very much looking forward to, but I find it frustrating to see that the team has a thousand+ outstanding issues to deal with, with threads ending up for quite a while unanswered, or labeled with "accepting PRs" while barely any outsider would even have a clue on where to start. I understand the TS team's efforts can only scale so far, especially with so few of them and so many of us, and we can't really expect to be served by them on our whims, but I wish in order to deal with that situation they'd further empower the community on how to get started with fixing things by themselves. As it stands, I'd dive into the compiler folder to be greeted with a couple super-long files without much of an explanation of what the file's in charge of. For all I know a file name like e.g. But if things wouldn't improve much even if you do actually manage to make a PR, that does sound worse. :( |
@tycho01 You can take a look at #9949 and specifically at #9949 (comment). I'd like to have this feature implemented as well, so I took a stab at it and made some progress. I'll come back to my fork this week and improve the unification. Currently a lot of tests break, some because the compiler infers the functions correctly but others are simply bugs. If I manage to get it working I'll submit a PR. I hope this will get the ball rolling. PS: I think #9949 is a prerequisite for Higher Kinded Types (#1213). The variadic types in this issue are too much of a stretch for an initial version |
FWIW, Flow seems to support this just fine. const wrap = <A,B,C>(f:(A,B)=>C):((A,B)=>C) => (a:A,b:B) => f(a,b);
const pair = <T>(a:T,b:T): [T, T] => [a,b];
const needs_number = (x:number):void => {};
needs_number(wrap(pair)('a','b')[0]);
It wouldn't be able to get that error without the generic for Another example: const flip = <A,B,C>(f:(A,B)=>C):((B,A)=>C) => (b:B,a:A) => f(a,b);
const pair = <A,B>(a:A,b:B):[A,B] => [a,b];
pair(1,'1')[1].charAt(0); // ok
pair(1,'1')[0].charAt(0); // error
flip(pair)(1,'1')[1].charAt(0); // error
flip(pair)(1,'1')[0].charAt(0); // ok
flip(flip(pair))(1,'1')[1].charAt(0); // ok
flip(flip(pair))(1,'1')[0].charAt(0); // error
Again, it wouldn't be able to get those errors without the generic for Try it here. I'll add that to the long list of things Flow gets right which TypeScript doesn't, along with variance (even for properties). The TypeScript team has said repeatedly that their objective is not complete strictness, correctness and reliability, but that is the objective of Flow. For those who need that, like myself, it seems Flow is your real home. |
@jesseschalken Well, to be honest Flow is not that good either. Every time I collect stamina and frustration I try it and get disappointed pretty quickly. It has far more questionable decisions than TypeScript has and the dev experience / community are nowhere near. For me a better goal is focusing on making TS better. |
And here I was l like, "time to ask Angular to switch!". But okay, guess we're technically a duplicate of that 9949 thread as well. :P @gcnew: mad props for actually figuring out where to start on this... that's already pretty amazing! |
I was like, well, I imagine it wouldn't have been erased intentionally! @gcnew: how were you testing those? I'd be curious to try as well. I tried setting my VSCode |
@tycho01 Yes, you have to PS: I think I've fixed the old compose problem, but now I'm facing a much bigger one. I lied to the compiler that one of the two functions is not generic and its types should be treated as rigid. That's why when its type parameters should be substituted they are not and the inference becomes wrong afterwards. See the updated examples. There are other problems as well. I'm thinking of workarounds but I'm not sure whether with the current inference model it can actually be made to work. |
I think this just sums it up perfectly. It would probably require some sort of lazy type resolution. |
related to #15680 |
looks like the same issue as #9366. |
|
See https://github.com/reactjs/redux/blob/v3.7.0/index.d.ts See reduxjs/redux#1936 (comment) See Function composition challenge for type system microsoft/TypeScript#10247 See microsoft/TypeScript#9949
okay, I feel you there now. |
I'm not quite sure if the type system bug that I've found is the same as the one described in this issue. If I use the Angular
It produces the compiler error:
As you can see in the source of the EventEmitter class, it is derived from However, if I change the declared type of my Works:
Works
Is this the same bug as described here or should I file a new issue? |
@Igorbek Is this still a issue? Did I miss something? |
How would you type the following JavaScript function?
Obviously, the naive typed declaration would be:
But it does not really describe its contract. So, let's say if I pass generic function, which is absolutely acceptable, it's not gonna work:
It's clear that
f1
must be<T>(a: T, b: T) => T
, but it's inferred to be(a: {}, b: {}) => {}
.And that's not about type inferring, because we wouldn't be able to express that contract at all.
The challenge here is that I want to be able to capture/propagate not only the generic parameters themselves, but also their relations. To illustrate, if I add an overload to capture that kind of signatures:
then, I still wouldn't be able to express
Because then I would need to add overloads of unbounded number of type compositions, such as
[T]
,T | number
,[[T & string, number], T] | Whatever
... infinite number of variations.Hypothetically, I could express with something like this:
but it doesn't solve the problem either:
F
isn't constrained to be requiring at least 2 argumentsF extends (a: {}, b: {}) => {}
would work, but doesn't currently, because it collapsesF
to(a: {}, b: {}) => {}
F
; see an example belowSo then we come to more complicated things:
How to express that in the type system?
A reader can think now that is very synthetic examples that unlikely be found in real world.
Actually, it came to me when I tried to properly type Redux store enhancers. Redux's store enhancers are powerful and built based on function compositions. Enhancers can be very generic or can be applicable to specific types of dispatchers, states etc etc. And more importantly they can be constructed being detached from specific store. If the issue falls to discussion I will provide more details of why it's required there.
So where's the proposal?
I've been thinking of that for awhile and haven't came out with something viable yet.
However, this is something we can start with:
Of course, ignoring the syntax, here's what was introduced:
~G
is generic set of unbinded (no such word?) generic parameters with their constraints. Since it's not the same as generic type parameter, I've marked it with~
. So than it's applied as<...G>
that means that set becomes set of generic parameters. For example~G=<T, R extends T>
, so then<...G>=<T, R extends T>
.Ʀ<T1, T2, R, ~G>
(maybe syntaxƦ<T1, T2, R, ...G>
would make more sense, btw) is a relation betweenT1
,T2
,R
,~G
. It is another kind of generic information. It could be a set of relations, such asT1=number
,T2=string
,R=T1|T2=number|string
. Important here, is that relations can introduce new names that can be used as generic parameters, and also, they can reference existing type parameters from enclosing generic info.Probably, examples could help to understand what I'm trying to say:
Simpler example from the beginning:
Ok, guys, what do you think of this?
The text was updated successfully, but these errors were encountered: