-
Notifications
You must be signed in to change notification settings - Fork 12.8k
[WIP] Higher kinded types #23809
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
[WIP] Higher kinded types #23809
Conversation
I find that @masaeedu is an endless source of example code. Here's his GenericMaybe: // Based on provided sample by Asad Saeeduddin
// just :: a -> T a
// none :: T a
// match :: { "just" :: a -> b, "none" :: b } -> T a -> b
const GenericMaybe = ({ just, none, match }) => {
// Functor
// map :: (a -> b) -> T a -> T b
const map = f => match({ none, just: value => just(f(value)) });
// Monad
// of :: a -> T a
const of = just;
// chain :: (a -> T b) -> T a -> T b
const chain = f => match({ none, just: f });
return { map, of, chain };
};
// A concrete representation of a Maybe
{
const MAYBE = Symbol("maybe");
const just = value => ({ [MAYBE]: value });
const none = {};
const match = ({ just, none }) => maybe =>
MAYBE in maybe ? just(maybe[MAYBE]) : none;
const { map, chain, of } = GenericMaybe({ just, none, match });
// Examples
console.log([
map((x: number) => x + 2)(of(42)),
map((x: number) => x + 2)(none),
chain((x: number) => (x > 40 ? none : of(x * 2)))(of(42)),
chain((x: number) => (x > 40 ? none : of(x * 2)))(of(32)),
map((x: number) => none)(of(42)),
map((x: number) => of(x * 2))(of(42))
]);
} I might have to checkout your PR myself to test the limits. Stellar work so far though, if all those examples type correctly already. |
This works for me on your branch! interface Fix<F<_T>> {
unFix: F<Fix<F>>;
}
interface ListContainer<A> {
h: number;
rest: A;
};
type List = Fix<ListContainer>;
const nil: List = { unFix: (<ListContainer<never>>undefined) };
const y: List = {
unFix: { h: 4, rest: nil }
};
function arrayToList(xs: number[]): List {
return xs.reduce((rest, h) => {
return { unFix: { h, rest } }
}, nil);
}
function listToArray({ unFix: xs }: List): number[] {
return (xs === undefined) ? [] : listToArray(xs.rest).concat(xs.h);
}
const result: number[] = listToArray(arrayToList([1, 2, 3])); I'm not sure it's possible to make |
Not close to finished but it does seem to work for simple happy path cases.
Improvements basically come from treating GenericTypeParameters as both a TypeParameter and a GenericType, which means they are also treated as an interface type since GenericType extends InterfaceType. Treating GenericTypeParameters as InterfaceTypes works by making it look like an interface that declares no members of its own but inherits members from its constraint if it has one. There also are changes related to inferring GenericTypeParameters where they are not fully erased in getBaseSignature because we need to be able to infer `T<_T>` from something like `T<_T> extends Functor<_T, T>` in order to capture the fact that it references itself in its constraint. See class `InvalidFunctor2` and `expectErrorD1` in `higherKindedTypesLift.ts` for an example of this.
Resulted in inferring wrapper types (meaning Number for number) in some cases. Also adds test that catches the bug.
Tests completionListInTypeParameterOfTypeAlias1 and 2 were failing before this commit. The completions.ts changes are self-explanatory but the parser.ts change is that I added semicolon as a terminator for type parameter lists. The reason is the code in the failing test looks like: ```ts type List1< type List2<T> = T[]; type List4<T> = T[]; type List3<T1> = ; ``` When type parameters were not allowed to have their own type parameters that code parsed as type `List1` with type parameters `type` and `List2`, with a missing comma between `type` and `List2`, and then the unexpected `<` from `List2<T>` was parsed as the list terminator for the assumed `List1` type parameter list. However now that type parameters are allowed to declare their own type parameter lists, the `<` from `List2<T>` is no longer unexpected so `List2<T> = T[]` is parsed as a syntactically correct definition of a type parameter `List2` that has its own type parameter `T` and a default of `T[]`. Then it ignored the unexpected semicolon and parsed the third line in exactly the same way as the second line, as additional type parameters of `List1`, with `List4<T>` defining its own child type parameter `T`. So adding semicolon as a type parameter list terminator stops the assumed `List1` type parameter list at the end of line 2.
Pushed some updated code today that I think is a good step forward in both implementation quality and functionality. In the initial commits a few weeks ago, instantiating generic type parameters worked mostly by just blindly throwing type arguments into the new type without knowing that they actually belonged where they were being put. In the updated implementation the generic type parameter's arguments are neatly mapped into place in the new type in the same way as all other type parameters. In terms of functionality, everything I changed was to make the following wall of code work correctly: /*** Setup code ***/
declare function stringLength(strarg: string): number;
export interface Functor<A, Container<_T>> {
map<B>(f: (a: A) => B): Container<B>;
}
declare class FunctorX<A> implements Functor<A, FunctorX> {
map<B>(f: (a: A) => B): FunctorX<B>
uniqueMethodX(): A | undefined
}
declare class InvalidFunctor<A> {
map<B>(f: (a: A) => B): FunctorX<B>
uniqueInvalidFunctorMethod(): void
}
function lift<C<_T>>(someStaticFunctor: <A, B>(ca: C<A>, f: (a: A) => B) => C<B>):
<A, B>(f: (a: A) => B) =>
<C2<_T> extends C<_T>>(ca: C2<A>) => C2<B> {
return f => ca => someStaticFunctor(ca, f);
}
function staticFunctor<C<_T> extends Functor<_T, C>, A, B>(ca: C<A>, f: (a: A) => B): C<B> {
return ca.map(f);
}
declare const functorXString: FunctorX<string>
const stringArray = ["not explicitly declared to implement functor"]; // string[]
declare const invalidFunctor: InvalidFunctor<string>;
/*** Tests ***/
// lifts staticFunctor function, which has the constraint that it works with types that implement the Functor interface
const liftedStaticFunctor = lift(staticFunctor);
// liftedStringLength has type <C<*> extends Functor<*, C>>(ca: C<string>) => C<number>
// (note the * is still pseudo-code for now)
const liftedStringLength = liftedStaticFunctor(stringLength);
const result1 = liftedStringLength(functorXString); // FunctorX<number>
result1.uniqueMethodX(); // we can call methods from FunctorX because result1 is still a FunctorX
const result2 = liftedStringLength(stringArray); // number[]
result2.filter(num => num > 10); // we can call methods from Array because result2 is still an Array
const expectError = liftedStringLength(invalidFunctor); // error Making the At runtime the Still a bunch of things to do, probably the biggest one is figuring out what to do about cases where the number of type parameters don't match exactly, but I'm optimistic they are doable. |
Also @jack-williams, I don't think your example actually works even though it doesn't have an error. It just looks like it works because of your type annotations. If you take them away you start getting interface Fix<F<_T>> {
unFix: F<Fix<F>>;
}
interface ListContainer<A> {
h: number;
rest: A;
};
type List = Fix<ListContainer>;
const nil: List = { unFix: (<ListContainer<never>>undefined) };
const y: List = {
unFix: { h: 4, rest: nil }
};
function arrayToList(xs: number[]): List {
return xs.reduce((rest, h) => {
return { unFix: { h, rest } }
}, nil);
}
function listToArray({ unFix: xs }: List) {
return (xs === undefined) ? [] : listToArray(xs.rest).concat(xs.h);
}
// result has type any
const result = listToArray(arrayToList([1, 2, 3])); However I actually don't think it should work. For example the part: interface Fix<F<_T>> {
unFix: F<Fix<F>>;
} is not something I currently expect to be valid once I get around to actually adding validations. The reason is that you are defining a value that has a type without all its type parameters instantiated. Specifically the second I'm guessing your intention was that the missing The reason is so that the pattern from my example can work: export interface Functor<A, Container<_T>> {
map<B>(f: (a: A) => B): Container<B>;
}
declare class FunctorX<A> implements Functor<A, FunctorX> {
map<B>(f: (a: A) => B): FunctorX<B>
uniqueMethodX(): A | undefined
} note how it's |
Would love to have a feedback from the TS team on that PR (not the implementation details but the general stance on including that feature). |
I'm not sure I understand everything 100%. When you say:
Do you mean that it is not possible to pass a type of kind The type of the value for property I couldn't quite understand all of @Artazor 's proposal, but is there a difference between how things are treated in value positions (like the type of
The missing interface ListContainer<H,T> {
h: H;
rest: T;
};
type List<A> = Fix<ListContainer<A>>; // partially applied version
type List2<A> = Fix< <T>ListContainer<A,T> >; // annonymous type-abstraction version. |
No worries because I'm positive I don't understand everything 100% when it comes to this stuff.
This is a very good point. I probably need to start actually implementing the validations I've been visualizing sooner than later to make sure they don't end up conflicting with themselves, but I think the answer is going to end up being that in some way or another, values have a kind That is assuming I can actually implement that anyway. Because while I don't feel like
I'm starting to understand I think. Are the type of values always going to be the same within the same list? I assume so since you are converting to and from an array and not a tuple. If so does the following make sense? (The following actually causes compile errors right now because I don't handle the mismatched number of type parameters at all yet) interface Fix<T, LC<_T> extends ListContainer<_T, LC>> {
unFix: LC<T> | undefined;
}
interface ListContainer<T, LC<_T> extends ListContainer<_T, LC>> {
h: T;
rest: Fix<T, LC>;
};
const nil = { unFix: undefined };
const y: Fix<number, ListContainer> = {
unFix: { h: 4, rest: nil }
};
function arrayToList<T>(xs: T[]): Fix<T, ListContainer> {
return xs.reduce((rest, h) => {
return { unFix: { h, rest } }
}, nil);
}
function listToArray<T>({ unFix: xs }: Fix<T, ListContainer>): T[] {
return (xs === undefined) ? [] : listToArray(xs.rest).concat(xs.h);
}
const result = listToArray(arrayToList([1, 2, 3])); // result is number[]
const result2 = listToArray(arrayToList(["a", "b", "c"])); // result2 is string[] Although I may still not be understanding because in that code the interface Fix<T> {
unFix: ListContainer<T> | undefined;
} |
Thanks for your contribution. This PR has not been updated in a while and cannot be automatically merged at the time being. For housekeeping purposes we are closing stale PRs. If you'd still like to continue working on this PR, please leave a message and one of the maintainers can reopen it. |
This is the start of an attempt at creating a PR for adding support for higher kinded types (#1213). It's very incomplete but I managed to get the basics working enough that I figured it's worth getting some feedback on.
The only syntax I've added so far is the ability to create type parameters that have their own type parameters. I haven't gotten as far as adding any type of
T<*>
syntax yet so they have to be named right now. Essentially this is an implementation of the third option @Artazor proposed at #1213 (comment). I used that comment to get me started so thank you @Artazor, and also thanks to @gcanti for his blog post and fp-ts library which I've been using for reference.I've only attempted using interfaces and classes as instantiations of generic type parameters so far. That is partially a result of not being finished, but also partially because internally those are implemented to be much easier to use with generics, so I'm not sure yet how well generic type aliases will eventually work with this.
Examples of what works right now:
Let me know what you think. I especially could use examples of patterns that people would expect to work with higher kinded types so I can create more tests.