Typescript: Permitir que las clases sean paramétricas en otras clases paramétricas

Creado en 19 nov. 2014  ·  140Comentarios  ·  Fuente: microsoft/TypeScript

Esta es una propuesta para permitir genéricos como parámetros de tipo. Actualmente es posible escribir ejemplos específicos de mónadas, pero para escribir la interfaz que satisfagan todas las mónadas, propongo escribir

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

De manera similar, es posible escribir ejemplos específicos de functores cartesianos, pero para escribir la interfaz que satisfacen todos los functores cartesianos, propongo escribir

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

Los parámetros de tipo paramétrico pueden tomar cualquier número de argumentos:

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

Es decir, cuando un parámetro de tipo va seguido de una tilde y una aridad natural, se debe permitir que el parámetro de tipo se utilice como un tipo genérico con la aridad dada en el resto de la declaración.

Al igual que ahora, al implementar dicha interfaz, los parámetros de tipo genérico deben completarse:

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

Además de permitir directamente composiciones de tipos genéricos en los argumentos, propongo que typedefs también sea compatible con la definición de genéricos de esta manera (ver el número 308 ):

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

Las aridades de la definición y el alias deben coincidir para que el typedef sea válido.

Suggestion help wanted

Comentario más útil

con la mentalidad de HKT se puede cambiar, los hábitos se rompen, las generaciones perdidas pueden revivir, sería lo más importante desde los genéricos y los nulos e indefinidos explícitos, puede cambiar todo

por favor considere como una próxima gran característica, parada de escuchar a las personas que te siguen pidiendo una mejor caballo, les dará af * g Ferrari

Todos 140 comentarios

No quiero hacer suposiciones apresuradas, pero creo que lo está escribiendo incorrectamente. Todos los tipos de parámetros requieren nombres de parámetros, por lo que probablemente quiso escribir

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

mientras que en este momento map es una función que toma un asignador del tipo any (donde el nombre del parámetro es A ) a B .

Intente usar la bandera --noImplicitAny para obtener mejores resultados.

Gracias, corregido.

Actualicé mi comentario en una propuesta.

: +1: un tipo de kinded superior sería una gran ventaja para la construcción de programación funcional, sin embargo, antes de eso, preferiría tener el soporte correcto para funciones de orden superior y genéricas: p

Cuasi aprobado.

Nos gusta mucho esta idea, pero necesitamos una implementación funcional para intentar comprender todas las implicaciones y los posibles casos extremos. Tener un PR de muestra que al menos aborde el 80% de los casos de uso de esto sería un próximo paso realmente útil.

¿Cuáles son las opiniones de la gente sobre la sintaxis de tilde? Una alternativa a T~2 sería algo como

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

que permite la composición directa de genéricos en lugar de necesitar alias de tipo:

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 extraño tener aridad explícita ya que realmente no hacemos eso en ningún otro lugar, así que

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

es un poco más claro, sin embargo, sé que otros idiomas usan * en contextos similares en lugar de ~ :

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

Aunque lleve ese punto al extremo, podría obtener:

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

Creo que T<~,~> es más claro que T~2 . Modificaré la propuesta anterior. No me importa si usamos ~ o * ; simplemente no puede ser un identificador JS, por lo que no podemos usar, digamos, _ . No veo qué beneficio proporciona la notación => ; todos los genéricos toman algunos tipos de entrada y devuelven un solo tipo de salida.

Una sintaxis más ligera dejaría por completo la aridez de los genéricos; el analizador lo resolvería desde el primer uso y arrojaría un error si el resto no fuera coherente con él.

Me encantaría empezar a trabajar en la implementación de esta función. ¿Cuál es el foro recomendado para molestar a los desarrolladores sobre los detalles de implementación del transpilador?

Puede registrar muchos problemas nuevos para preguntas más importantes con ejemplos de código más complicados o crear un problema de larga duración con una serie de preguntas sobre la marcha. Alternativamente, puede unirse a la sala de chat aquí https://gitter.im/Microsoft/TypeScript y podemos hablar allí.

@metaweta alguna noticia? Si necesita ayuda / discusión, estaría encantado de hacer una lluvia de ideas sobre este tema. Realmente quiero esta función.

No, las cosas en el trabajo se apoderaron del tiempo libre que tenía para trabajar en ellas.

bump: ¿existe la posibilidad de ver esta característica alguna vez considerada?

https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288 sigue siendo su estado actual. No veo nada aquí que nos haga cambiar la prioridad de la función.

Me parece que esto es útil en muchas más situaciones además de importar abstracciones de la teoría de categorías. Por ejemplo, sería útil poder escribir fábricas de módulos que tomen una implementación (constructor) Promise como argumento, por ejemplo, una base de datos con una implementación de promesa conectable:

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

: +1:

con la mentalidad de HKT se puede cambiar, los hábitos se rompen, las generaciones perdidas pueden revivir, sería lo más importante desde los genéricos y los nulos e indefinidos explícitos, puede cambiar todo

por favor considere como una próxima gran característica, parada de escuchar a las personas que te siguen pidiendo una mejor caballo, les dará af * g Ferrari

Sí, salté a esto los primeros 15 minutos después de intentar agregar tipos a la base de código JS existente. No voy a cambiar a TS hasta que lo vea.

¿Puedo ayudar, en realidad?

Me pregunto cómo se relacionaría esto con el número 7848. Son muy similares, aunque en la otra faceta de los tipos de orden superior.

@ boris-marinov La respuesta de Ryan Cavanaugh dice que puedes:

Tener un PR de muestra que al menos aborde el 80% de los casos de uso de esto sería un próximo paso realmente útil.

Ahora tengo tiempo para implementar un PR tan simple. Espero obtener algunas sugerencias de los desarrolladores principales, pero hasta ahora no hay preguntas, todo se ve bien y es comprensible. Seguirá un progreso aquí.

@Artazor ¿Te gustaría echarle un vistazo al craqueo # 7848 también? Eso soluciona el otro lado de este problema, que involucra genéricos, y en mi humilde opinión, esto se sentiría incompleto sin él (los parámetros genéricos realmente simplificarían una gran cantidad de código a nivel de tipo).

Creo que esta propuesta es absolutamente maravillosa. Tener tipos de kinded más altos en TypeScript lo llevaría a un nuevo nivel en el que podríamos describir abstracciones más poderosas de lo que es posible actualmente.

Sin embargo, ¿no hay algo de malo en los ejemplos dados en OP? El A en la línea

class ArrayMonad<A> implements Monad<Array> {

no se usa en ninguno de los métodos, ya que todos tienen su propio A genérico.

Además, si se implementa el functor con map como método que usa this ¿cómo se vería? ¿Tal vez así?

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

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

@paldepind Consulte el número 7848. Esa discusión es sobre ese caso de uso particular, aunque en mi humilde opinión, esto y aquello realmente necesita fusionarse en un solo PR.

¿Cuándo aterrizará este material? Eso parece algo esencial.

También hará posible lo siguiente:

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

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

?

@whitecolor Creo que hay peces más grandes para freír en este momento, que merecen una mayor prioridad:

  1. TypeScript 2.0 RC se lanzó hace poco menos de 2 semanas. Eso tomará mucho tiempo en sí mismo.
  2. bind , call y apply , funciones nativas de JS, no tienen tipo. En realidad, esto depende de la propuesta de genéricos variadic . Object.assign también necesita una solución similar, pero los genéricos variadic por sí solos no resolverán eso.
  3. Funciones como los métodos _.pluck Lodash, los métodos get y set modelos Backbone, etc., no están tipificados actualmente , y corregir esto básicamente hace que Backbone se pueda usar con TypeScript de una manera mucho más segura. También puede tener implicaciones para React en el futuro .

No es que no quiera esta función (me encantaría tenerla), simplemente no creo que sea probable que llegue pronto.

@isiahmeadows
Gracias por la explicación. Sí, el tercer elemento de la lista es muy importante, esperando https://github.com/Microsoft/TypeScript/issues/1295 también.

Pero espero que el número actual quizás esté en 2.1dev de alguna manera.

Estoy de acuerdo. Ojalá pueda hacerlo.

(El polimorfismo de rango 2, que este número quiere, también es una necesidad para
Usuarios de Fantasy Land, para escribir correctamente los diversos ADT dentro de esa especificación.
Ramda es un buen ejemplo de una biblioteca que necesita esto con bastante urgencia).

El martes 6 de septiembre de 2016 a las 11:00, Alex [email protected] escribió:

@isiahmeadows https://github.com/isiahmeadows
Gracias por la explicación. Sí, el tercer elemento de la lista es muy importante,
esperando # 1295 https://github.com/Microsoft/TypeScript/issues/1295
también.

Pero espero que el número actual quizás esté en 2.1dev de alguna manera.

-
Estás recibiendo esto porque te mencionaron.

Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -244978475,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AERrBMvxBALBe0aaLOp03vEvEyokvxpyks5qnX_8gaJpZM4C99VY
.

Parece que esta característica nos ayudaría mucho a definir formas de reacción. Por ejemplo, tienes una estructura:

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

Tengo un controlador, que se define como:

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

este controlador pasó como un apoyo a los componentes de React. También tengo una función, que toma la estructura y devuelve la misma estructura, pero con controladores en lugar de valores:

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

Por ahora, no puedo escribir esa función correctamente, porque TS no puede producir un tipo basado en la estructura de otro tipo.

Vaca ¿Podría escribir makeForm con HKT?

Hm, interesante.

Tal vez sea posible algo como esto:

//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 El punto más interesante es esta línea:

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

Vale la pena mencionar que HKT podría haber sido una respuesta a los llamados tipos parciales (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

No exactamente. {x: number?} no se puede asignar a {x?: number} , porque uno
se garantiza que existe, mientras que el otro no.

El martes 11 de octubre de 2016 a las 09:16, Aleksey Bykov [email protected] escribió:

Vale la pena mencionar que HKT podría haber sido una respuesta a los llamados
tipos parciales (# 4889 (comentario)
https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-247721155
):

escriba MyDataProto uno: K;
otro: K;
aún Otro: K;
} tipo Idéntico = a; tipo Opcional = a ?;
= {get (): a;

;
;
;

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -252913109,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AERrBNFYFfiW01MT99xv7UE2skQ3qiPMks5qy4wRgaJpZM4C99VY
.

@isiahmeadows tiene razón, por el momento no hay forma / sintaxis de hacer que una propiedad sea verdaderamente opcional basándose únicamente en su tipo, y eso es una pena

Otro más: sería bueno si la propiedad se pudiera hacer readonly . Parece que se requiere algún tipo de función de macros.

Solo lanzando esto ... prefiero la sintaxis * sobre la sintaxis ~ . Algo acerca de ~ parece muy alejado de la perspectiva de la distribución del teclado. Además, no estoy seguro de por qué, pero creo que * parece un poco más legible / distinguible con todos los corchetes angulares que están en la mezcla. Sin mencionar que las personas familiarizadas con otros lenguajes como Haskell podrían asociar inmediatamente la sintaxis a HKT. Parece un poco más natural.

Tendría que estar de acuerdo con la sintaxis * . Primero, es más distinguible,
y segundo, representa mejor un tipo "cualquier tipo funciona".


Isiah Meadows
[email protected]

El domingo 6 de noviembre de 2016 a las 12:10 a. M., Landon Poch [email protected]
escribió:

Solo lanzando esto ... prefiero la sintaxis * sobre la sintaxis ~.
Algo sobre ~ parece estar muy alejado de una distribución de teclado
perspectiva. Además, no estoy seguro de por qué, pero creo que * parece un poco más
legible / distinguible con todos los corchetes angulares que están en la mezcla.
Sin mencionar que las personas familiarizadas con otros idiomas como Haskell podrían
asociar inmediatamente la sintaxis a HKT. Parece un poco más natural.

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -258659277,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AERrBHQ4SYeIiptB8lhxEAJGOYaxwCkiks5q7VMvgaJpZM4C99VY
.

Hito: community ? ¿Cuál es el estado actual de este problema / función?

@whitecolor el estado es DIY (hágalo usted mismo)

El problema tiene la etiqueta Accepting PRs . esto significa que las solicitudes de extracción para implementar esta función son bienvenidas. Consulte https://github.com/Microsoft/TypeScript/wiki/FAQ#what -do-the-labels-on-these-issues-mean para obtener más detalles.

También consulte https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288

Ok, veo las etiquetas, solo tengo dudas de que si el equipo no TS es capaz de lograrlo.

Ahora tengo tiempo para implementar un PR tan simple. Espero obtener algunas sugerencias de los desarrolladores principales, pero hasta ahora no hay preguntas, todo se ve bien y es comprensible. Seguirá un progreso aquí.

@Artazor ¿Tienes suerte con esto?

@raveclassic : resultó ser más difícil de lo que parecía, sin embargo, todavía espero seguir adelante. Sintácticamente es obvio, pero las reglas / fases de verificación de tipo no son tan claras para mí como quiero -)

Intentemos revivir mi actividad -)

