Skip to content
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

Allow classes to be parametric in other parametric classes #1213

Open
metaweta opened this issue Nov 19, 2014 · 140 comments · May be fixed by #40368
Open

Allow classes to be parametric in other parametric classes #1213

metaweta opened this issue Nov 19, 2014 · 140 comments · May be fixed by #40368

Comments

@metaweta
Copy link

@metaweta metaweta commented Nov 19, 2014

This is a proposal for allowing generics as type parameters. It's currently possible to write specific examples of monads, but in order to write the interface that all monads satisfy, I propose writing

interface Monad<T<~>> {
  map<A, B>(f: (a: A) => B): T<A> => T<B>;
  lift<A>(a: A): T<A>;
  join<A>(tta: T<T<A>>): T<A>;
}

Similarly, it's possible to write specific examples of cartesian functors, but in order to write the interface that all cartesian functors satisfy, I propose writing

interface Cartesian<T<~>> {
  all<A>(a: Array<T<A>>): T<Array<A>>;
}

Parametric type parameters can take any number of arguments:

interface Foo<T<~,~>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

That is, when a type parameter is followed by a tilde and a natural arity, the type parameter should be allowed to be used as a generic type with the given arity in the rest of the declaration.

Just as is the case now, when implementing such an interface, the generic type parameters should be filled in:

class ArrayMonad<A> implements Monad<Array> {
  map<A, B>(f: (a:A) => B): Array<A> => Array<B> {
    return (arr: Array<A>) =>  arr.map(f);
  }
  lift<A>(a: A): Array<A> { return [a]; }
  join<A>(tta: Array<Array<A>>): Array<A> {
    return tta.reduce((prev, cur) => prev.concat(cur));
  }
}

In addition to directly allowing compositions of generic types in the arguments, I propose that typedefs also support defining generics in this way (see issue 308):

typedef Maybe<Array<~>> Composite<~> ;
class Foo implements Monad<Composite<~>> { ... }

The arities of the definition and the alias must match for the typedef to be valid.

@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Nov 19, 2014

Not to make any rash assumptions, but I believe you're typing it incorrectly. All parameter types require parameter names, so you probably meant to type

map<A, B>(f: (x: A) => B): T<A> => T<B>;

whereas right now map is a function that takes a mapper from type any (where your parameter name is A) to B.

Try using the --noImplicitAny flag for better results.

@metaweta
Copy link
Author

@metaweta metaweta commented Nov 19, 2014

Thanks, corrected.

@metaweta
Copy link
Author

@metaweta metaweta commented Nov 28, 2014

I've updated my comment into a proposal.

@fdecampredon
Copy link

@fdecampredon fdecampredon commented Jan 27, 2015

👍 higher kinded type would be a big bonus for functional programming construct, however before that I would prefer to have correct support for higher order function and generic :p

@RyanCavanaugh RyanCavanaugh added this to the Community milestone Apr 28, 2015
@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Apr 28, 2015

Quasi-approved.

We like this idea a lot, but need a working implementation to try out to understand all the implications and potential edge cases. Having a sample PR that at least tackles the 80% use cases of this would be a really helpful next step.

@metaweta
Copy link
Author

@metaweta metaweta commented Apr 28, 2015

What are people's opinions on the tilde syntax? An alternative to T~2 would be something like

interface Foo<T<~,~>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

that allows direct composition of generics instead of needing type aliases:

interface Foo<T<~,~,~>, U<~>, V<~, ~>> {
  bar<A, B, C, D>(a: A, f: (b: B) => C, d: D): T<U<A>, V<B, C>, D>;
}
@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Apr 28, 2015

It's odd to have explicit arity since we don't really do that anywhere else, so

interface Foo<T<~,~>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

is a little clearer, though, I know other languages use * in similar contexts instead of ~:

interface Foo<T<*,*>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

Though taking that point to an extreme, you might get:

interface Foo<T: (*,*) => *> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}
@metaweta
Copy link
Author

@metaweta metaweta commented Apr 28, 2015

I think T<~,~> is clearer than T~2, too. I'll modify the proposal above. I don't care whether we use ~ or *; it just can't be a JS identifier, so we can't use, say, _ . I don't see what benefit the => notation provides; all generics take some input types and return a single output type.

@metaweta
Copy link
Author

@metaweta metaweta commented Apr 28, 2015

A lighter-weight syntax would be leaving off the arity of the generics entirely; the parser would figure it out from the first use and throw an error if the rest weren't consistent with it.

