Rust: Problema de rastreamento para `impl Trait` (RFC 1522, RFC 1951, RFC 2071)

Criado em 27 jun. 2016  ·  417Comentários  ·  Fonte: rust-lang/rust

NOVO PROBLEMA DE RASTREAMENTO = https://github.com/rust-lang/rust/issues/63066

Status de implementação

O recurso básico especificado na RFC 1522 foi implementado, no entanto, houve revisões que ainda precisam de trabalho:

RFCs

Houve uma série de RFCs sobre características imp, todas rastreadas por esse problema de rastreamento central.

Perguntas não resolvidas

A implementação também levantou uma série de questões interessantes:

  • [x] Qual é a precedência da palavra-chave impl ao analisar tipos? Discussão: 1
  • [ ] Devemos permitir impl Trait depois de -> nos tipos fn ou açúcar entre parênteses? #45994
  • [ ] Temos que impor um DAG em todas as funções para permitir o vazamento com segurança automática ou podemos usar algum tipo de adiamento. Discussão: 1

    • Semântica presente: DAG.

  • [x] Como devemos integrar o traço imp no regionck? Discussão: 1 , 2
  • [ ] Devemos permitir a especificação de tipos se alguns parâmetros são implícitos e alguns são explícitos? por exemplo, fn foo<T>(x: impl Iterator<Item = T>>) ?
  • [ ] [Algumas preocupações sobre o uso do recurso de implementação aninhada](https://github.com/rust-lang/rust/issues/34511#issuecomment-350715858)
  • [x] A sintaxe em um impl deve ser existential type Foo: Bar ou type Foo = impl Bar ? ( veja aqui para discussão )
  • [ ] O conjunto de "usos de definição" para um existential type em um impl deve ser apenas itens do impl, ou incluir itens aninhados dentro das funções do impl etc? ( veja aqui por exemplo )
B-RFC-implemented B-unstable C-tracking-issue T-lang disposition-merge finished-final-comment-period

Comentários muito úteis

Como esta é a última chance antes do fechamento do FCP, gostaria de fazer um último argumento contra as características automáticas automáticas. Sei que isso é um pouco de última hora, então, no máximo, gostaria de abordar formalmente esse problema antes de nos comprometermos com a implementação atual.

Para esclarecer para quem não tem acompanhado impl Trait , esta é a questão que estou apresentando. Um tipo representado por tipos impl X atualmente implementa automaticamente características automáticas se e somente se o tipo concreto por trás deles implementa essas características automáticas. Concretamente, se a seguinte alteração de código for feita, a função continuará a compilar, mas qualquer uso da função que dependa do fato de que o tipo que ela retorna implementa Send falhará.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(exemplo mais simples: trabalhando , mudanças internas causam falha )

Esta questão não é clara. Houve uma decisão muito deliberada de ter "vazamento" de traços automáticos: se não o fizéssemos, teríamos que colocar + !Send + !Sync em cada função que retornasse algo que não fosse Enviar ou Sincronizar, e teríamos tem uma história pouco clara com outras características automáticas em potencial que simplesmente não podem ser implementadas no tipo concreto que a função está retornando. Esses são dois problemas que abordarei mais adiante.

Primeiro, gostaria de simplesmente declarar minha objeção ao problema: isso permite alterar um corpo de função para alterar a API voltada para o público. Isso reduz diretamente a capacidade de manutenção do código.

Ao longo do desenvolvimento da ferrugem, foram tomadas decisões que erram no lado da verbosidade sobre a usabilidade. Quando os recém-chegados veem isso, eles pensam que é verbosidade por causa da verbosidade, mas não é o caso. Cada decisão, seja para não ter estruturas implementando Copiar automaticamente, ou ter todos os tipos explícitos nas assinaturas de função, é por causa da manutenção.

Quando apresento Rust às pessoas, com certeza, posso mostrar a eles velocidade, produtividade, segurança de memória. Mas ir tem velocidade. Ada tem segurança de memória. Python tem produtividade. O que Rust tem supera tudo isso, tem capacidade de manutenção. Quando um autor de biblioteca deseja alterar um algoritmo para torná-lo mais eficiente, ou quando deseja refazer a estrutura de uma caixa, ele tem uma forte garantia do compilador de que ele informará quando cometerem erros. Na ferrugem, posso ter certeza de que meu código continuará funcionando não apenas em termos de segurança de memória, mas também de lógica e interface. _Toda interface de função em Rust é totalmente representável pela declaração de tipo da função_.

Estabilizar impl Trait como está tem uma grande chance de ir contra essa crença. Claro, é extremamente bom para escrever código rapidamente, mas se eu quiser prototipar, usarei python. Rust é a linguagem de escolha quando se precisa de manutenção de longo prazo, não de código somente de escrita de curto prazo.


Eu digo que há apenas uma "grande chance" de isso ser ruim aqui porque, novamente, a questão não é clara. Toda a ideia de 'autotraços' em primeiro lugar não é explícita. Enviar e sincronizar são implementados com base no conteúdo de uma estrutura, não na declaração pública. Como essa decisão funcionou para ferrugem, impl Trait agindo de forma semelhante também poderia funcionar bem.

No entanto, funções e estruturas são usadas de maneira diferente em uma base de código, e esses não são os mesmos problemas.

Ao modificar os campos de uma estrutura, mesmo campos privados, fica imediatamente claro que se está alterando o conteúdo real da mesma. Estruturas com campos não-Send ou não-Sync fizeram essa escolha, e os mantenedores da biblioteca sabem verificar novamente quando um PR altera os campos de uma estrutura.

Ao modificar as partes internas de uma função, fica claro que uma pode afetar tanto o desempenho quanto a correção. No entanto, em Rust, não precisamos verificar se estamos retornando o tipo correto. Declarações de função são um contrato rígido que devemos manter, e rustc protege. É uma linha tênue entre características automáticas em structs e em retornos de função, mas alterar os internos de uma função é muito mais rotineiro. Assim que tivermos Future s com gerador completo, será ainda mais rotineiro modificar funções retornando -> impl Future . Essas serão todas as alterações que os autores precisam rastrear para implementações de envio/sincronização modificadas se o compilador não detectá-las.

Para resolver isso, podemos decidir que essa é uma carga de manutenção aceitável, como fez a discussão original da RFC . Esta seção na RFC conservadora de características implícitas apresenta os maiores argumentos para o vazamento de características automáticas ("OIBIT" é o nome antigo para características automáticas).

Eu já expus minha resposta principal a isso, mas aqui vai uma última nota. Alterar o layout de uma estrutura não é tão comum de se fazer; ele pode ser protegido contra. A carga de manutenção para garantir que as funções continuem a implementar as mesmas características automáticas é maior do que a das estruturas, simplesmente porque as funções mudam muito mais.


Como nota final, gostaria de dizer que as características automáticas automáticas não são a única opção. É a opção que escolhemos, mas a alternativa de autocaracterísticas de desativação ainda é uma alternativa.

Poderíamos exigir funções que retornam itens que não são de envio/sincronização para declarar + !Send + !Sync ou para retornar uma característica (alias possivelmente?) que tenha esses limites. Esta não seria uma boa decisão, mas pode ser melhor do que a que estamos escolhendo atualmente.

Quanto à preocupação com as características automáticas personalizadas, eu argumentaria que quaisquer novas características automáticas não deveriam ser implementadas apenas para novos tipos introduzidos após a característica automática. Isso pode fornecer mais problemas do que posso resolver agora, mas não é um problema que não possamos resolver com mais design.


Isso é muito tarde e muito prolixo, e tenho certeza de que já levantei essas objeções antes. Estou feliz por poder comentar uma última vez e garantir que estamos totalmente de acordo com a decisão que estamos tomando.

Obrigado por ler, e espero que a decisão final coloque Rust na melhor direção possível.

Todos 417 comentários

@aturon Podemos realmente colocar o RFC no repositório? ( @mbrubeck comentou lá que isso era um problema.)

Feito.

A primeira tentativa de implementação é #35091 (segunda, se você contar minha ramificação do ano passado).

Um problema que encontrei é com vidas. A inferência de tipo gosta de colocar variáveis ​​de região _em todos os lugares_ e sem nenhuma alteração de verificação de região, essas variáveis ​​não inferem nada além de escopos locais.
No entanto, o tipo concreto _deve_ ser exportável, então eu o restringi a 'static e nomeei explicitamente parâmetros de vida de ligação inicial, mas é _nunca_ qualquer um deles se alguma função estiver envolvida - mesmo um literal de string não infere para 'static , é praticamente completamente inútil.

Uma coisa que pensei, que teria 0 impacto na própria verificação de região, é apagar vidas:

  • nada expondo o tipo concreto de um impl Trait deve se preocupar com a vida útil - uma pesquisa rápida por Reveal::All sugere que esse já é o caso no compilador
  • um limite precisa ser colocado em todos os tipos concretos de impl Trait no tipo de retorno de uma função, que sobrevive à chamada dessa função - isso significa que qualquer tempo de vida é, por necessidade, 'static ou um dos parâmetros de tempo de vida da função - _even_ se não pudermos saber qual (por exemplo, "menor de 'a e 'b ")
  • devemos escolher uma variação para o parametrismo de tempo de vida implícito de impl Trait (ou seja, em todos os parâmetros de tempo de vida no escopo, o mesmo que para os parâmetros de tipo): a invariância é mais fácil e dá mais controle ao chamado, enquanto a contravariância permite que o chamador faça more e exigiria verificar se todo tempo de vida no tipo de retorno está em uma posição contravariante (o mesmo com parametrismo de tipo covariante em vez de invariante)
  • o mecanismo de vazamento de traço automático requer que um limite de traço possa ser colocado no tipo concreto, em outra função - já que apagamos os tempos de vida e não temos ideia de qual tempo de vida vai para onde, cada tempo de vida apagado no tipo concreto terá que ser substituído com uma nova variável de inferência que garante não ser menor que o tempo de vida mais curto de todos os parâmetros de tempo de vida reais; o problema está no fato de que imps de traço podem acabar exigindo relacionamentos de vida mais fortes (por exemplo, X<'a, 'a> ou X<'static> ), que devem ser detectados e errôneos, pois não podem ser comprovados para aqueles vidas

Esse último ponto sobre o vazamento de traços automáticos é minha única preocupação, todo o resto parece direto.
Não está totalmente claro neste momento quanto da verificação de região podemos reutilizar como está. Esperemos que todos.

cc @rust-lang/lang

@eddyb

Mas as vidas _são_ importantes com impl Trait - por exemplo

fn get_debug_str(s: &str) -> impl fmt::Debug {
    s
}

fn get_debug_string(s: &str) -> impl fmt::Debug {
    s.to_string()
}

fn good(s: &str) -> Box<fmt::Debug+'static> {
    // if this does not compile, that would be quite annoying
    Box::new(get_debug_string())
}

fn bad(s: &str) -> Box<fmt::Debug+'static> {
    // if this *does* compile, we have a problem
    Box::new(get_debug_str())
}

Eu mencionei isso várias vezes nos tópicos RFC

versão trait-object-less:

fn as_debug(s: &str) -> impl fmt::Debug;

fn example() {
    let mut s = String::new("hello");
    let debug = as_debug(&s);
    s.truncate(0);
    println!("{:?}", debug);
}

Isso é UB ou não, dependendo da definição de as_debug .

@arielb1 Ah, certo, esqueci que uma das razões pelas quais fiz o que fiz foi apenas capturar parâmetros de vida, não os de ligação tardia anônimos, exceto que realmente não funciona.

@arielb1 Temos uma relação estrita de sobrevivência que podemos colocar entre os 'a outlives 'b direto ou _indireto onde 'a é _qualquer coisa_ diferente de 'static ou um parâmetro vitalício e 'b aparece no tipo concreto de um impl Trait .

Desculpe por demorar um pouco para escrever de volta aqui. Então eu estive pensando
sobre este problema. Meu sentimento é que nós, em última análise, temos que (e
deseja) estender regionck com um novo tipo de restrição - vou chamá-lo
uma restrição \in , porque permite que você diga algo como '0 \in {'a, 'b, 'c} , o que significa que a região usada para '0 deve ser
'a , 'b ou 'c . Não tenho certeza da melhor maneira de integrar
isso para se resolver - certamente se o conjunto \in for um singleton
set, é apenas uma relação de igualdade (que atualmente não temos como
coisa de primeira classe, mas que pode ser composta de dois limites), mas
caso contrário, torna as coisas complicadas.

Tudo isso está relacionado ao meu desejo de fazer o conjunto de restrições de região
mais expressivo do que temos hoje. Certamente se poderia compor um
\in restrições OR e == . Mas claro mais
restrições expressivas são mais difíceis de resolver e \in não é diferente.

De qualquer forma, deixe-me expor um pouco do meu pensamento aqui. Vamos trabalhar com isso
exemplo:

pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}

Eu acho que o desaçucar mais preciso para um impl Trait é provavelmente um
novo tipo:

pub struct FooReturn<'a, 'b> {
    field: XXX // for some suitable type XXX
}

impl<'a,'b> Iterator for FooReturn<'a,'b> {
    type Item = <XXX as Iterator>::Item;
}

Agora o impl Iterator<Item=u32> em foo deve se comportar da mesma forma que
FooReturn<'a,'b> se comportaria. Não é uma combinação perfeita embora. Um
diferença, por exemplo, é a variância, como eddyb criou -- eu sou
supondo que faremos tipos impl Foo -like invariantes sobre o tipo
parâmetros de foo . O comportamento do traço automático funciona, no entanto.
(Outra área em que a correspondência pode não ser ideal é se adicionarmos o
capacidade de "perfurar" a abstração impl Iterator , de modo que o código
"dentro" a abstração conhece o tipo preciso - então classificaria
de ter uma operação implícita de "desempacotamento" ocorrendo.)

De certa forma, uma combinação melhor é considerar um tipo de traço sintético:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    type Type = XXX;
}

Agora podemos considerar o tipo impl Iterator como <() as FooReturn<'a,'b>>::Type . Esta também não é uma combinação perfeita, porque nós
normalmente o normalizaria. Você pode imaginar usando especialização
para evitar isso, porém:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    default type Type = XXX; // can't really be specialized, but wev
}

Neste caso, <() as FooReturn<'a,'b>>::Type não normalizaria,
e temos um jogo muito mais próximo. A variância, em particular, se comporta
direito; se alguma vez quiséssemos ter algum tipo que estivesse "dentro" do
abstração, eles seriam os mesmos, mas eles estão autorizados a
normalizar. No entanto, há um problema: o material de auto-traço não
bastante trabalho. (Podemos considerar harmonizar as coisas aqui,
na verdade.)

De qualquer forma, meu objetivo ao explorar esses potenciais desaçucarados não é
sugiro que implementemos "impl Trait" como uma remoção de açúcar _real_
(embora possa ser bom...) mas para dar uma intuição para o nosso trabalho. eu
acho que o segundo desaçucar - em termos de projeções - é um
muito útil para nos guiar para a frente.

Um lugar em que essa remoção de açúcar de projeção é um guia realmente útil é
a relação "sobreviva". Se quisermos verificar se <() as FooReturn<'a,'b>>::Type: 'x , RFC 1214 nos diz que podemos provar isso
contanto que 'a: 'x _and_ 'b: 'x mantenha. Isso é eu acho que nós queremos
para lidar com as coisas para o traço impl também.

Em tempo trans, e para autotraços, teremos que saber o que XXX
é, claro. A idéia básica aqui, suponho, é criar um tipo
variável para XXX e verifique se os valores reais que são retornados
todos podem ser unificados com XXX . Essa variável de tipo deve, em teoria,
diga-nos a nossa resposta. Mas é claro que o problema é que esse tipo
variável pode se referir a muitas regiões que não estão no escopo do
assinatura fn -- por exemplo, as regiões do corpo fn. (este mesmo problema
não ocorre com tipos; mesmo que, tecnicamente, você possa colocar
por exemplo, uma declaração de struct no corpo fn e seria inominável,
isso é um tipo de restrição artificial - pode-se mover
a estrutura fora do fn.)

Se você olhar tanto para o struct desugaring quanto para o impl, há um
(implícita na estrutura lexical de Rust) restrição que XXX pode
apenas nomeie 'static ou vidas como 'a e 'b , que
aparecem na assinatura da função. Essa é a coisa que não somos
modelagem aqui. Não tenho certeza da melhor maneira de fazer isso - algum tipo
esquemas de inferência têm uma representação mais direta do escopo, e
Eu sempre quis adicionar isso ao Rust, para nos ajudar com fechamentos. Mas
vamos pensar em deltas menores primeiro, eu acho.

É daí que vem a restrição \in . Pode-se imaginar adicionar
uma regra de verificação de tipo que (basicamente) FR(XXX) \subset {'a, 'b} --
significando que as "regiões gratuitas" que aparecem em XXX só podem ser 'a e
'b . Isso acabaria se traduzindo em requisitos de \in para o
várias regiões que aparecem em XXX .

Vejamos um exemplo real:

fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Aqui, o tipo se condition for verdadeiro seria algo como
Cloned<SliceIter<'a, i32>> . Mas se condition for falso, teríamos
quero Cloned<SliceIter<'b, i32>> . Claro que em ambos os casos teríamos
termine com algo como (usando números para variáveis ​​de tipo/região):

Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()

Se então instanciarmos a variável 0 para Cloned<SliceIter<'2, i32>> ,
temos '0: '2 e '1: '2 , ou um conjunto total de relações de região
Como:

'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body

Então, qual valor devemos usar para '2 ? Temos também o adicional
restrição que '2 in {'a, 'b} . Com o fn como escrito, acho que
teria que relatar um erro, pois nem 'a nem 'b são
escolha correta. Curiosamente, porém, se adicionarmos a restrição 'a: 'b , haveria um valor correto ( 'b ).

Observe que, se apenas executarmos o algoritmo _normal_, acabaríamos com
'2 sendo 'body . Não tenho certeza de como lidar com as relações \in
exceto pela busca exaustiva (embora eu possa imaginar alguns
casos).

OK, isso é até onde eu cheguei. =)

No PR #35091, @arielb1 escreveu:

Eu não gosto da abordagem "capturar todas as vidas no traço impl" e preferiria algo mais parecido com a elisão da vida.

Achei que faria mais sentido discutir aqui. @arielb1 , você pode elaborar mais sobre o que você tem em mente? Em termos das analogias que fiz acima, acho que você está falando fundamentalmente sobre "poda" do conjunto de tempos de vida que apareceriam como parâmetros no newtype ou na projeção (ou seja, <() as FooReturn<'a>>::Type vez de <() as FooReturn<'a,'b>>::Type ou algo assim?

Eu não acho que as regras de elisão de tempo de vida, como existem, seriam um bom guia a esse respeito: se apenas escolhermos o tempo de vida de &self para incluir apenas, então não seríamos necessariamente capazes de incluir o tempo de vida de Self , nem parâmetros de tipo do método, pois eles podem ter condições WF que exigem que nomeemos alguns dos outros tempos de vida.

De qualquer forma, seria ótimo ver alguns exemplos que ilustram as regras que você tem em mente e talvez as vantagens delas. :) (Além disso, acho que precisaríamos de alguma sintaxe para substituir a escolha.) Todas as outras coisas sendo iguais, se pudermos evitar ter que escolher entre N tempos de vida, eu prefiro isso.

Eu não vi interações de impl Trait com privacidade discutidas em qualquer lugar.
Agora fn f() -> impl Trait pode retornar um tipo privado S: Trait similarmente aos objetos trait fn f() -> Box<Trait> . Ou seja, objetos de tipos privados podem andar livremente fora de seu módulo de forma anônima.
Isso parece razoável e desejável - o tipo em si é um detalhe de implementação, apenas sua interface, disponível através de uma característica pública Trait é pública.
No entanto, há uma diferença entre objetos trait e impl Trait . Com objetos trait sozinhos, todos os métodos trait de tipos privados podem obter ligação interna, eles ainda poderão ser chamados por meio de ponteiros de função. Com impl Trait s métodos trait de tipos privados podem ser chamados diretamente de outras unidades de tradução. O algoritmo fazendo "internalização" de símbolos terá que se esforçar mais para internalizar métodos apenas para tipos não anonimizados com impl Trait , ou ser muito pessimista.

@nikomatsakis

A maneira "explícita" de escrever foo seria

fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Aqui não há dúvida sobre o limite da vida. Obviamente, ter que escrever o limite de vida toda vez seria bastante repetitivo. No entanto, a forma como lidamos com esse tipo de repetição é geralmente através da elisão ao longo da vida. No caso de foo , a elisão falharia e forçaria o programador a especificar explicitamente os tempos de vida.

Sou contra a adição de elisão vitalícia sensível à explicitação, como o @eddyb fez apenas no caso específico de impl Trait e não de outra forma.

@arielb1 hmm, não tenho 100% de certeza de como pensar nessa sintaxe proposta em termos dos "desugarings" que discuti. Ele permite que você especifique o que parece ser um limite de vida útil, mas o que estamos tentando inferir é principalmente quais vidas aparecem no tipo oculto. Isso sugere que no máximo uma vida poderia ser "oculta" (e que teria que ser especificada exatamente?)

Parece que nem sempre um "parâmetro de vida útil único" é suficiente:

fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    x.iter().chain(y).cloned()
}

Nesse caso, o tipo de iterador oculto refere-se a 'a e 'b (embora seja _variante em ambos; mas acho que poderíamos criar um exemplo que seja invariável).

Então @aturon e eu discutimos um pouco esse assunto e eu queria compartilhar. Há realmente algumas questões ortogonais aqui e eu quero separá-las. A primeira pergunta é "quais parâmetros de tipo/vida podem potencialmente ser usados ​​no tipo oculto?" Em termos de (quase) redução de açúcar em default type , isso se resume a "quais parâmetros de tipo aparecem na característica que introduzimos". Então, por exemplo, se esta função:

fn foo<'a, 'b, T>() -> impl Trait { ... }

seria desaçucarado para algo como:

fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
  type Type: Trait;
}
impl<...> Foo<...> for () {
  default type Type = /* inferred */;
}

então esta pergunta se resume a "que parâmetros de tipo aparecem no traço Foo e seu impl"? Basicamente, o ... aqui. Claramente isso inclui incluir o conjunto de parâmetros de tipo que aparecem são usados ​​pelo próprio Trait , mas quais parâmetros de tipo adicionais? (Como observei antes, essa remoção de açúcar é 100% fiel, exceto pelo vazamento de características automáticas, e eu argumentaria que devemos vazar características automáticas também para imps especializadas.)

A resposta padrão que estamos usando é "todos eles", então aqui ... seria 'a, 'b, T (junto com quaisquer parâmetros anônimos que possam aparecer). Este _pode_ ser um padrão razoável, mas não é _necessariamente_ o melhor padrão. (Como @arielb1 apontou.)

Isso tem um efeito sobre a relação de sobrevivência, uma vez que, para determinar que <() as Foo<...>>::Type (referindo-se a alguma instanciação opaca particular de impl Trait ) sobrevive a 'x , devemos efetivamente mostrar que ...: 'x (ou seja, cada parâmetro de vida e tipo).

Por isso digo que não basta considerar parâmetros de vida: imagine que temos alguma chamada para foo como foo::<'a0, 'b0, &'c0 i32> . Isso implica que todas as três vidas, '[abc]0 , devem durar mais que 'x -- em outras palavras, enquanto o valor de retorno estiver em uso, isso prolongará os empréstimos de todos os dados fornecidos à função . Mas, como @arielb1 apontou , a elisão sugere que isso geralmente será mais longo do que o necessário.

Então eu imagino que o que precisamos é:

  • estabelecer um padrão razoável, talvez usando a intuição da elisão;
  • ter uma sintaxe explícita para quando o padrão não for apropriado.

@aturon cuspiu algo como impl<...> Trait como a sintaxe explícita, o que parece razoável. Portanto, pode-se escrever:

fn foo<'a, 'b, T>(...) -> impl<T> Trait { }

para indicar que o tipo oculto não se refere de fato a 'a ou 'b mas apenas T . Ou pode-se escrever impl<'a> Trait para indicar que nem 'b nem T são capturados.

Quanto aos padrões, parece que ter mais dados seria bastante útil - mas a lógica geral da elisão sugere que faríamos bem em capturar todos os parâmetros nomeados no tipo self , quando aplicável. Por exemplo, se você tiver fn foo<'a,'b>(&'a self, v: &'b [u8]) e o tipo for Bar<'c, X> , então o tipo de self seria &'a Bar<'c, X> e, portanto, capturaríamos 'a , 'c e X por padrão, mas não 'b .


Outra nota relacionada é qual é o significado de um limite de vida. Eu acho que os limites da vida útil do som têm um significado existente que não deve ser alterado: se escrevermos impl (Trait+'a) isso significa que o tipo oculto T sobrevive a 'a . Da mesma forma, pode-se escrever impl (Trait+'static) para indicar que não há ponteiros emprestados presentes (mesmo que alguns tempos de vida sejam capturados). Ao inferir o tipo oculto T , isso implicaria um limite vitalício como $T: 'static , onde $T é a variável de inferência que criamos para o tipo oculto. Isso seria tratado da maneira usual. Da perspectiva de um chamador, onde o tipo oculto está, bem, oculto, o limite 'static nos permitiria concluir que impl (Trait+'static) sobrevive a 'static mesmo se houver parâmetros de vida útil capturados.

Aqui ele se comporta exatamente como o desaçucar se comportaria:

fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
  type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
  default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}

Tudo isso é ortogonal por inferência. Ainda queremos (eu acho) adicionar a noção de uma restrição "escolher de" e modificar a inferência com algumas heurísticas e, possivelmente, uma pesquisa exaustiva (a experiência da RFC 1214 sugere que heurísticas com um fallback conservador podem realmente nos levar muito longe; Não estou ciente de pessoas que enfrentam limitações a esse respeito, embora provavelmente haja um problema em algum lugar). Certamente, adicionar limites de vida como 'static ou 'a' pode influenciar a inferência e, portanto, ser útil, mas essa não é uma solução perfeita: por um lado, eles são visíveis para o chamador e se tornam parte da API, o que pode não ser desejado.

Opções possíveis:

Limite de tempo de vida explícito com elisão de parâmetro de saída

Como os objetos trait hoje, os objetos impl Trait têm um único parâmetro de limite de tempo de vida, que é inferido usando as regras de elisão.

Desvantagem: não ergonômico
Vantagem: claro

Limites de tempo de vida explícitos com elisão "toda genérica"

Assim como os objetos trait hoje, os objetos impl Trait têm um único parâmetro de limite de tempo de vida.

No entanto, a elisão cria novos parâmetros de limite inicial que sobrevivem a todos os parâmetros explícitos:

fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total

Desvantagem: adiciona um parâmetro de limite antecipado

mais.

Eu me deparei com este problema com impl Trait +'a e emprestando: https://github.com/rust-lang/rust/issues/37790

Se estou entendendo essa alteração corretamente (e a chance disso provavelmente é baixa!), acho que este código de playground deve funcionar:

https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0

Ambos ThingOne e ThingTwo implementam o traço Thing . build diz que retornará algo que implementa Thing , o que ele faz. Mesmo assim não compila. Então eu estou claramente entendendo algo errado.

Esse "algo" deve ter um tipo, mas no seu caso você tem dois tipos conflitantes. @nikomatsakis sugeriu anteriormente fazer isso funcionar em geral, criando, por exemplo, ThingOne | ThingTwo medida que as incompatibilidades de tipo aparecem.

@eddyb você poderia elaborar ThingOne | ThingTwo ? Você não precisa ter Box se só conhecemos o tipo em tempo de execução? Ou é uma espécie de enum ?

Sim, poderia ser um tipo ad-hoc enum - que delegou chamadas de método de traço, sempre que possível, para suas variantes.

Eu já quis esse tipo de coisa antes também. A enumeração anônima RFC: https://github.com/rust-lang/rfcs/pull/1154

É um caso raro de algo que funciona melhor se for orientado por inferência, porque se você criar esses tipos apenas em uma incompatibilidade, as variantes serão diferentes (o que é um problema com a forma generalizada).
Além disso, você pode obter algo por não ter correspondência de padrões (exceto em casos obviamente disjuntos?).
Mas o açúcar de delegação da IMO "simplesmente funcionaria" em todos os casos relevantes, mesmo se você conseguir obter um T | T .

Você poderia soletrar as outras metades implícitas dessas frases? Não entendo a maior parte e suspeito que estou perdendo algum contexto. Você estava respondendo implicitamente aos problemas com os tipos de união? Esse RFC é simplesmente enums anônimos, não tipos de união - (T|T) seria exatamente tão problemático quanto Result<T, T> .

Ah, deixa pra lá, eu confundi as propostas (também estou preso no celular até eu resolver meu HDD com defeito, então peço desculpas por parecer no Twitter).

Acho (posicional, ou seja, T|U != U|T ) enums anônimos intrigantes, e acredito que eles poderiam ser experimentados em uma biblioteca se tivéssemos genéricos variádicos (você pode contornar isso usando hlist ) e const genéricos (idem, com números peano).

Mas, ao mesmo tempo, se tivéssemos suporte ao idioma para algo, seriam tipos de união, não enumerações anônimas. Por exemplo, não Result mas tipos de erro (para evitar o tédio dos wrappers nomeados para eles).

Não tenho certeza se este é o lugar certo para perguntar, mas por que uma palavra-chave como impl necessária? Não consegui encontrar uma discussão (pode ser minha culpa).

Se uma função retorna impl Trait, seu corpo pode retornar valores de qualquer tipo que implemente Trait

Desde a

fn bar(a: &Foo) {
  ...
}

significa "aceitar uma referência a um tipo que implementa o traço Foo " eu esperaria

fn bar() -> Foo {
  ...
}

para significar "retorne um tipo que implementa o traço Foo ". Isso é impossível?

@kud1ing o motivo é não remover a possibilidade de ter uma função que retorna o tipo de tamanho dinâmico Trait se o suporte para valores de retorno de tamanho dinâmico for adicionado no futuro. Atualmente Trait já é um horário de verão válido, apenas não é possível retornar um horário de verão, então você precisa encaixotá-lo para torná-lo um tipo de tamanho.

EDIT: Há alguma discussão sobre isso no tópico RFC vinculado.

Bem, por um lado, independentemente de os valores de retorno dimensionados dinamicamente serem adicionados, prefiro a sintaxe atual. Ao contrário do que acontece com objetos trait, isso não é apagamento de tipo, e quaisquer coincidências como "parâmetro f: &Foo pega algo que implique Foo , enquanto isso retorna algo que implique Foo " poderia ser enganoso.

Eu reuni da discussão RFC que agora impl é uma implementação de espaço reservado, e nenhum impl é muito desejado. Existe alguma razão para _não_ querer uma Característica impl se o valor de retorno não for DST?

Eu acho que a técnica impl atual para lidar com "vazamento de características automáticas" é problemática. Em vez disso, devemos impor um pedido de DAG para que, se você definir um fn fn foo() -> impl Iterator e tiver um chamador fn bar() { ... foo() ... } , tenhamos que verificar o tipo foo() antes de bar() (para que saibamos qual é o tipo oculto). Se ocorrer um ciclo, informaremos um erro. Esta é uma postura conservadora - provavelmente podemos fazer melhor - mas acho que a técnica atual, em que coletamos obrigações de auto-característica e as verificamos no final, não funciona em geral. Por exemplo, não funcionaria bem com especialização.

(Outra possibilidade que pode ser mais permissiva do que exigir um DAG estrito é verificar o tipo de ambos os fns "juntos" até certo ponto. Acho que isso é algo a ser considerado somente depois de rearquitetarmos um pouco o sistema de traços.)

@Nercury Eu não entendo. Você está perguntando se há razões para não querer que fn foo() -> Trait signifique -> impl Trait ?

@nikomatsakis Sim, eu estava perguntando exatamente isso, desculpe pela linguagem confusa :). Eu pensei que fazer isso sem a palavra-chave impl seria mais simples, porque esse comportamento é exatamente o que se esperaria (quando um tipo concreto é retornado no lugar do tipo de retorno trait). No entanto, posso estar faltando alguma coisa, por isso estou perguntando.

A diferença é que as funções que retornam impl Trait sempre retornam o mesmo tipo - é basicamente uma inferência de tipo de retorno. IIUC, as funções que retornam apenas Trait poderiam retornar qualquer implementação dessa característica dinamicamente, mas o chamador precisaria estar preparado para alocar espaço para o valor de retorno por meio de algo como box foo() .

@Nercury A razão simples é que a sintaxe -> Trait já tem um significado, então temos que usar outra coisa para esse recurso.

Na verdade, vi pessoas esperarem os dois tipos de comportamento por padrão, e esse tipo de confusão surge com bastante frequência. Sinceramente, prefiro que fn foo() -> Trait não signifique nada (ou seja um aviso por padrão) e houvesse palavras-chave para o caso "algum tipo conhecido em tempo de compilação que posso escolher, mas o chamador não vê" e o caso "trait object que pode ser despachado dinamicamente para qualquer tipo que implemente Trait", por exemplo, fn foo() -> impl Trait vs fn foo() -> dyn Trait . Mas obviamente esses navios já partiram.

