microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.19k stars 12.38k forks source link

Allow classes to be parametric in other parametric classes #1213

Open metaweta opened 9 years ago

metaweta commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

Thanks, corrected.

metaweta commented 9 years ago

I've updated my comment into a proposal.

fdecampredon commented 9 years ago

:+1: 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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

danquirk commented 9 years ago

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 commented 8 years ago

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

metaweta commented 8 years ago

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

zpdDG4gta8XKpMCd commented 8 years ago

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

RyanCavanaugh commented 8 years ago

https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-96854288 is still the current state of it. I don't see anything here that would make us change the priority of the feature.

spion commented 8 years ago

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> 
}
bcherny commented 8 years ago

Would come in handy here too http://stackoverflow.com/questions/36900619/how-do-i-express-this-in-typescript

gneuvill commented 8 years ago

:+1:

zpdDG4gta8XKpMCd commented 8 years ago

with HKT's mindsets can be changed, habits broken, lost generations brought back to life, it would the biggest thing since generics and explicit nulls and undefineds, it can change everything

please consider it as a next big feature, stop listen to people who keep asking you for a better horse, give them a f***g ferrari

abuseofnotation commented 8 years ago

Yup, Bumped to this the first 15 minutes after trying to add types to existing JS codebase. I am not switching to TS until I see it.

CanI help, actually?

dead-claudia commented 8 years ago

I wonder how this would relate to #7848? They're very similar, although about the other facet of higher order kinds.

lynn commented 8 years ago

@boris-marinov Ryan Cavanaugh’s reply says you can:

Having a sample PR that at least tackles the 80% use cases of this would be a really helpful next step.

Artazor commented 8 years ago

Now I have time to implement such a simple PR Hope to get some hints frome core devs, but there are no questions so far - all looks good and understandable. Will track a progess here.

dead-claudia commented 8 years ago

@Artazor Would you like to take a look at cracking #7848 as well? That takes care of the other side of this problem, involving generics, and IMHO this would feel incomplete without it (generic parameters would really simplify a lot of type-level code).

paldepind commented 8 years ago

I think this proposal is absolutely wonderful. Having higher kinded types in TypeScript would take it up to a hole new level where we could describe more powerful abstractions than what is currently possible.

However, isn't there something wrong with the examples given in OP? The A in the line

class ArrayMonad<A> implements Monad<Array> {

isn't used in any of the methods, since they all have their own generic A.

Also, if implementing functor with map as a method that uses this what would it look like? Like this maybe?

interface Functor<T, A> {
  map<B>(f: (a: A) => B): T<A> => T<B>;
}

class Maybe<A> implements Functor<Maybe, A> {
  ...
}
dead-claudia commented 8 years ago

@paldepind Check out #7848. That discussion is about that particular use case, although IMHO this and that one really needs merged into a single PR.

wclr commented 8 years ago

When does this stuff is going to land? That seems like a kind of essential.

Also will it going to make possible such:

interface SomeX<X, T> {
   ...// some complex definition
  some: X<T>
}

interface SomeA<T> extends SomeX<A, T> {
}

?

dead-claudia commented 8 years ago

@whitecolor I think there's bigger fish to fry at the moment, which merit higher priority:

  1. TypeScript 2.0 RC was released only a little under 2 weeks ago. That'll take up a lot of time in of itself.
  2. bind, call, and apply, native JS functions, are untyped. This actually depends on the variadic generics proposal. Object.assign also needs a similar fix, but variadic generics alone won't solve that.
  3. Functions like Lodash's _.pluck, Backbone models' get and set methods, etc. are currently untyped, and fixing this basically makes Backbone usable with TypeScript in a much safer way. It also may have implications for React in the future.

Not that I don't want this feature (I would love for such a feature), I just don't see it as likely coming soon.

wclr commented 8 years ago

@isiahmeadows Thanks for explanation. Yeah 3rd item in the list is very important, waiting for https://github.com/Microsoft/TypeScript/issues/1295 too.

But I hope for current issue maybe in 2.1dev somehow.

dead-claudia commented 8 years ago

I agree. Hopefully it can make it in.

(Rank 2 polymorphism, which this issue wants, is also a necessity for Fantasy Land users, to properly type the various ADTs within that spec. Ramda is a good example of a library that needs this fairly badly.)

On Tue, Sep 6, 2016, 11:00 Alex notifications@github.com wrote:

@isiahmeadows https://github.com/isiahmeadows Thanks for explanation. Yeah 3rd item in the list is very important, waiting for #1295 https://github.com/Microsoft/TypeScript/issues/1295 too.

But I hope for current issue maybe in 2.1dev somehow.

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-244978475, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBMvxBALBe0aaLOp03vEvEyokvxpyks5qnX_8gaJpZM4C99VY .

Strate commented 7 years ago

Seems that this feature would helps us a lot to define react forms. For example, you have struct:

interface Model {
  field1: string,
  field2: number,
  field3?: Model
}

I have a handler, which defined as:

interface Handler<T> {
  readonly value: T;
  onChange: (newValue: T) => void;
}

this handler passed as a props to React components. Also I have a function, which takes struct and returns same struct, but with Handlers instead of values:

function makeForm(value: Model): {
  field1: Handler<string>,
  field2: Handler<number>,
  field3: Handler<Model>,
}

As for now I can't type that function properly, because TS can't produce type based on structure of other type.

Cow I could type makeForm with HKT?

abuseofnotation commented 7 years ago

Hm, interesting.

Maybe something like this may be possible:

//Just a container
interface Id <A> {
  value: A
}

interface Model <T> {
  field1: T<string>,
  field2: T<number>,
  field3?: T<Model>
}

makeForm (Model<Id>): Model<Handler>
Strate commented 7 years ago

@boris-marinov The most interesting point is this line:

interface Model<T> {
  //...
  field3?: T<Model> // <- Model itself is generic.
                    // Normally typescript will error here, requiring generic type parameter.
}
zpdDG4gta8XKpMCd commented 7 years ago

might be worth mentioning that HKT could have been an answer to so called partial types (https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-247721155):

type MyDataProto<K<~>> = {
    one: K<number>;
    another: K<string>;
    yetAnother: K<boolean>;
}
type Identical<a> = a;
type Optional<a> = a?; // or should i say: a | undefined;
type GettableAndSettable<a> = { get(): a; set(value: a): void }

type MyData = MyDataProto<Identical>; // the basic type itself
type MyDataPartial = MyDataProto<Optional>; // "partial" type or whatever you call it
type MyDataProxy = MyDataProto<GettableAndSettable>; // a proxy type over MyData
// ... etc
dead-claudia commented 7 years ago

Not quite. {x: number?} isn't assignable to {x?: number}, because one is guaranteed to exist, while the other isn't.

On Tue, Oct 11, 2016, 09:16 Aleksey Bykov notifications@github.com wrote:

might be worth mentioning that HKT could have been an answer to so called partial types (#4889 (comment) https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-247721155 ):

type MyDataProto<K<~>> = { one: K; another: K; yetAnother: K; }type Identical = a;type Optional = a?; // or should i say: a | undefined;type GettableAndSettable = { get(): a; set(value: a): void } type MyData = MyDataProto; // the basic type itselftype MyDataPartial = MyDataProto; // "partial" type or whatever you call ittype MyDataProxy = MyDataProto; // a proxy type over MyData// ... etc

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-252913109, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBNFYFfiW01MT99xv7UE2skQ3qiPMks5qy4wRgaJpZM4C99VY .

zpdDG4gta8XKpMCd commented 7 years ago

@isiahmeadows you are right, at the moment there is no way/syntax to make a property truely optional based solely on its type, and thats a shame

Strate commented 7 years ago

Yet another one: it would be good if property can be made readonly. Seems some kind of macros feature required.

landonpoch commented 7 years ago

Just throwing this out there... I prefer the * syntax over the ~ syntax. Something about ~ just seems so far out of the way from a keyboard layout perspective. Also, I'm not sure why, but I think * seems a bit more readable/distinguishable with all the angle brackets that are in the mix. Not to mention, people familiar with other languages like Haskell might immediately associate the syntax to HKT. Seems a bit more natural.

dead-claudia commented 7 years ago

I'd have to agree with the * syntax. First, it is more distinguishable, and second, it better represents an "any type works" type.


Isiah Meadows me@isiahmeadows.com

On Sun, Nov 6, 2016 at 12:10 AM, Landon Poch notifications@github.com wrote:

Just throwing this out there... I prefer the * syntax over the ~ syntax. Something about ~ just seems so far out of the way from a keyboard layout perspective. Also, I'm not sure why, but I think * seems a bit more readable/distinguishable with all the angle brackets that are in the mix. Not to mention, people familiar with other languages like Haskell might immediately associate the syntax to HKT. Seems a bit more natural.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-258659277, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBHQ4SYeIiptB8lhxEAJGOYaxwCkiks5q7VMvgaJpZM4C99VY .

wclr commented 7 years ago

Milestone: community? What is current state of this issue/feature?

zpdDG4gta8XKpMCd commented 7 years ago

@whitecolor the status is DIY (do it yourself)

mhegazy commented 7 years ago

The issue has Accepting PRs label. this means that pull requests to implement this feature are welcomed. See https://github.com/Microsoft/TypeScript/wiki/FAQ#what-do-the-labels-on-these-issues-mean for more details.

Also please see https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-96854288

wclr commented 7 years ago

Ok, I see the labels, just have doubts that if non-TS team is capable of accomplishing it.

raveclassic commented 7 years ago

Now I have time to implement such a simple PR Hope to get some hints frome core devs, but there are no questions so far - all looks good and understandable. Will track a progess here.

@Artazor Do you have any luck with this?

Artazor commented 7 years ago

@raveclassic - it turned to be more difficult than it seemed, however I still hope to move forward. Syntactically it is obvious, but the typechecking rules/phases are not as clear to me as I want -)

Lets try to revive my activity -)

