Typescript: Autoriser les classes à être paramétriques dans d'autres classes paramétriques

Créé le 19 nov. 2014  ·  140Commentaires  ·  Source: microsoft/TypeScript

Il s'agit d'une proposition pour autoriser les génériques comme paramètres de type. Il est actuellement possible d'écrire des exemples spécifiques de monades, mais afin d'écrire l'interface que toutes les monades satisfont, je propose d'écrire

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

De même, il est possible d'écrire des exemples spécifiques de foncteurs cartésiens, mais pour écrire l'interface que tous les foncteurs cartésiens satisfont, je propose d'écrire

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

Les paramètres de type paramétrique peuvent prendre n'importe quel nombre d'arguments:

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

Autrement dit, lorsqu'un paramètre de type est suivi d'un tilde et d'une arité naturelle, le paramètre de type doit être autorisé à être utilisé comme type générique avec l'arité donnée dans le reste de la déclaration.

Tout comme c'est le cas maintenant, lors de l'implémentation d'une telle interface, les paramètres de type générique doivent être renseignés:

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));
  }
}

En plus d'autoriser directement les compositions de types génériques dans les arguments, je propose que les typedefs prennent également en charge la définition des génériques de cette manière (voir numéro 308 ):

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

Les arités de la définition et de l'alias doivent correspondre pour que le typedef soit valide.

Suggestion help wanted

Commentaire le plus utile

avec les mentalités de HKT peuvent être changées, les habitudes brisées, les générations perdues ramenées à la vie, ce serait la chose la plus importante puisque les génériques et les nuls explicites et non définis, cela peut tout changer

S'il vous plaît, considérez-le comme une prochaine grande fonctionnalité, arrêtez d'écouter les gens qui ne cessent de vous demander un meilleur cheval, donnez-leur une ferrari f * g

Tous les 140 commentaires

Ne pas faire d'hypothèses irréfléchies, mais je pense que vous ne le saisissez pas correctement. Tous les types de paramètres nécessitent des noms de paramètres, vous avez donc probablement voulu taper

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

alors que maintenant map est une fonction qui prend un mappeur du type any (où le nom de votre paramètre est A ) à B .

Essayez d'utiliser l'indicateur --noImplicitAny pour obtenir de meilleurs résultats.

Merci, corrigé.

J'ai mis à jour mon commentaire dans une proposition.

: +1: un type de type supérieur serait un gros bonus pour la construction de programmation fonctionnelle, mais avant cela, je préférerais avoir un support correct pour les fonctions d'ordre supérieur et génériques: p

Quasi-approuvé.

Nous aimons beaucoup cette idée, mais nous avons besoin d'une implémentation fonctionnelle pour essayer de comprendre toutes les implications et les cas extrêmes potentiels. Avoir un exemple de PR qui aborde au moins 80% des cas d'utilisation serait une prochaine étape très utile.

Quelle est l'opinion des gens sur la syntaxe du tilde? Une alternative à T~2 serait quelque chose comme

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

qui permet la composition directe de génériques au lieu d'avoir besoin d'alias de type:

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

Il est étrange d'avoir une arité explicite puisque nous ne le faisons pas vraiment ailleurs, donc

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

est un peu plus clair, cependant, je sais que d'autres langages utilisent * dans des contextes similaires au lieu de ~ :

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

En poussant ce point à l'extrême, vous pourriez obtenir:

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

Je pense que T<~,~> est aussi plus clair que T~2 . Je modifierai la proposition ci-dessus. Peu m'importe si nous utilisons ~ ou * ; il ne peut tout simplement pas être un identifiant JS, donc nous ne pouvons pas utiliser, disons, _ . Je ne vois pas quel avantage la notation => offre; tous les génériques prennent certains types d'entrée et renvoient un seul type de sortie.

Une syntaxe plus légère abandonnerait entièrement l'arité des génériques; l'analyseur le comprendrait dès la première utilisation et lèverait une erreur si le reste n'était pas cohérent avec lui.

Je serais heureux de commencer à travailler sur la mise en œuvre de cette fonctionnalité. Quel est le forum recommandé pour harceler les développeurs sur les détails de l'implémentation de transpiler?

Vous pouvez enregistrer de nombreux nouveaux problèmes pour des questions plus importantes avec des exemples de code plus complexes, ou créer un problème de longue durée avec une série de questions au fur et à mesure. Vous pouvez également rejoindre la salle de discussion ici https://gitter.im/Microsoft/TypeScript et nous pouvons y parler.

@metaweta des nouvelles? Si vous avez besoin d'aide / de discussion, je serais heureux de réfléchir à cette question. Je veux vraiment cette fonctionnalité.

Non, les choses au travail ont pris le dessus sur le temps libre dont je disposais pour y travailler.

bump: y a-t-il une chance de voir cette fonctionnalité jamais envisagée?

https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288 en est toujours l'état actuel. Je ne vois rien ici qui nous ferait changer la priorité de la fonctionnalité.

Cela me semble utile dans bien plus de situations que la simple importation d'abstractions de théorie des catégories. Par exemple, il serait utile de pouvoir écrire des usines de modules qui prennent une implémentation Promise (constructeur) comme argument, par exemple une base de données avec une implémentation de promesse enfichable:

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

: +1:

avec les mentalités de HKT peuvent être changées, les habitudes brisées, les générations perdues ramenées à la vie, ce serait la chose la plus importante puisque les génériques et les nuls explicites et non définis, cela peut tout changer

S'il vous plaît, considérez-le comme une prochaine grande fonctionnalité, arrêtez d'écouter les gens qui ne cessent de vous demander un meilleur cheval, donnez-leur une ferrari f * g

Oui, je suis arrivé à cela les 15 premières minutes après avoir essayé d'ajouter des types à la base de code JS existante. Je ne passe pas à TS avant de le voir.

Puis-je aider, en fait?

Je me demande comment cela se rapporterait à # 7848? Ils sont très similaires, bien qu'ils concernent l'autre facette des types d'ordre supérieur.

La réponse de @ boris-marinov

Avoir un exemple de PR qui aborde au moins 80% des cas d'utilisation serait une prochaine étape très utile.

Maintenant, j'ai le temps de mettre en œuvre un si simple PR Hope pour obtenir des conseils des principaux développeurs, mais il n'y a pas de questions pour l'instant - tout semble bon et compréhensible. Suivra un progrès ici.

@Artazor Souhaitez-vous également jeter un œil au cracking # 7848? Cela prend en charge l'autre côté de ce problème, impliquant des génériques, et à mon humble avis, cela se sentirait incomplet sans lui (les paramètres génériques simplifieraient vraiment beaucoup de code au niveau du type).

Je pense que cette proposition est absolument magnifique. Avoir des types de types plus élevés dans TypeScript le porterait à un nouveau niveau où nous pourrions décrire des abstractions plus puissantes que ce qui est actuellement possible.

Cependant, n'y a-t-il pas un problème avec les exemples donnés dans OP? Le A dans la ligne

class ArrayMonad<A> implements Monad<Array> {

n'est utilisé dans aucune des méthodes, car elles ont toutes leur propre A générique.

De plus, si vous implémentez un foncteur avec map comme méthode qui utilise this quoi cela ressemblerait-il? Comme ça peut-être?

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

class Maybe<A> implements Functor<Maybe, A> {
  ...
}

@paldepind Découvrez # 7848. Cette discussion porte sur ce cas d'utilisation particulier, bien que, à mon humble avis, ceci et que l'on ait vraiment besoin de fusionner en un seul PR.

Quand ce truc va-t-il atterrir? Cela semble être une sorte d’essentiel.

Cela permettra-t-il également de rendre possible:

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

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

?

@whitecolor Je pense qu'il y a de plus gros poissons à faire frire en ce moment, qui méritent une priorité plus élevée:

  1. TypeScript 2.0 RC est sorti il ​​y a seulement un peu moins de 2 semaines. Cela prendra beaucoup de temps en soi.
  2. bind , call et apply , fonctions JS natives, ne sont pas typées. Cela dépend en fait de la proposition de génériques variadiques . Object.assign également besoin d'un correctif similaire, mais les génériques variadiques seuls ne résoudront pas cela.
  3. Des fonctions telles que les méthodes _.pluck Lodash, les méthodes get et set , etc. des modèles Backbone, etc. Cela peut également

Non pas que je ne veuille pas de cette fonctionnalité (j'adorerais une telle fonctionnalité), je ne la vois tout simplement pas comme probable bientôt.

@isiahmeadows
Merci pour l'explication. Oui, le troisième élément de la liste est très important, en attente de https://github.com/Microsoft/TypeScript/issues/1295 aussi.

Mais j'espère pour le numéro actuel peut-être en 2.1dev d'une manière ou d'une autre.

Je suis d'accord. J'espère qu'il pourra réussir.

(Le polymorphisme de rang 2, que souhaite ce numéro, est également une nécessité
Utilisateurs de Fantasy Land, pour saisir correctement les différents ADT dans cette spécification.
Ramda est un bon exemple de bibliothèque qui en a vraiment besoin.)

Le mardi 6 septembre 2016, 11h00, Alex [email protected] a écrit:

@isiahmeadows https://github.com/isiahmeadows
Merci pour l'explication. Oui, le troisième élément de la liste est très important,
en attente de # 1295 https://github.com/Microsoft/TypeScript/issues/1295
aussi.

Mais j'espère pour le numéro actuel peut-être en 2.1dev d'une manière ou d'une autre.

-
Vous recevez cela parce que vous avez été mentionné.

Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -244978475,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AERrBMvxBALBe0aaLOp03vEvEyokvxpyks5qnX_8gaJpZM4C99VY
.

Il semble que cette fonctionnalité nous aiderait beaucoup à définir les formes de réaction. Par exemple, vous avez struct:

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

J'ai un gestionnaire, défini comme:

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

ce gestionnaire est passé comme accessoire aux composants React. J'ai aussi une fonction, qui prend la structure et retourne la même structure, mais avec des gestionnaires au lieu de valeurs:

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

Pour le moment, je ne peux pas taper correctement cette fonction, car TS ne peut pas produire de type basé sur une structure d'un autre type.

Vache je pourrais taper makeForm avec HKT?

Hm, intéressant.

Peut-être que quelque chose comme ça peut être 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>

@ boris-marinov Le point le plus intéressant est cette ligne:

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

pourrait valoir la peine de mentionner que HKT aurait pu être une réponse à des types dits partiels (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

Pas assez. {x: number?} n'est pas attribuable à {x?: number} , car un
est garanti d'exister, tandis que l'autre ne l'est pas.

Le mar 11 octobre 2016, 09:16, Aleksey Bykov [email protected] a écrit:

pourrait valoir la peine de mentionner que HKT aurait pu être une réponse à ce qu'on appelle
types partiels (# 4889 (commentaire)
https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-247721155
):

tapez MyDataProto un: K;
un autre: K;
encoreUn autre: K;
} type Identique = a; type Facultatif = a ?;
= {get (): a;

;
;
;

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -252913109,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AERrBNFYFfiW01MT99xv7UE2skQ3qiPMks5qy4wRgaJpZM4C99VY
.

@isiahmeadows vous avez raison, pour le moment il n'y a aucun moyen / syntaxe de rendre une propriété vraiment optionnelle basée uniquement sur son type, et c'est dommage

Encore une autre: ce serait bien si la propriété pouvait être faite readonly . Semble une sorte de fonctionnalité de macros requise.

Juste jeter ceci là-bas ... Je préfère la syntaxe * syntaxe ~ . Quelque chose à propos de ~ semble tellement éloigné du point de vue de la disposition du clavier. De plus, je ne sais pas pourquoi, mais je pense que * semble un peu plus lisible / distinguable avec tous les crochets angulaires qui sont dans le mélange. Sans oublier que les personnes familiarisées avec d'autres langages comme Haskell pourraient immédiatement associer la syntaxe à HKT. Cela semble un peu plus naturel.

Je devrais être d'accord avec la syntaxe * . Premièrement, il est plus distinctif,
et deuxièmement, il représente mieux un type "tout type fonctionne".


Isiah Meadows
[email protected]

Le dimanche 6 novembre 2016 à 00h10, Landon Poch [email protected]
a écrit:

Juste jeter ça là-bas ... Je préfère la syntaxe * à la syntaxe ~.
Quelque chose à propos de ~ semble tellement éloigné d'une disposition de clavier
perspective. Aussi, je ne sais pas pourquoi, mais je pense que * semble un peu plus
lisible / distinguable avec tous les crochets angulaires qui sont dans le mélange.
Sans oublier que les personnes familiarisées avec d'autres langues comme Haskell pourraient
associez immédiatement la syntaxe à HKT. Cela semble un peu plus naturel.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -258659277,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AERrBHQ4SYeIiptB8lhxEAJGOYaxwCkiks5q7VMvgaJpZM4C99VY
.

Jalon: community ? Quel est l'état actuel de ce problème / fonctionnalité?

@whitecolor le statut est bricolage (faites-le vous-même)

Le problème a le libellé Accepting PRs . cela signifie que les demandes d'extraction pour implémenter cette fonctionnalité sont les bienvenues. Voir https://github.com/Microsoft/TypeScript/wiki/FAQ#what -do-the-labels-on-these-issues-mean pour plus de détails.

Veuillez également consulter https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288

Ok, je vois les étiquettes, j'ai juste des doutes que si l'équipe non TS est capable de l'accomplir.

Maintenant, j'ai le temps de mettre en œuvre un si simple PR Hope pour obtenir des conseils des principaux développeurs, mais il n'y a pas de questions pour l'instant - tout semble bon et compréhensible. Suivra un progrès ici.

@Artazor Avez-vous de la chance avec ça?

@raveclassic - cela s'est avéré plus difficile qu'il n'y paraissait, mais j'espère toujours aller de l'avant. Syntaxiquement, c'est évident, mais les règles / phases de vérification de type ne sont pas aussi claires pour moi que je le souhaite -)

Essayons de relancer mon activité -)

Il suffit de suivre une progression et le chemin du développement de l'idée. J'ai envisagé trois options pour implémenter cette fonctionnalité.

J'ai prévu d'enrichir une TypeParameterDeclaration avec higherShape propriété TypeParameterDeclaration en option

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

et ont envisagé trois options de mise en œuvre de HigherShape

1. Arity simple pour le domaine

type HigherShape = number

il correspond à l'usage suivant:

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

dans ce cas le plus simple, il semble que le type number serait suffisant. Néanmoins, nous devrions être en mesure de déterminer une forme supérieure réelle pour chaque type donné pour être sûr de pouvoir l'utiliser comme argument de type pour les exigences de forme spécifiques. Et ici, nous sommes confrontés à un problème: la forme supérieure de la classe Demo elle-même n'est pas exprimable sous forme de nombre. Si c'est le cas, alors il devrait être représenté par 2 - car il a deux paramètres de type,
et il serait possible d'écrire

var x: Demo<Array, Demo>

puis lutter contre le problème de vérification de type différée avec la propriété .both . Ainsi le type number n'est pas suffisant (je crois);

en fait, le type Demo a la forme d'ordre supérieur suivante:

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

2. Domaine et co-domaine entièrement structurés

Ensuite, j'ai étudié la représentation opposée, la plus complète des formes supérieures, qui permettrait de représenter des formes telles que celles mentionnées ci-dessus, et même pire:

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

La structure des données pour cela est simple, mais elle n'interagit pas bien avec le système de type TypeScript. Si nous autorisons de tels types d'ordre supérieur, nous ne saurons jamais si * signifie le type de base, qui pourrait être utilisé pour le typage des valeurs. D'ailleurs, je n'ai même pas réussi à trouver une syntaxe appropriée pour exprimer des contraintes d'ordre supérieur aussi monstrueuses.

3. Domaine structuré / co-domaine simple implicite

L'expression de type idée principale (même avec des arguments de type réels) aboutit toujours à un type de base - qui peut être utilisé pour saisir une variable. D'un autre côté, chaque paramètre de type peut avoir ses propres paramètres de type détaillés dans le même format que celui utilisé ailleurs.

C'était ma décision finale que j'essaierais de défendre.

type HigherShape = NodeArray<TypeParameterDeclaration>;

exemple:

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

Ici, je veux noter que M est local pour T1<M extends A> extends Z<M> et existe dans une portée de visibilité plus profonde que T1. Ainsi M n'est pas disponible dans le corps SomeClass .
Et * signifie simplement un nouvel identifiant (type anonyme) qui n'entre jamais en conflit avec quoi que ce soit (et pourrait être implémenté plus tard)


Ainsi la signature finale du TypeParameterDeclaration

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

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

Vous voulez entendre une opinion sur @DanielRosenwasser , @ aleksey-bykov, @isiahmeadows et autres -)