Por que o compilador não gera uma enumeração que contém todos os diferentes tipos de retorno da função, implementa a característica passando os argumentos para cada variante e retorna isso?

Isso ignoraria a única regra permitida do tipo de retorno.

@NeoLegends Fazer isso manualmente é bastante comum, e um pouco de açúcar pode ser bom e foi proposto no passado, mas é um terceiro conjunto de semântica completamente diferente de retornar impl Trait ou um objeto de traço, então não é realmente relevante para esta discussão.

@Ixrec Sim, eu sei que isso está sendo feito manualmente, mas o caso de uso real dos enums anônimos como tipos de retorno gerados pelo compilador são tipos que você não pode soletrar, como longas cadeias de iterador ou adaptadores futuros.

Como é essa semântica diferente? As enumerações anônimas (na medida em que o compilador as gera, não de acordo com as enumerações anônimas RFC) como valores de retorno só fazem sentido se houver uma API comum como uma característica que abstrai as diferentes variantes. Estou sugerindo um recurso que ainda se parece e se comporta como o Trait impl regular, apenas com o limite de um tipo removido por meio de um enum gerado pelo compilador que o consumidor da API nunca verá diretamente. O consumidor deve sempre ver apenas 'impl Trait'.

As enumerações anônimas geradas automaticamente dão a impl Trait um custo oculto que é fácil de perder, então isso é algo a ser considerado.

Eu suspeito que a coisa de "passagem automática de enumeração" só faz sentido para características seguras de objeto. A mesma coisa vale para o próprio impl Trait ?

@rpjohnst A menos que essa variante do método real esteja nos metadados da caixa e monomorfizada no local da chamada. Claro, isso requer que a mudança de uma variante para outra não interrompa o chamador. E isso pode ser muito mágico.

@glaebhoerl

Eu suspeito que a coisa de "passagem automática de enumeração" só faz sentido para características seguras de objeto. A mesma coisa vale para o próprio impl Trait?

Este é um ponto interessante! Eu tenho debatido qual é a maneira certa de "desaçucar" o traço impl, e na verdade estava prestes a sugerir que talvez nós quiséssemos pensar nisso mais como uma "estrutura com um campo privado" em oposição à "projeção de tipo abstrato" " interpretação. No entanto, isso parece implicar algo muito parecido com a derivação generalizada de novos tipos, o que, é claro, foi notoriamente considerado incorreto em Haskell quando combinado com famílias de tipos . Confesso não ter um entendimento completo dessa inconveniência "em cache", mas parece que teríamos que ser muito cautelosos aqui sempre que quisermos gerar automaticamente uma implementação de um trait para algum tipo F<T> de um impl por T .

@nikomatsakis

O problema é que, em termos de Rust

trait Foo {
    type Output;
    fn get() -> Self::Output;
}

fn foo() -> impl Foo {
    // ...
    // what is the type of return_type::get?
}

O tl;dr é que a derivação generalizada de newtype foi (e é) implementada simplesmente transmute ing a vtable -- afinal, uma vtable consiste em funções no tipo, e um tipo e seu newtype têm a mesma representação , então deve estar bem, certo? Mas ele quebra se essas funções também usarem tipos que são determinados por ramificação de nível de tipo na identidade (em vez de representação) do tipo fornecido - por exemplo, usando funções de tipo ou tipos associados (ou em Haskell, GADTs). Porque não há garantia de que as representações desses tipos também sejam compatíveis.

Observe que esse problema só é possível devido ao uso de transmutação insegura. Se, em vez disso, apenas gerasse o código clichê chato para agrupar/desembrulhar o novo tipo em todos os lugares e despachar todos os métodos para sua implementação a partir do tipo base (como algumas das propostas de delegação automática para Rust IIRC?), então o pior resultado possível seria um tipo erro ou talvez um ICE. Afinal, por construção, se você não usar um código inseguro, não poderá ter um resultado inseguro. Da mesma forma, se gerarmos código para algum tipo de "passagem automática de enumeração", mas não usarmos nenhuma primitiva unsafe para fazer isso, não haveria nenhum perigo.

(Não tenho certeza se ou como isso se relaciona com minha pergunta original sobre se os traços usados ​​com impl Trait e/ou passagem automática de enumeração, por necessidade, teriam que ser seguros para objetos?)

@rpjohnst Pode-se fazer o opt-in do caso enum para marcar o custo:

fn foo() -> enum impl Trait { ... }

Isso é quase certamente alimento para um RFC diferente.

@glaebhoerl sim, passei algum tempo investigando o problema e me senti bastante convencido de que não seria um problema aqui, pelo menos.

Desculpe se for algo óbvio, mas estou tentando entender as razões pelas quais impl Trait não pode aparecer em tipos de retorno de métodos de traço, ou se faz sentido em primeiro lugar? Por exemplo:

trait IterInto {
    type Output;
    fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}

@aldanor Faz todo o sentido, e a AFAIK a intenção é fazer isso funcionar, mas ainda não foi implementado.

Isso meio que faz sentido, mas não é o mesmo recurso subjacente (isso já foi muito discutido):

// What that trait would desugar into:
trait IterInto {
    type Output;
    type X: Into<Self::Output>;
    type Y: Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y;
}

// What an implementation would desugar into:
impl InterInto for FooList {
    type Output = Foo;
    // These could potentially be left unspecified for
    // a similar effect, if we want to allow that.
    type X = impl Into<Foo>;
    type Y = impl Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y {...}
}

Especificamente, impl Trait nos RHSes dos tipos associados impl Trait for Type seria semelhante ao recurso implementado hoje, pois não pode ser desaçucarado para Rust estável, enquanto no trait pode ser.

Eu sei que isso provavelmente é tarde demais e principalmente bikeshedding, mas foi documentado em algum lugar por que a palavra-chave impl foi introduzida? Parece-me que já temos uma maneira no código Rust atual de dizer "o compilador descobre que tipo vai aqui", ou seja, _ . Não poderíamos reutilizar isso aqui para fornecer a sintaxe:

fn foo() -> _ as Iterator<Item=u8> {}

@jonhoo Não é isso que o recurso faz, o tipo não é o retornado da função, mas sim um "empacotador semântico" que oculta tudo, exceto as APIs escolhidas (e OIBITs porque são uma dor).

Poderíamos permitir que algumas funções inferissem tipos em suas assinaturas forçando um DAG, mas esse recurso nunca foi aprovado e é improvável que seja adicionado ao Rust, pois seria tocar em "inferência global".

Sugira o uso da sintaxe @Trait para substituir impl Trait , conforme mencionado aqui .

É mais fácil estender para outras posições de tipo e em composição como Box<@MyTrait> ou &@MyTrait .

@Trait por any T where T: Trait e ~Trait por some T where T: Trait :

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
    move |x| g(f(x))
}

Em fn func(t: T) -> V , não há necessidade de distinguir nenhum t ou algum v, então como traço.

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
    move |x| g(f(x))
}

ainda funciona.

@JF-Liu Eu pessoalmente me oponho a ter any e some combinados em uma palavra-chave/sigilo, mas você está tecnicamente correto que poderíamos ter um único sigilo e usá-lo como o impl Trait RFC.

@JF-Liu @eddyb Houve uma razão pela qual os sigilos foram removidos do idioma. Por que essa razão não se aplica a este caso?

@ também é usado na correspondência de padrões, não removido do idioma.

O que eu tinha em mente é que os sigilos do AFAIK foram usados ​​em excesso.

Sintaxe bikesheding: Estou profundamente insatisfeito com a notação impl Trait , porque usar uma palavra-chave (fonte em negrito em um editor) para nomear um tipo é muito alto. Você se lembra da observação de sintaxe em voz alta struct e Stroustroup (slide 14)?

Em https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761 , @konstin sugeriu a <Trait> . Parece muito bom, especialmente nas posições de entrada:

fn take_iterator(iterator: <Iterator<Item=i32>>)

Vejo que vai entrar um pouco em conflito com o UFCS, mas talvez isso possa ser resolvido?

Eu também sinto que usar colchetes em vez de impl Trait é uma escolha melhor, pelo menos na posição do tipo de retorno, por exemplo:

fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}

<Trait> sintaxe

Vec<<FnOnce() -> bool>> vs Vec<@FnOnce() -> bool>

Se Vec<FnOnce() -> bool> for permitido, então <Trait> é uma boa ideia, significa a equivalência ao parâmetro de tipo genérico. Mas como Box<Trait> é diferente de Box<@Trait> , tem que desistir da sintaxe <Trait> .

Eu prefiro a sintaxe de palavra-chave impl porque quando você lê a documentação rapidamente, isso permite menos maneiras de interpretar mal os protótipos.
O que você acha ?

Estou percebendo que propus um superconjunto para este rfc no thread interno (Obrigado por @matklad por me apontar aqui):

Permita que as características nos parâmetros de função e os tipos de retorno sejam usados ​​cercando-os com colchetes angulares, como no exemplo a seguir:

fn transform(iter: <Iterator>) -> <Iterator> {
    // ...
}

O compilador então monomorfizaria o parâmetro usando as mesmas regras atualmente aplicadas aos genéricos. O tipo de retorno pode, por exemplo, ser derivado da implementação das funções. Isso significa que você não pode simplesmente chamar esse método em um Box<Trait_with_transform> ou usá-lo em objetos despachados dinamicamente em geral, mas ainda tornaria as regras mais permissivas. Eu não li toda a discussão do RFC, então talvez já exista uma solução melhor que eu tenha perdido.

Eu prefiro a sintaxe de palavra-chave impl porque quando você lê a documentação rapidamente, isso permite menos maneiras de interpretar mal os protótipos.

Uma cor diferente no realce de sintaxe deve resolver o problema.

Este artigo de Stroustrup discute escolhas sintáticas semelhantes para conceitos de C++ na seção 7: http://www.stroustrup.com/good_concepts.pdf

Não use a mesma sintaxe para genéricos e existenciais. Eles não são a mesma coisa. Os genéricos permitem que o chamador decida qual é o tipo concreto, enquanto (esse subconjunto restrito de) existenciais permite que a função que está sendo chamada decida qual é o tipo concreto. Este exemplo:

fn transform(iter: <Iterator>) -> <Iterator>

deve ser equivalente a isso

fn transform<T: Iterator, U: Iterator>(iter: T) -> U

ou deve ser equivalente a isso

fn transform(iter: impl Iterator) -> impl Iterator

O último exemplo não compilará corretamente, mesmo no nightly, e não é realmente chamável com a característica do iterador, mas uma característica como FromIter permitiria ao chamador construir uma instância e passá-la para a função sem poder para determinar o tipo concreto do que eles estão passando.

Talvez a sintaxe deva ser semelhante, mas não deve ser a mesma.

Não há necessidade de distinguir nenhum dos (genéricos) ou alguns dos (existenciais) no nome do tipo, depende de onde o tipo é usado. Quando usado em variáveis, argumentos e campos de struct sempre aceitam qualquer um de T, quando usado em tipo de retorno fn sempre obtém um pouco de T.

  • use Type , &Type , Box<Type> para tipos de dados concretos, despacho estático
  • use @Trait , &@Trait , Box<@Trait> e parâmetro de tipo genérico para tipo de dados abstrato, despacho estático
  • use &Trait , Box<Trait> para tipo de dados abstrato, despacho dinâmico

fn func(x: @Trait) é equivalente a fn func<T: Trait>(x: T) .
fn func<T1: Trait, T2: Trait>(x: T1, y: T2) pode ser simplesmente escrito como fn func(x: <strong i="22">@Trait</strong>, y: @Trait) .
T parâmetro fn func<T: Trait>(x: T, y: T) .

struct Foo { field: <strong i="28">@Trait</strong> } é equivalente a struct Foo<T: Trait> { field: T } .

Quando usado em variáveis, argumentos e campos de struct sempre aceitam qualquer um de T, quando usado em tipo de retorno fn sempre obtém um pouco de T.

Você pode retornar qualquer traço, agora mesmo, em Rust estável, usando a sintaxe genérica existente. É um recurso muito usado. serde_json::de::from_slice recebe &[u8] como parâmetro e retorna T where T: Deserialize .

Você também pode retornar significativamente algumas das Características, e esse é o recurso que estamos discutindo. Você não pode usar existenciais para a função desserialize, assim como não pode usar genéricos para retornar encerramentos sem caixa. São características diferentes.

Para um exemplo mais familiar, Iterator::collect pode retornar qualquer T where T: FromIterator<Self::Item> , sugerindo minha notação preferida: fn collect(self) -> any FromIterator<Self::Item> .

Que tal a sintaxe
fn foo () -> _ : Trait { ... }
para valores de retorno e
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
para parâmetros?

Para mim, nenhuma das novas sugestões chega perto de impl Trait em sua elegância. impl é uma palavra-chave já conhecida por todos os programadores de ferrugem e, como é usada para implementar traits, na verdade sugere o que o recurso está fazendo por conta própria.

Sim, ficar com palavras-chave existentes parece ideal para mim; Eu gostaria de ver impl para existenciais e for para universais.

Eu pessoalmente me oponho a ter any e some combinados em uma palavra-chave/sigilo

@eddyb Eu não consideraria uma conflação. Segue naturalmente da regra:

((∃ T . F⟨T⟩) → R)  →  ∀ T . (F⟨T⟩ → R)

Edit: é unidirecional, não um isomorfismo.


Não relacionado: Existe alguma proposta relacionada para permitir também impl Trait em outras posições covariantes, como

~ferrugemfn foo(retorno de chamada: F) -> Ronde F: FnOnce(impl SomeTrait) -> R {callback(create_something())}~

No momento , esse não é um recurso necessário, pois você sempre pode colocar um tempo concreto para impl SomeTrait , o que prejudica a legibilidade, mas de outra forma não é grande coisa.

Mas se o recurso RFC 1522 se estabilizar, seria impossível atribuir uma assinatura de tipo a programas como o acima se create_something resultar em impl SomeTrait (pelo menos sem encaixotá-lo). Eu acho que isso é problemático.

@Rufflewind No mundo real, as coisas não são tão claras, e esse recurso é uma marca muito específica de existenciais (Rust já tem vários).

Mas mesmo assim, tudo o que você tem é o uso de covariância para determinar o que impl Trait significa dentro e fora dos argumentos da função.

Isso não é suficiente para:

  • usando o oposto do padrão
  • desambiguando dentro do tipo de um campo (onde any e some são igualmente desejáveis)

@Rufflewind Isso parece o bracketing errado para o que impl Trait é. Eu sei que Haskell explora esse relacionamento para usar apenas a palavra-chave forall para representar universais e existenciais, mas não funciona no contexto que estamos discutindo.

Veja esta definição, por exemplo:

fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }

Se usarmos a regra de que " impl em argumentos é universal, impl em tipos de retorno é existencial", então o tipo de item da função foo é logicamente este (em notação de tipo inventada):

forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))

Ingenuamente tratar impl como tecnicamente apenas significando universal ou apenas significando existencial e deixar a lógica funcionar por si só não funciona. Você obteria isso:

forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Ou isto:

exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

E nenhuma delas se reduz ao que queremos por regras lógicas. Então, em última análise, any / some captam uma distinção importante você não pode capturar com uma única palavra-chave. Existem até exemplos razoáveis ​​em std onde você quer universais na posição de retorno. Por exemplo, este método Iterator :

fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;

E não há como escrevê-lo com impl e a regra de argumento/retorno.

tl;dr tendo impl contextualmente denotando universal ou existencial realmente lhe dá dois significados distintos.


Para referência, na minha notação, o relacionamento forall/exists @Rufflewind mencionado se parece com:

fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)

O que está relacionado ao conceito de objetos traços (existenciais) serem equivalentes a genéricos (universais), mas não a essa questão impl Trait .

Dito isso, não sou mais a favor de any / some . Eu queria ser preciso sobre o que estamos falando, e any / some ter essa gentileza teórica e visual, mas eu ficaria bem em usar impl com o contexto regra. Acho que abrange todos os casos comuns, evita problemas de gramática de palavras-chave contextuais e podemos cair para parâmetros de tipo nomeados para o resto.

Nessa nota, para corresponder à generalidade total dos universais, acho que eventualmente precisaremos de uma sintaxe para existenciais nomeados, que permite cláusulas where arbitrárias e a capacidade de usar o mesmo existencial em vários lugares na assinatura.

Em resumo, eu ficaria feliz com:

  • impl Trait como abreviação para universais e existenciais (contextualmente).
  • Parâmetros de tipo nomeados como a extensão totalmente geral para universais e existenciais. (Menos comumente necessário.)

Tratar ingenuamente impl como tecnicamente apenas significando universal ou apenas existencial e deixar a lógica funcionar por si só não funciona. Você obteria isso:

@solson Para mim, uma tradução “ingênua” resultaria nos quantificadores existenciais ao lado do tipo que está sendo quantificado. Portanto

~ferrugem(impl MyTrait)~

é apenas açúcar sintático para

~ferrugem(existeT)~

que é uma transformação local simples. Assim, uma tradução ingênua obedecendo à regra “ impl é sempre existencial” resultaria em:

~ferrugemfn(existeT) -> (existeR)~

Então, se você retirar o quantificador do argumento da função, ele se torna

~ferrugemporfn(T) -> (existeR)~

Portanto, embora T seja sempre existencial em relação a si mesmo, ele aparece como universal em relação a todo o tipo de função.


IMO, acho que impl pode se tornar a palavra-chave de fato para tipos existenciais. No futuro, talvez se possa construir tipos existenciais mais complicados como:

~~ferrugem(impl(Vec, T))~ ~

em analogia aos tipos universais (via HRTB)

~ferrugem(para<'a> FnOnce(&'a T))~

@Rufflewind Essa visualização não funciona porque fn(T) -> (exists<R: ReturnTrait>(R)) não é logicamente equivalente a exists<R: ReturnTrait>(fn(T) -> R) , que é o que o tipo de retorno impl Trait realmente significa.

(Pelo menos não na lógica construtiva geralmente aplicada a sistemas de tipos, onde o testemunho específico escolhido para um existencial é relevante. O primeiro implica que a função pode escolher diferentes tipos para retornar com base, digamos, nos argumentos, enquanto o último implica que há um tipo específico para todas as invocações da função, como é o caso de impl Trait .)

Eu sinto que estamos ficando um pouco longe, também. Acho que impl contextual é um bom compromisso a ser feito, e não acho que buscar esse tipo de justificativa seja necessário ou particularmente útil (certamente não ensinaríamos a regra em termos desse tipo de conexão lógica ).

@solson Sim, você está certo: os existenciais não podem ser divulgados. Este não se sustenta em geral:

(T → ∃R. f(R))  ⥇  ∃R. T → f(R)

considerando que estes valem em geral:

(∃R. T → f(R))  →   T → ∃R. f(R)
(∀A. g(A) → T)  ↔  ((∃A. g(A)) → T)

O último é responsável pela reinterpretação dos existenciais em argumentos como genéricos.

Edit: Opa, (∀A. g(A) → T) → (∃A. g(A)) → T vale .

Publiquei um RFC com uma proposta detalhada para expandir e estabilizar impl Trait . Ele se baseia em muito da discussão sobre este e tópicos anteriores.

Vale a pena notar que https://github.com/rust-lang/rfcs/pull/1951 foi aceito.

Qual é o status disso atualmente? Temos um RFC que chegou, temos pessoas usando a implementação inicial, mas não estou claro sobre quais itens estão fazendo.

Foi encontrado em #43869 que a função -> impl Trait não suporta um corpo puramente divergente:

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

Isso é esperado (já que ! não implementa Iterator ), ou considerado um bug?

Que tal definir tipos inferidos, que não só podem ser usados ​​como valores de retorno, mas como qualquer coisa (eu acho) que um tipo pode ser usado atualmente?
Algo como:
type Foo: FnOnce() -> f32 = #[infer];
Ou com uma palavra-chave:
infer Foo: FnOnce() -> f32;

O tipo Foo pode ser usado como um tipo de retorno, tipo de parâmetro ou qualquer outra coisa para a qual um tipo possa ser usado, mas seria ilegal usá-lo em dois lugares diferentes que exigem um tipo diferente, mesmo que isso type implementa FnOnce() -> f32 em ambos os casos. Por exemplo, o seguinte não compilaria:

infer Foo: FnOnce() -> f32;

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo {
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Isso não deve compilar porque mesmo que os tipos de retorno de return_closure e return_closure2 sejam FnOnce() -> f32 , seus tipos são realmente diferentes, porque não há dois closures com o mesmo tipo em Rust . Para que o acima seja compilado, você precisaria definir dois tipos inferidos diferentes:

infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo2 { //Changed Foo to Foo2
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Acho que o que está acontecendo aqui é bastante óbvio depois de ver o código, mesmo que você não saiba de antemão o que a palavra-chave infer faz, e é muito flexível.

A palavra-chave infer (ou macro) essencialmente diria ao compilador para descobrir qual é o tipo, com base em onde ele é usado. Se o compilador não for capaz de inferir o tipo, ele lançará um erro, isso pode acontecer quando não houver informações suficientes para restringir o tipo que deve ser (se o tipo inferido não for usado em nenhum lugar, por exemplo, embora talvez seja melhor fazer desse caso específico um aviso), ou quando for impossível encontrar um tipo que se encaixe em todos os lugares em que é usado (como no exemplo acima).

@cramertj Ahh, então é por isso que esse problema ficou tão silencioso ..

Então, @cramertj estava me perguntando sobre como eu achava que seria melhor resolver o problema de regiões atrasadas que eles encontraram em suas relações públicas. Minha opinião é que provavelmente queremos "reorganizar" um pouco nossa implementação para tentar esperar o modelo anonymous type Foo .

Por contexto, a ideia é mais ou menos que

fn foo<'a, 'b, T, U>() -> impl Debug + 'a

seria (mais ou menos) desaçucarado para algo assim

anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>

Observe que neste formulário, você pode ver quais parâmetros genéricos são capturados porque aparecem como argumentos para Foo -- notadamente, 'b não é capturado, porque não aparece na referência de traço em de qualquer forma, mas os parâmetros de tipo T e U sempre são.

De qualquer forma, atualmente no compilador, quando você tem uma referência impl Debug , criamos um def-id que -- efetivamente -- representa esse tipo anônimo. Então temos a consulta generics_of , que calcula seus parâmetros genéricos. No momento, isso retorna o mesmo que o contexto "incluso" -- ou seja, a função foo . É isso que queremos mudar.

Do "outro lado", ou seja, na assinatura de foo , representamos impl Foo como um TyAnon . Isso está basicamente certo -- o TyAnon representa a referência a Foo que vemos na remoção de açúcar acima. Mas a maneira como obtemos os "substs" para esse tipo é usando a função "identity" , que está claramente errada - ou pelo menos não generaliza.

Então, em particular, há um tipo de "violação de namespace" ocorrendo aqui. Quando geramos as subs de "identidade" para um item, isso normalmente nos dá as substituições que usaríamos ao verificar o tipo desse item -- isto é, com todos os seus parâmetros genéricos no escopo. Mas neste caso, estamos criando a referência a Foo que aparece dentro da função foo() , e assim queremos que os parâmetros genéricos de foo() apareçam em Substs , não os de Foo . Isso acontece porque agora eles são a mesma coisa, mas não é realmente certo .

Acho que o que devemos fazer é algo assim:

Primeiro, quando computamos os parâmetros de tipo genérico de Foo (ou seja, o próprio tipo anônimo), começaríamos a construir um novo conjunto de genéricos. Naturalmente incluiria os tipos. Mas, por muitas vidas, caminharíamos sobre os limites dos traços e identificaríamos cada uma das regiões que aparecem dentro deles. Isso é muito semelhante a este código existente que cramertj escreveu , exceto que não queremos acumular def-ids, porque nem todas as regiões no escopo têm def-ids.

Acho que o que queremos fazer é acumular o conjunto de regiões que aparecem e colocá-las em alguma ordem, e também rastrear os valores dessas regiões do ponto de vista de foo() . É um pouco chato fazer isso, porque não temos uma estrutura de dados uniforme que represente uma região lógica. (Costumávamos ter a noção de FreeRegion , o que quase teria funcionado, mas não usamos mais FreeRegion para coisas com encadernação antecipada, apenas para coisas com encadernação tardia.)

Talvez a opção mais fácil e melhor seria usar apenas um Region<'tcx> , mas você teria que mudar as profundidades do índice debruijn à medida que for "cancelar" qualquer fichário que tenha sido introduzido. Esta é talvez a melhor escolha embora.

Então, basicamente, à medida que obtemos callbacks em visit_lifetime , nós os transformaríamos em Region<'tcx> expressos na profundidade inicial (teremos que rastrear enquanto passamos pelos fichários). Vamos acumulá-los em um vetor, eliminando duplicatas.

Quando terminarmos, teremos duas coisas:

  • Primeiro, para cada região no vetor, precisamos criar um parâmetro de região genérico. Todos eles podem ter nomes anônimos ou qualquer outra coisa, não importa muito (embora talvez precisemos que eles tenham def-ids ou algo assim...? Eu tenho que olhar para as estruturas de dados RegionParameterDef ...) .
  • Em segundo lugar, as regiões no vetor também são as coisas que queremos usar para os "substs".

OK, desculpe se isso é enigmático. Eu não consigo descobrir como dizer isso com mais clareza. Algo que eu não tenho certeza - agora, eu sinto que nosso manuseio de regiões é bastante complexo, então talvez haja uma maneira de refatorar as coisas para torná-lo mais uniforme? Eu apostaria $ 10 que @eddyb tem alguns pensamentos aqui. ;)

@nikomatsakis Eu acredito que muito disso é semelhante ao que eu disse a @cramertj , mas mais detalhado!

Estive pensando em impl Trait existencial e me deparei com um caso curioso em que acho que devemos proceder com cautela. Considere esta função:

trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
  ()
}

Como você pode validar no play , esse código compila hoje. No entanto, se investigarmos o que está acontecendo, isso destaca algo que tem um perigo de "compatibilidade futura" que me preocupa.

Especificamente, está claro como deduzimos o tipo que está sendo retornado aqui ( () ). É menos claro como deduzimos o tipo do parâmetro impl Debug . Ou seja, você pode pensar nesse valor de retorno como algo como -> ?T onde ?T: Foo<?U> . Temos que deduzir os valores de ?T e ?U base apenas no fato de que ?T = () .

No momento, fazemos isso aproveitando o fato de que existe apenas um impl. No entanto, esta é uma propriedade frágil. Se um novo impl for adicionado, o código não será mais compilado , porque agora não podemos determinar exclusivamente o que ?U deve ser.

Isso pode acontecer em muitos cenários no Rust - o que é bastante preocupante, mas ortogonal - mas há algo diferente no caso impl Trait . No caso de impl Trait , não temos como o usuário adicionar anotações de tipo para guiar a inferência! Nem temos realmente um plano para tal. A única solução é alterar a interface fn para impl Foo<()> ou algo mais explícito.

No futuro, usando abstract type , pode-se imaginar permitir que os usuários forneçam explicitamente o valor oculto (ou talvez apenas dicas incompletas, usando _ ), o que poderia ajudar a inferir, mantendo aproximadamente o mesma interface pública

abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
  ()
}

Ainda assim, acho que seria prudente evitar estabilizar usos "aninhados" de impl existencial Trait, exceto em ligações de tipo associado (por exemplo, impl Iterator<Item = impl Debug> não sofre com essas ambiguidades).

No caso do impl Trait, não temos como os usuários adicionarem anotações de tipo para guiar a inferência! Nem temos realmente um plano para tal.

Talvez possa parecer UFCS? por exemplo <() as Foo<()>> -- não alterando o tipo como um simples as , apenas desambiguando-o. Esta é uma sintaxe inválida no momento, pois espera que :: e muito mais se sigam.

Acabei de encontrar um caso interessante sobre inferência de tipos com impl Trait for Fn :
O código a seguir compila bem :

fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
    match s {
        "+" => ::std::ops::Add::add,
        "-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    }
}

Se comentarmos a Sub-linha, um erro de compilação é lançado :

error[E0308]: match arms have incompatible types
 --> src/main.rs:4:5
  |
4 | /     match s {
5 | |         "+" => ::std::ops::Add::add,
6 | | //         "-" => ::std::ops::Sub::sub,
7 | |         "<" => |a,b| (a < b) as i32,
8 | |         _ => unimplemented!(),
9 | |     }
  | |_____^ expected fn item, found closure
  |
  = note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
             found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
 --> src/main.rs:7:16
  |
7 |         "<" => |a,b| (a < b) as i32,
  |                ^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

@oberien Isso não parece relacionado a impl Trait - é verdade para inferência em geral. Tente esta pequena modificação do seu exemplo:

fn main() {
    let _: i32 = (match "" {
        "+" => ::std::ops::Add::add,
        //"-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    })(5, 5);
}

Parece que agora está fechado:

ICEs ao interagir com elisão

Uma coisa que não vejo listada nesta edição ou na discussão é a capacidade de armazenar encerramentos e geradores – que não são fornecidos pelo chamador – em campos de struct. No momento, isso é possível, mas parece feio: você precisa adicionar um parâmetro de tipo à estrutura para cada campo de fechamento/gerador e, em seguida, na assinatura da função construtora, substituir esse parâmetro de tipo por impl FnMut/impl Generator . Aqui está um exemplo , e funciona, o que é muito legal! Mas deixa muito a desejar. Seria muito melhor se você pudesse se livrar do parâmetro de tipo:

struct Counter(impl Generator<Yield=i32, Return=!>);

impl Counter {
    fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

impl Trait pode não ser a maneira correta de fazer isso – provavelmente tipos abstratos, se eu li e entendi a RFC 2071 corretamente. O que precisamos é algo que possamos escrever na definição da estrutura para que o tipo real ( [generator@src/main.rs:15:17: 21:10 _] ) possa ser inferido.

Os tipos abstratos

abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

Existe um caminho de fallback se for impl Generator de outra pessoa que eu quero colocar no meu struct, mas eles não fizeram um abstract type para eu usar?

@scottmcm Você ainda pode declarar seu próprio abstract type :

// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }

// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        let inner: MyGenerator = foo();
        Counter(inner)
    }
}

@cramertj Espere, os tipos abstratos já estão no nightly?! Onde está o PR?

@alexreg Não, eles não são.

Edit: Saudações, visitantes do futuro! O problema abaixo foi resolvido.


Eu gostaria de chamar a atenção para este caso de uso estranho que aparece em #47348

use ::std::ops::Sub;

fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }

O retorno de uma projeção em impl Trait como este deveria ser permitido? (porque atualmente, __é.__)

Não consegui localizar nenhuma discussão sobre o uso como esse, nem encontrei nenhum caso de teste para isso.

@ExpHP Hum. Parece problemático, pela mesma razão que impl Foo<impl Bar> é problemático. Basicamente, não temos nenhuma restrição real sobre o tipo em questão - apenas sobre as coisas projetadas a partir dele.

Acho que queremos reutilizar a lógica em torno de "parâmetros de tipo restrito" de impls. Resumindo, especificar o tipo de retorno deve "restringir" o impl Sub . A função a que me refiro é esta:

https://github.com/rust-lang/rust/blob/a0dcecff90c45ad5d4eb60859e22bb3f1b03842a/src/librustc_typeck/constrained_type_params.rs#L89 -L93

Um pouquinho de triagem para pessoas que gostam de caixas de seleção:

  • #46464 está feito -> caixa de seleção
  • #48072 está feito -> caixa de seleção

@rfcbot fcp mesclar

