Setting boundaries for integer function parameters

Is it possible to set a function parameter so that an integer number entered is within a certain range i.e. between zero and fifteen. Much like making the parameter type safe, only a step further so that it has an upper limit ?

This sounds like an ideal use for an integer based enum.

One cannot do this trivially when accepting Int. This is because the problem you’re expressing (“must be within the range of zero and fifteen”) is currently expressed in the value domain, not the type domain.

You can transform this by forcing the transformation of the Int into a different type that does provide that constraint. @Joanna_Carter has suggested one alternative, but here is mine:

struct ZeroToFifteenInt: RawRepresentable, ExpressibleByIntegerLiteral {
    var rawValue: Int {
        didSet {
            precondition((0...15).contains(newValue))
        }
    }

    init(rawValue: Int) {
        precondition((0...15).contains(rawValue))
        self.rawValue = rawValue
    }

    init(integerLiteral: Int) {
        self = .init(rawValue: integerLiteral)
    }
}
2 Likes

Unfortunately, this can fail at runtime.

Here is the enum idea, which is safe :

enum ZeroToFifteen: Int
{
  case zero = 0
  case one
  case two
  case three
  case four
  case five
  case six
  case seven
  case eight
  case nine
  case ten
  case eleven
  case twelve
  case thirteen
  case fourteen
  case fifteen
}


func testZeroToFifteen(value: ZeroToFifteen)
{
  let intValue = value.rawValue
  
  // …
}


  {
    testFifteen(value: .seven)
  }

It can fail at runtime because I wrote it that way, but the same is true in general if you have to transform between Int and your other type. It is not possible to transform an Int into another type without dealing with the situation where the Int is out of range.

Mine can't fail at runtime if you check for the result of the failable initialiser.

    if let test = ZeroToFifteen(rawValue: 16)
    {
      print(test) // never gets executed
    }

That’s just choosing the type of failure you prefer. You are using a faisable initializer, which by definition can fail.

@glessard is right. Transforming my version to yours is trivial: replace the preconditionFailure with an init? and all is well again. The two approaches are not meaningfully different in what they can express (though they are different in their runtime layout).

This is the key, and why I’d rather go for your version personally (with an appropriate failure mode for the context where it will be used).

One approach (the wrapper struct) is an Int that has been validated to be within a specific range; the other is an enum which is representable as an Int. It’s a subtle difference, but I’d guess that the former is friendlier for the optimiser.

Terms of Service

Privacy Policy

Cookie Policy