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 · 182 comments
Open

Allow classes to be parametric in other parametric classes #1213

metaweta opened this issue Nov 19, 2014 · 182 comments
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@metaweta
Copy link

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

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

Thanks, corrected.

@danquirk danquirk added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Nov 25, 2014
@metaweta
Copy link
Author

I've updated my comment into a proposal.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Dec 1, 2014
@fdecampredon
Copy link

👍 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 Help Wanted You can do this and removed In Discussion Not yet reached consensus labels Apr 28, 2015
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Apr 28, 2015
@RyanCavanaugh
Copy link
Member

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

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

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

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

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 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 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 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

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

@zpdDG4gta8XKpMCd
Copy link

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

@RyanCavanaugh
Copy link
Member

#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 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> 
}

@bcherny
Copy link

bcherny commented Apr 27, 2016

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

@zedryas
Copy link

zedryas commented Sep 2, 2022

@Caleb-T-Owens yep but it seems if i'm not mistaken that every solution are towards HKT, and non necessarly using parametics type parameters such as type MyType <T<~,~>> = ... - which can be used in other circumstances than HKT.

@geoffreytools
Copy link

geoffreytools commented Oct 24, 2022

I made some progress on this issue on the user end of things : free-types

I focused my implementation on general-purpose type-level programming, so I included things like:

  • the handling of type constraints
  • partial application
  • composition and other combinators
  • higher order types like map/lift/reduce for tuples and objects
  • abstraction over type constructors
  • pattern matching on types and other experiments

I didn't do an amazing job at providing an overview of the functionalities in the readme, because there are just so many things you can do with something like that, but there is a good guide which explains how the library works, how it's designed and the limitations it has.

I used it in the project type-lenses to enable reaching the values of arbitrary types or manipulating them with arbitrary type-level functions, and in ts-spec to implement equality. I'm also using it in a project that aims at converting a class to a collection of curried functions with all the types correctly wired, which could be a way to easily turn a fantasy-land compliant class into a static-land-like collection of functions, but also any arbitrary class, with some more work from the part of the implementer.

I need advice regarding performance: stacking combinators and compositions on top of each other adds up, but I don't have a solid way of evaluating my design decisions to make the building blocks as lightweight as possible. The value of reusing types to an extreme is questionable but I find it to be an interesting question ;)

There are also limitations and pain points which may have workarounds or even solutions I don't know about.

I don't know if this approach is a dead end but I definitely hit a wall at some point and I don't imagine a full featured and performant solution can be implemented solely on the user end.

@J-Cake
Copy link

J-Cake commented Nov 20, 2022

Alternative syntax proposal: Using the infer keyword outside of a conditional clause:

type A<X> = X;
type M = [A<infer Intermediate>, A<Intermediate>];

This toy example obviously simplifies to

type M<X> = x[];

but that doesn't matter.

@exoRift
Copy link

exoRift commented Jan 4, 2023

"2014" 😔

@craigphicks
Copy link