@metaweta
Copy link
Author

@metaweta metaweta commented Jun 1, 2015

I'd be happy to start work on implementing this feature. What's the recommended forum for pestering devs about transpiler implementation details?

@danquirk
Copy link
Member

@danquirk danquirk commented Jun 1, 2015

You can log many new issues for larger questions with more involved code samples, or make a long running issue with a series of questions as you go. Alternatively you can join the chat room here https://gitter.im/Microsoft/TypeScript and we can talk there.

@Artazor
Copy link
Contributor

@Artazor Artazor commented Dec 10, 2015

@metaweta any news? If you need any help/discussion I would be glad to brainstorm on this issue. I really want this feature.

@metaweta
Copy link
Author

@metaweta metaweta commented Dec 10, 2015

No, things at work took over what free time I had to work on it.

@zpdDG4gta8XKpMCd
Copy link

@zpdDG4gta8XKpMCd zpdDG4gta8XKpMCd commented Feb 29, 2016

bump: is there a chance to see this feature ever considered?

@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Feb 29, 2016

#1213 (comment) is still the current state of it. I don't see anything here that would make us change the priority of the feature.

@spion
Copy link

@spion spion commented Apr 18, 2016

Seems to me like this is useful in far more situations than just importing category theory abstractions. For example, it would be useful to be able to write module factories that take a Promise implementation (constructor) as an argument, e.g. a Database with a pluggable promise implementation:

interface Database<P<~> extends PromiseLike<~>> {   
    query<T>(s:string, args:any[]): P<T> 
}
@Sytten
Copy link

@Sytten Sytten commented Jun 12, 2020

Any update?

@agyemanjp
Copy link

@agyemanjp agyemanjp commented Aug 3, 2020

Still no update? In my opinion, this issue ranks as the number one obstacle to TypeScript achieving its full potential. There are so many instances where I try to type my libraries properly, only to give up after a long struggle, realizing that I have run up against this limitation again. It is pervasive, showing up even in seemingly very simple scenarios. Really hope it will be addressed soon.

@ShuiRuTian ShuiRuTian linked a pull request that will close this issue Sep 3, 2020
3 of 12 tasks complete
@ShuiRuTian
Copy link
Contributor

@ShuiRuTian ShuiRuTian commented Sep 9, 2020

interface Monad<T<X>> {
    map1<A, B>(f: (a: A) => B): (something: A) => B;

    map<A, B>(f: (a: A) => B): (something: T<A>) => T<B>;

    lift<A>(a: A): T<A>;
    join<A>(tta: T<T<A>>): T<A>;
}

type sn = (tmp: string) => number

function MONAD(m: Monad<Set>,f:sn) {
    var w = m.map1(f);    // (method) Monad<Set>.map1<string, number>(f: (a: string) => number): (something: string) => number
    var w2 = m.map(f);    // (method) Monad<Set>.map<string, number>(f: (a: string) => number): (something: Set<string>) => Set<number>
    var q = m.lift(1);    // (method) Monad<Set>.lift<number>(a: number): Set<number>
    var a = new Set<Set<number>>();
    var w = m.join(q);    // (method) Monad<Set>.join<unknown>(tta: Set<Set<unknown>>): Set<unknown>.  You could see that typeParameter infer does not work for now.
    var w1 = m.join<number>(q);    // (method) Monad<Set>.join<number>(tta: Set<Set<number>>): Set<number>
}

A lot of work still need to be done, like: fix quickinfo, typeParameter infer, add error message, hightlight same typeConstructor.....
But it starts to work, and here is what I could get for now.
The example interface is from @millsp #1213 (comment) , the conclusion is really really helpful, great thanks to that.

I hope communicity could provide more user cases like that, to check whether current way works for most situations.

It would also be sweet to provide some info about HKT/function programming/lambda(when I say lambda, I mean the math, I could only find example written by some language, without math)

Here are things that help me a lot:

@isiahmeadows
Copy link

@isiahmeadows isiahmeadows commented Sep 9, 2020

@ShuiRuTian Regarding that m.join(q) returning Set<unknown>, I assume --noImplicitAny causes that to emit a warning as well?

@lukeshiru
Copy link

@lukeshiru lukeshiru commented Sep 9, 2020

I hope community could provide more user cases like that, to check whether current way works for most situations.

