Playing With Gold – Units and Operators in Swift


While playing around with the idea of creating a treasure generator for my tabletop gamer, I ran across Soroush Khanlou’s post on Units which seemed fun to try out.

To summarize (and butcher) his post, he wants to decrease the amount of magic numbers in code. Every code has some magic numbers, even yours. In his example, the magic number is 7200… look familiar? Perhaps as 2 * 60 * 60? Right, 2 hours in the amount of seconds. That number is annoying, but what when you want to have 15 days and 2 hours? 15 * 24 * 60 * 60. Nope, that doesn’t go over well.

Why can’t we just do something like 2 hours + 30 minutes instead? Or in my case, 25 gold + 60 silver + 18 copper?

⚔ + 👾 = 💀 + 💰💰💰

As a tabletop gamer, a lot of the loot I encounter is measured in gold. You kill a poor goblin that was just minding his own business and grab the pouch from his corpse to see what you earned.

“Show me your coin!” – Defining our unit

Gold pieces, silver pieces and copper pieces, are the usual measures of currency. We can define the unit in a Money protocol;

protocol Money: CustomStringConvertible {
    var amount:  Double { get }
    var inGold: Double { get }
    var symbol: String { get }
}

extension Money {
    var description: String {
        return "\(amount)\(symbol)"
    }
}

Great, we now have a protocol that any currency can conform to. For example, Silver;

struct Silver: Money {
    let amount: Double
    
    var inGold: Double {
        return value * 0.1
    }
    
    var symbol: String {
        return "sp"
    }
}

You can see our basic building blocks; We have a Silver struct that represents a certain amount of silver, convert it into a value inGold and gives it a currency symbol too.

We can make Copper and Gold in exactly the same way (but with inGold values 0.01 and 1.0 respectively).

And thanks to our extension giving a default implementation of description for CustomStringConvertible, we can read it properly too.

Silver(amount: 60)    // prints as '60sp'

But we don’t want to type that constantly. Instead, we can extend Int to help us out with our currency;

extension Int {
    var silver: Money {
        return Silver(amount: Double(self))
    }
    // And the same style for copper and gold...
}

So now we can do the following;

60.silver    // 60sp
25.gold      // 25gp
18.copper    // 18cp

Well, that looks easy enough. Time to trade in this loot for some items!

“I’m not counting that!” – Overriding operators

You can’t show up at the item shop with a chest full of copper without at least telling the shopkeeper how much that would be in gold!

So we override an operator – the + – to help us out here.

func + (lhs: Money, rhs: Money) -> Money {
    return Gold(amount: lhs.inGold + rhs.inGold)
}

Great! now we can add or substract Money! Since we use Gold as our default type, whenever you do calculations with money, we end up with the value in gold.

25.gold + 60.silver + 18.copper
// 31.18gp 

That makes counting the chest full of coppers much easier.

“It’s the journey that counts!” – Custom operators

But, half of the fun is finding an entire pouch full of assorted coins.

You find a purse with coins worth 31.18 gold.

That doesn’t have the same ring to it as;

You find a bulging pouch spilling over with 25 gold pieces, 60 silver pieces and some change in 18 copper pieces.

So how can we accomodate this? Let’s create a new type Pouch;

struct Pouch: CustomStringConvertible {
    let monies: [Money]
    init(monies: [Money] = []) {
        self.monies = monies
    }
}

extension Pouch {
    var description: String {
        return monies.reduce("Pouch containing:\n", combine: { (current, money) -> String in
            var result = current
            if result.characters.count > 0 {
                result += "\n"
            }
            result += "  \(money)"
            return result
        })
    }
}

With that, we can create our pouch that stores an array of money and can print this into a handy list!

Pouch(monies: [25.gold, 60.silver, 18.copper])
/*
Pouch containing:
  25gp
  60sp
  18cp
*/

But filling the pouch is a drag. Noone wants to keep writing money like an array. We want to fill our pouch! Let’s make a custom operator for that.

infix operator <<< { associativity left precedence 160 }
func <<< (lhs: Pouch, rhs: Money) -> Pouch {
    return Pouch(monies: lhs.monies + [rhs])
}

Our operator <<< takes a Pouch on its left hand side, and fill it up with the Money on the right hand side. After filling it up, you have a new Pouch with all the cash together.

So now, we can fill up our pouch like this;

Pouch() <<< 25.gold <<< 60.silver <<< 18 copper

That looks much nicer!

This was my first custom operator. I probably should’ve chosen a better one, but in my opinion the least custom operators you have, the better. But this one is for fun 😎

“I have a task for you.” – What’s next?

This was a fun little experiment in creating unit types and playing around with operators in Swift. Hopefully you’ve enjoyed the read.

One thing that I would like to improve is adding money of the same type to the Pouch. Now, it shows each added money separately, instead of combining money of the same type;

Pouch() <<< 25.gold <<< 10.gold
/*
Pouch containing:
  25gp
  10gp
*/

So instead, I would like to improve the <<< operator to check whether the Pouch already contains money of the same type and combine those too. Something like;

infix operator <<< { associativity left precedence 160 }
func <<< (lhs: Pouch, rhs: Money) -> Pouch {
    for money in monies {
        if(money.type == rhs.type) {
            money.type(money.value + rhs.value)
        }
    }
    return Pouch(monies: lhs.monies + [rhs])
}

However, that would require some kind of dynamic type checking, which I’m not sure that Swift would allow yet. So the above doesn’t work yet.

“Thank you, brave hero!” – Epilogue

You can check out my playground code here. If you enjoyed this post, or if you have any suggestions for adding combining money of the same type in the pouch, drop me tweet at @brunoscheele.

P.S. If you’re into fantasy and anime, watch Hai to Gensou no Grimgar. It’s amazing.