Cela me convient, mais je connais très peu la structure interne de la base de code de TypeScript.

Je voudrais ajouter ma voix à la chorale demandant cela et vous encourager, Artazor! :)

Cette fonctionnalité me serait utile dans ma mise en œuvre de la sécurisation de type Redux.

@michaeltontchev Quels problèmes rencontrez-vous pour sécuriser Redux?

Au cas où cela vous intéresserait, j'ai récemment publié https://github.com/bcherny/tdux et https://github.com/bcherny/typed-rx-emitter , qui s'appuient sur des idées de Redux et EventEmitter.

Maintenant , regarde, besoin de rebasage à la branche @rbuckton # 13487 avec des paramètres génériques par défaut. Dans un autre cas, nous entrerons en conflit largement.

@bcherny - merci pour les liens, je vais les vérifier!

Je cherchais à rendre combineReducers un type sûr en m'assurant qu'il dispose d'un réducteur du bon type pour chaque propriété de l'état (sans extras). J'ai réussi à le faire dans ce cas précis sans génériques imbriqués, mais une solution imbriquée aurait été plus agréable. J'ai ce qui suit:

import { combineReducers, Reducer } from 'redux';

interface IState {
    // my global state definition
}

type StatePropertyNameAndTypeAwareReducer\<S> = {
    [P in keyof S]: Reducer<S[P]>;
};

let statePropertyToReducerMap : StatePropertyNameAndTypeAwareReducer<IState> = {
    navBarSelection: navBarReducer,
};

let combinedReducers = combineReducers<IState>(statePropertyToReducerMap);

Fondamentalement, le type que j'introduis ci-dessus garantit que les mappages de réducteurs que vous transmettez à combineReducers couvrent toutes les propriétés de votre état et ont les types de retour appropriés. Je n'ai pas pu trouver une telle solution lors de la recherche en ligne - il me semble que cela ne peut pas être fait sans la fonction keyof, qui est sortie il y a seulement deux mois :)

Je m'attends à ce que la fonctionnalité keyof soit également utile pour ImmutableJs pour rendre les setters et les getters sûrs, bien que vous ayez toujours besoin d'outils supplémentaires à ce sujet.

Edit: pour clarifier, les génériques imbriqués m'auraient permis de ne pas avoir à coder en dur le type Reducer dans le type StatePropertyNameAndTypeAwareReducer, mais au lieu de le passer en tant que générique, mais il faudrait qu'il s'agisse d'un générique imbriqué, ce qui n'est pas possible à le moment.

Edit2: a créé un problème pour Redux ici: https://github.com/reactjs/redux/issues/2238 On dirait qu'ils ne font pas beaucoup de TypeScript, ils recherchent donc des personnes TypeScript qui connaissent Redux pour peser.

Comment ça se passe?

Peut-être une question naïve, mais pourquoi ~ ou * au lieu d'un paramètre générique régulier? Est-ce pour indiquer qu'il n'est pas utilisé? C'est à dire. pourquoi pas:

type Functor<A<T>> = {
  map(f: (value: T) => U): A<U>
}

Ou:

kind Functor<A<T>> = {
  map(f: (value: T) => U): A<U>
}

Ou même:

abstract type Functor<A<T>> = {
  map(f: (value: T) => U): A<U>
}

@bcherny Je pense que cela provoque des ambiguïtés dans la syntaxe, puisque Functor<A<T>> signifiait auparavant " A de T ", où T est un type en local portée. C'est peu probable, mais cette syntaxe pourrait également finir par être un changement radical pour certaines bases de code, pour la même raison.

@masaeedu je vois. La nouvelle syntaxe signifie "bind T paresseusement", plutôt que "bind T strictement dans la portée actuelle".

Cela dit, je pense que la proposition de T: * => * est la plus logique ici, car il y a un "art antérieur" pour cela.

Dans Haskell, l'opérateur -> est en fait un type paramétré (il est peut-être plus facile de visualiser Func<TArg, TRet> ). Le constructeur de type -> accepte deux types arbitraires T et U et produit le type d'un constructeur de valeur (c'est-à-dire une fonction) qui mappe des valeurs de type T à valeurs de type U .

Ce qui est intéressant, c'est que c'est aussi un constructeur aimable! Le constructeur kind -> accepte deux types arbitraires T* et U* (astérisque juste pour la distinction visuelle), et produit le genre de constructeur de type qui mappe les types de kind T* aux types du genre U* .

Vous remarquerez peut-être un motif à ce stade. La syntaxe et la sémantique utilisées pour définir et faire référence à des types sont simplement réutilisées pour définir et faire référence à des types. En fait, il n'est même pas réutilisé, il définit simplement implicitement des choses dans deux univers différents à la fois. (le fait qu'il soit isomorphe signifie en fait qu'il est capable de définir des choses en niveaux infinis, valeurs -> types -> genres -> tris -> ..., sauf pour le malheureux * , mais c'est un sujet pour une autre heure)

En fait, ce modèle a tellement de sens que certaines personnes ont implémenté une extension GHCi largement utilisée qui la généralise à tous les constructeurs de types, pas seulement -> . L'extension s'appelle "types de données", et c'est ainsi que Haskell vient de ses listes hétérogènes ( [] est à la fois le type de listes de valeurs et le type de listes de types), des tuples hétérogènes, "longueur intelligente "vecteurs, et bien d'autres fonctionnalités en plus.

Peut-être que nous ne voulons pas aller aussi loin que DataKinds l'instant, nous allons donc nous en tenir aux constructeurs de genre * et -> , mais si nous suivons la syntaxe proposée par Daniel , ou plus généralement rendre les définitions de genre isomorphes aux définitions de type, nous nous ouvrons pour profiter des développements futurs dans ce domaine.

Dans le prolongement de mon précédent article, j'aimerais vous recommander d'utiliser any au lieu de * ; cela représente à la fois le type de chaque valeur et le type de chaque type. Si la syntaxe semble confuse, nous pourrions retirer une page du livre de Haskell et utiliser un préfixe ' pour lever l'ambiguïté des genres et des types.

L'exemple d'OP s'écrirait alors comme ceci:

interface Monad<(T: 'any => 'any)> {
    // ...
}

Nitpick: Je trouve any déroutant en général dans le sens où il fait deux choses différentes.
C'est un super type de tous les autres, comme jamais est un sous-type de tous les autres, donc si une fonction demande un paramètre any , vous pouvez mettre n'importe quoi. Jusqu'ici tout va bien.
La partie où cela devient drôle, c'est quand une fonction demande quelque chose de spécifique, et que vous fournissez any . Ce type vérifie, tandis que tout autre type plus large que ce qui a été demandé le ferait plutôt en erreur.
Mais ouais, peu importe.

Sur une autre note, ' serait déroutant car il est également utilisé dans les chaînes littérales.

@Artazor Des nouvelles avec ça? La dernière fois que vous avez mentionné, vous devez rebaser les paramètres génériques par défaut. Et il me semble que vous êtes le seul assez proche d'un POC fonctionnel.

Il convient également de réfléchir à la manière dont cela interagit avec le sous-typage. Utiliser * en lui-même n'est pas suffisant; dans les langages qui utilisent le polymorphisme ad hoc au lieu du polymorphisme borné, vous avez des types de contraintes pour limiter les arguments de type acceptables. Par exemple, le genre de Monad T est en fait Constraint , et non * .

Dans TypeScript, nous utilisons le sous-typage structurel à la place, donc nos types doivent refléter les relations de sous-types entre les types. L'article de Scala sur ce sujet pourrait donner de bonnes idées sur la façon de représenter la variance et les relations de sous-types dans un système de genre: « Vers des droits égaux pour les types supérieurs ».

Des progrès à ce sujet?

Une approche alternative par @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe

Le problème avec l'approche adoptée par fp-ts est qu'elle vous oblige à réimplémenter des bibliothèques éprouvées par ailleurs. Pour moi, l'idée du tapuscrit est de pouvoir taper correctement ce qui est actuellement considéré comme les meilleures pratiques en JavaScript, et non de vous forcer à le réimplémenter d'une manière ts.

Il y a beaucoup d'exemples ici qui montrent que HKT est nécessaire pour décrire correctement les contrats que nous utilisons actuellement dans js libs, que ce soit des formes fantasy land, ramda ou react.

Ce serait vraiment chouette de voir ça mis en œuvre :)

~ Y a-t-il quelqu'un qui souhaite / est capable de travailler sur ce salaire? N'hésitez pas à me contacter pour en discuter. Ou n'importe qui est capable de coacher quelqu'un que nous pourrions trouver pour travailler là-dessus, merci de me le faire savoir. ~ [EDIT: J'ai [probablement décidé] (https://github.com/keean/zenscript/issues/35#issuecomment -357567767) d'abandonner cet écosystème et mon commentaire ultérieur dans ce fil m'a fait réaliser que ce serait probablement une entreprise massive]

Une approche alternative par @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe

Je n'ai pas pris la peine de bien comprendre cela, car j'observe que le résultat map spécifie toujours explicitement le type de conteneur Option et n'est donc pas entièrement générique de la même manière que les types de types plus élevés (HKT) peut fournir:

function map<A, B>(f: (a: A) => B, fa: HKTOption<A>): Option<B> {
  return (fa as Option<A>).map(f)
}

Comme @spion l'a noté le 26 août 2016 , les HKT sont nécessaires pour rendre générique toute fonction nécessitant une fabrique et dans laquelle le type de conteneur paramétré doit être lui-même générique. Nous avions exploré cela dans nos discussions sur la conception du langage de programmation.

PS Si vous êtes curieux, cette fonctionnalité joue un rôle important dans mon analyse (y compris celle de @keean ) du paysage du langage de programmation .

@ shelby3 FWIW Option map (in Option.ts ) n'est pas générique car représente l' instance tandis que Functor map (in Functor.ts ) est générique car représente la classe de type . Ensuite, vous pouvez définir une fonction générique lift qui peut fonctionner avec n'importe quelle instance de foncteur.

Ce serait vraiment chouette de voir ça mis en œuvre :)