Simplemente rastreando un progreso y el camino del desarrollo de la idea. He considerado tres opciones para implementar esta función.

He planeado enriquecer una propiedad TypeParameterDeclaration con higherShape opcional

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

y he considerado tres opciones para implementar HigherShape

1. Arity simple para el dominio

type HigherShape = number

corresponde al siguiente uso:

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

en este caso más simple, parece que el tipo number sería suficiente. Sin embargo, deberíamos poder determinar una forma superior real para cada tipo dado para estar seguros de que podemos usarla como un argumento de tipo para los requisitos de forma específicos. Y aquí nos enfrentamos a un problema: la forma más alta de la clase Demo sí misma no se puede expresar como un número. Si fuera así, entonces debería representarse como 2 , ya que tiene dos parámetros de tipo,
y sería posible escribir

var x: Demo<Array, Demo>

y luego luchar con el problema de verificación de tipo diferido con la propiedad .both . Por lo tanto, el tipo number no es suficiente (creo);

de hecho, el tipo Demo tiene la siguiente forma de orden superior:

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

2. Dominio y co-dominio completamente estructurados

Luego he investigado la representación opuesta, más completa de las formas superiores, que permitiría representar formas como la mencionada anteriormente, e incluso peor:

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

La estructura de datos para esto es sencilla, pero no interactúa bien con el sistema de tipos TypeScript. Si permitiéramos tales tipos de orden superior, nunca sabremos si * significa el tipo de suelo, que podría usarse para escribir valores. Además, ni siquiera logré encontrar una sintaxis apropiada sobre cómo expresar restricciones tan monstruosas de orden superior.

3. Dominio estructurado / Codominio simple implícito

La idea principal - expresión de tipo (incluso con argumentos de tipo reales) siempre da como resultado un tipo básico - que se puede usar para escribir una variable. Por otro lado, cada parámetro de tipo puede tener sus propios parámetros de tipo detallados en el mismo formato que se utiliza en otros lugares.

Esta fue mi decisión final que trataría de defender.

type HigherShape = NodeArray<TypeParameterDeclaration>;

ejemplo:

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

Aquí quiero señalar que M es local por T1<M extends A> extends Z<M> y existe en un alcance de visibilidad más profundo que T1. Por tanto, M no está disponible en el cuerpo de SomeClass .
Y * significa simplemente un identificador nuevo (tipo anónimo) que nunca choca con nada (y podría implementarse en una etapa posterior)


Por lo tanto, la firma final de la TypeParameterDeclaration

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

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

¿Quieres escuchar cualquier opinión de @DanielRosenwasser , @ aleksey-bykov, @isiahmeadows y otros?

Me parece bien, pero sé muy poco sobre la estructura interna del código base de TypeScript.

¡Me gustaría sumar mi voz al coro que solicita esto y animarte, Artazor! :)

Esta característica me sería útil en mi implementación para hacer que Redux sea seguro para los tipos.

@michaeltontchev ¿Qué problemas tiene para hacer que Redux sea seguro para escribir?

En caso de que esté interesado, publiqué recientemente https://github.com/bcherny/tdux y https://github.com/bcherny/typed-rx-emitter , que se basan en ideas de Redux y EventEmitter.

Ahora parece, es necesario volver a establecer la base en la rama @rbuckton # 13487 con los parámetros genéricos predeterminados. En otro caso, entraremos en conflicto en gran medida.

@bcherny : gracias por los enlaces, ¡los

Estaba viendo cómo hacer que combineReducers sea seguro para los tipos asegurándome de que tenga un reductor del tipo correcto para cada propiedad del estado (sin extras). Logré hacerlo en este caso específico sin genéricos anidados, pero una solución anidada hubiera sido mejor. Tengo lo siguiente:

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

Básicamente, el tipo que presento anteriormente garantiza que las asignaciones de reductores que pasa a combineReducers cubren todas las propiedades de su estado y tienen los tipos de retorno adecuados. No pude encontrar ninguna solución de este tipo mientras buscaba en línea; me parece que no se puede hacer sin la función keyof, que apareció hace solo dos meses :)

Espero que la función keyof también sea útil para ImmutableJs para hacer que los setters y getters sean seguros para escribir, aunque es posible que necesite algunas herramientas adicionales al respecto.

Editar: para aclarar, los genéricos anidados me habrían permitido no tener que codificar el tipo Reducer en el tipo StatePropertyNameAndTypeAwareReducer, sino pasarlo como genérico, pero tendría que ser un genérico anidado, lo que no es posible en el momento.

Edit2: creó un problema para Redux aquí: https://github.com/reactjs/redux/issues/2238 Parece que no hacen mucho TypeScript, por lo que están buscando personas de TypeScript que conozcan Redux para opinar.

¿Cómo va?

Quizás una pregunta ingenua, pero ¿por qué ~ o * lugar de un parámetro genérico normal? ¿Es para indicar que no se usa? Es decir. Por qué no:

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

O:

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

O incluso:

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

@bcherny Creo que esto causa ambigüedades en la sintaxis, ya que Functor<A<T>> anteriormente habría significado " A de T ", donde T es algún tipo en local alcance. Es poco probable, pero esta sintaxis también podría terminar siendo un cambio importante para algunas bases de código, por la misma razón.

@masaeedu ya veo. La nueva sintaxis significa "enlazar T perezosamente", en lugar de "enlazar T estrictamente en el ámbito actual".

Dicho esto, creo que la propuesta de @DanielRosenwasser para T: * => * tiene más sentido aquí, ya que existe un "estado de la técnica".

En Haskell, el operador -> es en realidad un tipo parametrizado (quizás sea más fácil de visualizar Func<TArg, TRet> ). El constructor de tipo -> acepta dos tipos arbitrarios T y U y produce el tipo de un constructor de valor (es decir, función) que asigna valores de tipo T a valores de tipo U .

Lo interesante es que también es un constructor de clase! El constructor de tipos -> acepta dos tipos arbitrarios T* y U* (asterisco solo para distinción visual), y produce el tipo de constructor de tipos que asigna tipos de tipo T* a tipos de tipo U* .

Es posible que notes un patrón en este punto. La sintaxis y la semántica que se utilizan para definir y hacer referencia a tipos simplemente se reutilizan para definir y hacer referencia a tipos. De hecho, ni siquiera se está reutilizando, simplemente está definiendo implícitamente cosas en dos universos diferentes a la vez. (el hecho de que sea isomórfico de hecho significa que es capaz de definir cosas en niveles infinitos, valores -> tipos -> clases -> clases -> ..., excepto por el desafortunado * , pero eso es un tema para otro momento)

De hecho, este patrón tiene tanto sentido que algunas personas implementaron una extensión GHCi ampliamente utilizada que la generaliza a todos los constructores de tipos, no solo a -> . La extensión se llama "tipos de datos", y así es como Haskell viene con sus listas heterogéneas ( [] es tanto el tipo de listas de valores como el tipo de listas de tipos), tuplas heterogéneas, "longitud inteligente "vectores, y muchas otras características además.

Quizás no queremos ir tan lejos como DataKinds todavía, así que nos quedaremos con los constructores de tipo * y -> , pero si seguimos la sintaxis propuesta por Daniel , o más en general hacer definiciones de tipo isomorfas a las definiciones de tipo, nos abrimos para aprovechar el desarrollo futuro en esta área.

Siguiendo con mi publicación anterior, me gustaría recomendar que usemos any lugar de * ; esto representa tanto el tipo de cada valor como el tipo de cada tipo. Si la sintaxis parece confusa, podríamos sacar una página del libro de Haskell y usar un prefijo ' para eliminar la ambigüedad de tipos y tipos.

El ejemplo de OP se escribiría así:

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

