How to know if you're being too safe or too unsafe while writing Swift code?

I often struggle with finding the balance between safe but complex looking code vs unsafe but very easy to understand code in Swift. A good example would be:

struct Account { 
    var coins: Int
}

vs.

struct Account { 
    var _coins: Int

    var coins: Int { 
        get { _coins } 
        set { _coins = abs(newValue) }
    }
}

vs.

struct Account { 
    var _coins: Int

    var coins: Int { 
        get { _coins } 
        set {
            // Outright aborting the set
            guard newValue >= .zero else { print("Negative set aborted"); return } 
            _coins = newValue
            // or going through with it but printing a warning
            if newValue < .zero else { print("Coins set to negative value") } 
            _coins = newValue
        }
    }
}

vs.

struct Account { 
    var coins: Int
    mutating func setCoins(to newValue: Int) -> Bool { 
        guard newValue >= .zero else { return false } 
        coins = newValue
        return true
    }
}

vs.

struct Account { 
    var coins: Int
    mutating func setCoins(to newValue: Int) throws { 
        guard newValue >= .zero else { throw AccountError.negativeCoins } 
        // potentially different types of errors could be thrown here
        coins = newValue
    }
}

vs.

struct Account { 
    var _coins: Int

    var coins: Int { 
        get { _coins } 
        set {
            assert(coins >= .zero)
            // or
            precondition(coins >= .zero)

            _coins = newValue 
        }
    }
}

Each one of these have soooo many different implications to the safety of my code (yes I know, welcome to coding). For example:

Option 1: Relying on not making a mistake
Pro: Definitely the easiest to read. I can simply be very careful while coding.
Con: I have to be very careful while coding.

Option 2: Correcting input silently
Pro: Still maintains the naturalness of myAccount.coins += 100
Con: My code will silently convert the negative value to a positive one and I'll never know that somewhere in my code this strange thing happened.

Option 3: Printing that something went wrong
Pro: Still has the naturalness of using setters like myAccount.coins = <whatever>
Con: This is the weirdest cuz it's an unsatisfying mix between "I'm trying to be careful" and "I don't care". Printing an error message seems like a flimsy way to deal with this.

Option 4: Using a boolean to check for success
Pro: Now I am properly notified that something went wrong and my code hasn't become crowded with any do-try-catch blocks
Con: I wouldn't know what exactly caused the error so I won't know how to handle it

Option 5: Using Error Handling
Pro: Seems like the most solid solution
Con: Makes code kinda ugly in my opinion. Also using a function-setter when Swift has it's own custom setters for properties makes me feel like I'm doing something wrong

Option 6: Using assert or precondition
Pro: This is what I tend to use as it maintains cleanliness while also raising a flag
Con: Now I have to decide between using assert or precondition which is the whole essence of my dilemma. The balance between too safe vs. too unsafe.

How do you guys go about balancing this out?

P.S. This entire crisis stemmed from this: (My Stackoverflow Question)

struct Account {
    var strikes: Int = 0
    
    private var _monetizationLevel: Int = 1
    
    var monetizationLevel: Int {
        get { _monetizationLevel }
        set {
            guard strikes == 0 else { print("Please resolve copyright strike before adjusting moentization level"); return }
            guard newValue <= _monetizationLevel else { print("Monetization level can only be increased by a server admin"); return }
            _monetizationLevel = newValue
        }
    }
}

I was reluctant to use function-setters and really wanted to preserve the cleanliness of myAccount.monetizationLevel -= 7. So if in your replies you can keep this structure in mind that'll help me a lot with my actual problem

1 Like

Since it must hold nonnegative values, why not use the type UInt instead of Int for coins to specify the constraint clearly and loudly.

struct Account { 
    var coins: UInt
}
5 Likes

hahaha very true I didn't realise that. But if coins were to be var money: Double instead how would you go about making that restriction?

Then you would apply the preconditions as you have done earlier.