Artazor commented 7 years ago

Just tracking a progress, and the path of the idea development. I've considered three options how to implement this feature.

I've planned to enrich a TypeParameterDeclaration with optional higherShape property

    export interface TypeParameterDeclaration extends Declaration {
        kind: SyntaxKind.TypeParameter;
        name: Identifier;
        higherShape?: HigherShape // For Higher-Kinded Types <--- this one 
        constraint?: TypeNode;

        // For error recovery purposes.
        expression?: Expression;
    }

and have considered three options how HigherShape could beimplemented

1. Simple Arity for the Domain

type HigherShape = number

it corresponds to the following usage:

class Demo<Wrap<*>, WrapTwo<*,*>> {   // 1 and 2
    str: Wrap<string>;
    num: Wrap<number>;
    both: WrapTwo<number, string>;
}

in this simplest case, looks like that the number type would be sufficient. Nevertheless, we should be able to determine an actual higherShape for every given type to be sure we can use it as a type argument for the specific shape requirements. And here we're facing a problem: the higher shape of the Demo class itself is not expressible as a number. If it would, then it should be represented as 2 - since it has two type parameters, and it would be possible to write

var x: Demo<Array, Demo>

and then battling with the deferred type-checking problem with property .both. Thus the number type is not sufficient (i believe);

in fact type Demo has the following high order shape:

(* => *, (*,*) => *) => *

2. Fully structured Domain and Co-Domain

Then I've investigated the opposite, most full representation of the higher shapes, that would allow representing such shapes as aforementioned one, and even worse:

(* => (*,*)) => ((*,*) => *)

The data structure for this is straightforward, but it does not interplay well with the TypeScript type system. If we would allow such higher-order types then we will never know whether * means the ground type, that could be used for the typing of values. Besides, I even did not manage to find an appropriate syntax how to express such a monstrous higher order constraints.

3. Structured Domain / Implicit Simple Co-Domain

The main idea - type expression (even with actual type arguments) always results in a ground type - that can be used to type a variable. On the other hand, each type parameter can have its own detailed type parameters in the same format that is used elsewhere.

This was my final decision that I would try to advocate.

type HigherShape = NodeArray<TypeParameterDeclaration>;

example:

class A {x: number}
class A2 extends A { y: number }
class Z<T> { z: T; }

class SomeClass<T1<M extends A> extends Z<M>, T2<*,*<*>>, T3<* extends string>> {
        var a: T1<A2>; // checked early
        var b: T2<string, T1>; // second argument of T2 should be generic with one type parameter  
        var c: T3<"A"|"B">; // not very clever but it is checked
        // ...
        test() {
             this.a.z.y = 123 // OK
             // nothing meaningful can be done with this.b and this.c
        }
}

Here I want to note, that M is local for T1<M extends A> extends Z<M> and exists in a deeper visibility scope than T1. Thus M is not available in the SomeClass body. And * means simply a fresh identifier (anonymous type) that never clash with anything (and could be implemented at later stage)


Thus the final signature of the TypeParameterDeclaration

    export interface TypeParameterDeclaration extends Declaration {
        kind: SyntaxKind.TypeParameter;
        name: Identifier;
        typeParameters?: NodeArray<TypeParameterDeclaration> // !!! 
        constraint?: TypeNode;

        // For error recovery purposes.
        expression?: Expression;
    }

Want to hear any opinion of @DanielRosenwasser, @aleksey-bykov, @isiahmeadows and others -)

dead-claudia commented 7 years ago

Sounds okay to me, but I know very little about the internal structure of TypeScript's code base.

MichaelTontchev commented 7 years ago

Would like to add my voice to the choir requesting this and to cheer you on, Artazor! :)

This feature would be useful to me in my implementation of making Redux type-safe.

bcherny commented 7 years ago

@michaeltontchev What issues are you having making Redux typesafe?

In case you're interested, I recently published https://github.com/bcherny/tdux and https://github.com/bcherny/typed-rx-emitter, which build on ideas from Redux and EventEmitter.

Artazor commented 7 years ago

Now looks, need to rebase to the @rbuckton branch #13487 with default generic parameters. In other case we will conflict largely.