Come posso implementare "From <some trait's associated type>?"

Aug 28 2020

Sto scrivendo una nuova cassa e voglio che sia utilizzabile con qualsiasi implementazione di un tratto (definito in un'altra cassa). Il tratto è simile a questo:

pub trait Trait {
   type Error;
   ...
}

Ho il mio Errortipo, ma a volte voglio solo inoltrare l'errore sottostante senza modifiche. Il mio istinto è definire un tipo come questo:

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

Questo è simile al modello incoraggiato da questo errore e sembra essere idiomatico. Funziona bene, ma voglio anche usarlo ?nella mia implementazione, quindi devo implementare From:

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

Ciò fallisce, perché è in conflitto con impl<T> core::convert::From<T> for T. Penso di capire perché - qualche altro implementatore di Traitpotrebbe impostare in modo type Error = my_crate::Errortale che implsi applichino entrambe le s - ma in quale altro modo posso ottenere una semantica simile?

Ho esaminato alcune altre casse e sembrano gestirlo rendendo il loro Error(o equivalente) generico sul tipo di errore stesso, piuttosto che sull'implementazione del tratto. Funziona, ovviamente, ma:

  • finché non abbiamo tipi associati intrinseci , è molto più prolisso. In Trealtà, My implementa più tratti, ciascuno con i propri Errortipi, quindi ora dovrei restituire tipi come Result<..., Error<<T as TraitA>::Error, <T as TraitB>::Error>>ecc;
  • è probabilmente meno espressivo (perché la relazione con Traitè persa).

Rendere il mio Errorgenerico rispetto ai singoli tipi è l'opzione migliore (più idiomatica) oggi?

Risposte

2 FilipeRodrigues Aug 28 2020 at 05:53

Invece di implementare Fromper la tua Errorenumerazione, considera invece di utilizzare Result::map_errin combinazione con ?per specificare quale variante restituire. Funziona anche per enumerazioni generiche su un tipo che utilizza tipi associati, in quanto tali:

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

Sul campo di gioco

Qui i bit importanti sono che Errornon ha Fromimplementazioni (a parte quelle generali ) e che la variante è specificata usando map_err. Funziona come Error::DoStuffpuò essere interpretato come fn(<T as TraitA>::Error) -> Errorquando viene passato a map_err. Lo stesso accade con Error::DoOtherStuff.

Questo approccio è scalabile con tutte le varianti disponibili Errore indipendentemente dal fatto che siano o meno dello stesso tipo. Potrebbe anche essere più chiaro a qualcuno che sta leggendo la funzione, poiché può capire che un certo errore proviene da un certo punto senza bisogno di controllare le Fromimplementazioni e dove il tipo da convertire appare nella funzione.