Writing safe code was not meant to be easy. I am not even sure it is even possible in general. :slight_smile:

1 Like

Yes I definitely prefer preconditions. I think it's much simpler to have the it crash and fixing it instantly rather than littering my code with do-try-catch everywhere for the smallest of constraints.

However I must ask, how do you decide between an assert and a precondition. And when would you think using throwing functions would be more appropriate than preconditions?

In this case (and the money example you mention), it's probably best to use a custom type rather than a raw numeric value. That way you can encapsulate the various bits of edge case logic within the type and offer a narrow set of operations to mutate the value, allowing you to precisely control what can happen given any particular input. You could even allow for custom behaviors, like a NegativeBehavior type which allows you to represent behaviors like .assert, .zero, or .allow.

5 Likes

Well I think it depends whether the error is really a logic error or just a problem that happened while trying to do something. Unless your account can be in debt, having a negative number of coins is a logic error, like an out-of-bounds array index. Compare this to fetching the bytes of a URL, for example, where an unforeseen error can happen at runtime even if the logic of the program is sound.

2 Likes

That would probably be a bad idea.

I'd say the "safest" would be your option 5 (error handling). The others are either easy to ignore (3, 4), possible to ignore (6) or somewhat error prone (1, 2). Note that even with 5 you can still ignore the error by catching and ignoring the error.

1 Like

Just as side note about nomenclature (please skip this comment if you are only interested in the proposed problem):

As the words “safe”/“unsafe” are used on the Swift website, “unsafe” operations are meant to have an undefined behavior for some inputs, example when using "pointer arithmetic" via explicitly “unsafe” operations. So an operation leading to a number underflow is still considered a safe operation. Obviously this is not what you mean in this forums topic (if “safe” is already used differently, maybe we can talk about robust operations). I do not know if the official meaning of “safe” for Swift is somehow a misleading formulation (so we can say “Swift is safe”), leading a to a misinterpretation of what the “philosophy” of Swift is: instead of silently failing (e.g. letting numbers overflow silently as in Java and C#) and giving wrong results, the philosophy is that it is better to let the whole process fail.

Sorry for the distraction.

4 Likes

To take a different tack, when thinking about when to write super bullet proof code, I ask myself "is this a public interface for an external audience, or something where I can ensure that no one will ever send a bogus value to it, and I can just put in a testing assert to help me or my colleagues find when mistakes are being made?"

I.e. private helper functions won't be fed bad data, because you will have checked before then, for other reasons, and redundant checks are generally* pointless.

*But not always.

3 Likes

So maybe you're overthinking this a bit? This isn't really just a question of "writing safe code" as your topic states it is. Your different options actually do subtly different things (allow negative numbers or not, throw a warning or error or not, etc.) Which means the question really is, how do I encapsulate logic in my Swift code? Which of course there can be a million different ways with different levels of simplicity and tradeoffs and you have demonstrated some of them.

That's why this hypothetical isn't actually helpful, it really depends on what you want the code to do. You want non negative coins, great, maybe one of your 5 or maybe UInt as someone suggested. You want something else, non negative money whatever, then it's gonna be something else, not Double tho ;) as others have suggested. There's no general answer here because its not the right kind of general question.

2 Likes

Hi there, I would add in the direction of Hacksaw, that the "audience" for your code is importante.

My usual thinking is the following :
First, does a known type does what I need (here for exemple Uint instead of int)
If not I would make my own business type for the requirement of my app.

Then I would use one of the approach you stated depending on the use case :

  • let + set function, if I want the setting of the value to be explicitly/conscientiously made and convey the idea of a processing of the value (ex: set(positiveBalance:Double) throw... meaning i'll verify the value).
  • error if that can be used by anyone (eg: user via a form).
  • assert if it's an api value and only dev will use it, so they can debug while coding.

Also I would use let as often as possible and explicit accessibility keyword (public, internal, private, private(set))

I hope that helps

1 Like