Nitpick: Encuentro any confuso en general en el sentido de que hace dos cosas diferentes.
Es un súper tipo de todos los demás, como nunca es un subtipo de todos los demás, por lo que si una función solicita un parámetro any , puede ingresar lo que sea. Hasta aquí todo bien.
La parte en la que se vuelve divertido es cuando una función solicita algo específico y usted proporciona any . Este tipo verifica, mientras que cualquier otro tipo que sea más amplio de lo que se solicitó lo convertiría en un error.
Pero sí, lo que sea.

En otra nota, ' sería confuso ya que también se usa en cadenas literales.

@Artazor ¿ Alguna novedad con esto? La última vez que mencionaste que necesitas cambiar la base de los parámetros genéricos predeterminados. Y me parece que eres el único lo suficientemente cerca de un POC en funcionamiento.

También vale la pena pensar en cómo esto interactúa con el subtipo. Usar * por sí solo no es suficiente; en lenguajes que usan polimorfismo ad-hoc en lugar de polimorfismo acotado, tiene tipos de restricción para limitar los argumentos de tipo aceptable. Como ejemplo, el tipo de Monad T es en realidad Constraint , no * .

En TypeScript usamos subtipos estructurales en su lugar, por lo que nuestros tipos deben reflejar las relaciones de subtipo entre tipos. El artículo de Scala sobre este tema podría producir algunas buenas ideas sobre cómo representar las relaciones de varianza y subtipo en un sistema de tipos: " Hacia la igualdad de derechos para tipos de parentesco superior ".

¿Algún progreso en esto?

Un enfoque alternativo de @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe

El problema con el enfoque adoptado por fp-ts es que le obliga a volver a implementar bibliotecas que de otro modo han sido probadas. Para mí, la idea del mecanografiado es poder escribir correctamente lo que actualmente se piensa como las mejores prácticas en JavaScript, no para obligarlo a volver a implementarlo de una manera ts.

Hay muchos ejemplos aquí que muestran que HKT son necesarios para describir correctamente los contratos que usamos actualmente en js libs, ya sea fantasy land, ramda o formas de reacción.

Sería muy bueno ver esto impementado :)

~ ¿Hay alguien dispuesto / capaz de trabajar en este pago? No dude en contactarme para discutir. O cualquiera puede asesorar a alguien que podamos encontrar para que trabaje en esto, por favor, también hágamelo saber. ~ [EDITAR: probablemente lo he decidido] -357567767) para abandonar este ecosistema y mi comentario posterior en este hilo me hizo darme cuenta de que probablemente sería una empresa masiva]

Un enfoque alternativo de @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe

No me molesté en asimilar eso por completo, porque observo que el map resultante todavía especifica explícitamente el tipo de contenedor Option y, por lo tanto, no es completamente genérico en la forma en que los tipos de tipo superior (HKT) puede proporcionar:

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

Como señaló @spion el 26 de agosto de 2016 , los HKT son necesarios para hacer genérica cualquier función que necesite una fábrica y en la que el tipo de contenedor parametrizado sea genérico. Habíamos explorado esto en nuestras discusiones sobre el diseño de lenguajes de programación.

PD: Si tiene curiosidad, esta característica influye significativamente en mi análisis (incluido el de @keean ) del panorama del lenguaje de programación .

@ shelby3 FWIW Option map (en Option.ts ) no es genérico porque representa la instancia mientras que Functor 's map (en Functor.ts ) es genérico porque representa la clase de tipo . Luego, puede definir una función lift genérica que pueda operar con cualquier instancia de functor.

Sería muy bueno ver esto impementado :)

Estoy totalmente de acuerdo :)

@ shelby3 : para fusionar una función como esta, su mejor opción podría ser obtener
ellos para priorizarlo en la hoja de ruta de TS; Tuve algunas relaciones públicas que principalmente consiguieron
retroalimentación / fusiones ya sea cuando se realizan pequeñas correcciones o si ya estaban buscando
en ellos. No quiero ser negativo, pero es una consideración si estás
a punto de invertir recursos en esto.

El 8 de enero de 2018 a las 4:05 p.m., "shelby3" [email protected] escribió:

¿Hay alguien dispuesto / capaz de trabajar en este pago? Siéntase libre de contactarnos
yo [email protected] para discutir. O cualquiera puede entrenar a alguien
es posible que encontremos para trabajar en esto, por favor también hágamelo saber.

Un enfoque alternativo de @gcanti https://github.com/gcanti
https://medium.com/@gcanti/higher -kinded-types-in
mecanografiado-tierra-estática-y-fantasía-d41c361d0dbe

No me molesté en asimilar completamente eso, porque observo el mapa resultante
todavía especifica explícitamente el tipo de contenedor Option y, por lo tanto, no está completamente
genérico de manera que los tipos de tipo superior (HKT) pueden proporcionar:

mapa de funciones (f: (a: A) => B, fa: HKTOption ): Opción { return (fa como Opción ) .map (f) }

HKT son necesarios para hacer genérica cualquier función que necesite una fábrica y
en el que el tipo de contenedor parametrizado debe ser él mismo genérico. Tuvimos
exploré esto https://github.com/keean/zenscript/issues/10 en nuestro
discusiones sobre el diseño de lenguajes de programación.

PD: si tienes curiosidad, esta función influye significativamente en mi
(incluido el análisis de @keean https://github.com/keean ) del
paisaje del lenguaje de programación
https://github.com/keean/zenscript/issues/35#issuecomment-355850515 . yo
darse cuenta de que nuestras opiniones no se correlacionan completamente con las prioridades de TypeScript
con el objetivo principal de ser un superconjunto de Javascript / ECMAScript y soporte
ese ecosistema.

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-355990644 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AC6uxYOZ0a8G86rUjxvDaO5qIWiq55-Fks5tIi7GgaJpZM4C99VY
.

@gcaniti se disculpa por el ruido y gracias por la explicación adicional. Debería haber estudiado más antes de comentar. Por supuesto, es mi error de conceptualización porque (ya lo sé) un functor requiere una implementación de instancia.

Afaics, su ingenioso "truco" permite hacer referencia a una fábrica de forma genérica (por ejemplo, lift ), pero requiere el texto estándar adicional de Aumento de módulo para (actualizar y) especializar cada escritura de la fábrica genérica para el tipo especializado de functor , por ejemplo, Option . ¿No sería necesario ese texto estándar para cada uso genérico de una fábrica genérica, por ejemplo, el ejemplo genérico sort @keean y yo discutimos? ¿Es posible que también haya otros casos de esquina por descubrir?

¿Kotlin copió tu idea o viceversa? (algunas críticas adicionales en ese enlace, pero no sé si se aplican al caso de TypeScript)

No quiero ser negativo, pero es una consideración si está a punto de invertir recursos en esto.

Sí, ese pensamiento también se me ocurrió. Gracias por articularlo. Sospecho que una de las consideraciones serían los impactos en general en el sistema de tipos y los casos extremos que podría generar, como lo señaló @masaeedu. Quizás habría resistencia a menos que esto se pensara y demostrara muy a fondo.

Tenga en cuenta que también estoy investigando Ceilán para determinar mejor cuál será su nivel de inversión en el objetivo de compilación de EMCAScript. (Necesito estudiar más).

También me acaba de morder esta limitación. Me gustaría que I en el siguiente ejemplo se infiriera automáticamente:


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

Por lo que puedo decir, esto requiere un tipo de tipo superior o especificar un parámetro genérico duplicado (por ejemplo, doStuff<User, number>() ) que parece redundante.

Recientemente, también me ha sorprendido esta limitación.

He estado trabajando en una biblioteca por promesas . Proporciona varias funciones de utilidad para trabajar con ellos.

Una característica central de la biblioteca es que devuelve el mismo tipo de promesa que puso en su interior. Entonces, si estuviera utilizando una promesa Bluebird y llamara a una de las funciones, devolvería una promesa Bluebird con todas las funciones adicionales que brindan.

Quería codificar esto en el sistema de tipos, pero rápidamente me di cuenta de que esto requiere trabajar con un tipo P del tipo * -> * tal que P<T> extends Promise<T> .

Aquí hay un ejemplo de una de esas funciones:

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

En la situación anterior, pude evitar la necesidad de tipos más altos mediante el uso del tipo this bastante hacky.

Sin embargo, el siguiente caso no se puede resolver:

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

Porque no hay ningún truco que me permita hacer this<T[]> o algo así.

Tenga en cuenta mi pequeña disculpa en la documentación.

Tengo otro escenario en el que creo que esta función sería útil (suponiendo que haya interpretado correctamente la propuesta), como se indica en la referencia anterior.

En el paquete en cuestión, es necesario tener una clase o función genérica sin tipo utilizada como tipo, ya que los tipos genéricos son normalmente creados por el usuario.

Aplicando la propuesta a mi escenario, creo que se vería así:

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

Dado que no hay forma de hacer referencia a los tipos genéricos de manera confiable en mi implementación, estoy seguro de que me he perdido algo.

@ Silic0nS0ldier En realidad, ese caso se puede resolver ahora mismo. Utiliza un tipo de constructor estructural, como este:

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

Y luego diga component ?: ComponentConstructor .

Incluso de manera más general, puede tener un tipo de función genérico:

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

Esto se llama polimorfismo paramétrico de rango n y en realidad es una característica bastante rara en los idiomas. Entonces, es aún más desconcertante por qué TypeScript no tiene tipos de tipo superior, que es una característica mucho más común.

La limitación discutida aquí aparecerá si necesita hacer referencia a un TComponent<T, S> específico. Pero en su caso, esto parece innecesario.


También puede usar typeof Component que le dará el tipo de constructor Component pero esto causará varios problemas con los subtipos.

@GregRos Su solución propuesta parecía prometedora (acepta los tipos dentro del archivo de definición), pero los tipos compatibles están siendo rechazados. https://gist.github.com/Silic0nS0ldier/3c379367b5e6b1abd76e4a41d1be8217

@ Silic0nS0ldier Vea mis comentarios sobre la esencia.

@chrisdavies ¿

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 Sí. Eso funciona para mi escenario. No había encontrado ese ejemplo en los documentos (¡aunque soy conocido por faltarme cosas!). ¡Gracias!

He estado pensando mucho en esta característica y tengo algunas ideas al respecto, inclinándome hacia algún tipo de especificación, pero todavía puedo ver muchos problemas. Mis sugerencias son un poco diferentes a las propuestas hasta ahora.


En primer lugar, creo que usar cualquier tipo de sintaxis T<*, *> para constructores de tipos es una mala idea porque no se adapta bien a la complejidad del constructor de tipos. Tampoco estoy seguro de si tiene sentido especificar el tipo de constructor de tipo cada vez que se hace referencia a él, ya que no hacemos esto para funciones, incluso para funciones con parámetros de tipo.

Creo que la mejor manera de implementar esto es tratar los tipos de tipo superior como otros tipos, con nombres regulares, y definir una buena relación de subtipos sobre los propios constructores de tipos que se pueden usar para imponer restricciones.

Creo que deberíamos usar algún tipo de prefijo o sufijo para distinguirlos de otros tipos, principalmente para proteger a los usuarios de mensajes de error incomprensibles que involucran constructores de tipos cuando solo han querido escribir código regular. Me gusta el aspecto de: ~Type, ^Type, &Type o algo así.

Entonces, por ejemplo, una firma de función podría ser:

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

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

(No estoy usando el prefijo ~ para los tipos construidos a propósito)

Al usar extends aquí, básicamente he dicho dos cosas:

** 1. Si es necesario: ~L es un constructor de tipo que tiene el mismo tipo que ~List , es decir, el tipo * -> * (o tal vez * => * , ya que => es la flecha de TypeScript).

  1. ~L es un subtipo de ~List . **

El uso de extends para denotar el tipo de constructor de tipos escala a constructores de tipos arbitrariamente complejos, incluidos elementos como ((* => *) => (* => *)) => * .

Realmente no puede ver ese tipo en el identificador del tipo, pero no creo que sea necesario. Ni siquiera estoy seguro de que una relación de subtipo entre constructores de tipos deba preservar los tipos, por lo que (1) podría no ser necesario.

No construir tipos incompletos

Creo que no deberíamos apoyar la construcción de tipos incompletos. Es decir, algo como esto:

(*, *) => * => *

Creo que crearía más problemas de los que vale la pena. es decir, cada constructor de tipos debe construir algún tipo concreto, y ese tipo concreto debe especificarse en el contexto donde se define el TC.

Manera estructural de definir constructores de tipos

También creo que debería haber una forma estructural de especificar constructores de tipos, de la misma manera que cualquier otro tipo puede especificarse estructuralmente, incluidos los tipos de funciones genéricas de muy alto orden. He estado pensando en una sintaxis como:

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

Que es similar a la sintaxis existente para tipos de función con parámetros de tipo:

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

Los dos incluso se pueden combinar para obtener esto:

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

Que es un constructor de tipos que construye un tipo de función genérico.

La ventaja es que estos tipos estructurales se pueden utilizar al especificar otros tipos estructurales, al especificar restricciones de tipo, etc. A veces, esto significa que pueden usar símbolos locales de referencia a los que no se puede hacer referencia desde ningún otro lugar.

Aquí hay un ejemplo:

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

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

En el ejemplo anterior, el constructor de tipo estructural ~<A>List<A, B> referencia al parámetro de tipo B . No es posible especificar esta relación de ninguna otra manera, al menos sin codificar el tipo construido parcialmente List<A, *> . También hay otros ejemplos.

La relación de subtipo

La relación de subtipo parece tener sentido, pero me he encontrado con una serie de dificultades al tratar de caracterizarla.

Mi primera idea fue la siguiente. Para que ~A sea ​​un subtipo de ~B :

  1. (a) Deben ser del mismo tipo (en términos de aridad, no de restricciones).
  2. (b) Para cada parametrización legal T₁, T₂, ... de ~A , A<T₁, T₂, ...> debe ser un subtipo de B<T₁, T₂, ...> .

Sin embargo, esto tiene varias limitaciones.

  1. class MySpecialPromise implementa PromiseLike {} En este caso, ~MySpecialPromise no es un subtipo de ~PromiseLike porque tienen diferentes tipos.

  2. clase MyArrayPromiseimplementa PromiseLike

    La relación de subtipo tampoco se conserva en este caso.

Una versión más generalizada de (b) es la siguiente:

(b) Para cada parametrización legal T₁, T₂, ... de ~A , existe una parametrización S₁, S₂, ... de ~B tal que A<T₁, T₂, ...> es un subtipo de B<S₁, S₂, ...> .

En otras palabras, hay un mapeo F (T₁, T₂, ...) = S₁, S₂, ... con las propiedades anteriores. Este mapeo debe usarse para construir el B<...> parametrizado a partir de un A<...> parametrizado. Puede permitirnos hacer esto incluso si los constructores de tipos tienen tipos diferentes.

El problema con esta relación es que no estoy seguro de cómo sería posible encontrar el mapeo correcto. En idiomas con mecanografía nominal, cada declaración en la línea de:

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

Define un mapeo entre los parámetros de tipo de ~A y los parámetros de tipo de ~B , así es como se puede recuperar el mapeo. Sin embargo, en el sistema de tipificación estructural de TypeScript, no podemos confiar en declaraciones explícitas de este tipo.

Una forma es admitir solo constructores de tipos para tipos con la información de tipo correcta, como cláusulas implements o algún tipo de miembro de tipo abstracto similar a Scala. Sin embargo, no estoy seguro de si este es el camino a seguir.

@GregRos - ¡Notas interesantes! Unas cuantas preguntas.


¿Qué quiere decir con tipo de hormigón? ¿Quiere decir algo con el tipo * , o un tipo sin parámetros de tipo enlazado?


No construir tipos incompletos
Creo que no deberíamos apoyar la construcción de tipos incompletos. Es decir, algo como esto:
(*, *) => * => *

¿Qué quiere decir con la construcción de tipos incompletos? ¿Quiere decir que cada aplicación como L<A> debería tener el tipo * . ¿Es el constructor tipo par especial en su ejemplo, por ejemplo, (* => *) => * => * estaría bien?


Manera estructural de definir constructores de tipos

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

¿Son estos ejemplos diferentes, además de que el primero es anónimo?


La relación de subtipo

~A y ~B no se refieren a tipos, así que ¿tiene sentido que tengan una relación de subtipo? ¿Cuándo es necesario comprobar que un constructor es un 'subtipo' de otro? ¿Es posible esperar hasta que se apliquen los constructores y verificar los tipos resultantes?

@ jack-williams ¡Gracias por los comentarios!

¿Qué quiere decir con la construcción de tipos incompletos? ¿Quiere decir que todas las aplicaciones como L<A> deberían tener archivos kind *. ¿Es el constructor tipo par especial en su ejemplo, por ejemplo, (* => *) => * => * estaría bien?

Sí exactamente. Cada aplicación como L<A> debe tener el tipo * . No estoy seguro de cuán vendido estoy en eso.


¿Son estos ejemplos diferentes, además de que el primero es anónimo?

La primera es una expresión de tipo, mientras que la segunda es una declaración. Son idénticos en la mayoría de los aspectos, de la misma manera que estos tipos son idénticos en la mayoría de los aspectos:

{
     a : number;
     b : string;
}

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

La sintaxis tiene varias motivaciones:

  1. Al igual que todo lo demás en TypeScript, permite que los constructores de tipos se especifiquen de forma estructural y anónima.
  2. Una expresión de tipo (como el objeto anónimo escrito mencionado anteriormente) puede usarse en ciertos contextos donde no se pueden usar declaraciones de declaración, como en las firmas de tipo de funciones. Esto les permite capturar identificadores locales y expresar cosas que no se pueden expresar de otra manera.

~A y ~B no se refieren a tipos, así que ¿tiene sentido que tengan una relación de subtipo? ¿Cuándo es necesario comprobar que un constructor es un 'subtipo' de otro? ¿Es posible esperar hasta que se apliquen los constructores y verificar los tipos resultantes?

Los constructores de tipos pueden o no considerarse tipos. Propongo considerarlos como tipos, simplemente incompletos que no tienen valores y no pueden aparecer en ningún contexto que requiera el tipo de un valor. Esta es la misma filosofía adoptada por Scala, en este documento

Por relación de subtipo, me refiero esencialmente a algún tipo de relación de "conformidad" que se puede utilizar para restringir los constructores de tipos. Por ejemplo, si quiero escribir una función que funcione en todo tipo de promesas de varios tipos, como Promise<T> , Bluebird<T> , etc., necesito la capacidad de restringir los parámetros TC con el interfaz PromiseLike<T> de alguna manera.

La palabra natural para este tipo de relación es una relación de subtipo.

Veamos un ejemplo. Suponiendo que hemos elaborado una relación de subtipo entre los constructores de tipos, puedo escribir una función como esta:

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

Y se supone que la restricción ~P extends ~PromiseLike garantiza que esta es una función que funciona con promesas y solo promesas. La restricción también garantizará que dentro del cuerpo de la función, promise implementará PromiseLike<A> , y así sucesivamente. Después de todo, los miembros reconocidos por TypeScript en el cuerpo de la función son precisamente aquellos cuya existencia puede demostrarse a través de restricciones.

De la misma manera Promise<T> extends PromiseLike<T> , porque son estructuralmente compatibles y pueden reemplazarse entre sí, ~Promise extends ~PromiseLike porque construyen tipos estructuralmente compatibles y por lo tanto pueden reemplazarse entre sí.


Para subrayar el problema del subtipo, considere una vez más:

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

¿Podemos abstraer más de ~MyPromise de la misma manera que abstraemos más de ~Promise ? ¿Cómo captamos la relación entre ellos?

El mapeo del que hablé antes es el mapeo que, dada una parametrización de ~MyPromise , producirá una parametrización de ~Promise modo que el tipo construido por ~MyPromise es un subtipo del uno construido por ~Promise .

En este caso, el mapeo es así:

T => T[]

@GregRos

En este caso, ~MySpecialPromise no es un subtipo de ~PromiseLike porque tienen diferentes tipos.

En Haskell, este tipo de problema se resuelve permitiendo la aplicación parcial de tipos y definiendo tipos para que el parámetro de tipo final coincida con el parámetro de tipo de cualquier interfaz que esté implementando.

En su ejemplo, MySpecialPromise se definiría como MySpecialPromise<TSpecial, TPromiseVal> , y ~MySpecialPromise<SpecialType> tendría un tipo idéntico a ~Promise .

@GregRos

Por relación de subtipo, me refiero esencialmente a algún tipo de relación de "conformidad" que se puede utilizar para restringir los constructores de tipos. Por ejemplo, si quiero escribir una función que funcione en todo tipo de promesas de varios tipos, como Promise, Bluebird, y así sucesivamente, necesito la capacidad de restringir los parámetros de TC con la interfaz PromiseLikede alguna manera.
function mapPromise<~P extends ~PromiseLike, A, B>(promise : P<A>, func : (x : A) => B) : P<B> ;

Creo que cuando se trata de verificar el tipo de esa función, intentaría unificar BlueBird<T> y PromiseLike<T> para el T elegido, estos son solo tipos concretos y caen bajo subtipo. No veo por qué necesitaría una relación especial para los constructores ~BlueBird y ~PromiseLike .

¿Supongo que se usaría en algo como esto?


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;

Aquí es posible que desee verificar que las restricciones de y implican las restricciones de x, pero ¿TypeScript no tiene ya maquinaria para verificar que BlueBird<T> extiende PromiseLike<T> que podría usarse?

@ jack-williams Todo se reduce a cómo se especifica la siguiente restricción:

~ P es un constructor de tipo tal que, para todo A , P<A> es un subtipo de PromiseLike<A> .

¿Qué tipo de sintaxis usarías? ¿Qué tipo de concepto usarías? Puedes escribir algo como esto:

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

Pero esta sintaxis tiene limitaciones. Por ejemplo, no puede expresar esta clase en absoluto, porque no podemos construir el tipo P<A> en el punto donde se declara para restringirlo:

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

Pero supongo que puedes usar tipos existenciales para eso, así:

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

Luego, puede requerir que todos los constructores de tipos estén restringidos a través de sus tipos construidos dentro de la firma de la función o tipo, opcionalmente usando tipos existenciales.

Con tipos existenciales, esto tendría el mismo poder expresivo que una relación de subtipo con un mapeo.

Sin embargo, esto tendría varios problemas:

  1. Especificar constructores de tipos con tipos como ((* => *) => *) => * requeriría introducir muchos tipos existenciales, algunos de los cuales tendrían que ser de orden superior. Todos ellos tendrían que aparecer en la firma de la función o clase.
  2. No estoy del todo seguro de que sea más fácil encontrar los tipos existenciales en cuestión que encontrar el mapeo.
  3. Creo que es menos elegante que la relación de subtipos.
  4. Potencialmente introduce otra forma de tipo con la que tendrías que lidiar.

@GregRos

¿Qué tipo de sintaxis usarías? ¿Qué tipo de concepto usarías?

_Personalmente_ No usaría ninguna sintaxis especial y solo usaría:

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

pero esta es solo mi opinión, ya que veo cosas como number como un constructor nulo: por lo que no es necesario que haya una distinción.

Mi opinión sobre el subtipo de funciones de constructor sería mantenerlo lo más simple posible. Deben tener la misma aridad y los parámetros deben ser subgrupos entre sí, teniendo en cuenta la contravarianza y la covarianza al igual que el papel Scala.

La aplicación parcial puede solucionar los casos en los que tienen arity diferente (no me importaría auto-curryng para constructores de tipos, por lo que puede escribir MySpecialPromise<SpecialType> ).

En el ejemplo interface MyPromise<T> extends Promise<T[]> {} , tendría que ser honesto y decir que no estoy convencido de que valga la pena la complejidad de manejar este caso; creo que sería una característica bastante útil sin ella.

Manejar ese caso es equivalente a (creo), decir: ~MyPromise extends ~(Promise . []) donde [] es el constructor de la lista y . es la composición del constructor. Parece que las cosas se ponen mucho más difíciles, ya que ahora no es suficiente inspeccionar la estructura de los constructores, ¡sino que también hay que razonar sobre la composición!

@ jack-williams Esto no funciona con los parámetros de tipo predeterminados. Si escribo P extends Foo , donde Foo tiene un parámetro de tipo predeterminado, es decir, type Foo<T = {}> = ... , ¿cuál es el tipo de P ?

Solo me gustaría decir que apruebo los tipos de orden superior (he tenido situaciones en proyectos reales de TypeScript en las que serían útiles).

Sin embargo , no creo que deban apoyar el curry. Amo a Haskell, pero eso simplemente no encaja con TypeScript.

Los tipos de orden superior son útiles incluso sin curry o aplicación parcial, pero si se necesita una aplicación parcial, preferiría ver una sintaxis explícita para ella. Algo como esto:

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

@ cameron-martin

Editar: Lo siento, no creo que mis comentarios sean _no_ muy claros. Por P tiene su propia clase, quiero decir que tiene una clase impuesta por su uso. Digamos que siempre se asume que las restricciones son del tipo más alto, por lo que se asume que Foo es ~Foo . Solo si forzamos que P sea ​​de un tipo inferior, comprobamos si Foo tiene un parámetro predeterminado. Mi preocupación con esto es una inferencia amable, pero en ese caso ~ no ayudará y creo que necesitamos anotaciones completas.

P tiene su propia especie, ¿no es así? ¿La pregunta no puede hacer tratamos Foo como ~Foo , o como Foo<{}> : Yo diría que podrían alcanzarse con el tipo de P. Así que si P es un tipo forzamos el parámetro predeterminado, y si P es un constructor * => * , entonces tratamos Foo la misma manera.

@Pauan De acuerdo con sus sugerencias.

@ jack-williams He considerado la noción de subtipo, como mencioné anteriormente:

Mi primera idea fue la siguiente. Para que ~A sea ​​un subtipo de ~B :

  1. (a) Deben ser del mismo tipo (en términos de aridad, no de restricciones).
  2. (b) Para cada parametrización legal T₁, T₂, ... de ~A , A<T₁, T₂, ...> debe ser un subtipo de B<T₁, T₂, ...> .

El problema es que si mantenemos las cosas lo más simples posible, terminaríamos con una relación de subtipo que es paradójica y no encaja en el lenguaje.

Si MyPromise<T> extends Promise<T[]> eso significa que MyPromise<T> tiene que ser utilizable donde sea que se pueda utilizar Promise<T[]> , pero este ya no sería el caso.

Si usaras as para convertir a : MyPromise<T> en Promise<T[]> , estarías subiendo, pero esto, paradójicamente, haría que a más asignable.

Las restricciones genéricas existentes, que siguen la relación de subtipo existente, también se pueden usar para lograr un efecto similar y causar un comportamiento extraño:

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

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

La mecanografía también se volvería al menos parcialmente nominal como efecto secundario. Estos tipos de repente serían diferentes, donde actualmente son los mismos:

type GenericNumber<T> = number;

type RegularNumber = number;

Ni siquiera estoy seguro del efecto que tendría en tipos complejos de unión / intersección con parámetros de tipo, tipos puramente estructurales, tipos con comprensión de miembros y similares.

Mi sentimiento personal es que: La relación de subtipo sobre constructores de tipo necesita respetar la existente, no oponerse a ella . Lamentablemente, esto requiere que las cosas sean más complejas.


La razón principal para usar algún tipo de notación especial para los constructores de tipos es que el 99% de los desarrolladores no saben qué es un constructor de tipos y no querrían ser bombardeados con mensajes de error sobre ellos.

Esto es muy diferente de Haskell, donde cada desarrollador está obligado por ley a tomar un curso avanzado en teoría de categorías.

Una razón secundaria es que en algunos casos (como el caso de los parámetros predeterminados antes mencionado), la sintaxis sería ambigua o, de lo contrario, no sería posible abstraer en absoluto un constructor de tipo específico.

EDITAR: Lo siento @GregRos, ¡no vi tus últimos comentarios!

La relación de subtipos sobre los constructores de tipos debe respetar la existente, no oponerse a ella.

Si esto se puede lograr, estoy de acuerdo. Simplemente no entiendo todos los detalles y lo fácil que sería.

Una razón secundaria es que en algunos casos (como el caso de los parámetros predeterminados antes mencionado), la sintaxis sería ambigua o, de lo contrario, no sería posible abstraer en absoluto un constructor de tipo específico.

No estoy seguro de estar de acuerdo en que sería ambiguo si siempre asume el tipo más alto de restricción hasta que necesite que sea más bajo. Esto no es una afirmación y si hay otros ejemplos que muestren lo contrario, entonces es bastante justo.


El problema es que si mantenemos las cosas lo más simples posible, terminaríamos con una relación de subtipo que es paradójica y no encaja en el lenguaje.

Eso podría ser cierto, supongo que solo me preocupa si la alternativa es posible de implementar. Por lo que vale, si la solución más compleja funciona, ¡sería genial!

Tener la noción más general de subtipificación que muestra la existencia de una función de mapeo parece difícil de implementar en general. ¿Mi siguiente ejemplo está interpretando sus reglas correctamente?

(b) Para cada parametrización legal T₁, T₂, ... de ~ A, existe una parametrización S₁, S₂, ... de ~ B tal que A

¿X sería un subtipo de Y en el siguiente caso, dado un mapeo de F (A, B) = (número, B)?

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

Sin embargo, X<string,number> no sería un subtipo de Y<string,number> .

Supongo que no tengo claro si la _existencia_ de un mapeo es suficiente. Si tomamos ~ A y ~ B como funciones, y queremos mostrar que ~ B se aproxima a ~ A, o ~ A es un subtipo de ~ B, entonces mostrar que hay alguna función ~ C, tal que ~ A es un subtipo de (~ B. ~ C), no es suficiente, creo (C es el mapeador). Tengo que ser el caso de _todas_ las asignaciones.

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

No sigo este ejemplo, ¿no debería ocurrir el error aquí? Mi lectura de estos es que id1 debería tener una entrada construida por la función P que dé un PromiseLike para todas las _inputs_. Mientras que id2 está hablando de un valor que debe ser un subtipo de aplicar Promise a A []. No estoy seguro de si es posible recuperar la información necesaria para id1 , del tipo id2 . Aunque creo que podría estar malinterpretando tu punto.

Estos tipos de repente serían diferentes, donde actualmente son los mismos

Una vez más, me temo que podría estar perdiendo su punto, pero no sé en qué se parecen. No puedo reemplazar RegularNumber con GenericNumber en un tipo, tendría que darle un argumento a este último.

Supongo que no tengo claro si la existencia de un mapeo es suficiente. Si tomamos ~ A y ~ B como funciones, y queremos mostrar que ~ B se aproxima a ~ A, o ~ A es un subtipo de ~ B, entonces mostrar que hay alguna función ~ C, tal que ~ A es un subtipo de (~ B. ~ C), no es suficiente, creo (C es el mapeador). Tengo que ser el caso para todas las asignaciones.

Sí, tiene razón, y también el contraejemplo que proporcionó. Encontré otros contraejemplos. No funciona en absoluto.

He vuelto a leer este hilo y muchas de tus respuestas. Creo que tienes razón en muchas cosas y he estado viendo el problema de una manera incorrecta. Llegaré a lo que quiero decir.

No estoy seguro de estar de acuerdo en que sería ambiguo si siempre asume el tipo más alto de restricción hasta que necesite que sea más bajo. Esto no es una afirmación y si hay otros ejemplos que muestren lo contrario, entonces es bastante justo.

Es ambiguo o se vuelve imposible hacer referencia a algo. Como en el ejemplo anterior, el constructor de tipos de Foo vuelve imposible de referenciar porque está oculto por el tipo en sí. Si escribe ~Foo o para el caso Foo<*> o ~<A>Foo<A> o cualquier otra cosa que no entre en conflicto con otras cosas, no tendría este tipo de problema.

Sí, puede solucionarlo definiendo un alias, aunque no es muy bonito:

type Foo2<T> = Foo<T>

Como dije, no creo que esta sea la preocupación más importante.

No sigo este ejemplo, ¿no debería ocurrir el error aquí? Mi lectura de estos es que id1 debería tener una entrada construida por la función P que dé un PromiseLike para todas las entradas. Mientras que id2 se refiere a un valor que debe ser un subtipo de aplicar Promise a A []. No estoy seguro de si es posible recuperar la información necesaria para id1, del tipo de id2. Aunque creo que podría estar malinterpretando tu punto.

Esa es la lectura correcta, sí. Pero si P extends Promise<A[]> , debería poder asignarse a cualquier lugar que acepte un Promise<A[]> , como id1 . Así es en este momento y lo que significa el subtipo.

Realmente no creo que pueda evitarse más.

Una vez más, me temo que podría estar perdiendo su punto, pero no sé en qué se parecen. No puedo reemplazar RegularNumber con GenericNumber en un tipo, tendría que darle un argumento a este último.

Lo que quise decir es esto: el tipo GenericNumber<T> , para todos T , y el tipo RegularNumber son idénticos e intercambiables. No hay contexto en el que uno escriba check y el otro no. Ahora mismo, al menos.

De lo que hemos estado hablando los haría diferentes. Debido a que GenericNumber<T> es de un TC, sería asignable en lugares donde RegularNumber no podría ser. Por lo que ya no sería intercambiable.

He pensado en esto y supongo que puede ser inevitable y no necesariamente malo. Solo un comportamiento nuevo y diferente.

Una forma de pensarlo es que el parámetro de tipo se convierte en parte de la "estructura" del tipo.

Creo que los TC darán lugar a comportamientos más diferentes.

Nueva dirección

En primer lugar, creo que tiene razón en que la relación de subtipo correcta es la que no tiene el mapeo:

Mi primera idea fue la siguiente. Para que ~A sea ​​un subtipo de ~B :

  1. (a) Deben ser del mismo tipo (en términos de aridad, no de restricciones).
  2. (b) Para cada parametrización legal T₁, T₂, ... de ~A , A<T₁, T₂, ...> debe ser un subtipo de B<T₁, T₂, ...> .

Lo del mapeo ... honestamente, es bastante estúpido. No creo que haya ninguna forma de unificar más MyPromise<T> extends Promise<T[]> y ~Promise . Me encantaría saber si alguien piensa lo contrario.

También me encantaría saber si me falta algún ejemplo en el que ni siquiera esta regla funcione.

Si estamos de acuerdo en que las restricciones del constructor de tipos deben expresarse utilizando una relación de subtipo, que parece funcionar muy bien, podemos pasar a otras cosas.

Sobre la sintaxis

Parece que no estamos realmente de acuerdo sobre este tema. Prefiero una sintaxis de prefijo similar a ~Promise . Conceptualmente, el ~ puede verse como una "referencia al operador TC de" o algo así.

Creo que he dado varias razones por las que es mejor que las alternativas:

  1. Totalmente inequívoco.

    1. Los errores relacionados con esta sintaxis también son inequívocos. Si alguien se olvida de parametrizar un tipo, no obtendrá errores sobre los constructores de tipos cuando no sepan cuáles son.

    2. Como efecto secundario, no será necesario modificar la lógica y los textos de error existentes. Si alguien escribe Promise el mensaje de error será exactamente el mismo que ahora. No tendrá que cambiar para hablar de CT.

  2. Se extiende bien a una sintaxis estructural.
  3. Fácil de analizar, creo. Se supondrá que cualquier ~\w aparezca donde se espera un tipo indica una referencia a un TC.
  4. Fácil de escribir.

Espero que otras personas puedan opinar.

Acerca de las uniones, las intersecciones y los parámetros de tipo predeterminados

¿Son legales los tipos sobrecargados / mixtos de las formas * & (* => *) , * | (* => *) , etc.? ¿Tienen usos interesantes?

Creo que son una mala idea y es difícil razonar sobre ellos. Tampoco estoy seguro de qué tipo de anotación de tipo necesitaría para eliminar la ambigüedad de * | (* => *) para poder construir un tipo a partir de ella.

Una forma en que se puede decir que existen estos tipos en este momento son los tipos con parámetros de tipo predeterminados:

type Example<A = number> = {}

Se puede decir que este tipo tiene el tipo * & (* => *) porque puede aceptar un parámetro de tipo para construir un tipo, pero no es necesario.

Creo que los parámetros de tipo predeterminados deberían ser una forma de taquigrafía, no una forma de describir tipos. Así que creo que los parámetros de tipo predeterminados deberían ignorarse al determinar el tipo de tipo.

Sin embargo, puede tener sentido hablar de tipos como ~Promise | ~Array . Tienen el mismo tipo, por lo que no son incompatibles. Creo que esto debería ser apoyado.

Cosas con las que tendrás que lidiar

Deberán tratarse situaciones relacionadas, como esta situación:

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

Pero esto realmente no involucra el tipo (* => *) | (*, *) => * , sino algo diferente

¿Está construyendo otras CT?

Como mencioné antes, no creo que sea una buena idea tener TC que construyan otros TC, como * => (* => *) . Son la norma en los lenguajes que admiten currying y similares, pero no en TypeScript.

No hay forma de que pueda ver para definir tales tipos usando la sintaxis ~ y la relación de subtipo, por lo que no requeriría ninguna regla especial para prohibirlos. Requeriría reglas especiales para que funcionen.

Supongo que podría decirse que podría definirlos estructuralmente así:

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

Esa es prácticamente la única forma en que pienso.

Interacción con funciones de rango superior?

Existe una interacción natural pero compleja con los tipos de función que toman parámetros de tipo:

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

¿Debería detenerse esta interacción de alguna manera? Puedo ver que tipos como estos se vuelven muy complicados.

En general, ¿hay lugares en los que los parámetros de TC no deberían aparecer?

¿Mi sintaxis estructural es una buena idea?

No creo que deba implementarse de inmediato, pero sigo pensando que mi sintaxis estructural es una buena idea. Te permite:

  1. Utilice identificadores locales como otros parámetros de tipo en sus restricciones.
  2. Aplique parcialmente los constructores de tipos, de una manera muy explícita y flexible: ~<A>Map<string, A> , ~<A, B>Map<B, A> , y así sucesivamente.
  3. Todos los demás aspectos de un tipo tienen una sintaxis estructural, por lo que los TC también deberían tener dicha sintaxis.

Dicho esto, los CT pueden funcionar totalmente sin esto, y el primer RP probablemente no los involucre.

Interacción con tipos condicionales

¿Cómo funcionará la característica con tipos condicionales? ¿Deberías poder hacer esto?

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

Yo mismo no estoy del todo seguro. Todavía no he asimilado completamente los tipos condicionales.

Resolución de sobrecarga

Tengo la sensación de que este será difícil de hacer. En realidad, esto es bastante importante porque diferentes formas de resolución de sobrecarga terminarían construyendo diferentes tipos.

Dicho esto, no puedo dar buenos ejemplos en este momento.

Sabes, mucho de esto habría sido un punto discutible si se hubiera utilizado un lenguaje intermedio bien definido para describir TypeScript como punto de partida. Por ejemplo: System F <: o uno de los sistemas de tipo dependiente del sonido, como ML dependiente simplificado .

Me sorprendería honestamente si esto se resuelve antes
https://github.com/Microsoft/TypeScript/issues/14833

Siento que el # 17961 probablemente podría resolver esto indirectamente. Vea esta esencia para más detalles.

Tenga en cuenta que los tipos Bifunctor y Profunctor son un poco complejos en el nivel de restricción; sería mucho más simple si tuviera tipos universales obvios con los que trabajar, en lugar de los infer T que está puramente limitado a tipos condicionales. Además, sería bueno si pudiera haber usado this como el tipo de "retorno" (que es puramente a nivel de tipo) - eso hubiera hecho que la mayoría de mis interfaces fueran más fáciles de definir.

(No soy un gran usuario de TS, por lo que es posible que haya cometido errores. @ Tycho01 ¿Podrías echarle un vistazo para ver si metí la pata en algún lugar del lío de tipos? La razón por la que pregunto es porque tú eres el el PR anterior, y he visto algunos de sus otros experimentos y utilidades).

@isiahmeadows @ tycho01 Wow ...

Tienes razón. Si lo entiendo correctamente, tiene prácticamente los mismos resultados.

Hay algunas diferencias. Pero funcionalmente, son prácticamente idénticos y creo que estas diferencias se pueden resolver.

No es posible inferir la función de tipo correcta

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

Aquí puede inferir ~Promise y ~Bluebird partir de p . Sin embargo, si lo hace así:

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

Dudo mucho que esto funcione:

example(null as Promise<number>)

No hay forma de inferir que F esté destinado a ser:

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

Porque esta función no se considera especial de ninguna manera. Mientras que con los TC, algunos tipos tienen esencialmente una función de nivel de tipo "implícita": su TC.

No es posible hacer referencia a los TC existentes fácilmente

No puedes hacer ~Promise como en mi propuesta. Tendría que codificar el tipo directamente, usando un formato estructural:

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

Es cierto, y eso es motivo de preocupación. Eso es más un problema de inferencia de tipos en el que necesita la capacidad de inferir la función genérica en sí a partir de un tipo de argumento conocido (lo contrario de lo que generalmente sucede). Se puede resolver de una manera lo suficientemente general como para funcionar en la mayoría de los casos, pero requiere un nuevo caso especial que no sea trivial.

Podría resolverse en parte mediante el uso estratégico de NoInfer<T> , pero no estoy 100% seguro de cómo debería hacerse, y cuánto podría abordar incluso el caso común.

@GregRos

No estoy muy a favor de ninguna sintaxis, es más solo mi preferencia, hay muchos méritos en ~ . Creo que lo principal a considerar sería si se requiere sintaxis para anotaciones de tipo explícitas porque la inferencia no siempre es posible.


Lo del mapeo ... honestamente, es bastante estúpido. No creo que haya ninguna forma de unificar MyPromiseextiende promesa

Creo que el mapeo aún podría ser una noción útil, pero en el caso anterior, no creo que debamos intentar unificar ~MyPromise con ~Promise , lo que necesita ser unificado es ~MyPromise y ~<T>Promise<T[]> , que también podríamos escribir ~(Promise . []) . Creo que lo que faltaba es que el mapeo debe ser parte de la relación de subtipo: es una parte tan importante del constructor como Promise . En ese ejemplo, el mapeo es solo el constructor de listas.

interface A<T> {
    x: T;
} 

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

¿Extiende ~<T>B<T> ~<T>A<T[]> ? Si. ¿Extiende ~<T>B<T> ~<T>A<T> ? No. Pero, en última instancia, son dos cuestiones no relacionadas.

Si estamos de acuerdo en que las restricciones del constructor de tipos deben expresarse utilizando una relación de subtipo, que parece funcionar muy bien, podemos pasar a otras cosas.

Sí, creo que parece una buena forma de describir las cosas.


No es posible inferir la función de tipo correcta

function example<~P extends ~PromiseLike>(p : P<number>) : P<string>;
Aquí puede inferir ~Promise y ~Bluebird de p.

Esta no es una afirmación, más una pregunta abierta, ya que no estoy del todo seguro de cómo funciona la verificación de tipos. Pensé que, usando la interfaz A anterior como ejemplo, los tipos A<number> y {x: number} son indistinguibles y, por lo tanto, no estoy seguro de que sea posible inferir el constructor del tipo devuelto por una aplicación del constructor. ¿Sería posible recuperar P de P<number> ? Estoy seguro de que las cosas podrían cambiarse para respaldar esto, solo me pregunto qué hace ahora.

Respuesta cruzada de # 17961, pero no estoy seguro de cómo hacer que el enfoque de @isiahmeadows funcione, desafortunadamente. Me temo que la inferencia hacia atrás en las llamadas de tipo no es trivial.

Entonces, parece que basándonos en una entrada Promise<number> o Bluebird<number> queremos poder inferir versiones no aplicadas de estos tipos de modo que podamos volver a aplicarlas con, por ejemplo, string . Aunque suena difícil.

Incluso si los tipos de entrada son así en lugar de algún equivalente estructural (somos un lenguaje de tipo estructural, ¿verdad?), Este razonamiento también se vuelve turbio si, por ejemplo, Bluebird tuviera dos tipos de parámetros en su lugar, en cuyo punto nuestro <string> posible que la aplicación param
No estoy seguro de una buena solución. (descargo de responsabilidad: me he retrasado un poco en la discusión aquí).

@ tycho01 ¿Todos estos problemas desaparecerían si la gente T ?

Creo que es razonable, dado que dudo que la inferencia pueda resolverse para todos los casos de todos modos.

@ jack-williams: no con el # 17961 hasta ahora, pero creo que usarlo para el envío podría ayudar:

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 Sí, me di cuenta de que mi sugerencia era terrible porque T no se instancia en las llamadas al método.

¿Funcionaría algo como lo siguiente?

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 Supongo que la pregunta debería ser cómo se compararía en comportamiento con la implementación de ADT en fp-ts , pero parece que podría funcionar, sí. Probablemente también sin el TyCon .

@ jack-williams @isiahmeadows :

Probé la idea en el flujo de prueba , ya que ya tiene $Call disponibles. Sin embargo, para mí parece que no responde de alguna manera ...

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 Creo que no puedes simplemente obtener propiedades con $ElementType de this en flujo

@ tycho01 en realidad no puede hacer que esto funcione en mecanografiado también
zona de juegos: https://goo.gl/tMBKyJ

@goodmind : hm, parece que infiere Maybe<number> lugar de Functor<number> después de copiar fmap de Functor a Maybe .
Con las llamadas de tipo, supongo que eso mejoraría para tener solo el tipo allí en lugar de necesitar la implementación en tiempo de ejecución para el tipo.
Ahora, los functors ya necesitarían sus propias implementaciones fmap . Sin embargo, eso sería una mierda para los métodos derivados .
Volver al punto de partida. : /

Estoy planeando lanzar una versión alfa lo antes posible, pero puedes seguir conmigo escribiendo los ejemplos en ese número para familiarizarte.

Este problema en particular es un poco largo para analizarlo por completo, pero lo que estoy buscando es contenido simple, pero ejemplos reales de código que no puede escribir debido a la falta de tipos genéricos parametrizados. Creo que puedo escribir la mayoría de ellos (siempre que no se basen en constructores abstractos de tipo elevado). No dude en abrir problemas en el repositorio anterior con código y los escribiré por usted si puedo (o puede publicarlos aquí también).

Atención, he comenzado un intento de implementar esto en el # 23809. Todavía está muy incompleto, pero compruébalo si estás interesado.

Les prometí un ejemplo simple, aquí está. Esto usa algunos trucos que aprendí al escribir mi biblioteca.

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

Actualicé y simplifiqué la muestra, aquí también hay un enlace al patio de juegos:
Patio de recreo

Agregué una biblioteca NPM para lo anterior, para que pueda trabajar con ella más fácilmente. Actualmente en alfa hasta que obtenga las pruebas adecuadas, pero debería ayudarlos a intentar escribir HKT.

Enlace de Github

He estado jugando con un enfoque simple para simular HKT mediante el uso de tipos condicionales para sustituir variables de tipo virtual dentro de un tipo saturado:

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

Proyecto aquí: https://github.com/pelotom/hkts

¡Comentarios bienvenidos!

@pelotom Me gusta la ligereza de la sintaxis de tu enfoque. Hay otros dos enfoques que aún no se han mencionado en este hilo y que podrían despertar algo de creatividad en la forma en que se producen las soluciones actuales y futuras. Ambas son soluciones orientadas a objetos para este problema.

  1. Bertrand Meyer describió una forma de simular tipos genéricos en su libro de 1988 "Construcción de software orientado a objetos".

Los ejemplos están en Eiffel, pero una traducción aproximada a TypeScript se ve así:

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

Notará que pueden volverse bastante pesados ​​debido a la necesidad de representaciones de clases intermedias, pero con una forma de fábrica de clases o con el enfoque de TypeScript Mixin, esto podría reducirse significativamente.

Puede haber alguna aplicabilidad para # 17588

  1. El segundo enfoque se utiliza al simular álgebras de objetos (y fábricas abstractas):

C<T> está representado por App<t,T> donde T es la clase y t es una etiqueta única asociada con C

interface App<C,T> {}

Muestra:

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

Puede ver más detalles y justificación en el siguiente documento en la sección 3.5 "Emulación del polimorfismo del constructor de tipos":

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

@metaweta , ¿puede cambiar el nombre de este problema a Higher kinded types in TypeScript para que tenga una mejor visibilidad en la búsqueda de Google, por favor?

¿Quizás nuestros sabios y benevolentes mantenedores de repositorios (por ejemplo, @RyanCavanaugh , @DanielRosenwasser ) podrían editar el título, si dicho cambio se considera digno de su intervención?

Tengo curiosidad por saber qué significa que esto se haya trasladado de la comunidad a la acumulación. ¿Es esto algo que el equipo central está considerando más seriamente ahora o simplemente significa que el equipo ha decidido que este no es un buen candidato para la comunidad?

Lo encontré: el hito de la "Comunidad" aparentemente está en desuso en favor del "Backlog" , por lo que este problema probablemente se migró de la misma manera.

No es un miembro de TS, solo alguien que decidió hacer clic en el enlace donde se volvió a marcar.

+1

Aquí hay algo que estaba tratando de construir que parece un caso realmente práctico para tipos de clase superior.

Quiero crear una abstracción para una base de datos que se pueda ejecutar de forma sincrónica o asincrónica. En lugar de usar devoluciones de llamada y piratear eso, quería usar un genérico. Esto es lo que me gustaría hacer:

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

¡Esto sería realmente poderoso!

@ccorcos eso se llama estilo MTL. Puede echar un vistazo a https://github.com/gcanti/fp-ts/blob/master/tutorials/mtl.ts para ver un ejemplo funcional puro con fp-ts .

@mlegenhausen Lo siento, pero me cuesta mucho seguir ese ejemplo.

Cada vez que indago en fp-ts , me preocupa que las cosas se pongan tan complicadas que se vuelvan frágiles. Aunque el ejemplo de

¿Alguna razón por la que esto no se adopta en TypeScript?

@ccorcos En mi fp-ts , no recomendaría MTL / estilo sin etiquetas en absoluto. Agrega una capa adicional de abstracción a cada mónada eficaz que necesita administrar manualmente porque el mecanografiado no es capaz de detectar qué mónada desea usar y aquí es donde las cosas se complican. Lo que veo en la comunidad fp-ts es usar una mónada asíncrona (recomendaría TaskEither ) y seguir con ella. Incluso al probar, los beneficios de MTL no merecen la molestia que genera el código que no es de prueba. hyper-ts basado en fp-ts es un ejemplo de una biblioteca que dejó de admitir MTL recientemente.

Interesante ... hyper-ts ve realmente genial ...

Se me ocurrió una codificación de tipo ligero de tipo superior basada en el polimorfismo limitado por F: https://github.com/strax/tshkt

El beneficio de este enfoque es que no necesita una tabla de búsqueda (un tipo condicional único o un objeto con claves de cadena) para asociar constructores de tipos a tipos. La técnica también se puede utilizar para codificar la aplicación de funciones genéricas de nivel de tipo (piense en ReturnType<<T>(value: T) => Array<T>> ).

Sigue siendo una prueba de concepto, por lo que se agradecen enormemente los comentarios sobre la viabilidad de este enfoque.

Le echaré un vistazo a @strax , ¡se ve realmente genial!

Mientras tanto, aquí hay un ejemplo tonto de lo que podemos hacer ahora:

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

Me gustaría agregar índices de De Bruijn, ya que eso significaría que ya no necesitamos las interfaces, pero creo que requeriría algunas matemáticas de tuplas y estoy tratando de evitar eso.

Propuesta

Orden superior, Lamda, tipos de referencia

Pasar un tipo como referencia

En pocas palabras, un tipo de referencia o un tipo de orden superior permitiría diferir los parámetros tomados por un tipo para más adelante, o incluso inferir parámetros de tipo (genéricos) posteriormente. Pero, ¿por qué debería importarnos?

Si podemos pasar un tipo como referencia, significa que podemos retrasar que TypeScript evalúe un tipo hasta que decidamos hacerlo. Echemos un vistazo a un ejemplo del mundo real:

Vista previa con tubería

Imagine que está desarrollando un tipo genérico por pipe . La mayor parte del trabajo consiste en comprobar que las funciones que se van a canalizar son efectivamente canalizables, de lo contrario, le daríamos un error al usuario. Para hacerlo, usaríamos un tipo mapeado para canalizar los tipos de funciones como pipe(...) hace:

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

Ahora, solo tenemos que repetir esto sobre las funciones con un tipo mapeado:

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

( ver la implementación completa )

Ahora podemos canalizar funciones juntas y TypeScript puede darnos advertencias:

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
)

