简体   繁体   中英

How should library crates be designed around Rust's Orphan Rule?

My understanding of the orphan rule of interest is that:

  • For any impl of a Trait on a Type, either the Trait or the Type must be defined in the same crate as the impl .

or equivalently:

  • It is impossible to implement a trait defined in a foreign crate on a type which is also defined in a foreign crate.

So the question is, how should one design a library crate that provides a type which should implement a foreign trait, but leave it to the consumer crate to define the implementation of that trait?

For example, imagine a hypothetical library crate, cards , which provides types representing cards in a standard 52-card deck of playing cards intended for use in external crates to facilitate the development of card game programs. Such a library might provide the following type:

/// Ranks of a standard French-suited playing card
pub enum Rank {
    Ace,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine,
    Ten,
    Jack,
    Queen,
    King,
}

Since a substantial portion of popular card games involve comparing the ranks of 2 cards, it should make sense for enum Rank to implementstd::cmp::PartialOrd so that ranks can easily be compared with the < , > , <= , and >= operators. But crate cards should not define this implementation because the ranks of playing cards are not intrinsically equipped with a partial ordering relation ; such a relation is instead optionally imposed on them by the specific game within which the cards are being used and in general, the implementation varies by game. For example, some games consider Ace to be greater than all other ranks while others consider Ace to be less than all other ranks and certain games may not even compare ranks at all and therefor not need a PartialOrd implementation.

In keeping with best practice regarding separation of concerns , it seems obvious that crate cards should not implement PartialOrd , but instead leave its implementation up to the consumers of cards , allowing each consumer to define their own partial ordering relation for the ranks. However, since in a crate depending on cards , both Rank and PartialOrd are crate-foreign, this is prohibited by the Orphan rule.

What is the best way to deal with this scenario? It seems to me the only correct thing to do is to refrain from implementing PartialOrd for Rank and let the consumers make their own functions for rank comparison like:

fn blackjack_rank_compare(first: &Rank, second: &Rank) -> Option<Ordering> {...}
fn poker_rank_compare(first: &Rank, second: &Rank) -> Option<Ordering> {...}
fn war_rank_compare(first: &Rank, second: &Rank) -> Option<Ordering> {...}
// etc.

However this is inconvenient. Is there any viable workaround for this problem?

There are a lot of answers to your question.

You could for example use a type parameter for defining the ordering ( playground ):

use core::cmp::{ PartialOrd, Ordering, };
use std::marker::PhantomData;

pub enum Card {
    Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King,
}
pub use Card::{
    Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King,
};
// Evaluated card structure; default game is DefaultT
pub struct MyValuedCard<T=DefaultT>(Card,PhantomData<T>); 
impl<T> MyValuedCard<T> {
    pub fn new(card: Card) -> Self { Self(card,PhantomData) }
}
trait MyOrder { // Trait for customizing the order
    fn order(c: &Card) -> u8;
}
// PartialEq and PartialOrd implementation
impl<T> PartialEq for MyValuedCard<T> where T: MyOrder { 
    fn eq(self: &Self, &Self(ref second, _): &Self,) -> bool {
        PartialEq::eq(&T::order(&self.0), &T::order(second))
    }
}
impl<T> PartialOrd for MyValuedCard<T> where T: MyOrder {
    fn partial_cmp(self: &Self, &Self(ref second, _): &Self,) -> Option<Ordering> {
        PartialOrd::partial_cmp(&T::order(&self.0), &T::order(second))
    }
}
// Default game with its order
pub enum DefaultT {}
impl MyOrder for DefaultT {
    fn order(c: &Card) -> u8 {
        match c {
            Ace => 1, Two => 2, Three => 3, Four => 4, Five => 5, _ => 6,
        }
    }
}
// Game G1 defined by user
pub enum G1 {}
impl MyOrder for G1 {
    fn order(c: &Card) -> u8 {
        match c {
            Ace => 1, Two => 2, Three => 3, Four => 4, Five => 5, _ => 6,
        }
    }
}
// Game G2 defined by user
pub enum G2 {}
impl MyOrder for G2 {
    fn order(c: &Card) -> u8 {
        match c {
            Ace => 6, Two => 5, Three => 4, Four => 3, Five => 2, _ => 1,
        }
    }
}

fn main() {
    // Default game
    let ace: MyValuedCard = MyValuedCard::new(Ace);
    let king: MyValuedCard = MyValuedCard::new(King);
    // Game G1
    let ace_g1 = MyValuedCard::<G1>::new(Ace);
    let king_g1 = MyValuedCard::<G1>::new(King);
    // Game G2
    let ace_g2 = MyValuedCard::<G2>::new(Ace);
    let king_g2 = MyValuedCard::<G2>::new(King);
    println!("ace <= king -> {}", ace <= king);
    println!("ace_g1 <= king_g1 -> {}", ace_g1 <= king_g1);
    println!("ace_g2 <= king_g2 -> {}", ace_g2 <= king_g2);
}

which results in:

Standard Error

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.86s
     Running `target/debug/playground`

Standard Output

ace <= king -> true
ace_g1 <= king_g1 -> true
ace_g2 <= king_g2 -> false

In this case, the user of your crate is able to change the order of your cards by defining a type that implements MyOrder.

But have I well understood your question?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM