1

I have a little bit of experience with Go, that I have been trying to use as a reference point to wrap my mind around Rust via a cards game I wrote in Go that I would like to now write in Rust.

I know how I would approach this project with a language like Python or Ruby and of course Go, but I am unclear how I would approach it with Rust.

With object oriented programming for this cards game. We might decide to make something like a Deck class. A deck class is a sort of blueprint that has some number of methods and properties attached to it that describe what an instance or a copy of a deck looks like. So we can use that deck class to then create a deck instance. A deck instance might have a property called cards, which could be an array of strings. We could then make use of functions attached to the deck instance such as print, shuffle and saveToFile to manipulate that cards property attached to the deck instance.

Obviously in the Go world things are significantly different.

So as you might already know, with Go we have some of these very basic data types available to us like strings, integers and floats, but we also have arrays and slices as well, which are technically also data types inside of Go. In order to bring about this idea of a deck inside of our Go program, we can define a new type inside of Go of deck type and a deck type is essentially going to be a slice of strings and so in that slice of strings we will have our playing cards which will be a string.

Now to attach some customized functionality or some functions to work with this custom deck type we put together, we create functions called functions with receiver.

Anyway, we are going to take an existing type in the language and we are going to extend its functionality, we are going to add more properties to it. Add some functions that make use of this new type that we create. This is a very common pattern that we will see in a lot of Go programs.

So my question is, how would this cards game look like in Rust? It's design pattern that is?

So far in Rust I have something like this:

fn main() {
    let mut cards = vec!["Ace of Diamonds", new_card()];
    let additional_card: [&str; 1] = ["Six of Spades"];
    cards.extend(additional_card);
    // or for a single card
    // cards.push("A single card");

    for i in cards {
        println!("{i} cards");
    }
}

fn new_card() -> &'static str {
    "Five of Diamonds"
}

which is somewhat similar syntactically to its equivalent in Go, but again I am looking for some understanding on what the design pattern would be for Rust for such a game.

I will try to be more specific, in Go, we may want to create a new type of deck which is a slice of strings. Can a similar approach be expected in Rust? Is the concept of a slice of strings similar?

2
  • Upvoted, but this will be closed as too broad anyway
    – Basilevs
    Nov 29, 2022 at 16:38
  • @Basilevs, I was hoping this would be the place to talk about software design patterns using this card game as a specific example.
    – Daniel
    Nov 29, 2022 at 16:51

1 Answer 1

5

Rust is broadly similar to how you would approach Golang, with the caveat that there are some quite relevant differences in the type system. For example:

  • With Golang, you can create new methods for existing types. With Rust, you can only add new methods on types or traits that you defined in your crate, but you can then implement your traits for existing types.
  • With Golang, you can define interfaces in an ad-hoc manner. With Rust, you must name and explicitly implement traits up front. With Rust, you can't cast or convert between dyn-traits.
  • Golang allows pseudo-inheritance via struct embedding, Rust does not (Deref should only be implemented for smart pointers).
  • Golang provides garbage collection. Rust has the ownership/borrowing system, which is far more restrictive. In particular, it is often easier if you work with owned data such as String instead of borrowed views such as &str.

All in all, Rust tends to guide you more towards defining your own types instead of reusing existing types. For example, for modelling a Deck, you might use a slice of strings, and then add suitable methods onto slices. In Rust, you would probably define a new struct, containing a vec of cards:

#[derive(Debug, Clone)]
struct Card(String);

#[derive(Debug, Clone)]
struct Deck {
  cards: Vec<Card>,
}

Then, we can implement needed methods for your types. For example, we might implement the Display trait so that they can be printed out easily:

impl std::fmt::Display for Card {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result<()> {
    let Self(name) = self;
    write!(f, "{name}")
  }
}

impl std::fmt::Display for Deck {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result<()> {
    let len = self.cards.len();
    writeln!(f, "Deck of {len} cards:")?;
    for card in self.cards {
      writeln!(f, "- {card}")?;
    }
    Ok(())
  }
}

fn main() {
  let deck = ...;
  println!("{deck}");
}

We might implement a shuffling method on the Deck:

// using the "rand" crate
use rand::prelude::*;

impl Deck {
  fn shuffle(&mut self, rng: &mut dyn Rng) {
    self.cards.shuffle(rng);
  }
}

fn main() {
  let mut rng = thread_rng();
  let mut deck = ...;
  deck.shuffle(&mut rng);
  ...
}

For saving/loading to/from files, you'd probably avoid writing your own methods and instead use Serde. After importing Serde we can add Serialize, Deserialize to the derive-attribute of our structs. Then, we can save/load using a particular format, e.g. serde_json:

// using the "serde" and "serde_json" crates

fn main() {
  let file = std::fs::File::open("deck.json")
    .expect("input file `deck.json` must exist");
  let deck = serde_json::from_reader(file)
   .expect("input file `deck.json` must contain valid Deck data");

  ... // do something with the deck

  let file = std::fs::File::create("deck.json")
    .unwrap();
  serde_json::to_writer(file, &deck)
    .expect("could not serialize Deck to JSON file");
}

At some point, you might decide that stringly typed cards are not good enough, and might start defining enums:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Card {
  suite: CardSuit,
  rank: CardRank,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CardSuit { Diamonds, Clubs, Hearts, Spades }

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CardRank { Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King }

However, this would require more effort for implementing the Display trait for cards, unless we use crates like strum.

Such enums might make it easier to implement game logic, for example:

fn is_pair(a: Card, b: Card) -> bool {
  (a.suit != b.suit) && (a.rank == b.rank)
}
2
  • why do you use a generic here: fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result<()>?
    – Daniel
    Dec 12, 2022 at 3:18
  • @Daniel that function isn't generic. It's just how the Display trait is defined.
    – amon
    Dec 12, 2022 at 8:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.