¡Funciona! tenemos un error adecuado:

El argumento de tipo '(mensaje: objeto) => booleano' no se puede asignar al parámetro de tipo '(arg: cadena) => booleano'.

El problema

Pero hay un problema. Si bien esto funciona muy bien para operaciones simples, encontrará que falla completamente cuando comienza a usar genéricos (plantillas) en las funciones que le pasa:

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

En ambos casos, TypeScript perdió la pista de los tipos de funciones.
> Aquí es donde entran en juego los tipos de orden superior <

Sintaxis

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

En resumen, inferimos manual y dinámicamente los genéricos con * . De hecho, el uso de * aplazó la evaluación de los genéricos. Entonces, * tiene diferentes comportamientos, dependiendo del contexto. Si * está en un tipo que:

  • puede recibir genéricos : se hace cargo de sus genéricos / obtiene su referencia
  • es un genérico en sí mismo : difiere la evaluación hasta que se conoce la referencia. Para esto, se construye un árbol de referencia desde el padre (s) hacia abajo. En otras palabras, las referencias se pueden anidar. Esto es exactamente lo que sucede arriba con Return<*(Fns[Pos<Prev<IterationOf<K & string>>>])> que se asignó a T . En este contexto, podemos decir que * "protege" contra una evaluación inmediata.
  • no es ninguno de los anteriores : no hace nada, se resuelve en el mismo tipo
