Typescript: Permitir que as classes sejam paramétricas em outras classes paramétricas

Criado em 19 nov. 2014  ·  140Comentários  ·  Fonte: microsoft/TypeScript

Esta é uma proposta para permitir genéricos como parâmetros de tipo. Atualmente é possível escrever exemplos específicos de mônadas, mas para escrever a interface que todas as mônadas satisfazem, proponho escrever

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

Da mesma forma, é possível escrever exemplos específicos de functores cartesianos, mas para escrever a interface que todos os functores cartesianos satisfazem, proponho escrever

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

Os parâmetros de tipo paramétrico podem receber qualquer número de argumentos:

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

Ou seja, quando um parâmetro de tipo é seguido por um til e um arity natural, o parâmetro de tipo deve ser permitido para ser usado como um tipo genérico com o arity fornecido no resto da declaração.

Assim como é o caso agora, ao implementar essa interface, os parâmetros de tipo genérico devem ser preenchidos:

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

Além de permitir diretamente composições de tipos genéricos nos argumentos, proponho que os typedefs também apóiem ​​a definição de genéricos dessa forma (consulte a edição 308 ):

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

As aridades da definição e do alias devem corresponder para que o typedef seja válido.

Suggestion help wanted

Comentários muito úteis

com HKT as mentalidades podem ser mudadas, hábitos quebrados, gerações perdidas trazidas de volta à vida, seria a maior coisa desde genéricos e nulos explícitos e indefinidos, pode mudar tudo

por favor, considere isso como o próximo grande recurso, pare de ouvir as pessoas que ficam pedindo um cavalo melhor, dê a eles um f * g ferrari

Todos 140 comentários

Para não fazer suposições precipitadas, mas acredito que você está digitando incorretamente. Todos os tipos de parâmetro requerem nomes de parâmetro, então provavelmente você quis digitar

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

ao passo que, agora, map é uma função que leva um mapeador do tipo any (onde o nome do parâmetro é A ) para B .

Tente usar a bandeira --noImplicitAny para obter melhores resultados.

Obrigado, corrigido.

Eu atualizei meu comentário em uma proposta.

: +1: tipo de tipo superior seria um grande bônus para a construção de programação funcional, no entanto, antes disso, eu preferiria ter o suporte correto para função de ordem superior e genérico: p

Quase aprovado.

Gostamos muito dessa ideia, mas precisamos de uma implementação funcional para tentar entender todas as implicações e possíveis casos extremos. Ter um exemplo de RP que atenda a 80% dos casos de uso disso seria uma próxima etapa muito útil.

Quais são as opiniões das pessoas sobre a sintaxe do til? Uma alternativa para T~2 seria algo como

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

que permite a composição direta de genéricos em vez de precisar de aliases 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>;
}

É estranho ter aridade explícita, já que não fazemos isso em nenhum outro lugar, então

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

é um pouco mais claro, porém, sei que outras línguas usam * em contextos semelhantes em vez de ~ :

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

Embora levando esse ponto ao extremo, você pode obter:

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

Também acho que T<~,~> é mais claro do que T~2 . Vou modificar a proposta acima. Não me importa se usamos ~ ou * ; simplesmente não pode ser um identificador JS, então não podemos usar, digamos, _ . Não vejo qual benefício a notação => oferece; todos os genéricos aceitam alguns tipos de entrada e retornam um único tipo de saída.

Uma sintaxe mais leve seria omitir inteiramente a aridade dos genéricos; o analisador descobriria desde o primeiro uso e geraria um erro se o resto não fosse consistente com ele.

Terei prazer em começar a trabalhar na implementação desse recurso. Qual é o fórum recomendado para incomodar os desenvolvedores sobre os detalhes de implementação do transpiler?

Você pode registrar muitos novos problemas para questões maiores com exemplos de código mais envolvidos ou criar um problema de longa duração com uma série de questões conforme avança. Alternativamente, você pode entrar na sala de bate-papo aqui https://gitter.im/Microsoft/TypeScript e podemos conversar lá.

@metaweta alguma notícia? Se você precisar de alguma ajuda / discussão, ficarei feliz em fazer um brainstorm sobre esse assunto. Eu realmente quero esse recurso.

Não, as coisas no trabalho ocuparam o tempo livre que eu tinha para trabalhar nisso.

bump: há uma chance de ver esse recurso já considerado?

https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288 ainda é o estado atual dele. Não vejo nada aqui que nos faça mudar a prioridade do recurso.

Parece-me que isso é útil em muito mais situações do que apenas importar abstrações da teoria das categorias. Por exemplo, seria útil ser capaz de escrever fábricas de módulos que recebessem uma implementação Promise (construtor) como argumento, por exemplo, um banco de dados com uma implementação de promessa conectável:

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

: +1:

com HKT as mentalidades podem ser mudadas, hábitos quebrados, gerações perdidas trazidas de volta à vida, seria a maior coisa desde genéricos e nulos explícitos e indefinidos, pode mudar tudo

por favor, considere isso como o próximo grande recurso, pare de ouvir as pessoas que ficam pedindo um cavalo melhor, dê a eles um f * g ferrari

Sim, esbarrei nisso nos primeiros 15 minutos depois de tentar adicionar tipos à base de código JS existente. Não vou mudar para o TS até ver.

Posso ajudar, na verdade?

Eu me pergunto como isso se relacionaria com # 7848? Eles são muito semelhantes, embora sobre a outra faceta dos tipos de ordem superior.

@ boris-marinov A resposta de Ryan Cavanaugh diz que você pode:

Ter um exemplo de RP que atenda a 80% dos casos de uso disso seria uma próxima etapa muito útil.

Agora eu tenho tempo para implementar um PR tão simples. Espero obter algumas dicas dos desenvolvedores principais, mas não há perguntas até agora - tudo parece bom e compreensível. Rastreará um progresso aqui.

@Artazor Você gostaria de dar uma olhada no cracking # 7848 também? Isso cuida do outro lado do problema, envolvendo genéricos e, IMHO, isso pareceria incompleto sem ele (parâmetros genéricos realmente simplificariam muito o código de nível de tipo).

Eu acho essa proposta absolutamente maravilhosa. Ter tipos de tipo mais alto no TypeScript o levaria a um novo nível, onde poderíamos descrever abstrações mais poderosas do que é possível atualmente.

No entanto, não há algo de errado com os exemplos dados no OP? O A na linha

class ArrayMonad<A> implements Monad<Array> {

não é usado em nenhum dos métodos, pois todos eles têm seus próprios A genéricos.

Além disso, se estiver implementando um functor com map como um método que usa this qual seria a sua aparência? Tipo assim, talvez?

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

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

@paldepind Confira # 7848. Essa discussão é sobre esse caso de uso específico, embora IMHO isso e aquele realmente precise ser mesclado em um único PR.

Quando isso vai pousar? Isso parece meio essencial.

Também vai tornar possível:

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

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

?

@whitecolor Acho que há peixes maiores para fritar no momento, que merecem maior prioridade:

  1. O TypeScript 2.0 RC foi lançado há pouco menos de 2 semanas. Isso vai levar muito tempo por si só.
  2. bind , call e apply , funções JS nativas, não foram digitadas. Na verdade, isso depende da proposta dos genéricos variados . Object.assign também precisa de uma correção semelhante, mas genéricos variados por si só não resolverão isso.
  3. Funções como _.pluck Lodash, métodos de modelos de Backbone get e set , etc. não estão atualmente tipados e corrigir isso basicamente torna o Backbone utilizável com TypeScript de uma maneira muito mais segura. Também pode ter implicações para o React no futuro .

Não que eu não queira esse recurso (eu _adoro_ por ele), só não o vejo como provável em breve.

@isiahmeadows
Obrigado pela explicação. Sim, o terceiro item da lista é muito importante, aguardando https://github.com/Microsoft/TypeScript/issues/1295 também.

Mas espero que para a edição atual talvez em 2.1dev de alguma forma.

Concordo. Espero que possa entrar.

(Polimorfismo de classificação 2, que esta edição deseja, também é uma necessidade para
Usuários do Fantasy Land, para digitar corretamente os vários ADTs dentro dessa especificação.
Ramda é um bom exemplo de uma biblioteca que precisa muito disso.)

Na terça-feira, 6 de setembro de 2016, 11h, Alex [email protected] escreveu:

@isiahmeadows https://github.com/isiahmeadows
Obrigado pela explicação. Sim, o terceiro item da lista é muito importante,
aguardando # 1295 https://github.com/Microsoft/TypeScript/issues/1295
também.

Mas espero que para a edição atual talvez em 2.1dev de alguma forma.

-
Você está recebendo isso porque foi mencionado.

Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -244978475,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AERrBMvxBALBe0aaLOp03vEvEyokvxpyks5qnX_8gaJpZM4C99VY
.

Parece que esse recurso nos ajudaria muito a definir as formas de reação. Por exemplo, você tem struct:

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

Eu tenho um manipulador, que é definido como:

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

esse manipulador passou como adereços para os componentes do React. Além disso, tenho uma função que recebe struct e retorna a mesma estrutura, mas com Handlers em vez de valores:

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

Por enquanto, não consigo digitar essa função corretamente, porque o TS não pode produzir tipo com base na estrutura de outro tipo.

Vaca, eu poderia digitar makeForm com HKT?

Hm, interessante.

Talvez algo assim seja possível:

//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 O ponto mais interessante é esta linha:

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

pode valer a pena mencionar que HKT pode ter sido uma resposta aos chamados tipos parciais (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

Não exatamente. {x: number?} não pode ser atribuído a {x?: number} , porque um
tem a garantia de existir, enquanto o outro não.

Na terça-feira, 11 de outubro de 2016, 09:16, Aleksey Bykov [email protected] escreveu:

pode valer a pena mencionar que HKT pode ter sido uma resposta ao chamado
tipos parciais (# 4889 (comentário)
https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-247721155
):

tipo MyDataProto um: K;
outro: K;
ainda Outro: K;
} tipo Idêntico = a; tipo Opcional = a ?;
= {get (): a;

;
;
;

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -252913109,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AERrBNFYFfiW01MT99xv7UE2skQ3qiPMks5qy4wRgaJpZM4C99VY
.

@isiahmeadows você está certo, no momento não há nenhuma maneira / sintaxe de tornar uma propriedade verdadeiramente opcional com base somente em seu tipo, e isso é uma pena

Ainda outro: seria bom se a propriedade pudesse ser feita readonly . Parece que algum tipo de recurso de macros é necessário.

Apenas jogando isso lá fora ... Eu prefiro a sintaxe * sintaxe ~ . Algo sobre ~ parece tão fora do caminho da perspectiva do layout do teclado. Além disso, não tenho certeza do porquê, mas acho que * parece um pouco mais legível / distinguível com todos os colchetes angulares que estão na mistura. Sem mencionar que pessoas familiarizadas com outras linguagens como Haskell podem associar imediatamente a sintaxe ao HKT. Parece um pouco mais natural.

Eu teria que concordar com a sintaxe * . Primeiro, é mais distinguível,
e, segundo, representa melhor um tipo "qualquer tipo funciona".


Isiah Meadows
[email protected]

No domingo, 6 de novembro de 2016 às 12h10, Landon Poch [email protected]
escrevi:

Apenas jogando isso lá fora ... Eu prefiro a sintaxe * em vez da sintaxe ~.
Algo sobre ~ parece tão diferente do layout de um teclado
perspectiva. Além disso, não tenho certeza do porquê, mas acho que * parece um pouco mais
legível / distinguível com todos os colchetes angulares que estão na mistura.
Sem mencionar que pessoas familiarizadas com outras línguas como Haskell podem
associar imediatamente a sintaxe ao HKT. Parece um pouco mais natural.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -258659277,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AERrBHQ4SYeIiptB8lhxEAJGOYaxwCkiks5q7VMvgaJpZM4C99VY
.

Marco: community ? Qual é o estado atual deste problema / recurso?

@whitecolor o status é DIY (faça você mesmo)

O problema tem o rótulo Accepting PRs . isso significa que as solicitações pull para implementar esse recurso são bem-vindas. Consulte https://github.com/Microsoft/TypeScript/wiki/FAQ#what -do-the-labels-on-these-issues-mean para mais detalhes.

Consulte também https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288

Ok, estou vendo os rótulos, só tenho dúvidas se uma equipe não-TS é capaz de fazer isso.

Agora eu tenho tempo para implementar um PR tão simples. Espero obter algumas dicas dos desenvolvedores principais, mas não há perguntas até agora - tudo parece bom e compreensível. Rastreará um progresso aqui.

@Artazor Você tem alguma sorte com isso?

@raveclassic - tornou-se mais difícil do que parecia, no entanto, ainda espero seguir em frente. Sintaticamente, é óbvio, mas as regras / fases de verificação de tipos não são tão claras para mim quanto eu gostaria -)

Vamos tentar reviver minha atividade -)

