Typescript: Zulassen, dass Klassen in anderen parametrischen Klassen parametrisch sind

Erstellt am 19. Nov. 2014  ·  140Kommentare  ·  Quelle: microsoft/TypeScript

Dies ist ein Vorschlag, um Generika als Typparameter zuzulassen. Es ist derzeit möglich, bestimmte Beispiele für Monaden zu schreiben, aber um die Schnittstelle zu schreiben, die alle Monaden erfüllen, schlage ich das Schreiben vor

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

Ebenso ist es möglich, bestimmte Beispiele für kartesische Funktoren zu schreiben, aber um die Schnittstelle zu schreiben, die alle kartesischen Funktoren erfüllen, schlage ich das Schreiben vor

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

Parametrische Typparameter können eine beliebige Anzahl von Argumenten annehmen:

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

Das heißt, wenn auf einen Typparameter eine Tilde und eine natürliche Arität folgen, sollte der Typparameter im Rest der Deklaration als generischer Typ mit der angegebenen Arität verwendet werden dürfen.

Genau wie jetzt sollten bei der Implementierung einer solchen Schnittstelle die generischen Typparameter ausgefüllt werden:

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

Ich schlage vor, dass typedefs nicht nur Kompositionen generischer Typen in den Argumenten direkt zulässt, sondern auch die Definition von Generika auf diese Weise unterstützt (siehe Ausgabe 308 ):

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

Die Aritäten der Definition und des Alias ​​müssen übereinstimmen, damit das typedef gültig ist.

Suggestion help wanted

Hilfreichster Kommentar

Mit HKT können die Einstellungen geändert, Gewohnheiten gebrochen, verlorene Generationen wieder zum Leben erweckt werden. Es wäre das Größte seit Generika und expliziten Nullen und Undefinierten. Es kann alles ändern

Bitte betrachtet es als nächstes großes Feature, hört Stopp für Menschen , die Sie für ein besseres Pferd fragen immer wieder, gibt sie af * g ferrari

Alle 140 Kommentare

Keine vorschnellen Annahmen zu treffen, aber ich glaube, Sie tippen es falsch. Alle Parametertypen erfordern Parameternamen, daher wollten Sie wahrscheinlich eingeben

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

Während Map im Moment eine Funktion ist, die einen Mapper vom Typ any (wobei Ihr Parametername A ) zu B .

Verwenden Sie das Flag --noImplicitAny um bessere Ergebnisse zu erzielen.

Danke, korrigiert.

Ich habe meinen Kommentar in einen Vorschlag aktualisiert.

: +1: Ein höherwertiger Typ wäre ein großer Bonus für ein funktionales Programmierkonstrukt, aber vorher würde ich es vorziehen, eine korrekte Unterstützung für Funktionen höherer Ordnung und generische: p zu haben

Quasi genehmigt.

Wir mögen diese Idee sehr, brauchen aber eine funktionierende Implementierung, um alle Implikationen und potenziellen Randfälle zu verstehen. Ein Beispiel-PR, das mindestens die 80% der Anwendungsfälle behandelt, wäre ein wirklich hilfreicher nächster Schritt.

Wie stehen die Leute zur Tilde-Syntax? Eine Alternative zu T~2 wäre so etwas wie

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

Dies ermöglicht die direkte Komposition von Generika, anstatt Typ-Aliase zu benötigen:

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

Es ist seltsam, explizite Arität zu haben, da wir das nirgendwo anders tun

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

ist jedoch etwas klarer, ich weiß, dass andere Sprachen * in ähnlichen Kontexten anstelle von ~ :

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

Wenn Sie diesen Punkt auf ein Extrem bringen, könnten Sie Folgendes bekommen:

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

Ich denke, T<~,~> ist auch klarer als T~2 . Ich werde den obigen Vorschlag ändern. Es ist mir egal, ob wir ~ oder * ; Es kann einfach keine JS-Kennung sein, daher können wir beispielsweise nicht _ . Ich sehe nicht, welchen Nutzen die Notation => bietet. Alle Generika verwenden einige Eingabetypen und geben einen einzelnen Ausgabetyp zurück.

Eine leichtere Syntax würde die Arität der Generika völlig auslassen; Der Parser würde es bei der ersten Verwendung herausfinden und einen Fehler auslösen, wenn der Rest nicht damit übereinstimmt.

Ich würde gerne mit der Implementierung dieser Funktion beginnen. Was ist das empfohlene Forum, um Entwickler über Details zur Transpiler-Implementierung zu belästigen?

Sie können viele neue Probleme für größere Fragen mit umfangreicheren Codebeispielen protokollieren oder ein lang anhaltendes Problem mit einer Reihe von Fragen erstellen, während Sie fortfahren. Alternativ können Sie sich hier https://gitter.im/Microsoft/TypeScript dem Chatraum anschließen und wir können uns dort unterhalten.

@metaweta irgendwelche Neuigkeiten? Wenn Sie Hilfe / Diskussion benötigen, würde ich gerne ein Brainstorming zu diesem Thema durchführen. Ich möchte diese Funktion wirklich.

Nein, die Dinge bei der Arbeit übernahmen die Freizeit, an der ich arbeiten musste.

Stoß: Gibt es eine Chance, diese Funktion jemals in Betracht zu ziehen?

https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288 ist immer noch der aktuelle Status. Ich sehe hier nichts, was uns dazu bringen würde, die Priorität der Funktion zu ändern.

Mir scheint, dass dies in weit mehr Situationen nützlich ist, als nur kategorietheoretische Abstraktionen zu importieren. Zum Beispiel wäre es nützlich, Modulfabriken schreiben zu können, die eine Promise -Implementierung (Konstruktor) als Argument verwenden, z. B. eine Datenbank mit einer steckbaren Versprechen-Implementierung:

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

: +1:

Mit HKT können die Einstellungen geändert, Gewohnheiten gebrochen, verlorene Generationen wieder zum Leben erweckt werden. Es wäre das Größte seit Generika und expliziten Nullen und Undefinierten. Es kann alles ändern

Bitte betrachtet es als nächstes großes Feature, hört Stopp für Menschen , die Sie für ein besseres Pferd fragen immer wieder, gibt sie af * g ferrari

Ja, in den ersten 15 Minuten nach dem Versuch, Typen zu einer vorhandenen JS-Codebasis hinzuzufügen, wurde darauf gestoßen. Ich wechsle nicht zu TS, bis ich es sehe.

Kann ich eigentlich helfen?

Ich frage mich, wie sich das auf # 7848 beziehen würde. Sie sind sich sehr ähnlich, obwohl es sich um die andere Facette höherer Arten handelt.

@ boris-marinov Ryan Cavanaughs Antwort sagt, dass Sie können:

Ein Beispiel-PR, das mindestens die 80% der Anwendungsfälle behandelt, wäre ein wirklich hilfreicher nächster Schritt.

Jetzt habe ich Zeit, eine so einfache PR-Hoffnung zu implementieren, um einige Hinweise von den Kernentwicklern zu erhalten, aber es gibt bisher keine Fragen - alles sieht gut und verständlich aus. Wird hier einen Fortschritt verfolgen.

@Artazor Möchten Sie auch einen Blick auf das Knacken von # 7848 werfen? Das kümmert sich um die andere Seite dieses Problems, das Generika betrifft, und meiner Meinung nach würde sich dies ohne sie unvollständig anfühlen (generische Parameter würden eine Menge Code auf Typebene wirklich vereinfachen).

Ich finde diesen Vorschlag absolut wunderbar. Höher sortierte Typen in TypeScript würden eine neue Ebene erreichen, in der wir leistungsfähigere Abstraktionen beschreiben könnten, als dies derzeit möglich ist.

Stimmt etwas nicht mit den Beispielen in OP? Das A in der Zeile

class ArrayMonad<A> implements Monad<Array> {

wird in keiner der Methoden verwendet, da alle ihre eigenen generischen A .

Wenn Sie functor mit map als Methode implementieren, die this wie würde es dann aussehen? So vielleicht?

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

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

@paldepind Check out # 7848. In dieser Diskussion geht es um diesen speziellen Anwendungsfall, obwohl IMHO dies und das wirklich in einer einzigen PR zusammengeführt werden muss.

Wann wird dieses Zeug landen? Das scheint eine Art wesentlich zu sein.

Wird es auch möglich machen, dass:

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

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

?

@whitecolor Ich denke, es gibt momentan größere Fische zum Braten, die eine höhere Priorität verdienen:

  1. TypeScript 2.0 RC wurde erst vor knapp 2 Wochen veröffentlicht. Das wird an sich viel Zeit in Anspruch nehmen.
  2. bind , call und apply , native JS-Funktionen, sind untypisiert. Dies hängt tatsächlich Vorschlag für verschiedene Generika ab . Object.assign benötigt ebenfalls einen ähnlichen Fix, aber verschiedene Generika allein lösen das nicht.
  3. Funktionen wie die Methoden _.pluck Lodash, die Methoden get und set Backbone-Modellen usw. sind derzeit untypisiert , und die Behebung dieses Problems macht Backbone mit TypeScript wesentlich sicherer. Dies kann auch Auswirkungen auf React in der Zukunft haben .

Nicht, dass ich diese Funktion nicht möchte (ich würde sie für eine solche Funktion lieben), ich sehe es einfach nicht als wahrscheinlich an, dass sie bald verfügbar sein wird.

@isiahmeadows
Danke für die Erklärung. Ja, der dritte Punkt in der Liste ist sehr wichtig und wartet auch auf https://github.com/Microsoft/TypeScript/issues/1295 .

Aber ich hoffe für die aktuelle Ausgabe vielleicht irgendwie in 2.1dev.

Genau. Hoffentlich schafft es das.

(Rang 2 Polymorphismus, den dieses Thema will, ist auch eine Notwendigkeit für
Benutzer von Fantasy Land, um die verschiedenen ADTs innerhalb dieser Spezifikation richtig einzugeben.
Ramda ist ein gutes Beispiel für eine Bibliothek, die dies ziemlich dringend benötigt.)

Am Dienstag, 6. September 2016, 11:00 Uhr schrieb Alex [email protected] :

@isiahmeadows https://github.com/isiahmeadows
Danke für die Erklärung. Ja, der dritte Punkt in der Liste ist sehr wichtig.
Warten auf # 1295 https://github.com/Microsoft/TypeScript/issues/1295
auch.

Aber ich hoffe für die aktuelle Ausgabe vielleicht irgendwie in 2.1dev.

- -
Sie erhalten dies, weil Sie erwähnt wurden.

Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -244978475,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AERrBMvxBALBe0aaLOp03vEvEyokvxpyks5qnX_8gaJpZM4C99VY
.

Diese Funktion scheint uns sehr dabei zu helfen, Reaktionsformen zu definieren. Zum Beispiel haben Sie struct:

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

Ich habe einen Handler, der definiert ist als:

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

Dieser Handler wurde als Requisite an React-Komponenten übergeben. Ich habe auch eine Funktion, die struct nimmt und dieselbe struct zurückgibt, aber mit Handlern anstelle von Werten:

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

Momentan kann ich diese Funktion nicht richtig eingeben, da TS keinen Typ basierend auf einer Struktur eines anderen Typs erzeugen kann.

Kuh Ich könnte mit HKT makeForm eingeben?

Hm, interessant.

Vielleicht ist so etwas möglich:

//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 Der interessanteste Punkt ist diese Zeile:

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

Erwähnenswert ist möglicherweise, dass HKT eine Antwort auf sogenannte Teiltypen gewesen sein könnte (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

Nicht ganz. {x: number?} nicht {x?: number} , weil einer
ist garantiert zu existieren, während der andere nicht existiert.

Am Dienstag, 11. Oktober 2016, 09:16 Uhr schrieb Aleksey Bykov [email protected] :

könnte erwähnenswert sein, dass HKT eine Antwort auf so genannte gewesen sein könnte
Teiltypen (# 4889 (Kommentar)
https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-247721155
):

Geben Sie MyDataProto ein eins: K.;;
ein anderer: K.;;
noch eine andere: K.;;
} Typ Identisch = a; Typ Optional = a?;
= {get (): a;

;;
;;
;;

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -252913109,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AERrBNFYFfiW01MT99xv7UE2skQ3qiPMks5qy4wRgaJpZM4C99VY
.

@isiahmeadows Sie haben Recht, im Moment gibt es keine Möglichkeit / Syntax, eine Eigenschaft wirklich optional zu machen, basierend nur auf ihrem Typ, und das ist eine Schande

Noch eine: Es wäre gut, wenn Eigentum zu readonly . Scheint eine Art Makrofunktion erforderlich zu sein.

Wirf das einfach raus ... Ich bevorzuge die Syntax * gegenüber der Syntax ~ . Etwas über ~ scheint aus Sicht des Tastaturlayouts so weit weg zu sein. Ich bin mir auch nicht sicher warum, aber ich denke, * scheint mit all den spitzen Klammern, die in der Mischung enthalten sind, ein bisschen besser lesbar / unterscheidbar zu sein. Ganz zu schweigen davon, dass Personen, die mit anderen Sprachen wie Haskell vertraut sind, die Syntax möglicherweise sofort mit HKT verknüpfen. Scheint etwas natürlicher.

Ich müsste der Syntax * zustimmen. Erstens ist es unterscheidbarer,
und zweitens repräsentiert es besser einen Typ "jeder Typ funktioniert".


Isiah Meadows
[email protected]

Am Sonntag, 6. November 2016, um 00:10 Uhr, Landon Poch [email protected]
schrieb:

Wirf das einfach raus ... Ich bevorzuge die * -Syntax gegenüber der ~ -Syntax.
Etwas an ~ scheint von einem Tastaturlayout so weit entfernt zu sein
Perspektive. Ich bin mir auch nicht sicher warum, aber ich denke * scheint ein bisschen mehr zu sein
lesbar / unterscheidbar mit allen spitzen Klammern, die in der Mischung sind.
Ganz zu schweigen von Leuten, die mit anderen Sprachen wie Haskell vertraut sind
Verknüpfen Sie die Syntax sofort mit HKT. Scheint etwas natürlicher.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -258659277,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AERrBHQ4SYeIiptB8lhxEAJGOYaxwCkiks5q7VMvgaJpZM4C99VY
.

Meilenstein: community ? Wie ist der aktuelle Status dieses Problems / dieser Funktion?

@whitecolor der Status ist DIY (mach es selbst)

Das Problem hat das Label Accepting PRs . Dies bedeutet, dass Pull-Anforderungen zur Implementierung dieser Funktion willkommen sind. Weitere Informationen finden Sie unter https://github.com/Microsoft/TypeScript/wiki/FAQ#what -do-the-Labels-on-this-Issues-Mean.

Siehe auch https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288

Ok, ich sehe die Etiketten, habe nur Zweifel, ob ein Nicht-TS-Team dazu in der Lage ist.

Jetzt habe ich Zeit, eine so einfache PR-Hoffnung zu implementieren, um einige Hinweise von den Kernentwicklern zu erhalten, aber es gibt bisher keine Fragen - alles sieht gut und verständlich aus. Wird hier einen Fortschritt verfolgen.

@Artazor Hast du Glück damit?

@raveclassic - es wurde schwieriger als es schien, aber ich hoffe immer noch, vorwärts zu kommen. Syntaktisch ist es offensichtlich, aber die Typprüfungsregeln / -phasen sind mir nicht so klar, wie ich will -)

Versuchen wir meine Aktivität wiederzubeleben -)