type  piped  =  Piper<[
    (a:  string) =>  string,
    <B>(b:  B) =>  B
    <C>(c:  C) =>  C
]>
// [
//     (a:  string) =>  string,
//     (b:  string) =>  string,
//     (c:  string) =>  string
// ]

Por lo tanto, TypeScript debe comenzar / continuar evaluando solo si se ha proporcionado el genérico y bloquear la evaluación cuando sea necesario (genérico incompleto). Por el momento, TS evalúa de una sola vez al convertir los genéricos en tipos unknown . Con esta propuesta, cuando algo no se puede resolver:

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

Detalles

* recupera una referencia a un tipo, lo que permite manipulaciones en sus genéricos. Por lo tanto, colocar el comodín delante de un tipo recupera una referencia a él:

*[type]

Recuperar una referencia a un tipo habilita automáticamente la manipulación de genéricos:

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

Los genéricos solo son consumidos / establecidos por el tipo de destino si puede serlo. Entonces haciendo esto:

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

Pero también podría ser verificado por el propio TypeScript, si debería mostrar una advertencia o no. Pero internamente, TS no debería hacer nada al respecto.

También pensé que era una buena idea usar * ya que puede simbolizar un puntero a algo (como en los lenguajes C / C ++), y TypeScript no lo toma prestado.