Je suis entièrement d'accord :)

@ shelby3 : pour fusionner une fonctionnalité comme celle-ci, votre meilleur pari pourrait être d'obtenir
les hiérarchiser sur la feuille de route TS; J'ai eu quelques PR qui ont principalement
feedback / fusion soit lors de petites corrections, soit si elles cherchaient déjà
en eux. Je ne veux pas être négatif mais c'est une considération si tu es
sur le point d'investir des ressources dans ce domaine.

Le 8 janvier 2018 à 16 h 05, "shelby3" [email protected] a écrit:

Y a-t-il quelqu'un qui souhaite / est capable de travailler sur ce salaire? Hésitez pas à contacter
moi [email protected] pour discuter. Ou n'importe qui est capable de coacher quelqu'un
nous pourrions trouver à travailler là-dessus, merci de me le faire savoir.

Une approche alternative par @gcanti https://github.com/gcanti
https://medium.com/@gcanti/higher -kinded-types-in
typographie-statique-et-fantastique-land-d41c361d0dbe

Je n'ai pas pris la peine de grok complètement, car j'observe la carte résultante
spécifie toujours explicitement le type de conteneur Option et n'est donc pas entièrement
générique de la manière dont les types de type supérieur (HKT) peuvent fournir:

carte de fonction (f: (a: A) => B, fa: HKTOption ): Option { return (fa comme option ) .map (f) }

Les HKT sont nécessaires pour rendre générique toute fonction nécessitant une usine et
dans lequel le type de type conteneur paramétré doit être lui-même générique. Nous avons eu
exploré ce https://github.com/keean/zenscript/issues/10 dans notre
discussions sur la conception du langage de programmation.