Verfolgen Sie einfach einen Fortschritt und den Weg der Ideenentwicklung. Ich habe drei Optionen zur Implementierung dieser Funktion in Betracht gezogen.

Ich habe geplant, ein TypeParameterDeclaration mit einer optionalen higherShape -Eigenschaft anzureichern

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

und haben drei Optionen in Betracht gezogen, wie HigherShape implementiert werden könnte

1. Einfache Arität für die Domain

type HigherShape = number

es entspricht der folgenden Verwendung:

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

In diesem einfachsten Fall würde der Typ number ausreichen. Trotzdem sollten wir in der Lage sein, für jeden Typ eine tatsächliche höhere Form zu bestimmen, um sicherzugehen, dass wir sie als Typargument für die spezifischen Formanforderungen verwenden können. Und hier stehen wir vor einem Problem: Die höhere Form der Demo -Klasse selbst ist nicht als Zahl auszudrücken. Wenn dies der Fall wäre, sollte es als 2 - da es zwei Typparameter hat,
und es wäre möglich zu schreiben

var x: Demo<Array, Demo>

und dann mit dem Problem der verzögerten Typprüfung mit der Eigenschaft .both kämpfen. Daher ist der Typ number nicht ausreichend (glaube ich);

Tatsächlich hat Typ Demo die folgende Form höherer Ordnung:

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

2. Vollständig strukturierte Domain und Co-Domain

Dann habe ich die entgegengesetzte, vollständigste Darstellung der höheren Formen untersucht, die es ermöglichen würde, solche Formen wie die oben genannten darzustellen, und noch schlimmer:

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

Die Datenstruktur hierfür ist unkompliziert, spielt jedoch nicht gut mit dem TypeScript-Typsystem zusammen. Wenn wir solche Typen höherer Ordnung zulassen würden, würden wir nie wissen, ob * den Bodentyp bedeutet, der für die Eingabe von Werten verwendet werden könnte. Außerdem habe ich es nicht einmal geschafft, eine geeignete Syntax zu finden, um solch monströse Einschränkungen höherer Ordnung auszudrücken.

3. Strukturierte Domäne / implizite einfache Co-Domäne

Die Hauptidee - Typausdruck (auch mit tatsächlichen Typargumenten) führt immer zu einem Grundtyp - der zum Eingeben einer Variablen verwendet werden kann. Andererseits kann jeder Typparameter seine eigenen detaillierten Typparameter in demselben Format haben, das an anderer Stelle verwendet wird.

Dies war meine endgültige Entscheidung, für die ich mich einsetzen würde.

type HigherShape = NodeArray<TypeParameterDeclaration>;

Beispiel:

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

Hier möchte ich darauf hinweisen, dass M für T1<M extends A> extends Z<M> lokal ist und in einem tieferen Sichtbarkeitsbereich als T1 existiert. Daher ist M im SomeClass -Körper nicht verfügbar.
Und * bedeutet einfach eine neue Kennung (anonymer Typ), die niemals mit irgendetwas in Konflikt gerät (und zu einem späteren Zeitpunkt implementiert werden könnte).


Somit die endgültige Signatur der TypeParameterDeclaration

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

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

Willst du eine Meinung von @DanielRosenwasser , @ aleksey-bykov, @isiahmeadows und anderen hören -)

Klingt für mich in Ordnung, aber ich weiß sehr wenig über die interne Struktur der Codebasis von TypeScript.

Ich möchte dem Chor meine Stimme hinzufügen und Sie anfeuern, Artazor! :) :)

Diese Funktion wäre für mich hilfreich, wenn ich Redux typsicher machen möchte.

@michaeltontchev Welche Probleme haben Sie, Redux typsicher zu machen?

Falls Sie interessiert sind, habe ich kürzlich https://github.com/bcherny/tdux und https://github.com/bcherny/typed-rx-emitter veröffentlicht , die auf Ideen von Redux und EventEmitter aufbauen.

Nun sieht es so aus, als müsste auf den @ rbuckton- Zweig # 13487 mit generischen Standardparametern zurückgegriffen werden. In anderen Fällen werden wir uns weitgehend widersprechen.

@bcherny - danke für die Links, ich werde sie überprüfen!

Ich habe mir überlegt, wie Mähdrescher typsicher gemacht werden können, indem sichergestellt wird, dass für jede Eigenschaft des Staates ein Reduzierer des richtigen Typs vorhanden ist (ohne Extras). Ich habe es in diesem speziellen Fall ohne verschachtelte Generika geschafft, aber eine verschachtelte Lösung wäre schöner gewesen. Ich habe folgendes:

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

Grundsätzlich garantiert der oben eingeführte Typ, dass die Reduzierungszuordnungen, die Sie an combinReducers übergeben, alle Eigenschaften Ihres Staates abdecken und die richtigen Rückgabetypen haben. Ich konnte bei der Online-Suche keine solche Lösung finden - scheint mir, dass dies ohne die Keyof-Funktion, die erst vor zwei Monaten herauskam, nicht möglich ist :)

Ich gehe davon aus, dass die Keyof-Funktion auch für ImmutableJs nützlich sein wird, um Setter und Getter typsicher zu machen, obwohl Sie möglicherweise noch einige zusätzliche Werkzeuge benötigen.

Bearbeiten: Um dies zu verdeutlichen, hätten verschachtelte Generika es mir ermöglicht, den Reducer-Typ nicht im StatePropertyNameAndTypeAwareReducer-Typ fest zu codieren, sondern ihn als Generikum zu übergeben, aber es müsste ein verschachteltes Generikum sein, was bei nicht möglich ist der Moment.

Edit2: Hier wurde ein Problem für Redux erstellt: https://github.com/reactjs/redux/issues/2238 Scheint, als würden sie nicht viel TypeScript ausführen, daher suchen sie nach TypeScript-Leuten, die Redux kennen, um abzuwägen.

Wie gehts?

Vielleicht eine naive Frage, aber warum ~ oder * anstelle eines regulären generischen Parameters? Soll es anzeigen, dass es nicht verwendet wird? Dh. warum nicht:

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

Oder:

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

Oder auch:

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

@bcherny Ich glaube, dies führt zu Unklarheiten in der Syntax, da Functor<A<T>> früher " A von T " bedeutet hätte, wobei T lokaler Typ ist Umfang. Es ist unwahrscheinlich, aber diese Syntax könnte aus demselben Grund auch für einige Codebasen eine bahnbrechende Änderung darstellen.

@masaeedu Ich T faul binden" und nicht " T streng im aktuellen Bereich binden".

Trotzdem denke ich, dass T: * => * hier am sinnvollsten ist, da es dafür "Stand der Technik" gibt.

In Haskell ist der Operator -> tatsächlich ein parametrisierter Typ (möglicherweise ist es einfacher, Func<TArg, TRet> zu visualisieren). Der Typkonstruktor -> akzeptiert zwei beliebige Typen T und U und erzeugt den Typ eines Wertekonstruktors (dh einer Funktion), der Werte vom Typ T zuordnet Werte vom Typ U .

Das Interessante ist, dass es auch ein freundlicher Konstruktor ist! Der Artenkonstruktor -> akzeptiert zwei beliebige Arten T* und U* (Sternchen nur zur visuellen Unterscheidung) und erzeugt die Art eines Typkonstruktors, der Arten der Art T* abbildet U* .

Möglicherweise bemerken Sie an dieser Stelle ein Muster. Die Syntax und Semantik, die zum Definieren und Verweisen auf Typen verwendet wird, wird einfach zum Definieren und Verweisen auf Typen wiederverwendet. Tatsächlich wird es nicht einmal wiederverwendet, sondern definiert implizit Dinge in zwei verschiedenen Universen gleichzeitig. (Die Tatsache, dass es tatsächlich isomorph ist, bedeutet, dass dies in der Lage ist, Dinge in unendlichen Ebenen, Werten -> Typen -> Arten -> Sorten -> ... zu definieren, mit Ausnahme der unglücklichen * , aber das ist es ein Thema für eine andere Zeit)

Tatsächlich ist dieses Muster so sinnvoll, dass einige Leute eine weit verbreitete GHCi-Erweiterung implementiert haben, die es auf alle Typkonstruktoren verallgemeinert, nicht nur auf -> . Die Erweiterung heißt "Datentypen", und so kommt Haskell durch seine heterogenen Listen ( [] ist sowohl die Art der Wertelisten als auch die Art der Typenlisten), heterogene Tupel, "intelligente Länge" "Vektoren und viele andere Merkmale außerdem.

Vielleicht wollen wir noch nicht so weit wie DataKinds , also bleiben wir einfach bei den freundlichen Konstruktoren * und -> , aber wenn wir Daniels vorgeschlagener Syntax folgen oder allgemeiner Artdefinitionen isomorph zu Typdefinitionen machen, öffnen wir uns, um die zukünftige Entwicklung in diesem Bereich zu nutzen.

In Anlehnung an meinen vorherigen Streifzug möchte ich empfehlen, dass wir any anstelle von * . Dies repräsentiert sowohl den Typ jedes Wertes als auch die Art jedes Typs. Wenn die Syntax verwirrend erscheint, können wir eine Seite aus Haskells Buch herausnehmen und ein Präfix ' , um Arten und Typen zu unterscheiden.

Das Beispiel von OP würde dann so geschrieben:

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

Nitpick: Ich finde any im Allgemeinen verwirrend in dem Sinne, dass es zwei verschiedene Dinge tut.
Es ist ein Supertyp aller anderen, wie nie ein Untertyp aller anderen. Wenn also eine Funktion nach einem any -Parameter fragt, können Sie alles eingeben. So weit, ist es gut.
Der Teil, in dem es lustig wird, ist, wenn eine Funktion nach etwas Bestimmtem fragt und Sie any . Diese Typprüfung, während jeder andere Typ, der breiter ist als gewünscht, stattdessen einen Fehler verursacht.
Aber ja, was auch immer.