Tipos de órdenes superiores

Ahora que hemos visto cómo funciona en su forma más básica, me gustaría presentar el concepto central: tipos lambda . Sería bueno poder tener tipos anónimos, similares a devoluciones de

El ejemplo anterior muestra cómo asumir los genéricos de una función. Pero como estamos hablando de referencias, cualquier tipo se puede utilizar junto con * . En pocas palabras, una referencia de tipo es un tipo que podemos pasar pero que aún no ha recibido sus genéricos:

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

Tipos de tipo superior

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

Términos de búsqueda

mayor #orden #tipo #referencias #lambda #HKTs

@ pirix-gh, si lee solo algunos mensajes, puede ver que mucho de lo que pide ya es posible o ya se ha solicitado.

Los leí, pensé que podría resumir mis ideas como todos los demás (para una solución todo en uno), principalmente sobre la sintaxis.

Edité la propuesta anterior para una mejor explicación de cómo podríamos encadenar referencias, y arreglé la forma en que un tipo como Pipe funcionaría con él (hubo algunos errores con respecto a la lógica).

¿Cualquier actualización?

¿Aún no hay actualizaciones? En mi opinión, este problema se ubica como el principal obstáculo para que TypeScript alcance su máximo potencial. Hay tantos casos en los que trato de escribir mis bibliotecas correctamente, solo para rendirme después de una larga lucha, dándome cuenta de que me he encontrado con esta limitación nuevamente. Es omnipresente, apareciendo incluso en escenarios aparentemente muy simples. Realmente espero que se aborde pronto.

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

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

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