It would also be sweet to provide some info about HKT/function programming/lambda(when I say lambda, I mean the math, I could only find example written by some language, without math)

Without going any further, I recently tried to create a generic curried filter function, and I wanted it to do something like this:

const filterNumbers = filter(
	(item: number | string): item is number => typeof item === "number"
);

const array = ["foo", 1, 2, "bar"]; // (number | string)[]
const customObject = new CustomObject(); // CustomObject<number | string>

filterNumbers(array); // number[] inferred
filterNumbers(customObjectWithFilterFunction); // CustomObject<number> inferred

And I don't have a way of actually doing that, because I need a way of having telling TypeScript "return the same type you received, but with this other parameter". Something like this:

const filter = <Item, FilteredItem>(predicate: (item: Item) => item is FilteredItem) =>
  <Filterable<~>>(source: Filterable<Item>): Filterable<FilteredItem> => source.filter(predicate);
@raveclassic
Copy link

@raveclassic raveclassic commented Sep 10, 2020

@lukeshiru yeah, essentially this is https://pursuit.purescript.org/packages/purescript-filterable/2.0.1/docs/Data.Filterable#v:filter
There're lots of other similar usecases for HKT in TypeScript.

@ShuiRuTian
Copy link
Contributor

@ShuiRuTian ShuiRuTian commented Sep 10, 2020

@isiahmeadows I tried it. You are right.
@lukeshiru and @raveclassic Thanks! This feature seems pretty resonable. I would have a look at this after reading https://gcanti.github.io/fp-ts/learning-resources/

Luxcium added a commit to Luxcium/100DaysOfCode that referenced this issue Sep 17, 2020
microsoft/TypeScript#1213
Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>
@Luxcium
Copy link

@Luxcium Luxcium commented Sep 17, 2020

I am stuck and I don't know what is the current ⸘workaround‽...
I am trying to implement the Chain specification:

m['fantasy-land/chain'](f)

A value that implements the Chain specification must also implement the Apply specification.

a['fantasy-land/ap'](b)

I did a FunctorSimplex which is then extended by a FunctorComplex then extended by the Apply but now that I want to extend Apply as a Chain it is breaking...

So I need that (image below and link to code):

(I need to pass a type to the ApType so that on line 12 the Apply is not « hardcoded » but is generic... to also take any types extended from an IApply)

Screenshot

Permalink to the code snippet lines 11 to 22 in 7ff8b9c

export type ApType<A = unknown> = <B = unknown>(
  ap: Apply<(val: A) => B>,
) => IApply<B>;

/* [...] */

export interface IApply<A = unknown> extends FunctorComplex<A> {
  /** `Fantasy-land/ap :: Apply f => f a ~> f (a -> b) -> f b` */
  ap: ApType<A>;
}

 Referenced into a Stack Overflow question: TypeScript Generic Types problem: Workaround required

Luxcium added a commit to Luxcium/100DaysOfCode that referenced this issue Sep 17, 2020
microsoft/TypeScript#1213

microsoft/TypeScript#1213 (comment)

