Value Types Style Question

This is more of a style question than a technical one. I am experimenting with value types by building a simple poker card system.


I have a 'Deck' struct which consists of two UInt64 (one for the cards in the deck, and the other for the discard) and has various functions for drawing cards, discarding, shuffling, etc.... There is, of course, also a 'Card' struct with sub-enums for rank and suit.


The issue arose when I made a 'Hand' struct (which is basically just an array of Card structs + helper functions). As I was adding functions to discard a hand, I found I had a few options:


  1. discardHand(hand:Hand) on the 'Deck' - This discards the hand, but the hand struct which is passed in will still contain the cards afterwards, which feels a bit weird to me (perhaps I am just too used to reference types). Could this lead to errors because someone expects the hand to be empty afterwards?
  2. discardHand(inout hand:Hand) on the 'Deck' - This discards the hand and updates the hand struct, but is called 'discardHand(&hand)' which is surprising to me because it isn't reflected in the name (is there a naming convension to let the caller know they need to use '&' besides the compiler error?)
  3. discardHand(hand:Hand)->Hand on the 'Deck' - This discards the hand and returns a new empty hand.
  4. discard(deck deck:Deck) on the 'Hand'- This results in an empty hand, but unchanged deck... which is a non-starter because things are out of sync
  5. discard(inout deck deck:Deck) on the 'Hand'- Similar to #2
  6. Make hand an object - This would give the reference behavior I am used to, but it seems strange to have one object be a reference type when all of the others are value types
  7. Make everything an object - This would somewhat defeat the purpose of my explorations, but perhaps value types are a bad fit for this problem (though I doubt it)


From the perspective of someone who would be calling this function to discard a poker hand, which of the above would give the behavior that you most expect? Do you expect the hand that you pass to discardHand() to be empty after the operation? Is there an option which I am missing?

Replies

#6


SInce you want Hand to be mutable, it must be a reference type. If it were a mutable value type, then you end up with multiple instances (I.e. copies) with possibly different states, and which one is "the" state? Or, if you use & everywhere, you are in fact passing around references.


I don't see a problem with mixing value and reference types in a case like this.

I don't really agree with this at all. “The” state is the one stored in your Player/GameState instance, and you can mutate that directly or assign a new Hand to it, whatever is a better fit. I don't agree that reference type follows from mutability, and I don't agree that using inout with value types is really the same as passing around references.


As to the original question, using inout would be fine. Not sure I understand why you're asking for a naming convention when it says inout in the method signature, Xcode shows &parameter in code autocomplete and it even inserts the & for you automatically.

A reference type doesn't "follow" from mutability, but if you want One True State then you must always arrange to access that One True State. You can certainly arrange to do this with a value type by passing &oneTrueHand everywhere that might mutate the state, and by making sure that any non-& copy the compiler makes isn't used after the original state has been modified, but that's a lot of error-prone housekeeping that an actual reference type gives you for free.


'inout' with value types is literally passing a reference. Both cause mutations to be made to the One True State. What other distinction did you have in mind between the two?

Sure, if you want to share a reference to one state from several locations, then you want a reference type. I don't see how that followed from the original post though.


I'm not 100% sure of the guarantees here, but generally inout on value types results in a copy being made and then that copy being written back when the function returns, rather than being mutated through a reference. I think these semantics are guaranteed when you have observers like didSet on your value type, though it may be optimised to work using a reference when deemed safe.

I'd suggest a model with a Table class, which has stored properties that keep track of each of your value-type objects, and methods which manage the interactions between them.


let PlayerCount = 3

class Table
{
    var deck: Deck = Deck()
    var hands = [Hand](count: PlayerCount, repeatedValue: Hand())
  
    func discardWholeHandForPlayer(player: Int)
    {
        let discards = hands[player].discardAll()   // hands[player] automatically updates with new Hand value implicitly returned by mutating method
        deck.addDiscardedCards(discards)
    }

     /*...*/
}


struct Deck
{
    /*...*/
  
    mutating func drawCards(count: Int) -> [Card] {/*...*/}
    mutating func addDiscardedCards(cards: [Card]) {/*...*/}
}

struct Hand
{
    /*...*/
  
    mutating func addDrawnCards(cards: [Card]) {/*...*/}
    mutating func discardAll() -> [Card] {/*...*/}
}

struct Card {/*...*/}