Apenas acompanhando um progresso e o caminho de desenvolvimento da ideia. Eu considerei três opções de como implementar esse recurso.

Planejei enriquecer uma TypeParameterDeclaration com propriedade opcional higherShape

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

e considerou três opções de como HigherShape poderia ser implementado

1. Arity Simples para o Domínio

type HigherShape = number

corresponde ao seguinte uso:

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

neste caso mais simples, parece que o tipo number seria suficiente. No entanto, devemos ser capazes de determinar um higherShape real para cada tipo dado para ter certeza de que podemos usá-lo como um argumento de tipo para os requisitos de forma específicos. E aqui estamos enfrentando um problema: a forma superior da própria classe Demo não pode ser expressa como um número. Se fosse, então deveria ser representado como 2 - uma vez que tem dois parâmetros de tipo,
e seria possível escrever

var x: Demo<Array, Demo>

e então lutando com o problema de verificação de tipo adiado com a propriedade .both . Assim, o tipo number não é suficiente (eu acredito);

na verdade, o tipo Demo tem o seguinte formato de ordem alta:

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

2. Domínio e co-domínio totalmente estruturados

Então eu investiguei o oposto, a representação mais completa das formas superiores, que permitiria representar formas como a mencionada acima, e ainda pior:

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

A estrutura de dados para isso é direta, mas não interage bem com o sistema de tipos TypeScript. Se permitirmos esses tipos de ordem superior, nunca saberemos se * significa o tipo de base, que poderia ser usado para a digitação de valores. Além disso, eu nem mesmo consegui encontrar uma sintaxe apropriada para expressar tais restrições monstruosas de ordem superior.

3. Domínio Estruturado / Co-Domínio Simples Implícito

A ideia principal - expressão de tipo (mesmo com argumentos de tipo reais) sempre resulta em um tipo de base - que pode ser usado para digitar uma variável. Por outro lado, cada parâmetro de tipo pode ter seus próprios parâmetros de tipo detalhados no mesmo formato que é usado em outro lugar.

Esta foi a minha decisão final que tentaria defender.

type HigherShape = NodeArray<TypeParameterDeclaration>;

exemplo:

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

Aqui, quero observar que M é local para T1<M extends A> extends Z<M> e existe em um escopo de visibilidade mais profundo do que T1. Portanto, M não está disponível no corpo SomeClass .
E * significa simplesmente um novo identificador (tipo anônimo) que nunca entra em conflito com nada (e pode ser implementado em um estágio posterior)


Assim, a assinatura final do TypeParameterDeclaration

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

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

Quer ouvir a opinião de @DanielRosenwasser , @ aleksey-bykov, @isiahmeadows e outros -)

Parece bom para mim, mas sei muito pouco sobre a estrutura interna da base de código do TypeScript.

Gostaria de somar minha voz ao coro solicitando isso e torcer por você, Artazor! :)

Este recurso seria útil para mim em minha implementação de tornar o Redux seguro para tipos.

@michaeltontchev Que problemas você está tendo para tornar o Redux seguro para tipos?

Caso você esteja interessado, publiquei recentemente https://github.com/bcherny/tdux e https://github.com/bcherny/typed-rx-emitter , que se baseia em ideias do Redux e EventEmitter.

Agora olhares, necessidade de rebase ao ramo @rbuckton # 13487 com parâmetros genéricos padrão. Em outro caso, entraremos em conflito amplamente.

@bcherny - obrigado pelos links, vou dar uma olhada!

Eu estava tentando descobrir como tornar combineReducers seguro para tipos, garantindo que tenha um redutor do tipo certo para cada propriedade do estado (sem extras). Consegui fazer isso neste caso específico sem genéricos aninhados, mas uma solução aninhada teria sido mais agradável. Eu tenho o seguinte:

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

Basicamente, o tipo que apresento acima garante que os mapeamentos de redutor que você passa para combineReducers cobrem todas as propriedades do seu estado e têm os tipos de retorno adequados. Não consegui encontrar nenhuma solução enquanto pesquisava online - parece-me que não pode ser feito sem a funcionalidade keyof, que foi lançada há apenas dois meses :)

Espero que o recurso keyof seja útil para ImmutableJs também para tornar os setters e getters seguros para tipos, embora você ainda possa precisar de algumas ferramentas adicionais em torno disso.

Edit: para esclarecer, os genéricos aninhados teriam me permitido não ter que codificar o tipo Redutor no tipo StatePropertyNameAndTypeAwareReducer, mas em vez disso, passá-lo como um genérico, mas precisaria ser um genérico aninhado, o que não é possível em o momento.

Edit2: criou um problema para o Redux aqui: https://github.com/reactjs/redux/issues/2238 Parece que eles não fazem muito TypeScript, então eles estão procurando por pessoas do TypeScript que conheçam Redux para pesar.

Como vão as coisas?

Talvez uma pergunta ingênua, mas por que ~ ou * vez de um parâmetro genérico regular? É para indicar que não é usado? Ou seja, Por que não:

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

Ou:

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

Ou ainda:

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

@bcherny Eu acredito que isso causa ambigüidades na sintaxe, uma vez que Functor<A<T>> significaria anteriormente " A de T ", onde T é algum tipo em local escopo. É improvável, mas essa sintaxe também pode acabar sendo uma alteração importante para algumas bases de código, pelo mesmo motivo.

@masaeedu entendo. A nova sintaxe significa "vincular T lazily", ao invés de "vincular T estritamente no escopo atual".

Dito isso, acho que a proposta de @DanielRosenwasser para T: * => * faz mais sentido aqui, já que há "arte anterior" para ela.

Em Haskell, o operador -> é na verdade um tipo parametrizado (talvez seja mais fácil de visualizar Func<TArg, TRet> ). O construtor de tipo -> aceita dois tipos arbitrários T e U e produz o tipo de um construtor de valor (ou seja, função) que mapeia valores do tipo T para valores do tipo U .

O interessante é que também é um construtor gentil! O construtor de tipo -> aceita dois tipos arbitrários T* e U* (asterisco apenas para distinção visual) e produz o tipo de um construtor de tipo que mapeia tipos do tipo T* para tipos do tipo U* .

Você pode notar um padrão neste ponto. A sintaxe e a semântica sendo usadas para definir e referir-se a tipos estão simplesmente sendo reutilizadas para definir e referir-se a tipos. Na verdade, ele nem mesmo está sendo reutilizado, está apenas definindo implicitamente as coisas em dois universos diferentes ao mesmo tempo. (o fato de ser isomórfico na verdade significa que ele é capaz de definir coisas em níveis infinitos, valores -> tipos -> tipos -> tipos -> ..., exceto para o infeliz * , mas isso é um tópico para um momento diferente)

Na verdade, esse padrão faz tanto sentido que algumas pessoas implementaram uma extensão GHCi amplamente usada que a generaliza para todos os construtores de tipo, não apenas -> . A extensão é chamada de "tipos de dados" e é assim que Haskell vem por suas listas heterogêneas ( [] é tanto o tipo de lista de valores quanto o tipo de lista de tipos), tuplas heterogêneas, "comprimento inteligente "vetores e muitos outros recursos.

Talvez não queiramos ir tão longe quanto DataKinds ainda, então vamos ficar com os construtores do tipo * e -> , mas se seguirmos a sintaxe proposta por Daniel , ou mais geralmente tornar as definições de tipo isomórficas às definições de tipo, nos abrimos para tirar proveito do desenvolvimento futuro nesta área.

Seguindo a minha postagem anterior, gostaria de recomendar que usemos any vez de * ; isso representa o tipo de cada valor e o tipo de cada tipo. Se a sintaxe parecer confusa, poderíamos tirar uma página do livro de Haskell e usar um prefixo ' para eliminar a ambiguidade de tipos e tipos.