Proponho que estabilizemos os recursos conservative_impl_trait e universal_impl_trait , com uma alteração pendente (uma correção para https://github.com/rust-lang/rust/issues/46541).

Testes que documentam a semântica atual

Os testes para esses recursos podem ser encontrados nos seguintes diretórios:

run-pass/impl-trait
ui/impl-trait
compile-fail/impl-trait

Questões Resolvidas Durante a Implementação

Os detalhes da análise de impl Trait foram resolvidos na RFC 2250 e implementados em https://github.com/rust-lang/rust/pull/45294.

impl Trait foi banido da posição do tipo aninhado não associado e de certas posições de caminho qualificado para evitar ambiguidade. Isso foi implementado em https://github.com/rust-lang/rust/pull/48084.

Características instáveis ​​restantes

Após esta estabilização, será possível utilizar impl Trait em posição de argumento e posição de retorno de funções não-características. No entanto, o uso de impl Trait em qualquer lugar na sintaxe Fn ainda não é permitido para permitir futuras iterações de design. Além disso, não é permitido especificar manualmente os parâmetros de tipo de funções que usam impl Trait na posição do argumento.

O membro da equipe @cramertj propôs mesclar isso. O próximo passo é a revisão pelo resto das equipes marcadas:

  • [x] @aturon
  • [x] @cramertj
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @withoutboats

Nenhuma preocupação listada no momento.

Uma vez que a maioria dos revisores aprove (e nenhum se oponha), isso entrará em seu período final de comentários. Se você identificar um problema importante que não foi levantado em nenhum momento deste processo, por favor, fale!

Consulte este documento para obter informações sobre quais comandos os membros da equipe marcados podem me fornecer.

Após esta estabilização, será possível utilizar impl Trait em posição de argumento e posição de retorno de funções não-características. No entanto, o uso de impl Trait em qualquer lugar na sintaxe Fn ainda não é permitido para permitir futuras iterações de design. Além disso, não é permitido especificar manualmente os parâmetros de tipo de funções que usam impl Trait na posição do argumento.

Qual é o status de usar impl Trait em posições de argumento/retorno em funções de traço, ou na sintaxe Fn, para esse assunto?

@alexreg A posição de retorno impl Trait em traits está bloqueada em um RFC, embora o RFC 2071 permita uma funcionalidade semelhante uma vez implementada. A posição de argumento impl Trait em traits não está bloqueada em nenhum recurso técnico que eu conheça, mas não foi explicitamente permitida na RFC, portanto foi omitida por enquanto.

impl Trait na posição de argumento da sintaxe Fn está bloqueado no HRTB de nível de tipo, já que algumas pessoas pensam que T: Fn(impl Trait) deve desugar para T: for<X: Trait> Fn(X) . impl Trait na posição de retorno da sintaxe Fn não está bloqueada por nenhum motivo técnico que eu saiba, mas foi desautorizado na RFC pendente de trabalho de design adicional-- eu esperaria veja outro RFC ou pelo menos um FCP separado antes de estabilizar isso.

@cramertj Ok, obrigado pela atualização. Espero que possamos ver esses dois recursos que não estão bloqueados em nada em breve, após alguma discussão. O desaçucar faz sentido, na posição de argumento, um argumento foo: T onde T: Trait é equivalente a foo: impl Trait , a menos que eu esteja enganado.

Preocupação: https://github.com/rust-lang/rust/issues/34511#issuecomment -322340401 ainda é o mesmo. É possível permitir o seguinte?

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

@kennytm Não, isso não é possível no momento. Essa função retorna ! , que não implementa a característica que você forneceu, nem temos um mecanismo para convertê-la em um tipo apropriado. Isso é lamentável, mas não há uma maneira fácil de corrigi-lo agora (além de implementar mais características para ! ). Também é compatível com versões anteriores para corrigir no futuro, pois fazê-lo funcionar permitiria que mais código fosse compilado.

A questão do turbofish foi resolvida apenas pela metade. Devemos pelo menos avisar sobre impl Trait em argumentos de funções efetivamente públicas, considerando impl Trait em argumentos como um tipo privado para o novo private in public check .

A motivação é evitar que as libs quebrem os turbofishes dos usuários alterando um argumento de genérico explícito para impl Trait . Ainda não temos um bom guia de referência para libs saberem o que é e o que não é uma mudança importante e é muito improvável que os testes detectem isso. Esta questão não foi suficientemente discutida, se quisermos nos estabilizar antes de decidir completamente, devemos pelo menos apontar a arma para longe do pé dos autores da lib.

A motivação é evitar que as libs quebrem os turbofishes dos usuários alterando um argumento de genérico explícito para impl Trait .

Espero que quando isso começar a acontecer e as pessoas começarem a reclamar, as pessoas da equipe lang que estão atualmente em dúvida estarão convencidas de que impl Trait deve suportar explicitamente o fornecimento de argumentos de tipo com turbofish.

@leodasvacas

A questão do turbofish foi resolvida apenas pela metade. Devemos pelo menos alertar sobre impl Trait em argumentos de funções efetivamente públicas, considerando impl Trait em argumentos como um tipo privado para o novo privado em verificação pública.

Eu discordo - isso foi resolvido. Por enquanto, estamos proibindo completamente o turbofish para essas funções. Alterar a assinatura de uma função pública para usar impl Trait vez de parâmetros genéricos explícitos é uma alteração importante.

Se permitirmos turbofish para essas funções no futuro, provavelmente só permitirá especificar parâmetros do tipo não impl Trait .

:bell: Isso agora está entrando em seu período final de comentários , conforme a revisão acima . :Sino:

Devo acrescentar que não quero estabilizar até https://github.com/rust-lang/rust/pull/49041 chegar. (Mas espero que seja em breve.)

Portanto, #49041 contém uma correção para #46541, mas essa correção tem mais impacto do que eu previa - por exemplo, o compilador não inicializa agora - e está me dando uma medida de pausa sobre o curso certo aqui. O problema em #49041 é que podemos acidentalmente permitir que vidas vazem que não deveríamos. Aqui está como isso se manifesta no compilador. Podemos ter um método como este:

impl TyCtxt<'cx, 'gcx, 'tcx>
where 'gcx: 'tcx, 'tcx: 'cx
{
    fn foos(self) -> impl Iterator<Item = &'tcx Foo> + 'cx {
        /* returns some type `Baz<'cx, 'gcx, 'tcx>` that captures self */
    }
}

A principal coisa aqui é que TyCtxt é invariante w/r/t 'tcx e 'gcx , então eles devem aparecer no tipo de retorno. E, no entanto, apenas 'cx e 'tcx aparecem nos limites da característica imp, portanto, apenas essas duas vidas devem ser "capturadas". O compilador antigo estava aceitando isso porque 'gcx: 'cx , mas isso não é realmente correto se você pensar na remoção de açúcar que temos em mente. Essa remoção de açúcar criaria um tipo abstrato como este:

abstract type Foos<'cx, 'tcx>: Iterator<Item = &'tcx Foo> + 'cx;

e ainda o valor para este tipo abstrato seria Baz<'cx, 'gcx, 'tcx> -- mas 'gcx não está no escopo!

A solução aqui é que temos que nomear 'gcx nos limites. Isso é meio chato de fazer; não podemos usar 'cx + 'gcx . Podemos supor fazer um traço fictício:

trait Captures<'a> { }
impl<T: ?Sized> Captures<'a> for T { }

e, em seguida, retorne algo assim impl Iterator<Item = &'tcx Foo> + Captures<'gcx> + Captures<'cx> .

Algo que esqueci de observar: se o tipo de retorno declarado fosse dyn Iterator<Item = &'tcx Foo> + 'cx , tudo bem, porque se espera que os tipos dyn apaguem vidas. Portanto, eu não acredito que qualquer mal-estar seja possível aqui, presumindo que você não possa fazer nada problemático com um impl Trait que não seria possível com um dyn Trait .

Pode-se imaginar vagamente a ideia de que o valor do tipo abstrato é um existencial semelhante: exists<'gcx> Baz<'cx, 'gcx, 'tcx> .

No entanto, parece-me ok estabilizar um subconjunto conservador (que exclui o fns acima) e revisitá-lo como uma possível expansão mais tarde, uma vez que tenhamos decidido como queremos pensar sobre isso.

ATUALIZAÇÃO: Para esclarecer meu significado sobre dyn traços: estou dizendo que eles podem "esconder" uma vida como 'gcx desde que o limite ( 'cx , aqui) garanta que 'gcx ainda estará ativo onde quer que o dyn Trait seja usado.

@nikomatsakis Esse é um exemplo interessante, mas não acho que mude o cálculo básico aqui, ou seja, queremos que todos os tempos de vida relevantes sejam claros apenas pelo tipo de retorno.

O traço Captures parece ser uma abordagem boa e leve para essa situação. Parece que poderia entrar em std::marker como instável no momento?

@nikomatsakis Seu comentário de acompanhamento me fez perceber que não tinha juntado todas as peças aqui para entender por que você pode esperar elidir o 'gcx neste caso, ou seja, 'gcx não é um "vida útil relevante" do ponto de vista do cliente. De qualquer forma, começar de forma conservadora parece bom.

Minha opinião pessoal é que https://github.com/rust-lang/rust/issues/46541 não é realmente um bug - é o comportamento que eu esperaria, não vejo como isso poderia ser infundado, e é uma dor para contornar. IMO, deve ser possível retornar um tipo que implemente Trait e sobreviva ao tempo de vida 'a como impl Trait + 'a , não importa quais outros tempos de vida ele contenha. No entanto, estou bem em estabilizar uma abordagem mais conservadora para começar, se é isso que @rust-lang/lang prefere.

(Uma outra coisa para esclarecer: a única vez que você obterá erros com a correção em #49041 é quando o tipo oculto é invariável em relação ao tempo de vida ausente 'gcx , então isso provavelmente ocorre relativamente raramente.)

@cramertj

Minha opinião pessoal é que #46541 não é realmente um bug-- é o comportamento que eu esperaria, não vejo como isso poderia ser infundado, e é difícil de contornar.

Eu sou simpático a esse ponto de vista, mas estou relutante em estabilizar algo para o qual não entendemos como desaçucar (por exemplo, porque parece depender de alguma noção vaga de vidas existenciais).

@rfcbot diz respeito a vários sites de retorno

Gostaria de registrar uma última preocupação com o traço impl existencial. Uma fração substancial das vezes que eu quero usar o traço impl, eu realmente quero retornar mais de um tipo. Por exemplo:

fn foo(empty: bool) -> impl Iterator<Item = u32> {
    if empty { None.into_iter() } else { &[1, 2, 3].cloned() }
}

É claro que isso não funciona hoje, e definitivamente está fora do escopo fazê-lo funcionar. No entanto, a maneira que impl característica funciona agora, estamos efetivamente fechando a porta para que ele funcione sempre (com que a sintaxe). Isso ocorre porque - atualmente - você pode acumular restrições de vários sites de retorno:

fn foo(empty: bool) -> (impl Debug, impl Debug) {
    if empty { return (22, Default::default()); }
    return (Default::default(), false);
}

Aqui, o tipo inferido é (i32, bool) , onde o primeiro return restringe a parte i32 e o segundo return restringe a parte bool .

Isso implica que nunca poderíamos suportar casos em que as duas instruções return não unificam (como no meu primeiro exemplo) -- ou seria muito chato fazer isso.

Gostaria de saber se devemos colocar uma restrição que exija que cada return (em geral, cada fonte de uma restrição) seja totalmente especificado de forma independente? (E nós os unificamos após o fato?)

Isso tornaria meu segundo exemplo ilegal e deixaria espaço para potencialmente apoiarmos o primeiro caso em algum momento no futuro.

@rfcbot resolve vários sites de retorno

Então conversei um pouco com @cramertj em #rust-lang . Estávamos discutindo a ideia de tornar o "retorno antecipado" instável para impl Trait , para que pudéssemos eventualmente alterá-lo.

Eles argumentaram que seria melhor ter um opt-in explícito para esse tipo de sintaxe, especificamente porque existem outros casos (por exemplo, let x: impl Trait = if { ... } else { ... } ) em que alguém gostaria, e não podemos esperar para lidar com todos eles implicitamente (definitivamente não).

Acho isso bastante persuasivo. Antes disso, eu estava assumindo que teríamos alguma sintaxe opt-in aqui de qualquer maneira, mas eu só queria ter certeza de que não fechamos nenhuma porta prematuramente. Afinal, explicar quando você precisa inserir o "calço dinâmico" é meio complicado.

@nikomatsakis Apenas minha opinião possivelmente menos informada: Embora permitir que uma função retorne um dos vários tipos possíveis em tempo de execução possa ser útil, eu ficaria relutante em ter a mesma sintaxe para inferência de tipo de retorno estático para um único tipo e permitindo situações em que alguma decisão em tempo de execução é necessária internamente (o que você acabou de chamar de "calço dinâmico").

Esse primeiro exemplo foo , até onde entendi o problema, poderia resolver para (1) um Iterator<Item = u32> box + type-erased ou (2) um tipo de soma de std::option::Iter ou std::slice::Iter , que por sua vez derivaria uma implementação de Iterator . Tentando ser breve, já que houve algumas atualizações na discussão (ou seja, eu li os logs do IRC agora) e está ficando mais difícil de entender: eu certamente concordaria com uma sintaxe dyn para o shim dinâmico, embora eu também entenda que chamar de dyn pode não ser o ideal.

Plug sem vergonha e uma pequena nota apenas para o registro: você pode obter tipos e produtos de soma "anônimos" facilmente com:

@Centril Sim, essas coisas do frunk são super legais. No entanto, observe que para que CoprodInjector::inject funcione, o tipo resultante deve ser inferível, o que geralmente é impossível sem nomear o tipo resultante (por exemplo, -> Coprod!(A, B, C) ). Muitas vezes, você está trabalhando com tipos inomináveis, então você precisa de -> Coprod!(impl Trait, impl Trait, impl Trait) , que falhará na inferência porque não sabe qual variante deve conter qual impl Trait tipo.

@cramertj Muito verdadeiro ( Map<Namable, Unnameable> ).

A ideia enum impl Trait foi discutida antes em https://internals.rust-lang.org/t/pre-rfc-anonymous-enums/5695

@Centril Sim, é verdade. Estou pensando especificamente em futuros, onde costumo escrever coisas como

fn foo(x: Foo) -> impl Future<Item = (), Error = Never> {
    match x {
        Foo::Bar => do_request().and_then(|res| ...).left().left(),
        Foo::Baz => do_other_thing().and_then(|res| ...).left().right(),
        Foo::Boo => do_third_thing().and_then(|res| ...).right(),
    }
}

@cramertj Eu não diria que a enumeração anônima é semelhante a enum impl Trait , porque não podemos concluir X: Tr && Y: Tr(X|Y): Tr (contra-exemplo: Default característica). Portanto, os autores da biblioteca precisarão manualmente impl Future for (X|Y|Z|...) .

@kennytm Presumivelmente, gostaríamos de gerar automaticamente alguns impls de traço para enums anônimos, então parece basicamente o mesmo recurso.

@cramertj Como um enum anônimo pode ser nomeado (heh), se um Default impl for gerado para (i32|String) , poderíamos escrever <(i32|String)>::default() . OTOH <enum impl Default>::default() simplesmente não compilará, então não importa o que gerarmos automaticamente, ainda seria seguro, pois não pode ser invocado.

No entanto, existem alguns casos em que a geração automática ainda pode causar problemas com enum impl Trait . Considerar

pub trait Rng {
    fn next_u32(&mut self) -> u32;
    fn gen<T: Rand>(&mut self) -> T where Self: Sized;
    fn gen_iter<'a, T: Rand>(&'a mut self) -> Generator<'a, T, Self> where Self: Sized;
}

É perfeitamente normal que, se tivermos um mut rng: (XorShiftRng|IsaacRng) possamos calcular rng.next_u32() ou rng.gen::<u64>() . No entanto, rng.gen_iter::<u16>() não pode ser construído porque a geração automática só pode produzir (Generator<'a, u16, XorShiftRng>|Generator<'a, u16, IsaacRng>) , enquanto o que realmente queremos é Generator<'a, u16, (XorShiftRng|IsaacRng)> .

(Talvez o compilador possa rejeitar automaticamente uma chamada não segura de delegação, assim como a verificação Sized .)

FWIW esse recurso me parece estar mais próximo em espírito de encerramentos do que de tuplas (que são, é claro, a contrapartida anônima struct para os hipotéticos anônimos enum s). As maneiras pelas quais essas coisas são "anônimas" são diferentes.

Para struct s anônimos e enum s (tuplas e "disjunções"), o "anônimo" está no sentido de tipos "estruturais" (em oposição a "nominais") - eles são são integrados, totalmente genéricos sobre seus tipos de componentes e não são uma declaração nomeada em nenhum arquivo de origem. Mas o programador ainda os escreve e os usa como qualquer outro tipo, implementações de traços para eles são escritas explicitamente como de costume, e eles não são particularmente mágicos (além de terem sintaxe embutida e serem 'variádicos', que outros tipos ainda não pode ser). Em certo sentido, eles têm um nome, mas em vez de serem alfanuméricos, seu 'nome' é a sintaxe usada para escrevê-los (parênteses e vírgulas).

Os encerramentos, por outro lado, são anônimos no sentido de que seu nome é secreto . O compilador gera um novo tipo com um novo nome cada vez que você escreve um, e não há como descobrir qual é esse nome ou se referir a ele, mesmo que você queira. O compilador implementa uma ou duas características para esse tipo secreto, e a única maneira de interagir com ele é por meio dessas características.

Ser capaz de retornar diferentes tipos de diferentes ramificações de um if , atrás de um impl Trait , parece mais próximo do último -- o compilador gera implicitamente um tipo para conter os diferentes ramos, implementa o traço solicitado nele para despachar para o apropriado, e o programador nunca escreve ou vê que tipo é esse, e não pode se referir a ele nem tem qualquer motivo real para querer.

(Na verdade, esse recurso parece meio relacionado aos "literais de objeto" hipotéticos - que seriam para outros traços o que a sintaxe de fechamento existente é para Fn . Ou seja, em vez de uma única expressão lambda, você ' implementaria cada método da característica fornecida (com self sendo implícito) usando as variáveis ​​no escopo, e o compilador geraria um tipo anônimo para manter os upvars e implementaria a característica fornecida para ele, ele tem um modo opcional move da mesma forma, e assim por diante. De qualquer forma, eu suspeito que uma maneira diferente de expressar if foo() { (some future) } else { (other future) } seria object Future { fn poll() { if foo() { (some future).poll() } else { (other future).poll() } } } (bem, você também precisaria para elevar o resultado de foo() em let para que seja executado apenas uma vez). Isso é um pouco menos ergonômico e provavelmente não deve ser considerado uma *alternativa real para o outro recurso, mas sugere que há um relacionamento. Talvez o primeiro possa desaçucar no último, ou algo assim.)

@glaebhoerl essa é uma ideia muito interessante! Há também alguma arte anterior de Java aqui.

Alguns pensamentos em cima da minha cabeça (então não muito cozido):

  1. [bikeshed] o prefixo object sugere que este é um objeto de traço em vez de apenas existencial - mas não é.

Uma possível sintaxe alternativa:

impl Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
// ^ --
// this conflicts with inherent impls for types, so you have to delay
// things until you know whether `Future` is a type or a trait.
// This might be __very__ problematic.

// and perhaps (but probably not...):
dyn Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
  1. [macros/sugar] você pode fornecer um pouco de açúcar sintático trivial para obter:
future!(if foo() { a.poll() } else { b.poll() })

Sim, a questão da sintaxe é uma bagunça porque não está claro se você quer se inspirar em literais struct , encerramentos ou blocos impl :) Acabei de escolher um no topo da minha cabeça, por exemplo interesse. (De qualquer forma, meu ponto principal não era que deveríamos adicionar literais de objeto [embora devêssemos], mas que eu acho que enum s anônimos são uma pista falsa aqui [embora devêssemos adicioná-los também].)

Ser capaz de retornar diferentes tipos de diferentes ramos de um if, por trás de um impl Trait, parece mais próximo do último -- o compilador gera implicitamente um tipo para conter os diferentes ramos, implementa o traço solicitado nele para despachar para o apropriado, e o programador nunca anota ou vê que tipo é esse, e não pode se referir a ele nem tem qualquer razão real para querer.

Hum. Então, eu assumi que, em vez de gerar "nomes novos" para tipos de enumeração, usaríamos os tipos | , correspondentes a impls como este:

impl<A: IntoIterator, B: IntoIterator> IntoIterator for (A|B)  { /* dispatch appropriately */ }

Obviamente, haveria problemas de coerência com isso, no sentido de que várias funções gerariam imps idênticas. Mas mesmo deixando isso de lado, percebo agora que essa ideia pode não funcionar por outros motivos - por exemplo, se houver vários tipos associados, em alguns contextos eles podem ter que ser os mesmos, mas em outros podem ser diferentes. Por exemplo, talvez retornemos:

-> impl IntoIterator<Item = Y>

mas em outro lugar nós fazemos

-> impl IntoIterator<IntoIter = X, Item = Y>

Estes seriam dois imps sobrepostos que eu acho que não podem ser "unidos"; bem, talvez com especialização.

De qualquer forma, a noção de "enums secretos" parece mais limpa, suponho.

Gostaria de registrar uma última preocupação com o traço impl existencial. Uma fração substancial das vezes que eu quero usar o traço impl, eu realmente quero retornar mais de um tipo.

@nikomatsakis : É justo dizer que, neste caso, o que está sendo retornado está mais próximo de dyn Trait do que impl Trait , porque o valor de retorno sintético/anônimo implementa algo semelhante ao despacho dinâmico?

cc https://github.com/rust-lang/rust/issues/49288 , um problema que tenho encontrado muito ultimamente trabalhando com Future s e Future - métodos de traço de retorno.

Como esta é a última chance antes do fechamento do FCP, gostaria de fazer um último argumento contra as características automáticas automáticas. Sei que isso é um pouco de última hora, então, no máximo, gostaria de abordar formalmente esse problema antes de nos comprometermos com a implementação atual.

Para esclarecer para quem não tem acompanhado impl Trait , esta é a questão que estou apresentando. Um tipo representado por tipos impl X atualmente implementa automaticamente características automáticas se e somente se o tipo concreto por trás deles implementa essas características automáticas. Concretamente, se a seguinte alteração de código for feita, a função continuará a compilar, mas qualquer uso da função que dependa do fato de que o tipo que ela retorna implementa Send falhará.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(exemplo mais simples: trabalhando , mudanças internas causam falha )

Esta questão não é clara. Houve uma decisão muito deliberada de ter "vazamento" de traços automáticos: se não o fizéssemos, teríamos que colocar + !Send + !Sync em cada função que retornasse algo que não fosse Enviar ou Sincronizar, e teríamos tem uma história pouco clara com outras características automáticas em potencial que simplesmente não podem ser implementadas no tipo concreto que a função está retornando. Esses são dois problemas que abordarei mais adiante.

Primeiro, gostaria de simplesmente declarar minha objeção ao problema: isso permite alterar um corpo de função para alterar a API voltada para o público. Isso reduz diretamente a capacidade de manutenção do código.

Ao longo do desenvolvimento da ferrugem, foram tomadas decisões que erram no lado da verbosidade sobre a usabilidade. Quando os recém-chegados veem isso, eles pensam que é verbosidade por causa da verbosidade, mas não é o caso. Cada decisão, seja para não ter estruturas implementando Copiar automaticamente, ou ter todos os tipos explícitos nas assinaturas de função, é por causa da manutenção.

Quando apresento Rust às pessoas, com certeza, posso mostrar a eles velocidade, produtividade, segurança de memória. Mas ir tem velocidade. Ada tem segurança de memória. Python tem produtividade. O que Rust tem supera tudo isso, tem capacidade de manutenção. Quando um autor de biblioteca deseja alterar um algoritmo para torná-lo mais eficiente, ou quando deseja refazer a estrutura de uma caixa, ele tem uma forte garantia do compilador de que ele informará quando cometerem erros. Na ferrugem, posso ter certeza de que meu código continuará funcionando não apenas em termos de segurança de memória, mas também de lógica e interface. _Toda interface de função em Rust é totalmente representável pela declaração de tipo da função_.

Estabilizar impl Trait como está tem uma grande chance de ir contra essa crença. Claro, é extremamente bom para escrever código rapidamente, mas se eu quiser prototipar, usarei python. Rust é a linguagem de escolha quando se precisa de manutenção de longo prazo, não de código somente de escrita de curto prazo.


Eu digo que há apenas uma "grande chance" de isso ser ruim aqui porque, novamente, a questão não é clara. Toda a ideia de 'autotraços' em primeiro lugar não é explícita. Enviar e sincronizar são implementados com base no conteúdo de uma estrutura, não na declaração pública. Como essa decisão funcionou para ferrugem, impl Trait agindo de forma semelhante também poderia funcionar bem.

No entanto, funções e estruturas são usadas de maneira diferente em uma base de código, e esses não são os mesmos problemas.

Ao modificar os campos de uma estrutura, mesmo campos privados, fica imediatamente claro que se está alterando o conteúdo real da mesma. Estruturas com campos não-Send ou não-Sync fizeram essa escolha, e os mantenedores da biblioteca sabem verificar novamente quando um PR altera os campos de uma estrutura.

Ao modificar as partes internas de uma função, fica claro que uma pode afetar tanto o desempenho quanto a correção. No entanto, em Rust, não precisamos verificar se estamos retornando o tipo correto. Declarações de função são um contrato rígido que devemos manter, e rustc protege. É uma linha tênue entre características automáticas em structs e em retornos de função, mas alterar os internos de uma função é muito mais rotineiro. Assim que tivermos Future s com gerador completo, será ainda mais rotineiro modificar funções retornando -> impl Future . Essas serão todas as alterações que os autores precisam rastrear para implementações de envio/sincronização modificadas se o compilador não detectá-las.

Para resolver isso, podemos decidir que essa é uma carga de manutenção aceitável, como fez a discussão original da RFC . Esta seção na RFC conservadora de características implícitas apresenta os maiores argumentos para o vazamento de características automáticas ("OIBIT" é o nome antigo para características automáticas).

Eu já expus minha resposta principal a isso, mas aqui vai uma última nota. Alterar o layout de uma estrutura não é tão comum de se fazer; ele pode ser protegido contra. A carga de manutenção para garantir que as funções continuem a implementar as mesmas características automáticas é maior do que a das estruturas, simplesmente porque as funções mudam muito mais.


Como nota final, gostaria de dizer que as características automáticas automáticas não são a única opção. É a opção que escolhemos, mas a alternativa de autocaracterísticas de desativação ainda é uma alternativa.

Poderíamos exigir funções que retornam itens que não são de envio/sincronização para declarar + !Send + !Sync ou para retornar uma característica (alias possivelmente?) que tenha esses limites. Esta não seria uma boa decisão, mas pode ser melhor do que a que estamos escolhendo atualmente.

Quanto à preocupação com as características automáticas personalizadas, eu argumentaria que quaisquer novas características automáticas não deveriam ser implementadas apenas para novos tipos introduzidos após a característica automática. Isso pode fornecer mais problemas do que posso resolver agora, mas não é um problema que não possamos resolver com mais design.


Isso é muito tarde e muito prolixo, e tenho certeza de que já levantei essas objeções antes. Estou feliz por poder comentar uma última vez e garantir que estamos totalmente de acordo com a decisão que estamos tomando.

Obrigado por ler, e espero que a decisão final coloque Rust na melhor direção possível.

Expandindo a revisão de

trait FutureNSS<T, E> = Future<Item = T, Error= E> + !Send + !Sync;

fn does_some_operation() -> impl FutureNSS<(), ()> {
     let data_stored = Rc::new("hello");
     some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

Isso não é tão ruim - você teria que criar um bom nome (o que FutureNSS não é). O principal benefício é que reduz o corte de papel incorrido pela repetição dos limites.

Não seria possível estabilizar este recurso com os requisitos para declarar auto-características explicitamente e depois talvez remover esses requisitos assim que encontrarmos uma solução adequada para esse problema de manutenção ou quando tivermos certeza de que de fato não há encargos de manutenção por a decisão de levantar os requisitos?

Que tal exigir Send menos que esteja marcado como !Send , mas não fornecer Sync menos que esteja marcado como Sincronização? O Send não deveria ser mais comum em comparação com o Sync?

Como isso:

fn provides_send_only1() -> impl Trait {  compatible_with_Send_and_Sync }
fn provides_send_only2() -> impl Trait {  compatible_with_Send_only }
fn fails_to_complile1() -> impl Trait {  not_compatible_with_Send }
fn provides_nothing1() -> !Send + impl Trait { compatible_with_Send}
fn provides_nothing2() -> !Send + impl Trait { not_compatible_with_Send }
fn provides_send_and_sync() -> Sync + impl Trait {  compatible_with_Send_and_Sync }
fn fails_to_compile2() -> Sync + impl Trait { compatible_with_Send_only }

Existe uma inconsistência entre impl Trait na posição do argumento e na posição de retorno wrt. traços automáticos?

fn foo(x: impl ImportantTrait) {
    // Can't use Send cause we have not required it...
}

Isso faz sentido para a posição do argumento porque se você tivesse permissão para assumir Send aqui, você obteria erros de pós-monomorfização. É claro que as regras para posição de retorno e posição de argumento não precisam coincidir aqui, mas apresenta um problema em termos de capacidade de aprendizado.

Quanto à preocupação com as características automáticas personalizadas, eu argumentaria que quaisquer novas características automáticas não deveriam ser implementadas apenas para novos tipos introduzidos após a característica automática.

Bem, isso é verdade para o próximo traço automático Unpin (apenas não implementado para geradores auto-referenciais), mas... isso parece ser uma sorte idiota? Essa é uma limitação com a qual podemos realmente conviver? Eu não posso acreditar que não haverá algo no futuro que precisaria ser desabilitado por exemplo &mut ou Rc ...

Acredito que isso já foi discutido, e é claro que é muito tarde, mas ainda estou insatisfeito com impl Trait em posição de argumento.

As habilidades para a) trabalhar com closures/futuros por valor eb) tratar alguns tipos como "saídas" e, portanto, detalhes de implementação, são idiomáticas e existem desde antes da 1.0, porque suportam diretamente os valores centrais de desempenho, estabilidade, E segurança.

-> impl Trait está, portanto, apenas cumprindo uma promessa feita por 1.0, ou removendo um caso extremo, ou generalizando recursos existentes: ele adiciona tipos de saída a funções, pegando o mesmo mecanismo que sempre foi usado para lidar com tipos anônimos e aplicando-o em mais casos. Pode ter sido mais baseado em princípios começar com abstract type , ou seja, tipos de saída para módulos, mas dado que Rust não possui um sistema de módulos ML, a ordem não é grande coisa.

fn f(t: impl Trait) vez disso, parece que foi adicionado "só porque podemos", tornando a linguagem maior e mais estranha sem dar o suficiente em troca. Eu lutei e não consegui encontrar alguma estrutura existente para encaixá-lo. Eu entendo o argumento em torno da concisão de fn f(f: impl Fn(...) -> ...) , e a justificativa de que os limites já podem estar nas cláusulas <T: Trait> e where , mas elas parecem vazias. Eles não negam as desvantagens:

  • Agora você precisa aprender duas sintaxes para limites - pelo menos <> / where compartilham uma única sintaxe.

    • Isso também cria um abismo de aprendizado e obscurece a ideia de usar o mesmo tipo genérico em vários lugares.

    • A nova sintaxe torna mais difícil dizer sobre o que uma função é genérica - você precisa varrer toda a lista de argumentos.

  • Agora o que deveria ser o detalhe de implementação de uma função (como ela declara seus parâmetros de tipo) se torna parte de sua interface, porque você não pode escrever seu tipo!

    • Isso também se relaciona com as complicações de autocaracterísticas atualmente em discussão - confusão adicional do que é a interface pública de uma função e o que não é.

  • A analogia com dyn Trait é, honestamente, falsa:

    • dyn Trait sempre significa a mesma coisa, e não "infecta" suas declarações circundantes a não ser por meio do mecanismo de autocaracterística existente.

    • dyn Trait é utilizável em estruturas de dados, e este é realmente um de seus principais casos de uso. impl Trait em estruturas de dados não faz sentido sem olhar para todos os usos da estrutura de dados.

    • Parte do que dyn Trait significa é apagar tipo, mas impl Trait não implica nada sobre sua implementação.

    • O ponto anterior será ainda mais confuso se introduzirmos genéricos não monomorfizados. De fato, em tal situação, fn f(t: impl Trait) provavelmente a) não funcionará com o novo recurso e/ou b) exigirá ainda mais advocacia em casos extremos, como o problema com características automáticas. Imagine fn f<dyn T: Trait>(t: T, u: dyn impl Urait) ! :gritar:

Então, o que acontece para mim é que impl Trait na posição de argumento adiciona casos extremos, usa mais o orçamento de estranheza, faz a linguagem parecer maior, etc. enquanto impl Trait na posição de retorno unifica, simplifica e torna a linguagem mais unida.

Que tal exigir Send a menos que esteja marcado como !Send, mas não fornecer Sync a menos que esteja marcado como Sync? O Send não deveria ser mais comum em comparação com o Sync?

Isso parece muito... arbitrário e ad-hoc. Talvez seja menos digitação, mas mais memória e mais caos.

Ideia de galpão de bicicleta aqui para não distrair meus pontos acima: em vez de impl , use type ? Essa é a palavra-chave usada para tipos associados, é provável (uma das) palavras-chave usadas para abstract type , ainda é bastante natural e sugere mais a ideia de "tipos de saída para funções":

// keeping the same basic structure, just replacing the keyword:
fn f() -> type Trait

// trying to lean further into the concept:
fn f() -> type R: Trait
fn f() -> type R where R: Trait
fn f() -> (i32, type R) where R: Trait
// or perhaps:
fn f() -> type R: Trait in R
// or maybe just:
fn f() -> type: Trait

Obrigado por ler, e espero que a decisão final coloque Rust na melhor direção possível.

Eu aprecio a objeção bem escrita. Como você apontou, as características automáticas sempre foram uma escolha deliberada para "expor" alguns detalhes de implementação que se poderia esperar que permanecessem ocultos. Acho que - até agora - essa escolha funcionou muito bem, mas confesso que estou constantemente nervoso com isso.

Parece-me que a questão importante é até que ponto as funções realmente são diferentes das estruturas:

Alterar o layout de uma estrutura não é tão comum de se fazer; ele pode ser protegido contra. A carga de manutenção para garantir que as funções continuem a implementar as mesmas características automáticas é maior do que a das estruturas, simplesmente porque as funções mudam muito mais.