PS Si vous êtes curieux, cette fonctionnalité joue un rôle important dans mon
(y compris l' analyse de @keean https://github.com/keean ) des
paysage du langage de programmation
https://github.com/keean/zenscript/issues/35#issuecomment-355850515 . je
réaliser que nos opinions ne sont pas entièrement corrélées aux priorités de Typescript
avec l'objectif principal d'être un sur-ensemble de Javascript / ECMAScript et de support
cet écosystème.

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-355990644 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AC6uxYOZ0a8G86rUjxvDaO5qIWiq55-Fks5tIi7GgaJpZM4C99VY
.

@gcaniti excuses pour le bruit et merci pour l'explication supplémentaire. J'aurais dû étudier plus avant de commenter. Bien sûr, c'est mon erreur de conceptualisation car (je sais déjà) un foncteur nécessite une implémentation d'instance.

Afaics, votre astucieux "hack" permet de faire référence à une usine de manière générique (par exemple lift ), mais nécessite le passe-partout supplémentaire de Module Augmentation pour (mettre à jour et) spécialiser chaque typage de l'usine générique pour le type spécialisé du foncteur , par exemple Option . Est-ce que ce passe-partout ne serait pas nécessaire pour chaque utilisation générique d'une usine générique, par exemple l'exemple générique sort @keean et j'ai discuté? Peut-être y a-t-il d'autres cas secondaires à découvrir également?

Kotlin a-t-il copié votre idée ou vice versa? (quelques critiques supplémentaires sur ce lien mais je ne sais pas si elles s'appliquent au cas Typescript)

Je ne veux pas être négatif, mais c'est une considération si vous êtes sur le point d'investir des ressources dans ce domaine.

Ouais, cette pensée m'est venue aussi. Merci de l'avoir exprimé. Je soupçonne que l' une des considérations serait les impacts généralement sur le système de types et les cas de coin qu'il pourrait générer, comme l'a souligné @masaeedu. Peut-être y aura-t-il une résistance à moins que cela ne soit bien pensé et démontré.

Remarque Je regarde également Ceylan pour mieux déterminer quel sera leur niveau d'investissement dans la cible de compilation EMCAScript. (Je dois étudier davantage).

Je viens également d'être mordu par cette limitation. Je voudrais que I dans l'exemple suivant soit automatiquement déduit:


interface IdType<T> {
  id: T;
}

interface User {
  id: number;
  name: string;
}

function doStuff<T extends IdType<I>>() {
  const recs = new Map<I, T>();
  return {
    upsert(rec: T) {
      recs.set(rec.id, rec);
    },
    find(id: I) {
      return recs.get(id);
    },
  };
}

(function () {
  const stuff = doStuff<User>();
  stuff.upsert({id: 2, name: "greg"});
  console.log(stuff.find(2));
})();

Autant que je sache, cela nécessite un type de type supérieur ou bien la spécification d'un paramètre générique en double (par exemple doStuff<User, number>() ) qui semble redondant.

J'ai également été frappé récemment par cette limitation.

J'ai travaillé sur une bibliothèque pour des promesses . Il fournit diverses fonctions utilitaires pour travailler avec eux.

Une caractéristique centrale de la bibliothèque est qu'elle renvoie le même type de promesse que vous y mettez. Donc, si vous utilisiez une promesse Bluebird et appeliez l'une des fonctions, elle renverrait une promesse Bluebird avec toutes les fonctionnalités supplémentaires qu'elles fournissent.

Je voulais encoder cela dans le système de types, mais je me suis vite rendu compte que cela nécessitait de travailler avec un type P de type * -> * tel que P<T> extends Promise<T> .

Voici un exemple d'une telle fonction:

/**
* Returns a promise that waits for `this` to finish for an amount of time depending on the type of `deadline`.
* If `this` does not finish on time, `onTimeout` will be called. The returned promise will then behave like the promise returned by `onTimeout`.
* If `onTimeout` is not provided, the returned promise will reject with an Error.
*
* Note that any code already waiting on `this` to finish will still be waiting. The timeout only affects the returned promise.
* <strong i="14">@param</strong> deadline If a number, the time to wait in milliseconds. If a date, the date to wait for.
* <strong i="15">@param</strong> {() => Promise<*>} onTimeout Called to produce an alternative promise once `this` expires. Can be an async function.
*/
timeout(deadline : number | Date, onTimeout ?: () => PromiseLike<T>) : this;

Dans la situation ci-dessus, j'ai pu éviter d'avoir besoin de types plus élevés en utilisant le type plutôt hacky this .

Cependant, le cas suivant ne peut pas être résolu:

/**
* Returns a promise that will await `this` and all the promises in `others` to resolve and yield their results in an array.
* If a promise rejects, the returned promise will rejection with the reason of the first rejection.
* <strong i="21">@param</strong> {Promise<*>} others The other promises that must be resolved with this one.
* <strong i="22">@returns</strong> {Promise<*[]>} The return type is meant to be `Self<T[]>`, where `Self` is the promise type.
*/
and(...others : PromiseLike<T>[]) : ExtendedPromise<T[]>;

Parce qu'il n'y a pas de hack qui me permet de faire this<T[]> ou quelque chose comme ça.

Notez mes petites excuses dans la documentation.

Vous avez un autre scénario où je pense que cette fonctionnalité serait utile (en supposant que j'ai bien interprété la proposition), comme indiqué par la référence ci-dessus.

Dans le package en question, il est nécessaire d'avoir une classe ou une fonction générique non typée utilisée comme type, car les types génériques sont généralement créés par l'utilisateur.

En appliquant la proposition à mon scénario, je pense que cela ressemblerait à quelque chose comme:

import { Component, FunctionalComponent } from 'preact';

interface IAsyncRouteProps {
    component?: Component<~,~> | FunctionalComponent<~>;
    getComponent?: (
        this: AsyncRoute,
        url: string,
        callback: (component: Component<~,~> | FunctionalComponent<~>) => void,
        props: any
    ) => Promise<any> | void;
    loading?: () => JSX.Element;
}

export default class AsyncRoute extends Component<IAsyncRouteProps, {}> {
    public render(): JSX.Element | null;
}

Étant donné qu'il n'y a aucun moyen de référencer les types génériques de manière fiable dans mon implémentation, je suis sûr que j'ai manqué quelque chose.

@ Silic0nS0ldier En fait, ce cas peut être résolu dès maintenant. Vous utilisez un type de constructeur structurel, comme ceci:

type ComponentConstructor = {
    new<A, B>() : Component<A, B>;
}

Et puis dites, component ?: ComponentConstructor .

De manière encore plus générale, vous pouvez en fait avoir un type de fonction générique:

let f : <T>(x : T) => T

C'est ce qu'on appelle le polymorphisme paramétrique de rang n et c'est en fait une caractéristique assez rare dans les langages. Il est donc encore plus surprenant de savoir pourquoi TypeScript n'a pas de types de type supérieur, ce qui est une fonctionnalité beaucoup plus courante.

La limitation discutée ici apparaîtra si vous devez référencer un TComponent<T, S> spécifique. Mais dans votre cas, cela semble inutile.


Vous pouvez également utiliser typeof Component qui vous donnera le type du constructeur Component mais cela causera divers problèmes avec les sous-types.

@ Silic0nS0ldier Voir mes commentaires sur l'essentiel.

@chrisdavies Est-ce que ça marche?

interface IdType<T> {
    id: T;
}

interface User {
    id: number;
    name: string;
}

function doStuff<T extends IdType<any>>() {
    type I = T['id']; // <==== Infer I
    const recs = new Map<I, T>();
    return {
        upsert(rec: T) {
            recs.set(rec.id, rec);
        },
        find(id: I) {
            return recs.get(id);
        },
    };
}

(function() {
    const stuff = doStuff<User>();
    stuff.upsert({ id: 2, name: "greg" });
    console.log(stuff.find(2));
})();

@ jack-williams Ouais. Cela fonctionne pour mon scénario. Je n'avais pas trouvé cet exemple dans la documentation (même si je suis connu pour avoir manqué des choses!). Je vous remercie!

J'ai beaucoup réfléchi à cette fonctionnalité, et j'ai quelques réflexions à ce sujet, en me penchant vers une sorte de spécification, mais je peux encore voir beaucoup de problèmes. Mes suggestions sont un peu différentes de ce qui a été proposé jusqu'à présent.


Tout d'abord, je pense que l'utilisation de tout type de syntaxe T<*, *> pour les constructeurs de types est une mauvaise idée car elle ne s'adapte pas bien à la complexité du constructeur de types. Je ne suis pas non plus sûr qu'il soit utile de spécifier le type de constructeur de type chaque fois qu'il est référencé, car nous ne le faisons pas pour les fonctions, même pour les fonctions avec des paramètres de type.

Je pense que la meilleure façon d'implémenter cela est de traiter les types de type supérieur comme les autres types, avec des noms réguliers, et de définir une bonne relation de sous-type sur les constructeurs de types eux-mêmes qui peuvent être utilisés pour imposer des contraintes.

Je pense que nous devrions utiliser une sorte de préfixe ou de suffixe pour les distinguer des autres types, principalement pour protéger les utilisateurs des messages d'erreur incompréhensibles impliquant des constructeurs de types lorsqu'ils ont seulement voulu écrire du code normal. J'aime un peu le look de: ~Type, ^Type, &Type ou quelque chose comme ça.

Ainsi, par exemple, une signature de fonction peut être:

interface List<T> {
    push(x : T);
}

function mapList<~L extends ~List, A, B>(list : L<A>, f : (x : A) => B) : L<B>;

(Je n'utilise pas volontairement le préfixe ~ pour les types construits)

En utilisant extends ici, j'ai essentiellement dit deux choses:

**1. Si c'est nécessaire: ~L est un constructeur de type qui a le même genre que ~List , c'est-à-dire le genre * -> * (ou peut-être * => * , depuis => est la flèche TypeScript).

  1. ~L est un sous-type de ~List . **

L'utilisation de extends pour désigner le type d'un constructeur de type évolue vers des constructeurs de type arbitrairement complexes, y compris des choses comme ((* => *) => (* => *)) => * .

Vous ne pouvez pas vraiment voir ce type dans l'identifiant du type, mais je ne pense pas que vous en ayez besoin. Je ne suis même pas sûr qu'une relation de sous-type entre les constructeurs de types doive préserver les genres, donc (1) pourrait ne pas être nécessaire.

Pas de construction de types incomplets

Je pense que nous ne devrions pas soutenir la construction de types incomplets. Autrement dit, quelque chose comme ceci:

(*, *) => * => *

Je pense que cela créerait plus de problèmes que cela ne vaut la peine. c'est-à-dire que chaque constructeur de type doit construire un type concret, et ce type concret doit être spécifié dans le contexte où le TC est défini.

Méthode structurelle de définition des constructeurs de types

Je pense aussi qu'il devrait y avoir une manière structurelle de spécifier les constructeurs de types, de la même manière que tout autre type peut être spécifié structurellement, y compris les types de fonctions génériques d'ordre très élevé. J'ai pensé à une syntaxe telle que:

~<A, B> { 
    a : A,
    b : B
}

Ce qui est similaire à la syntaxe existante pour les types de fonction avec des paramètres de type:

<A, B>() => { a : A, b : B};

Les deux peuvent même être combinés pour obtenir ceci:

~<A><B, C> => [A, B, C]

Qui est un constructeur de type construisant un type de fonction générique.

L'avantage est que ces types de structure peuvent être utilisés lors de la spécification d'autres types de structure, lors de la spécification de contraintes de type, etc. Parfois, cela signifie qu'ils peuvent utiliser des symboles locaux de référence qui ne peuvent être référencés nulle part ailleurs.

Voici un exemple:

type List<A, B> = ...;

type AdvancedType<~L extends ~<A>List<A, B>, B> = ...;

Dans l'exemple ci-dessus, le constructeur de type structurel ~<A>List<A, B> référence au paramètre de type B . Il n'est pas possible de spécifier cette relation d'une autre manière, du moins sans encoder le type partiellement construit List<A, *> . Il existe également d'autres exemples.

La relation de sous-type

La relation de sous-type semble avoir un sens, mais j'ai rencontré un certain nombre de difficultés pour essayer de la caractériser.

Ma première idée était la suivante. Pour que ~A soit un sous-type de ~B :

  1. (a) Ils doivent avoir le même type (en termes d'arité, pas de contraintes).
  2. (b) Pour chaque paramétrage légal T₁, T₂, ... de ~A , A<T₁, T₂, ...> doit être un sous-type de B<T₁, T₂, ...> .

Cependant, cela a plusieurs limites.

  1. La classe MySpecialPromise implémente PromiseLike {} Dans ce cas, ~MySpecialPromise n'est pas un sous-type de ~PromiseLike car ils ont des types différents.

  2. class MyArrayPromisemet en œuvre PromiseLike

    La relation de sous-type n'est pas non plus conservée dans ce cas.

Une version plus généralisée de (b) est la suivante:

(b) Pour chaque paramétrage légal T₁, T₂, ... de ~A , il existe un paramétrage S₁, S₂, ... de ~B tel que A<T₁, T₂, ...> soit un sous-type de B<S₁, S₂, ...> .

En d'autres termes, il existe une application F (T₁, T₂, ...) = S₁, S₂, ... avec les propriétés ci-dessus. Ce mappage doit être utilisé pour construire le B<...> paramétré A<...> partir d'un

Le problème avec cette relation est que je ne sais pas comment il serait possible de trouver le mappage correct. Dans les langues à typage nominal, chaque instruction du type:

A<...> extends B<...>

Définit un mappage entre les paramètres de type de ~A et les paramètres de type de ~B , c'est ainsi que le mappage peut être récupéré. Cependant, dans le système de typage structurel de TypeScript, nous ne pouvons pas nous fier à des déclarations explicites de ce type.

Une façon est de ne prendre en charge que les constructeurs de types pour les types avec les bonnes informations de type, comme les clauses implements ou une sorte de membre de type abstrait similaire à Scala. Je ne sais pas si c'est la voie à suivre, cependant.

@GregRos - Notes intéressantes! Quelques questions.


Qu'entendez-vous par type de béton? Voulez-vous dire quelque chose avec kind * , ou un type sans paramètre de type lié?


Pas de construction de types incomplets
Je pense que nous ne devrions pas soutenir la construction de types incomplets. Autrement dit, quelque chose comme ceci:
(*, *) => * => *

Qu'entendez-vous par construction de types incomplets? Voulez-vous dire que chaque application comme L<A> devrait avoir le genre * . Le constructeur de types de paires est-il spécial dans votre exemple, par exemple, (* => *) => * => * serait-il correct?


Méthode structurelle de définition des constructeurs de types

~<A, B> { 
    a : A,
    b : B
}
inferface TyCon<A, B> { 
    a : A,
    b : B
}

Ces exemples sont-ils différents, à part le premier étant anonyme?


La relation de sous-type

~A et ~B ne font pas référence à des types, est-il donc logique qu'ils aient une relation de sous-type? Quand avez-vous réellement besoin de vérifier qu'un constructeur est un «sous-type» d'un autre? Est-il possible d'attendre que les constructeurs soient appliqués et de vérifier les types résultants?

@ jack-williams Merci pour vos commentaires!

Qu'entendez-vous par construction de types incomplets? Voulez-vous dire que chaque application comme L<A> devrait avoir kind *. Le constructeur de type de paire est-il spécial dans votre exemple, par exemple, (* => *) => * => * serait-il correct?

Oui, exactement. Chaque application comme L<A> doit avoir le genre * . Je ne sais pas à quel point je suis convaincu.


Ces exemples sont-ils différents, à part le premier étant anonyme?

Le premier est une expression de type, tandis que le second est une déclaration. Ils sont identiques à bien des égards, de la même manière que ces types sont identiques à bien des égards:

{
     a : number;
     b : string;
}

interface Blah {
    a : number;
    b : string;
}

La syntaxe a plusieurs motivations:

  1. Comme tout le reste de TypeScript, il permet de spécifier les constructeurs de types de manière structurelle et anonyme.
  2. Une expression de type (comme l'objet anonyme typé mentionné ci-dessus) peut être utilisée dans certains contextes où les instructions de déclaration ne peuvent pas être utilisées, comme dans les signatures de type de fonctions. Cela leur permet de capturer des identifiants locaux et d'exprimer des choses qui ne peuvent pas être exprimées autrement.

~A et ~B ne font pas référence aux types, est-il donc logique qu'ils aient une relation de sous-type? Quand avez-vous réellement besoin de vérifier qu'un constructeur est un «sous-type» d'un autre? Est-il possible d'attendre que les constructeurs soient appliqués et de vérifier les types résultants?

Les constructeurs de type peuvent ou non être considérés comme des types. Je propose de les considérer comme des types, juste des types incomplets qui n'ont aucune valeur et ne peuvent apparaître dans aucun contexte qui nécessite le type d'une valeur. C'est la même philosophie adoptée par Scala, dans ce document

Par relation de sous-type, j'entends essentiellement une sorte de relation de «conformité» qui peut être utilisée pour contraindre les constructeurs de types. Par exemple, si je veux écrire une fonction qui fonctionne sur toutes sortes de promesses de différents types, comme Promise<T> , Bluebird<T> , et ainsi de suite, j'ai besoin de la possibilité de contraindre les paramètres TC avec le interface PromiseLike<T> d'une certaine manière.

Le mot naturel pour ce type de relation est une relation de sous-type.

Regardons un exemple. En supposant que nous ayons élaboré une relation de sous-type entre les constructeurs de types, je peux écrire une fonction comme celle-ci:

function mapPromise<~P extends ~PromiseLike, A, B>(promise : P<A>, func : (x : A) => B) : P<B>;

Et la contrainte ~P extends ~PromiseLike est censée garantir qu'il s'agit d'une fonction qui fonctionne sur les promesses, et uniquement sur les promesses. La contrainte garantira également qu'à l'intérieur du corps de la fonction, promise sera connu pour implémenter PromiseLike<A> , et ainsi de suite. Après tout, les membres reconnus par TypeScript dans le corps de la fonction sont précisément ceux dont on peut prouver l'existence par le biais de contraintes.

De la même manière Promise<T> extends PromiseLike<T> , parce qu'ils sont structurellement compatibles et peuvent être remplacés les uns par les autres, ~Promise extends ~PromiseLike parce qu'ils construisent des types structurellement compatibles et peuvent donc être remplacés les uns par les autres.


Pour souligner le problème avec le problème de sous-type, considérez à nouveau:

interface MyPromise<T> extends Promise<T[]> {}

Pouvons-nous abstraire plus de ~MyPromise de la même manière que nous abstraits sur ~Promise ? Comment capturer la relation entre eux?

Le mappage dont j'ai parlé plus tôt est le mappage qui, étant donné un paramétrage de ~MyPromise , produira un paramétrage de ~Promise sorte que le type construit par ~MyPromise soit un sous-type du un construit par ~Promise .

Dans ce cas, le mappage est comme ceci:

T => T[]

@GregRos

Dans ce cas, ~MySpecialPromise n'est pas un sous-type de ~PromiseLike car ils ont des types différents.

Dans Haskell, ce type de problème est résolu en permettant une application partielle des types et en définissant des types de sorte que le paramètre de type final coïncide avec le paramètre de type de l'interface que vous implémentez.

Dans votre exemple, MySpecialPromise serait défini comme MySpecialPromise<TSpecial, TPromiseVal> , et ~MySpecialPromise<SpecialType> aurait le même genre que ~Promise .

@GregRos

Par relation de sous-type, j'entends essentiellement une sorte de relation de «conformité» qui peut être utilisée pour contraindre les constructeurs de types. Par exemple, si je veux écrire une fonction qui fonctionne sur toutes sortes de promesses de différents types, comme Promise, Oiseau bleu, et ainsi de suite, j'ai besoin de la possibilité de contraindre les paramètres TC avec l'interface PromiseLikeen quelque sorte.
function mapPromise<~P extends ~PromiseLike, A, B>(promise : P<A>, func : (x : A) => B) : P<B> ;

Je pense que quand il s'agit de vérifier le type de cette fonction, vous essayez d'unifier BlueBird<T> et PromiseLike<T> pour les T choisis, ce ne sont que des types concrets et relèvent du sous-typage. Je ne vois pas pourquoi vous auriez besoin d'une relation spéciale pour les constructeurs ~BlueBird et ~PromiseLike .

Je suppose qu'il serait utilisé dans quelque chose comme ça?


let x: <P extends ~PromiseLike>(input : P<A>, func : (x : A) => B) : P<B>;
let y: <P extends ~BlueBird>(input : P<A>, func : (x : A) => B) : P<B>;
x = y;

Ici, vous voudrez peut-être vérifier que les contraintes de y impliquent les contraintes de x, mais TypeScript n'a-t-il pas déjà des machines pour vérifier que le BlueBird<T> étend PromiseLike<T> qui pourrait être utilisé?

@ jack-williams Cela se résume à la façon dont vous spécifiez la contrainte suivante:

~ P est un constructeur de type tel que, pour tout A , P<A> est un sous-type de PromiseLike<A> .

Quel genre de syntaxe utiliseriez-vous? Quel genre de concept utiliseriez-vous? Vous pouvez écrire quelque chose comme ceci:

function mapPromise<~P, A, B where P<A> extends PromiseLike<A>>

Mais cette syntaxe a des limites. Par exemple, vous ne pouvez pas du tout exprimer cette classe, car nous ne pouvons pas construire le type P<A> au point où il est déclaré afin de le contraindre:

class PromiseCreator<~P extends ~PromiseLike> {
    create<A>() : P<A>;
}

Mais je suppose que vous pouvez utiliser des types existentiels pour cela, comme ceci:

//Here A is not a captured type parameter
//It's an existential type we introduce to constrain ~P
class PromiseCreator<~P with some A where P<A> extends PromiseLike<A>> {
    create<A>() : P<A>;
}

Ensuite, vous pouvez exiger que tous les constructeurs de types soient contraints via leurs types construits dans la signature de la fonction ou du type, en utilisant éventuellement des types existentiels.

Avec les types existentiels, cela aurait le même pouvoir expressif qu'une relation de sous-type avec une cartographie.

Cependant, cela aurait plusieurs problèmes:

  1. Spécifier des constructeurs de types avec des types tels que ((* => *) => *) => * nécessiterait l'introduction de nombreux types existentiels, dont certains devraient être d'ordre supérieur. Tous devraient apparaître dans la signature de la fonction ou de la classe.
  2. Je ne suis pas tout à fait sûr qu'il serait plus facile de trouver les types existentiels en question que de trouver le mappage.
  3. Je pense que c'est moins élégant que la relation de sous-type.
  4. Introduit potentiellement une autre forme de type que vous auriez à gérer.

@GregRos

Quel genre de syntaxe utiliseriez-vous? Quel genre de concept utiliseriez-vous?

_Personnellement_ je n'utiliserais aucune syntaxe spéciale et j'utiliserais simplement:

function mapPromise<P extends PromiseLike, A, B>(p: P<A>, f: (x: A) => B): P<B>

class PromiseCreator<P extends PromiseLike> {
    create<A>() : P<A>;
}

mais ce n'est que mon opinion car je considère des choses comme number comme un constructeur nul-aire: il n'est donc pas nécessaire de faire une distinction.

Mon point de vue sur le sous-typage des fonctions de constructeur serait de le garder aussi simple que possible. Ils devraient avoir la même arité et les paramètres devraient être des sous-types les uns des autres, en tenant compte de la contravariance et de la covariance tout comme le papier Scala.

Une application partielle peut contourner les cas où ils ont une arité différente (cela ne me dérangerait pas de curryng automatique pour les constructeurs de types afin que vous puissiez simplement écrire MySpecialPromise<SpecialType> ).

Dans l'exemple interface MyPromise<T> extends Promise<T[]> {} je dois être honnête et dire que je ne suis pas convaincu que cela vaille la peine de gérer ce cas - je pense que ce serait une fonctionnalité assez utile sans elle.

Gérer ce cas équivaut (je pense) à dire: ~MyPromise extends ~(Promise . [])[] est le constructeur de la liste et . est la composition du constructeur. Il semble que les choses deviennent beaucoup plus difficiles car il ne suffit plus d'inspecter la structure des constructeurs, mais il faut aussi raisonner sur la composition!

@ jack-williams Cela ne fonctionne pas avec les paramètres de type par défaut. Si j'écris P extends Foo , où Foo a un paramètre de type par défaut, c'est- type Foo<T = {}> = ... dire P ?

Je voudrais juste dire que j'approuve les types d'ordre supérieur (j'ai eu des situations dans de vrais projets TypeScript où ils seraient utiles).

Cependant, je ne pense

Les types d'ordre supérieur sont utiles même sans currying ou application partielle, mais si une application partielle est nécessaire, je préférerais voir une syntaxe explicite pour cela. Quelque chose comme ça:

Foo<number, _>  // equivalent to `type Foo1<A> = Foo<number, A>`

@ cameron-martin

Edit: Désolé, je ne pense pas que mes commentaires n'étaient _pas_ très clairs. Par P a son propre genre, je veux dire qu'il a un genre qui lui est imposé par son utilisation. Supposons que les contraintes soient toujours supposées être du type le plus élevé, donc Foo est supposé être ~Foo . Ce n'est que si nous forçons P à être un genre inférieur que nous vérifions si Foo a un paramètre par défaut. Ce qui me préoccupe, c'est une inférence aimable, mais dans ce cas, ~ n'aidera pas et je pense que nous avons besoin d'annotations complètes.

P a son propre genre, n'est-ce pas? La question ne serait-elle pas de savoir si nous traitons Foo comme ~Foo , ou comme Foo<{}> : je dirais que cela serait motivé par le type de P. Donc, si P est un type nous forçons le paramètre par défaut, et si P est un constructeur * => * , alors nous traitons Foo la même manière.

@Pauan D'accord avec vos suggestions.

@ jack-williams J'ai considéré cette notion de sous-typage, comme je l'ai mentionné plus tôt:

Ma première idée était la suivante. Pour que ~A soit un sous-type de ~B :

  1. (a) Ils doivent avoir le même type (en termes d'arité, pas de contraintes).
  2. (b) Pour chaque paramétrage légal T₁, T₂, ... de ~A , A<T₁, T₂, ...> doit être un sous-type de B<T₁, T₂, ...> .

Le problème est que si nous gardons les choses aussi simples que possible, nous nous retrouverons avec une relation de sous-type qui est paradoxale et ne rentre pas dans le langage.

Si MyPromise<T> extends Promise<T[]> cela signifie que MyPromise<T> doit être utilisable partout où Promise<T[]> est utilisable, mais ce ne serait plus le cas.

Si vous utilisiez as pour convertir un a : MyPromise<T> en Promise<T[]> , vous seriez upcasting, mais cela rendrait paradoxalement a plus assignable.

Les contraintes génériques existantes, qui suivent la relation de sous-type existante, peuvent également être utilisées pour obtenir un effet similaire et provoquer un comportement étrange:

function id1<A, ~P extends ~PromiseLike>(p : P<A>) : P<A>;

function id2<A, P extends Promise<A[]>>(p : P) : P {
    //ERROR - P does not extend PromiseLike<A>
    return id1(p);
}

La frappe deviendrait également au moins partiellement nominale comme effet secondaire. Ces types seraient soudainement différents, où ils sont actuellement les mêmes:

type GenericNumber<T> = number;

type RegularNumber = number;

Je ne sais même pas quel effet cela aurait sur les types complexes d'union / intersection avec des paramètres de type, des types purement structurels, des types avec des compréhensions de membres, etc.

Mon sentiment personnel est que: La relation de sous-type sur les constructeurs de types doit respecter l'existant, pas s'y opposer . Malheureusement, cela nécessite des choses plus complexes.


La principale raison d'utiliser une sorte de notation spéciale pour les constructeurs de types est que 99% des développeurs ne savent pas ce qu'est un constructeur de types et ne voudraient pas être bombardés de messages d'erreur à leur sujet.

C'est très différent de Haskell, où chaque développeur est tenu par la loi de suivre un cours avancé en théorie des catégories.

Une raison secondaire est que dans certains cas (comme le cas des paramètres par défaut mentionné ci-dessus), la syntaxe serait soit ambiguë, soit il ne serait pas possible d'abstraire du tout un constructeur de type spécifique.

EDIT: Désolé @GregRos, je n'ai pas vu vos derniers commentaires!

La relation de sous-type sur les constructeurs de type doit respecter l'existant, pas s'y opposer.

Si cela peut être réalisé, je suis d'accord. Je n'ai tout simplement pas compris tous les détails et à quel point ce serait facile.

Une raison secondaire est que dans certains cas (comme le cas des paramètres par défaut mentionné ci-dessus), la syntaxe serait soit ambiguë, soit il ne serait pas possible d'abstraire du tout un constructeur de type spécifique.

Je ne suis pas sûr de convenir que ce serait ambigu si vous supposez toujours le type de contrainte le plus élevé jusqu'à ce que vous en ayez besoin. Ce n'est pas une affirmation et s'il y a d'autres exemples qui montrent le contraire, alors assez juste.


Le problème est que si nous gardons les choses aussi simples que possible, nous nous retrouverons avec une relation de sous-type qui est paradoxale et ne rentre pas dans le langage.

Cela pourrait être vrai, je suppose que je suis simplement préoccupé de savoir si l'alternative est possible à mettre en œuvre réellement. Pour ce que ça vaut, si la solution la plus complexe fonctionne, ce serait génial!

Avoir la notion plus générale de sous-typage qui montre l'existence d'une fonction de mappage semble difficile à implémenter en général. L'exemple suivant interprète-t-il correctement vos règles?

(b) Pour toute paramétrisation légale T₁, T₂, ... de ~ A, il existe une paramétrisation S₁, S₂, ... de ~ B telle que A

X serait-il un sous-type de Y dans le cas suivant, étant donné une application de F (A, B) = (nombre, B).

type X = ~<A,B> = {x : B};
type Y = ~<A,B> = A extends number ? {x: B} : never;

Cependant X<string,number> ne serait pas un sous-type de Y<string,number> .

Je suppose que je ne suis pas clair si l '_existence_ d'un mappage est suffisante. Si nous considérons ~ A et ~ B comme des fonctions, et nous voulons montrer que ~ B se rapproche de ~ A, ou ~ A est un sous-type de ~ B, alors montrer qu'il y a une fonction ~ C, telle que ~ A sous-type de (~ B. ~ C), ne suffit pas je pense (C est le mappeur). Je dois être le cas pour tous les mappages.

function id1<A, ~P extends ~PromiseLike>(p : P<A>) : P<A>;

function id2<A, P extends Promise<A[]>>(p : P) : P {
    //ERROR - P does not extend PromiseLike<A>
    return id1(p);
}

Je ne suis pas tout à fait cet exemple, est-ce que l'erreur ici ne devrait pas se produire? Ma lecture de ceux-ci est que id1 devrait avoir une entrée construite par la fonction P qui donne un PromiseLike pour toutes les _ entrées_. Alors que id2 parle d'une valeur qui doit être un sous-type d'application de Promise à A []. Je ne sais pas s'il est possible de récupérer les informations nécessaires pour id1 , à partir du type id2 . Je pense cependant que je pourrais mal comprendre votre point.

Ces types seraient soudainement différents, où ils sont actuellement les mêmes

Encore une fois, j'ai peur de manquer votre point, mais je ne sais pas en quoi ils sont identiques. Je ne peux pas remplacer RegularNumber par GenericNumber dans un type, je devrais donner un argument à ce dernier.

Je suppose que je ne sais pas si l'existence d'une cartographie est suffisante. Si nous considérons ~ A et ~ B comme des fonctions, et nous voulons montrer que ~ B se rapproche de ~ A, ou ~ A est un sous-type de ~ B, alors montrer qu'il y a une fonction ~ C, telle que ~ A sous-type de (~ B. ~ C), ne suffit pas je pense (C est le mappeur). Je dois être le cas pour tous les mappages.

Oui, vous avez raison, tout comme le contre-exemple que vous avez fourni. J'ai trouvé d'autres contre-exemples. Ça ne marche pas du tout.

J'ai relu ce fil et beaucoup de vos réponses. Je pense que vous avez raison sur beaucoup de choses et j'ai considéré le problème d'une mauvaise manière. J'arriverai à ce que je veux dire.

Je ne suis pas sûr de convenir que ce serait ambigu si vous supposez toujours le type de contrainte le plus élevé jusqu'à ce que vous en ayez besoin. Ce n'est pas une affirmation et s'il y a d'autres exemples qui montrent le contraire, alors assez juste.

C'est soit ambigu, soit quelque chose devient impossible à référencer. Comme dans l'exemple ci-dessus, le constructeur de type de Foo devient impossible à référencer car il est masqué par le type lui-même. Si vous écrivez ~Foo ou d'ailleurs Foo<*> ou ~<A>Foo<A> ou toute autre chose qui n'entre pas en conflit avec d'autres choses, vous n'auriez pas ce genre de problème.

Oui, vous pouvez contourner cela en définissant un alias, même si ce n'est pas très joli:

type Foo2<T> = Foo<T>

Comme je l'ai dit, je ne pense pas que ce soit la préoccupation la plus importante.

Je ne suis pas tout à fait cet exemple, est-ce que l'erreur ici ne devrait pas se produire? Ma lecture de ceux-ci est que id1 devrait avoir une entrée construite par la fonction P qui donne un PromiseLike pour toutes les entrées. Alors que id2 parle d'une valeur qui doit être un sous-type d'application de Promise à A []. Je ne sais pas s'il est possible de récupérer les informations nécessaires pour id1, à partir du type d'id2. Je pense cependant que je pourrais mal comprendre votre point.

C'est la lecture correcte, ouais. Mais si P extends Promise<A[]> il devrait être assignable à n'importe quel endroit qui accepte un Promise<A[]> , tel que id1 . C'est comme ça que ça se passe actuellement et ce que signifie le sous-typage.

Je ne pense plus vraiment que cela puisse être évité.

Encore une fois, j'ai peur de manquer votre point, mais je ne sais pas en quoi ils sont identiques. Je ne peux pas remplacer RegularNumber par GenericNumber dans un type, je devrais donner un argument à ce dernier.

Ce que je voulais dire, c'est ceci: le type GenericNumber<T> , pour tout T , et le type RegularNumber sont identiques et interchangeables. Il n'y a pas de contexte dans lequel l'un taperait check et l'autre pas. En ce moment, du moins.

Ce dont nous avons parlé les rendrait différents. Parce que GenericNumber<T> provient d'un TC, il serait assimilable là où RegularNumber ne pourrait pas être. Ce ne serait donc plus interchangeable.

J'y ai pensé, et je suppose que c'est peut-être inévitable, et pas nécessairement mauvais. Juste un nouveau comportement différent.

Une façon d'y penser est que le paramètre type devient une partie de la "structure" du type.

Je pense que les TC conduiront à un comportement plus différent.

Nouvelle direction

Tout d'abord, je pense que vous avez raison en ce que la relation de sous-type correcte est celle qui n'a pas de mappage:

Ma première idée était la suivante. Pour que ~A soit un sous-type de ~B :

  1. (a) Ils doivent avoir le même type (en termes d'arité, pas de contraintes).
  2. (b) Pour chaque paramétrage légal T₁, T₂, ... de ~A , A<T₁, T₂, ...> doit être un sous-type de B<T₁, T₂, ...> .

Le truc de la cartographie ... honnêtement, c'est assez stupide. Je ne pense pas qu'il y ait plus moyen d'unifier MyPromise<T> extends Promise<T[]> et ~Promise . J'adorerais savoir si quelqu'un pense le contraire.

J'aimerais aussi savoir s'il me manque un exemple où même cette règle ne fonctionne pas.

Si nous convenons que les contraintes de constructeur de type doivent être exprimées en utilisant une relation de sous-type, ce qui semble très bien fonctionner, nous pouvons passer à d'autres choses.

À propos de la syntaxe

On n'est pas vraiment d'accord sur ce sujet, semble-t-il. Je préfère fortement une syntaxe de préfixe similaire à ~Promise . Conceptuellement, le ~ peut être vu comme une "référence au TC de" opérateur ou quelque chose.

Je pense avoir donné plusieurs raisons pour lesquelles c'est mieux que les alternatives:

  1. Totalement sans ambiguïté.

    1. Les erreurs impliquant cette syntaxe sont également sans ambiguïté. Si quelqu'un oublie de paramétrer un type, il n'obtiendra pas d'erreurs sur les constructeurs de types lorsqu'il ne sait pas ce qu'ils sont.

    2. En conséquence, les textes d'erreur et la logique existants ne devront pas être modifiés. Si quelqu'un écrit Promise le message d'erreur sera exactement le même que maintenant. Cela n'aura pas à changer pour parler des TC.

  2. S'étend bien à une syntaxe structurelle.
  3. Facile à analyser, je crois. Tout ~\w apparaissant là où un type est attendu sera supposé indiquer une référence à un TC.
  4. Facile à taper.

J'espère que d'autres personnes pourront donner leur avis.

A propos des unions, intersections et paramètres de type par défaut

Les types surchargés / mixtes des formulaires * & (* => *) , * | (* => *) et ainsi de suite sont-ils légaux? Ont-ils des utilisations intéressantes?

Je pense que c'est une mauvaise idée sur laquelle il est difficile de raisonner. Je ne suis pas non plus sûr du type d'annotation de type dont vous auriez besoin pour lever l'ambiguïté de * | (* => *) afin que vous puissiez en construire un.

Une façon dont on peut dire que de tels types existent actuellement est les types avec des paramètres de type par défaut:

type Example<A = number> = {}

On peut dire que ce type a le genre * & (* => *) car il peut accepter un paramètre de type pour construire un type, mais ce n'est pas obligatoire.

Je crois que les paramètres de type par défaut devraient être une forme de raccourci, pas une manière de décrire les types. Je pense donc que les paramètres de type par défaut doivent simplement être ignorés lors de la détermination du type d'un type.

Cependant, il peut être judicieux de parler de types tels que ~Promise | ~Array . Ils ont le même type, donc ils ne sont pas incompatibles. Je pense que cela devrait être soutenu.

Les choses qui devront être traitées

Des situations connexes devront être traitées, comme cette situation:

type Example = (<~P extends ~Promise>() => P<number>) | (<~M extends ~Map>() => Map<string, number>);

Mais cela n'implique pas vraiment le genre (* => *) | (*, *) => * , mais quelque chose de différent

Construire d'autres TC?

Comme je l'ai déjà mentionné, je ne pense pas que ce soit une bonne idée d'avoir des TC qui construisent d'autres TC, comme * => (* => *) . Ils sont la norme dans les langages qui prennent en charge le curry et autres, mais pas dans TypeScript.

Il n'y a aucun moyen que je puisse voir pour définir de tels types en utilisant la syntaxe ~ et la relation de sous-type, donc cela ne nécessiterait pas de règles spéciales pour les interdire. Il faudrait des règles spéciales pour les faire fonctionner.

Je suppose que vous pourriez sans doute les définir structurellement comme ceci:

~<A>~<B>{a : A, b : B}

C'est à peu près la seule façon dont je pense.

Interaction avec des fonctions de rang supérieur?

Il existe une interaction naturelle mais complexe avec les types de fonctions qui prennent des paramètres de type:

type Example<T> = <~P extends ~Promise>(p : P<T>) : P<T>;

Cette interaction devrait-elle être interrompue d'une manière ou d'une autre? Je peux voir des types comme ceux-ci devenir très compliqués.

En général, y a-t-il des endroits où les paramètres TC ne devraient pas apparaître?

Ma syntaxe structurelle est-elle une bonne idée?

Je ne pense pas que cela devrait être implémenté tout de suite, mais je pense toujours que ma syntaxe structurelle est une bonne idée. Il vous permet:

  1. Utilisez des identificateurs locaux comme d'autres paramètres de type dans vos contraintes.
  2. Appliquez partiellement les constructeurs de types, de manière très explicite et flexible: ~<A>Map<string, A> , ~<A, B>Map<B, A> , et ainsi de suite.
  3. Tous les autres aspects d'un type ont une syntaxe structurelle, donc les TC devraient également avoir une telle syntaxe.

Cela dit, les TC peuvent totalement fonctionner sans cela, et le premier PR ne les impliquerait probablement pas.

Interaction avec les types conditionnels

Comment la fonctionnalité fonctionnera-t-elle avec les types conditionnels? Devriez-vous pouvoir le faire?

type Example<~P extends ~PromiseLike> = ~P extends ~Promise ? 0 : 1

Je ne suis pas entièrement sûr moi-même. Les types conditionnels n'ont pas encore été complètement digérés.

Résolution de surcharge

J'ai le sentiment que celui-ci va être difficile à faire. C'est en fait assez important car différentes formes de résolution de surcharge finiraient par créer différents types.

Cela dit, je ne peux pas trouver de bons exemples pour le moment.

Vous savez, une grande partie de cela aurait été un point discutable si un langage intermédiaire bien défini avait été utilisé pour décrire TypeScript comme point de départ. Par exemple: System F <: ou l'un des systèmes de type dépendant du son tels que Simplified Dependent ML .

Je serais honnêtement surpris si cela était résolu avant
https://github.com/Microsoft/TypeScript/issues/14833

Je pense que # 17961 pourrait probablement résoudre ce problème indirectement. Voir l'essentiel pour plus de détails.

Notez que les types Bifunctor et Profunctor sont un peu complexes au niveau des contraintes - ce serait beaucoup plus simple si j'avais des types universels évidents avec lesquels travailler, plutôt que les infer T qui est purement limité aux types conditionnels. De plus, ce serait bien si j'aurais pu utiliser this comme type "return" (qui est purement de niveau de type) - cela aurait rendu la plupart de mes interfaces plus faciles à définir.

(Je ne suis pas un gros utilisateur de TS, donc j'ai peut-être commis des erreurs. @ Tycho01 Pourriez-vous jeter un coup d'œil à cela pour voir si je me suis trompé quelque part dans le désordre de type? La raison pour laquelle je vous pose la question est que vous êtes le derrière le PR ci-dessus, et j'ai vu certaines de vos autres expériences et utilitaires.)

@isiahmeadows @ tycho01 Wow ...

Vous avez raison. Si je comprends bien, cela donne à peu près les mêmes résultats.

Il y a quelques différences. Mais fonctionnellement, ils sont à peu près identiques et je pense que ces différences peuvent être résolues.

Impossible de déduire la fonction de type correcte

function example<~P extends ~PromiseLike>(p : P<number>) : P<string>;

Ici, vous pouvez déduire ~Promise et ~Bluebird partir de p . Cependant, si vous le faites comme ceci:

function example<F extends <T>(t: T) => PromiseLike<T>>(p : F(number)) : F(string)

Je doute fortement que cela fonctionne:

example(null as Promise<number>)

Il n'y a aucun moyen de déduire que F est censé être:

<T>(t : T) => Promise<T>

Parce que cette fonction n'est en aucun cas considérée comme spéciale. Alors qu'avec les TC, certains types ont essentiellement une fonction de niveau de type "implicite": leur TC.

Impossible de référencer facilement les TC existants

Vous ne pouvez pas faire de ~Promise comme dans ma proposition. Vous devrez encoder le type directement, en utilisant un format structurel:

type PromiseTC = <T>() => Promise<T>

C'est vrai, et c'est une préoccupation. Il s'agit davantage d'un problème d'inférence de type où vous avez besoin de la capacité d'inférer la fonction générique elle-même à partir d'un type d'argument connu (l'inverse de ce qui se passe habituellement). Il peut être résolu d'une manière suffisamment générale pour fonctionner dans la plupart des cas, mais cela nécessite un nouveau cas spécial qui n'est pas trivial.

Cela pourrait être résolu en partie grâce à l'utilisation stratégique de NoInfer<T> , mais je ne suis pas sûr à 100% de la manière dont cela devrait être fait, ni à quel point cela pourrait résoudre même le cas courant.

@GregRos

Je ne suis pas fortement en faveur d'une syntaxe, c'est plus juste ma préférence, il y a beaucoup de mérites à ~ . Je pense que la principale chose à considérer serait de savoir si la syntaxe pour les annotations de type explicite est requise car l'inférence n'est pas toujours possible.


Le truc de la cartographie ... honnêtement, c'est assez stupide. Je ne pense pas qu'il existe un moyen d'unifier MyPromiseprolonge la promesse

Je pense que la cartographie pourrait encore être une notion utile, mais dans le cas ci-dessus, je ne pense pas que nous devrions jamais essayer d'unifier ~MyPromise avec ~Promise , la chose qui doit être unifiée est ~MyPromise et ~<T>Promise<T[]> , que nous pourrions également écrire ~(Promise . []) . Je pense que ce qui manquait, c'est que le mappage doit faire partie de la relation de sous-typage: il fait autant partie du constructeur que Promise . Dans cet exemple, le mappage est simplement le constructeur de liste.

interface A<T> {
    x: T;
} 

interface B<T> {
    x: T[];
}

Est-ce que ~<T>B<T> étend ~<T>A<T[]> ? Oui. Est-ce que ~<T>B<T> prolonge ~<T>A<T> ? Non. Mais en fin de compte, ce sont deux questions sans rapport.

Si nous convenons que les contraintes de constructeur de type doivent être exprimées en utilisant une relation de sous-type, ce qui semble très bien fonctionner, nous pouvons passer à d'autres choses.

Oui, je pense que cela semble être une belle façon de décrire les choses.


Impossible de déduire la fonction de type correcte

function example<~P extends ~PromiseLike>(p : P<number>) : P<string>;
Ici, vous pouvez déduire ~Promise et ~Bluebird partir de p.

Ce n'est pas une assertion, mais plutôt une question ouverte car je ne suis pas tout à fait sûr du fonctionnement de la vérification de type. Je pensais qu'en utilisant l'interface A ci-dessus comme exemple, les types A<number> et {x: number} sont indiscernables et je ne suis donc pas sûr qu'il soit possible de déduire le constructeur à partir du type renvoyé par une application du constructeur. Serait-il possible de récupérer P de P<number> ? Je suis sûr que les choses pourraient être modifiées pour soutenir cela, je me demande simplement ce que cela fait maintenant.

Réponse croisée de # 17961, mais je ne sais pas comment faire @isiahmeadows , malheureusement. Je crains que l'inférence en arrière sur les appels de type ne soit pas triviale.

Donc, il semble que sur la base d'une entrée Promise<number> ou Bluebird<number> nous voulons pouvoir déduire des versions non appliquées de ces types de sorte que nous pourrions les réappliquer avec par exemple string . Cela semble difficile cependant.

Même si les types d'entrée sont comme ça au lieu d'un équivalent structurel (nous sommes un langage structurellement typé, non?), Ce raisonnement devient également trouble si, par exemple, Bluebird avait deux types de paramètres à la place, à quel point notre <string> application de paramètre de type
Je ne suis pas sûr d'une bonne solution là-bas. (avertissement: j'ai pris un peu de retard sur la discussion ici.)

@ tycho01 Tous ces problèmes disparaîtraient-ils si les gens instanciaient explicitement T ?

Je pense que c'est raisonnable, étant donné que je doute que l'inférence puisse être résolue de toute façon dans tous les cas.

@ jack-williams: pas avec # 17961 jusqu'à présent, mais je pense que son utilisation pour la répartition pourrait aider:

let arr = [1, 2, 3];
let inc = (n: number) => n + 1;
let c = arr.map(inc); // number[]
let map = <Functor extends { map: Function }, Fn extends Function>(x: Functor, f: Fn) => x['map'](f); // any on 2.7 :(
let e = map(arr, inc);

@ tycho01 Oui, j'ai réalisé que ma suggestion était terrible car T n'est pas instancié lors des appels de méthode.

Quelque chose comme le travail suivant?

interface TyCon<A> {
    C: <A>(x: A) => TyCon<A>
}

interface Functor<A> extends TyCon<A> {
    C: <A>(x: A) => Functor<A>;
    fmap<B>(this: this["C"](A), f: (x: A) => B): this["C"](B);
}

interface Option<A> extends Functor<A> {
    C: <A>(x: A) => Option<A>;
}

@ jack-williams Je suppose que la question devrait être de savoir comment cela se comparerait en comportement à l'implémentation ADT dans fp-ts , mais cela semble pouvoir fonctionner, ouais. Probablement aussi sans le TyCon .

@ jack-williams @isiahmeadows :

J'ai essayé l'idée à try flow , car il a déjà $Call disponible. Pour moi, cela semble devenir insensible d'une manière ou d'une autre ...

interface Functor<A> {
    C: <A>(x: A) => Functor<A>;
    fmap<B>(f: (x: A) => B): $Call<$ElementType<this, "C">, B>;
}
// this: $Call<$ElementType<this, "C">, A>, 
// ^ flow doesn't seem to do `this` params

interface Option<A> extends Functor<A> {
    C: <A>(x: A) => Option<A>;
}

let o: Option<string>;
let f: (s: string) => number;
let b = o.fmap(f);

@ tycho01 Je pense que vous ne pouvez pas simplement obtenir des propriétés avec $ElementType partir de this en flux

@ tycho01 vous ne pouvez pas non plus faire fonctionner cela en tapuscrit
aire de jeux: https://goo.gl/tMBKyJ

@goodmind : hm, on dirait qu'il infère Maybe<number> au lieu de Functor<number> après avoir copié sur fmap de Functor à Maybe .
Avec les appels de type, je suppose que cela améliorerait pour avoir juste le type là-bas plutôt que d'avoir besoin de l'implémentation d'exécution pour le type.
Désormais, les foncteurs auraient déjà besoin de leurs propres implémentations fmap . Cela serait cependant nul pour les méthodes dérivées .
Retour à la case départ. : /

Quelques idées pertinentes sur https://github.com/SimonMeskens/TypeProps/issues/1.

Je prévois de publier une version alpha dès que possible, mais vous pouvez suivre avec moi en écrivant les exemples de ce numéro pour déjà en avoir une idée.

Ce problème particulier est un peu long à traiter entièrement, mais ce que je recherche est simple, mais de vrais exemples de code que vous ne pouvez pas taper en raison du manque de types génériques paramétrés. Je pense que je peux taper la plupart d'entre eux (à condition qu'ils ne reposent pas sur des constructeurs de types abstraits levés). N'hésitez pas à ouvrir les problèmes dans le référentiel ci-dessus avec du code et je les taperai pour vous si je le peux (ou vous pouvez les publier ici aussi).

Attention, j'ai commencé une tentative d'implémentation de ceci au # 23809. C'est encore très incomplet mais vérifiez-le si vous êtes intéressé.

Je vous ai promis un exemple simple, le voici. Cela utilise quelques astuces que j'ai apprises en écrivant ma bibliothèque.

type unknown = {} | null | undefined;

// Functor
interface StaticFunctor<G> {
    map<F extends Generic<G>, U>(
        transform: (a: Parameters<F>[0]) => U,
        mappable: F
    ): Generic<F, [U]>;
}

// Examples
const arrayFunctor: StaticFunctor<any[]> = {
    map: <A, B>(fn: (a: A) => B, fa: A[]): B[] => {
        return fa.map(fn);
    }
};
const objectFunctor: StaticFunctor<object> = {
    map: <A, B>(fn: (a: A) => B, fa: A): B => {
        return fn(fa);
    }
};
const nullableFunctor: StaticFunctor<object | null | undefined> = {
    map: <A, B>(
        fn: (a: A) => B,
        fa: A | null | undefined
    ): B | null | undefined => {
        return fa != undefined ? fn(fa) : fa;
    }
};

const doubler = (x: number) => x * 2;

const xs = arrayFunctor.map(doubler, [4, 2]); // xs: number[]
const x = objectFunctor.map(doubler, 42); // x: number
const xNull = nullableFunctor.map(doubler, null); // xNull: null
const xSome = nullableFunctor.map(doubler, 4 as number | undefined); // xSome: number | undefined

const functor: StaticFunctor<unknown | any[]> = {
    map(fn, fa) {
        return Array.isArray(fa)
            ? arrayFunctor.map(fn, fa)
            : fa != undefined
                ? objectFunctor.map(fn, fa)
                : nullableFunctor.map(fn, fa);
    }
};

const ys = functor.map(doubler, [4, 2]); // ys: number[]
const y = functor.map(doubler, 42); // y: number
const yNull = functor.map(doubler, null); // yNull: null
const ySome = functor.map(doubler, 42 as number | undefined); // ySome: number | undefined

// Plumbing
interface TypeProps<T = {}, Params extends ArrayLike<any> = never> {
    array: {
        infer: T extends Array<infer A> ? [A] : never;
        construct: Params[0][];
    };
    null: {
        infer: null extends T ? [never] : never;
        construct: null;
    };
    undefined: {
        infer: undefined extends T ? [never] : never;
        construct: undefined;
    };
    unfound: {
        infer: [NonNullable<T>];
        construct: Params[0];
    };
}

type Match<T> = T extends infer U
    ? ({} extends U ? any
        : TypeProps<U>[Exclude<keyof TypeProps, "unfound">]["infer"]) extends never
    ? "unfound"
    : {
        [Key in Exclude<keyof TypeProps, "unfound">]:
        TypeProps<T>[Key]["infer"] extends never
        ? never : Key
    }[Exclude<keyof TypeProps, "unfound">] : never;


type Parameters<T> = TypeProps<T>[Match<T>]["infer"];

type Generic<
    T,
    Params extends ArrayLike<any> = ArrayLike<any>,
    > = TypeProps<T, Params>[Match<T>]["construct"];

J'ai mis à jour et simplifié l'exemple, voici aussi un lien de terrain de jeu:
Terrain de jeux

J'ai ajouté une bibliothèque NPM pour ce qui précède, afin que vous puissiez l'utiliser plus facilement. Actuellement en alpha jusqu'à ce que je fasse des tests appropriés, mais cela devrait vous aider à essayer d'écrire des HKT.

Lien Github

J'ai joué avec une approche simple pour simuler les HKT en utilisant des types conditionnels pour substituer des variables de type virtuel dans un type saturé:

declare const index: unique symbol;

// A type for representing type variables
type _<N extends number = 0> = { [index]: N };

// Type application (substitutes type variables with types)
type $<T, S, N extends number = 0> =
  T extends _<N> ? S :
  T extends undefined | null | boolean | string | number ? T :
  T extends Array<infer A> ? $Array<A, S, N> :
  T extends (x: infer I) => infer O ? (x: $<I, S, N>) => $<O, S, N> :
  T extends object ? { [K in keyof T]: $<T[K], S, N> } :
  T;

interface $Array<T, S, N extends number> extends Array<$<T, S, N>> {}

// Let's declare some familiar type classes...

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

interface Monad<M> {
  pure: <A>(a: A) => $<M, A>;
  bind: <A, B>(ma: $<M, A>, f: (a: A) => $<M, B>) => $<M, B>;
}

interface MonadLib<M> extends Monad<M>, Functor<M> {
  join: <A>(mma: $<M, $<M, A>>) => $<M, A>;
  // sequence, etc...
}

const Monad = <M>({ pure, bind }: Monad<M>): MonadLib<M> => ({
  pure,
  bind,
  map: (ma, f) => bind(ma, a => pure(f(a))),
  join: mma => bind(mma, ma => ma),
});

// ... and an instance

type Maybe<A> = { tag: 'none' } | { tag: 'some'; value: A };
const none: Maybe<never> = { tag: 'none' };
const some = <A>(value: A): Maybe<A> => ({ tag: 'some', value });

const { map, join } = Monad<Maybe<_>>({
  pure: some,
  bind: (ma, f) => ma.tag === 'some' ? f(ma.value) : ma,
});

// Not sure why the `<number>` annotation is required here...
const result = map(join<number>(some(some(42))), n => n + 1);
expect(result).toEqual(some(43));

Projetez ici: https://github.com/pelotom/hkts

Vos commentaires sont les bienvenus!

@pelotom J'aime la légèreté de la syntaxe de votre approche. Il existe deux autres approches qui n'ont pas encore été mentionnées dans ce fil de discussion qui pourraient susciter une certaine créativité dans la manière dont les solutions actuelles et futures sont produites. Les deux sont des solutions orientées objet à ce problème.

  1. Bertrand Meyer a décrit une façon de simuler des types génériques dans son livre de 1988 "Construction de logiciels orientés objet".

Les exemples sont dans Eiffel, mais une traduction approximative de TypeScript ressemble à ceci:

https://gist.github.com/mlhaufe/089004abd14ad8e7171e2a122198637f

Vous remarquerez qu'ils peuvent devenir assez lourds en raison du besoin de représentations de classes intermédiaires, mais avec une forme de classe-factory ou avec l'approche TypeScript Mixin, cela pourrait être considérablement réduit.

Il peut y avoir une certaine applicabilité à # 17588

  1. La deuxième approche est utilisée lors de la simulation d'algèbres d'objets (et d'usines abstraites):

C<T> est représenté par App<t,T>T est la classe, et t est une balise unique associée à C

interface App<C,T> {}

Échantillon:

interface IApp<C,T> {}

interface IList<C> {
    Nil<T>(): IApp<C,T>
    Cons<T>(head: T, tail: IList<C>): IApp<C,T>
}

// defining data
abstract class List<T> implements IApp<typeof List, T> {
    // type-safe down-cast
    static prj<U>(app: IApp<typeof List, U>): List<U> { return app as List<U> }
}
class Nil<T> extends List<T> { }
class Cons<T> extends List<T> {
    constructor(readonly head: T, readonly tail: List<T>) {
        super()
    }
}

// The abstract factory where the HKT is needed
class ListFactory<T> implements IList<typeof List> {
    Nil<T>(): IApp<typeof List, T> { return new Nil() }
    Cons<T>(head: T, tail: IApp<typeof List, T>): IApp<typeof List, T> {
        return new Cons(head, tail)
    }
}

Vous pouvez voir plus de détails et de justification dans l'article suivant sous la section 3.5 «Émulation du polymorphisme type-constructeur»:

https://blog.acolyer.org/2015/08/13/streams-a-la-carte-extensible-pipelines-with-object-algebras/

@metaweta , pouvez-vous renommer ce problème en Higher kinded types in TypeScript afin qu'il obtienne une meilleure visibilité de la recherche Google s'il vous plaît?

Peut-être que nos sages et bienveillants responsables du dépôt (par exemple, @RyanCavanaugh , @DanielRosenwasser ) pourraient éditer le titre, si un tel changement est jugé digne de leur intervention?

Je suis curieux de savoir ce que cela signifie que cela est passé de la communauté à l'arriéré. Est-ce quelque chose que l'équipe de base considère plus sérieusement maintenant ou cela signifie simplement que l'équipe a décidé que ce n'était pas un bon candidat pour la communauté?

Trouvé: le jalon "Communauté" est apparemment obsolète au profit de celui "Backlog" , et donc ce problème a probablement été migré en nature.

Pas un membre TS, juste quelqu'un qui a décidé de cliquer sur le lien où il a été repassé.

+1

Voici quelque chose que j'essayais juste de construire qui semble être un cas vraiment pratique pour les types plus élevés.

Je souhaite créer une abstraction pour une base de données qui peut s'exécuter de manière synchrone ou asynchrone. Plutôt que d'utiliser des rappels et de pirater autour de cela, je voulais utiliser un générique. Voici ce que j'aimerais faire:

type Identity<T> = T

interface DatabaseStorage<Wrap<T> extends Promise<T> | Identity<T>> {
    get(key: string): Wrap<any>
    set(key: string, value: any): Wrap<void>
}

Ce serait vraiment puissant!

@ccorcos qui s'appelle MTL-style. Vous pouvez consulter https://github.com/gcanti/fp-ts/blob/master/tutorials/mtl.ts pour un exemple fonctionnel pur avec fp-ts .

@mlegenhausen Je suis désolé, mais j'ai du mal à suivre cet exemple.

Chaque fois que je creuse dans fp-ts , je crains que les choses ne deviennent si compliquées qu'elles deviennent fragiles. L'exemple de @pelotom semble plus facile à suivre cependant ...

Une raison pour laquelle cela n'est pas adopté dans TypeScript?

@ccorcos IMHO même lorsque j'ai recommandé l'exemple de fp-ts je ne recommanderais pas du tout le style MTL / tagless. Vous ajoutez une couche supplémentaire d'abstraction à chaque monade efficace que vous devez gérer manuellement car le dactylographie n'est pas capable de détecter la monade que vous souhaitez utiliser et c'est là que les choses se compliquent. Ce que je vois de la communauté fp-ts est d'utiliser une monade asynchrone (je recommanderais TaskEither ) et de s'y tenir. Même en testant les avantages de MTL ne valent pas les tracas que vous obtenez dans votre code non test. hyper-ts basé sur fp-ts est un exemple de bibliothèque qui a récemment abandonné la prise en charge de MTL.

Intéressant ... hyper-ts air vraiment cool ...

J'ai proposé un encodage de type supérieur léger basé sur le polymorphisme lié à F: https://github.com/strax/tshkt

L'avantage de cette approche est que vous n'avez pas besoin d'une table de recherche (un seul type conditionnel ou un objet avec des clés de chaîne) pour associer des constructeurs de types à des types. La technique peut également être utilisée pour coder l'application de fonctions génériques au niveau du type (pensez à ReturnType<<T>(value: T) => Array<T>> ).

C'est toujours une preuve de concept, donc les commentaires sur la viabilité de cette approche sont grandement appréciés!

Je vais jeter un oeil à @strax , qui a l'air vraiment cool!

En attendant, voici un exemple idiot de ce que nous pouvons faire maintenant:

type Test1 = λ<Not, [True]>;        // False
type Test2 = λ<And, [True, False]>; // False
type Test3 = λ<And, [True, True]>;  // True

// Boolean

interface True extends Func {
    expression: Var<this, 0>;
}

interface False extends Func {
    expression: Var<this, 1>;
}

interface Not extends Func {
    expression: λ<Var<this, 0>, [False, True]>
}

interface And extends Func {
    expression: λ<Var<this, 0>, [Var<this, 1>, Var<this, 0>]>
}

// Plumbing

type Func = {
    variables: Func[];
    expression: unknown;
}

type Var<F extends Func, X extends number> = F["variables"][X];

type λ<Exp extends Func, Vars extends unknown[]> = (Exp & {
    variables: Vars;
})["expression"];

Je voudrais ajouter des indices De Bruijn, car cela signifierait que nous n'avons plus besoin des interfaces, mais cela nécessiterait des maths de tuple je pense et j'essaie d'éviter cela.

Proposition

Ordre supérieur, Lamda, types de référence

Passer un type comme référence

En termes simples, un type de référence ou un type d'ordre supérieur permettrait de différer les paramètres pris par un type pour plus tard, ou même d'inférer des paramètres de type (génériques) par la suite. Mais pourquoi devrions-nous nous en soucier?

Si nous pouvons passer un type comme référence, cela signifie que nous pouvons retarder TypeScript d'évaluer un type jusqu'à ce que nous décidions de le faire. Jetons un coup d'œil à un exemple du monde réel:

Aperçu avec Pipe

Imaginez que vous développiez un type générique pour pipe . La plupart du travail consiste à vérifier que les fonctions à pipeter sont bien canalisables, sinon nous signalerions une erreur à l'utilisateur. Pour ce faire, nous utiliserions un type mappé pour diriger les types des fonctions comme le fait pipe(...) :

type  PipeSync<Fns  extends  Function[], K  extends  keyof  Fns> = 
    K  extends  '0'
    // If it's the first function, we leave it unchanged
    ?  Fns[K]
    // For all the other functions, we link input<-output
    : (arg:  Return<Fns[Pos<Prev<IterationOf<K & string>>>]>) =>
        Return<Fns[Pos<IterationOf<K & string>>]>;

Maintenant, il suffit de répéter ceci sur les fonctions avec un type mappé:

type  Piper<Fns  extends  Function[]> = {
    [K  in  keyof  Fns]:  PipeSync<Fns, K>
}

( voir l'implémentation complète )

Nous sommes maintenant capables de canaliser les fonctions ensemble et TypeScript peut nous donner des avertissements:

declare  function  pipe<Fns  extends  F.Function[]>(...args:  F.Piper<Fns>):  F.Pipe<Fns>

const  piped  =  pipe(
    (name:  string, age:  number) => ({name, age}),
    (info: {name:  string, age:  number}) =>  `Welcome, ${info.name}`,
    (message:  object) =>  false, // /!\ ERROR
)

Ça marche! nous avons eu une erreur appropriée:

L'argument de type '(message: object) => boolean' n'est pas assignable au paramètre de type '(arg: string) => boolean'.

Le problème

Mais il y a un problème. Bien que cela fonctionne très bien pour les opérations simples, vous constaterez que cela échoue complètement lorsque vous commencez à utiliser des génériques (modèles) sur les fonctions que vous lui passez:

const  piped  =  pipe(
    (a:  string) =>  a,
    <B>(b:  B) =>  b, // any
    <C>(c:  C) =>  c, // any
)

type  piped  =  Piper<[
    (a:  string) =>  string,
    <B>(b:  B) =>  B,
    <C>(c:  C) =>  C,
]>
// [
//     (a:  string) =>  string,
//     (b:  string) =>  unknown,
//     (c:  unknown) => unknown
// ]

Dans les deux cas, TypeScript a perdu la trace des types de fonctions.
> C'est là que les types d'ordre supérieur entrent en jeu <

Syntaxe

type  PipeSync<Fns  extends  Function[], K  extends  keyof  Fns> = 
    K  extends  '0'
    // If it's the first function, we leave it unchanged
+   ?  *(Fns[K]) // this will preserve the generics
    // For all the other functions, we link input<-output
+   :  *( // <- Any type can be made a reference
+       <T>(arg:  T) => Return<*(Fns[Pos<IterationOf<K  &  string>>])>
+       // vvv It is now a reference, we can assign generics
+       )<Return<*(Fns[Pos<Prev<IterationOf<K  &  string>>>])>>
+       // ^^^ We also tell TS not to evaluate the previous return
+       // and this could be achieved by making it a reference too

En bref, nous avons déduit manuellement et dynamiquement les génériques avec * . En fait, l'utilisation de * retardé l'évaluation des génériques. Ainsi, le * a des comportements différents, selon le contexte. Si le * est d'un type qui:

  • peut recevoir des génériques : il reprend ses génériques / obtient sa référence
  • est un générique lui - Return<*(Fns[Pos<Prev<IterationOf<K & string>>>])> qui a été assigné à T . Dans ce contexte, on peut dire que * "protège" contre une évaluation immédiate.
  • n'est rien de ce qui précède : il ne fait rien, résout ce même type
type  piped  =  Piper<[
    (a:  string) =>  string,
    <B>(b:  B) =>  B
    <C>(c:  C) =>  C
]>
// [
//     (a:  string) =>  string,
//     (b:  string) =>  string,
//     (c:  string) =>  string
// ]

Donc, TypeScript doit démarrer / continuer l'évaluation uniquement si le générique a été fourni, et bloquer l'évaluation si nécessaire (générique incomplet). Pour le moment, TS évalue d'un seul coup en transformant les génériques en types unknown . Avec cette proposition, lorsque quelque chose ne peut être résolu:

type  piped  =  Piper<[
    <A>(a:  A) =>  A, // ?
    <B>(b:  B) =>  B, // ?
    <C>(c:  C) =>  C, // ?
]>
// [
//     <A>(a:  A) =>  A,
//     (b:  A) =>  A,
//     (c:  A) =>  A
// ]

Détails

Le * récupère une référence à un type, permettant des manipulations sur ses génériques. Ainsi, placer le caractère générique devant un type récupère une référence à celui-ci:

*[type]

La récupération d'une référence à un type permet automatiquement la manipulation des génériques:

*[type]<T0, T1, T2...>

Les génériques ne sont consommés / définis par le type ciblé que si cela peut l'être. Alors en faisant ceci:

*string<object, null> // Will resolve to `string`

Mais il pourrait également être vérifié par TypeScript lui-même, s'il doit afficher un avertissement ou non. Mais en interne, TS ne devrait rien faire de cela.

J'ai aussi pensé que c'était une bonne idée d'utiliser * car il peut symboliser un pointeur vers quelque chose (comme dans les langages C / C ++), et n'est pas emprunté par TypeScript.

Types d'ordre supérieur

Maintenant que nous avons vu comment cela fonctionne dans sa forme la plus basique, je voudrais vous présenter le concept de base: les types lambda . Ce serait bien de pouvoir avoir des types anonymes, similaires aux callbacks, lambdas, références en JavaScript .

L'exemple ci-dessus montre comment reprendre les génériques d'une fonction. Mais puisque nous parlons de références, n'importe quel type peut être utilisé en conjonction avec * . En termes simples, une référence de type est un type que nous pouvons transmettre mais qui n'a pas encore reçu ses génériques:

type  A<T  extends  string> = {0:  T}
type  B<T  extends  string> = [T]
type  C<T  extends  number> = 42

// Here's our lamda
type  Referer<*Ref<T  extends  string>, T  extends  string> =  Ref<T>
// Notice that `T` & `T` are not in conflict
// Because they're bound to their own scopes

type  testA  =  Referer<A, 'hi'> // {0: 'hi'}
type  testB  =  Referer<B, 'hi'> // ['hi']
type  testC  =  Referer<C, 'hi'> // ERROR

Types supérieurs

interface Monad<*T<X extends any>> {
  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>;
}

Termes de recherche

supérieur #order #type #references #lambda #HKTs

@ pirix-gh si vous ne lisez que quelques messages, vous pouvez voir que beaucoup de ce que vous demandez est déjà possible ou a déjà été demandé.

Je les ai lus, je pensais pouvoir résumer mes idées comme tout le monde l'a fait (pour une solution tout-en-un), principalement sur la syntaxe.

J'ai édité la proposition ci-dessus pour une meilleure explication de la façon dont nous pouvions enchaîner les références, et j'ai corrigé la façon dont un type comme Pipe fonctionnerait avec (il y avait quelques erreurs concernant la logique de celui-ci).

Toute mise à jour?

Toujours pas de mise à jour? À mon avis, ce problème est le principal obstacle à la réalisation de son plein potentiel par TypeScript. Il y a tellement de cas où j'essaie de taper correctement mes bibliothèques, pour abandonner après une longue lutte, réalisant que je me suis de nouveau heurté à cette limitation. Il est omniprésent et apparaît même dans des scénarios apparemment très simples. J'espère vraiment qu'il sera traité bientôt.

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

Il reste encore beaucoup de travail à faire, comme: corriger quickinfo, typeParameter infer, ajouter un message d'erreur, mettre en évidence le même typeConstructor .....
Mais cela commence à fonctionner, et voici ce que je pourrais obtenir pour le moment.
L'exemple d'interface provient de @millsp https://github.com/microsoft/TypeScript/issues/1213#issuecomment -523245130, la conclusion est vraiment très utile, grâce à cela.

J'espère que la communicité pourrait fournir plus de cas d'utilisateurs comme celui-là, pour vérifier si la méthode actuelle fonctionne dans la plupart des situations.

Il serait également agréable de fournir des informations sur HKT / programmation de fonctions / lambda (quand je dis lambda , je veux dire les maths, je ne pourrais trouver qu'un exemple écrit par un langage, sans maths)

Voici des choses qui m'aident beaucoup:

@ShuiRuTian En ce qui concerne m.join(q) renvoyant Set<unknown> , je suppose que --noImplicitAny provoque également l'émission d'un avertissement?

J'espère que la communauté pourra fournir plus de cas d'utilisateurs comme celui-là, pour vérifier si la méthode actuelle fonctionne dans la plupart des situations.

Il serait également agréable de fournir des informations sur HKT / programmation de fonctions / lambda (quand je dis lambda , je veux dire les maths, je ne pourrais trouver qu'un exemple écrit par un langage, sans maths)

Sans aller plus loin, j'ai récemment essayé de créer une fonction générique au curry filter , et je voulais qu'elle fasse quelque chose comme ceci:

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

Et je n'ai pas de moyen de le faire, car j'ai besoin d'un moyen de dire à TypeScript "retourner le même type que vous avez reçu, mais avec cet autre paramètre". Quelque chose comme ça:

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

@lukeshiru ouais, c'est essentiellement https://pursuit.purescript.org/packages/purescript-filterable/2.0.1/docs/Data.Filterable#v : filter
Il existe de nombreux autres cas d'utilisation similaires pour HKT dans TypeScript.

@isiahmeadows je l'ai essayé. Vous avez raison.
@lukeshiru et @raveclassic Merci! Cette fonctionnalité semble assez raisonnable. Je jetterais un œil à ceci après avoir lu https://gcanti.github.io/fp-ts/learning-resources/

Je suis coincé et je ne sais pas quelle est la « solution de contournement» actuelle ...
J'essaye d'implémenter la spécification de chaîne :

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

Une valeur qui implémente la spécification Chain doit également implémenter la spécification Apply.

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

J'ai fait un FunctorSimplex qui est ensuite prolongé par un FunctorComplex puis étendu par le Apply mais maintenant que je veux étendre Apply comme Chain ça casse ...

J'ai donc besoin de cela (image ci-dessous et lien vers le code):

(J'ai besoin de passer un type au ApType pour qu'à la ligne 12 le Apply ne soit pas «codé en dur» mais générique ... pour prendre également tous les types étendus d'un IApply )

Screenshot

Lien permanent vers l'extrait de code lignes 11 à 22 dans 7ff8b9c

`` `dactylographié
type d'exportation ApType = ( ap: Appliquer <(val: A) => B>, ) => IApply ;

/ * [...] * /

l'interface d'exportation IApply étend FunctorComplex {
/ ** Fantasy-land/ap :: Apply f => f a ~> f (a -> b) -> f b * /
ap: ApType ;
}
''

Référencé dans une question de débordement de pile: problème de types génériques TypeScript: solution de contournement requise

@Luxcium Tant que TS ne prend pas en charge les types de types supérieurs, seule leur émulation est possible. Vous voudrez peut-être y jeter un œil pour voir comment il est possible de réaliser:

Jusqu'à ce que TS prenne en charge les types de type supérieur, seule leur émulation est possible. Vous voudrez peut-être y jeter un œil pour voir comment il est possible de réaliser

Tanks beaucoup @kapke Je suis probablement trop dans ces jours de FP et comme en Javascript on peut renvoyer une fonction à partir d'une fonction on peut écrire pseudoFnAdd(15)(27) // 42 Je voudrais être capable, avec TypeScript, d'écrire pseudoType<someClassOrConstructor><number> // unknown mais je suis un enfant du scénario, pas un _ ° une sorte de personne qui a étudié pendant longtemps à l'université ou quelque chose comme ça ° _

Ces informations et conférences (lectures) sont très appréciées ...

Note: je suis francophone, en français le mot _lecture (s) _ a le sens de _readings_ et non `` un discours en colère ou sérieux donné à quelqu'un pour critiquer son comportement '' ...

Probablement ce que j'ai proposé comme solution de contournement simple sans PR ne fonctionnera pas dans tous les cas, mais je pense qu'il vaut la peine de le mentionner:

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[]
}
Cette page vous a été utile?
0 / 5 - 0 notes
bleepcoder.com utilise des informations sous licence publique GitHub pour fournir aux développeurs du monde entier des solutions à leurs problèmes. Nous ne sommes pas affiliés à GitHub, Inc. ni à aucun développeur qui utilise GitHub pour ses projets. Nous n'hébergeons aucune des vidéos ou images sur nos serveurs. Tous les droits appartiennent à leurs propriétaires respectifs.
Source pour cette page: Source

Langages de programmation populaires
Projets GitHub populaires
Plus de projets GitHub

© 2024 bleepcoder.com - Contact
Made with in the Dominican Republic.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.