type sn = (tmp: string) => number

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

Aún queda mucho trabajo por hacer, como: arreglar información rápida, inferir typeParameter, agregar mensaje de error, resaltar el mismo typeConstructor .....
Pero empieza a funcionar, y esto es lo que puedo conseguir por ahora.
La interfaz de ejemplo es de @millsp https://github.com/microsoft/TypeScript/issues/1213#issuecomment -523245130, la conclusión es realmente muy útil, muchas gracias por eso.

Espero que la comunidad pueda proporcionar más casos de usuarios como ese, para verificar si la forma actual funciona para la mayoría de las situaciones.

También sería bueno proporcionar información sobre HKT / programación de funciones / lambda (cuando digo lambda , me refiero a las matemáticas, solo pude encontrar ejemplos escritos por algún lenguaje, sin matemáticas)

Aquí hay cosas que me ayudan mucho:

@ShuiRuTian Con respecto a que m.join(q) devuelve Set<unknown> , supongo que --noImplicitAny hace que eso también emita una advertencia.

Espero que la comunidad pueda proporcionar más casos de usuarios como ese, para verificar si la forma actual funciona para la mayoría de las situaciones.

También sería bueno proporcionar información sobre HKT / programación de funciones / lambda (cuando digo lambda , me refiero a las matemáticas, solo pude encontrar ejemplos escritos por algún lenguaje, sin matemáticas)

Sin ir más lejos, recientemente intenté crear una función genérica filter curry, y quería que hiciera algo como esto:

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

Y no tengo una forma de hacerlo, porque necesito una forma de decirle a TypeScript "devuelve el mismo tipo que recibiste, pero con este otro parámetro". Algo como esto:

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

@lukeshiru sí, esencialmente esto es https://pursuit.purescript.org/packages/purescript-filterable/2.0.1/docs/Data.Filterable#v : filter
Hay muchos otros casos de uso similares para HKT en TypeScript.

@isiahmeadows Lo probé. Tienes razón.
@lukeshiru y @raveclassic ¡Gracias! Esta característica parece bastante razonable. Echaría un vistazo a esto después de leer https://gcanti.github.io/fp-ts/learning-resources/

Estoy atascado y no sé cuál es la ⸘ solución alternativa‽ actual ...
Estoy tratando de implementar la especificación Chain :

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

Un valor que implementa la especificación Chain también debe implementar la especificación Apply.

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

Hice un FunctorSimplex que luego se extiende en FunctorComplex luego se extiende en Apply pero ahora que quiero extender Apply como Chain se está rompiendo ...

Entonces lo necesito (imagen a continuación y enlace al código):

(Necesito pasar un tipo al ApType para que en la línea 12 el Apply no esté "codificado" pero sea genérico ... para tomar también cualquier tipo extendido desde un IApply )

Screenshot

Enlace permanente a las líneas 11 a 22 del fragmento de código

mecanografiado
tipo de exportación ApType = ( ap: Aplicar <(val: A) => B>, ) => IApply ;

/ * [...] * /

La interfaz de exportación IApply extiende FunctorComplex {
/ ** Fantasy-land/ap :: Apply f => f a ~> f (a -> b) -> f b * /
ap: ApType ;
}
''

Referenciado en una pregunta de desbordamiento de pila: problema de tipos genéricos de TypeScript: se requiere una solución alternativa

@Luxcium Hasta que TS tenga soporte para tipos de emularlos . Es posible que desee echar un vistazo allí para ver cómo es posible lograr:

Hasta que TS tenga soporte para tipos de tipo superior, solo es posible emularlos. Es posible que desee echar un vistazo allí para ver cómo es posible lograr

Tanques mucho @kapke Probablemente estoy demasiado metido en estos días de FP y dado que en Javascript se puede devolver una función de una función, podemos escribir pseudoFnAdd(15)(27) // 42 Me gustaría ser capaz, con TypeScript, de escribir pseudoType<someClassOrConstructor><number> // unknown pero soy un niño de guiones, no un _ ° algún tipo de persona que estudió durante mucho tiempo en la universidad o algo así ° _

Esta información y conferencias (lecturas) son muy apreciadas ...

Nota: hablo francés, en francés la palabra _lecture (s) _ tiene el significado de _readings_ y no 'una charla enojada o seria dada a alguien para criticar su comportamiento' ...

Probablemente, lo siguiente que se me ocurrió como una solución alternativa simple sin RP no funcionará para todos los casos, pero creo que vale la pena mencionarlo:

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[]
}
¿Fue útil esta página
0 / 5 - 0 calificaciones
bleepcoder.com utiliza la información de GitHub con licencia pública para proporcionar a los desarrolladores de todo el mundo soluciones a sus problemas. No estamos afiliados a GitHub, Inc. o a ningún desarrollador que use GitHub para sus proyectos. No alojamos ninguno de los vídeos o imágenes en nuestros servidores. Todos los derechos pertenecen a sus respectivos propietarios.
Fuente de esta página: Fuente

Lenguajes de programación populares
Proyectos populares de GitHub
Más proyectos de GitHub

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