É realmente difícil saber até que ponto isso será verdade. Parece que a regra geral será que a introdução de Rc é algo a ser feito com cautela - não é tanto uma questão de onde você o armazena. (Na verdade, o caso que eu realmente trabalho não é Rc mas sim a introdução de dyn Trait , já que isso pode ser menos óbvio.)

Eu suspeito fortemente que no código que está retornando futuros, trabalhar com tipos não seguros para thread e assim por diante será raro. Você tenderá a evitar esses tipos de bibliotecas. (Além disso, é claro, sempre vale a pena ter testes exercitando seu código em cenários realistas.)

De qualquer forma, isso é frustrante porque é o tipo de coisa que é difícil saber com antecedência, não importa quanto tempo de estabilização tenhamos.

Como nota final, gostaria de dizer que as características automáticas automáticas não são a única opção. É a opção que escolhemos, mas a alternativa de autocaracterísticas de desativação ainda é uma alternativa.

É verdade, embora eu definitivamente me sinta nervoso com a ideia de "escolher" traços específicos de auto como Send . Também é importante ter em mente que existem outros casos de uso para a característica impl além de futuros. Por exemplo, retornar iteradores ou encerramentos -- e nesses casos, não é óbvio que você deseja enviar ou sincronizar por padrão. De qualquer forma, o que você realmente deseja, e o que estamos tentando adiar =), é um tipo de limite "condicional" (Enviar se T for Enviar). Isso é precisamente o que os traços automáticos lhe dão.

@rpjohnst

acredito que isso já foi discutido

Na verdade, tem :) desde o primeiro impl Trait RFC lo estes muitos anos atrás. (Woah, 2014. Eu me sinto velho.)

Eu lutei e não consegui encontrar alguma estrutura existente para encaixá-lo.

Eu sinto exatamente o contrário. Para mim, sem impl Trait em posição de argumento, impl Trait em posição de retorno se destaca ainda mais. O tópico unificador que vejo é:

  • impl Trait -- onde aparece, indica que haverá "algum tipo monomorfizado que implementa Trait ". (A questão de quem especifica esse tipo - o chamador ou o chamado - depende de onde o impl Trait aparece.)
  • dyn Trait -- onde aparece, indica que haverá algum tipo que implementa Trait , mas que a escolha do tipo é feita dinamicamente.

Também há planos para expandir o conjunto de lugares onde impl Trait podem aparecer, com base nessa intuição. Por exemplo, https://github.com/rust-lang/rfcs/pull/2071 permite

let x: impl Trait = ...;

O mesmo princípio se aplica: a escolha do tipo é conhecida estaticamente. Da mesma forma, a mesma RFC introduz abstract type (para o qual impl Trait pode ser entendido como uma espécie de syntacti sugar), que pode aparecer em traits imps e até mesmo como membros em módulos.

Ideia de galpão de bicicleta aqui para não distrair meus pontos acima: em vez de impl , use type ?

Pessoalmente, não estou inclinado a reinstigar um bicicletário aqui. Passamos algum tempo discutindo a sintaxe em https://github.com/rust-lang/rfcs/pull/2071 e em outros lugares. Não parece haver uma "palavra-chave perfeita", mas ler impl como "algum tipo que implementa" funciona muito bem.

Deixe-me adicionar um pouco mais sobre o vazamento de características automáticas:

Em primeiro lugar, em última análise, acho que o vazamento de autotraços é realmente a coisa certa a fazer aqui, precisamente porque é consistente com o resto da linguagem. As características automáticas foram - como eu disse anteriormente - sempre uma aposta, mas parecem ter sido uma que basicamente valeu a pena. Eu simplesmente não vejo impl Trait sendo tão diferente.

Mas também, estou muito nervoso em atrasar aqui. Concordo que há outros pontos interessantes no espaço de design e não estou 100% confiante de que chegamos ao ponto certo, mas não sei se algum dia teremos certeza disso. Estou bastante preocupado se atrasarmos agora, teremos dificuldade em cumprir nosso roteiro para o ano.

Finalmente, vamos considerar as implicações se eu estiver errado: O que estamos falando basicamente aqui é que sempre se torna ainda mais sutil para julgar. Essa é uma preocupação, eu acho, mas que pode ser mitigada de várias maneiras. Por exemplo, podemos usar lints que avisam quando os tipos !Send ou !Sync são introduzidos. Há muito falamos sobre a introdução de um verificador de semver que ajuda a evitar violações acidentais de semver -- esse parece ser outro caso em que isso ajudaria. Em suma, um problema, mas não acho crítico.

Então - pelo menos neste momento - ainda me sinto inclinado a continuar o caminho atual.

Pessoalmente, não estou inclinado a reinstigar um bicicletário aqui.

Também não estou muito interessado nisso; foi uma reflexão tardia com base na minha impressão de que impl Trait na posição do argumento parece ser motivado por "preencher buracos" sintaticamente em vez de semanticamente , o que parece estar correto, dada sua resposta. :)

Para mim, sem impl Trait em posição de argumento, impl Trait em posição de retorno se destaca ainda mais.

Dada a analogia com os tipos associados, isso se parece muito com "sem type T na posição do argumento, os tipos associados se destacam ainda mais". Suspeito que essa objeção em particular não surgiu porque a sintaxe que escolhemos faz parecer sem sentido - a sintaxe existente é boa o suficiente para que ninguém sinta a necessidade de açúcar sintático como trait Trait<type SomeAssociatedType> .

Já temos sintaxe para "algum tipo monomorfizado que implementa Trait ." No caso de traços, temos variantes especificadas por "chamador" e "chamado". No caso de funções, temos apenas a variante especificada pelo chamador, portanto, precisamos da nova sintaxe para a variante especificada pelo chamador.

A expansão dessa nova sintaxe para variáveis ​​locais pode ser justificada, porque essa também é uma situação semelhante a um tipo associado - é uma maneira de ocultar + nomear o tipo de saída de uma expressão e é útil para encaminhar os tipos de saída das funções de chamada.

Como mencionei no meu comentário anterior, também sou fã de abstract type . É, novamente, simplesmente uma expansão do conceito de "tipo de saída" para módulos. E aplicar o uso de -> impl Trait , let x: impl Trait e abstract type de inferência para tipos associados de trait impls também é ótimo.

É especificamente o conceito de adicionar essa nova sintaxe para argumentos de função que eu não gosto. Ele não faz a mesma coisa que nenhum dos outros recursos com os quais está sendo puxado. Ele faz a mesma coisa que a sintaxe que já temos, apenas com mais casos extremos e menos aplicabilidade. :/

@nikomatsakis

É realmente difícil saber até que ponto isso será verdade.

Parece-me que devemos errar por sermos conservadores então? Podemos ganhar mais confiança no design com mais tempo (deixando o vazamento de auto-características estar sob um portão de recurso separado e apenas à noite enquanto estabilizamos o restante de impl Trait )? Sempre podemos adicionar suporte para vazamento de características automáticas mais tarde, se não vazarmos agora.

Mas também, estou muito nervoso em atrasar aqui. [..] Estou bastante preocupado se adiarmos agora, teremos dificuldade em cumprir nosso roteiro para o ano.

Compreensível! No entanto, e como tenho certeza que você já considerou, as decisões aqui permanecerão conosco por muitos anos.

Por exemplo, podemos usar lints que avisam quando os tipos !Send ou !Sync são introduzidos. Há muito falamos sobre a introdução de um verificador de semver que ajuda a evitar violações acidentais de semver -- esse parece ser outro caso em que isso ajudaria. Em suma, um problema, mas não acho crítico.

Isto é bom de ouvir! 🎉 E acho que isso ameniza principalmente minhas preocupações.

É verdade, embora eu definitivamente me sinta nervoso com a ideia de "escolher" traços específicos de auto como Send .

Eu concordo muito com esse sentimento 👍.

De qualquer forma, o que você realmente deseja, e o que estamos tentando adiar =), é um tipo de limite "condicional" (Enviar se T for Enviar). Isso é precisamente o que os traços automáticos lhe dão.

Eu sinto que T: Send => Foo<T>: Send seria melhor entendido se o código declarasse isso explicitamente.

fn foo<T: Extra, trait Extra = Send>(x: T) -> impl Bar + Extra {..}

Embora, como discutimos em WG-Traits, você pode não obter nenhuma inferência aqui, então você sempre precisa especificar Extra se quiser algo diferente de Send , o que seria uma chatice total .

@rpjohnst

A analogia com dyn Trait é, honestamente, falsa:

Com relação a impl Trait na posição do argumento, é falso, mas não com -> impl Trait pois ambos são tipos existenciais.

  • Agora o que deveria ser o detalhe de implementação de uma função (como ela declara seus parâmetros de tipo) se torna parte de sua interface, porque você não pode escrever seu tipo!

Eu gostaria de observar que a ordem dos parâmetros de tipo nunca foi um detalhe de implementação devido ao turbofish e, a esse respeito, acho que impl Trait pode ajudar, pois permite que você deixe certos argumentos de tipo não especificados no turbofish .

[..] a sintaxe existente é boa o suficiente para que ninguém sinta a necessidade de açúcar sintático como trait Trait.

Nunca diga nunca? https://github.com/rust-lang/rfcs/issues/2274

Como @nikomatsakis , eu realmente aprecio o cuidado nesses comentários de última hora; Eu sei que pode parecer como tentar se jogar na frente de um trem de carga, especialmente para um recurso tão desejado quanto este!


@daboross , eu queria

Infelizmente, porém, ele se depara com alguns problemas quando você começa a olhar para o quadro maior:

  • Se as características automáticas foram tratadas como opt-out para impl Trait , elas também deveriam ser para dyn Trait .
  • Obviamente, isso se aplica mesmo quando essas construções são usadas em posição de argumento.
  • Mas então, seria bastante estranho que os genéricos se comportassem de maneira diferente. Em outras palavras, para fn foo<T>(t: T) , você poderia razoavelmente esperar T: Send por padrão.
  • Claro que temos um mecanismo para isso, atualmente aplicado apenas a Sized ; é uma característica que é assumida por padrão em todos os lugares, e para a qual você exclui escrevendo ?Sized

O mecanismo ?Sized continua sendo um dos aspectos mais obscuros e difíceis de ensinar do Rust, e em geral temos sido extremamente relutantes em expandi-lo para outros conceitos. Usá-lo para um conceito tão central quanto Send parece arriscado - sem mencionar, é claro, que seria uma grande mudança.

Além do mais: nós realmente não queremos adotar uma suposição de autocaracterística para genéricos, porque parte da beleza dos genéricos hoje é que você pode efetivamente ser genérico sobre se um tipo implementa uma característica automática e ter essa informação apenas "fluir através". Por exemplo, considere fn f<T>(t: T) -> Option<T> . Podemos passar T independentemente de ser Send , e a saída será Send se T era. Esta é uma parte extremamente importante da história dos genéricos em Rust.

Há também problemas com dyn Trait . Em particular, devido à compilação separada, teríamos que restringir essa natureza de "exclusão" apenas a características automáticas "bem conhecidas" como Send e Sync ; provavelmente significaria nunca estabilizar auto trait para uso externo.

Por fim, vale a pena reiterar que o design de "vazamento" foi modelado explicitamente após o que acontece hoje quando você cria um wrapper newtype para retornar um tipo opaco. Fundamentalmente, acredito que "vazamento" é um aspecto inerente aos traços de personalidade em primeiro lugar; ele tem desvantagens, mas é o que o recurso é essencial, e acho que devemos nos esforçar para que novos recursos interajam com ele de acordo.


@rpjohnst

Não tenho muito a acrescentar sobre a questão da posição do argumento após as extensas discussões sobre o RFC e o comentário resumido de @nikomatsakis acima.

Agora o que deveria ser o detalhe de implementação de uma função (como ela declara seus parâmetros de tipo) se torna parte de sua interface, porque você não pode escrever seu tipo!

Não entendo o que você quer dizer com isso. Você pode expandir?

Também quero observar que frases como:

fn f(t: impl Trait) parece que foi adicionado "só porque podemos"

minar a discussão de boa fé (estou chamando isso porque é um padrão repetido). O RFC faz um esforço considerável para motivar o recurso e refutar alguns dos argumentos que você está fazendo aqui - sem mencionar a discussão no tópico, é claro, e em iterações anteriores do RFC, etc etc.

Existem compensações, existem sim desvantagens, mas isso não nos ajuda a chegar a uma conclusão fundamentada para caricaturar "o outro lado" do debate.

Obrigado a todos pelos comentários detalhados! Estou muito animado para finalmente lançar impl Trait no stable, então estou fortemente inclinado para a implementação atual e as decisões de design que levaram a isso. Dito isso, farei o possível para responder da forma mais imparcial possível e considerar as coisas como se estivéssemos começando do zero:

auto Trait Vazamento

A ideia de vazamento de auto Trait me incomodou por um longo tempo - de certa forma, pode parecer antitético para muitos dos objetivos de design de Rust. Comparado com seus ancestrais, como C++ ou a família ML, o Rust é incomum, pois exige que os limites genéricos sejam declarados explicitamente nas declarações de função. Na minha opinião, isso torna as funções genéricas do Rust mais fáceis de ler e entender, e deixa relativamente claro quando uma mudança incompatível com versões anteriores está sendo feita. Continuamos esse padrão em nossa abordagem para const fn , exigindo que as funções se especifiquem explicitamente como const vez de inferir const ness dos corpos das funções. Assim como os limites explícitos de características, isso torna mais fácil dizer quais funções podem ser usadas de que maneiras e dá aos autores de biblioteca a confiança de que pequenas mudanças de implementação não prejudicarão os usuários.

Dito isso, usei a posição de retorno impl Trait extensivamente em meus próprios projetos, incluindo meu trabalho no sistema operacional Fuchsia, e acredito que o vazamento de autocaracterística é o padrão certo aqui. Praticamente, a consequência de remover o vazamento seria que eu teria que voltar e adicionar + Send a basicamente todas as funções impl Trait que eu já escrevi. Limites negativos (exigindo + !Send ) são uma ideia interessante para mim, mas então eu estaria escrevendo + !Unpin em quase todas as mesmas funções. A clareza é útil quando informa as decisões dos usuários ou torna o código mais compreensível. Nesse caso, acho que não faria nenhum dos dois.

Send e Sync são "contextos" nos quais os usuários programam: é extremamente raro escrever uma aplicação ou biblioteca que use os tipos Send e !Send (especialmente ao escrever código assíncrono para ser executado em um executor central, que é multithread ou não). A escolha de ser thread-safe ou não é uma das primeiras escolhas que devem ser feitas ao escrever um aplicativo e, a partir daí, escolher ser thread-safe significa que todos os meus tipos devem ser Send . Para bibliotecas, é quase sempre o caso que eu prefiro tipos Send , já que não usá-los geralmente significa que minha biblioteca é inutilizável (ou requer a criação de um thread dedicado) quando usado em um contexto de thread. Um parking_lot::Mutex não contestado terá desempenho quase idêntico ao RefCell quando usado em CPUs modernas, então não vejo nenhuma motivação para empurrar os usuários para a funcionalidade de biblioteca especializada para !Send use- casos. Por essas razões, não acho importante ser capaz de discernir entre os tipos Send e !Send no nível de assinatura de função, e não acho que seja comum para autores de bibliotecas introduzam acidentalmente tipos !Send em tipos impl Trait que anteriormente eram Send . É verdade que esta escolha vem com um custo de legibilidade e clareza, mas acredito que a troca vale a pena pelos benefícios ergonômicos e de usabilidade.

Argumento-posição impl Trait

Não tenho muito a dizer aqui, exceto que toda vez que chego à posição do argumento impl Trait , descobri que isso aumentou muito a legibilidade e o prazer geral das minhas assinaturas de função. É verdade que não adiciona uma nova capacidade que não é possível no Rust de hoje, mas é uma grande melhoria na qualidade de vida para assinaturas de funções complicadas, combina bem conceitualmente com a posição de retorno impl Trait , e facilita as transições dos programadores OOP para rustáceos felizes. Atualmente, há muita redundância em ter que introduzir um tipo genérico nomeado apenas para fornecer um limite (por exemplo, F em fn foo<F>(x: F) where F: FnOnce() vs. fn foo(x: impl FnOnce()) ). Essa alteração resolve esse problema e resulta em assinaturas de funções mais fáceis de ler e escrever, e o IMO parece um ajuste natural ao lado de -> impl Trait .

TL; DR: Acho que nossas decisões originais foram as corretas, embora sem dúvida venham com compensações.
Eu realmente aprecio todos que falam e dedicam tanto tempo e esforço para garantir que Rust seja a melhor linguagem possível.

@Centril

Com relação a impl Trait na posição do argumento, é falso, mas não com -> impl Trait, pois ambos são tipos existenciais.

Sim, foi isso que eu quis dizer.

@aturon

frases como ... minam a discussão de boa fé

Tens razão, desculpa por isso. Acredito que fiz meu ponto melhor em outro lugar.

Agora o que deveria ser o detalhe de implementação de uma função (como ela declara seus parâmetros de tipo) se torna parte de sua interface, porque você não pode escrever seu tipo!

Não entendo o que você quer dizer com isso. Você pode expandir?

Com suporte para impl Trait na posição do argumento, você pode escrever esta função de duas maneiras:

fn f(t: impl Trait)
fn f<T: Trait>(t: T)

A escolha da forma determina se o consumidor da API pode até mesmo escrever o nome de qualquer instanciação em particular (por exemplo, para obter seu endereço). A variante impl Trait não permite que você faça isso, e isso nem sempre pode ser contornado sem reescrever a assinatura para usar a sintaxe <T> . Além disso, mudar para a sintaxe <T> é uma mudança radical!

Correndo o risco de mais caricaturas, a motivação para isso é que é mais fácil ensinar, aprender e usar. No entanto, como a escolha entre os dois também é uma parte importante da interface da função, assim como a ordem do parâmetro de tipo, não sinto que isso tenha sido adequadamente abordado - na verdade, não discordo que seja mais fácil de usar ou que seja resulta em assinaturas de função mais agradáveis.

Não tenho certeza se nenhuma de nossas outras mudanças "simples, mas limitadas -> complexas, mas gerais", motivadas pela capacidade de aprendizado/ergonomia, envolvem mudanças que quebram a interface dessa maneira. Ou o equivalente complexo da maneira simples se comporta de forma idêntica e você só precisa alternar quando já estiver alterando a interface ou o comportamento (por exemplo, elisão vitalícia, ergonomia de correspondência, -> impl Trait ), ou a alteração é tão geral e destinada a ser aplicado universalmente (por exemplo, módulos/caminhos, tempos de vida em banda, dyn Trait ).

Para ser mais concreto, eu me preocupo que começaremos a encontrar esse problema nas bibliotecas, e será muito parecido com "todo mundo precisa se lembrar de derivar Copy / Clone ", mas pior porque a) isso será uma mudança de ruptura, e b) sempre haverá uma tensão para voltar atrás, especificamente porque é para isso que o recurso foi projetado!

@cramertj No que diz respeito à redundância de assinatura de função... poderíamos nos livrar dela de outra maneira? Tempos de vida na banda foram capazes de escapar sem referências anteriores; talvez pudéssemos fazer o equivalente moral de "parâmetros do tipo in-band" de alguma forma. Ou, em outras palavras, "a mudança é igualmente geral e destinada a ser aplicada universalmente".

@rpjohnst

Além disso, mudar para a sintaxe <T> é uma mudança radical!

Não necessariamente, com https://github.com/rust-lang/rfcs/pull/2176 você poderia adicionar um parâmetro de tipo extra T: Trait ao final e o turbofish ainda funcionaria (a menos que você esteja se referindo a quebra por algum outro meio que não a quebra de turbofish).

A variante impl Trait não permite que você faça isso, e isso nem sempre pode ser contornado sem reescrever a assinatura para usar a sintaxe <T> . Além disso, mudar para a sintaxe <T> é uma mudança radical!

Além disso, acho que você quer dizer que mudar da sintaxe <T> é uma mudança importante (porque os chamadores não podem mais especificar o valor de T explicitamente usando turbofish).

ATUALIZAÇÃO: Observe que, se uma função usa impl Trait, atualmente não permitimos o uso de turbofish - mesmo que tenha alguns parâmetros genéricos normais.

@nikomatsakis Mudar para a sintaxe explícita também pode ser uma mudança importante, se a assinatura antiga tivesse uma mistura de parâmetros de tipo explícitos e implícitos - qualquer pessoa que fornecesse parâmetros de tipo n agora precisará fornecer n + 1 vez disso. Esse foi um dos casos que a RFC do @Centril quis resolver.

ATUALIZAÇÃO: Observe que, se uma função usa impl Trait, atualmente não permitimos o uso de turbofish - mesmo que tenha alguns parâmetros genéricos normais.

