Como posso implementar `De <algum tipo associado de característica>?`

Aug 28 2020

Estou escrevendo uma nova caixa e quero que seja utilizável com qualquer implementação de uma característica (definida em outra caixa). O traço é mais ou menos assim:

pub trait Trait {
   type Error;
   ...
}

Eu tenho meu próprio Errortipo, mas às vezes quero apenas encaminhar o erro subjacente sem modificações. Meu instinto é definir um tipo como este:

pub enum Error<T: Trait> {
    TraitError(T::Error),
    ...
}

Isso é semelhante ao padrão encorajado por thiserror e parece ser idiomático. Funciona bem, mas também quero usar ?na minha implementação, então preciso implementar From:

impl<T: Trait> From<T::Error> for Error<T> {
    fn from(e: T::Error) -> Self { Self::TraitError(e) }
}

Isso falha, porque entra em conflito com impl<T> core::convert::From<T> for T. Acho que entendo por que - algum outro implementador de Traitpoderia definir de type Error = my_crate::Errorforma que ambos implse aplicassem - mas de que outra forma posso conseguir uma semântica semelhante?

Eu olhei para alguns outros engradados e eles parecem lidar com isso tornando seu Error(ou equivalente) genérico sobre o tipo de erro em si, em vez da implementação de característica. Isso funciona, é claro, mas:

  • até que tenhamos tipos inerentes associados , é muito mais detalhado. TNa verdade, meu implementa vários traços, cada um com seus próprios Errortipos, então agora tenho que retornar tipos como Result<..., Error<<T as TraitA>::Error, <T as TraitB>::Error>>etc;
  • é indiscutivelmente menos expressivo (porque o relacionamento com Traitfoi perdido).

Tornar meu Errorgenérico em vez de tipos individuais é a melhor opção (mais idiomática) hoje?

Respostas

2 FilipeRodrigues Aug 28 2020 at 05:53

Em vez de implementar Frompara seu Errorenum, considere usar Result::map_errem combinação com ?para especificar qual variante retornar. Isso funciona até mesmo para enums genéricos sobre um tipo usando tipos associados, como:

trait TraitA {
  type Error;
  fn do_stuff(&self) -> Result<(), Self::Error>;
}

trait TraitB {
  type Error;
  fn do_other_stuff(&self) -> Result<(), Self::Error>;
}

enum Error<T: TraitA + TraitB> {
  DoStuff(<T as TraitA>::Error),
  DoOtherStuff(<T as TraitB>::Error),
}

fn my_function<T: TraitA + TraitB>(t: T) -> Result<(), Error<T>> {
  t.do_stuff().map_err(Error::DoStuff)?;
  t.do_other_stuff().map_err(Error::DoOtherStuff)?;
  Ok(())
}

No parquinho

Aqui, os bits importantes são aqueles que Errornão têm Fromimplementações (exceto as gerais) e que a variante é especificada usando map_err. Isso funciona como Error::DoStuffpode ser interpretado como um fn(<T as TraitA>::Error) -> Errorquando passado para map_err. O mesmo acontece com Error::DoOtherStuff.

Essa abordagem é escalonável com quantas variantes Errortem e sejam ou não do mesmo tipo. Também pode ficar mais claro para quem está lendo a função, pois eles podem descobrir que um determinado erro vem de um determinado local sem a necessidade de verificar as Fromimplementações e de onde o tipo que está sendo convertido aparece na função.