Using Any to Store Numbers in Swift

In the last post we implemented an accumulator using generics, so it could handle either integers or floating point types.

But how about handling both in the same accumulator? In Paul Graham’s guidelines for submitting an accumulator, he says it needs to work

for any numeric type – i.e. can take both ints and floats and returns functions that can take both ints and floats. (It is not enough simply to convert all input to floats. An accumulator that has only seen integers must return integers.)

He then gives some example usage: 1

E.g. if after the example, you added the following code (in a made-up language):
x = foo(1);
x(5);
foo(3);
print x(2.3)
It should print 8.3.

But if you try to do this with our generics implementation, you will get a error when you pass in the final number. “Cannot convert the expression’s type ‘Int’ to type ‘Int’”, the compiler will say, which is a little confusingly worded given it’s a Double that can’t be converted, but what it means is it can’t convert what you passed to it’s Int parameter into an Int.

So I guess our example still isn’t valid for submission. What behaviour do we need? In Ruby, you can do the following:

irb> n = 1
 => 1
2.1.0 :002 > n.class
 => Fixnum
2.1.0 :003 > n += 2.2
 => 3.2
2.1.0 :005 > n.class
 => Float

The value n silently gets upgraded to a float. How can we do this in Swift? Well, there is an Any type that can hold any kind of type. But it’s not as easy as that. Just trying the following:

  func foo(var n: Any) -> (Any) -> Any {
    return { n += $0; return n }
  }

gets you the familiar compiler error about there being no overloaded += operator. But unlike when we had this problem with generics, this can’t be fixed with type constraints. Any doesn’t implement +=.2 In fact, Any doesn’t let you do much of anything except store a value and pass it around. If you want to maniuplate what’s inside, you must use a switch statement:

let a: Any = 1
switch a {
case let i as Int:
  // do appropriate logic when it's an Int
...
}

If we revisit our incf implementation, but adapt it to take and return Any, and upgrade an Int to Double by switching to identify them, we get the following monstrocity:

func incf(inout lhs: Any, rhs: Any) -> Any {
    switch lhs {
    case let i as Int:
        switch rhs {
        case let j as Int:
            lhs = i + j
        case let d as Double:
            lhs = Double(i) + d
        default:
            // what to do here?
        }
    case let d as Double:
        switch rhs {
        case let e as Double:
            lhs = d + e
        case let i as Int:
            lhs = d + Double(i)
        default:
            // what to do here?
        }
    default:
            // what to do here?
    }
    return lhs
}

What to put in for those default cases, where one of the types isn’t supported. Maybe throw an exception? Hahaha only kidding, Swift doesn’t have exceptions.3 The Swift way of handling this would be to allow incf to return a nil type.4 Users of foo then have to account for the possibility of an error. Let’s do that, and add an implementation of foo with our new incf:

func incf(inout lhs: Any, rhs: Any) -> Any? {
    switch lhs {
    case let i as Int:
        switch rhs {
        case let j as Int:
            lhs = i + j
        case let d as Double:
            lhs = Double(i) + d
        default:
            lhs = nil
        }
    case let d as Double:
        switch rhs {
        case let e as Double:
            lhs = d + e
        case let i as Int:
            lhs = d + Double(i)
        default:
            lhs = nil
        }
    default:
        lhs = nil
    }
    return lhs
}

func foo(var n: Any) -> (Any) -> Any? {
    return { incf(&n, $0) }
}

There are many flaws in this approach, but it does at least work with the example above now – pass an Int and then a Double and you’ll get a Double.

Pass a Float in though and you get back a nil. But adding Float to the already nasty switch statement will make it even nastier. If we wanted to extend incf to cover even more types (maybe numeric classes like a Rational or Decimal) it gets even worse. And if we want to implement a decrement function we’d have to replicate the whole statement.

There’s one more way to write the switch statement. Swift’s pattern matching can operate on multiple values together, giving us the following alternative to having to nest multiple switch statements:

func incf(inout lhs: Any, rhs: Any) -> Any? {
    switch (lhs,rhs) {
    case let (i as Int, j as Int):
        lhs = i + j
    case let (d as Double, e as Double):
        lhs = d + e
    case let (i as Int, d as Double):
        lhs = Double(i) + d
    case let (d as Double, i as Int):
        lhs = Double(i) + d
    default:
        lhs = nil
    }
    return lhs
}

Unfortunately, while this code is a little more succinct, and possibly less error-prone, it still suffers from the same problems if you tried to extend it to more types.

The next post will be about ditching Any in favor of a more type-safe approach to this problem.


  1. Note, he calls foo a second time in his example. Presumably he got a lot of duff submissions where people were using global variables to accumulate. 
  2. And before you try it, no, you can’t extend Any to try and implement +=. Or anything else for that matter, it’s unextendable. 
  3. This is a good example of how removing exceptions forces you to find a perfectly valid alternative in many cases. Exceptions were always discouraged in Objective-C except for truly exceptional circumstances, Swift took this one step further and did away with them entirely. 
  4. If you think the right thing to do is not add the number, you’ll hit a different problem. That means the default action should do nothing, but Swift insists you have some code there. So you have to write something that doesn’t have any effect. Feel free to get creative. No, I’m afraid a comment saying “do nothing” doesn’t count as something. 

2 thoughts on “Using Any to Store Numbers in Swift

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s