Isso reduz tecnicamente o número de casos de interrupção, mas, por outro lado, aumenta o número de casos em que você não pode nomear uma instanciação específica. :(

@nikomatsakis

Obrigado por abordar esta preocupação sinceramente.

Ainda estou hesitante em dizer que o vazamento de traços automáticos é _a solução certa_, mas concordo que não podemos realmente saber o que é melhor até depois do fato.

Eu considerei principalmente o caso de uso Futuros, mas esse não é o único. Sem vazar Send/Sync de tipos locais, não há realmente uma boa história para usar impl Trait em muitos contextos diferentes. Dado isso, e dadas características automáticas adicionais, minha sugestão não é realmente viável.

Eu não queria destacar Sync e Send e _only_ assumi-los, já que isso é um pouco arbitrário e apenas melhor para _one_ caso de uso. No entanto, a alternativa de assumir todas as características automáticas também não seria boa. + !Unpin + !... em todos os tipos não parece uma solução viável.

Se tivéssemos mais cinco anos de design de linguagem para criar um sistema de efeitos e outras idéias sobre as quais não tenho idéia agora, talvez pudéssemos criar algo melhor. Mas, por enquanto, e para Rust, parece que ter 100% de características automáticas "automáticas" é o melhor caminho a seguir.

@lfairy

Mudar para a sintaxe explícita também pode ser uma mudança importante, se a assinatura antiga tiver uma mistura de parâmetros de tipo explícitos e implícitos -- qualquer pessoa que forneceu n parâmetros de tipo agora precisará fornecer n + 1 .

Isso não é permitido atualmente. Se você usar impl Trait , você não obterá turbofish para nenhum parâmetro (como observei). No entanto, isso não se destina a ser uma solução de longo prazo, é mais um passo conservador para evitar discordâncias sobre como proceder até que tenhamos tempo de apresentar um design arredondado. (E, como observou @rpjohnst , tem suas próprias desvantagens .)

O design que eu gostaria de ver é (a) sim para aceitar o RFC do @centril ou algo parecido e (b) dizer que você pode usar turbofish para parâmetros explícitos (mas não impl Trait tipos de um parâmetro explícito para um recurso implícito.

@lfairy

Esse foi um dos casos que a RFC do @Centril quis resolver.

_[Curiosidades]_ Aliás, foi na verdade @nikomatsakis quem me chamou a atenção que o turbofish parcial poderia facilitar as quebras entre <T: Trait> e impl Trait ;) Não era um objetivo do RFC em tudo desde o início, mas foi uma agradável surpresa. 😄

Espero que, uma vez que ganhemos mais confiança em relação à inferência, padrões, parâmetros nomeados, etc., também possamos ter um turbofish parcial, Eventualmente™.

O período final de comentários está concluído.

Se isso está sendo enviado em 1.26, https://github.com/rust-lang/rust/issues/49373 parece muito importante para mim, Future e Iterator são dois dos principais usos -cases e ambos são muito dependentes de conhecer os tipos associados.

Fiz uma pesquisa rápida no rastreador de problemas e o nº 47715 é um ICE que ainda precisa ser corrigido. Podemos obter isso antes de entrar no estável?

Algo que encontrei com o impl Trait hoje:
https://play.rust-lang.org/?gist=69bd9ca4d41105f655db5f01ff444496&version=stable

Parece que impl Trait é incompatível com unimplemented!() - esse é um problema conhecido?

sim, veja #36375 e #44923

Acabei de perceber que a suposição 2 da RFC 1951 se depara com alguns dos meus usos planejados de impl Trait com blocos assíncronos. Especificamente, se você pegar um parâmetro genérico AsRef ou Into para ter uma API mais ergonômica e transformá-lo em algum tipo de propriedade antes de retornar um bloco async , você ainda obterá o retorno impl Trait tipo sendo vinculado por quaisquer tempos de vida nesse parâmetro, por exemplo

impl HttpClient {
    fn get(&mut self, url: impl Into<Url>) -> impl Future<Output = Response> + '_ {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

fn foo(client: &mut HttpClient) -> impl Future<Output = Response> + '_ {
    let url = Url::parse("http://foo.example.com").unwrap();
    client.get(&url)
}

Com isso, você obterá um error[E0597]: `url` does not live long enough porque get inclui o tempo de vida da referência temporária no impl Future retornado. Este exemplo é um pouco artificial, pois você pode passar a url por valor para get , mas quase certamente haverá casos semelhantes surgindo no código real.

Tanto quanto eu posso dizer, a correção esperada para isso são tipos abstratos, especificamente

impl HttpClient {
    abstract type Get<'a>: impl Future<Output = Response> + 'a;
    fn get(&mut self, url: impl Into<Url>) -> Self::Get<'_> {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

Ao adicionar a camada de indireção, você deve passar explicitamente por quais parâmetros de tipo genérico e tempo de vida são necessários para o tipo abstrato.

Eu estou querendo saber se há potencialmente uma maneira mais sucinta de escrever isso, ou isso acabará com tipos abstratos sendo usados ​​para quase todas as funções e nunca o tipo de retorno impl Trait ?

Portanto, se eu entender o comentário de @cramertj sobre esse problema, você receberá um erro na definição de HttpClient::get algo como `get` returns an `impl Future` type which is bounded to live for `'_`, but this type could potentially contain data with a shorter lifetime inside the type of `url` . (Porque o RFC especifica explicitamente que impl Trait captura _all_ parâmetros de tipo genérico e é um bug que você tem permissão para capturar um tipo que pode conter um tempo de vida menor que o tempo de vida declarado explicitamente).

A partir disso, a única correção ainda parece declarar um tipo abstrato nominal para permitir declarar explicitamente quais parâmetros de tipo são capturados.

Na verdade, isso parece que seria uma mudança de ruptura. Portanto, se um erro nesse caso for adicionado, é melhor que seja em breve.

EDIT: E relendo o comentário, não acho que seja isso que está dizendo, então ainda estou confuso se existe uma maneira potencial de contornar isso sem usar tipos abstratos ou não.

@Nemo157 Sim, corrigir #42940 resolveria seu problema de vida útil, pois você pode especificar que o tipo de retorno deve durar tanto quanto o empréstimo de self, independentemente da vida útil de Url . Esta é definitivamente uma mudança que queremos fazer, mas acredito que seja compatível com versões anteriores -- não está permitindo que o tipo de retorno tenha uma vida útil mais curta, está restringindo demais as maneiras pelas quais o tipo de retorno pode ser usado.

Por exemplo, os seguintes erros com "o parâmetro Iter pode não durar o suficiente":

fn foo<'a, Iter>(_: &'a mut u32, iter: Iter) -> impl Iterator<Item = u32> + 'a
    where Iter: Iterator<Item = u32>
{
    iter
}

Apenas ter o Iter nos genéricos para a função não é suficiente para permitir que ele esteja presente no tipo de retorno, mas atualmente os chamadores da função assumem incorretamente que está. Este é definitivamente um bug e deve ser corrigido, mas acredito que pode ser corrigido de forma compatível com versões anteriores e não deve bloquear a estabilização.

Parece que #46541 está pronto. Alguém pode atualizar o OP?

Existe uma razão pela qual a sintaxe abstract type Foo = ...; foi escolhida em vez de type Foo = impl ...; ? Eu preferia muito o último, pela consistência da sintaxe, e me lembro de alguma discussão sobre isso há algum tempo, mas não consigo encontrá-lo.

Eu sou parcial para type Foo = impl ...; ou type Foo: ...; , abstract parece um estranho desnecessário.

Se bem me lembro, uma das principais preocupações era que as pessoas aprenderam a interpretar type X = Y como uma substituição textual ("substitua X por Y quando aplicável"). Isso não funciona para type X = impl Y .

Eu prefiro type X = impl Y porque minha intuição é que type funciona como let , mas...

@alexreg Há muita discussão sobre o assunto na RFC 2071 . TL;DR: type Foo = impl Trait; quebra a capacidade de reduzir o açúcar impl Trait em alguma forma "mais explícita", e quebra as intuições das pessoas sobre aliases de tipo funcionando como uma substituição sintática um pouco mais inteligente.

Eu sou parcial para digitar Foo = impl ...; ou digite Foo: ...;, abstract parece um estranho desnecessário

Você deveria participar do meu acampamento exists type Foo: Trait; :wink:

@cramertj Hum. Acabei de me atualizar sobre isso e, para ser honesto, não posso dizer que entendo o raciocínio de @withoutboats . Parece o mais intuitivo para mim (você tem um contra-exemplo?) e a parte sobre desaçucar eu simplesmente não entendo. Acho que minha intuição funciona como @lnicola. Também acho que essa sintaxe é a melhor para fazer coisas como https://github.com/rust-lang/rfcs/pull/2071#issuecomment -319012123 - isso pode ser feito na sintaxe atual?

exists type Foo: Trait; é uma pequena melhoria, embora eu ainda abandone a palavra-chave exists . type Foo: Trait; não me incomodaria o suficiente para reclamar. 😉 abstract é apenas supérfluo/excêntrico, como diz @eddyb .

@alexreg

isso pode ser feito na sintaxe atual?

Sim, mas é muito mais estranho. Esta foi a minha principal razão para preferir a sintaxe = impl Trait (módulo da palavra-chave abstract ).

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item=impl Display>;

// can be written like this:

exists type Foo1: Bar;
exists type Foo2: Baz;
exists type Foo: (Foo1, Foo2);

exists type IterDisplayItem: Display;
exists type IterDisplay: Iterator<Item=IterDisplayItem>;

Edit: exists type Foo: (Foo1, Foo2); acima deveria ter sido type Foo = (Foo1, Foo2); . Desculpe pela confusão.

@cramertj A sintaxe parece boa. exists ser uma palavra-chave adequada?

@cramertj Certo, eu estava pensando que você teria que fazer algo assim... uma boa razão para preferir = impl Trait , eu acho! Honestamente, se as pessoas pensam que a intuição sobre substituição se quebra o suficiente para tipos existenciais aqui (em comparação com aliases de tipo simples), então por que não o seguinte compromisso?

exists type Foo = (impl Bar, impl Baz);

(Honestamente, porém, prefiro apenas ter a consistência de usar a única palavra-chave type para tudo.)

Eu acho:

exists type Foo: (Foo1, Foo2);

profundamente estranho. Usar Foo: (Foo1, Foo2) onde o RHS não é um limite não é consistente com a forma como Ty: Bound é usado em outras partes do idioma.

As seguintes formas me parecem boas:

exists type Foo: Bar + Baz;  // <=> "There exists a type Foo which satisfies Bar and Baz."
                             // Reads super well!

type Foo = impl Bar + Baz;

type Bar = (impl Foo, impl Bar);

Eu também prefiro não usar abstract como uma palavra aqui.

Acho exists type Foo: (Foo1, Foo2); profundamente estranho

Isso certamente parece um erro para mim, e acho que deveria dizer type Foo = (Foo1, Foo2); .

Se estamos jogando abstract type vs exists type aqui, eu definitivamente apoiaria o primeiro. Principalmente porque "abstrato" funciona como um adjetivo. Eu poderia facilmente chamar algo de "tipo abstrato" em uma conversa, enquanto parece estranho dizer que estamos fazendo um "tipo existente".

Eu também prefiro : Foo + Bar a : (Foo, Bar) , = Foo + Bar , = impl Foo + Bar ou = (impl Foo, impl Bar . O uso de + funciona bem com todos os outros lugares em que os limites podem estar, e a falta de = realmente significa que não podemos escrever o tipo completo. Não estamos criando um alias de tipo aqui, estamos criando um nome para algo que garantimos ter certos limites, mas que não podemos nomear explicitamente.


Eu também ainda gosto da sugestão de sintaxe de https://github.com/rust-lang/rfcs/pull/2071#issuecomment -318852774 de:

type ExistentialFoo: Bar;
type Bar: Baz + Bax;

Embora isso seja, como mencionado nesse tópico, uma diferença um pouco pequena e não muito explícita.

Devo estar interpretando (impl Foo, impl Bar) muito diferente de alguns de vocês... para mim, isso significa que o tipo é uma tupla de 2 tipos de alguns tipos existenciais e é completamente diferente de impl Foo + Bar .

@alexreg Se essa fosse a intenção de @cramertj , eu ainda acharia isso muito estranho com a sintaxe : :

exists type Foo: (Foo1, Foo2);

parece ainda muito obscuro quanto ao que está fazendo - os limites geralmente não especificam uma tupla de tipos possíveis em qualquer caso, e podem ser facilmente confundidos com o significado da sintaxe Foo: Foo1 + Foo2 .

= (impl Foo, impl Bar) é uma ideia interessante - permitir a criação de tuplas existenciais com tipos que não são conhecidos seria interessante. Eu não acho que _precisamos_ dar suporte a isso, já que podemos apenas introduzir dois tipos existenciais para impl Foo e impl Bar então um terceiro tipo de alias para a tupla.

@daboross Bem, você está fazendo um "tipo existencial" , não um "tipo existe" ; que é o que é chamado na teoria dos tipos. Mas acho que a frase "existe um tipo Foo que ..." funciona bem tanto com o modelo mental quanto com uma perspectiva teórica de tipos.

Eu não acho que precisamos dar suporte a isso, já que podemos apenas introduzir dois tipos existenciais para impl Foo e impl Bar e um terceiro tipo de alias para a tupla.

Isso não parece ergonômico... temporários não são tão legais imo.

@alexreg Nota: eu não quis dizer que impl Bar + Baz; é o mesmo que (impl Foo, impl Bar) , o último é obviamente a 2-tupla.

@daboross

Se essa fosse a intenção de @cramertj , eu ainda acharia isso muito estranho com a sintaxe ::

exists type Foo: (Foo1, Foo2);

parece ainda muito obscuro sobre o que está fazendo - os limites geralmente não especificam uma tupla de tipos possíveis em qualquer caso, e podem ser facilmente confundidos com o significado da sintaxe Foo: Foo1 + Foo2.

Talvez seja um pouco obscuro (não tão explícito quanto (impl Foo, impl Bar) , que eu entenderia intuitivamente imediatamente) – mas acho que nunca confundiria com Foo1 + Foo2 , pessoalmente.

= (impl Foo, impl Bar) é uma ideia interessante - permitir a criação de tuplas existenciais com tipos que não são conhecidos seria interessante. Eu não acho que precisamos dar suporte a isso, já que podemos apenas introduzir dois tipos existenciais para impl Foo e impl Bar e depois um terceiro tipo de alias para a tupla.

Sim, essa foi uma proposta inicial, e eu ainda gosto bastante. Foi observado que isso pode ser feito de qualquer maneira usando a sintaxe atual, mas requer 3 linhas de código, o que não é muito ergonômico. Eu também mantenho que alguma sintaxe como ... = (impl Foo, impl Bar) é a mais clara para o usuário, mas eu sei que há contenção aqui.

@Centril Eu não pensei assim no começo, mas era um pouco ambíguo, e então @daboross pareceu interpretar dessa maneira, hah. De qualquer forma, feliz por termos esclarecido isso.

Ops, veja minha edição para https://github.com/rust-lang/rust/issues/34511#issuecomment -386763340. exists type Foo: (Foo1, Foo2); deveria ser type Foo = (Foo1, Foo2); .

@cramertj Ah, isso faz mais sentido agora. De qualquer forma, você não acha que poder fazer o seguinte é o mais ergonômico? Mesmo navegando nesse outro tópico, eu realmente não vi um bom argumento contra isso.

type A = impl Foo;
type B = (impl Foo, impl Bar, String);

@alexreg Sim, eu acho que é a sintaxe mais ergonómico.

Usando RFC https://github.com/rust-lang/rfcs/pull/2289 , é assim que eu reescreveria o trecho de @cramertj :

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item: Display>;

// alternatively:

exists type IterDisplay: Iterator<Item: Display>;

type IterDisplay: Iterator<Item: Display>;

No entanto, acho que para aliases de tipo, não introduzir exists ajudaria a manter o poder expressivo sem tornar a sintaxe da linguagem mais complexa desnecessariamente; então, de um POV de orçamento de complexidade, impl Iterator parece melhor que exists . A última alternativa, no entanto, não introduz uma nova sintaxe e também é a mais curta, embora seja clara.

Resumindo, acho que as duas formas a seguir devem ser permitidas (porque funciona sob impl Trait e limites em sintaxes de tipos associados que já temos):

type Foo = (impl Bar, impl Baz);
type IterDisplay: Iterator<Item: Display>;

EDIT: Qual sintaxe deve ser usada? IMO, clippy deve preferir sem ambiguidade a sintaxe Type: Bound quando for possível usar, pois é mais ergonômico e direto.

Eu prefiro muito mais a variante type Foo: Trait sobre a variante type Foo = impl Trait . Ele corresponde à sintaxe do tipo associado, o que é bom porque também é um "tipo de saída" do módulo que o contém.

A sintaxe impl Trait é usada para tipos de entrada e saída, o que significa que corre o risco de dar a impressão de módulos polimórficos. :(

Se impl Trait fosse usado apenas para tipos de saída, então eu poderia preferir a variante type Foo = impl Trait com o argumento de que a sintaxe de tipo associada é mais para traços (que correspondem vagamente a assinaturas de ML) enquanto o type Foo = .. sintaxe

@rpjohnst

Eu prefiro muito mais a variante type Foo: Trait sobre a variante type Foo = impl Trait .

Concordo, deve ser usado sempre que possível; mas e o caso de (impl T, impl U) onde a sintaxe vinculada não pode ser usada diretamente? Parece-me que a introdução de aliases de tipo temporários prejudica a legibilidade.

Usar apenas type Name: Bound parece confuso quando usado dentro de blocos impl :

impl Iterator for Foo {
    type Item: Display;

    fn next(&mut self) -> Option<Self::Item> { Some(5) }
}

Tanto para essa sintaxe quanto para o plano atual(?) de prefixo de palavra-chave, o custo de introdução de aliases de tipo temporários a serem usados ​​em blocos impl também é muito maior, esses aliases de tipo agora precisam ser exportados no nível do módulo ( e dado um nome semanticamente significativo...), que bloqueia um padrão relativamente comum (pelo menos para mim) de definir implementações de características dentro de módulos privados.

pub abstract type First: Display;
pub abstract type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

vs

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@ Nemo157 Por que não permitir ambos:

pub type First: Display;
pub type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

e:

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

?

Não vejo por que precisa haver duas sintaxes para o mesmo recurso, usar apenas a sintaxe type Name = impl Bound; fornecer nomes explicitamente para as duas partes ainda seria possível:

pub type First = impl Display;
pub type Second = impl Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@ Nemo157 Concordo que não precisa (e não deve) haver duas sintaxes diferentes. Eu não acho type (sem palavra-chave de prefixo) confuso, devo dizer.

@rpjohnst O que diabos é um módulo polimórfico? :-) De qualquer forma, não vejo por que devemos modelar a sintaxe após definições de tipo associadas, que estão colocando limites de características em um tipo. Isso não tem nada a ver com limites .

@alexreg Um módulo polimórfico é aquele que possui parâmetros de tipo, da mesma forma que fn foo(x: impl Trait) . Não é algo que existe, então não quero que as pessoas pensem que existe.

abstract type ( edit: para nomear o recurso, não para sugerir o uso da palavra-chave) tem tudo a ver com limites! Os limites são a única coisa que você sabe sobre o tipo. A única diferença entre eles e os tipos associados é que eles são inferidos, porque geralmente são inomináveis.

@Nemo157 a Foo: Bar já é mais familiar em outros contextos (limites em tipos associados e em parâmetros de tipo) e é mais ergonômica e (IMO) clara quando pode ser usada sem introduzir temporários.

Escrevendo:

type IterDisplay: Iterator<Item: Display>;

parece um wrt muito mais direto. o que eu quero dizer, em comparação com

type IterDisplay = impl Iterator<Item = impl Display>;

Acho que isso é apenas aplicar consistentemente a sintaxe que já temos; então não é realmente novo.

EDIT2: A primeira sintaxe também é como eu gostaria que fosse renderizada em rustdoc.

Passar de um trait que requer algo em um tipo associado, para um impl também se torna muito fácil:

trait Foo {
    type Bar: Baz;
    // stuff...
}

struct Quux;

impl Foo for Quux {
    type Bar: Baz; // Oh look! Same as in the trait; I had to do nothing!
    // stuff...
}

A sintaxe impl Bar parece melhor quando você teria que introduzir temporários, mas também está aplicando a sintaxe de forma consistente por toda parte.

Ser capaz de usar ambas as sintaxes não seria muito diferente de poder usar impl Trait na posição do argumento, bem como ter um parâmetro de tipo explícito T: Trait que é então usado por um argumento.

EDIT1: Na verdade, ter apenas uma sintaxe seria uma caixa especial, e não o contrário.

@rpjohnst Eu discordo, embora eu devesse ter dito que não tem nada a ver explicitamente com limites.

De qualquer forma, não sou contra a sintaxe type Foo: Bar; , mas pelo amor de Deus, vamos nos livrar da palavra-chave abstract . type por si só é bastante claro, em qualquer circunstância.

Pessoalmente, sinto que usar = e impl é uma boa dica visual de que a inferência está acontecendo. Também torna mais fácil identificar esses lugares ao percorrer um arquivo maior.

Além disso, supondo que eu veja type Iter: Iterator<Item = Foo> , terei que encontrar Foo e descobrir se é um tipo ou uma característica antes de saber o que está acontecendo.

E, por último, acho que a pista visual dos pontos de inferência também ajudará a depurar erros de inferência e interpretar mensagens de erro de inferência.

Então eu acho que a variante = / impl resolve um pouco mais de cortes de papel.

@phaylon

Além disso, supondo que eu veja o tipo Iter: Iterator<Item = Foo> , terei que encontrar Foo e descobrir se é um tipo ou uma característica antes de saber o que está acontecendo.

Isso eu não entendo; Item = Foo deve sempre ser um tipo hoje em dia, dado que dyn Foo é estável (e a característica nua está sendo eliminada ...)?

@Centril

Isso eu não entendo; Item = Foo deve sempre ser um tipo hoje em dia, dado que dyn Foo é estável (e o traço básico está sendo eliminado ...)?

Sim, mas na variante menos impl proposta, pode ser um tipo inferido com um limite ou um tipo concreto. Por exemplo, Iterator<Item = String> vs Iterator<Item = Display> . Eu tenho que conhecer os traços para saber se a inferência está acontecendo.

Edit: Ah, não notei um usado : . Tipo o que quero dizer com fácil de perder :) Mas você está certo que eles são diferentes.

Edit 2: Eu acho que esse problema ficaria fora dos tipos associados. Dado type Foo: (Bar, Baz) você precisaria conhecer Bar e Baz para saber onde a inferência acontece.

@Centril

EDIT1: Na verdade, ter apenas uma sintaxe seria uma caixa especial, e não o contrário.

Atualmente, existe apenas uma maneira de declarar tipos _existenciais_, -> impl Trait . Existem duas maneiras de declarar tipos _universal_ ( T: Trait e : impl Trait em uma lista de argumentos).

Se tivéssemos módulos polimórficos que aceitassem tipos universais, eu poderia ver alguns argumentos em torno disso, mas acredito que o uso atual de type Name = Type; em ambos os módulos e definições de características é como um parâmetro de tipo de saída, que deve ser um existencial modelo.


@phaylon

Sim, mas na variante menos impl proposta, pode ser um tipo inferido com um limite ou um tipo concreto. Por exemplo, Iterator<Item = String> vs Iterator<Item = Display> . Eu tenho que conhecer os traços para saber se a inferência está acontecendo.

Eu acredito que a variante menos impl está usando : Bound em todos os casos para tipos existenciais, então você pode ter Iterator<Item = String> ou Iterator<Item: Display> como limites de traço, mas Iterator<Item = Display> seria uma declaração inválida.

@Nemo157
Você está certo em relação ao tipo de caso associado, meu erro. Mas (como observado na minha edição) acho que ainda há um problema com type Foo: (A, B) . Como A ou B pode ser um tipo ou característica.

Acredito que esse também seja um bom motivo para ir com = . O : apenas informa que algumas coisas são inferidas, mas não informa quais. type Foo = (A, impl B) me parece mais claro.

Também presumo que ler e fornecer trechos de código seja mais fácil com impl , pois o contexto adicional sobre o que é uma característica e o que não é nunca precisa ser fornecido.

Edit: Alguns créditos: Meu argumento é basicamente o mesmo do @alexreg aqui , eu só queria expandir o motivo pelo qual acho que impl é preferível.

Atualmente, existe apenas uma maneira de declarar tipos existenciais, -> impl Trait . Existem duas maneiras de declarar tipos universais ( T: Trait e : impl Trait em uma lista de argumentos).

É isso que estou dizendo :PI Por que a quantificação universal deveria ter duas maneiras, mas apenas uma existencial (ignorando dyn Trait ) em outros lugares ?

Parece-me igualmente provável que um usuário escreva type Foo: Bound; e type Foo = impl Bound; tendo aprendido diferentes partes da linguagem, e não posso dizer que uma sintaxe seja distintamente melhor em todos os casos; Está claro para mim que uma sintaxe é melhor para algumas coisas e outra para coisas diferentes.

@phaylon

Eu acredito que esta também é uma boa razão para ir com =. O : apenas informa que algumas coisas são inferidas, mas não informa quais. tipo Foo = (A, impl B) me parece mais claro.

Sim, esta é provavelmente outra boa razão. Realmente é preciso um pouco de desempacotamento para descobrir o que está sendo quantificado existencialmente – pulando de definição em definição.

Outra coisa é: alguém permitiria : dentro de uma associação de tipo associada, sob essa sintaxe? Parece um caso especial estranho para mim, dado que os tipos existenciais não podem ser compostos/combinados de nenhuma outra forma nesta sintaxe proposta. Eu imagino que o seguinte seria a abordagem mais consistente usando essa sintaxe:

type A: Foo;
type B: Bar;
type C: Baz;
type D: Iterator<Item = C>; 
type E = (A, Vec<B>, D);

Usando a sintaxe que eu (e alguns outros aqui) prefiro, poderíamos escrever tudo isso em uma única linha e, além disso, fica imediatamente claro onde a quantificação está acontecendo!

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item = impl Baz>);

Não relacionado ao acima: quando jogamos para implementar let x: impl Trait no nightly? Fazia tempo que eu estava sem esse recurso.

@alexreg

Outra coisa é: alguém permitiria : dentro de uma associação de tipo associada, sob essa sintaxe?

Sim, porque não; Este seria um efeito natural de rust-lang/rfcs#2289 + type Foo: Bound .

Você também poderia fazer:

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item: Baz>);

@Centril Acho uma má ideia permitir duas sintaxes alternativas. Cheira como "nós não conseguimos decidir, então vamos apoiar os dois" síndrome. Ver o código que os mistura e combina será uma verdadeira monstruosidade!

@Centril Estou meio que com @nikomatsakis nessa sua RFC BTW, desculpe. Prefere escrever impl Iterator<Item = impl Baz> . Bonito e explícito.

@alexreg Isso é justo;

Mas (in)felizmente (dependendo do seu POV), já iniciamos o "permitir duas sintaxes alternativas" com impl Trait em posição de argumento, de modo que temos Foo: Bar e impl Bar trabalhando para significar a mesma coisa;

É para quantificação universal, mas a notação impl Trait realmente não se importa com o lado da dualidade em que está; afinal, não fomos com any Trait e some Trait .

Dado que já fizemos a escolha "não podemos decidir" e "o lado da dualidade não importa sintaticamente" , parece-me consistente aplicar "não podemos decidir" em todos os lugares para que os usuários não em "mas eu poderia escrever assim ali, por que não aqui?" ;)


Obs:

Ré. impl Iterator<Item = impl Baz> não funciona como um limite em uma cláusula where; então você teria que misturar como Iter: Iterator<Item = impl Baz> . Você teria que permitir: Iter = impl Iterator<Item = impl Baz> para que funcionasse uniformemente (talvez devêssemos?).

Usar : Bound também é em vez de = impl Bound também é explícito, apenas mais curto ^,-
Acho que a diferença no espaçamento entre X = Ty e X: Ty torna a sintaxe legível.

Tendo ignorado meu próprio conselho, vamos continuar essa conversa no RFC ;)

Mas (in)felizmente (dependendo do seu POV), já iniciamos o "permitir duas sintaxes alternativas" com impl Trait em posição de argumento, de modo que temos Foo: Bar e impl Bar trabalhando para significar a mesma coisa;

Fizemos, mas acredito que a escolha foi feita mais do ponto de vista da simetria/consistência. Os argumentos de tipo genérico são estritamente mais poderosos do que os de tipo universal ( impl Trait ). Mas estávamos introduzindo impl Trait na posição de retorno, fazia sentido introduzir a posição do argumento.

Dado que já fizemos a escolha "não podemos decidir" e "o lado da dualidade não importa sintaticamente", parece-me consistente aplicar "não podemos decidir" em todos os lugares para que os usuários não em "mas eu poderia escrever assim ali, por que não aqui?" ;)

Não tenho certeza se é o ponto em que devemos levantar os braços e dizer "vamos implementar tudo". Não há um argumento tão claro aqui quanto ao ganho.

Obs:

Ré. impl Iterator<Item = impl Baz> não funciona como um limite em uma cláusula where; então você teria que misturar como Iter: Iterator<Item = impl Baz> . Você teria que permitir: Iter = impl Iterator<Item = impl Baz> para que funcionasse uniformemente (talvez devêssemos?).

Eu diria que nós apenas apoiamos where Iter: Iterator<Item = T>, T: Baz (como temos agora) ou vamos até o fim com Iter = impl Iterator<Item = impl Baz> (como você sugeriu). Permitir apenas a casa de meio-termo parece uma desculpa.

Usar : Bound também é em vez de = impl limite também é explícito, apenas mais curto ^,-
Acho que a diferença de espaçamento entre X = Ty e X: Ty torna a sintaxe legível.

É legível, mas não acho que seja tão claro/explícito que um tipo existencial está sendo usado. Isso é agravado quando a definição precisa ser dividida em várias linhas devido à limitação dessa sintaxe.

Tendo ignorado meu próprio conselho, vamos continuar essa conversa no RFC ;)

Espere, você quer dizer o seu RFC? Eu acho que é relevante tanto para isso quanto para este, pelo que posso dizer. :-)

Espere, você quer dizer o seu RFC? Eu acho que é relevante tanto para isso quanto para este, pelo que posso dizer. :-)

OK; Vamos continuar aqui então;

Fizemos, mas acredito que a escolha foi feita mais do ponto de vista da simetria/consistência. Os argumentos de tipo genérico são estritamente mais poderosos do que os de tipo universal ( impl Trait ). Mas estávamos introduzindo impl Trait na posição de retorno, fazia sentido introduzir a posição do argumento.

Todo o meu ponto é sobre consistência e simetria. =P
Se você tem permissão para escrever impl Trait tanto para quantificação existencial quanto universal, para mim faz sentido que você também tenha permissão para usar Type: Trait para quantificação universal e existencial.

Em relação ao poder expressivo, o primeiro é mais poderoso que o segundo, como você diz, mas isso não precisa necessariamente ser o caso; Eles poderiam ser igualmente poderosos se quiséssemos que fossem AFAIK (embora eu absolutamente não esteja dizendo que devemos fazer isso ..).

fn foo(bar: impl Trait, baz: typeof bar) { // eww... but possible!
    ...
}

Não tenho certeza se é o ponto em que devemos levantar os braços e dizer "vamos implementar tudo". Não há um argumento tão claro aqui quanto ao ganho.

Meu argumento é que surpreender os usuários com "Esta sintaxe pode ser usada em outro lugar e seu significado está claro aqui, mas você não pode escrevê-la neste lugar" custa mais do que ter duas maneiras de fazê-lo (com as quais você deve se familiarizar de qualquer maneira ). Fizemos coisas semelhantes com https://github.com/rust-lang/rfcs/pull/2300 (mesclado), https://github.com/rust-lang/rfcs/pull/2302 (PFCP), https ://github.com/rust-lang/rfcs/pull/2175 (merged) onde preenchemos buracos de consistência mesmo que antes fosse possível escrever de outra forma.

É legível, mas não acho que seja tão claro/explícito que um tipo existencial está sendo usado.

Legível é suficiente na minha opinião; Eu não acho que Rust atribui a "explícito acima de tudo" e ser excessivamente detalhado (o que eu acho a sintaxe quando usado demais) também custa desencorajar o uso.
(Se você quiser que algo seja usado com frequência, dê uma sintaxe mais sucinta... cf ? como um suborno contra .unwrap() ).

Isso é agravado quando a definição precisa ser dividida em várias linhas devido à limitação dessa sintaxe.

Isso eu não entendo; Parece-me que Assoc = impl Trait deve causar divisões de linha ainda mais do que Assoc: Trait simplesmente porque o primeiro é mais longo.

Eu diria que ou apenas apoiamos where Iter: Iterator<Item = T>, T: Baz (como temos agora) ou vamos até o fim com Iter = impl Iterator<Item = impl Baz> (como você sugeriu).
Permitir apenas a casa de meio-termo parece uma desculpa.

Exatamente!, não vamos entrar no meio do caminho / cop-out e implementar where Iter: Iterator<Item: Baz> ;)

@Centril Ok, você me conquistou, principalmente no argumento de simetria/consistência. 😉 A ergonomia de ter as duas formas também ajuda. O linting pesado é uma obrigação para esse recurso, em pouco tempo.

Vou editar isso com minha resposta completa amanhã.

Editar

Como o @Centril aponta, já suportamos tipos universais usando a sintaxe : Trait (vinculado). por exemplo

fn foo<T: Trait>(x: T) { ... }

ao lado de tipos universais "próprios" ou "reificados", por exemplo

fn foo(x: impl Trait) { ... }

Claro, o primeiro é mais poderoso do que o último, mas o último é mais explícito (e sem dúvida mais legível) quando é tudo o que é necessário. Na verdade, acredito fortemente que deveríamos ter um lint do compilador em favor da última forma sempre que possível.

Agora, já temos impl Trait na posição de retorno da função também, que representa um tipo existencial. Os tipos de traços associados são existenciais na forma e já usam a sintaxe : Trait .

Isso, dada a existência do que eu devo tanto as formas próprias e limitadas de tipos universais em Rust no presente, e também a existência de formas próprias e vinculadas para tipos existenciais (este último apenas dentro de traços no momento), eu acredito fortemente que devemos estender o suporte para as formas próprias e vinculadas dos tipos existenciais para fora dos traços. Ou seja, devemos oferecer suporte ao seguinte geralmente e para tipos associados .

type A: Iterator<Item: Foo + Bar>;
type B = (impl Baz, impl Debug, String);

Eu também apoio o comportamento de linting do compilador sugerido neste comentário , que deve reduzir fortemente a variação de expressão de tipos existenciais comuns na natureza.

Eu ainda acredito que confundir quantificação universal e existencial em uma palavra-chave foi um erro, e então esse argumento de consistência não funciona para mim. A única razão pela qual uma única palavra-chave funciona em assinaturas de função é que o contexto necessariamente o restringe a usar apenas uma forma de quantificação em cada posição. Existem açúcares em potencial que eu vejo como algo em que você não tem as mesmas restrições

struct Foo {
    pub foo: impl Display,
}

Essa é uma abreviação para quantificação existencial ou universal? Da intuição derivada do uso de impl Trait em assinaturas de funções, não vejo como você poderia decidir. Se você realmente tentar usá-lo como ambos, perceberá rapidamente que a quantificação universal anônima nesta posição é inútil, então deve ser uma quantificação existencial, mas isso parece inconsistente com impl Trait em argumentos de função.

Essas são duas operações fundamentalmente diferentes, sim, ambas usam limites de traço, mas não vejo nenhuma razão para que ter duas maneiras de declarar um tipo existencial reduziria a confusão para os recém-chegados. Se tentar usar type Name: Trait for realmente uma coisa provável para os recém-chegados, isso pode ser resolvido por meio de um lint:

    type Foo: Display;
    ^^^^^^^^^^^^^^^^^^
note: were you attempting to create an existential type?
note: suggested replacement `type Foo = impl Display`

E acabei de apresentar uma formulação alternativa do seu argumento que eu seria muito mais receptivo, terá que esperar até que eu esteja em um computador real para reler a RFC e postar sobre.

Sinto que ainda não tenho experiência suficiente com Rust para comentar sobre RFCs. No entanto, estou interessado em ver esse recurso mesclado em Rust noturno e estável, a fim de usá-lo com Rust libp2p para construir um protocolo de fragmentação para Ethereum como parte da implementação de fragmentação do

Sinto que ainda não tenho experiência suficiente com Rust para comentar sobre RFCs. No entanto, estou interessado em ver esse recurso mesclado em Rust noturno e estável, a fim de usá-lo com Rust libp2p para construir um protocolo de fragmentação para Ethereum como parte da implementação de fragmentação do

Eu ainda acredito que confundir quantificação universal e existencial em uma palavra-chave foi um erro, e então esse argumento de consistência não funciona para mim.

Como princípio geral, independente dessa característica, considero essa linha de raciocínio problemática.

Acredito que devemos abordar o design de linguagem a partir de como uma linguagem é, e não de como gostaríamos que fosse sob algum desdobramento alternativo da história. A sintaxe impl Trait como quantificação universal na posição do argumento está estabilizada, portanto você não pode desejar que ela desapareça. Mesmo se você acredita que X, Y e Z foram erros (e eu poderia encontrar muitas coisas que pessoalmente acho que são erros no design de Rust, mas eu os aceito e assumo...), temos que conviver com eles agora, e acho que sobre como podemos fazer tudo se encaixar com o novo recurso (tornar as coisas consistentes).

Na discussão, acho que todo o corpus de RFCs e a linguagem como está deve ser tomado como se não axiomas, então argumentos fortes.


Você poderia argumentar (mas eu não) que:

struct Foo {
    pub foo: impl Display,
}

é semanticamente equivalente a:

struct Foo<T: Display> {
    pub foo: T,
}

sob o raciocínio função-argumento.

Basicamente, dado impl Trait , você tem que pensar "isso é tipo tipo de retorno ou tipo argumento?" , o que pode ser difícil.


Se tentar usar type Name: Trait for realmente uma coisa provável para os recém-chegados, isso pode ser resolvido por meio de um lint:

Eu também soltava fiapos, mas na outra direção; Eu acho que as seguintes maneiras devem ser idiomáticas:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Ok, formulação alternativa que acredito que a RFC 2071 está sugerindo e pode ter sido discutida na edição, mas nunca foi declarada explicitamente:

Há apenas _uma maneira_ de declarar tipos quantificados existencialmente: existential type Name: Bound; (usando existential porque isso está especificado na RFC, não sou totalmente contra a eliminação da palavra-chave nesta formulação).

Além disso, há açúcar para declarar implicitamente um tipo quantificado existencialmente sem nome no escopo atual: impl Bound (ignorando o açúcar de quantificação universal em argumentos de função por enquanto).

Portanto, o uso atual do tipo de retorno é uma simples remoção de açúcar:

fn foo() -> impl Iterator<Item = impl Display> { ... }
existential type _0: Display;
existential type _1: Iterator<Item = _0>;
fn foo() -> _1 { ... }

estender para const , static e let é igualmente trivial.

A única extensão não mencionada no RFC é: suportar este açúcar na sintaxe type Alias = Concrete; , então quando você escreve

type Foo = impl Iterator<Item = impl Display>;

isso é realmente açúcar para

existential type _0: Display;
existential type _1: Iterator<Item = _0>;
type Foo = _1;

que então depende da natureza transparente dos aliases de tipo para permitir que o módulo atual examine Foo e veja que ele se refere a um tipo existencial.

Na verdade, acredito fortemente que deveríamos ter um lint do compilador em favor da última forma sempre que possível.

Estou principalmente alinhado com o comentário de @alexreg , mas tenho algumas preocupações sobre o linting em relação a arg: impl Trait , principalmente devido ao risco de encorajar sempre mudanças de quebra nas bibliotecas já que impl Trait não funciona com turbofish (agora, e você precisaria de turbofish parcial para fazê-lo funcionar bem). Portanto, linting em clippy parece menos direto do que no caso de aliases de tipo (onde não há turbofish para causar problemas).

Estou principalmente alinhado com o comentário do @alexreg , mas tenho algumas preocupações sobre o linting em relação ao arg: impl Trait, principalmente devido ao risco de encorajar alterações semver nas bibliotecas, já que o impl Trait não funciona com turbofish (no momento, e você precisaria de turbofish parcial para fazê-lo funcionar bem). Portanto, linting em clippy parece menos direto do que no caso de aliases de tipo (onde não há turbofish para causar problemas).

O @Centril acabou de trazer isso ao IRC comigo, e concordo que é um ponto justo em relação à compatibilidade com versões anteriores (muito fácil de quebrar). Quando/se o turbofish parcial pousar, acho que um lint do compilador deve ser adicionado, mas não até então.

Então... nós tivemos muita discussão sobre a sintaxe para tipos existenciais nomeados agora. Devemos tentar chegar a uma conclusão e escrevê-la no post RFC/PR, para que alguém possa começar a trabalhar na implementação real? :-)

Pessoalmente, uma vez que nomeamos os existenciais, eu preferiria um fiapo (se houver) longe de qualquer uso de impl Trait qualquer lugar .

@rpjohnst Bem, você certamente concorda comigo e @Centril com relação aos existenciais nomeados ... quanto a fiapos deles em argumentos de função, isso é uma possibilidade, mas provavelmente uma discussão para outro lugar. Depende se se deseja favorecer a simplicidade ou a generalidade neste contexto.

O RFC em impl Trait em posição de argumento está atualizado? Em caso afirmativo, é seguro afirmar que sua semântica é _universal_? Se sim: eu quero chorar. Profundamente.

@phaazon : Notas de versão do Rust 1.26 por impl Trait :

Nota lateral para os teóricos do tipo por aí: isso não é um existencial, ainda é um universal. Em outras palavras, impl Trait é universal em uma posição de entrada, mas existencial em uma posição de saída.

Só para expressar meus pensamentos sobre isso:

  • Nós já tínhamos uma sintaxe para variáveis ​​de tipo e, na verdade, existem alguns usos para variáveis ​​de tipo qualquer (ou seja, é muito comum você querer usar a variável de tipo em vários lugares ao invés de simplesmente soltá-la em um único lugar).
  • Existenciais covariantes nos abririam as portas para funções de rank-n, algo que é difícil de fazer agora sem um traço (veja isso ) e é um recurso que realmente está faltando no Rust.
  • impl Trait é fácil de se referir como “tipo escolhido pelo receptor”, porque… porque é a única construção de linguagem por enquanto que nos permite fazer isso! A escolha do tipo pelo chamador já está disponível por meio de várias construções.

Eu realmente acho que a decisão atual de impl Trait na posição do argumento é uma pena. :choro:

Eu realmente acho que o impl Trait na decisão atual da posição do argumento é uma pena. 😢

Embora eu esteja um pouco dividido com isso, certamente acho que o tempo seria melhor gasto na implementação de let x: impl Trait agora!

Existenciais covariantes nos abririam as portas para funções de rank-n

Já temos uma sintaxe para ele ( fn foo(f: impl for<T: Trait> Fn(T)) ), (também conhecido como "type HRTB"), mas ainda não foi implementado. fn foo(f: impl Fn(impl Trait)) produz um erro que "aninhado impl Trait não é permitido", e espero que queiramos que isso signifique a versão de classificação mais alta, quando obtivermos o tipo HRTB.

Isso é semelhante a como Fn(&'_ T) significa for<'a> Fn(&'a T) , então não espero que seja controverso.

Olhando para o rascunho atual, impl Trait em posição de argumento é um _universal_, mas você está dizendo que impl for<_> Trait transforma em um _existencial_?! Quão louco é isso?

Por que achamos que tínhamos a necessidade de introduzir _ainda outra maneira_ de construir um _universal_? Quero dizer:

fn foo(x: impl MyTrait)

Só é interessante porque a variável de tipo anônimo aparece apenas uma vez no tipo . Se você precisar retornar o mesmo tipo:

fn foo(x: impl Trait) -> impl Trait

Obviamente não funcionará. Estamos dizendo às pessoas para mudar de um idioma mais geral para um mais restritivo, em vez de apenas aprender um e usá-lo em todos os lugares. Esse é todo o meu desabafo. Adicionando um recurso que não tem where e in-place-template-parâmetros.

Argh, acho que tudo isso já foi aceito e estou reclamando por nada. Só acho uma pena mesmo. Tenho certeza de que não sou o único desapontado com a decisão do RFC.

(Provavelmente não há muito sentido em continuar a debater isso depois que o recurso foi estabilizado, mas veja aqui um argumento convincente (com o qual eu concordo) por que impl Trait na posição de argumento com a semântica que tem é sensato e Tl;dr é pelo mesmo motivo pelo qual fn foo(arg: Box<Trait>) funciona mais ou menos da mesma maneira que fn foo<T: Trait>(arg: Box<T>) , mesmo que dyn Trait seja um existencial; agora substitua dyn com impl .)

Olhando para o rascunho atual, impl Trait em posição de argumento é universal, mas você está dizendo que impl for<_> Trait transforma em um existencial?!

Não, ambos são universais. Estou dizendo que os usos de classificação mais alta ficariam assim:

fn foo<F: for<G: Fn(X) -> Y> Fn(G) -> Z>(f: F) {...}

que poderia, ao mesmo tempo em que é adicionado (ou seja, sem alterações em impl Trait ) ser escrito como:

fn foo(f: impl for<G: Fn(X) -> Y> Fn(G) -> Z) {...}

Isso é impl Trait universal, só que Trait é um HRTB (semelhante a impl for<'a> Fn(&'a T) ).
Se decidirmos (o que eu espero que seja provável) que impl Trait dentro dos argumentos Fn(...) também é universal, você pode escrever isso para obter o mesmo efeito:

fn foo(f: impl Fn(impl Fn(X) -> Y) -> Z) {...}

Isto é o que eu pensei que você quis dizer com "classificação mais alta", se você não o fez, por favor me avise.

Uma decisão ainda mais interessante poderia ser aplicar o mesmo tratamento em posição existencial, ou seja, permitir isso (o que significaria "retornar algum fechamento que leva qualquer outro fechamento"):

fn foo() -> impl for<G: Fn(X) -> Y> Fn(G) -> Z {...}

ser escrito assim:

fn foo() -> impl Fn(impl Fn(X) -> Y) -> Z {...}

Isso seria um impl Trait existencial contendo um impl Trait universal (vinculado ao existencial, em vez da função delimitadora).

@eddyb Não faria mais sentido ter duas palavras-chave separadas para quantificação existencial e universal em geral, para consistência e para não confundir os novatos?
A palavra-chave para quantificação existencial também não seria reutilizável para tipos existenciais?
Por que estamos usando impl para quantificação existencial ( e universal), mas existential para tipos existenciais?

Gostaria de fazer três pontos:

  • Não há muito mérito em discutir se impl Trait é existencial ou universal. A maioria dos programadores provavelmente não leu manuais de teoria de tipos suficientes. A questão deve ser se as pessoas gostam ou se acham confuso. Para responder a essa pergunta, algum tipo de feedback pode ser visto aqui neste tópico, no reddit ou no fórum . Se algo precisar de mais explicações, ele falha em um teste decisivo para recurso intuitivo ou não surpreendente. Portanto, devemos observar quantas pessoas e quão confusas elas estão e se são mais perguntas do que com outro recurso. É realmente triste que esse feedback chegue após a estabilização e algo deve ser feito sobre esse fenômeno, mas é para uma discussão separada.
  • Tecnicamente, mesmo após a estabilização, haveria uma maneira de se livrar do recurso neste caso (deixando de lado a decisão se deveria). Seria possível usar lint contra funções de escrita que usam isso e remover a habilidade na próxima edição (enquanto preserva a capacidade de chamá-las se vierem de caixas de edição diferente). Isso satisfaria as garantias de estabilidade da ferrugem.
  • Não, adicionar mais duas palavras-chave para especificar tipos existenciais e universais não melhoraria a confusão, apenas tornaria as coisas ainda piores.

É realmente triste que esse feedback chegue após a estabilização e algo deve ser feito sobre esse fenômeno

Houve objeções a impl Trait na posição do argumento, desde que tenha sido uma ideia. Feedback como este _não é novo_, foi muito debatido mesmo no tópico RFC relevante. Houve muita discussão não apenas sobre tipos universais/existenciais de uma perspectiva da teoria dos tipos, mas também sobre como isso seria confuso para novos usuários.

É verdade que não obtivemos perspectivas reais de novos usuários, mas isso não surgiu do nada.

@Boscop any e some foram propostos como um par de palavras-chave para fazer este trabalho, mas foram decididos contra (embora eu não saiba se a lógica foi escrita em algum lugar).

É verdade que não conseguimos obter feedback de pessoas novas na ferrugem e que não eram teóricos de tipos

E o argumento para a inclusão sempre foi que facilitaria para os recém-chegados. Então, se agora temos feedback real dos recém-chegados, não deveria ser um tipo de feedback muito relevante , em vez de discutir como os recém-chegados devem entendê-lo?

Acho que se alguém tivesse tempo, algum tipo de pesquisa nos fóruns e outros lugares como as pessoas estavam confusas antes e depois da inclusão poderia ser feito (eu não era muito bom em estatística, mas tenho certeza que alguém que fosse poderia inventar algo que seja melhor do que previsões cegas).

E o argumento para a inclusão sempre foi que facilitaria para os recém-chegados. Então, se agora temos feedback real dos recém-chegados, não deveria ser um tipo de feedback muito relevante em vez de discutir como os recém-chegados devem entendê-lo?

Sim? Quero dizer, não estou discutindo se o que aconteceu foi uma boa ou má ideia. Quero apenas salientar que o segmento RFC recebeu feedback sobre isso, e foi decidido de qualquer maneira.

Como você disse, provavelmente é melhor ter a meta discussão sobre feedback em outro lugar, embora eu não tenha certeza de onde isso seria.

Não, adicionar mais duas palavras-chave para especificar tipos existenciais e universais não melhoraria a confusão, apenas tornaria as coisas ainda piores.

Pior? Como assim? Prefiro ter mais para lembrar do que ambiguidade/confusão.

Sim? Quero dizer, não estou discutindo se o que aconteceu foi uma boa ou má ideia. Quero apenas salientar que o segmento RFC recebeu feedback sobre isso, e foi decidido de qualquer maneira.

Certo. Mas ambos os lados discutindo eram programadores velhos, marcados e experientes com profundo entendimento do que acontece sob o capô, adivinhando sobre um grupo do qual não fazem parte (recém-chegados) e adivinhando sobre o futuro. De um ponto de vista factual, isso não é muito melhor do que jogar dados em relação ao que realmente acontece na realidade. Não se trata de experiência inadequada dos especialistas, mas de não ter dados adequados para basear as decisões.

Agora ele foi introduzido e temos uma maneira de obter os dados reais reais, ou dados tão concretos quanto qualquer um pode ser obtido na terra de quanto as pessoas ficam confusas em uma escala de 0 a 10.

Como você disse, provavelmente é melhor ter a meta discussão sobre feedback em outro lugar

Por exemplo aqui, eu já comecei essa discussão e existem alguns passos reais que podem ser dados, mesmo que pequenos: https://internals.rust-lang.org/t/idea-mandate-n-independent-uses -antes-estabilizar-um-recurso/7522/14. Eu não tive tempo de escrever o RFC, então se alguém me vencer ou quiser ajudar, não me importo.

Pior? Como assim?

Porque, a menos que impl Trait esteja obsoleto, você tem todos os 3, portanto, tem mais para lembrar além da confusão. Se impl Trait fosse embora, a situação seria diferente e haveria ponderação de prós e contras das duas abordagens.

impl Trait como no callee-picking seria suficiente. Se você tentar usá-lo em posição de argumento, então você introduz a confusão. Os HRTBs removeriam essa confusão.

@vorner Anteriormente, argumentei que deveríamos fazer testes A/B reais com iniciantes em Rust para ver o que eles acham realmente mais fácil e difícil de aprender, porque é difícil adivinhar como alguém que é proficiente em Rust.
FWIW, eu me lembro, quando eu estava aprendendo Rust (vindo de C++, D, Java etc), os genéricos de tipo quantificado universalmente (incluindo sua sintaxe) eram fáceis de entender (vidas em genéricos eram um pouco mais difíceis).
Eu acho que impl Trait para tipos de argumentos resultará em muita confusão de novatos no futuro e muitas perguntas como esta .
Na ausência de qualquer evidência de quais mudanças tornariam o Rust mais fácil de aprender, devemos nos abster de fazer tais mudanças e, em vez disso, fazer alterações que tornem/mantenham o Rust mais consistente porque a consistência o torna pelo menos fácil de lembrar. Os novatos em ferrugem terão que ler o livro algumas vezes de qualquer maneira, então introduzir impl Trait para argumentos para permitir adiar os genéricos no livro até mais tarde não elimina nenhuma complexidade.

@eddyb Aliás, por que precisamos de outra palavra-chave existential para tipos além de impl ? (Gostaria que usássemos some para ambos..)

FWIW, eu me lembro, quando eu estava aprendendo Rust (vindo de C++, D, Java etc), os genéricos de tipo quantificado universalmente (incluindo sua sintaxe) eram fáceis de entender (vidas em genéricos eram um pouco mais difíceis).

Eu mesmo também não acho que seja um problema. Na minha empresa atual, estou dando aulas de Rust ‒ por enquanto nos encontramos uma vez por semana e tento ensinar na implementação prática. As pessoas são programadores experientes, vindos principalmente de Java e Scala. Embora houvesse alguns bloqueios, genéricos (pelo menos lendo-os – eles são um pouco cuidadosos em realmente escrevê-los) na posição do argumento não era um problema. Houve um pouco de surpresa sobre os genéricos na posição de retorno (por exemplo, o chamador escolhe o que a função retorna), especialmente porque muitas vezes pode ser elidida, mas a explicação levou cerca de 2 minutos antes de clicar. Mas tenho medo até de mencionar a existência de impl Trait em posição de argumento, porque agora eu teria que responder à pergunta por que ele existe – e não tenho uma resposta real para isso. Isso não é bom para a motivação e ter motivação é crucial para o processo de aprendizagem.

Então, a questão é: a comunidade tem voz suficiente para reabrir o debate com alguns dados para respaldar os argumentos?

@eddyb Aliás, por que precisamos de outra palavra-chave existencial para tipos além de impl? (Gostaria que usássemos alguns para ambos ..)

Por que não forall … /me se esgueira lentamente

@phaazon Temos forall (ou seja, "universal") e é for , por exemplo, em HRTB ( for<'a> Trait<'a> ).

@eddyb Sim, use-o também para existencial , como Haskell faz com forall , por exemplo.

Toda a discussão é muito opinativa, estou um pouco surpreso que a ideia do argumento tenha sido estabilizada. Espero que haja uma maneira de empurrar outro RFC mais tarde para desfazer isso (estou completamente disposto a escrevê-lo porque realmente não gosto de toda a confusão que isso trará).

Eu realmente não entendo. Qual é o ponto de tê-los em posição de argumento? Não escrevo muito Rust, mas gostei muito de poder fazer -> impl Trait . Quando eu iria usá-lo na posição de argumento?

Meu entendimento era que era principalmente por consistência. Se eu posso escrever o tipo impl Trait em uma posição de argumento em uma assinatura fn, por que não posso escrever em outro lugar?

Dito isto, eu pessoalmente preferiria apenas dizer às pessoas "apenas use parâmetros de tipo" ...

Sim, é para consistência. Mas não tenho certeza se é um argumento bom o suficiente, quando os parâmetros de tipo são tão fáceis de usar. Além disso, surge o problema de qual lint para / contra!

Além disso, surge o problema de qual lint para / contra!

Considerando que você não pode expressar várias coisas com impl Trait , funcione com impl Trait pois um dos argumentos não pode fazer turbofish e, portanto, você não pode pegar seu endereço (esqueci alguns outra desvantagem?), acho que faz pouco sentido usar lint contra parâmetros de tipo, porque você precisa usá-los de qualquer maneira.

portanto você não pode pegar seu endereço

Você pode, por ter inferido a partir da assinatura.

Qual é o ponto de tê-los em posição de argumento?

Não há nenhum porque é exatamente a mesma coisa que usar um traço vinculado.

fn foo(x: impl Debug)

É exatamente a mesma coisa que

fn foo<A>(x: A) where A: Debug
fn foo<A: Debug>(x: A)

Além disso, considere isso:

fn foo<A>(x: A) -> A where A: Debug

impl Trait na posição de argumento não permite que você faça isso porque é anônimo . Este é então um recurso bastante inútil porque já temos tudo o que precisamos para lidar com tais situações. As pessoas não aprenderão esse novo recurso facilmente porque praticamente todo mundo conhece variáveis ​​de tipo / parâmetros de modelo e Rust é a única linguagem que usa essa sintaxe impl Trait . É por isso que muitas pessoas estão reclamando que ele deveria ter ficado com o valor de retorno / ligações let, porque introduziu uma semântica nova e necessária (ou seja, tipo escolhido pelo callee).

Para resumir, @iopq : você não precisará disso, e não há outro ponto além de “Vamos adicionar outra construção sintática de açúcar que ninguém realmente precisará porque lida com um uso muito específico – ou seja, variáveis ​​de tipo anônimas” .

Além disso, algo que esqueci de dizer: fica muito mais difícil ver como sua função é parametrizada/monomorfizada.

@Verner Com

Como é consistente quando -> impl Trait o chamador escolhe o tipo, enquanto em x: impl Trait o chamador escolhe o tipo?

Eu entendo que não há outra maneira de funcionar, mas isso não parece "consistente", parece o oposto de consistente

Eu realmente concordo que é tudo menos consistente e que as pessoas ficarão confusas, tanto os recém-chegados quanto os rustáceos proficientes e avançados.

Tivemos dois RFCs, que receberam um total de quase 600 comentários entre eles, começando há mais de 2 anos, para resolver as questões que estão sendo religadas neste tópico:

  • ferrugem-lang/rfcs#1522 ("Mínimo impl Trait ")
  • rust-lang/rfcs#1951 ("Finalizar sintaxe e escopo de parâmetro para impl Trait , enquanto expande para argumentos")

(Se você ler essas discussões, verá que eu era inicialmente um forte defensor da abordagem de duas palavras-chave. Agora acho que usar uma única palavra-chave é a abordagem correta.)

Após 2 anos e centenas de comentários, uma decisão foi tomada e o recurso foi estabilizado. Este é o problema de rastreamento para o recurso, que está aberto para rastrear os casos de uso ainda instáveis ​​para impl Trait . Religar os aspectos resolvidos de impl Trait está fora do tópico para este problema de rastreamento. Você pode continuar falando sobre isso, mas não no rastreador de problemas.

Como isso foi estabilizado quando impl Trait ainda não obteve apoio na posição argumentativa para fns em traits??

@daboross Então a caixa de seleção no post original precisa ser marcada!

(Apenas descobrindo que https://play.rust-lang.org/?gist=47b1c3a3bf61f33d4acb3634e5a68388&version=stable atualmente funciona)

Acho estranho que https://play.rust-lang.org/?gist=c29e80715ac161c6dc95f96a7f91aa8c&version=stable&mode=debug não funcione (ainda), o que é mais com esta mensagem de erro. Sou o único a pensar assim? Talvez fosse necessário adicionar uma caixa de seleção para impl Trait na posição de retorno em traços, ou foi uma decisão consciente de permitir apenas impl Trait na posição de argumento para funções de traço, forçando o uso de existential type para tipos de retorno? (o que… pareceria inconsistente para mim, mas talvez eu esteja perdendo um ponto?)

@Ekleog

foi uma decisão consciente permitir apenas impl Trait em posição de argumento para funções de traço, forçando o uso de tipo existencial para tipos de retorno?

Sim - a posição de retorno impl Trait em traços foi adiada até que tenhamos mais experiência prática usando tipos existenciais em traços.

@cramertj Ainda estamos no ponto em que temos experiência prática suficiente para implementar isso?

Eu gostaria de ver o impl Trait em algumas versões estáveis ​​antes de adicionarmos mais recursos.

@mark-im Eu não consigo ver o que é remotamente controverso sobre a posição de retorno impl Trait para métodos de traço, pessoalmente... talvez eu esteja perdendo alguma coisa.

Não acho polêmico. Sinto que estamos adicionando recursos muito rapidamente. Seria bom parar e se concentrar na dívida técnica por um tempo e obter experiência com o conjunto de recursos atual primeiro.

Eu vejo. Acho que considero apenas uma parte ausente de um recurso existente mais do que um novo recurso.

Acho que @alexreg está certo, é muito tentador usar impl Trait existencial em métodos de traits. Não é realmente um novo recurso, mas acho que há algumas coisas a serem abordadas antes de tentar implementá-lo?

@phaazon Talvez, sim... Eu realmente não sei o quanto os detalhes de implementação seriam diferentes em comparação com o que já temos hoje, mas talvez alguém possa comentar sobre isso. Eu adoraria ver tipos existenciais para ligações let/const também, mas posso definitivamente aceitar isso como um recurso além deste, esperando outro ciclo antes de começar com ele.

Eu me pergunto se podemos conter a implicância universal Trait em traits...

Mas sim, acho que entendi seu ponto.

@mark-im Não, não podemos, eles já estão estáveis .

Eles estão em funções, mas e as declarações de Trait?

@mark-im como mostra o trecho, eles são estáveis ​​em declarações de impls e traits.

Apenas entrando para saber onde estamos nos estabelecendo com abstract type . Pessoalmente, estou bastante de acordo com a sintaxe e as práticas recomendadas recentemente propostas do @Centril :

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

Que se aplica a algum código meu, acho que seria algo como:

// Concrete type with a generic body
struct Data<TBody> {
    ts: Timestamp,
    body: TBody,
}


// A name for an inferred iterator
type IterData = Data<impl Read>;
type Iter: Iterator<Item = IterData>;


// A function that gives us an iterator. Also takes some arbitrary range
fn iter(&self, range: impl RangeBounds<Timestamp>) -> Result<Iter, Error> { ... }


// A struct that holds on to that iterator
struct HoldsIter {
    iter: Iter,
}

Não faz sentido para mim que type Bar = (impl Display,); seja bom, mas type Bar = impl Display; seja ruim.

Se estivermos decidindo sobre diferentes sintaxes de tipo existencial alternativas (todas diferentes de rfc 2071 ?), um tópico do fórum em https://users.rust-lang.org/ seria um bom lugar para fazer isso?

Não tenho conhecimento suficiente das alternativas para iniciar um tópico agora, mas como os tipos existenciais ainda não foram implementados, acho que a discussão nos fóruns e, em seguida, um novo RFC provavelmente seria melhor do que falar sobre isso no problema de rastreamento .

O que há de errado com type Foo = impl Trait ?

@daboross Provavelmente o fórum interno. Estou pensando em escrever um RFC sobre isso para finalizar a sintaxe.

@daboross Já houve discussão mais do que suficiente sobre a sintaxe neste tópico. Acho que se o @Centril puder escrever um RFC para ele neste momento, ótimo.

Existe alguma questão que eu possa me inscrever para discussão sobre existenciais em traços?

Existe algum argumento relacionado a macro para uma sintaxe ou outra?

@tomaka no primeiro caso o type Foo = (impl Display,) é realmente a única sintaxe que você tem. Minha preferência por type Foo: Trait sobre type Foo = impl Trait vem apenas do fato de que estamos vinculando um tipo que podemos nomear, como <TFoo: Trait> ou where TFoo: Trait , enquanto que com impl Trait não podemos nomear o tipo.

Para esclarecer, não estou dizendo que type Foo = impl Bar é ruim, estou dizendo que type Foo: Bar é melhor em casos simples, em parte devido à motivação de @KodrAus .

O último eu li como: "o tipo Foo satisfaz Bar" e o primeiro como: "o tipo Foo é igual a algum tipo que satisfaz Bar". A primeira é assim, a meu ver, mais direta e natural a partir de uma visão extensional ("o que posso fazer com Foo"). Para entender o último, você precisa envolver uma compreensão mais profunda da quantificação existencial dos tipos.

type Foo: Bar também é bem legal porque se essa for a sintaxe usada como o limite em um tipo associado em uma característica, você pode simplesmente copiar a declaração na característica para o impl e ela simplesmente funcionará (se for todas as informações que você deseja expor..).

A sintaxe também é mais sucinta, especialmente quando os limites de tipo associados estão envolvidos e quando há muitos tipos associados. Isso pode reduzir o ruído e, portanto, ajudar na legibilidade.

@KodrAus

Aqui está como eu leio essas definições de tipo:

  • type Foo: Trait significa " Foo é um tipo que implementa Trait "
  • type Foo = impl Trait significa " Foo é um alias de algum tipo implementando Trait "

Para mim, Foo: Trait simplesmente declara uma restrição em Foo implementando Trait . De certa forma, type Foo: Trait parece incompleto. Parece que temos uma restrição, mas a definição real de Foo está faltando.

Por outro lado, impl Trait é evocativo de "este é um tipo único, mas o compilador descobre seu nome". Portanto, type Foo = impl Trait implica que já temos um tipo concreto (que implementa Trait ), do qual Foo é apenas um alias.

Acredito que type Foo = impl Trait transmite o significado correto de forma mais clara: Foo é um alias de algum tipo de implementação de Trait .

@stjepang

type Foo: Trait significa "Foo é um tipo que implementa Trait"
[..]
De certa forma, type Foo: Trait parece incompleto.

É assim que eu leio também (modulo fraseado...), e é uma interpretação extensionalmente correta. Isso diz tudo sobre o que você pode fazer com Foo (os morfismos que o tipo oferece). Portanto, é extensionalmente completo. Do ponto de vista dos leitores e principalmente dos iniciantes, acho que a extensionalidade é mais importante.

Por outro lado, impl Trait evoca "este é um tipo único, mas o compilador preenche a lacuna". Portanto, type Foo = impl Trait implica que já temos um tipo concreto (que implementa Trait ), do qual Foo é um alias, mas o compilador descobrirá qual tipo ele realmente é.

Trata-se de uma interpretação mais detalhada e intensional, preocupada com a representação, redundante do ponto de vista extensional. Mas isso é mais completo no sentido intensional.

@Centril

Do ponto de vista dos leitores e principalmente dos iniciantes, acho que a extensionalidade é mais importante.

Esta é uma interpretação mais detalhada e intensional preocupada com a representação que é redundante do ponto de vista extensional

A dicotomia extensional vs intensional é interessante - nunca pensei em impl Trait desta forma antes.

Ainda assim, eu discordo na conclusão. FWIW, eu nunca consegui grocar tipos existenciais em Haskell e Scala, então me conte como iniciante. :) impl Trait no Rust pareceu muito intuitivo desde o primeiro dia, o que provavelmente se deve ao fato de eu pensar nele como um alias restrito ao invés do que pode ser feito com o tipo. Então entre saber o Foo é eo que pode ser feito com ele, eu pegar o primeiro.

Apenas o meu 2c, no entanto. Outros podem ter diferentes modelos mentais de impl Trait .

Concordo inteiramente com este comentário : type Foo: Trait parece incompleto. E type Foo = impl Trait parece mais análogo aos usos de impl Trait outros lugares, o que ajuda a linguagem a parecer mais consistente e memorável.

@joshtriplett Consulte https://github.com/rust-lang/rust/issues/34511#issuecomment -387238653 para iniciar a discussão sobre consistência; Eu acredito que permitir formas de formulário é de fato a coisa mais consistente a se fazer. E permitir apenas uma das formas (qualquer que seja...) é inconsistente. Permitir type Foo: Trait também se encaixa particularmente bem com https://github.com/rust-lang/rfcs/pull/2289 com o qual você pode declarar: type Foo: Iterator<Item: Display>; que torna as coisas perfeitamente uniformes.

@stjepang A perspectiva extensional de type Foo: Bar; não requer que você entenda a quantificação existencial na teoria dos tipos. Tudo o que você realmente precisa entender é que Foo permite que você faça todas as operações oferecidas por Bar , é isso. Do ponto de vista de um usuário Foo , isso também é tudo o que é interessante.

@Centril

Acredito que agora entendo de onde você está vindo e o apelo de empurrar a sintaxe Type: Trait para o maior número possível de lugares.

Há uma forte conotação em torno de : sendo usado para limites de características de implementos de tipo e = sendo usado para definições de tipo e limites de tipo igual a outro tipo.

Eu acho que isso é aparente em seu RFC também. Por exemplo, considere estes dois limites de tipo:

  • Foo: Iterator<Item: Bar>
  • Foo: Iterator<Item = impl Bar>

Esses dois limites no final têm o mesmo efeito, mas são (eu acho) sutilmente diferentes. O primeiro diz " Item deve implementar o traço Bar ", enquanto o segundo diz " Item deve ser igual a algum tipo de implementação Bar ".

Deixe-me tentar ilustrar essa ideia usando outro exemplo:

trait Person {
    type Name: Into<String>; // Just a type bound, not a definition!
    // ...
}

struct Alice;

impl Person for Alice {
    type Name = impl Into<String>; // A concrete type definition.
    // ...
}

Como devemos definir um tipo existencial que implementa Person então?

  • type Someone: Person , que se parece com um tipo de limite.
  • type Someone = impl Person , que se parece com uma definição de tipo.

@stjepang Parecer um tipo de limite não é uma coisa ruim :) Podemos implementar Person for Alice assim:

struct Alice;
trait Person          { type Name: Into<String>; ... }
impl Person for Alice { type Name: Into<String>; ... }

Olha m'a! O material dentro de { .. } tanto para trait quanto para impl é idêntico, o que significa que você pode copiar o texto do trait intocado no que diz respeito a Name .

Como um tipo associado é uma função de nível de tipo (onde o primeiro argumento é Self ), podemos ver um alias de tipo como um tipo associado de 0-aridade, então nada de estranho está acontecendo.

Esses dois limites no final têm o mesmo efeito, mas são (eu acho) sutilmente diferentes. O primeiro diz "O item deve implementar a barra de característica", enquanto o último diz "O item deve ser igual a algum tipo de implementação da barra".

Sim; Acho a primeira frase mais direta e natural. :)

@Central Heh. Isso significa que type Thing; sozinho é suficiente para introduzir um tipo abstrato?

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { type Output; fn neg(self) -> Self::Output { self } }

@kennytm acho que é tecnicamente possível; mas você pode perguntar se é desejável ou não dependendo de seus pensamentos sobre implícito/explícito. Nesse caso em particular, acho que seria tecnicamente suficiente escrever:

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { fn neg(self) -> Self::Output { self } }

e o compilador poderia apenas inferir type Output: Sized; para você (que é um limite profundamente desinteressante que não fornece nenhuma informação). É algo a ser considerado para limites mais interessantes, mas não estará na minha proposta inicial porque acho que pode incentivar APIs de baixa affordance, mesmo quando o tipo concreto é muito simples, devido à preguiça do programador :) Nem type Output; inicialmente pelo mesmo motivo.

Acho que depois de ler tudo isso, tendo a concordar mais com o @Centril. Quando vejo type Foo = impl Bar , costumo pensar que Foo é um tipo específico, como com outros aliases. Mas não é. Considere este exemplo:

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

IMHO é um pouco estranho ver = na declaração de Displayable, mas não ter os tipos de retorno foo e bar iguais (ou seja, isso = não é transitivo, diferente de todos os outros). O problema é que Foo _não_ é um alias para um tipo específico que implique algum Trait. Dito de outra forma, é um tipo único em qualquer contexto em que é usado, mas esse tipo pode ser diferente para usos diferentes, como no exemplo.

Algumas pessoas mencionaram que type Foo: Bar parece "incompleto". Para mim isso é uma coisa boa. Em certo sentido Foo está incompleto; não sabemos o que é, mas sabemos que satisfaz Bar .

@mark-im

O problema é que Foo não é um alias para um tipo específico que implique algum Trait. Dito de outra forma, é um tipo único em qualquer contexto em que é usado, mas esse tipo pode ser diferente para usos diferentes, como no exemplo.

Uau, isso é realmente verdade? Isso certamente seria muito confuso para mim.

Existe uma razão pela qual Displayable seria uma abreviação de impl Display vez de um único tipo concreto? Esse comportamento é útil considerando que os aliases de traço (problema de rastreamento: https://github.com/rust-lang/rust/issues/41517) podem ser usados ​​de maneira semelhante? Exemplo:

trait Displayable = Display;

fn foo() -> impl Displayable { "hi" }
fn bar() -> impl Displayable { 42 }

@mark-im

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Isso não é um exemplo válido. Da seção de referência sobre tipos existenciais na RFC 2071 :

existential type Foo = impl Debug;

Foo pode ser usado como i32 em vários lugares ao longo do módulo. No entanto, cada função que usa Foo como i32 deve independentemente colocar restrições em Foo modo que deve ser i32

Cada declaração de tipo existencial deve ser restrita por pelo menos um corpo de função ou inicializador const/static. Um corpo ou inicializador deve restringir totalmente ou não colocar restrições em um determinado tipo existencial.

Não mencionado diretamente, mas necessário para que o restante da RFC funcione, é que duas funções no escopo do tipo existencial não podem determinar um tipo concreto diferente para esse existencial. Isso será algum tipo de erro de tipo conflitante.

Eu acho que seu exemplo daria algo como expected type `&'static str` but found type `i32` no retorno de bar , já que foo já teria definido o tipo concreto de Displayable para &'static str .

EDIT: A menos que você esteja chegando a isso pela intuição de que

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

é equivalente a

fn foo() -> impl Display { "hi" }
fn bar() -> impl Display { 42 }

ao invés da minha expectativa de

existential type _0 = impl Display;
type Displayable = _0;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Eu acho que qual dessas duas interpretações está correta pode depender do RFC que o @Centril pode estar escrevendo.

O problema é que Foo não é um alias para um tipo específico que implique algum Trait.

Eu acho que qual dessas duas interpretações está correta pode depender do RFC que o @Centril pode estar escrevendo.

A razão pela qual type Displayable = impl Display; existe é que é um alias para um tipo específico.
Veja https://github.com/rust-lang/rfcs/issues/1738 , que é o problema que esse recurso resolve.

@ Nemo157 Sua expectativa está correta. :)

A seguir:

type Foo = (impl Bar, impl Bar);
type Baz = impl Bar;

seria desaçucarado para:

/* existential */ type _0: Bar;
/* existential */ type _1: Bar;
type Foo = (_0, _1);

/* existential */ type _2: Bar;
type Baz = _2;

onde _0 , _1 e _2 são todos tipos nominalmente diferentes, portanto Id<_0, _1> , Id<_0, _2> , Id<_1, _2> (e o instâncias simétricas) são todas desabitadas, onde Id é definido em refl .

Isenção de responsabilidade: Eu (de boa vontade) não li a RFC (mas sei do que se trata), para poder comentar sobre o que parece “intuitivo” com sintaxes.

Para a sintaxe type Foo: Trait , eu esperaria completamente que algo assim fosse possível:

trait Trait {
    type Foo: Display;
    type Foo: Debug;
}

Da mesma forma que where Foo: Display, Foo: Debug é atualmente possível.

Se não for permitido sintaxe, acho que é um problema com a sintaxe.

Ah, e acho que quanto mais sintaxe o Rust tiver, mais difícil será aprendê-lo. Mesmo que uma sintaxe seja “mais fácil de aprender”, contanto que as duas sintaxes sejam necessárias, o iniciante eventualmente terá que aprender ambas, e provavelmente mais cedo ou mais tarde se entrar em um projeto já existente.

@Ekleog

Para a sintaxe type Foo: Trait , eu esperaria completamente que algo assim fosse possível:

É possível. Esses "aliases de tipo" declaram tipos associados (alias de tipo podem ser interpretados como funções de nível de tipo 0-ary enquanto tipos associados são funções de nível de tipo 1+-ary). Claro que você não pode ter vários tipos associados com o mesmo nome em uma característica, isso seria como tentar definir dois aliases de tipo com o mesmo nome em um módulo. Em um impl , type Foo: Bar também corresponde à quantificação existencial.

Ah, e acho que quanto mais sintaxe o Rust tiver, mais difícil será aprendê-lo.

Ambas as sintaxes já são usadas. type Foo: Bar; já é legal em traços, e também para quantificação universal como Foo: Bar onde Foo é uma variável de tipo. impl Trait é usado para quantificação existencial em posição de retorno e para quantificação universal em posição de argumento. Permitindo que ambos os plugs tenham lacunas de consistência na linguagem. Eles também são ótimos para diferentes cenários e, portanto, ambos oferecem o ótimo global.

Além disso, é improvável que o iniciante precise de type Foo = (impl Bar, impl Baz); . A maioria dos usos provavelmente será type Foo: Bar; .

O pull request original para RFC 2071 menciona uma palavra-chave typeof que parece ter sido totalmente descartada nesta discussão. Acho que a sintaxe proposta atualmente é bastante implícita, tendo o compilador e qualquer humano que lê o código procurando pelo tipo concreto.

Eu preferiria que isso fosse explícito. Então, em vez de

type Foo = impl SomeTrait;
fn foo_func() -> Foo { ... }

nós escreveríamos

fn foo_func() -> impl SomeTrait { ... }
type Foo = return_type_of(foo_func);

(com o nome do return_type_of a ser bikeshed), ou mesmo

fn foo_func() -> impl SomeTrait as Foo { ... }

que nem precisaria de novas palavras-chave e é facilmente compreensível por qualquer pessoa que conheça a sintaxe do impl Trait. A última sintaxe é concisa e tem todas as informações em um só lugar. Para traços, pode ser assim:

trait Bar
{
    type Assoc: SomeTrait;
    fn func() -> Assoc;
}

impl Bar for SomeType
{
    type Assoc = return_type_of(Self::func);
    fn func() -> Assoc { ... }
}

ou mesmo

impl Bar for SomeType
{
    fn func() -> impl SomeTrait as Self::Assoc { ... }
}

Desculpe se isso já foi discutido e descartado, mas não consegui encontrá-lo.

@Centril

É possível. Esses "aliases de tipo" declaram tipos associados (alias de tipo podem ser interpretados como funções de nível de tipo 0-ary enquanto tipos associados são funções de nível de tipo 1+-ary). Claro que você não pode ter vários tipos associados com o mesmo nome em uma característica, isso seria como tentar definir dois aliases de tipo com o mesmo nome em um módulo. Em um impl, digite Foo:Bar também corresponde à quantificação existencial.

(desculpe, eu quis colocar em impl Trait for Struct , não em trait Trait )