In einem anderen Punkt wäre ' verwirrend, da es auch in String-Literalen verwendet wird.

@Artazor Gibt es Neuigkeiten dazu? Als Sie das letzte Mal erwähnt haben, müssen Sie die generischen Standardparameter neu festlegen. Und es klingt für mich so, als wären Sie der einzige, der einem funktionierenden POC nahe genug kommt.

Es lohnt sich auch darüber nachzudenken, wie dies mit der Subtypisierung interagiert. Die Verwendung von * allein reicht nicht aus. In Sprachen, die Ad-hoc-Polymorphismus anstelle von beschränktem Polymorphismus verwenden, gibt es Einschränkungsarten zum Einschränken akzeptabler Typargumente. Zum Beispiel ist die Art von Monad T tatsächlich Constraint , nicht * .

In TypeScript verwenden wir stattdessen strukturelle Untertypisierung, daher müssen unsere Arten die Untertypbeziehungen zwischen Typen widerspiegeln. Das Scala-Papier zu diesem Thema könnte einige gute Ideen zur Darstellung von Varianz- und Subtyp-Beziehungen in einem freundlichen System liefern: "Auf dem Weg zu gleichen Rechten für höherwertige Typen ".

Irgendwelche Fortschritte in diesem Bereich?

Ein alternativer Ansatz von @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasie-land-d41c361d0dbe

Das Problem mit dem Ansatz von fp-ts besteht darin, dass Sie ansonsten bewährte Bibliotheken erneut implementieren können. Für mich besteht die Idee von Typoskript darin, in der Lage zu sein, das, was derzeit als Best Practices in JavaScript angesehen wird, korrekt einzugeben, und Sie nicht zu einer erneuten Implementierung zu zwingen.

Es gibt hier viele Beispiele, die zeigen, dass HKT benötigt wird, um die Verträge, die wir derzeit in js libs verwenden, korrekt zu beschreiben, entweder Fantasy Land, Ramda oder Reaktionsformen.

Es wäre wirklich schön zu sehen, wie dies umgesetzt wird :)

~ Gibt es jemanden, der bereit / in der Lage ist, daran zu arbeiten? Fühlen Sie sich frei, mich zu

Ein alternativer Ansatz von @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasie-land-d41c361d0dbe

Ich habe mich nicht darum gekümmert, das vollständig zu verstehen, da ich beobachte, dass das resultierende map immer noch explizit den Containertyp Option spezifiziert und daher nicht vollständig generisch ist, wie dies bei höherwertigen Typen (HKT) der Fall ist. kann bieten:

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

Wie @spion am 26. August 2016 feststellte, ist HKT erforderlich, um generische Funktionen generisch zu machen, die eine Factory benötigen und in denen der typparametrisierte Containertyp selbst generisch sein soll. Wir hatten dies in unseren Diskussionen über das Design von Programmiersprachen untersucht.

PS Wenn Sie neugierig sind, spielt diese Funktion eine wichtige Rolle bei meiner (einschließlich Analyse der Programmiersprachenlandschaft .

@ Shelby3 FWIW Option 's map (in Option.ts ) ist nicht generisch, da es die Instanz darstellt, während Functor map (in Functor.ts ) ist generisch, da es die Typklasse darstellt . Anschließend können Sie eine generische lift -Funktion definieren, die mit jeder Funktorinstanz ausgeführt werden kann.

Es wäre wirklich schön zu sehen, wie dies umgesetzt wird :)

Ich bin sehr einverstanden :)

@ Shelby3 : Um eine Funktion wie diese zusammenzuführen, ist es möglicherweise am besten, sie zu erhalten
sie sollen es auf der TS-Roadmap priorisieren; Ich hatte ein paar PRs, die hauptsächlich bekamen
Feedback / Zusammenführungen entweder bei kleinen Korrekturen oder wenn sie bereits gesucht haben
in sie. Ich möchte nicht negativ sein, aber es ist eine Überlegung, wenn Sie sind
im Begriff, Ressourcen in diese zu investieren.

Am 8. Januar 2018, 16:05 Uhr, schrieb "shelby3" [email protected] :

Gibt es jemanden, der bereit / in der Lage ist, daran zu arbeiten? Fühlen Sie sich frei zu kontaktieren
mich [email protected] zu diskutieren. Oder jeder kann jemanden coachen
Wir könnten feststellen, dass wir daran arbeiten können. Bitte lassen Sie es mich auch wissen.

Ein alternativer Ansatz von @gcanti https://github.com/gcanti
https://medium.com/@gcanti/higher -kinded-types-in
Typoskript-statisch-und-Fantasie-Land-d41c361d0dbe

Ich habe mir nicht die Mühe gemacht, das vollständig zu verstehen, weil ich die resultierende Karte beobachte
Gibt immer noch explizit den Option-Containertyp an und ist daher nicht vollständig
generisch in einer Weise, die höherwertige Typen (HKT) bieten können:

Funktionszuordnung (f: (a: A) => B, fa: HKTOption ): Option { return (fa als Option ) .map (f) }}

HKT sind notwendig, um generische Funktionen zu erstellen, die eine Fabrik benötigen und
in dem der typparametrisierte Containertyp selbst generisch sein soll. Wir hatten
erkundete diese https://github.com/keean/zenscript/issues/10 in unserem
Diskussionen über das Design von Programmiersprachen.

