2017-05-01 6 views
3

J'ai deux traits qui sont fondamentalement équivalents, mais l'un fournit une interface de niveau inférieur à l'autre. Étant donné le trait de niveau supérieur, on peut facilement mettre en œuvre le trait de niveau inférieur. Je veux écrire une bibliothèque qui accepte une implémentation de l'un ou l'autre trait.Trait de rouille avec les versions "simples" et "avancées"

Mon cas particulier est un trait pour traverser un arbre:

// "Lower level" version of the trait 
pub trait RawState { 
    type Cost: std::cmp::Ord + std::ops::Add<Output = Self::Cost> + std::marker::Copy; 
    type CulledChildrenIterator: Iterator<Item = (Self, Self::Cost)>; 
    fn cull(&self) -> Option<Self::Cost>; 
    fn children_with_cull(&self) -> Self::CulledChildrenIterator; 
} 
// "Higher level" version of the trait 
pub trait State: RawState { 
    type ChildrenIterator: Iterator<Item = (Self, Self::Cost)>; 
    fn children(&self) -> Self::ChildrenIterator; 
} 

// Example of how RawState could be implemented using State 
fn state_children_with_cull<S: State> (s: S) 
    -> impl Iterator<Item = (S, S::Cost)> 
{ 
    s.children() 
     .filter_map(move |(state, transition_cost)| 
     state.cull().map(move |emission_cost| 
      (state, transition_cost + emission_cost) 
     ) 
    ) 
} 

Ici, trait État fournit une interface où vous définissez la fonction .children() à la liste des enfants, et la fonction .cull() potentiellement abattage un état.

Le trait RawState fournit une interface dans laquelle vous définissez une fonction .children_with_cull() à la place, qui itère à travers les enfants et les supprime dans un appel de fonction unique. Cela permet à une implémentation de RawState de ne jamais même générer des enfants dont il sait qu'ils seront éliminés.

Je souhaite autoriser la plupart des utilisateurs à implémenter uniquement le trait State et à générer automatiquement l'implémentation RawState en fonction de leur implémentation d'état. Cependant, lors de la mise en œuvre de State, certaines parties du trait font toujours partie de RawState, par ex.

#[derive(Clone, Eq, PartialEq, Hash, Debug)] 
struct DummyState {} 

impl State for DummyState { 
    type Cost = u32; 
    type ChildrenIterator = DummyIt; 
    fn emission(&self) -> Option<Self::Cost> { 
     Some(0u32) 
    } 
    fn children(&self) -> DummyIt { 
     return DummyIt {}; 
    } 
} 

Donner des erreurs, car le type "Coût" est défini dans RawState, pas dans State. En solution de contournement potentiel, de redéfinir toutes les parties pertinentes de l'intérieur RawState Etat, à savoir définir Etat

pub trait State: RawState { 
    type Cost: std::cmp::Ord + std::ops::Add<Output = Self::Cost> + std::marker::Copy; 
    type ChildrenIterator: Iterator<Item = (Self, Self::Cost)>; 
    fn cull(&self) -> Option<Self::Cost>; 
    fn children(&self) -> Self::ChildrenIterator; 
} 

Mais le compilateur se plaindra des définitions ambiguës en double. Par exemple dans l'implémentation DummyState pour State, il se plaindra que Self::Cost est ambigu, car il ne peut pas dire si vous faites référence à <Self as State>::Cost, ou <Self as RawState>::Cost.

+0

Êtes-vous sûr de vouloir deux traits, et pas seulement un trait avec une [implémentation par défaut] (https://doc.rust-lang.org/book/traits.html#default-methods) de 'children_with_cull'? – trentcl

+1

@trentcl: Les types qui préfèrent implémenter 'RawState' directement devraient encore implémenter' children', même si 'children' n'est pas utilisé directement. –

Répondre

5

Considérant que les deux RawState et State ne sont pas object-safe (parce qu'ils utilisent Self dans les types de retour), je suppose que vous ne comptez pas créer des objets trait à ces traits (à savoir pas &RawState).

Le supertrait lié State: RawState est surtout importante lorsqu'on utilise des objets trait, parce que les objets de trait ne peuvent spécifier un trait (plus un sélectionner quelques traits de la liste blanche de la bibliothèque standard qui ont pas de méthodes, comme Copy, Send et Sync). Le vtable auquel l'objet trait fait référence contient uniquement des pointeurs vers les méthodes définies dans ce trait. Mais si le trait a des limites supertrait, alors les méthodes de ces traits sont également inclus dans le vtable. Ainsi, un &State (si c'était légal) vous donnerait accès à children_with_cull. Une autre situation où le supertrait lié est important est lorsque le sous -trait fournit des implémentations par défaut pour certaines méthodes. L'implémentation par défaut peut utiliser le supertrait lié pour accéder aux méthodes d'un autre trait. Etant donné que vous ne pouvez pas utiliser les objets trait, et que vous n'avez pas les implémentations par défaut pour les méthodes dans State, je pense que vous ne devez simplement pas déclarer le supertrait lié State: RawState, car il n'ajoute rien (et en fait, provoque problèmes).

Avec cette approche, il devient nécessaire de copier les membres de RawState que nous devons implémenter State, comme vous l'avez suggéré. State serait ainsi défini comme ceci:

pub trait State: Sized { 
    type Cost: std::cmp::Ord + std::ops::Add<Output = Self::Cost> + std::marker::Copy; 
    type ChildrenIterator: Iterator<Item = (Self, Self::Cost)>; 

    fn cull(&self) -> Option<Self::Cost>; 
    fn children(&self) -> Self::ChildrenIterator; 
} 

(.. Notez que la limite State: Sized est nécessaire parce que nous utilisons Self dans ChildrenIteratorRawState également besoin de la limite RawState: Sized)

Enfin, nous pouvons fournir une couverture impl de RawState pour tous les types qui implémentent State. Avec ce impl, tout type qui implémente State mettra automatiquement en œuvre RawState.

impl<T> RawState for T 
where 
    T: State 
{ 
    type Cost = <Self as State>::Cost; 
    type CulledChildrenIterator = std::iter::Empty<(Self, Self::Cost)>; // placeholder 

    fn cull(&self) -> Option<Self::Cost> { <Self as State>::cull(self) } 
    fn children_with_cull(&self) -> Self::CulledChildrenIterator { 
     unimplemented!() 
    } 
} 

Notez la syntaxe pour désambiguïser les noms contradictoires: <Self as State>. Il est utilisé sur les deux membres que nous avons dupliqués afin que RawState se reporte à State.

+0

Très cool! Exactement ce que je cherche. Je vais devoir lire sur la sécurité des objets, mais je pense que mon code fonctionne bien parce que ma bibliothèque utilise des fonctions basées sur des modèles plutôt que des objets de caractère. –