O exemplo de OP seria então escrito assim:

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

Nitpick: Acho any confuso em geral no sentido de que faz duas coisas diferentes.
É um supertipo de todos os outros, como nunca é um subtipo de todos os outros, então se uma função solicitar um parâmetro any , você pode inserir qualquer coisa. Por enquanto, tudo bem.
A parte em que fica engraçado é quando uma função pede algo específico e você fornece any . Este tipo verifica, enquanto qualquer outro tipo que seja mais amplo do que o que foi solicitado causaria um erro.
Mas sim, tanto faz.

Por outro lado, ' seria confuso, pois também é usado em literais de string.

@Artazor Alguma novidade com isso? A última vez que você mencionou, é necessário fazer o rebase nos parâmetros genéricos padrão. E parece-me que você é o único perto o suficiente de um POC em funcionamento.

Também vale a pena pensar em como isso interage com a subtipagem. Usar * por si só não é suficiente; em linguagens que usam polimorfismo ad-hoc em vez de polimorfismo limitado, você tem tipos de restrição para limitar argumentos de tipo aceitáveis. Como exemplo, o tipo de Monad T é na verdade Constraint , não * .

No TypeScript, usamos subtipos estruturais, portanto, nossos tipos precisam refletir as relações de subtipo entre os tipos. O artigo do Scala sobre este assunto pode render algumas boas idéias sobre como representar as relações de variância e subtipo em um sistema de espécie: " Rumo a direitos iguais para tipos de espécie superior ".

Algum progresso nisso?

Uma abordagem alternativa por @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe

O problema com a abordagem adotada pelo fp-ts é que ela faz com que você reimplemente bibliotecas comprovadas de outra forma. Para mim, a ideia do typescript é ser capaz de digitar corretamente o que atualmente é considerado as melhores práticas em JavaScript, não forçar você a reimplementá-lo da maneira certa.

Existem muitos exemplos aqui que mostram que os HKT são necessários para descrever corretamente os contratos que usamos atualmente em js libs, seja fantasy land, ramda ou react forms.

Seria muito bom ver isso implementado :)

~ Há alguém disposto / capaz de trabalhar nisso? Sinta-se à vontade para entrar em

Uma abordagem alternativa por @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe

Eu não me preocupei em grocar totalmente isso, porque eu observo que o map resultante ainda especifica explicitamente o tipo de contêiner Option e, portanto, não é totalmente genérico da maneira que os tipos de tipo superior (HKT) pode fornecer:

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

Como @spion observou em 26 de agosto de 2016 , HKT são necessários para tornar genérica qualquer função que precise de uma fábrica e na qual o tipo de contêiner parametrizado seja ele próprio genérico. Exploramos isso em nossas discussões sobre design de linguagem de programação.

PS: Se você está curioso, esse recurso influencia significativamente em minha análise (incluindo @keean ) do cenário da linguagem de programação .

@ shelby3 FWIW Option 's map (in Option.ts ) não é genérico porque representa a instância, enquanto Functor 's map (em Functor.ts ) é genérico porque representa a classe de tipo . Então, você pode definir uma função lift genérica que pode operar com qualquer instância de functor.

Seria muito bom ver isso implementado :)

Concordo plenamente :)

@ shelby3 : para obter um recurso como este mesclado, sua melhor aposta pode ser obter
eles para priorizá-lo no roteiro de TS; Tive alguns PRs que basicamente conseguiram
feedback / mescla quando há pequenas correções ou se eles já estavam procurando
neles. Eu não quero ser negativo, mas é uma consideração se você é
prestes a investir recursos nisso.

Em 8 de janeiro de 2018, às 16h05, "shelby3" [email protected] escreveu:

Há alguém disposto / capaz de trabalhar nisso? Sinta-se à vontade para contactar
me [email protected] para discutir. Ou qualquer um é capaz de treinar alguém
podemos encontrar algo para trabalhar nisso, por favor, me avise.

Uma abordagem alternativa por @gcanti https://github.com/gcanti
https://medium.com/@gcanti/higher -kinded-types-in
typescript-static-and-fantasy-land-d41c361d0dbe

Eu não me preocupei em grocar totalmente isso, porque eu observo o mapa resultante
ainda especifica explicitamente o tipo de recipiente Option e, portanto, não é totalmente
genérico de forma que tipos de tipo superior (HKT) podem fornecer:

mapa de funções (f: (a: A) => B, fa: HKTOption ): Opção { return (fa como opção ) .map (f) }

HKT são necessários para tornar genérica qualquer função que precise de uma fábrica e
em que o tipo de contêiner parametrizado de tipo deve ser genérico. Nós tínhamos
explorou este https://github.com/keean/zenscript/issues/10 em nosso
discussões sobre design de linguagem de programação.