I am stuck and I don't know what is the current **⸘workaround‽**...
I am trying to implement the [Chain specification](https://github.com/fantasyland/fantasy-land#chain):

    m['fantasy-land/chain'](f)

A value that implements the Chain specification must also implement the Apply specification.

    a['fantasy-land/ap'](b)

I did a `FunctorSimplex` which is then extended by a `FunctorComplex` then extended by the `Apply` but now that I want to extend `Apply` as a `Chain`  it is breaking...

So I did that:
(I need to pass a type to the `ApType` so that on line 12 the `Apply` is not *hard coded* but generic to also take types extended from any `IApply`)

<img src="https://user-images.githubusercontent.com/42672814/93421381-357ad980-f87f-11ea-9984-159757a6ca02.png" alt="Screenshot" width="400">

[Permalink to the code snippet *lines 11 to 22*](https://github.com/Luxcium/100DaysOfCode/blob/5d858b416c742f85c40a3c3f330d392c3b8d65b1/packages/apply/lib/types.ts#L11-L22)

```typescript
export type ApType<A = unknown> = <B = unknown>(
  ap: Apply<(val: A) => B>,
) => IApply<B>;

/* [...] */

export interface IApply<A = unknown> extends FunctorComplex<A> {
  /** `Fantasy-land/ap :: Apply f => f a ~> f (a -> b) -> f b` */
  ap: ApType<A>;
}
 ```

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>
@kapke
Copy link

@kapke kapke commented Sep 17, 2020

@Luxcium Until TS has support for higher-kinded-types, only emulation of them is possible. You might want to take a look there to see how it is possible to achieve:

@Luxcium
Copy link

@Luxcium Luxcium commented Sep 17, 2020

Until TS has support for higher-kinded-types, only emulation of them is possible. You might want to take a look there to see how it is possible to achieve

Tanks a lot @kapke I am probably too much into FP theses days and since in Javascript one can return a function from a function we can write pseudoFnAdd(15)(27) // 42 I would like to be capable, with TypeScript, to write pseudoType<someClassOrConstructor><number> // unknown but I am a script kiddie, not a °some kind of person that studied for a long time at the university or something°

This information and lectures (readings) are much appreciated...

Note: I am french speaking, in french the word lecture(s) have the meaning of readings and not 'an angry or serious talk given to someone in order to criticize their behaviour' ...

Luxcium added a commit to Luxcium/100DaysOfCode that referenced this issue Sep 18, 2020

* add link to  github.com/fantasyland/fantasy-land/

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* add folders structure

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* update file naming convention

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* include functor

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* update naming conventions

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* remove tslint suppoort for lodash and lodash/fp

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* remove lodash definitions from eslint

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* change main source type inside âckage

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* figuring out complex generic types in the packages

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* Question: how can I have a Generic Generic 'Apply'
23 out of #100DaysOfCode I have a problem
related to @typescript #GenericTypes
implementing #FantasyLand #JS 'b must be
same Apply as a'

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* classes to be parametric in parametric classes

microsoft/TypeScript#1213
Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* remove extra lines of code

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* Allow classes to be parametric in parametric classes
microsoft/TypeScript#1213

microsoft/TypeScript#1213 (comment)

I am stuck and I don't know what is the current **⸘workaround‽**...
I am trying to implement the [Chain specification](https://github.com/fantasyland/fantasy-land#chain):

    m['fantasy-land/chain'](f)

A value that implements the Chain specification must also implement the Apply specification.

    a['fantasy-land/ap'](b)

I did a `FunctorSimplex` which is then extended by a `FunctorComplex` then extended by the `Apply` but now that I want to extend `Apply` as a `Chain`  it is breaking...

So I did that:
(I need to pass a type to the `ApType` so that on line 12 the `Apply` is not *hard coded* but generic to also take types extended from any `IApply`)

<img src="https://user-images.githubusercontent.com/42672814/93421381-357ad980-f87f-11ea-9984-159757a6ca02.png" alt="Screenshot" width="400">

[Permalink to the code snippet *lines 11 to 22*](https://github.com/Luxcium/100DaysOfCode/blob/5d858b416c742f85c40a3c3f330d392c3b8d65b1/packages/apply/lib/types.ts#L11-L22)

```typescript
export type ApType<A = unknown> = <B = unknown>(
  ap: Apply<(val: A) => B>,
) => IApply<B>;

/* [...] */

export interface IApply<A = unknown> extends FunctorComplex<A> {
  /** `Fantasy-land/ap :: Apply f => f a ~> f (a -> b) -> f b` */
  ap: ApType<A>;
}
 ```

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* close the day as it is will continue tomorow

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>

* add support to get video streaming capabilities

Signed-off-by: Benjamin Vincent (Luxcium) <luxcium@neb401.com>
@Remirror-zz
Copy link

@Remirror-zz Remirror-zz commented Sep 21, 2020

Probably the following I came up with as a simple workaround without PRs will not work for all cases, but I think it is worth mentioning:

type AGenericType<T> = T[];

type Placeholder = {'aUniqueKey': unknown};
type Replace<T, X, Y> = {
  [k in keyof T]: T[k] extends X ? Y : T[k];
};

interface Monad<T> {
  map<A, B>(f: (a: A) => B): (v: Replace<T, Placeholder, A>) => Replace<T, Placeholder, B>;
  lift<A>(a: A): Replace<T, Placeholder, A>;
  join<A>(tta: Replace<T, Placeholder, Replace<T, Placeholder, A>>): Replace<T, Placeholder, A>;
}

function MONAD(m: Monad<AGenericType<Placeholder>>, f: (s: string) => number) {
  var a = m.map(f); // (v: string[]) => number[]
  var b = m.lift(1); // number[]
  var c = m.join([[2], [3]]); // number[]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

You can’t perform that action at this time.