About the syntax - I think arity can be inferred from usage. More important would be

  1. Indicating that the type definition is a meta-type definition
  2. Indicating that a specific parameter is a generic function
  3. Supplying default values to a generic function (which wouldn't need to be a feature in the first version anyway).

For example

type* OneOf<GenFunc><A extends any[], Acc extends Record<number, any>={}> = A extends [] ? Acc :
    A extends [infer H, ... infer Rem] ? OneOf_GenFunc<Rem, Acc & GenFunc<H>> : never ; 

type* rather than metatype or some other alphabetic identifier because metatype is not reserved.

Most of the type checking could be deferred to the instance call

type GenFunc<T> = <T>(a:T,b:T): T;
type FuncTypeInstance = OneOf<GenFunc><[number,string]>

I think it is more clear for the reader and probably more clear to implement.

@dead-claudia
Copy link

@craigphicks You could go one simpler: ditch the type* and just use what's there.

  • Accept a HK type: foo<A<B>>(): ..., type Foo<A<B>> = ...
  • Define a HK type: type Foo<A><B> = ..., interface Foo<A><B> { ... }

The hard part of implementing this isn't the syntax though - it's the semantics. Retrofitting higher-kinded types into a language with type inference almost always requires at least a partial rewrite of the type checker as it mucks with inference a lot (and can rapidly slow it down).

@craigphicks
Copy link

craigphicks commented Dec 16, 2023

@dead-claudia @achinaou

@craigphicks - ... Retrofitting higher-kinded types into a language with type inference almost always requires at least a
partial rewrite of the type checker as it mucks with inference a lot (and can rapidly slow it down).

Note: I originally posted it as a separate proposal but was redirected here as my proposal was claimed to be a duplicate.

What I am proposing imposes no extra logic on the type checker - because the meta type is never evaluated until all it's parameters have been supplied, at which time it rendered to a type.

The original proposal 1213 is a lot more powerful - but also much harder to implement.

I am starting from a very simple simple problem:

type SomeGenericType<T> = Body 1
type MetaTypeResolved<Generic Parameter and Constraints> = Body 2 which references SomeGenericType

For each SomeGenericType, Body 2 has to be rewritten.
This proposal is just to get around that limitation, so that the body can be written once, and applied to many instances of
SomeGenericType X Generic Parameters.

That is the simplest version, the result of which has no unresolved generic parameters, so obviously no inference is involved.

The next level of complexity would be to implement resolving just SomeGenericType

type MetaTypeResolved = MetaType<SomeGenericType>

which would be equivalent to

type MetaTypeResolved<Generic Parameter and Constraints> = Body with VariableGenericName replaced by SomeGenericType

and that would only be a current TypeScript generic, so it would not impose any extra requirements on the inference logic.

@matthew-dean
Copy link

@PhilippDehler I don't see how your solution addresses the problem of passing generics to generics, since the return types are fixed and not generic.

@IARI
Copy link

IARI commented Jun 6, 2024

I dream of functional programming with TS types - I really wish that there was a chance that at some point this could happen.
But, checking in on this every other year, it doesn't look like it 😢

@geoffreytools
Copy link

@IARI did you have a look at Effect-TS? It uses a lot of functional patterns with a very pragmatic approach. The job they are doing is incredible. Its implementation of HKT is sufficient for its needs, with a fixed number of parameters of a specific variance. On my end I am working on improving free-types API and behaviour but I am convinced that you need some framework to do FP in TS comfortably, or you need to be prepared for basically rewriting it yourself.

@ecyrbe
Copy link

ecyrbe commented Jun 22, 2024

if anyone want inspiration for HKT with any number of parameters,
You can take a look at my experiment with typeskell library.
A lot of mata type programming going on here, with a template string literal type (haskell like syntax) to typescript type (without using a compiler, it's all typescript native.

https://github.com/ecyrbe/typeskell

example for defining a monad :

export interface Of<F extends Kind> {
  /**
   * of :: `a -> F a`
   */
  of: <A>(a: A) => $<F, [A, ...Tail<ToDefaultParam<F['signature']>>]>;
}

export interface Functor<F extends Kind> {
  /**
   * map :: `(a -> b) -> F a -> F b`
   */
  map: TypeSkell<'(a -> b) -> F a ..e -> F b ..e', { F: F }>;
}

export interface Applicative<F extends Kind> extends Functor<F>, Of<F> {
  /**
   * ap :: `F a -> F (a -> b) -> F b`
   */
  ap: TypeSkell<'F a ..x -> F (a -> b) ..y -> F b ..xy', { F: F }>;
}
export interface Monad<M extends Kind> extends Applicative<M> {
  /**
   * flatMap :: `(a -> M b) -> M a -> M b`
   */
  flatMap: TypeSkell<'(a -> M b ..x) -> M a ..y -> M b ..xy', { M: M }>;;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.