PS: Se você está curioso, esse recurso influencia significativamente na minha
(incluindo @keean https://github.com/keean 's) análise do
paisagem da linguagem de programação
https://github.com/keean/zenscript/issues/35#issuecomment-355850515 . Eu
perceber que nossas visões não se correlacionam inteiramente com as prioridades do Typecript
com o objetivo principal de ser um superconjunto de Javascript / ECMAScript e suporte
esse ecossistema.

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-355990644 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AC6uxYOZ0a8G86rUjxvDaO5qIWiq55-Fks5tIi7GgaJpZM4C99VY
.

@gcaniti pede desculpas pelo barulho e obrigado pela explicação adicional. Eu deveria ter estudado mais antes de comentar. Claro, é meu erro na conceituação porque (eu já sei) um functor requer uma implementação de instância.

Afaics, seu "hack" inteligente permite referir-se a uma fábrica genericamente (por exemplo, lift ), mas requer o clichê adicional de Aumento do Módulo para (atualizar e) especializar cada tipo da fábrica genérica para o tipo especializado do functor , por exemplo, Option . Esse clichê não seria necessário para todo uso genérico de uma fábrica genérica, por exemplo, o sort exemplo genérico @keean e eu discutimos? É possível que haja outros casos esquivos a serem descobertos também?

Kotlin copiou sua ideia ou vice-versa? (algumas críticas adicionais nesse link, mas não sei se se aplicam ao caso do texto datilografado)

Não quero ser negativo, mas é uma consideração se você está prestes a investir recursos nisso.

Sim, esse pensamento também me ocorreu. Obrigado por articular isso. Eu suspeito que uma das considerações seriam os impactos geralmente no sistema de tipos e em quaisquer casos que ele possa gerar, conforme apontado por @masaeedu. Talvez houvesse resistência, a menos que isso fosse muito bem pensado e demonstrado.

Observação: também estou pesquisando o Ceylon para verificar melhor qual será seu nível de investimento no destino de compilação do EMCAScript. (Eu preciso estudar mais).

Eu também fui mordido por essa limitação. Gostaria que I no exemplo a seguir fosse inferido automaticamente:


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

Até onde eu posso dizer, isso requer um tipo de tipo superior ou então a especificação de um parâmetro genérico duplicado (por exemplo, doStuff<User, number>() ) que parece redundante.

Recentemente, também fui atingido por essa limitação.

Tenho trabalhado em uma biblioteca de promessas . Ele fornece várias funções utilitárias para trabalhar com eles.

Uma característica central da biblioteca é que ela retorna o mesmo tipo de promessa que você colocou dentro dela. Portanto, se você estivesse usando uma promessa do Bluebird e chamasse uma das funções, ela retornaria uma promessa do Bluebird com todas as funcionalidades adicionais que eles fornecem.

Eu queria codificar isso no sistema de tipos, mas rapidamente percebi que isso requer trabalhar com um tipo P do tipo * -> * tal que P<T> extends Promise<T> .

Aqui está um exemplo de uma dessas funções:

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

Na situação acima, fui capaz de evitar a necessidade de tipos superiores usando o tipo bastante hacky this .

No entanto, o seguinte caso não pode ser resolvido:

/**
* 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 não há nenhum hack que me permite fazer this<T[]> ou algo assim.

Observe meu pequeno pedido de desculpas na documentação.

Tenho outro cenário em que acredito que esse recurso seria útil (presumindo que interpretei a proposta corretamente), conforme indicado pela referência acima.

No pacote em questão, é necessário ter uma classe ou função genérica não tipificada usada como um tipo, já que os tipos genéricos são normalmente criados pelo usuário.

Aplicando a proposta ao meu cenário, acredito que seria algo como:

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

Visto que não há como referenciar os tipos genéricos de maneira confiável em minha implementação, tenho certeza de que perdi algo.

@ Silic0nS0ldier Na verdade, esse caso pode ser resolvido agora. Você usa um tipo de construtor estrutural, como este:

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

E então diga, component ?: ComponentConstructor .

Ainda mais genericamente, você pode realmente ter um tipo de função genérico:

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

Isso é chamado de polimorfismo paramétrico de classificação n e, na verdade, é um recurso bastante raro em linguagens. Portanto, é ainda mais intrigante saber por que o TypeScript não tem tipos de tipo superior, que é um recurso muito mais comum.

A limitação discutida aqui aparecerá se você precisar fazer referência a um TComponent<T, S> . Mas, no seu caso, isso parece desnecessário.


Você também pode usar typeof Component que fornecerá o tipo de construtor Component mas isso causará vários problemas com os subtipos.

@GregRos Sua solução proposta parecia promissora (ela aceita os tipos dentro do arquivo de definição), mas os tipos compatíveis estão sendo rejeitados. https://gist.github.com/Silic0nS0ldier/3c379367b5e6b1abd76e4a41d1be8217

@ Silic0nS0ldier Veja meus comentários sobre a essência.

@chrisdavies Isso funciona?

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 Sim. Isso funciona para o meu cenário. Eu não tinha encontrado esse exemplo nos documentos (embora seja conhecido por perder coisas!). Obrigado!

Tenho pensado muito sobre esse recurso e tenho algumas ideias sobre o assunto, inclinando-me para algum tipo de especificação, mas ainda posso ver muitos problemas. Minhas sugestões são um pouco diferentes do que foi proposto até agora.


Em primeiro lugar, acho que usar qualquer tipo de sintaxe T<*, *> para construtores de tipo é uma má ideia porque não se adapta bem à complexidade do construtor de tipo. Também não tenho certeza se há um ponto em especificar o tipo de construtor de tipo sempre que ele é referenciado, uma vez que não fazemos isso para funções, mesmo para funções com parâmetros de tipo.

Acho que a melhor maneira de implementar isso é tratar tipos de tipo superior como outros tipos, com nomes regulares, e definir uma boa relação de subtipo sobre os próprios construtores de tipo que podem ser usados ​​para impor restrições.

Eu realmente acho que devemos usar algum tipo de prefixo ou postfix para distingui-los de outros tipos, principalmente para proteger os usuários de mensagens de erro incompreensíveis envolvendo construtores de tipo quando eles estão apenas querendo escrever código normal. Eu meio que gosto da aparência de: ~Type, ^Type, &Type ou algo parecido.

Portanto, por exemplo, uma assinatura de função pode ser:

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

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

(Não estou usando o prefixo ~ para os tipos construídos de propósito)

Ao usar extends aqui, eu disse basicamente duas coisas:

** 1. Se for necessário: ~L é um construtor de tipo que tem o mesmo tipo de ~List , ou seja, o tipo * -> * (ou talvez * => * , uma vez que => é a seta TypeScript).

  1. ~L é um subtipo de ~List . **

Usar extends para denotar o tipo de construtor de tipo escala para construtores de tipo arbitrariamente complexos, incluindo coisas como ((* => *) => (* => *)) => * .

Você realmente não pode ver esse tipo no identificador do tipo, mas não acho que você precise. Eu nem tenho certeza se uma relação de subtipo entre construtores de tipo deve preservar os tipos, então (1) pode não ser necessário.

Sem construção de tipos incompletos

Acho que não devemos apoiar a construção de tipos incompletos. Ou seja, algo assim:

(*, *) => * => *

Acho que criaria mais problemas do que vale a pena. isto é, todo construtor de tipo deve construir algum tipo concreto, e esse tipo concreto deve ser especificado no contexto onde o TC é definido.

Forma estrutural de definir construtores de tipo

Eu também acho que deveria haver uma maneira estrutural de especificar construtores de tipo, da mesma forma que qualquer outro tipo pode ser especificado estruturalmente, incluindo tipos de função genérica de ordem muito alta. Tenho pensado em sintaxe como:

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

Que é semelhante à sintaxe existente para tipos de função com parâmetros de tipo:

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

Os dois podem até ser combinados para obter isso:

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

Que é um construtor de tipo que constrói um tipo de função genérica.

A vantagem é que esses tipos estruturais podem ser usados ​​ao especificar outros tipos estruturais, ao especificar restrições de tipo e assim por diante. Às vezes, isso significa que eles podem usar símbolos locais de referência que não podem ser referenciados de qualquer outro lugar.

Aqui está um exemplo:

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

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

No exemplo acima, o construtor de tipo estrutural ~<A>List<A, B> referência ao parâmetro de tipo B . Não é possível especificar esta relação de outra forma, pelo menos sem codificar o tipo parcialmente construído List<A, *> . Existem outros exemplos também.

A relação de subtipo

A relação do subtipo parece fazer sentido, mas encontrei várias dificuldades ao tentar caracterizá-la.

Minha primeira ideia foi a seguinte. Para ~A ser um subtipo de ~B :

  1. (a) Eles devem ter o mesmo tipo (em termos de aridade, não de restrições).
  2. (b) Para cada parametrização legal T₁, T₂, ... de ~A , A<T₁, T₂, ...> deve ser um subtipo de B<T₁, T₂, ...> .

No entanto, isso tem várias limitações.

  1. classe MySpecialPromise implementa PromiseLike {} Neste caso, ~MySpecialPromise não é um subtipo de ~PromiseLike porque eles têm tipos diferentes.

  2. classe MyArrayPromiseimplementa PromiseLike

    A relação de subtipo também não é conservada neste caso.

Uma versão mais generalizada de (b) é a seguinte:

(b) Para cada parametrização legal T₁, T₂, ... de ~A , existe uma parametrização S₁, S₂, ... de ~B tal que A<T₁, T₂, ...> é um subtipo de B<S₁, S₂, ...> .

Em outras palavras, existe um mapeamento F (T₁, T₂, ...) = S₁, S₂, ... com as propriedades acima. Este mapeamento deve ser usado para construir o B<...> parametrizado a partir de um A<...> parametrizado. Isso pode nos permitir fazer isso mesmo que os construtores de tipo tenham tipos diferentes.

O problema com essa relação é que não tenho certeza de como seria possível encontrar o mapeamento correto. Em idiomas com digitação nominal, cada declaração ao longo das linhas de:

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

Define um mapeamento entre os parâmetros de tipo de ~A e os parâmetros de tipo de ~B , então é assim que o mapeamento pode ser recuperado. No entanto, no sistema de tipagem estrutural do TypeScript, não podemos confiar em instruções explícitas desse tipo.

Uma maneira é oferecer suporte apenas a construtores de tipo para tipos com as informações de tipo corretas, como cláusulas implements ou algum tipo de membro de tipo abstrato semelhante ao Scala. Não tenho certeza se este é o caminho a seguir.

@GregRos - Notas interessantes! Algumas questões.


O que você quer dizer com tipo de concreto? Você quer dizer algo com tipo * , ou um tipo sem parâmetros de tipo de limite?


Sem construção de tipos incompletos
Acho que não devemos apoiar a construção de tipos incompletos. Ou seja, algo assim:
(*, *) => * => *

O que você quer dizer com construir tipos incompletos? Você quer dizer que todo aplicativo como L<A> deve ter o tipo * . O construtor de tipo de par é especial em seu exemplo, por exemplo, (* => *) => * => * estaria certo?


Forma estrutural de definir construtores de tipo

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

Esses exemplos são diferentes, exceto o primeiro sendo anônimo?


A relação de subtipo

~A e ~B não se referem a tipos, então faz sentido que eles tenham uma relação de subtipo? Quando você realmente precisa verificar se um construtor é um 'subtipo' de outro? É possível esperar até que os construtores sejam aplicados e verificar os tipos resultantes?

@ jack-williams Obrigado pelo feedback!

O que você quer dizer com construir tipos incompletos? Você quer dizer que todo aplicativo como L<A> deve ter um tipo *. O construtor de tipo de par é especial em seu exemplo, por exemplo, (* => *) => * => * estaria certo?

Sim, exatamente. Cada aplicativo como L<A> deve ter o tipo * . Não tenho certeza de como estou convencido disso.


Esses exemplos são diferentes, exceto o primeiro sendo anônimo?

O primeiro é uma expressão de tipo, enquanto o segundo é uma declaração. Eles são idênticos em muitos aspectos, da mesma forma que esses tipos são idênticos em muitos aspectos:

{
     a : number;
     b : string;
}

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

A sintaxe tem várias motivações:

  1. Assim como tudo no TypeScript, ele permite que os construtores de tipo sejam especificados estruturalmente e anonimamente.
  2. Uma expressão de tipo (como o objeto anônimo digitado mencionado acima) pode ser usada em certos contextos onde as instruções de declaração não podem ser usadas, como nas assinaturas de tipo de funções. Isso permite que eles capturem identificadores locais e expressem coisas que não podem ser expressas de outra forma.

~A e ~B não se referem a tipos, então faz sentido que eles tenham uma relação de subtipo? Quando você realmente precisa verificar se um construtor é um 'subtipo' de outro? É possível esperar até que os construtores sejam aplicados e verificar os tipos resultantes?

Os construtores de tipo podem ou não ser considerados como tipos. Proponho considerá-los como tipos, apenas incompletos que não têm valores e não podem aparecer em qualquer contexto que requeira o tipo de um valor. Esta é a mesma filosofia adotada por Scala, neste documento

Por relação de subtipo, quero dizer essencialmente algum tipo de relação de "conformidade" que pode ser usada para restringir construtores de tipo. Por exemplo, se eu quiser escrever uma função que funcione em todos os tipos de promessas de vários tipos, como Promise<T> , Bluebird<T> e assim por diante, preciso ter a capacidade de restringir os parâmetros TC com o interface PromiseLike<T> de alguma forma.

A palavra natural para esse tipo de relação é uma relação de subtipo.

Vejamos um exemplo. Supondo que tenhamos trabalhado em uma relação de subtipo entre construtores de tipo, posso escrever uma função como esta:

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

E a restrição ~P extends ~PromiseLike deve garantir que esta é uma função que funciona com promessas, e somente promessas. A restrição também garantirá que dentro do corpo da função, promise será conhecido por implementar PromiseLike<A> e assim por diante. Afinal, os membros reconhecidos pelo TypeScript no corpo da função são precisamente aqueles cuja existência pode ser comprovada por meio de restrições.

Da mesma forma Promise<T> extends PromiseLike<T> , porque eles são estruturalmente compatíveis e podem ser substituídos entre si, ~Promise extends ~PromiseLike porque eles constroem tipos estruturalmente compatíveis e, portanto, podem ser substituídos uns pelos outros.


Para sublinhar o problema com o problema do subtipo, considere mais uma vez:

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

Podemos abstrair ~MyPromise da mesma maneira que abstraímos ~Promise ? Como capturamos a relação entre eles?

O mapeamento de que falei anteriormente é o mapeamento que, dada uma parametrização de ~MyPromise , produzirá uma parametrização de ~Promise modo que o tipo construído por ~MyPromise seja um subtipo de um construído por ~Promise .

Nesse caso, o mapeamento é assim:

T => T[]

@GregRos

Nesse caso, ~MySpecialPromise não é um subtipo de ~PromiseLike porque eles têm tipos diferentes.

Em Haskell, esse tipo de problema é resolvido permitindo a aplicação parcial de tipos e definindo tipos de forma que o parâmetro de tipo final coincida com o parâmetro de tipo de qualquer interface que você esteja implementando.

Em seu exemplo, MySpecialPromise seria definido como MySpecialPromise<TSpecial, TPromiseVal> , e ~MySpecialPromise<SpecialType> teria um tipo idêntico a ~Promise .

@GregRos

Por relação de subtipo, quero dizer essencialmente algum tipo de relação de "conformidade" que pode ser usada para restringir construtores de tipo. Por exemplo, se eu quiser escrever uma função que funcione em todos os tipos de promessas de vários tipos, como Promise, Pássaro azule assim por diante, preciso restringir os parâmetros TC com a interface PromiseLikede algum modo.
function mapPromise<~P extends ~PromiseLike, A, B>(promise : P<A>, func : (x : A) => B) : P<B> ;

Acho que quando se trata de verificar o tipo dessa função, você tentaria unificar BlueBird<T> e PromiseLike<T> para o T escolhido, esses são apenas tipos concretos e se enquadram na subtipagem. Não vejo por que você precisaria de uma relação especial para os construtores ~BlueBird e ~PromiseLike .

Eu acho que seria usado em algo assim?


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;

Aqui você pode querer verificar se as restrições de y implicam nas restrições de x, mas o TypeScript ainda não tem mecanismo para verificar se BlueBird<T> estende PromiseLike<T> que poderia ser usado?

@ jack-williams Tudo se resume a como você especifica a seguinte restrição:

~ P é um construtor de tipo tal que, para todos os A , P<A> é um subtipo de PromiseLike<A> .

Que tipo de sintaxe você usaria? Que tipo de conceito você usaria? Você pode escrever algo assim:

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

Mas essa sintaxe tem limitações. Por exemplo, você não pode expressar esta classe, porque não podemos construir o tipo P<A> no ponto em que é declarado para restringi-lo:

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

Mas acho que você pode usar tipos existenciais para isso, como este:

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

Então, você pode exigir que todos os construtores de tipo sejam restringidos por meio de seus tipos construídos dentro da assinatura da função ou tipo, opcionalmente usando tipos existenciais.

Com os tipos existenciais, isso teria o mesmo poder expressivo de uma relação de subtipo com um mapeamento.

No entanto, isso teria vários problemas:

  1. Especificar construtores de tipo com tipos como ((* => *) => *) => * exigiria a introdução de muitos tipos existenciais, alguns dos quais teriam que ser de ordem superior. Todos eles teriam que aparecer na assinatura da função ou classe.
  2. Não tenho certeza se seria mais fácil encontrar os tipos existenciais em questão do que encontrar o mapeamento.
  3. Acho que é menos elegante do que a relação de subtipo.
  4. Potencialmente, introduz outra forma de tipo com a qual você teria que lidar.

@GregRos

Que tipo de sintaxe você usaria? Que tipo de conceito você usaria?

_Pessoalmente_ eu não usaria nenhuma sintaxe especial e apenas usaria:

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

mas esta é apenas minha opinião, visto que vejo coisas como number como um construtor nulo: portanto, não precisa haver uma distinção.

Minha visão sobre subtipagem para funções de construtor seria mantê-la o mais simples possível. Eles devem ter a mesma aridade e os parâmetros devem ser subespécies um do outro, levando em consideração a contravariância e a covariância, assim como o papel Scala.

A aplicação parcial pode contornar os casos em que eles têm aridade diferente (eu não me importaria de fazer o currying automático para construtores de tipo, então você pode simplesmente escrever MySpecialPromise<SpecialType> ).

No exemplo interface MyPromise<T> extends Promise<T[]> {} eu teria que ser honesto e dizer que não estou convencido de que vale a pena a complexidade de lidar com este caso - acho que seria um recurso útil o suficiente sem ele.

Lidar com esse caso é equivalente a (eu acho), dizendo: ~MyPromise extends ~(Promise . []) onde [] é o construtor da lista e . é a composição do construtor. Parece que as coisas estão ficando muito mais difíceis, pois agora não é suficiente apenas inspecionar a estrutura dos construtores, mas você também deve pensar na composição!

@ jack-williams Isso não funciona com parâmetros de tipo padrão. Se eu escrever P extends Foo , onde Foo tem um parâmetro de tipo padrão, ou seja, type Foo<T = {}> = ... , então qual é o tipo de P ?

Gostaria apenas de dizer que aprovo os tipos de ordem superior (já tive situações em projetos reais do TypeScript em que eles seriam úteis).

No entanto , não acho que eles deveriam apoiar currying. Eu amo Haskell, mas isso simplesmente não se encaixa no TypeScript.

Os tipos de ordem superior são úteis mesmo sem currying ou aplicação parcial, mas se a aplicação parcial for necessária, prefiro ver a sintaxe explícita para ela. Algo assim:

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

@ cameron-martin

Edit: Desculpe, eu não acho que meus comentários _não_ foram muito claros. Por P tem seu próprio tipo, quero dizer que tem um tipo imposto por seu uso. Digamos que as restrições sejam sempre assumidas como o tipo mais alto, então Foo é considerado ~Foo . Somente se forçarmos P a ser um tipo inferior, verificamos se Foo tem um parâmetro padrão. Minha preocupação com isso é uma inferência gentil, mas nesse caso ~ não ajudará e acho que precisamos de anotações completas.

P tem sua própria espécie, não tem? A questão não seria: tratamos Foo como ~Foo ou como Foo<{}> : eu diria que isso seria motivado pelo tipo de P. Então, se P é um tipo que forçamos o parâmetro padrão, e se P é um construtor * => * , então tratamos Foo da mesma forma.

@Pauan Concordo com suas sugestões.

@ jack-williams Eu considerei essa noção de subtipagem, como mencionei antes:

Minha primeira ideia foi a seguinte. Para ~A ser um subtipo de ~B :

  1. (a) Eles devem ter o mesmo tipo (em termos de aridade, não de restrições).
  2. (b) Para cada parametrização legal T₁, T₂, ... de ~A , A<T₁, T₂, ...> deve ser um subtipo de B<T₁, T₂, ...> .

O problema é que se mantivermos as coisas o mais simples possível, acabaremos com uma relação de subtipo que é paradoxal e não se encaixa na linguagem.

Se MyPromise<T> extends Promise<T[]> significa que MyPromise<T> tem que ser utilizável onde quer que Promise<T[]> seja utilizável, mas este não seria mais o caso.

Se você usou as para converter a : MyPromise<T> em Promise<T[]> , você estaria fazendo upcasting, mas isso tornaria, paradoxalmente, a mais atribuível.

As restrições genéricas existentes, que seguem a relação de subtipo existente, também podem ser usadas para obter um efeito semelhante e causar um comportamento estranho:

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

A digitação também se tornaria pelo menos parcialmente nominal como um efeito colateral. Esses tipos de repente seriam diferentes, onde atualmente são os mesmos:

type GenericNumber<T> = number;

type RegularNumber = number;

Eu nem tenho certeza de que efeito isso teria em tipos complexos de união / interseção com parâmetros de tipo, tipos puramente estruturais, tipos com abrangências de membros e assim por diante.

Meu sentimento pessoal é que: A relação de subtipo sobre os construtores de tipo precisa respeitar a existente, não se opor a ela . Infelizmente, isso exige que as coisas sejam mais complexas.


A maior razão para usar algum tipo de notação especial para construtores de tipo é que 99% dos desenvolvedores não sabem o que é um construtor de tipo e não gostariam de ser bombardeados com mensagens de erro sobre ele.

Isso é muito diferente de Haskell, onde cada desenvolvedor é obrigado por lei a fazer um curso avançado em teoria das categorias.

Uma razão secundária é que, em alguns casos (como o caso dos parâmetros padrão mencionado anteriormente), a sintaxe seria ambígua ou não seria possível abstrair sobre um construtor de tipo específico.

EDIT: Desculpe @GregRos , não vi seus últimos comentários!

A relação de subtipo sobre construtores de tipo precisa respeitar o existente, não se opor a ele.

Se isso puder ser alcançado, eu concordo. Eu simplesmente não tenho minha cabeça em torno de todos os detalhes e como isso seria fácil.

Uma razão secundária é que, em alguns casos (como o caso dos parâmetros padrão mencionado anteriormente), a sintaxe seria ambígua ou não seria possível abstrair sobre um construtor de tipo específico.

Não tenho certeza se concordo que seria ambíguo se você sempre assumisse o tipo mais alto de restrição até precisar que fosse mais baixo. Esta não é uma afirmação e se houver outros exemplos que mostram o contrário, então é justo.


O problema é que se mantivermos as coisas o mais simples possível, acabaremos com uma relação de subtipo que é paradoxal e não se encaixa na linguagem.

Isso pode ser verdade, acho que estou apenas preocupado se a alternativa é realmente possível implementar. Por que vale a pena, se a solução mais complexa funcionar, isso seria ótimo!

Ter a noção mais geral de subtipagem que mostra a existência de uma função de mapeamento parece difícil de implementar em geral. Meu exemplo a seguir está interpretando suas regras corretamente?

(b) Para cada parametrização legal T₁, T₂, ... de ~ A, existe uma parametrização S₁, S₂, ... de ~ B tal que A

Seria X um subtipo de Y no seguinte caso, dado um mapeamento de F (A, B) = (número, B).

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

No entanto, X<string,number> não seria um subtipo de Y<string,number> .

Acho que não estou certo se a _existência_ de um mapeamento é suficiente. Se tomarmos ~ A e ~ B como funções, e queremos mostrar que ~ B se aproxima de ~ A, ou ~ A é um subtipo de ~ B, mostrando que há alguma função ~ C, de modo que ~ A é um subtipo de (~ B. ~ C), não é suficiente, eu acho (C é o mapeador). I tem que ser o caso para _todos_ mapeamentos.

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

Não estou seguindo esse exemplo, o erro aqui não deveria acontecer? Minha leitura disso é que id1 deve ter uma entrada construída pela função P que dá PromiseLike para todas as _inputs_. Considerando que id2 está falando sobre um valor que deve ser um subtipo da aplicação de Promise to A []. Não tenho certeza se é possível recuperar as informações necessárias para id1 , do tipo id2 . Eu acho que posso estar entendendo mal o seu ponto, no entanto.

Esses tipos de repente seriam diferentes, onde atualmente são os mesmos

Novamente, temo estar perdendo o seu ponto, mas não sei como eles são iguais. Não posso substituir RegularNumber por GenericNumber em um tipo, eu teria que dar a este último um argumento.

Acho que não tenho certeza se a existência de um mapeamento é o suficiente. Se tomarmos ~ A e ~ B como funções, e queremos mostrar que ~ B se aproxima de ~ A, ou ~ A é um subtipo de ~ B, mostrando que há alguma função ~ C, de modo que ~ A é um subtipo de (~ B. ~ C), não é suficiente, eu acho (C é o mapeador). I tem que ser o caso para todos os mapeamentos.

Sim, você está certo, e também o contra-exemplo que você forneceu. Encontrei outros contra-exemplos. Não funciona de jeito nenhum.

Eu reli este tópico e muitas de suas respostas. Acho que você está certo em muitas coisas e tenho visto o problema de maneira errada. Vou entender o que quero dizer.

Não tenho certeza se concordo que seria ambíguo se você sempre assumisse o tipo mais alto de restrição até precisar que fosse mais baixo. Esta não é uma afirmação e se houver outros exemplos que mostram o contrário, então é justo.

Ou é ambíguo ou então algo se torna impossível de fazer referência. Como no exemplo acima, o construtor de tipo de Foo torna-se impossível de fazer referência porque está oculto pelo próprio tipo. Se você escrever ~Foo ou Foo<*> ou ~<A>Foo<A> ou qualquer outra coisa que não entre em conflito com outras coisas, você não terá esse tipo de problema.

Sim, você pode contornar isso definindo um alias, embora não seja muito bonito:

type Foo2<T> = Foo<T>

Como eu disse, não acho que essa seja a preocupação mais importante.

Não estou seguindo esse exemplo, o erro aqui não deveria acontecer? Minha leitura disso é que id1 deve ter uma entrada construída pela função P que fornece uma PromiseLike para todas as entradas. Considerando que id2 está falando sobre um valor que deve ser um subtipo de aplicação de Promise a A []. Não tenho certeza se é possível recuperar as informações necessárias para id1, do tipo de id2. Eu acho que posso estar entendendo mal o seu ponto, no entanto.

Essa é a leitura correta, sim. Mas se P extends Promise<A[]> deve ser atribuído a qualquer lugar que aceite Promise<A[]> , como id1 . É assim que está agora e o que significa subtipagem.

Eu realmente não acho que isso possa ser mais evitado.

Novamente, temo estar perdendo o seu ponto, mas não sei como eles são iguais. Não posso substituir RegularNumber por GenericNumber em um tipo, eu teria que dar a este último um argumento.

O que eu quis dizer é o seguinte: o tipo GenericNumber<T> , para todos os T , e o tipo RegularNumber são idênticos e intercambiáveis. Não há contexto no qual um digitaria verificar e o outro não. Agora, pelo menos.

O que estivemos falando os tornaria diferentes. Como GenericNumber<T> é de um TC, seria compatível em lugares onde RegularNumber não poderia ser. Portanto, não seria mais intercambiável.

Já pensei sobre isso e acho que pode ser inevitável, e não necessariamente ruim. Apenas um comportamento novo e diferente.

Uma maneira de pensar sobre isso é que o parâmetro de tipo se torna parte da "estrutura" do tipo.

Acho que os TCs levarão a um comportamento mais diferente.

Nova direção

Em primeiro lugar, acho que você está certo em que a relação de subtipo correta é aquela que não tem o mapeamento:

Minha primeira ideia foi a seguinte. Para ~A ser um subtipo de ~B :

  1. (a) Eles devem ter o mesmo tipo (em termos de aridade, não de restrições).
  2. (b) Para cada parametrização legal T₁, T₂, ... de ~A , A<T₁, T₂, ...> deve ser um subtipo de B<T₁, T₂, ...> .

A coisa do mapeamento ... honestamente, é muito estúpido. Não acho que haja mais nenhuma maneira de unificar MyPromise<T> extends Promise<T[]> e ~Promise . Adoraria saber se alguém pensa o contrário.

Eu também adoraria saber se há um exemplo que estou perdendo onde até mesmo essa regra não funciona.

Se concordarmos que as restrições do construtor de tipo devem ser expressas usando uma relação de subtipo, que parece funcionar muito bem, podemos passar para outras coisas.

Sobre a sintaxe

Não estamos realmente de acordo sobre este assunto, ao que parece. Eu prefiro fortemente uma sintaxe de prefixo semelhante a ~Promise . Conceitualmente, o ~ pode ser visto como uma "referência ao TC de" operador ou algo assim.

Acho que dei vários motivos pelos quais é melhor do que alternativas:

  1. Totalmente inequívoco.

    1. Erros envolvendo essa sintaxe também não são ambíguos. Se alguém se esquece de parametrizar um tipo, não obterá erros sobre os construtores de tipo quando não souber o que são.

    2. Como efeito colateral, os textos e a lógica de erro existentes não precisarão ser alterados. Se alguém escrever Promise a mensagem de erro será exatamente a mesma que é agora. Não será necessário mudar para falar sobre TCs.

  2. Estende-se bem a uma sintaxe estrutural.
  3. Fácil de analisar, eu acredito. Qualquer ~\w apareça onde um tipo é esperado será considerado uma referência a um TC.
  4. Fácil de digitar.

Espero que outras pessoas possam dar suas opiniões.

Sobre sindicatos, cruzamentos e parâmetros de tipo padrão

Os tipos sobrecarregados / mistos dos formulários * & (* => *) , * | (* => *) e assim por diante são legais? Eles têm usos interessantes?

Acho que são uma má ideia e difíceis de raciocinar. Também não tenho certeza de que tipo de anotação você precisaria para eliminar a ambigüidade de * | (* => *) para que possa construir um tipo a partir dela.

Uma maneira pela qual esses tipos podem existir agora são os tipos com parâmetros de tipo padrão:

type Example<A = number> = {}

Pode-se dizer que esse tipo tem o tipo * & (* => *) porque ele pode aceitar um parâmetro de tipo para construir um tipo, mas não precisa.

Eu acredito que os parâmetros de tipo padrão devem ser uma forma de abreviação, não uma maneira de descrever tipos. Portanto, acho que os parâmetros de tipo padrão devem ser simplesmente ignorados ao determinar o tipo de um tipo.

No entanto, pode fazer sentido falar sobre tipos como ~Promise | ~Array . Eles têm o mesmo tipo, então não são incompatíveis. Eu acho que isso deve ser apoiado.

Coisas que terão que ser tratadas

Situações relacionadas terão que ser tratadas, como esta situação:

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

Mas isso não envolve realmente o tipo (* => *) | (*, *) => * , mas algo diferente

Construindo outros TCs?

Como mencionei antes, não acho uma boa ideia ter TCs que constroem outros TCs, como * => (* => *) . Eles são a norma em linguagens que suportam currying e similares, mas não no TypeScript.

Não há nenhuma maneira que eu possa ver para definir tais tipos usando a sintaxe ~ e a relação de subtipo, então não exigiria nenhuma regra especial para proibi-los. Isso exigiria regras especiais para fazê-los funcionar.

Eu acho que você poderia defini-los estruturalmente assim:

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

Essa é praticamente a única maneira que penso.

Interação com funções de alto escalão?

Existe uma interação natural, mas complexa com tipos de função que usam parâmetros de tipo:

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

Essa interação deve ser interrompida de alguma forma? Eu posso ver tipos como esses se tornando muito complicados.

Em geral, existem lugares onde os parâmetros TC não devem aparecer?

Minha sintaxe estrutural é uma boa ideia?

Não acho que deva ser implementado imediatamente, mas ainda acho que minha sintaxe estrutural é uma boa ideia. Ele permite que você:

  1. Use identificadores locais como outros parâmetros de tipo em suas restrições.
  2. Aplique construtores de tipo parcialmente, de uma forma muito explícita e flexível: ~<A>Map<string, A> , ~<A, B>Map<B, A> e assim por diante.
  3. Todos os outros aspectos de um tipo têm uma sintaxe estrutural, portanto, os TCs também devem ter essa sintaxe.

Dito isso, os TCs podem funcionar totalmente sem isso, e o primeiro PR provavelmente não os envolveria.

Interação com tipos condicionais

Como o recurso funcionará com tipos condicionais? Você deveria ser capaz de fazer isso?

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

Eu mesmo não tenho certeza. Ainda não digeriu completamente os tipos condicionais.

Resolução de sobrecarga

Tenho a sensação de que esse vai ser difícil de fazer. Na verdade, isso é muito importante porque diferentes formas de resolução de sobrecarga acabariam criando diferentes tipos.

Dito isso, não consigo dar nenhum bom exemplo agora.

Você sabe, muito disso teria sido um ponto discutível se uma linguagem intermediária bem definida fosse usada para descrever o TypeScript como um ponto de partida. Por exemplo: System F <: ou um dos sistemas de tipo dependente de som, como Simplified Dependent ML .

Eu ficaria honestamente surpreso se isso fosse resolvido antes
https://github.com/Microsoft/TypeScript/issues/14833

Acho que o # 17961 provavelmente poderia resolver isso indiretamente. Veja esta essência para mais detalhes.

Observe que os tipos Bifunctor e Profunctor são um pouco complexos no nível de restrição - seria muito mais simples se eu tivesse tipos universais óbvios para trabalhar, em vez de infer T que é puramente limitado a tipos condicionais. Além disso, seria bom se eu pudesse ter usado this como o tipo de "retorno" (que é puramente nível de tipo) - isso teria tornado a maioria das minhas interfaces mais fáceis de definir.

(Eu não sou um usuário pesado de TS, então posso ter cometido erros. @ Tycho01 Você poderia dar uma olhada nisso para ver se eu

@isiahmeadows @ tycho01 Uau ...

Você está certo. Se bem entendi, os resultados são praticamente os mesmos.

Existem algumas diferenças. Mas, funcionalmente, eles são praticamente idênticos e acho que essas diferenças podem ser resolvidas.

Não é possível inferir o tipo correto de função

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

Aqui você pode inferir ~Promise e ~Bluebird de p . No entanto, se você fizer assim:

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

Eu duvido muito que isso funcione:

example(null as Promise<number>)

Não há como inferir que F deva ser:

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

Porque esta função não é considerada especial de forma alguma. Enquanto com TCs, alguns tipos têm essencialmente uma função de nível de tipo "implícita": seu TC.

Não é possível fazer referência a TCs existentes facilmente

Você não pode fazer ~Promise como na minha proposta. Você teria que codificar o tipo diretamente, usando um formato estrutural:

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

Verdade, e isso é uma preocupação. Esse é mais um problema de inferência de tipo, em que você precisa da capacidade de inferir a própria função genérica de um tipo de argumento conhecido (o inverso do que geralmente acontece). É solucionável de uma forma geral o suficiente para funcionar na maioria dos casos, mas requer um novo caso especial que não é trivial.

Pode ser solucionável em parte por meio do uso estratégico de NoInfer<T> , mas não estou 100% certo de como isso precisaria ser feito e o quanto isso poderia resolver até mesmo o caso comum.

@GregRos

Não sou fortemente a favor de nenhuma sintaxe, é mais apenas minha preferência, existem muitos méritos em ~ . Acho que a principal coisa a considerar seria se a sintaxe para anotações de tipo explícitas são necessárias porque a inferência nem sempre é possível.


A coisa do mapeamento ... honestamente, é muito estúpido. Não acho que haja alguma maneira de unificar MyPromiseestende a promessa

Acho que o mapeamento ainda pode ser uma noção útil, mas, no caso acima, não acho que deveríamos tentar unificar ~MyPromise com ~Promise , o que precisa ser unificado é ~MyPromise e ~<T>Promise<T[]> , que também poderíamos escrever ~(Promise . []) . Eu acho que o que estava faltando é que o mapeamento precisa ser parte da relação de subtipagem: ele faz parte do construtor tanto quanto Promise . Nesse exemplo, o mapeamento é apenas o construtor da lista.

interface A<T> {
    x: T;
} 

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

~<T>B<T> estende ~<T>A<T[]> ? Sim. ~<T>B<T> estende ~<T>A<T> ? Não. Mas, em última análise, são duas questões não relacionadas.

Se concordarmos que as restrições do construtor de tipo devem ser expressas usando uma relação de subtipo, que parece funcionar muito bem, podemos passar para outras coisas.

Sim, acho que parece uma boa maneira de descrever as coisas.


Não é possível inferir o tipo correto de função

function example<~P extends ~PromiseLike>(p : P<number>) : P<string>;
Aqui você pode inferir ~Promise e ~Bluebird de p.

Esta não é uma afirmação, mais uma questão em aberto, pois não tenho certeza de como funciona a verificação de tipo. Pensei que, usando a interface A acima como exemplo, os tipos A<number> e {x: number} são indistinguíveis e, portanto, não tenho certeza se seria possível inferir o construtor do tipo retornado de um aplicativo do construtor. Seria possível recuperar P de P<number> ? Tenho certeza de que as coisas poderiam ser alteradas para dar suporte a isso, estou apenas me perguntando o que isso faz agora.

Resposta cruzada de # 17961, mas não tenho certeza de como fazer a abordagem de @isiahmeadows funcionar, infelizmente. Temo que a inferência retroativa nas chamadas de tipo não seja trivial.

Portanto, parece que, com base em uma entrada Promise<number> ou Bluebird<number> , queremos ser capazes de inferir versões não aplicadas desses tipos de modo que possamos reaplicá-los com, por exemplo, string . Mas isso parece difícil.

Mesmo se os tipos de entrada forem assim, em vez de algum equivalente estrutural (somos uma linguagem estruturalmente tipada, certo?), Esse raciocínio também fica obscuro se, por exemplo, Bluebird tivesse dois tipos de parâmetros, em cujo ponto nosso <string> aplicativo
Não tenho certeza de uma boa solução aqui. (isenção de responsabilidade: fiquei um pouco atrasado na discussão aqui.)

@ tycho01 Todos esses problemas desapareceriam se as pessoas instanciassem explicitamente T ?

Acho isso razoável, visto que duvido que a inferência possa ser resolvida para todos os casos de qualquer maneira.

@ jack-williams: não com o # 17961 até agora, mas acho que usá-lo para despacho pode ajudar:

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 Sim, percebi que minha sugestão era terrível porque T não é instanciado nas chamadas de método.

Algo como o seguinte funciona?

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 Eu acho que a questão deveria ser como ele se compararia em comportamento à implementação ADT em fp-ts , mas parece que pode funcionar, sim. Provavelmente também sem o TyCon .

@ jack-williams @isiahmeadows :

Experimentei a ideia no try flow , pois já tem $Call disponível. Para mim, parece não responder de alguma forma ...

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 Acho que você não pode simplesmente obter propriedades com $ElementType de this no fluxo

@ tycho01 você realmente não pode fazer isso funcionar no
playground: https://goo.gl/tMBKyJ

@goodmind : hm, parece que infere Maybe<number> vez de Functor<number> depois de copiar fmap de Functor para Maybe .
Com chamadas de tipo, acho que seria melhor ter apenas o tipo lá, em vez de precisar da implementação de tempo de execução para o tipo.
Agora, os functores já precisariam de suas próprias fmap implementações. Isso seria péssimo para os métodos derivados .
De volta à estaca zero. : /

Estou planejando lançar uma versão alfa o mais rápido possível, mas você pode acompanhar escrevendo os exemplos dessa edição para já ter uma ideia.

Esse problema específico é um pouco longo para ser analisado inteiramente, mas o que estou procurando é um conteúdo simples, mas exemplos reais de código que você não consegue digitar devido à falta de tipos genéricos parametrizados. Acho que posso digitar a maioria deles (desde que não dependam de construtores de tipo abstrato elevado). Sinta-se à vontade para abrir questões no repositório acima com o código e eu irei digitá-las para você se puder (ou você pode publicá-las aqui também).

Atenção, comecei uma tentativa de implementar isso em # 23809. Ainda está muito incompleto, mas verifique se você estiver interessado.

Prometi a vocês um exemplo simples, aqui está. Isso usa alguns truques que aprendi escrevendo minha 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"];

Atualizei e simplifiquei o exemplo, aqui está um link de playground também:
Parque infantil

Eu adicionei uma biblioteca NPM para o acima, para que você possa trabalhar com isso mais facilmente. Atualmente em alfa até eu conseguir os testes adequados, mas deve ajudar vocês tentando escrever HKTs.

Github Link

Estou brincando com uma abordagem simples para simular HKTs usando tipos condicionais para substituir variáveis ​​de tipo virtual em um 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));

Projeto aqui: https://github.com/pelotom/hkts

Feedback seja bem-vindo!

@pelotom Eu gosto da leveza da sintaxe de sua abordagem. Existem duas outras abordagens que ainda não foram mencionadas neste tópico que podem despertar alguma criatividade em como as soluções atuais e futuras são produzidas. Ambas são soluções orientadas a objetos para esse problema.

  1. Bertrand Meyer descreveu uma maneira de simular tipos genéricos em seu livro de 1988 "Object-oriented Software Construction".

Os exemplos estão em Eiffel, mas uma tradução aproximada para TypeScript fica assim:

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

Você notará que eles podem ficar muito pesados ​​devido à necessidade de representações de classes intermediárias, mas com uma forma de fábrica de classes ou com a abordagem TypeScript Mixin isso pode ser reduzido significativamente.

Pode haver alguma aplicabilidade para # 17588

  1. A segunda abordagem é usada ao simular álgebras de objetos (e fábricas abstratas):

C<T> é representado por App<t,T> onde T é a classe e t é uma tag exclusiva associada a C

interface App<C,T> {}

Amostra:

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

Você pode ver mais detalhes e justificativas no seguinte artigo na seção 3.5 "Emulando Polimorfismo de Construtor de Tipo":

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

@metaweta , você pode renomear este problema para Higher kinded types in TypeScript para que tenha uma melhor visibilidade da pesquisa do Google, por favor?

Talvez nossos sábios e benevolentes mantenedores do repositório (por exemplo, @RyanCavanaugh , @DanielRosenwasser ) pudessem editar o título, se tal mudança for considerada digna de sua intervenção?

Estou curioso para saber o que significa que isso foi movido da comunidade para o backlog. Isso é algo que a equipe principal está considerando mais seriamente agora ou significa simplesmente que a equipe decidiu que este não é um bom candidato da comunidade?

Encontrei: o marco "Comunidade" está aparentemente obsoleto em favor do "Backlog" e, portanto, esse problema provavelmente foi migrado na mesma moeda.

Não é um membro do TS, apenas alguém que decidiu clicar no link onde foi remarcado.

+1

Aqui está algo que eu estava tentando construir que parece ser um caso realmente prático para tipos de tipo superior.

Desejo criar uma abstração para um banco de dados que pode ser executado de forma síncrona ou assíncrona. Em vez de usar callbacks e contornar isso, eu queria usar um genérico. Aqui está o que eu gostaria de fazer:

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

Isso seria muito poderoso!

@ccorcos isso é chamado de estilo MTL. Você pode dar uma olhada em https://github.com/gcanti/fp-ts/blob/master/tutorials/mtl.ts para um exemplo funcional puro com fp-ts .

@mlegenhausen Sinto muito, mas é difícil seguir esse exemplo.

Sempre que procuro fp-ts , me preocupo que as coisas estejam ficando tão complicadas que se tornem frágeis. O exemplo de @pelotom parece mais fácil de seguir ...

Alguma razão para que isso não seja adotado no TypeScript?

@ccorcos IMHO mesmo quando eu recomendei o exemplo de fp-ts eu não recomendaria o estilo MTL / tagless de forma alguma. Você adiciona uma camada extra de abstração a cada mônada eficaz que precisa gerenciar manualmente, pois o texto digitado não é capaz de detectar qual mônada você deseja usar e é aí que as coisas ficam complicadas. O que vejo da comunidade fp-ts é usar uma mônada assíncrona (eu recomendaria TaskEither ) e ficar com ela. Mesmo ao testar, os benefícios do MTL não compensam o incômodo que você obtém em seu código que não é de teste. hyper-ts baseados em fp-ts é um exemplo de uma biblioteca que abandonou o suporte para MTL recentemente.

Interessante ... hyper-ts parece muito legal ...

Eu vim com uma codificação leve de tipo superior baseada no polimorfismo F-bounded: https://github.com/strax/tshkt

O benefício dessa abordagem é que você não precisa de uma tabela de pesquisa (um único tipo condicional ou um objeto com chaves de string) para associar construtores de tipo a tipos. A técnica também pode ser usada para codificar a aplicação de funções genéricas de nível de tipo (pense em ReturnType<<T>(value: T) => Array<T>> ).

Ainda é uma prova de conceito, portanto, o feedback sobre a viabilidade dessa abordagem é muito apreciado!

Vou dar uma olhada nesse @strax , que parece muito legal!

Enquanto isso, aqui está um exemplo bobo do que podemos fazer agora:

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

Eu gostaria de adicionar índices De Bruijn, pois isso significaria que não precisamos mais das interfaces, mas seria necessário um pouco de matemática de tupla, eu acho, e estou tentando evitar isso.

Proposta

Ordem superior, Lamda, tipos de referência

Passe um tipo como referência

Simplificando, um tipo de referência ou um tipo de ordem superior permitiria adiar os parâmetros tomados por um tipo para mais tarde, ou mesmo inferir parâmetros de tipo (genéricos) posteriormente. Mas por que devemos nos importar?

Se pudermos passar um tipo como referência, isso significa que podemos atrasar a avaliação de um tipo do TypeScript até decidirmos fazê-lo. Vamos dar uma olhada em um exemplo do mundo real:

Pré-visualizar com tubo

Imagine que você esteja desenvolvendo um tipo genérico para pipe . A maior parte do trabalho é sobre como verificar se as funções a serem canalizadas são realmente capazes de canalizar, caso contrário, levantaríamos um erro para o usuário. Para fazer isso, usaríamos um tipo mapeado para canalizar os tipos de funções como pipe(...) faz:

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

Agora, só temos que repetir isso sobre as funções com um tipo mapeado:

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

( veja a implementação completa )

Agora podemos canalizar funções e o TypeScript pode nos dar avisos:

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! temos um erro adequado:

O argumento do tipo '(mensagem: objeto) => booleano' não é atribuível ao parâmetro do tipo '(arg: string) => booleano'.

O problema

Mas há um problema. Embora funcione muito bem para operações simples, você descobrirá que falha completamente quando começa a usar genéricos (modelos) nas funções que você passa para ele:

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

Em ambos os casos, o TypeScript perdeu o controle dos tipos de função.
> É aqui que os tipos de ordem superior entram em jogo <

Sintaxe

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

Resumindo, inferimos manual e dinamicamente os genéricos com * . Na verdade, o uso de * adiou a avaliação dos genéricos. Portanto, o * tem comportamentos diferentes, dependendo do contexto. Se * estiver em um tipo que:

  • pode receber genéricos : ele assume seus genéricos / obtém sua referência
  • é um genérico : adia a avaliação até que a referência seja conhecida. Para isso, uma árvore de referência é construída do (s) pai (s) para baixo. Em outras palavras, as referências podem ser aninhadas. Isso é exatamente o que acontece acima com Return<*(Fns[Pos<Prev<IterationOf<K & string>>>])> que foi atribuído a T . Neste contexto, podemos dizer que * "protege" contra avaliação imediata.
  • não é nenhuma das opções acima : não faz nada, resolve para o mesmo tipo
type  piped  =  Piper<[
    (a:  string) =>  string,
    <B>(b:  B) =>  B
    <C>(c:  C) =>  C
]>
// [
//     (a:  string) =>  string,
//     (b:  string) =>  string,
//     (c:  string) =>  string
// ]

Portanto, o TypeScript deve iniciar / continuar a avaliação apenas se o genérico tiver sido fornecido e bloquear a avaliação quando necessário (genérico incompleto). No momento, o TS avalia de uma só vez, transformando genéricos em tipos unknown . Com esta proposta, quando algo não puder ser resolvido:

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

Detalhes

O * recupera uma referência a um tipo, permitindo manipulações em seus genéricos. Portanto, colocar o curinga na frente de um tipo recupera uma referência a ele:

*[type]

Recuperar uma referência a um tipo permite automaticamente a manipulação de genéricos:

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

Os genéricos só são consumidos / definidos pelo tipo de destino, se possível. Então, fazendo isso:

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

Mas também pode ser verificado pelo próprio TypeScript, se deve exibir um aviso ou não. Mas internamente, TS não deve fazer nada com isso.

Também achei uma boa ideia usar * , pois pode simbolizar um ponteiro para algo (como nas linguagens C / C ++) e não é emprestado pelo TypeScript.

Tipos de ordem superior

Agora que vimos como funciona em sua forma mais básica, gostaria de apresentar o conceito principal: tipos lambda . Seria bom poder ter tipos anônimos, semelhantes a callbacks, lambdas, referências em JavaScript .

O exemplo acima mostra como assumir os genéricos de uma função. Mas já que estamos falando de referências, qualquer tipo pode ser usado em conjunto com * . Simplificando, uma referência de tipo é um tipo que podemos repassar, mas que ainda não recebeu seus 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>;
}

Termos de pesquisa

superior #order #type #references #lambda #HKTs

@pirix-gh se você ler apenas algumas mensagens, verá que muito do que você pede já é possível ou já foi solicitado.

Eu os li, pensei que poderia resumir minhas idéias como todo mundo fez (para uma solução tudo-em-um), principalmente sobre a sintaxe.

Eu editei a proposta acima para uma explicação melhor de como poderíamos encadear referências e consertei a maneira como um tipo como Pipe funcionaria com ele (houve alguns erros em relação à lógica disso).

Qualquer atualização?

Ainda sem atualização? Em minha opinião, esse problema é o principal obstáculo para que o TypeScript atinja seu potencial máximo. Existem muitos casos em que tento digitar minhas bibliotecas corretamente, apenas para desistir após uma longa luta, percebendo que esbarrei nessa limitação novamente. É generalizado, aparecendo mesmo em cenários aparentemente muito simples. Realmente espero que seja resolvido em breve.

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

Muito trabalho ainda precisa ser feito, como: consertar informações rápidas, inferir typeParameter, adicionar mensagem de erro, destacar o mesmo typeConstructor .....
Mas começa a funcionar e aqui está o que eu poderia conseguir por agora.
A interface de exemplo é de @millsp https://github.com/microsoft/TypeScript/issues/1213#issuecomment -523245130, a conclusão é realmente muito útil, ótimo graças a isso.

Espero que a comunicação possa fornecer mais casos de usuários como esse, para verificar se a forma atual funciona para a maioria das situações.

Também seria ótimo fornecer algumas informações sobre HKT / programação de funções / lambda (quando digo lambda , quero dizer a matemática, só consegui encontrar exemplos escritos por alguma linguagem, sem matemática)

Aqui estão algumas coisas que me ajudam muito:

@ShuiRuTian Quanto a m.join(q) retornar Set<unknown> , suponho que --noImplicitAny faz com que ele emita um aviso também?

Espero que a comunidade possa fornecer mais casos de usuários como esse, para verificar se a forma atual funciona para a maioria das situações.

Também seria legal fornecer algumas informações sobre HKT / programação de funções / lambda (quando digo lambda , quero dizer a matemática, só consegui encontrar exemplos escritos por alguma linguagem, sem matemática)

Sem ir mais longe, recentemente tentei criar uma função filter genérica com curry e queria que ela fizesse algo assim:

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

E eu não tenho uma maneira de realmente fazer isso, porque preciso de uma maneira de dizer ao TypeScript "retornar o mesmo tipo que você recebeu, mas com este outro parâmetro". Algo assim:

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

@lukeshiru sim, essencialmente este é https://pursuit.purescript.org/packages/purescript-filterable/2.0.1/docs/Data.Filterable#v : filter
Existem muitos outros casos de uso semelhantes para HKT no TypeScript.

@isiahmeadows eu tentei. Você está certo.
@lukeshiru e @raveclassic Obrigado! Esse recurso parece bastante razoável. Eu daria uma olhada nisso depois de ler https://gcanti.github.io/fp-ts/learning-resources/

Estou preso e não sei qual é a atual " solução alternativa" ...
Estou tentando implementar a especificação da

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

Um valor que implementa a especificação Chain também deve implementar a especificação Apply.

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

Fiz um FunctorSimplex que é então estendido por FunctorComplex então estendido por Apply mas agora quero estender Apply como Chain está quebrando ...

Então eu preciso disso (imagem abaixo e link para o código):

(Eu preciso passar um tipo para ApType para que na linha 12 o Apply não seja «codificado», mas seja genérico ... para também receber quaisquer tipos estendidos de um IApply )

Screenshot

Link permanente para as linhas de snippet de código

`` `texto datilografado
tipo de exportação ApType = ( ap: Aplicar <(val: A) => B>, ) => IApply ;

/ * [...] * /

interface de exportação IApply estende FunctorComplex {
/ ** Fantasy-land/ap :: Apply f => f a ~> f (a -> b) -> f b * /
ap: ApType ;
}
`` `

Referenciado em uma questão de Stack Overflow: Problema de tipos genéricos de TypeScript: solução alternativa necessária

@Luxcium Até que o TS tenha suporte para tipos de tipo superior, apenas a emulação deles é possível. Você pode querer dar uma olhada lá para ver como é possível alcançar:

Até que o TS tenha suporte para tipos de tipo superior, apenas a emulação deles é possível. Você pode querer dar uma olhada lá para ver como é possível alcançar

Tanques muito @kapke Provavelmente estou muito interessado em FP nestes dias e como em Javascript pode-se retornar uma função de uma função, podemos escrever pseudoFnAdd(15)(27) // 42 Eu gostaria de ser capaz, com TypeScript, de escrever pseudoType<someClassOrConstructor><number> // unknown mas eu sou um script kiddie, não um _ ° algum tipo de pessoa que estudou por muito tempo na universidade ou algo assim ° _

Estas informações e palestras (leituras) são muito apreciadas ...

Nota: Eu falo francês, em francês a palavra _lição (s) _ tem o significado de _lições_ e não 'uma conversa séria ou raivosa dada a alguém para criticar seu comportamento' ...

Provavelmente o seguinte que eu criei como uma solução alternativa simples sem PRs não funcionará para todos os casos, mas acho que vale a pena mencionar:

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[]
}
Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

fwanicka picture fwanicka  ·  3Comentários

blendsdk picture blendsdk  ·  3Comentários

MartynasZilinskas picture MartynasZilinskas  ·  3Comentários

manekinekko picture manekinekko  ·  3Comentários

zhuravlikjb picture zhuravlikjb  ·  3Comentários
bleepcoder.com usa informações licenciadas publicamente pela GitHub para fornecer aos desenvolvedores em todo o mundo soluções para seus problemas. Não somos afiliados à GitHub, Inc. nem a nenhum desenvolvedor que utilize GitHub para seus projetos. Nós não hospedamos nenhum dos vídeos ou imagens em nossos servidores. Todos os direitos pertencem a seus respectivos proprietários.
Fonte para esta página: Fonte

Linguagens de programação populares
Projetos populares do GitHub
Mais projetos 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.