Desculpe, não sei se entendi. O que eu tento dizer é, para mim, código como

impl Trait for Struct {
    type Type: Debug;
    type Type: Display;

    fn foo() -> Self::Type { 42 }
}

(link do playground para a versão completa)
sente que deve funcionar.

Porque é apenas colocar dois limites em Type , da mesma forma que where Type: Debug, Type: Display funciona .

Se isso não for permitido (o que eu pareço entender por "É claro que você não pode ter vários tipos associados com o mesmo nome em um traço"? Mas dado meu erro ao escrever trait Trait vez de impl Trait for Struct não tenho certeza), então acho que é um problema com a sintaxe type Type: Trait .

Então, dentro de uma declaração trait , a sintaxe já é type Type: Trait e não permite múltiplas definições. Então eu acho que talvez este barco já tenha navegado há muito tempo…

No entanto, como apontado acima por @stjepang e @joshtriplett , type Type: Trait parece incompleto. E embora possa fazer sentido em declarações trait (na verdade, é projetado para ser incompleto, embora seja estranho não permitir várias definições), não faz sentido em um bloco impl Trait , onde o tipo deve ser conhecido com certeza (e atualmente só pode ser escrito como type Type = RealType )

impl Trait é usado para quantificação existencial em posição de retorno e para quantificação universal em posição de argumento.

Sim, eu também pensei em impl Trait em posição de argumento ao escrever isso, e me perguntei se eu deveria dizer que teria apoiado o mesmo argumento para impl Trait em posição de argumento se eu soubesse que estava passando por estabilização . Dito isto, acho melhor não reacender este debate :)

Permitindo que ambos os plugs tenham lacunas de consistência na linguagem. Eles também são ótimos para diferentes cenários e, portanto, ambos oferecem o ótimo global.

Óptimo e simplicidade

Bem, acho que às vezes perder o ótimo em favor da simplicidade é uma coisa boa. Tipo, C e ML nasceram na mesma época. C fez grandes concessões ao ótimo em favor da simplicidade, ML estava muito mais próximo do ótimo, mas muito mais complexo. Mesmo contando os derivados dessas linguagens, não acho que o número de desenvolvedores de C e de desenvolvedores de ML seja comparável.

impl Trait e :

Atualmente, em torno das sintaxes impl Trait e : , sinto que há uma tendência de criar duas sintaxes alternativas para o mesmo conjunto de recursos. No entanto, não acho que isso seja uma coisa boa, pois ter duas sintaxes para os mesmos recursos só pode confundir os usuários, especialmente quando eles sempre diferem sutilmente em sua semântica exata.

Imagine um iniciante que sempre viu type Type: Trait chegando ao seu primeiro type Type = impl Trait . Eles provavelmente podem adivinhar o que está acontecendo, mas tenho certeza que haverá um momento de “WTF é isso? Uso Rust há anos e ainda existe uma sintaxe que nunca vi?”. Que é mais ou menos a armadilha em que C++ caiu.

Inchaço de recursos

O que estou pensando é que, basicamente, quanto mais recursos ele tiver, mais difícil será aprender o idioma. E não vejo uma grande vantagem de usar type Type: Trait sobre type Type = impl Trait : são, tipo, 6 caracteres salvos?

Ter rustc emitindo um erro ao ver type Type: Trait que diz que a pessoa que está escrevendo para usar type Type = impl Trait faria muito mais sentido para mim: pelo menos há uma única maneira de escrever as coisas , faz sentido para todos ( impl Trait já é claramente reconhecido como existencial na posição de retorno), e abrange todos os casos de uso. E se as pessoas tentarem usar o que elas acham que é intuitivo (embora eu discorde disso, para mim = impl Trait é mais intuitivo, comparado ao atual = i32 ), elas são redirecionadas corretamente para o maneira convencionalmente correta de escrevê-lo.

O pull request original para RFC 2071 menciona uma palavra-chave typeof que parece ter sido totalmente descartada nesta discussão. Acho que a sintaxe proposta atualmente é bastante implícita, tendo o compilador e qualquer humano que lê o código procurando pelo tipo concreto.

typeof foi brevemente discutido na edição que abri há 1,5 anos: https://github.com/rust-lang/rfcs/issues/1738#issuecomment -258353755

Falando como iniciante, acho a sintaxe type Foo: Bar confusa. É a sintaxe de tipo associada, mas elas devem estar em traits, não em structs. Se você ver impl Trait uma vez, você pode descobrir o que é, ou então você pode procurar. É mais difícil fazer isso com a outra sintaxe e não tenho certeza de qual é o benefício.

Parece que algumas pessoas na equipe de linguagem realmente se opõem a usar impl Trait para nomear tipos existenciais, então eles preferem usar qualquer outra coisa. Até o comentário aqui faz pouco sentido para mim.

Mas de qualquer forma, acho que este cavalo foi espancado até a morte. Provavelmente existem centenas de comentários sobre a sintaxe e apenas um punhado de sugestões (percebo que só estou piorando as coisas). É claro que nenhuma sintaxe não deixará todos felizes, e há argumentos a favor e contra todos eles. Talvez devêssemos escolher um e ficar com ele.

Uau, não foi nada disso que eu entendi. Obrigado @Nemo157 por me

Nesse caso, eu realmente preferiria a sintaxe =.

@Ekleog

então eu acho que é um problema com a sintaxe type Type: Trait .

Poderia ser permitido e seria perfeitamente bem definido, mas você normalmente escreve where Type: Foo + Bar vez de where Type: Foo, Type: Bar , então isso não parece uma boa ideia. Você também pode facilmente disparar uma boa mensagem de erro para este caso sugerindo que você escreva Foo + Bar no caso do tipo associado.

type Foo = impl Bar; também tem problemas de compreensão em que você vê = impl Bar e conclui que você pode simplesmente substituí-lo em cada ocorrência onde ele é usado como -> impl Bar ; mas isso não funcionaria. @mark-im fez essa interpretação, o que parece um erro muito mais provável de ser cometido. Portanto, concluo que type Foo: Bar; é a melhor escolha para aprender.

No entanto, conforme apontado acima por @stjepang e @joshtriplett , digite Type: Trait parece incompleto.

Não está incompleto a partir de um POV extensional. Você obtém exatamente tanta informação de type Foo: Bar; quanto de type Foo = impl Bar; . Então, da perspectiva do que você pode fazer com type Foo: Bar; , está completo. Na verdade, o último é desaçucarado como type _0: Bar; type Foo = _0; .

EDIT: o que eu quis dizer foi que, embora possa parecer incompleto para alguns, não é do ponto de vista técnico.

Dito isto, acho melhor não reacender este debate :)

Essa é uma boa ideia. Devemos considerar a linguagem como ela é ao projetar, não como desejávamos que fosse.

Bem, acho que às vezes perder o ótimo em favor da simplicidade é uma coisa boa.

Se formos pela simplicidade, eu deixaria type Foo = impl Bar; vez disso.
Deve-se notar que a suposta simplicidade do C (suposta, porque Haskell Core e coisas semelhantes são provavelmente mais simples enquanto ainda soam ..) tem um preço alto quando se trata de expressividade e solidez. C não é minha estrela norte em design de linguagem; longe disso.

Atualmente, em torno das sintaxes impl Trait e : , sinto que há uma tendência de criar duas sintaxes alternativas para o mesmo conjunto de recursos. No entanto, não acho que isso seja uma coisa boa, pois ter duas sintaxes para os mesmos recursos só pode confundir os usuários, especialmente quando eles sempre diferem sutilmente em sua semântica exata.

Mas eles não diferirão em nada em sua semântica. Um açucar para o outro.
Acho que a confusão de tentar escrever type Foo: Bar; ou type Foo = impl Bar só para um deles não funcionar mesmo que ambos tenham semântica perfeitamente bem definida é só do jeito do usuário. Se um usuário tentar escrever type Foo = impl Bar; , um lint será acionado e proporá type Foo: Bar; . O lint está ensinando o usuário sobre a outra sintaxe.
Para mim, é importante que a linguagem seja uniforme e consistente; Se decidimos usar ambas as sintaxes em algum lugar, devemos aplicar essa decisão de forma consistente.

Imagine um iniciante que sempre viu type Type: Trait chegando ao seu primeiro type Type = impl Trait .

Nesse caso específico, um lint seria acionado e recomendaria a sintaxe anterior. Quando se trata de type Foo = (impl Bar, impl Baz); , o iniciante terá que aprender -> impl Trait em qualquer caso, então eles devem ser capazes de inferir o significado disso.

Que é mais ou menos a armadilha em que C++ caiu.

O problema do C++ é principalmente que ele é bastante antigo, tem a bagagem do C e muitos recursos que suportam muitos paradigmas. Esses não são recursos distintos, apenas sintaxe diferente.

O que estou pensando é que, basicamente, quanto mais recursos ele tiver, mais difícil será aprender o idioma.

Acho que aprender um novo idioma é principalmente aprender suas importantes bibliotecas. É aí que a maior parte do tempo será gasto. Os recursos certos podem tornar as bibliotecas muito mais combináveis ​​e funcionar em mais casos. Prefiro uma linguagem que dê um bom poder abstrativo do que uma que o force a pensar em baixo nível e que cause duplicação. Neste caso, não estamos adicionando mais poder abstrativo ou nem mesmo recursos, apenas melhor ergonomia.

E eu não vejo uma grande vantagem de usar type Type: Trait sobre type Type = impl Trait: são, tipo, 6 caracteres salvos?

Sim, apenas 6 caracteres salvos. Mas se considerarmos type Foo: Iterator<Item: Iterator<Item: Display>>; , então obteríamos: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; que tem muito mais ruído. type Foo: Bar; também é mais direto em comparação com o último, menos propenso a erros de interpretação (re. substituição..), e funciona melhor para tipos associados (copiar o tipo do traço..).
Além disso, type Foo: Bar poderia ser naturalmente estendido para type Foo: Bar = ConcreteType; que exporia o tipo concreto, mas também garantiria que ele satisfizesse Bar . Nada disso pode ser feito por type Foo = impl Trait; .

Fazer com que o rustc produza um erro ao ver type Type: Trait que diz que a pessoa que está escrevendo para usar type Type = impl Trait faria muito mais sentido para mim: pelo menos há uma única maneira de escrever as coisas,

eles são legitimamente redirecionados para a maneira convencionalmente correta de escrevê-lo.

Estou propondo que haja uma maneira convencional de escrever as coisas; type Foo: Bar; .

@lnicola

Falando como iniciante, acho a sintaxe type Foo: Bar confusa. É a sintaxe de tipo associada, mas elas devem estar em traits, não em structs.

Vou reiterar que os aliases de tipo realmente podem ser vistos como tipos associados. Você será capaz de dizer:

trait Foo        { type Baz: Quux; }
// User of `Bar::Baz` can conclude `Quux` but nothing more!
impl Foo for Bar { type Baz: Quux; }

// User of `Wibble` can conclude `Quux` but nothing more!
type Wibble: Quux;

Vemos que funciona exatamente da mesma forma em tipos associados e aliases de tipo.

Sim, apenas 6 caracteres salvos. Mas se considerarmos type Foo: Iterator<Item: Iterator<Item: Display>>; , então obteríamos: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>> ; que tem muito mais ruído.

Isso parece ortogonal à sintaxe para declarar um existencial nomeado. As quatro sintaxes que me lembro de terem sido propostas potencialmente permitiriam isso como

type Foo: Iterator<Item: Iterator<Item: Display>>;
type Foo = impl Iterator<Item: Iterator<Item: Display>>;
existential type Foo: Iterator<Item: Iterator<Item: Display>>;
existential type Foo = impl Iterator<Item: Iterator<Item: Display>>;

Ser capaz de usar sua abreviação proposta Trait<AssociatedType: Bound> vez da sintaxe Trait<AssociatedType = impl Bound> para declarar tipos existenciais anônimos para os tipos associados de um tipo existencial (nomeado ou anônimo) é um recurso independente (mas provavelmente relevante em termos de manter consistente todo o conjunto de características do tipo existencial).

@Nemo157 São recursos diferentes, sim; mas acho natural considerá-los juntos por uma questão de consistência.

@Centril

Sinto muito, mas eles estão errados. Não está incompleto a partir de um POV extensional.

Eu nunca sugeri que sua sintaxe proposta estivesse faltando informações; Eu estava sugerindo que parece incompleto; parece errado para mim e para os outros. Entendo que você discorde disso, da perspectiva de onde vem, mas isso não torna esse sentimento errado.

Observe também que neste tópico, as pessoas demonstraram um problema de interpretação com essa exata diferença de sintaxe. type Foo = impl Trait parece deixar mais claro que Foo é um tipo concreto específico, mas sem nome, não importa quantas vezes você o use, em vez de um alias para um traço que pode assumir um tipo concreto diferente cada vez que você usá-lo.

Acho que ajuda dizer às pessoas que elas podem pegar todas as coisas que sabem sobre -> impl Trait e aplicá-las a type Foo = impl Trait ; há um conceito generalizado impl Trait que eles podem ver usado como um bloco de construção em ambos os lugares. Sintaxe como type Foo: Trait esconde esse bloco de construção generalizado.

@joshtriplett

Eu estava sugerindo que parece incompleto; parece errado para mim e para os outros.

Tudo bem; Proponho que usemos um termo diferente de incomplete aqui porque, para mim, sugere falta de informação.

Observe também que neste tópico, as pessoas demonstraram um problema de interpretação com essa exata diferença de sintaxe.

O que observei foi um erro de interpretação, feito no tópico, sobre o que significa type Foo = impl Bar; . Uma pessoa interpretou diferentes usos de Foo como não sendo nominalmente do mesmo tipo, mas de tipos diferentes. Ou seja, exatamente: "um apelido para um traço que pode assumir um tipo concreto diferente cada vez que você o usa" .

Alguns afirmaram que type Foo: Bar; é confuso, mas não tenho certeza de qual é a interpretação alternativa de type Foo: Bar; que é diferente do significado pretendido. Eu estaria interessado em ouvir sobre interpretações alternativas.

@Centril

Vou reiterar que os aliases de tipo realmente podem ser vistos como tipos associados.

Eles podem, mas agora os tipos associados estão relacionados a características. impl Trait funciona em todos os lugares, ou quase. Se você quiser apresentar impl Trait como um tipo de tipo associado, você terá que introduzir dois conceitos de uma só vez. Ou seja, você vê impl Trait como um tipo de retorno de função, adivinha ou lê do que se trata, então quando você vê impl Trait em um alias de tipo, você pode reutilizar esse conhecimento.

Compare isso com a visualização de tipos associados em uma definição de traço. Nesse caso, você acha que é algo que outras estruturas devem definir ou implementar. Mas se você encontrar um type Foo: Debug fora de um traço, você não saberá o que é. Não há ninguém para implementá-lo, então é algum tipo de declaração de encaminhamento? Tem algo a ver com herança, como em C++? É como um módulo de ML onde outra pessoa escolhe o tipo? E se você já viu impl Trait antes, não há nada para fazer uma ligação entre eles. Escrevemos fn foo() -> impl ToString , não fn foo(): ToString .

tipo Foo = impl Bar; também tem problemas de compreensão, pois você vê = impl Bar e conclui que pode apenas substituí-lo em cada ocorrência em que é usado como -> impl Bar

Eu já disse isso aqui antes, mas é como pensar que let x = foo(); significa que você pode usar x vez de foo() . De qualquer forma, é um detalhe que alguém pode procurar rapidamente quando necessário, mas não muda fundamentalmente o conceito.

Ou seja, é fácil descobrir do que se trata (um tipo deduzido como em -> impl Trait ), mesmo que você não saiba exatamente como funciona (o que acontece quando você tem definições conflitantes para isso). Com a outra sintaxe é difícil até perceber o que é.

@Centril

Tudo bem; Proponho que usemos um termo diferente de incompleto aqui porque, para mim, sugere uma falta de informação.

"incompleto" não precisa significar falta de informação , pode significar que algo parece que deveria ter outra coisa e não tem.

type Foo: Trait; não parece uma declaração completa. Parece que está faltando alguma coisa. E parece gratuitamente diferente de type Foo = SomeType<X, Y, Z>; .

Talvez estejamos chegando ao ponto em que nossos one-liners por conta própria não podem realmente preencher essa lacuna de consenso entre type Inferred: Trait e type Inferred = impl Trait .

Você acha que valeria a pena montar uma implementação experimental desse recurso com qualquer sintaxe (mesmo a especificada na RFC) para que possamos começar a brincar com ele em programas maiores para ver como ele se encaixa no contexto?

@lnicola

[..] impl Trait funciona em qualquer lugar, ou quase

Bem, Foo: Bound também funciona em quase todos os lugares ;)

Mas se você encontrar um type Foo: Debug fora de um traço, você não saberá o que é.

Eu acho que a progressão de usá-lo em: trait -> impl -> type alias ajuda no aprendizado.
Além disso, acho que a inferência de que "o tipo Foo implementa Debug" é provavelmente de
vendo type Foo: Debug em traços e de limites genéricos e também está correto.

Tem algo a ver com herança, como em C++?

Acho que a falta de herança no Rust precisa ser aprendida em um estágio muito anterior ao aprender o recurso que estamos discutindo, pois isso é tão fundamental para o Rust.

É como um módulo de ML onde outra pessoa escolhe o tipo?

Essa inferência também pode ser feita para type Foo = impl Bar; devido a arg: impl Bar onde o chamador (usuário) escolhe o tipo. Para mim, a inferência de que o usuário escolhe o tipo parece menos provável para type Foo: Bar; .

Eu já disse isso aqui antes, mas é como pensar que let x = foo(); significa que você pode usar x vez de foo() .

Se a linguagem for referencialmente transparente, você pode substituir x por foo() . Até adicionarmos type Foo = impl Foo; ao sistema, os aliases de tipo são afaik referencialmente transparentes. Por outro lado, se já houver uma vinculação x = foo() disponível, outros foo() em serão substituídos por x .

@joshtriplett

"incompleto" não precisa significar falta de informação, pode significar que algo parece que deveria ter outra coisa e não tem.

É justo; mas o que é suposto ter que não tem?

type Foo: Trait; não parece uma declaração completa.

Parece-me completo. Parece um julgamento que Foo satisfaz Trait que é precisamente o significado pretendido.

@Centril para mim o "algo faltando" é o tipo real para o qual este é um alias. Isso está um pouco relacionado à minha confusão anterior. Não é que não exista tal tipo, apenas que esse tipo é anônimo... Usar = sutilmente implica que existe um tipo e é sempre o mesmo tipo, mas não podemos nomeá-lo.

Eu acho que estamos meio que esgotando esses argumentos. Seria ótimo apenas implementar ambas as sintaxes experimentalmente e ver o que funciona melhor.

@mark-im

@Centril para mim o "algo faltando" é o tipo real para o qual este é um alias. Isso está um pouco relacionado à minha confusão anterior. Não é que não exista tal tipo, apenas que esse tipo é anônimo... Usar = sutilmente implica que existe um tipo e é sempre o mesmo tipo, mas não podemos nomeá-lo.

Isso é exatamente o que parece para mim, também.

Alguma chance de resolver os dois itens adiados em breve, além da questão da elisão vitalícia? Eu mesma faria, mas não faço ideia de como!

Ainda há muita confusão sobre o que exatamente impl Trait significa, e não é nada óbvio. Eu acho que os itens adiados definitivamente devem esperar até que tenhamos uma ideia clara da semântica exata de impl Trait (que deve estar chegando em breve).

@varkor Que semântica não está clara? AFAIK nada sobre a semântica de impl Trait mudou desde o RFC 1951 e expandiu em 2071.

@alexreg Eu não tinha planos para isso, mas aqui está um esboço: Após a adição da análise, você precisa diminuir os tipos de static s e const s dentro de um impl existencial contexto trait, como é feito aqui para os tipos de retorno de funções. . No entanto, você vai querer tornar DefId em ImplTraitContext::Existential opcional, já que você não quer que seu impl Trait pegue genéricos de uma definição de função pai. Isso deve levar você a um bom caminho. Você pode ter mais facilidade se basear-se no tipo existencial PR de @oli-obk .

@cramertj : a semântica de impl Trait na linguagem é inteiramente restrita ao seu uso em assinaturas de funções e não é verdade que estendê-la para outras posições tenha um significado óbvio. Direi algo mais detalhado sobre isso em breve, onde a maior parte da conversa parece estar acontecendo.

@varkor

a semântica de impl Trait na linguagem é inteiramente restrita ao seu uso em assinaturas de funções e não é verdade que estendê-lo para outras posições tenha um significado óbvio.

O significado foi especificado na RFC 2071 .

@cramertj : o significado na RFC 2071 é ambíguo e permite múltiplas interpretações do que a frase "tipo existencial" significa lá.

TL;DR — Tentei definir um significado preciso para impl Trait , que acho que esclarece detalhes que eram, pelo menos intuitivamente, pouco claros; juntamente com uma proposta para uma nova sintaxe de alias de tipo.

Tipos existenciais em Rust (post)


Tem havido muita discussão acontecendo no Discord rust-lang chat sobre a semântica precisa (ou seja, formal, teórica) de impl Trait nos últimos dias. Acho que foi útil esclarecer muitos detalhes sobre o recurso e exatamente o que é e o que não é. Também esclarece quais sintaxes são plausíveis para aliases de tipo.

Escrevi um pequeno resumo de algumas de nossas conclusões. Isso fornece uma interpretação de impl Trait que eu acho que é bastante limpa, e descreve precisamente as diferenças entre a posição do argumento impl Trait e a posição de retorno impl Trait (que não é "universalmente- quantificado" versus "quantificado existencialmente"). Há também algumas conclusões práticas.

Nele, proponho uma nova sintaxe que atende aos requisitos comumente declarados de um "alias de tipo existencial":
type Foo: Bar = _;

Por ser um tópico tão complexo, há muitas coisas que precisam ser esclarecidas primeiro, então escrevi-o como um post separado. O feedback é muito apreciado!

Tipos existenciais em Rust (post)

@varkor

A RFC 2071 é ambígua e permite múltiplas interpretações do que a frase "tipo existencial" significa lá.

Como é ambíguo? Eu li seu post-- ainda estou ciente de apenas um significado de existencial não dinâmico em estáticas e constantes. Ele se comporta da mesma maneira que a posição de retorno impl Trait , introduzindo uma nova definição de tipo existencial por item.

type Foo: Bar = _;

Discutimos essa sintaxe durante a RFC 2071. Como eu disse lá, gosto que ela demonstre claramente que Foo é um único tipo inferido e que deixa espaço para tipos não inferidos que são deixados existenciais fora do módulo atual ( ex., type Foo: Bar = u32; ). Não gostei de dois aspectos: (1) não tem palavra-chave e, portanto, é mais difícil de pesquisar e (b) tem o mesmo problema de verbosidade em comparação com type Foo = impl Trait que a sintaxe abstract type Foo: Bar; tem: type Foo = impl Iterator<Item = impl Display>; torna-se type Foo: Iterator<Item = MyDisplay> = _; type MyDisplay: Display = _; . Eu não acho que nenhum deles seja decisivo, mas não é uma vitória clara de uma forma ou de outra IMO.

@cramertj A ambiguidade surge aqui:

type Foo = impl Bar;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Se Foo fosse realmente um alias de tipo para um tipo existencial, então f e g suportariam diferentes tipos de retorno concretos. Várias pessoas leram instintivamente essa sintaxe dessa maneira e, de fato, alguns participantes da discussão sobre a sintaxe RFC 2071 apenas perceberam que não é assim que a proposta funciona como parte da recente discussão do Discord.

O problema é que, especialmente em face da posição-argumento impl Trait , não está claro para onde o quantificador existencial deve ir. Para argumentos, ele tem um escopo bem definido; para a posição de retorno, parece bem delimitado, mas acaba sendo mais amplo do que isso; para type Foo = impl Bar ambas as posições são plausíveis. A sintaxe baseada em _ aponta para uma interpretação que nem envolve "existencial", evitando esse problema.

Se Foo fosse realmente um alias de tipo para um tipo existencial

(grifo meu). Eu li que 'an' como 'um específico' o que significa que f e g _não_ suportariam diferentes tipos de retorno concretos, uma vez que se referem ao mesmo tipo existencial. Eu sempre vi type Foo = impl Bar; usando o mesmo significado de let foo: impl Bar; , ou seja, introduzindo um novo tipo existencial anônimo; tornando seu exemplo equivalente a

existential type _0: Bar;
type Foo = _0;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

que espero seja relativamente inequívoco.


Um problema é que o significado de " impl Trait em aliases de tipo" nunca foi especificado em um RFC. É brevemente mencionado na seção "Alternativas" da RFC 2071 , mas explicitamente descontado por causa dessas ambiguidades inerentes ao ensino.

Também sinto que vi alguma menção de que os aliases de tipo já não são referencialmente transparentes. Acho que estava no u.rl.o, mas não consegui encontrar a discussão depois de algumas pesquisas.

@cramertj
Para seguir o ponto de @rpjohnst , existem várias interpretações da semântica de impl Trait , que são todas consistentes com o uso atual em assinaturas, mas têm consequências diferentes ao estender impl Trait para outros locais (eu conheço 2 além do descrito no post, mas que não estão prontos para discussão). E não acho que seja verdade que a interpretação no post seja necessariamente a mais óbvia (eu pessoalmente não vi nenhuma explicação semelhante sobre APIT e RTIP nessa perspectiva).

Em relação ao type Foo: Bar = _; , acho que talvez deva ser discutido novamente – não há mal nenhum em revisitar idéias antigas com novos olhos. Sobre seus problemas com ele:
(1) Não tem palavra-chave, mas é a mesma sintaxe da inferência de tipo em qualquer lugar. A pesquisa na documentação por "sublinhado" / "tipo de sublinhado" / etc. pode fornecer facilmente uma página sobre inferência de tipo.
(2) Sim, isso é verdade. Estamos pensando em uma solução para isso, que acho que se encaixa bem com a notação de sublinhado, que esperamos estar pronta para sugerir em breve.

Como @cramertj, eu realmente não estou vendo o argumento aqui.

Eu simplesmente não vejo a ambiguidade fundamental que o post de @varkor descreve. Acho que sempre interpretamos "tipo existencial" em Rust como "existe um tipo _único_ que..." e não "existe pelo menos um tipo que..." porque (como diz o post de @varkor ) o este último equivale a "tipos universais" e, portanto, a frase "tipo existencial" seria totalmente inútil se pretendêssemos permitir essa interpretação. afaik cada RFC sobre o assunto sempre assumiu tipos universais e existenciais eram duas coisas distintas. Eu entendo que na teoria dos tipos é isso que significa e que o isomorfismo é muito matematicamente real, mas para mim isso é apenas um argumento de que estamos usando mal a terminologia da teoria dos tipos e precisamos escolher algum outro jargão para isso, não um argumento que a semântica pretendida de impl Trait sempre foi pouco clara e precisa ser repensada.

A ambiguidade de escopo que @rpjohnst descreve é ​​um problema sério, mas toda sintaxe proposta é potencialmente confundida com qualquer tipo de alise ou tipos associados. Qual dessas confusões é "pior" ou "mais provável" é precisamente o interminável bicicletário que já falhamos em resolver após várias centenas de comentários. Eu gosto que type Foo: Bar = _; parece corrigir o problema de type Foo: Bar; de precisar de uma explosão de várias declarações para declarar qualquer existencial um pouco não trivial, mas não acho que seja suficiente para realmente mudar o situação de "bikeshed sem fim".

O que estou convencido é que qualquer sintaxe com a qual acabemos precisa ter uma palavra-chave diferente de type , porque todas as sintaxes "apenas type " são muito enganosas. Na verdade, talvez não use type na sintaxe _at all_, então não há como alguém assumir que está olhando para "um alias de tipo, mas mais existencial de alguma forma".

existential Foo = impl Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }
existential Foo: Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }



md5-b59626c5715ed89e0a93d9158c9c2535



existential Foo: Trait = _;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Não é óbvio para mim que qualquer um desses _previna_ completamente a interpretação errônea de que f e g poderiam retornar dois tipos diferentes de implementação de Trait , mas suspeito que isso seja o mais próximo da prevenção quanto poderíamos conseguir.

@Ixrec
A frase "tipo existencial" é problemática especificamente _por causa_ da ambiguidade do escopo. Eu não vi ninguém apontar que o escopo é totalmente diferente para o APIT e o RPIT. Isso significa que uma sintaxe como type Foo = impl Bar , onde impl Bar é um "tipo existencial" é inerentemente ambígua.

Sim, a terminologia da teoria dos tipos tem sido muito mal utilizada. Mas tem sido mal utilizado (ou pelo menos não explicado) no RFC - então há ambiguidade decorrente do próprio RFC.

A ambiguidade de escopo que @rpjohnst descreve é ​​um problema sério, mas toda sintaxe proposta é potencialmente confundida com qualquer tipo de alise ou tipos associados. Qual dessas confusões é "pior" ou "mais provável" é precisamente o interminável bicicletário que já falhamos em resolver após várias centenas de comentários.

Não, não acho que isso seja verdade. É possível criar uma sintaxe consistente que não tenha essa confusão. Eu arriscaria a queda de bicicletas porque as duas propostas atuais são ruins, então elas realmente não satisfazem ninguém.

O que estou convencido é que qualquer sintaxe com a qual acabamos precisa ter uma palavra-chave diferente de type

Também acho que isso não é necessário. Em seus exemplos, você inventou uma notação inteiramente nova, que é algo que você deseja evitar no design de linguagem sempre que possível — caso contrário, você cria uma linguagem enorme cheia de sintaxe inconsistente. Você deve explorar uma sintaxe completamente nova somente quando não houver opções melhores. E eu argumento que há uma opção melhor.

A parte: em uma nota lateral, acho que é possível se afastar totalmente dos "tipos existenciais", deixando toda a situação mais clara, que eu ou outra pessoa seguiremos em breve.

Eu me pego pensando que uma sintaxe diferente de type também ajudaria, precisamente porque muitas pessoas interpretam type como um simples alias substituível, o que implicaria na interpretação "tipo potencialmente diferente a cada vez".

Eu não vi ninguém apontar que o escopo é totalmente diferente para o APIT e o RPIT.

Eu pensei que o escopo sempre foi uma parte explícita das propostas do impl Trait, então não precisava "apontar". Tudo o que você disse sobre o escopo parece apenas reiterar o que já aceitamos em RFCs anteriores. Eu entendo que não é óbvio para todos da sintaxe e isso é um problema, mas não é como se ninguém tivesse entendido isso antes. Na verdade, eu pensei que uma grande parte da discussão sobre o RFC 2701 era sobre qual deveria ser o escopo de type Foo = impl Trait; , no sentido de que tipo de inferência é e não é permitido olhar.

É possível criar uma sintaxe consistente que não tenha essa confusão.

Você está tentando dizer que type Foo: Bar = _; é essa sintaxe, ou você acha que ainda não a encontramos?

Eu não acho que seja possível chegar a uma sintaxe sem qualquer confusão semelhante, não porque sejamos insuficientemente criativos, mas porque a maioria dos programadores não são teóricos de tipos. Provavelmente podemos encontrar uma sintaxe que reduza a confusão a um nível tolerável, e certamente há muitas sintaxes que seriam inequívocas para os veteranos da teoria dos tipos, mas nunca eliminaremos a confusão completamente.

você inventou uma notação inteiramente nova

Eu pensei que tinha acabado de substituir uma palavra-chave por outra palavra-chave. Você está vendo alguma mudança adicional que eu não pretendia?

Venha para pensar sobre isso, já que temos usado mal "existencial" todo esse tempo, isso significa que existential Foo: Trait / = impl Trait provavelmente não são mais sintaxes legítimas.

Então, precisamos de uma nova palavra-chave para colocar na frente de nomes que se referem a algum tipo de código desconhecido para externo... e estou desenhando um espaço em branco nisso. alias , secret , internal , etc todos parecem muito terríveis, e é improvável que tenham menos "confusão de singularidade" do que type .

Venha para pensar sobre isso, já que temos usado mal "existencial" todo esse tempo, isso significa que existential Foo: Trait / = impl Trait provavelmente não são mais sintaxes legítimas.

Sim, eu concordo completamente - acho que precisamos nos afastar completamente do termo "existencial"* (houve algumas ideias provisórias de como fazer isso enquanto ainda explica impl Trait ).

*(possivelmente reservando o prazo apenas para dyn Trait )

@joshtriplett , @Ixrec : Eu concordo que a notação _ significa que você não pode mais substituir da mesma forma que antes, e se isso for uma prioridade a ser mantida, precisaríamos de uma sintaxe diferente.

Tenha em mente que _ já é um caso especial com relação à substituição de qualquer maneira - não é apenas aliases de tipo que isso afeta: em qualquer lugar em que você possa usar _ , você está impedindo a transparência referencial completa.

Tenha em mente que _ já é um caso especial com relação à substituição de qualquer maneira - não é apenas aliases de tipo que isso afeta: em qualquer lugar que você possa usar _, você está impedindo a transparência referencial completa.

Você poderia nos explicar o que isso significa exatamente? Eu não estava ciente de uma noção de "transparência referencial" que é afetada por _ .