PS Wenn Sie neugierig sind, spielt diese Funktion eine wichtige Rolle für mich
(einschließlich @keean https://github.com/keean 's) Analyse der
Programmiersprachenlandschaft
https://github.com/keean/zenscript/issues/35#issuecomment-355850515 . ich
erkennen, dass unsere Ansichten nicht vollständig mit den Prioritäten von Typescript korrelieren
mit dem primären Ziel, eine Obermenge von Javascript / ECMAScript und Unterstützung zu sein
dieses Ökosystem.

- -
Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-355990644 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AC6uxYOZ0a8G86rUjxvDaO5qIWiq55-Fks5tIi7GgaJpZM4C99VY
.

@ gcaniti entschuldigt sich für den Lärm und danke für die zusätzliche Erklärung. Ich hätte vor dem Kommentieren mehr lernen sollen. Natürlich ist es mein Fehler bei der Konzeptualisierung, weil (ich weiß bereits) ein Funktor eine Instanzimplementierung benötigt.

Afaics, Ihr cleverer "Hack" ermöglicht die generische Bezugnahme auf eine Fabrik (z. B. lift ), erfordert jedoch das zusätzliche Boilerplate von Module Augmentation, um jede Typisierung der generischen Fabrik für den speziellen Typ des Funktors zu aktualisieren und zu spezialisieren , zB Option . Wäre dieses Boilerplate nicht für jede generische Verwendung einer generischen Fabrik erforderlich, z. B. das generische Beispiel sort @keean und ich haben darüber gesprochen? Möglicherweise müssen auch andere Eckfälle entdeckt werden?

Hat Kotlin Ihre Idee kopiert oder umgekehrt? (einige zusätzliche Kritikpunkte an diesem Link, aber ich weiß nicht, ob sie für den Typescript-Fall gelten)

Ich möchte nicht negativ sein, aber es ist eine Überlegung, ob Sie Ressourcen in diese investieren wollen.

Ja, dieser Gedanke kam mir auch in den Sinn. Danke, dass du es artikuliert hast. Ich vermute, eine der Überlegungen wären die Auswirkungen auf das Typsystem und alle möglichen Eckfälle, wie @masaeedu hervorhebt. Vielleicht würde es Widerstand geben, wenn dies nicht sehr gründlich durchdacht und demonstriert würde.

Hinweis: Ich untersuche auch Ceylon, um besser zu ermitteln, wie hoch die Investition in das EMCAScript-Kompilierungsziel sein wird. (Ich muss mehr lernen).

Ich bin auch gerade von dieser Einschränkung gebissen worden. Ich möchte, dass I im folgenden Beispiel automatisch abgeleitet wird:


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

Soweit ich das beurteilen kann, erfordert dies einen höherwertigen Typ oder die Angabe eines doppelten generischen Parameters (z. B. doStuff<User, number>() ), der redundant erscheint.

Diese Einschränkung hat mich kürzlich ebenfalls beeindruckt.

Ich habe an einer Bibliothek für Versprechen gearbeitet . Es bietet verschiedene Dienstprogrammfunktionen für die Arbeit mit ihnen.

Ein zentrales Merkmal der Bibliothek ist, dass sie dieselbe Art von Versprechen zurückgibt, die Sie in sie gegeben haben. Wenn Sie also ein Bluebird-Versprechen verwenden und eine der Funktionen aufrufen, wird ein Bluebird-Versprechen mit allen zusätzlichen Funktionen zurückgegeben, die sie bieten.

Ich wollte dies im Typsystem codieren, aber mir wurde schnell klar, dass dies die Arbeit mit einem Typ P der Art * -> * erfordert, so dass P<T> extends Promise<T> .

Hier ist ein Beispiel für eine solche Funktion:

/**
* 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;

In der obigen Situation konnte ich vermeiden, höhere Arten zu benötigen, indem ich den ziemlich hackigen Typ this verwendete.

Der folgende Fall kann jedoch nicht gelöst werden:

/**
* 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[]>;

Weil es keinen Hack gibt, mit dem ich this<T[]> oder so machen kann.

Beachten Sie meine kleine Entschuldigung in der Dokumentation.

Ich habe ein anderes Szenario erhalten, in dem ich glaube, dass diese Funktion nützlich wäre (vorausgesetzt, ich habe den Vorschlag richtig interpretiert), wie in der obigen Referenz angegeben.

In dem fraglichen Paket muss eine nicht typisierte generische Klasse oder Funktion als Typ verwendet werden, da die generischen Typen normalerweise vom Benutzer erstellt werden.

Wenn ich den Vorschlag auf mein Szenario anwende, glaube ich, dass er ungefähr so ​​aussehen würde:

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

Da es in meiner Implementierung keine Möglichkeit gibt, die generischen Typen zuverlässig zu referenzieren, bin ich sicher, dass ich etwas verpasst habe.

@ Silic0nS0ldier Eigentlich kann dieser Fall jetzt gelöst werden. Sie verwenden einen Strukturkonstruktortyp wie folgt:

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

Und dann sagen Sie component ?: ComponentConstructor .

Noch allgemeiner können Sie tatsächlich einen generischen Funktionstyp haben:

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

Dies wird als parametrischer Polymorphismus mit Rang n bezeichnet und ist in Sprachen eigentlich ein ziemlich seltenes Merkmal. Umso rätselhafter ist es, warum TypeScript keine höherwertigen Typen hat, was eine weitaus häufigere Funktion ist.

Die hier beschriebene Einschränkung wird angezeigt, wenn Sie auf ein bestimmtes TComponent<T, S> verweisen müssen. In Ihrem Fall scheint dies jedoch unnötig.


Sie können auch typeof Component , um den Typ des Konstruktors Component Dies führt jedoch zu verschiedenen Problemen mit Untertypen.

@GregRos Ihre vorgeschlagene Lösung sah vielversprechend aus (sie akzeptiert die Typen in der Definitionsdatei), aber kompatible Typen werden abgelehnt. https://gist.github.com/Silic0nS0ldier/3c379367b5e6b1abd76e4a41d1be8217

@ Silic0nS0ldier Siehe meine Kommentare zum Kern.

@chrisdavies Funktioniert das?

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 Yep. Das funktioniert für mein Szenario. Ich hatte dieses Beispiel nicht in den Dokumenten gefunden (obwohl ich dafür bekannt bin, dass Dinge fehlen!). Vielen Dank!

Ich habe viel über diese Funktion nachgedacht, und ich habe einige Gedanken zu diesem Thema, die sich auf eine Art Spezifikation stützen, aber ich kann immer noch viele Probleme erkennen. Meine Vorschläge unterscheiden sich ein wenig von den bisher vorgeschlagenen.


Zunächst denke ich, dass die Verwendung einer beliebigen T<*, *> -Syntax für Typkonstruktoren eine schlechte Idee ist, da sie sich nicht gut mit der Komplexität des Typkonstruktors skalieren lässt. Ich bin mir auch nicht sicher, ob es sinnvoll ist, die Art des Typkonstruktors anzugeben, wenn auf ihn verwiesen wird, da wir dies nicht für Funktionen tun, auch nicht für Funktionen mit Typparametern.

Ich denke, der beste Weg, dies zu implementieren, besteht darin, höherwertige Typen wie andere Typen mit regulären Namen zu behandeln und eine gute Subtypbeziehung über Typkonstruktoren selbst zu definieren, die zum Auferlegen von Einschränkungen verwendet werden kann.

Ich denke, wir sollten eine Art Präfix oder Postfix verwenden, um sie von anderen Typen zu unterscheiden, hauptsächlich um Benutzer vor unverständlichen Fehlermeldungen zu schützen, an denen Typkonstruktoren beteiligt sind, wenn sie nur regulären Code schreiben wollten. Ich mag das Aussehen von: ~Type, ^Type, &Type oder so ähnlich.

So könnte beispielsweise eine Funktionssignatur sein:

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

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

(Ich verwende absichtlich nicht das Präfix ~ für die erstellten Typen.)

Durch die Verwendung von extends hier habe ich im Grunde zwei Dinge gesagt:

** 1. Wenn es nötig ist: ~L ist ein Typkonstruktor, der die gleiche Art wie ~List , dh die Art * -> * (oder vielleicht * => * , seit => ist der TypeScript-Pfeil.

  1. ~L ist ein Subtyp von ~List . **

Die Verwendung von extends zur Bezeichnung der Art eines Typkonstruktors skaliert zu beliebig komplexen Typkonstruktoren, einschließlich Dingen wie ((* => *) => (* => *)) => * .

Sie können diese Art in der Kennung des Typs nicht wirklich sehen, aber ich glaube nicht, dass Sie das müssen. Ich bin mir nicht einmal sicher, ob eine Subtypbeziehung zwischen Typkonstruktoren Arten beibehalten muss, daher ist (1) möglicherweise nicht erforderlich.

Keine unvollständigen Typen konstruieren

Ich denke, wir sollten die Konstruktion unvollständiger Typen nicht unterstützen. Das heißt, so etwas:

(*, *) => * => *

Ich denke, es würde mehr Ärger verursachen, als es wert ist. Das heißt, jeder Typkonstruktor muss einen konkreten Typ erstellen, und dieser konkrete Typ muss in dem Kontext angegeben werden, in dem der TC definiert ist.

Strukturelle Art der Definition von Typkonstruktoren

Ich denke auch, dass es eine strukturelle Möglichkeit geben sollte, Typkonstruktoren anzugeben, genauso wie jeder andere Typ strukturell angegeben werden kann, einschließlich generischer Funktionstypen sehr hoher Ordnung. Ich habe über Syntax nachgedacht wie:

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

Dies ähnelt der vorhandenen Syntax für Funktionstypen mit Typparametern:

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

Die beiden können sogar kombiniert werden, um dies zu erhalten:

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

Welches ist ein Typkonstruktor, der einen generischen Funktionstyp erstellt.

Der Vorteil besteht darin, dass diese Strukturtypen verwendet werden können, wenn andere Strukturtypen angegeben werden, wenn Typbeschränkungen angegeben werden usw. Manchmal bedeutet dies, dass sie lokale Referenzsymbole verwenden können, auf die von nirgendwo anders verwiesen werden kann.

Hier ist ein Beispiel:

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

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

Im obigen Beispiel verweist der Strukturtypkonstruktor ~<A>List<A, B> auf den Typparameter B . Es ist nicht möglich, diese Beziehung auf andere Weise anzugeben, zumindest ohne den teilweise konstruierten Typ List<A, *> zu codieren. Es gibt auch andere Beispiele.

Die Subtyp-Beziehung

Die Subtyp-Beziehung scheint sinnvoll zu sein, aber ich bin auf eine Reihe von Schwierigkeiten gestoßen, sie zu charakterisieren.

Meine erste Idee war die folgende. Damit ~A ein Subtyp von ~B :

  1. (a) Sie müssen die gleiche Art haben (in Bezug auf Arität, nicht Einschränkungen).
  2. (b) Für jede rechtliche Parametrisierung T₁, T₂, ... von ~A muss A<T₁, T₂, ...> ein Subtyp von B<T₁, T₂, ...> .

Dies hat jedoch mehrere Einschränkungen.

  1. Klasse MySpecialPromise implementiert PromiseLike {} In diesem Fall ist ~MySpecialPromise kein Subtyp von ~PromiseLike da es verschiedene Arten gibt.

  2. Klasse MyArrayPromiseimplementiert PromiseLike

    Die Subtyp-Beziehung wird auch in diesem Fall nicht beibehalten.

Eine allgemeinere Version von (b) ist die folgende:

(b) Für jede legale Parametrisierung T₁, T₂, ... von ~A gibt es eine Parametrisierung S₁, S₂, ... von ~B so dass A<T₁, T₂, ...> ein Subtyp von ist B<S₁, S₂, ...> .

Mit anderen Worten gibt es eine Abbildung F (T₁, T₂, ...) = S₁, S₂, ... mit den obigen Eigenschaften. Diese Zuordnung muss verwendet werden, um das parametrisierte B<...> aus einem parametrisierten A<...> zu konstruieren. Dies kann uns auch dann ermöglichen, wenn die Typkonstruktoren unterschiedliche Arten haben.

Das Problem mit dieser Beziehung ist, dass ich nicht sicher bin, wie es möglich wäre, die richtige Zuordnung zu finden. In Sprachen mit nominaler Typisierung lautet jede Aussage wie folgt:

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

Definiert eine Zuordnung zwischen den Typparametern von ~A und den Typparametern von ~B . Auf diese Weise kann die Zuordnung wiederhergestellt werden. Im TypeScript-System der strukturellen Typisierung können wir uns jedoch nicht auf explizite Anweisungen dieses Typs verlassen.

Eine Möglichkeit besteht darin, Typkonstruktoren nur für Typen mit den richtigen Typinformationen zu unterstützen, z. B. implements -Klauseln oder eine Art abstraktes Typelement, das Scala ähnelt. Ich bin mir jedoch nicht sicher, ob dies der richtige Weg ist.

@ GregRos - Interessante Hinweise! Ein paar Fragen.


Was meinst du mit konkreter Art? Meinen Sie etwas mit der Art * oder einem Typ ohne gebundene Typparameter?


Keine unvollständigen Typen konstruieren
Ich denke, wir sollten die Konstruktion unvollständiger Typen nicht unterstützen. Das heißt, so etwas:
(*, *) => * => *

Was meinen Sie mit der Konstruktion unvollständiger Typen? Meinen Sie damit, dass jede Anwendung wie L<A> die Art * haben sollte? Ist der Paar-Konstruktor in Ihrem Beispiel etwas Besonderes, wäre (* => *) => * => * in Ordnung?


Strukturelle Art der Definition von Typkonstruktoren

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

Unterscheiden sich diese Beispiele, außer dass die ersten anonym sind?


Die Subtyp-Beziehung

~A und ~B beziehen sich nicht auf Typen. Ist es also sinnvoll, dass sie eine Subtyp-Beziehung haben? Wann müssen Sie tatsächlich überprüfen, ob ein Konstruktor ein "Subtyp" eines anderen ist? Ist es möglich zu warten, bis die Konstruktoren angewendet werden, und die resultierenden Typen zu überprüfen?

@ jack-williams Danke für das Feedback!

Was meinen Sie mit der Konstruktion unvollständiger Typen? Meinen Sie damit, dass jede Anwendung wie L<A> eine Art * haben sollte? Ist der Paar-Konstruktor in Ihrem Beispiel etwas Besonderes, wäre (* => *) => * => * in Ordnung?

Ja genau. Jede Anwendung wie L<A> sollte die Art * . Ich bin mir nicht sicher, wie verkauft ich davon bin.


Unterscheiden sich diese Beispiele, außer dass die ersten anonym sind?

Der erste ist ein Typausdruck, während der zweite eine Deklaration ist. Sie sind in den meisten Punkten identisch, genauso wie diese Typen in den meisten Punkten identisch sind:

{
     a : number;
     b : string;
}

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

Die Syntax hat mehrere Gründe:

  1. Wie alles andere in TypeScript können Typkonstruktoren strukturell und anonym angegeben werden.
  2. Ein Typausdruck (wie das oben erwähnte anonyme Objekt) kann in bestimmten Kontexten verwendet werden, in denen Deklarationsanweisungen nicht verwendet werden können, z. B. in den Typensignaturen von Funktionen. Auf diese Weise können sie lokale Kennungen erfassen und Dinge ausdrücken, die sonst nicht ausgedrückt werden können.

~A und ~B beziehen sich nicht auf Typen. Ist es also sinnvoll, dass sie eine Subtyp-Beziehung haben? Wann müssen Sie tatsächlich überprüfen, ob ein Konstruktor ein "Subtyp" eines anderen ist? Ist es möglich zu warten, bis die Konstruktoren angewendet werden, und die resultierenden Typen zu überprüfen?

Typkonstruktoren können als Typen angesehen werden oder nicht. Ich schlage vor, sie als Typen zu betrachten, nur als unvollständige, die keine Werte haben und in keinem Kontext erscheinen können, der den Typ eines Wertes erfordert. Dies ist die gleiche Philosophie, die Scala in diesem Dokument verfolgt

Mit einer Subtyp-Beziehung meine ich im Wesentlichen eine Art "Konformitäts" -Relation, die verwendet werden kann, um Typkonstruktoren einzuschränken. Wenn ich zum Beispiel eine Funktion schreiben möchte, die für alle Arten von Versprechungen verschiedener Typen funktioniert, wie z. B. Promise<T> , Bluebird<T> usw., muss ich die Fähigkeit haben, TC-Parameter mit dem zu beschränken Schnittstelle PromiseLike<T> in irgendeiner Weise.

Das natürliche Wort für diese Art von Beziehung ist eine Subtyp-Beziehung.

Schauen wir uns ein Beispiel an. Angenommen, wir haben eine Subtypbeziehung zwischen Typkonstruktoren ausgearbeitet, kann ich eine Funktion wie diese schreiben:

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

Und die Einschränkung ~P extends ~PromiseLike soll garantieren, dass dies eine Funktion ist, die Versprechen erfüllt und nur Versprechen. Die Einschränkung garantiert auch, dass promise im Hauptteil der Funktion PromiseLike<A> implementiert und so weiter. Schließlich sind die von TypeScript im Hauptteil der Funktion erkannten Elemente genau diejenigen, deren Existenz durch Einschränkungen nachgewiesen werden kann.

Auf die gleiche Weise Promise<T> extends PromiseLike<T> , weil sie strukturell kompatibel sind und untereinander ersetzt werden können, ~Promise extends ~PromiseLike weil sie strukturell kompatible Typen konstruieren und somit untereinander ersetzt werden können.


Um das Problem mit dem Subtyp-Problem zu unterstreichen, betrachten Sie noch einmal:

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

Können wir über ~MyPromise genauso abstrahieren wie über ~Promise ? Wie erfassen wir die Beziehung zwischen ihnen?

Das Mapping, über das ich zuvor gesprochen habe, ist das Mapping, das bei einer Parametrisierung von ~MyPromise eine Parametrisierung von ~Promise so dass der von ~MyPromise konstruierte Typ ein Subtyp von ist eine konstruiert von ~Promise .

In diesem Fall sieht das Mapping folgendermaßen aus:

T => T[]

@ GregRos

In diesem Fall ist ~MySpecialPromise kein Subtyp von ~PromiseLike da es verschiedene Arten gibt.

In Haskell wird diese Art von Problem gelöst, indem die teilweise Anwendung von Typen ermöglicht und Typen definiert werden, sodass der endgültige Typparameter mit dem Typparameter der von Ihnen implementierten Schnittstelle übereinstimmt.

In Ihrem Beispiel würde MySpecialPromise als MySpecialPromise<TSpecial, TPromiseVal> , und ~MySpecialPromise<SpecialType> hätte die gleiche Art wie ~Promise .

@ GregRos

Mit einer Subtyp-Beziehung meine ich im Wesentlichen eine Art "Konformitäts" -Relation, die verwendet werden kann, um Typkonstruktoren einzuschränken. Zum Beispiel, wenn ich eine Funktion schreiben möchte, die für alle Arten von Versprechungen verschiedener Art funktioniert, wie z. B. VersprechenBluebirdund so weiter, ich brauche die Fähigkeit, TC-Parameter mit der Schnittstelle PromiseLike zu beschränkenirgendwie.
function mapPromise<~P extends ~PromiseLike, A, B>(promise : P<A>, func : (x : A) => B) : P<B> ;

Ich denke, wenn es darum geht, diese Funktion zu überprüfen, würden Sie versuchen, BlueBird<T> und PromiseLike<T> für die ausgewählten T vereinheitlichen. Dies sind nur konkrete Typen und fallen unter die Untertypisierung. Ich verstehe nicht, warum Sie eine spezielle Beziehung für die Konstruktoren ~BlueBird und ~PromiseLike benötigen würden.

Ich denke, es würde in so etwas verwendet werden?


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;

Hier möchten Sie vielleicht überprüfen, ob die Einschränkungen von y die Einschränkungen von x implizieren. Verfügt TypeScript jedoch noch nicht über Maschinen, um zu überprüfen, ob BlueBird<T> PromiseLike<T> , die verwendet werden könnten?

@ jack-williams Es kommt darauf an, wie Sie die folgende Einschränkung angeben:

~ P ist ein Typkonstruktor , so dass für alle A , P<A> ein Subtyp von ist PromiseLike<A> .

Welche Syntax würden Sie verwenden? Was für ein Konzept würden Sie verwenden? Sie können so etwas schreiben:

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

Diese Syntax weist jedoch Einschränkungen auf. Zum Beispiel können Sie diese Klasse überhaupt nicht ausdrücken, da wir den Typ P<A> an der Stelle, an der er deklariert ist, nicht konstruieren können, um ihn einzuschränken:

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

Aber ich denke, Sie können dafür existenzielle Typen verwenden , wie folgt:

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

Dann können Sie verlangen, dass alle Typkonstruktoren über ihre konstruierten Typen innerhalb der Signatur der Funktion oder des Typs eingeschränkt werden, optional unter Verwendung existenzieller Typen.

Bei existenziellen Typen hätte dies die gleiche Ausdruckskraft wie eine Subtypbeziehung mit einer Zuordnung.

Dies hätte jedoch mehrere Probleme:

  1. Die Angabe von Typkonstruktoren mit Arten wie ((* => *) => *) => * würde die Einführung vieler existenzieller Typen erfordern, von denen einige höherwertiger sein müssten. Alle müssten in der Signatur der Funktion oder Klasse erscheinen.
  2. Ich bin mir nicht ganz sicher, ob es einfacher wäre, die fraglichen existenziellen Typen zu finden, als das Mapping.
  3. Ich denke, es ist weniger elegant als die Subtyp-Beziehung.
  4. Führt möglicherweise eine andere Form von Typ ein, mit der Sie sich befassen müssten.

@ GregRos

Welche Syntax würden Sie verwenden? Was für ein Konzept würden Sie verwenden?

Persönlich würde ich keine spezielle Syntax verwenden und nur verwenden:

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

Dies ist jedoch nur meine Meinung, da ich Dinge wie number als Null-Konstruktor betrachte: Es muss also keine Unterscheidung geben.

Meine Ansicht zur Untertypisierung für Konstruktorfunktionen wäre, sie so einfach wie möglich zu halten. Sie sollten die gleiche Arität haben und die Parameter sollten Unterarten voneinander sein, wobei Kontravarianz und Kovarianz ähnlich wie im Scala-Papier berücksichtigt werden.

Eine teilweise Anwendung kann die Fälle umgehen, in denen sie unterschiedliche Aritäten haben (es würde mir nichts ausmachen, wenn Typkonstruktoren automatisch curryng werden, sodass Sie einfach MySpecialPromise<SpecialType> schreiben können).

In dem Beispiel interface MyPromise<T> extends Promise<T[]> {} ich ehrlich sein und sagen, dass ich nicht davon überzeugt bin, dass es die Komplexität wert ist, diesen Fall zu behandeln - ich denke, es wäre eine ausreichend nützliche Funktion ohne sie.

Die Behandlung dieses Falls entspricht (glaube ich) der Aussage: ~MyPromise extends ~(Promise . []) wobei [] der Listenkonstruktor und . die Konstruktorkomposition ist. Dies scheint viel schwieriger zu werden, da es jetzt nicht mehr ausreicht, nur die Struktur der Konstruktoren zu untersuchen, sondern auch über die Komposition nachzudenken!

@ jack-williams Dies funktioniert nicht mit Standardtypparametern. Wenn ich P extends Foo schreibe, wobei Foo einen Standardtypparameter hat, dh type Foo<T = {}> = ... , was ist dann die Art von P ?

Ich möchte nur sagen, dass ich Typen höherer Ordnung gutheiße (ich hatte Situationen in echten TypeScript-Projekten, in denen sie nützlich wären).

Ich denke jedoch nicht, dass sie das Curry unterstützen sollten. Ich liebe Haskell, aber das passt einfach nicht zu TypeScript.

Typen höherer Ordnung sind auch ohne Curry oder Teilanwendung nützlich, aber wenn eine Teilanwendung erforderlich ist, würde ich es vorziehen, eine explizite Syntax dafür zu sehen. Etwas wie das:

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

@ Cameron-Martin

Edit: Sorry, ich glaube nicht, dass meine Kommentare nicht sehr klar waren. Mit P hat seine eigene Art meine ich, dass es eine Art hat, die ihm durch seine Verwendung auferlegt wird. Angenommen, Einschränkungen werden immer als die höchste Art angenommen, also wird Foo als ~Foo . Nur wenn wir P als niedrigere Art erzwingen, prüfen wir, ob Foo einen Standardparameter hat. Mein Anliegen ist eine freundliche Schlussfolgerung, aber in diesem Fall hilft ~ nicht und ich denke, wir brauchen vollständige Anmerkungen.

P hat seine eigene Art, nicht wahr? Wäre die Frage nicht, ob wir Foo als ~Foo oder als Foo<{}> : Ich würde argumentieren, dass dies von der Art von P bestimmt wird. Also, wenn P ist ein Typ, den wir den Standardparameter erzwingen, und wenn P ein Konstruktor * => * , behandeln wir Foo gleich.

@Pauan Stimmen Sie Ihren Vorschlägen zu.

@ jack-williams Ich habe diesen Begriff der Untertypisierung in Betracht gezogen, wie ich bereits erwähnt habe:

Meine erste Idee war die folgende. Damit ~A ein Subtyp von ~B :

  1. (a) Sie müssen die gleiche Art haben (in Bezug auf Arität, nicht Einschränkungen).
  2. (b) Für jede rechtliche Parametrisierung T₁, T₂, ... von ~A muss A<T₁, T₂, ...> ein Subtyp von B<T₁, T₂, ...> .

Das Problem ist, dass wir, wenn wir die Dinge so einfach wie möglich halten, eine Subtyp-Beziehung erhalten, die paradox ist und nicht in die Sprache passt.

Wenn MyPromise<T> extends Promise<T[]> bedeutet, dass MyPromise<T> überall dort verwendbar sein muss, wo Promise<T[]> verwendbar ist, aber dies wäre nicht mehr der Fall.

Wenn Sie as , um a : MyPromise<T> in Promise<T[]> umzuwandeln, würden Sie eine Übertragung durchführen, aber dies würde paradoxerweise a zuweisbarer machen.

Bestehende generische Einschränkungen, die der vorhandenen Subtyp-Beziehung folgen, können ebenfalls verwendet werden, um einen ähnlichen Effekt zu erzielen und seltsames Verhalten zu verursachen:

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

Das Tippen würde auch als Nebeneffekt zumindest teilweise nominal werden. Diese Typen wären plötzlich unterschiedlich, wo sie derzeit gleich sind:

type GenericNumber<T> = number;

type RegularNumber = number;

Ich bin mir nicht einmal sicher, welche Auswirkungen dies auf komplexe Vereinigungs- / Schnittpunkttypen mit Typparametern, rein strukturellen Typen, Typen mit Elementverständnis und dergleichen haben würde.

Mein persönliches Gefühl ist: Die Subtyp-Beziehung über Typ-Konstruktoren muss die bestehende respektieren, nicht ablehnen . Leider erfordert dies, dass die Dinge komplexer werden.


Der wichtigste Grund für eine Art spezieller Notation für Typkonstruktoren ist , dass 99% der Entwickler nicht wissen , was ein Typkonstruktor ist und möchte nicht mit Fehlermeldungen über sie bombardiert werden.

Dies unterscheidet sich stark von Haskell, wo jeder Entwickler gesetzlich verpflichtet ist, einen fortgeschrittenen Kurs in Kategorietheorie zu belegen.

Ein sekundärer Grund ist, dass in einigen Fällen (wie im Fall der oben genannten Standardparameter) die Syntax entweder mehrdeutig ist oder es überhaupt nicht möglich ist, über einen bestimmten Typkonstruktor zu abstrahieren.

EDIT: Sorry @GregRos Ich habe deine letzten Kommentare nicht gesehen!

Die Subtypbeziehung über Typkonstruktoren muss die vorhandene respektieren, nicht ablehnen.

Wenn dies erreicht werden kann, stimme ich zu. Ich habe einfach nicht alle Details im Kopf und wie einfach das sein würde.

Ein sekundärer Grund ist, dass in einigen Fällen (wie im Fall der oben genannten Standardparameter) die Syntax entweder mehrdeutig ist oder es überhaupt nicht möglich ist, über einen bestimmten Typkonstruktor zu abstrahieren.

Ich bin mir nicht sicher, ob ich damit einverstanden bin, dass es nicht eindeutig wäre, wenn Sie immer die höchste Art der Einschränkung annehmen, bis Sie eine niedrigere benötigen. Dies ist keine Behauptung, und wenn es andere Beispiele gibt, die etwas anderes zeigen, dann fair genug.


Das Problem ist, dass wir, wenn wir die Dinge so einfach wie möglich halten, eine Subtyp-Beziehung erhalten, die paradox ist und nicht in die Sprache passt.

Das könnte wahr sein, ich denke, ich bin nur besorgt darüber, ob die Alternative tatsächlich implementiert werden kann. Für das, was es wert ist, wenn die komplexere Lösung funktioniert, wäre das großartig!

Der allgemeinere Begriff der Subtypisierung, der die Existenz einer Zuordnungsfunktion zeigt, scheint im Allgemeinen schwer zu implementieren zu sein. Interpretiert mein folgendes Beispiel Ihre Regeln richtig?

(b) Für jede gesetzliche Parametrisierung T₁, T₂, ... von ~ A existiert eine Parametrisierung S₁, S₂, ... von ~ B, so dass A.

Wäre X im folgenden Fall ein Subtyp von Y bei einer Abbildung von F (A, B) = (Zahl, B).

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

X<string,number> wäre jedoch kein Subtyp von Y<string,number> .

Ich denke, ich bin nicht klar, ob die Existenz eines Mappings ausreicht. Wenn wir ~ A und ~ B als Funktionen nehmen und zeigen wollen, dass ~ B ~ A approximiert oder ~ A ein Subtyp von ~ B ist, dann zeigen wir, dass es eine Funktion ~ C gibt, so dass ~ A a ist Der Subtyp von (~ B. ~ C) reicht meiner Meinung nach nicht aus (C ist der Mapper). Ich muss für alle Zuordnungen der Fall sein.

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

Ich folge diesem Beispiel nicht ganz. Sollte der Fehler hier nicht auftreten? Ich habe gelesen, dass id1 eine Eingabe haben sollte, die von der Funktion P PromiseLike für alle _Eingaben_ id2 über einen Wert spricht, der ein Subtyp für die Anwendung von Promise auf A [] sein muss. Ich bin nicht sicher, ob es möglich ist, die für id1 erforderlichen Informationen aus dem Typ id2 wiederherzustellen. Ich glaube, ich könnte Ihren Standpunkt falsch verstehen.

Diese Typen wären plötzlich unterschiedlich, wo sie derzeit gleich sind

Ich fürchte, ich könnte Ihren Standpunkt verfehlen, aber er weiß nicht, wie sie gleich sind. Ich kann es nicht ersetzen RegularNumber mit GenericNumber in einer Art, ich würde das letztere Argument gegeben haben.

Ich glaube, ich bin mir nicht sicher, ob das Vorhandensein eines Mappings ausreicht. Wenn wir ~ A und ~ B als Funktionen nehmen und zeigen wollen, dass ~ B ~ A approximiert oder ~ A ein Subtyp von ~ B ist, dann zeigen wir, dass es eine Funktion ~ C gibt, so dass ~ A a ist Der Subtyp von (~ B. ~ C) reicht meiner Meinung nach nicht aus (C ist der Mapper). Ich muss bei allen Zuordnungen der Fall sein.

Ja, Sie haben Recht, ebenso wie das Gegenbeispiel, das Sie bereitgestellt haben. Ich habe andere Gegenbeispiele gefunden. Funktioniert überhaupt nicht.

Ich habe diesen Thread und viele Ihrer Antworten noch einmal gelesen. Ich denke, Sie haben in vielen Dingen Recht und ich habe das Problem falsch gesehen. Ich komme zu dem, was ich meine.

Ich bin mir nicht sicher, ob ich damit einverstanden bin, dass es nicht eindeutig wäre, wenn Sie immer die höchste Art der Einschränkung annehmen, bis Sie eine niedrigere benötigen. Dies ist keine Behauptung, und wenn es andere Beispiele gibt, die etwas anderes zeigen, dann fair genug.

Es ist entweder mehrdeutig oder es wird unmöglich, auf etwas zu verweisen. Wie im obigen Beispiel kann der Typkonstruktor von Foo nicht mehr referenziert werden, da er vom Typ selbst ausgeblendet wird. Wenn Sie ~Foo oder Foo<*> oder ~<A>Foo<A> oder irgendetwas anderes schreiben, das nicht mit anderen Dingen in Konflikt steht, hätten Sie kein solches Problem.

Ja, Sie können dies umgehen, indem Sie einen Alias ​​definieren, obwohl dieser nicht sehr hübsch ist:

type Foo2<T> = Foo<T>

Wie gesagt, ich denke nicht, dass dies das wichtigste Anliegen ist.

Ich folge diesem Beispiel nicht ganz. Sollte der Fehler hier nicht auftreten? Ich habe gelesen, dass id1 eine Eingabe haben sollte, die von der Funktion P erstellt wurde und für alle Eingaben ein PromiseLike ergibt. Während id2 von einem Wert spricht, der ein Subtyp für die Anwendung von Promise auf A [] sein muss. Ich bin nicht sicher, ob es möglich ist, die für id1 erforderlichen Informationen vom Typ id2 wiederherzustellen. Ich glaube, ich könnte Ihren Standpunkt falsch verstehen.

Das ist die richtige Lesart, ja. Aber wenn P extends Promise<A[]> , sollte es jedem Ort zugewiesen werden können, der ein Promise<A[]> akzeptiert, wie z. B. id1 . So ist es jetzt und was Subtypisierung bedeutet.

Ich denke nicht wirklich, dass es mehr vermieden werden kann.

Ich fürchte, ich könnte Ihren Standpunkt verfehlen, aber er weiß nicht, wie sie gleich sind. Ich kann RegularNumber in einem Typ nicht durch GenericNumber ersetzen, ich müsste letzterem ein Argument geben.

Was ich damit gemeint habe ist Folgendes: Der Typ GenericNumber<T> für alle T und der Typ RegularNumber sind identisch und austauschbar. Es gibt keinen Kontext, in dem einer check eingeben würde und der andere nicht. Zumindest jetzt.

Worüber wir gesprochen haben, würde sie anders machen. Da GenericNumber<T> von einem TC stammt, wäre es an Orten möglich, an denen RegularNumber nicht sein könnte. Es wäre also nicht mehr austauschbar.

Ich habe darüber nachgedacht, und ich denke, es kann unvermeidlich und nicht unbedingt schlecht sein. Nur ein neues, anderes Verhalten.

Eine Möglichkeit, darüber nachzudenken, besteht darin, dass der Typparameter Teil der "Struktur" des Typs wird.

Ich denke, TCs werden zu einem anderen Verhalten führen.

Neue Richtung

Zunächst einmal denke ich, dass Sie Recht haben, dass die richtige Subtyp-Beziehung diejenige ist, die nicht über die Zuordnung verfügt:

Meine erste Idee war die folgende. Damit ~A ein Subtyp von ~B :

  1. (a) Sie müssen die gleiche Art haben (in Bezug auf Arität, nicht Einschränkungen).
  2. (b) Für jede rechtliche Parametrisierung T₁, T₂, ... von ~A muss A<T₁, T₂, ...> ein Subtyp von B<T₁, T₂, ...> .

Die Mapping-Sache ... ehrlich gesagt ist es ziemlich dumm. Ich glaube nicht, dass es mehr eine Möglichkeit gibt, MyPromise<T> extends Promise<T[]> und ~Promise zu vereinen. Ich würde gerne wissen, ob jemand anders denkt.

Ich würde auch gerne wissen, ob es ein Beispiel gibt, das mir fehlt, wo selbst diese Regel nicht funktioniert.

Wenn wir uns einig sind, dass Einschränkungen des Typkonstruktors mithilfe einer Subtyp-Beziehung ausgedrückt werden sollten, die anscheinend sehr gut funktioniert, können wir zu anderen Dingen übergehen.

Über die Syntax

In diesem Punkt sind wir uns anscheinend nicht wirklich einig. Ich bevorzuge dringend eine Präfixsyntax ähnlich ~Promise . Konzeptionell kann das ~ als "Verweis auf den TC von" -Operator oder so etwas angesehen werden.

Ich glaube, ich habe mehrere Gründe angegeben, warum es besser ist als Alternativen:

  1. Völlig eindeutig.

    1. Fehler, die diese Syntax betreffen, sind ebenfalls eindeutig. Wenn jemand vergisst, einen Typ zu parametrisieren, erhält er keine Fehler über Typkonstruktoren, wenn er nicht weiß, was er ist.

    2. Als Nebeneffekt müssen vorhandene Fehlertexte und Logik nicht geändert werden. Wenn jemand Promise schreibt, ist die Fehlermeldung genau dieselbe wie jetzt. Es muss sich nicht ändern, um über TCs zu sprechen.

  2. Erweitert sich gut auf eine strukturelle Syntax.
  3. Einfach zu analysieren, glaube ich. Es wird angenommen, dass alle ~\w die dort erscheinen, wo ein Typ erwartet wird, einen Verweis auf einen TC angeben.
  4. Einfach zu tippen.

Ich hoffe, dass andere Leute ihre Meinung äußern können.

Informationen zu Gewerkschaften, Schnittpunkten und Standardtypparametern

Sind überladene / gemischte Arten der Formulare * & (* => *) , * | (* => *) usw. legal? Haben sie interessante Verwendungszwecke?

Ich denke, sie sind eine schlechte Idee und schwer zu überlegen. Ich bin mir auch nicht sicher, welche Art von Typanmerkung Sie benötigen würden, um * | (* => *) damit Sie daraus einen Typ erstellen können.

Eine Möglichkeit, von der gesagt werden kann, dass solche Arten derzeit existieren, sind Typen mit Standardtypparametern:

type Example<A = number> = {}

Man kann sagen, dass dieser Typ die Art * & (* => *) da er einen Typparameter zum Erstellen eines Typs akzeptieren kann, dies aber nicht muss.

Ich glaube, dass Standardtypparameter eine Form der Kurzform sein sollten, keine Art, Typen zu beschreiben. Daher denke ich, dass die Standardtypparameter bei der Bestimmung der Art eines Typs einfach ignoriert werden sollten.

Es kann jedoch sinnvoll sein, über Typen wie ~Promise | ~Array zu sprechen. Sie haben die gleiche Art, sind also nicht inkompatibel. Ich denke, das sollte unterstützt werden.

Dinge, die erledigt werden müssen

Verwandte Situationen müssen behandelt werden, wie diese Situation:

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

Aber das betrifft nicht wirklich die Art (* => *) | (*, *) => * , sondern etwas anderes

Andere TCs bauen?

Wie ich bereits erwähnt habe, halte ich es nicht für eine gute Idee, TCs zu haben, die andere TCs konstruieren, wie * => (* => *) . Sie sind die Norm in Sprachen, die Currying und dergleichen unterstützen, jedoch nicht in TypeScript.

Es gibt keine Möglichkeit, solche Typen mithilfe der Syntax ~ und der Subtyp-Beziehung zu definieren, sodass keine speziellen Regeln erforderlich sind, um sie zu verbieten. Es würde spezielle Regeln erfordern, damit sie funktionieren.

Ich denke, Sie könnten sie wohl strukturell so definieren:

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

Das ist so ziemlich der einzige Weg, den ich denke.

Interaktion mit höherrangigen Funktionen?

Es gibt eine natürliche, aber komplexe Interaktion mit Funktionstypen, die Typparameter verwenden:

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

Sollte diese Interaktion irgendwie gestoppt werden? Ich kann sehen, dass solche Typen sehr kompliziert werden.

Gibt es Orte, an denen TC-Parameter im Allgemeinen nicht angezeigt werden sollten?

Ist meine strukturelle Syntax eine gute Idee?

Ich denke nicht, dass es sofort implementiert werden sollte, aber ich denke immer noch, dass meine strukturelle Syntax eine gute Idee ist. Es lässt Sie:

  1. Verwenden Sie lokale Bezeichner wie andere Typparameter in Ihren Einschränkungen.
  2. Wenden Sie Typkonstruktoren teilweise sehr explizit und flexibel an: ~<A>Map<string, A> , ~<A, B>Map<B, A> und so weiter.
  3. Jeder andere Aspekt eines Typs hat eine strukturelle Syntax, daher sollten TCs auch eine solche Syntax haben.

Das heißt, TCs können ohne dies völlig funktionieren, und die erste PR würde sie wahrscheinlich nicht einbeziehen.

Interaktion mit bedingten Typen

Wie funktioniert die Funktion mit bedingten Typen? Sollten Sie dazu in der Lage sein?

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

Ich bin mir selbst nicht ganz sicher. Bedingte Typen sind noch nicht vollständig verdaut.

Überlastungsauflösung

Ich habe das Gefühl, dass dies schwierig sein wird. Dies ist eigentlich ziemlich wichtig, da unterschiedliche Formen der Überlastungsauflösung unterschiedliche Typen erzeugen würden.

Trotzdem kann ich mir derzeit keine guten Beispiele einfallen lassen.

Sie wissen, ein Großteil davon wäre ein strittiger Punkt gewesen, wenn eine genau definierte Zwischensprache verwendet worden wäre, um TypeScript als Ausgangspunkt zu beschreiben. Zum Beispiel: System F <: oder eines der schallabhängigen Typsysteme wie Simplified Dependent ML .

Ich wäre ehrlich überrascht, wenn dies vorher gelöst würde
https://github.com/Microsoft/TypeScript/issues/14833

Ich denke, # 17961 könnte dies wahrscheinlich indirekt lösen. Weitere Informationen finden Sie in

Beachten Sie, dass die Typen Bifunctor und Profunctor auf der Ebene der Einschränkungen etwas komplex sind - es wäre viel einfacher, wenn ich offensichtliche universelle Typen hätte, mit denen ich arbeiten könnte, als die Typen infer T das ist rein auf bedingte Typen beschränkt. Es wäre auch schön, wenn ich this als "return" -Typ hätte verwenden können (das ist reine Typenebene) - das hätte die Definition meiner meisten Schnittstellen einfacher gemacht.

(Ich bin kein schwerer TS-Benutzer, daher habe ich möglicherweise Fehler gemacht. @ Tycho01 Könnten Sie sich das ansehen, um zu sehen, ob ich irgendwo im Typ-Chaos Fehler gemacht habe? Der Grund, den ich frage, ist, dass Sie derjenige sind, der dahinter steht die obige PR, und ich habe einige Ihrer anderen Experimente und Dienstprogramme gesehen.)

@isiahmeadows @ tycho01 Wow ...

Du hast recht. Wenn ich es richtig verstehe, hat es fast die gleichen Ergebnisse.

Es gibt einige Unterschiede. Aber funktional sind sie ziemlich identisch, und ich denke, diese Unterschiede können gelöst werden.

Es ist nicht möglich, auf die richtige Typfunktion zu schließen

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

Hier können Sie ~Promise und ~Bluebird aus p ableiten. Wenn Sie es jedoch so machen:

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

Ich bezweifle sehr, dass dies funktionieren würde:

example(null as Promise<number>)

Es gibt keine Möglichkeit zu schließen, dass F soll:

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

Weil diese Funktion in keiner Weise als speziell angesehen wird. Während bei TCs einige Typen im Wesentlichen eine "implizite" Funktion auf Typebene haben: ihre TC.

Es ist nicht möglich, vorhandene TCs einfach zu referenzieren

Sie können nicht ~Promise wie in meinem Vorschlag tun. Sie müssten den Typ direkt in einem Strukturformat codieren:

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

Stimmt, und das ist ein Problem. Dies ist eher ein Typinferenzproblem, bei dem Sie die Fähigkeit benötigen, die generische Funktion selbst aus einem bekannten Argumenttyp abzuleiten (das Gegenteil von dem, was normalerweise passiert). Es ist allgemein genug lösbar, um in den meisten Fällen zu funktionieren, erfordert jedoch einen neuen Sonderfall, der nicht trivial ist.

Es könnte teilweise durch strategische Verwendung von NoInfer<T> lösbar sein, aber ich bin nicht 100% sicher, wie dies getan werden müsste und wie viel es selbst im allgemeinen Fall angehen könnte.

@ GregRos

Ich bin nicht stark für eine Syntax, es ist eher meine Präferenz, es gibt viele Vorteile für ~ . Ich denke, die Hauptsache wäre, ob Syntax für explizite Annotationen erforderlich ist, da Inferenz nicht immer möglich ist.


Die Mapping-Sache ... ehrlich gesagt ist es ziemlich dumm. Ich glaube nicht, dass es eine Möglichkeit gibt, MyPromise zu vereinheitlichenerweitert Versprechen

Ich denke, das Mapping-Ding könnte immer noch ein nützlicher Begriff sein, aber im obigen Fall denke ich nicht, dass wir jemals versuchen sollten, ~MyPromise mit ~Promise zu vereinen, das Ding, das vereinheitlicht werden muss ist ~MyPromise und ~<T>Promise<T[]> , die wir auch schreiben könnten ~(Promise . []) . Ich denke, das, was fehlte, war, dass das Mapping Teil der Subtypisierungsrelation sein muss: Es ist genauso Teil des Konstruktors wie Promise . In diesem Beispiel ist das Mapping nur der Listenkonstruktor.

interface A<T> {
    x: T;
} 

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

Erweitert ~<T>B<T> ~<T>A<T[]> ? Ja. Erweitert ~<T>B<T> ~<T>A<T> ? Nein, aber letztendlich sind es zwei nicht miteinander verbundene Fragen.

Wenn wir uns einig sind, dass Einschränkungen des Typkonstruktors mithilfe einer Subtyp-Beziehung ausgedrückt werden sollten, die anscheinend sehr gut funktioniert, können wir zu anderen Dingen übergehen.

Ja, ich denke, es scheint eine gute Art zu sein, Dinge zu beschreiben.


Es ist nicht möglich, auf die richtige Typfunktion zu schließen

function example<~P extends ~PromiseLike>(p : P<number>) : P<string>;
Hier können Sie ~Bluebird p ~Promise und ~Bluebird aus p ableiten.

Dies ist keine Behauptung, sondern eine offene Frage, da ich nicht ganz sicher bin, wie die Typprüfung funktioniert. Ich dachte, dass am Beispiel der obigen Schnittstelle A Typen A<number> und {x: number} nicht zu unterscheiden sind, und daher bin ich mir nicht sicher, ob es möglich wäre, auf den Konstruktor zu schließen vom Typ, der von einer Anwendung des Konstruktors zurückgegeben wurde. Wäre es möglich, P von P<number> ? Ich bin mir sicher, dass die Dinge geändert werden könnten, um dies zu unterstützen. Ich frage mich nur, was es jetzt tut.

Gegenreaktion von # 17961, aber ich bin mir leider nicht sicher, wie ich den Ansatz von Laufen bringen soll . Ich befürchte, dass die Rückwärtsinferenz bei Typaufrufen nicht trivial ist.

Es sieht also so aus, als ob wir basierend auf einer Eingabe Promise<number> oder Bluebird<number> in der Lage sein möchten, nicht angewendete Versionen dieser Typen so abzuleiten, dass wir sie erneut mit z. B. string anwenden können. Das klingt allerdings hart.

Selbst wenn Eingabetypen wie diese anstelle eines strukturellen Äquivalents sind (wir sind eine strukturell typisierte Sprache, oder?), Wird diese Argumentation auch trübe, wenn z. B. Bluebird stattdessen zwei Parametertypen hatte, an welchem ​​Punkt unser <string> Parameteranwendung vom Typ
Ich bin mir dort nicht sicher, ob es eine gute Lösung gibt. (Haftungsausschluss: Ich bin bei der Diskussion hier etwas zurückgeblieben.)

@ tycho01 Würden all diese Probleme T instanziieren würden?

Ich halte das für vernünftig, da ich bezweifle, dass Schlussfolgerungen ohnehin für alle Fälle gelöst werden können.

@ jack-williams: bisher nicht mit # 17961, aber ich denke, es für den Versand zu verwenden könnte helfen:

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 Ja, ich habe festgestellt, dass mein Vorschlag schrecklich war, weil T bei den Methodenaufrufen nicht instanziiert wird.

Würde so etwas wie folgendes funktionieren?

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 Ich denke, die Frage sollte sein, wie es sich im Verhalten mit der ADT-Implementierung in fp-ts , aber das sieht so aus, als könnte es funktionieren, ja. Wahrscheinlich auch ohne die TyCon .

@ jack-williams @isiahmeadows :

Ich habe die Idee bei try flow ausprobiert , da bereits $Call verfügbar ist. Für mich scheint es aber irgendwie nicht mehr zu reagieren ...

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 Ich denke, Sie können nicht einfach Eigenschaften mit $ElementType aus this im Fluss erhalten

@ tycho01 Sie können dies tatsächlich nicht auch in Typoskript zum
Spielplatz: https://goo.gl/tMBKyJ

@goodmind : hm, sieht so aus, als würde es Maybe<number> anstelle von Functor<number> nachdem über fmap von Functor auf Maybe kopiert wurde.
Bei Typaufrufen würde sich das verbessern, wenn nur der Typ vorhanden wäre, anstatt die Laufzeitimplementierung für den Typ zu benötigen.
Jetzt würden Funktoren bereits ihre eigenen fmap Implementierungen benötigen. Das wäre allerdings für die abgeleiteten Methoden scheiße.
Zurück zum ersten Platz. : /

Einige relevante Ideen finden Sie unter https://github.com/SimonMeskens/TypeProps/issues/1.

Ich plane, so schnell wie möglich eine Alpha-Version zu veröffentlichen, aber Sie können mir folgen und die Beispiele in dieser Ausgabe schreiben, um bereits ein Gefühl dafür zu bekommen.

Dieses spezielle Problem ist etwas langwierig, aber was ich suche, ist einfach enthalten, aber echte Beispiele für Code, den Sie aufgrund fehlender parametrisierter generischer Typen nicht eingeben können. Ich denke, ich kann die meisten von ihnen eingeben (vorausgesetzt, sie verlassen sich nicht auf abstrakte angehobene Typkonstruktoren). Fühlen Sie sich frei, Probleme im obigen Repo mit Code zu öffnen, und ich werde sie für Sie eingeben, wenn ich kann (oder Sie können sie auch hier posten).

Heads up Ich habe versucht, dies unter # 23809 zu implementieren. Es ist immer noch sehr unvollständig, aber probieren Sie es aus, wenn Sie interessiert sind.

Ich habe euch ein einfaches Beispiel versprochen, hier ist es. Dies verwendet einige Tricks, die ich beim Schreiben meiner Bibliothek gelernt habe.

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"];

Ich habe das Beispiel aktualisiert und vereinfacht. Hier ist auch ein Spielplatz-Link:
Spielplatz

Ich habe eine NPM-Bibliothek für die oben genannten hinzugefügt, damit Sie einfacher damit arbeiten können. Derzeit in Alpha, bis ich die richtigen Tests in bekomme, sollte euch aber helfen, HKTs zu schreiben.

Github Link

Ich habe mit einem einfachen Ansatz zur Simulation von HKTs gespielt, indem ich bedingte Typen verwendet habe, um virtuelle Typvariablen innerhalb eines gesättigten Typs zu ersetzen:

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

Projekt hier: https://github.com/pelotom/hkts

Feedback willkommen!

@pelotom Ich mag die Leichtigkeit der Syntax Ihres Ansatzes. Es gibt zwei andere Ansätze, die in diesem Thread noch nicht erwähnt wurden und die Kreativität bei der Herstellung aktueller und zukünftiger Lösungen anregen könnten. Beide sind objektorientierte Lösungen für dieses Problem.

  1. Bertrand Meyer beschrieb in seinem 1988 erschienenen Buch "Object-Oriented Software Construction" eine Möglichkeit, generische Typen zu simulieren.

Die Beispiele sind in Eiffel, aber eine grobe Übersetzung in TypeScript sieht folgendermaßen aus:

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

Sie werden feststellen, dass sie aufgrund der Notwendigkeit von Darstellungen für Zwischenklassen ziemlich schwer werden können, aber mit einer Form der Klassenfabrik oder mit dem TypeScript Mixin-Ansatz könnte dies erheblich reduziert werden.

Möglicherweise ist # 17588 anwendbar

  1. Der zweite Ansatz wird bei der Simulation von Objektalgebren (und abstrakten Fabriken) verwendet:

C<T> wird durch App<t,T> wobei T die Klasse ist und t ein eindeutiges Tag ist, das mit C verknüpft ist

interface App<C,T> {}

Stichprobe:

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

Weitere Einzelheiten und Begründungen finden Sie im folgenden Dokument unter Abschnitt 3.5 "Emulieren des Typ-Konstruktor-Polymorphismus":

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

@metaweta , können Sie dieses Problem in Higher kinded types in TypeScript umbenennen, damit die Google-Suche eine bessere Sichtbarkeit bietet?

Vielleicht könnten unsere weisen und wohlwollenden Repository-Betreuer (z. B. @RyanCavanaugh , @DanielRosenwasser ) den Titel bearbeiten, wenn eine solche Änderung als ihrer Intervention würdig erachtet wird?

Ich bin gespannt, was es bedeutet, dass dies von der Community in den Rückstand verschoben wurde. Ist dies etwas, worüber das Kernteam jetzt ernsthafter nachdenkt, oder bedeutet dies einfach, dass das Team entschieden hat, dass dies kein guter Community-Kandidat ist?

Gefunden: Der Meilenstein "Community" ist offenbar zugunsten des Meilensteins "Backlog" veraltet , weshalb dieses Problem wahrscheinlich in Form von Sachleistungen migriert wurde.

Kein TS-Mitglied, nur jemand, der beschlossen hat, auf den Link zu klicken, auf dem er erneut Meilenstein gesetzt wurde.

+1

Hier ist etwas, das ich nur bauen wollte und das für höherwertige Typen wie ein wirklich praktischer Fall erscheint.

Ich möchte eine Abstraktion für eine Datenbank erstellen, die entweder synchron oder asynchron ausgeführt werden kann. Anstatt Rückrufe zu verwenden und mich darum zu kümmern, wollte ich ein Generikum verwenden. Folgendes möchte ich tun:

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

Das wäre wirklich mächtig!

@ccorcos das heißt MTL-Stil. Unter https://github.com/gcanti/fp-ts/blob/master/tutorials/mtl.ts finden Sie ein reines Funktionsbeispiel mit fp-ts .

@mlegenhausen Es tut mir leid, aber es fällt mir schwer, diesem Beispiel zu folgen.

Immer wenn ich mich mit fp-ts , mache ich mir Sorgen, dass die Dinge so kompliziert werden, dass sie brüchig werden. Das Beispiel von

Gibt es einen Grund, warum dies nicht in TypeScript übernommen wird?

@ccorcos IMHO selbst wenn ich das Beispiel von fp-ts empfehlen würde, würde ich MTL / tagless style überhaupt nicht empfehlen. Sie fügen jeder effektiven Monade, die Sie manuell verwalten müssen, eine zusätzliche Abstraktionsebene hinzu, da Typoskript nicht erkennen kann, welche Monade Sie verwenden möchten, und hier wird es kompliziert. Was ich von der fp-ts Community sehe, ist, eine asynchrone Monade zu verwenden (ich würde TaskEither empfehlen) und dabei zu bleiben. Selbst beim Testen sind die Vorteile von MTL den Aufwand, den Sie in Ihrem nicht testenden Code erhalten, nicht wert. Hyper-ts basierend auf fp-ts ist ein Beispiel für eine Bibliothek, die kürzlich die Unterstützung für MTL eingestellt hat.

Interessant ... hyper-ts sieht wirklich cool aus ...

Ich habe eine leichtgewichtige Codierung höherer Art entwickelt, die auf F-gebundenem Polymorphismus basiert: https://github.com/strax/tshkt

Der Vorteil dieses Ansatzes besteht darin, dass Sie keine Nachschlagetabelle (einen einzelnen bedingten Typ oder ein Objekt mit Zeichenfolgenschlüsseln) benötigen, um Typen Konstruktoren zuzuordnen. Die Technik kann auch verwendet werden, um die Anwendung generischer Funktionen auf Typebene zu codieren (denken Sie an ReturnType<<T>(value: T) => Array<T>> ).

Es ist immer noch ein Proof of Concept, daher wird Feedback zur Realisierbarkeit dieses Ansatzes sehr geschätzt!

Ich werde mir das @strax ansehen , das sieht wirklich cool aus!

In der Zwischenzeit ist hier ein dummes Beispiel dafür, was wir jetzt tun können:

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"];

Ich würde gerne De Bruijn-Indizes hinzufügen, da dies bedeuten würde, dass wir die Schnittstellen nicht mehr benötigen, aber es würde einige Tupel-Berechnungen erfordern, denke ich, und ich versuche, dies zu vermeiden.

Vorschlag

Höhere Ordnung, Lamda, Referenztypen

Übergeben Sie einen Typ als Referenz

Einfach ausgedrückt, ein Referenztyp oder ein höherer Ordnung würde es einem ermöglichen, die von einem Typ übernommenen Parameter für später aufzuschieben oder anschließend sogar Typparameter (Generika) abzuleiten. Aber warum sollten wir uns darum kümmern?

Wenn wir einen Typ als Referenz übergeben können, bedeutet dies, dass wir TypeScript bei der Auswertung eines Typs verzögern können, bis wir uns dazu entschließen. Schauen wir uns ein Beispiel aus der Praxis an:

Vorschau mit Pipe

Stellen Sie sich vor, Sie entwickeln einen generischen Typ für pipe . Bei den meisten Arbeiten geht es darum, zu überprüfen, ob die zu leitenden Funktionen tatsächlich leitbar sind. Andernfalls würden wir dem Benutzer einen Fehler melden. Zu diesem Zweck verwenden wir einen zugeordneten Typ, um die Funktionstypen wie pipe(...) leiten:

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

Jetzt müssen wir dies nur noch über die Funktionen mit einem zugeordneten Typ wiederholen:

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

( siehe die vollständige Implementierung )

Jetzt können wir Funktionen zusammenführen und TypeScript kann uns Warnungen geben:

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
)

Es klappt! Wir haben einen richtigen Fehler:

Das Argument vom Typ '(message: object) => boolean' kann dem Parameter vom Typ '(arg: string) => boolean' nicht zugewiesen werden.

Das Problem

Aber es gibt ein Problem. Während dies für einfache Operationen sehr gut funktioniert, werden Sie feststellen, dass es vollständig fehlschlägt, wenn Sie Generika (Vorlagen) für die Funktionen verwenden, die Sie an sie übergeben:

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

In beiden Fällen verlor TypeScript den Überblick über die Funktionstypen.
> Hier kommen Typen höherer Ordnung ins Spiel <

Syntax

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

Kurz gesagt, wir haben die Generika manuell und dynamisch mit * . Tatsächlich hat die Verwendung von * die Bewertung der Generika verschoben. Das * hat also je nach Kontext unterschiedliche Verhaltensweisen. Wenn das * ein Typ ist, der:

  • kann Generika empfangen : Es übernimmt seine Generika / erhält seine Referenz
  • ist selbst ein Generikum : Es verschiebt die Bewertung, bis die Referenz bekannt ist. Zu diesem Zweck wird ein Referenzbaum von den übergeordneten Elementen abwärts erstellt. Mit anderen Worten, Aktualisierungen können verschachtelt werden. Dies ist genau das, was oben mit Return<*(Fns[Pos<Prev<IterationOf<K & string>>>])> passiert, das T zugewiesen wurde. In diesem Zusammenhang können wir sagen, dass * vor sofortiger Bewertung "schützt".
  • ist keines der oben genannten : Es tut nichts, löst sich auf den gleichen Typ
type  piped  =  Piper<[
    (a:  string) =>  string,
    <B>(b:  B) =>  B
    <C>(c:  C) =>  C
]>
// [
//     (a:  string) =>  string,
//     (b:  string) =>  string,
//     (c:  string) =>  string
// ]

Daher sollte TypeScript die Evaluierung nur starten / fortsetzen, wenn das Generikum bereitgestellt wurde, und die Evaluierung bei Bedarf blockieren (unvollständiges Generikum). Im Moment wertet TS auf einmal aus, indem Generika in unknown -Typen umgewandelt werden. Wenn mit diesem Vorschlag etwas nicht gelöst werden kann:

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

Einzelheiten

Das * ruft einen Verweis auf einen Typ ab und ermöglicht so Manipulationen an seinen Generika. Wenn Sie also den Platzhalter vor einem Typ platzieren, wird ein Verweis darauf abgerufen:

*[type]

Das Abrufen eines Verweises auf einen Typ ermöglicht automatisch die Manipulation von Generika:

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

Die Generika werden nur dann vom Zieltyp konsumiert / festgelegt, wenn dies möglich ist. Also mach das:

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

Es kann aber auch von TypeScript selbst überprüft werden, ob eine Warnung angezeigt werden soll oder nicht. Aber intern sollte TS nichts dagegen tun.

Ich dachte auch, dass es eine gute Idee ist, * da es einen Zeiger auf etwas symbolisieren kann (wie in C / C ++ - Sprachen) und nicht von TypeScript ausgeliehen wird.

Typen höherer Ordnung

Nachdem wir gesehen haben, wie es in seiner grundlegendsten Form funktioniert, möchte ich das Kernkonzept vorstellen: Lambda-Typen . Es wäre schön, anonyme Typen zu haben, ähnlich wie Rückrufe, Lambdas und Referenzen in JavaScript .

Das obige Beispiel zeigt, wie die Generika einer Funktion übernommen werden. Da es sich jedoch um Referenzen handelt, kann jeder Typ in Verbindung mit * . Einfach ausgedrückt, eine Typreferenz ist ein Typ, den wir weitergeben können, der jedoch noch keine Generika erhalten hat:

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

Höhere Arten

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

Suchbegriffe

höhere #order #type #references #lambda #HKTs

@ pirix-gh Wenn Sie nur ein paar Nachrichten lesen, können Sie sehen, dass vieles, wonach Sie fragen, entweder bereits möglich ist oder bereits angefragt wurde.

Ich habe sie gelesen und dachte, ich könnte meine Ideen wie alle anderen zusammenfassen (für eine All-in-One-Lösung), hauptsächlich über die Syntax.

Ich habe den obigen Vorschlag bearbeitet, um besser zu erklären, wie wir Referenzen verketten können, und die Art und Weise festgelegt, wie ein Typ wie Pipe damit funktioniert (es gab einige Fehler in Bezug auf die Logik).

Irgendein Update?

Immer noch kein Update? Meiner Meinung nach ist dieses Problem das größte Hindernis, damit TypeScript sein volles Potenzial entfalten kann. Es gibt so viele Fälle, in denen ich versuche, meine Bibliotheken richtig einzugeben, um nach einem langen Kampf aufzugeben und festzustellen, dass ich wieder auf diese Einschränkung gestoßen bin. Es ist allgegenwärtig und zeigt sich auch in scheinbar sehr einfachen Szenarien. Hoffe wirklich, dass es bald behoben wird.

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

Es muss noch viel Arbeit geleistet werden, wie z. B.: Quickinfo korrigieren, TypParameter ableiten, Fehlermeldung hinzufügen, gleichen TypeConstructor markieren .....
Aber es fängt an zu funktionieren, und hier ist, was ich jetzt bekommen könnte.
Die Beispielschnittstelle stammt von @millsp https://github.com/microsoft/TypeScript/issues/1213#issuecomment -523245130. Die Schlussfolgerung ist wirklich sehr hilfreich, großartig, dank dessen.

Ich hoffe, dass Communicity mehr Benutzerfälle wie diese bereitstellen kann, um zu überprüfen, ob der aktuelle Weg für die meisten Situationen funktioniert.

Es wäre auch süß, einige Informationen über HKT / Funktionsprogrammierung / Lambda bereitzustellen (wenn ich lambda sage, meine ich die Mathematik, ich konnte nur ein Beispiel finden, das von einer Sprache ohne Mathematik geschrieben wurde)

Hier sind Dinge, die mir sehr helfen:

@ShuiRuTian In Bezug auf m.join(q) , das Set<unknown> zurückgibt , --noImplicitAny eine Warnung ausgibt ?

Ich hoffe, dass die Community weitere derartige Benutzerfälle bereitstellen kann, um zu überprüfen, ob der aktuelle Weg für die meisten Situationen funktioniert.

Es wäre auch süß, einige Informationen über HKT / Funktionsprogrammierung / Lambda bereitzustellen (wenn ich lambda sage, meine ich die Mathematik, ich konnte nur ein Beispiel finden, das von einer Sprache ohne Mathematik geschrieben wurde)

Ohne weiter zu gehen, habe ich kürzlich versucht, eine generische Curry-Funktion filter zu erstellen, und ich wollte, dass sie ungefähr so ​​funktioniert:

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

Und ich habe keine Möglichkeit, dies tatsächlich zu tun, da ich TypeScript mitteilen muss, dass "derselbe Typ zurückgegeben wird, den Sie erhalten haben, aber mit diesem anderen Parameter". Etwas wie das:

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

@lukeshiru Ja, im Wesentlichen ist dies https://pursuit.purescript.org/packages/purescript-filterable/2.0.1/docs/Data.Filterable#v : filter
Es gibt viele ähnliche Anwendungsfälle für HKT in TypeScript.

@isiahmeadows Ich habe es versucht. Du hast recht.
@lukeshiru und @raveclassic Danke! Diese Funktion scheint ziemlich vernünftig zu sein. Ich würde mir das ansehen, nachdem ich https://gcanti.github.io/fp-ts/learning-resources/ gelesen habe.

Ich stecke fest und weiß nicht, was die aktuelle " Problemumgehung" ist ...
Ich versuche, die Kettenspezifikation zu implementieren:

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

Ein Wert, der die Kettenspezifikation implementiert, muss auch die Apply-Spezifikation implementieren.

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

Ich habe ein FunctorSimplex das dann um ein FunctorComplex dann um das Apply aber jetzt, wo ich Apply als Chain verlängern möchte

Also brauche ich das (Bild unten und Link zum Code):

(Ich muss einen Typ an ApType damit in Zeile 12 das Apply nicht «fest codiert» ist, sondern generisch ... um auch alle Typen zu verwenden, die von IApply )

Screenshot

Permalink zu den Code-Snippet- Zeilen 11 bis 22 in 7ff8b9c

`` `Typoskript
Exporttyp ApType = ( ap: Übernehmen <(val: A) => B>, ) => IApply ;

/ * [...] * /

Exportschnittstelle IApply erweitert FunctorComplex {
/ ** Fantasy-land/ap :: Apply f => f a ~> f (a -> b) -> f b * /
ap: ApType ;
}}
`` `

In einer Stapelüberlauffrage referenziert :

@Luxcium Bis TS Unterstützung für höherwertige Typen bietet, ist nur eine Emulation dieser Typen möglich. Vielleicht möchten Sie dort einen Blick darauf werfen, wie dies erreicht werden kann:

Bis TS Unterstützung für höherwertige Typen hat, ist nur eine Emulation von diesen möglich. Vielleicht möchten Sie dort einen Blick darauf werfen, wie dies erreicht werden kann

Tanks viel @kapke Ich bin wahrscheinlich zu sehr in FP-Tagen und da man in Javascript eine Funktion von einer Funktion zurückgeben kann, können wir pseudoFnAdd(15)(27) // 42 schreiben. Ich möchte mit TypeScript in der Lage sein, pseudoType<someClassOrConstructor><number> // unknown zu schreiben

Diese Informationen und Vorträge (Lesungen) werden sehr geschätzt ...

Hinweis: Ich spreche Französisch, auf Französisch hat das Wort _lecture (s) _ die Bedeutung von _readings_ und nicht "ein wütendes oder ernstes Gespräch, das jemandem gegeben wird, um sein Verhalten zu kritisieren" ...

Wahrscheinlich funktioniert das Folgende, das ich mir als einfache Problemumgehung ohne PRs ausgedacht habe, nicht in allen Fällen, aber ich denke, es ist erwähnenswert:

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[]
}
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

manekinekko picture manekinekko  ·  3Kommentare

siddjain picture siddjain  ·  3Kommentare

kyasbal-1994 picture kyasbal-1994  ·  3Kommentare

bgrieder picture bgrieder  ·  3Kommentare

jbondc picture jbondc  ·  3Kommentare
bleepcoder.com verwendet öffentlich lizenzierte GitHub-Informationen, um Entwicklern auf der ganzen Welt Lösungen für ihre Probleme anzubieten. Wir sind weder mit GitHub, Inc. noch mit anderen Entwicklern affiliiert, die GitHub für ihre Projekte verwenden. Wir hosten keine der Videos oder Bilder auf unseren Servern. Alle Rechte gehören ihren jeweiligen Eigentümern.
Quelle für diese Seite: Quelle

Beliebte Programmiersprachen
Beliebte GitHub Projekte
Mehr GitHub Projekte

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