Eu concordo que a notação _ significa que você não pode mais substituir na mesma medida que antes, e se isso for uma prioridade a ser mantida, precisaríamos de uma sintaxe diferente.

Não tenho certeza se é uma _prioridade_. Para mim, foi apenas o único argumento objetivo que encontramos que parecia preferir uma sintaxe à outra. Mas tudo isso provavelmente mudará com base em quais palavras-chave podemos criar para substituir type .

Você poderia nos explicar o que isso significa exatamente? Eu não estava ciente de uma noção de "transparência referencial" que é afetada por _ .

Sim, desculpe, estou jogando palavras sem explicá-las. Deixe-me reunir meus pensamentos e formularei uma explicação mais coesa. Ele se encaixa bem com uma maneira alternativa (e potencialmente mais útil) de olhar para impl Trait .

Por transparência referencial , entende-se que é possível substituir uma referência por sua definição e vice-versa sem alteração na semântica. Em Rust, isso claramente não se aplica ao nível de prazo para fn . Por exemplo:

fn foo() -> usize {
    println!("ey!");
    42
}

fn main() {
    let bar = foo();
    let baz = bar + bar;
}

se substituirmos cada ocorrência de bar por foo() (a definição de bar ), obteremos claramente uma saída diferente.

No entanto, para aliases de tipo, a transparência referencial é mantida (AFAIK) no momento. Se você tiver um alias:

type Foo = Definition;

Então você pode fazer (captura evitar) substituição de ocorrências de Foo por Definition e substituição de ocorrências de Definition por Foo sem alterar a semântica do seu programa , ou sua correção de tipo.

Apresentando:

type Foo = impl Bar;

significar que cada ocorrência de Foo é do mesmo tipo significa que se você escrever:

fn stuff() -> Foo { .. }
fn other_stuff() -> Foo { .. }

você não pode substituir as ocorrências de Foo por impl Bar e vice-versa. Ou seja, se você escrever:

fn stuff() -> impl Bar { .. }
fn other_stuff() -> impl Bar { .. }

os tipos de retorno não serão unificados com Foo . Assim, a transparência referencial é quebrada para aliases de tipo introduzindo impl Trait com a semântica da RFC 2071 dentro deles.

Sobre transparência referencial e type Foo = _; , continua... (por @varkor)

Eu me pego pensando que uma sintaxe diferente de tipo também ajudaria, precisamente porque muitas pessoas interpretam tipo como um simples alias substituível, o que implicaria na interpretação "tipo potencialmente diferente a cada vez".

Bom ponto. Mas o bit de atribuição = _ implica que é apenas um único tipo?

Já escrevi isso antes, mas...

Transparência referencial: acho que é mais útil olhar para type como uma ligação (como let ) em vez de uma substituição semelhante ao pré-processador C. Uma vez que você olha dessa forma, type Foo = impl Trait significa exatamente o que parece.

Imagino que os iniciantes serão menos propensos a pensar em impl Trait como tipos existenciais versus universais, mas como "uma coisa que impl sa Trait . If they want to know more, they can read the impl Trait documentação. alterar a sintaxe, você perde a conexão entre ela e o recurso existente sem muito benefício. _Você está apenas substituindo uma sintaxe potencialmente enganosa por outra._

Re type Foo = _ , sobrecarrega _ com um significado completamente não relacionado. Também pode parecer difícil de encontrar na documentação e/ou no Google.

@lnicola Você também pode usar ligações const vez de ligações let , onde a primeira é referencialmente transparente. Escolher let (que não é referencialmente transparente dentro de fn ) é uma escolha arbitrária que não acho particularmente intuitiva. Acho que a visão intuitiva dos aliases de tipo é que eles são referencialmente transparentes (mesmo que essa palavra não seja usada) porque são aliases .

Eu também não estou olhando para type como substituição do pré-processador C porque tem que ser captura evitando e respeitando os genéricos (sem SFINAE). Em vez disso, estou pensando em type exatamente como faria uma ligação em uma linguagem como Idris ou Agda, onde todas as ligações são puras.

Imagino que os iniciantes serão menos propensos a pensar em impl Trait como tipos existenciais versus universais, mas como "uma coisa que implica um Trait

Isso me parece uma distinção sem diferença. O jargão "existencial" não é usado, mas acredito que o usuário o esteja vinculando intuitivamente ao mesmo conceito de um tipo existencial (que nada mais é do que "algum tipo Foo que impls Bar" no contexto de Rust).

Re type Foo = _ , sobrecarrega _ com um significado completamente não relacionado.

Como assim? type Foo = _; aqui se alinha com o uso de _ em outros contextos onde um tipo é esperado.
Significa "inferir o tipo real", assim como quando você escreve .collect::<Vec<_>>() .

Também pode parecer difícil de encontrar na documentação e/ou no Google.

Não deveria ser tão difícil? "type alias underscore" deve trazer o resultado desejado...?
Não parece diferente de procurar por "type alias impl trait".

O Google não indexa caracteres especiais. Se minha pergunta do StackOverflow tiver um sublinhado, o Google não a indexará automaticamente para consultas que contenham a palavra sublinhado

@Centril

Como assim? type Foo = _; aqui se alinha com o uso de _ em outros contextos onde um tipo é esperado.
Significa "inferir o tipo real", assim como quando você escreve .collect::>().

Mas esse recurso não infere o tipo e fornece um alias de tipo para ele, ele cria um tipo existencial que (fora de algum escopo limitado como módulo ou caixa) não se unifica com "o tipo real".

O Google não indexa caracteres especiais.

Isso não é mais verdade (embora possivelmente dependente de espaço em branco...?).

Mas esse recurso não infere o tipo e fornece um alias de tipo para ele, ele cria um tipo existencial que (fora de algum escopo limitado como módulo ou caixa) não se unifica com "o tipo real".

A semântica sugerida de type Foo = _; é uma alternativa a ter um alias de tipo existencial, baseado inteiramente em inferência. Se isso não ficou totalmente claro, vou seguir em breve com algo que deve explicar um pouco melhor as intenções.

@iopq Além da nota de @varkor sobre as mudanças recentes, eu também gostaria de acrescentar que para outros mecanismos de busca, é sempre possível que a documentação oficial e tal explicitamente usem a palavra literal "sublinhado" em conjunto com type modo que se torne pesquisável.

Você ainda não obterá bons resultados com _ em sua consulta, por qualquer motivo. Se você pesquisar sublinhado, obterá coisas com a palavra sublinhado nelas. Se você pesquisar _ obterá tudo o que tem sublinhado, então nem sei se é relevante

@Centril

Escolher let (que não é referencialmente transparente dentro de fn) é uma escolha arbitrária que não acho particularmente intuitiva. Acho que a visão intuitiva dos aliases de tipo é que eles são referencialmente transparentes (mesmo que essa palavra não seja usada) porque são aliases.

Desculpe, ainda não consigo entender isso porque minha intuição está completamente ao contrário.

Por exemplo, se temos type Foo = Bar , minha intuição diz:
"Estamos declarando Foo , que se torna o mesmo tipo que Bar ."

Então, se escrevermos type Foo = impl Bar , minha intuição diz:
"Estamos declarando Foo , que se torna um tipo que implementa Bar ."

Se Foo for apenas um alias textual para impl Bar , isso não seria muito intuitivo para mim. Eu gosto de pensar nisso como aliases textuais versus semânticos .

Então, se Foo pode ser substituído por impl Bar qualquer lugar que apareça, isso é um alias textual , para mim mais uma reminiscência de macros e metaprogramação. Mas se Foo recebeu um significado no momento da declaração e pode ser usado em vários lugares com esse significado original (não um significado contextual!), isso é um alias semântico .

Além disso, não consigo entender a motivação por trás dos tipos existenciais contextuais de qualquer maneira. Eles seriam úteis, considerando que os aliases de traço podem alcançar exatamente a mesma coisa?

Talvez eu ache a transparência referencial pouco intuitiva por causa do meu histórico não Haskell, quem sabe... :) Mas em qualquer caso, definitivamente não é o tipo de comportamento que eu esperaria em Rust.

@Nemo157 @stjepang

Se Foo fosse realmente um alias de tipo para um tipo existencial

(grifo meu). Eu li que 'an' como 'um específico', o que significa que f e g não suportariam diferentes tipos de retorno concretos, pois se referem ao mesmo tipo existencial.

Este é um uso indevido do termo "tipo existencial", ou pelo menos uma forma que está em desacordo com a postagem de type Foo = impl Bar pode parecer fazer Foo um alias para o tipo ∃ T. T: Trait - e se você substituir ∃ T. T: Trait todos os lugares que usar Foo , mesmo não -textualmente , você pode obter um tipo concreto diferente em cada posição.

O escopo desse quantificador ∃ T (expresso em seu exemplo como existential type _0 ) é o que está em questão. É apertado assim no APIT - o chamador pode passar qualquer valor que satisfaça ∃ T. T: Trait . Mas não é em RPIT, e não em RFC 2071 de existential type declarações, e não no seu Dessacarificação exemplo- lá, o quantificador é mais longe, para o todo-função ou nível de módulo inteiro, e você lida com a mesmos T todos os lugares.

Assim, a ambiguidade - já temos impl Trait colocando seu quantificador em lugares diferentes dependendo de sua posição, então qual devemos esperar para type T = impl Trait ? Algumas pesquisas informais, bem como algumas realizações pós-fato por participantes do segmento RFC 2071, provam que não está claro de uma forma ou de outra.

É por isso que queremos nos afastar da interpretação de impl Trait como qualquer coisa a ver com existenciais e, em vez disso, descrever sua semântica em termos de inferência de tipos. type T = _ não tem o mesmo tipo de ambiguidade - ainda existe o nível de superfície "não é possível copiar e colar o _ no lugar de T ", mas não há mais "o único tipo que T é um alias pode significar vários tipos concretos." (O comportamento opaco/não unificado é o que @varkor está falando sobre acompanhar.)

transparência referencial

Só porque um alias de tipo atualmente é compatível com transparência referencial, não significa que as pessoas esperam que o recurso o siga.

Como exemplo, o item const é referencial transparente (mencionado em https://github.com/rust-lang/rust/issues/34511#issuecomment-402520768), e isso realmente causou confusão para novos e antigos usuários (rust-lang-nursery/rust-clippy#1560).

Então eu acho que para um programador Rust transparência referencial não é a primeira coisa que eles pensariam.

@stjepang @kennytm Não estou dizendo que todos esperam que os aliases de tipo com type Foo = impl Trait; ajam de maneira referencialmente transparente. Mas acho que uma quantidade não trivial de usuários, como evidenciado por confusões neste tópico e em outros lugares (a que @rpjohnst está se referindo ...). Este é um problema, mas talvez não insuperável. É algo a ter em mente à medida que avançamos.

Meu pensamento atual sobre o que deve ser feito nesse assunto foi alinhado com @varkor e @rpjohnst.

re: transparência referencial

type Foo<T> = (T, T);

type Bar = Foo<impl Copy>;   // not equivalent to (impl Copy, impl Copy)

ou seja, mesmo a geração de novos tipos em cada instância não é referencialmente transparente no contexto de aliases de tipo genérico.

@centril Eu levanto minha mão quando se trata de esperar transparência referencial para Foo em type Foo = impl Bar; . Com type Foo: Bar = _; no entanto, eu não esperaria transparência referencial.

Também é possível que possamos estender a posição de retorno impl Trait para suportar vários tipos, sem nenhum tipo de mecanismo semelhante a enum impl Trait , monomorfizando (partes de) o chamador . Isso fortalece a interpretação " impl Trait é sempre existencial", aproxima-a de dyn Trait e sugere uma sintaxe abstract type que não usa impl Trait em absoluto.

Eu escrevi isso em internos aqui: https://internals.rust-lang.org/t/extending-impl-trait-to-allow-multiple-return-types/7921

Apenas uma nota para quando estabilizarmos os novos tipos existenciais - "existencial" sempre foi destinado a ser uma palavra-chave temporária (de acordo com a RFC) e (IMO) é terrível. Devemos pensar em algo melhor antes de estabilizar.

A conversa sobre tipos “existenciais” não parece estar esclarecendo as coisas. Eu diria que impl Trait significa um tipo específico e inferido que implementa o Trait. Descrito dessa forma, type Foo = impl Bar é claramente um tipo específico, sempre o mesmo - e essa também é a única interpretação que é realmente útil: então pode ser usado em outros contextos além daquele do qual foi inferido, como em estruturas.

Nesse sentido, faria sentido também escrever impl Trait como _ : Trait .

@rpjohnst ,

Também é possível estender a posição de retorno impl Trait para suportar vários tipos

Isso tornaria estritamente menos útil IMO. O ponto de aliases para tipos impl é que uma função pode ser definida como retornando impl Foo , mas o tipo específico ainda é propagado pelo programa em outras estruturas e outras coisas. Isso funcionaria se o compilador gerasse implicitamente enum , mas não com monomorfização.

@jan-hudec Essas ideias surgiram em discussão no Discord, e existem alguns problemas, principalmente baseados no fato de que a interpretação atual da posição de retorno e da posição do argumento impl Trait são inconsistentes.

Fazer impl Trait representar um tipo inferido específico é uma boa opção, mas para corrigir essa inconsistência, deve ser um tipo de inferência de impl Trait . Este é provavelmente o caminho mais simples, mas não é tão simples quanto você diz.

Por exemplo, uma vez que impl Trait significa "use este novo tipo de inferência para encontrar um tipo polimórfico possível que implemente Trait ," type Foo = impl Bar começa a implicar coisas sobre módulos. As regras RFC 2071 sobre como inferir um abstract type dizem que todos os usos devem inferir independentemente o mesmo tipo, mas essa inferência polimórfica pelo menos implicaria que mais é possível. E se alguma vez tivéssemos módulos parametrizados (mesmo apenas ao longo da vida, uma ideia muito mais plausível), haveria perguntas em torno dessa interação.

Há também o fato de que algumas pessoas sempre interpretarão a sintaxe type Foo = impl Bar como um alias para um existencial, independentemente de entenderem a palavra "existencial" e independentemente de como a ensinamos. Portanto, escolher uma sintaxe alternativa, mesmo que funcione com a interpretação baseada em inferência, provavelmente ainda é uma boa ideia.

Além disso, embora a sintaxe _: Trait seja realmente o que inspirou a discussão em torno da interpretação baseada em inferência em primeiro lugar, ela não faz o que queremos. Primeiro, a inferência implícita em _ não é polimórfica, então essa é uma péssima analogia com o resto da linguagem. Em segundo lugar, _ implica que o tipo real é visível em outro lugar, enquanto impl Trait é projetado especificamente para ocultar o tipo real.

Finalmente, a razão pela qual escrevi essa proposta de monomorfização foi do ângulo de encontrar outra maneira de unificar o significado de argumento e posição de retorno impl Trait . E embora sim, isso significa que -> impl Trait não garante mais um único tipo de concreto, atualmente não temos uma maneira de tirar proveito disso de qualquer maneira. E as soluções propostas são todos clichê adicional workarounds- irritante abstract type truques, typeof , etc. todos Forçando que quer contar com o comportamento-tipo único de também citar que tipo único via o abstract type sintaxe

Essas ideias surgiram em discussão no Discord, e existem algumas questões, principalmente baseadas no fato de que a interpretação atual da posição de retorno e da posição do argumento impl Trait são inconsistentes.

Pessoalmente, não acho que essa inconsistência seja um problema na prática. O escopo no qual os tipos concretos são determinados para a posição do argumento versus a posição de retorno versus a posição do tipo parece funcionar de maneira bastante intuitiva.

Eu tenho uma função onde o chamador decide seu tipo de retorno. Claro, não posso usar impl Trait lá. Não é tão intuitivo quanto você sugere até que você entenda a diferença.

Pessoalmente, não acho que essa inconsistência seja um problema na prática.

De fato. O que isso me sugere não é que devemos ignorar a inconsistência, mas que devemos reexplicar o design para que seja consistente (por exemplo, explicando-o como inferência de tipo polimórfico). Dessa forma, extensões futuras (RFC 2071, etc.) podem ser verificadas em relação à nova e consistente interpretação para evitar que as coisas se tornem confusas.

@rpjohnst

Forçar todos que desejam confiar no comportamento de tipo único a também nomear esse tipo único por meio da sintaxe de tipo abstrato (seja qual for) é sem dúvida um benefício geral.

Para alguns casos eu concordo com esse sentimento, mas não funciona com closures ou geradores, e não é ergonômico para muitos casos em que você não se importa com o tipo e tudo o que importa é que ele implemente um determinado traço , por exemplo, com combinadores de iteradores.

@mikeyhew Você me entendeu mal - funciona bem para closures ou outros tipos inomináveis, porque estou falando sobre inventar um nome via sintaxe RFC 2071 abstract type . Você tem que inventar um nome independentemente de usar o tipo único em qualquer outro lugar.

@rpjohnst oh entendi, obrigado por esclarecer

Esperando por let x: impl Trait ansiosamente.

Como outro voto para let x: impl Trait isso simplificará alguns dos exemplos de futures , aqui está um exemplo de exemplo , atualmente ele está usando uma função apenas para obter a capacidade de usar impl Trait :

fn make_sink_async() -> impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> { // ... }

em vez disso, isso pode ser escrito como uma ligação let normal:

let future_sink: impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> = // ...;

Eu posso orientar alguém através da implementação de let x: impl Trait se desejado. Não é impossivelmente difícil de fazer, mas definitivamente também não é fácil. Um ponto de entrada:

Da mesma forma que visitamos o tipo de retorno impl Trait em https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 precisamos visitar o tipo de locals em https ://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 e certifique-se de que seus itens existenciais recém-gerados sejam retornados junto com o local.

Então, ao visitar o tipo de local, certifique-se de definir ExistentialContext para Return para realmente habilitá-lo.

Isso já deve nos levar muito longe. Não tenho certeza se todo o caminho, não é 100% como o recurso de impl de posição de retorno, mas principalmente deve se comportar como ele.

@rpjohnst ,

Essas ideias surgiram em discussão no Discord, e existem algumas questões, principalmente baseadas no fato de que a interpretação atual de posição de retorno e posição de argumento imp Trait são inconsistentes.

Nos leva de volta aos escopos sobre os quais você falou em seu artigo. E eu acho que eles realmente correspondem ao “parêntese” de fechamento: para a posição do argumento é a lista de argumentos, para a posição de retorno é a função – e para o alias seria o escopo no qual o alias é definido.

Abri uma RFC propondo uma resolução para a sintaxe concreta existential type , com base na discussão neste tópico, na RFC original e nas discussões síncronas: https://github.com/rust-lang/rfcs/pull /2515.

A implementação de tipo existencial atual não pode ser usada para representar todas as definições de impl Trait posição de retorno atual, pois impl Trait captura todos os argumentos de tipo genérico, mesmo que não sejam usados, deve ser possível fazer o mesmo com existential type , mas você recebe avisos de parâmetros de tipo não utilizados: (playground)

fn foo<T>(_: T) -> impl ::std::fmt::Display {
    5
}

existential type Bar<T>: ::std::fmt::Display;
fn bar<T>(_: T) -> Bar<T> {
    5
}

Isso pode ser importante porque os parâmetros de tipo podem ter tempos de vida internos que restringem o tempo de vida do impl Trait retornado mesmo que o valor em si não seja usado, remova o <T> de Bar no playground acima para ver que a chamada para foo falha, mas bar funciona.

A implementação do tipo existencial atual não pode ser usada para representar todas as definições de Trait impl de posição de retorno atual

você pode, é muito inconveniente. Você pode retornar um novo tipo com um campo PhantomData + campo de dados real e implementar a característica como encaminhamento para o campo de dados real

@oli-obk Obrigado pelo conselho adicional. Com seus conselhos anteriores e alguns de @cramertj , eu provavelmente poderia tentar em breve.

@fasihrana @Nemo157 Veja acima. Talvez em algumas semanas! :-)

Alguém pode esclarecer que o comportamento de existential type não capturar parâmetros de tipo implicitamente (que @Nemo157 mencionou) é intencional e permanecerá como está? Eu gosto porque resolve #42940

Eu implementei dessa maneira muito de propósito

@Arnavion Sim, isso é intencional e corresponde à maneira como outras declarações de itens (por exemplo, funções aninhadas) funcionam no Rust.

A interação entre existential_type e never_type já foi discutida?

Talvez ! deva ser capaz de preencher qualquer tipo existencial independente dos traços envolvidos.

existential type Mystery : TraitThatIsHardToEvenStartImplementing;

fn hack_to_make_it_compile() -> Mystery { unimplemented!() }

Ou deve haver algum tipo especial intocável servindo como nível de tipo unimplemented!() capaz de satisfazer automaticamente qualquer tipo existencial?

@vi Acho que isso se enquadraria no geral "nunca o tipo deve implementar todas as características sem métodos não próprios não padrão ou tipos associados". Eu não sei onde isso seria rastreado, no entanto.

Existe um plano para estender o suporte aos tipos de retorno do método trait em breve?

existential type já funciona para métodos de traço. Wrt impl Trait , isso é coberto por um RFC?

@alexreg Acredito que isso exija que os GATs possam desugar para um tipo associado anônimo quando você tem algo como fn foo<T>(..) -> impl Bar<T> (torna-se aproximadamente -> Self::AnonBar0<T> ).

@Centril você queria fazer <T> em impl Bar lá? O comportamento de captura de tipo implícito de impl Trait significa que você obtém a mesma necessidade de GATs mesmo com algo como fn foo<T>(self, t: T) -> impl Bar; .

@ Nemo157 não, desculpe, eu não fiz. Mas seu exemplo ilustra o problema ainda melhor. Obrigada :)

@alexreg Eu acredito que exige que os GATs possam desugar para um tipo associado anônimo quando você tem algo como fn foo(..) -> impl Bar(torna-se aproximadamente -> Self::AnonBar0).

Ah, eu vejo. Para ser honesto, não parece estritamente necessário, mas certamente é uma maneira de implementá-lo. A falta de movimento nos GATs é um pouco preocupante para mim... não ouço nada há muito tempo.

Triagem: https://github.com/rust-lang/rust/pull/53542 foi mesclado, então as caixas de seleção para {let,const,static} foo: impl Trait podem ser marcadas, eu acho.

Será que algum dia conseguirei escrever:

trait Foo {
    fn GetABar() -> impl Bar;
}

??

Provavelmente não. Mas há planos em andamento para preparar tudo para que possamos obter

trait Foo {
    type Assoc: Bar;
    fn get_a_bar() -> Assoc;
}

impl Foo for SomeType {
    fn get_a_bar() -> impl Bar {
        SomeThingImplingBar
    }
}

Você pode experimentar esse recurso todas as noites na forma de

impl Foo for SomeType {
    existential type Assoc;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBar
    }
}

Um bom começo para obter mais informações sobre isso é https://github.com/rust-lang/rfcs/pull/2071 (e tudo vinculado a ele)

@oli-obk em rustc 1.32.0-nightly (00e03ee57 2018-11-22) , preciso também dar os limites de traço para existential type para trabalhar em um bloco impl como esse. Isso é esperado?

@jonhoo poder especificar as características é útil porque você pode fornecer mais do que apenas as características necessárias

impl Foo for SomeDebuggableType {
    existential type Assoc: Bar + Debug;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBarAndDebug
    }
}

fn use_debuggable_foo<F>(f: F) where F: Foo, F::Assoc: Debug {
    println!("bar is: {:?}", f.get_a_bar())
}

As características necessárias podem ser adicionadas implicitamente a um tipo associado existencial, então você só precisa de limites lá ao estendê-los, mas pessoalmente eu prefiro a documentação local de ter que colocá-los na implementação.

@ Nemo157 Ah, desculpe, o que eu quis dizer é que atualmente você _deve_ ter limites lá. Ou seja, isso não será compilado:

impl A for B {
    existential type Assoc;
    // ...
}

considerando que isso irá:

impl A for B {
    existential type Assoc: Debug;
    // ...
}

Ah, então mesmo no caso em que um traço não requer limites do tipo associado, você ainda deve dar um limite ao tipo existencial (que pode estar vazio) ( playground ):

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Isso parece um caso extremo para mim, ter um tipo existencial sem limites significa que ele fornece _no_ operações para usuários (além de autotraços), então para que ele poderia ser usado?

Também é importante notar que não há como fazer a mesma coisa com -> impl Trait , -> impl () é um erro de sintaxe e -> impl por si só dá error: at least one trait must be specified ; se a sintaxe do tipo existencial se tornar type Assoc = impl Debug; ou semelhante, parece que não haveria como especificar o tipo associado sem pelo menos um limite de traço.

@ Nemo157 sim, eu só percebi porque tentei literalmente o código que você sugeriu acima e não funcionou: p Eu meio que assumi que isso inferiria os limites do traço. Por exemplo:

trait Foo {
    type Assoc: Future<Output = u32>;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc;
}

Parecia razoável não ter que especificar Future<Output = u32> uma segunda vez, mas isso não funciona. Eu suponho que existential type Assoc: ; (que também parece uma sintaxe super estranha) também não fará essa inferência?

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Isso parece um caso extremo para mim, ter um tipo existencial sem limites significa que ele fornece _no_ operações para usuários (além de autotraços), então para que ele poderia ser usado?

Eles não poderiam ser usados ​​para consumo na implementação da mesma característica? Algo assim:

trait Foo {
    type Assoc;
    fn create_constructor() -> Self::Assoc;
    fn consume(marker: Self::Assoc) -> Self;
    fn consume_box(marker: Self::Assoc) -> Box<Foo>;
}

É um pouco artificial, mas pode ser útil - eu poderia imaginar uma situação em que alguma parte preliminar precisa ser construída antes da estrutura real por motivos de vida útil. Ou pode ser algo como:

trait MarkupSystem {
    type Cache;
    fn create_cache() -> Cache;
    fn translate(cache: &mut Self::Cache, input: &str) -> String;
}

Em ambos os casos existential type Assoc; seria útil.

Qual é a maneira correta de definir tipos associados para impl Trait?

Por exemplo, se eu tiver um traço Action e quiser garantir que a implementação do tipo associado ao traço seja enviável, posso fazer algo assim:

pub trait Action {
    type Result;
    fn call(&self) -> Self::Result;
}

impl MyStruct {
    pub fn new(name: String) -> impl Action 
    where 
        Return::Result: Send //This Return should be the `impl Action`
    {
        ActionImplementation::new()
    }
}

É algo que isso não é possível atualmente?

@acycliczebra Eu acho que a sintaxe para isso é -> impl Action<Result = impl Send> - esta é a mesma sintaxe que, por exemplo, -> impl Iterator<Item = u32> apenas usando outro tipo impl Trait anônimo.

Houve alguma discussão sobre estender a sintaxe impl Trait para coisas como campos de estrutura? Por exemplo, se estou implementando um wrapper em torno de um tipo de iterador específico para minha interface pública:

struct Iter<'a> {
    inner: std::collections::hash_map::Iter<'a, i32, i32>,
}

Seria útil naquelas situações em que eu realmente não me importo com o tipo real, desde que satisfaça certos limites de características. Este exemplo é simples, mas encontrei situações no passado em que estou escrevendo tipos muito longos com vários parâmetros de tipo aninhados, e é realmente desnecessário porque realmente não me importo com nada, exceto que isso é um ExactSizeIterator .

No entanto IIRC, não acho que haja uma maneira de especificar vários limites com impl Trait no momento, então eu perderia algumas coisas úteis como Clone .

@AGausmann A última discussão sobre o assunto está em https://github.com/rust-lang/rfcs/pull/2515. Isso permitiria que você dissesse type Foo = impl Bar; struct Baz { field: Foo } ... . Acho que podemos considerar field: impl Trait como açúcar para isso depois de estabilizar type Foo = impl Bar; . Parece uma extensão de conveniência razoável para macros.

@Central ,

Acho que podemos querer considerar field: impl Trait como açúcar

Eu não acho que isso seria razoável. Um campo struct ainda precisa ter um tipo concreto, então você precisa informar ao compilador o retorno de qual função ele está vinculado. Ele poderia inferir, mas se você tiver várias funções, não seria tão fácil descobrir qual é - e a política usual do Rust é ser explícita nesses casos.

Poderia inferir, mas se você tiver várias funções, não seria tão fácil encontrar qual é

Você aumentaria o requisito de definição de usos para o tipo pai. Seria então todas essas funções no mesmo módulo que retornam o tipo pai. Não me parece tão difícil de encontrar. No entanto, acho que queremos resolver a história em type Foo = impl Bar; antes de avançar com as extensões.

Acho que encontrei um bug na implementação atual de existential type .


Código

trait Collection {
    type Element;
}
impl<T> Collection for Vec<T> {
    type Element = T;
}

existential type Existential<T>: Collection<Element = T>;

fn return_existential<I>(iter: I) -> Existential<I::Item>
where
    I: IntoIterator,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}


Erro

error: type parameter `I` is part of concrete type but not used in parameter list for existential type
  --> src/lib.rs:16:1
   |
16 | / {
17 | |     let item = iter.into_iter().next().unwrap();
18 | |     vec![item]
19 | | }
   | |_^

error: defining existential type use does not fully define existential type
  --> src/lib.rs:12:1
   |
12 | / fn return_existential<I>(iter: I) -> Existential<I::Item>
13 | | where
14 | |     I: IntoIterator,
15 | |     I::Item: Collection,
...  |
18 | |     vec![item]
19 | | }
   | |_^

error: could not find defining uses
  --> src/lib.rs:10:1
   |
10 | existential type Existential<T>: Collection<Element = T>;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Parque infantil

Você também pode encontrar isso no stackoverflow .

Não tenho 100% de certeza de que podemos oferecer suporte a este caso pronto para uso, mas o que você pode fazer é reescrever a função para ter dois parâmetros genéricos:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b4e53972e35af8fb40ffa9a735c6f6b1

fn return_existential<I, J>(iter: I) -> Existential<J>
where
    I: IntoIterator<Item = J>,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Obrigado!
Sim, isso é o que eu fiz conforme postado no post do stackoverflow:

fn return_existential<I, T>(iter: I) -> Existential<T>
where
    I: IntoIterator<Item = T>,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Existem planos para que impl Trait esteja disponível dentro de um contexto de traço?
Não apenas como tipo associado, mas também como valor de retorno em métodos.

impl trait in traits é um recurso separado daqueles que estão sendo rastreados aqui e atualmente não possui um RFC. Há um histórico bastante longo de designs neste espaço, e iterações adicionais estão sendo adiadas até que a implementação de 2071 (tipo existencial) seja estabilizada, que está bloqueada em problemas de implementação, bem como sintaxe não resolvida (que tem um RFC separado).

@cramertj A sintaxe está quase resolvida. Eu acredito que o principal bloqueador é o GAT agora.

@alexreg : https://github.com/rust-lang/rfcs/pull/2515 ainda está esperando em @withoutboats.

@varkor Sim, estou apenas sendo otimista de que eles verão a luz com esse RFC em breve. ;-)

Será possível algo como o seguinte?

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{
    let mut s = MyStruct {};
    cb(&mut s)
}

Você pode fazer isso agora, embora apenas com uma função hint para especificar o tipo concreto de Interface

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{

    fn hint(x: &mut MyStruct) -> &mut Interface { x }

    let mut s = MyStruct {};
    cb(hint(&mut s))
}

Como você escreveria se o retorno de chamada pudesse escolher seu tipo de argumento? Na verdade, nvm, acho que você poderia resolver isso por meio de um genérico normal.

@CryZe O que você está procurando não está relacionado a impl Trait . Veja https://github.com/rust-lang/rfcs/issues/2413 para tudo o que sei sobre isso.

Seria potencialmente algo assim:

trait MyTrait {}

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: for<I: Interface> FnOnce(&mut I) -> U
{
    let mut s = MyStruct {};
    cb(hint(&mut s))
}

@KrishnaSannasi Ah, interessante. Obrigado!

Isso é suposto funcionar?

#![feature(existential_type)]

trait MyTrait {
    type AssocType: Send;
    fn ret(&self) -> Self::AssocType;
}

impl MyTrait for () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

impl<'a> MyTrait for &'a () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

trait MyLifetimeTrait<'a> {
    type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType;
}

impl<'a> MyLifetimeTrait<'a> for &'a () {
    existential type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType {
        *self
    }
}

Temos que manter a palavra-chave existential no idioma para o recurso existential_type ?

@jethrogb Sim. O fato de que atualmente não é um bug.

@cramertj Tudo bem. Devo registrar um problema separado para isso ou meu post aqui é suficiente?

Registrar um problema seria ótimo, obrigado! :)

Temos que manter a palavra-chave existential no idioma para o recurso existential_type ?

Eu acho que a intenção é depreciar isso imediatamente quando o recurso type-alias-impl-trait for implementado (ou seja, colocar um lint) e, eventualmente, removê-lo da sintaxe.

Mas alguém pode esclarecer.

Fechando isso em favor de uma meta-edição que rastreia impl Trait mais geral: https://github.com/rust-lang/rust/issues/63066

nem um único bom exemplo em qualquer lugar sobre como usar impl Trait, muito triste

Esta página foi útil?
0 / 5 - 0 avaliações