Cast-Free Arithmetic

Updates: 10/13/2015
I recently gave a presentation on this topic @Realm, available in video format here.

Cast-Free Arithmetic Part 1:

Two months ago when WWDC week was happening and while many attendees were going to lectures, mingling at parties, and networking, I was resolved to take a deep dive into Swift 2.0 as quickly as I could. I started with the idea of using protocol extensions to consolidate all of the repeated code I had in an extension on several number types for dot property number conversions here. After working through it with a friend we were able to quickly accomplish this with the addition of pattern matching here. Afterward, whilst entering the next session related to UI, just on the cusp of our small success, I suddenly had another idea. Why not use generics in conjunction with our newly created numberConvertible protocol to completely transcend the need for casting between number types? Essentially crushing the type checker and giving us the ease of arithmetic we remember from Objective-C and were initially annoyed by it’s lack of in Swift. Thus, below is the road I took in achieving this end.

Inter-workings of Number Casting

Number casting is nothing more than a series of initializers on each number type. That is, each takes in all other “castable” types as input on creation. So writing Int(4.5) is doing the same as Int.init(4.5). In fact we can see where it is defined in the stdlb if you write out let test = Int.init(4.5) in XCode and command + click on the init part. To save you some clicking here is what it leads to.

Constructing our Protocol

With this understanding we can construct a protocol that has these initializers and extend our number types to conform to them.

There is only one hiccup so far, CGFloat does not have this init(_ value: CGFloat) so we have to add it ourselves. We will just initialize CGFloat to a Double and then use CGFloat‘s init(_ value: Double).

Update 1: A better way for extending init(_ value: CGFloat) on CGFloat is to assign it to self. Casting to a Double first is inefficient – The original implementation was written for the first beta of Xcode 7, on which I am not sure this current implementation works.

Generics, Inference, and Pattern Matching

Now we will rely on the type inference of generics and extend our protocol with a universal convert method.

We define our function with the expectation that our return type T will conform to NumberConvertible, giving us access to all of the init functions defined in the protocol. We can use a switch-statement on self, to check which number type we are casting from in the form of case let x as numberType. Then use x to initialize to type T with T(x) which is actually T.init(x) and call fatalError if something unexpected happens. At this point we can now use convert() to cast to another type without specifying the type.

Cast-Free Arithmetic Part 2:

Mixed Type Arithmetic

Stopping now doesn’t get us much, but beyond this things got complicated fast with compiler restrictions leading us down winding paths. We will start off by solving a simple case and build up to harder ones from there. So our goal to start will be to use our convert() function in something implicit that works with mixed-type arithmetic. Let’s shoot for this:

Overloading FTW

We can accomplish this by overloading our + operator, converting both numbers to a Double type, adding them, and converting the result back to the expected return type.

This works because our (v + w) addition defaults to using the stdlb definition of + which is what we want. If we were to not convert to Double first, adding v + w would cause a recursive loop.

More Complexity

Our accomplishment is short-lived, as a third type breaks our solution and becomes our next goal.

The compiler can take care of a single pair of mis-matched types, but if we add a third type the compiler throws an error. It seems the result type of the first is not inferable to the input of the next operation.

When I reached this point it was around mid-week of WWDC, so I took my problem to the labs, where I sat down and got to ask for help from the all-Swift-knowing Dave Abrahams. His resolution was that the compiler is probably not up for the task at hand, but he could graciously attempt a hacky work around if we liked, even though it would probably not work. Of course we said, yes please, by all means let’s see this hacky work around. Dave started writing a promote class to get the compiler to figure out what type should come next, but in the end the compiler failed us just as he predicted.

Initially discouraged,… I settled for something that I thought would actually be attainable, a single common return type. This would allow us to still mix and match number types in our arithmetic, but only allow for a single return type.

And there you have it, as long as we are only expecting a Double in the end, we can mix and match number types across multiple operations.

Cast-Free Arithmetic Part 3:

Unsafe Arithmetic: The Return of Multiple Number Types

So if you are still following along we have a solution that returns a Double, but of course this isn’t always what we want.

My first thought for solving the situation above, is to overload the assignment operator =, but this is restricted, so let’s try for a new operator ?=.

This works, but as you may have already realized, these examples are a bit contrived, since it only works for values that have already been initialized.
For instance we cannot assign a value to let m:Int. Since the assignment operators that we are allowed to overload have this restriction, we have to take another route in solving our next goal.

We could try to use an infix operator that takes in the number type on the left, and the operation result on the right, our Double. If we change the precedence to always call this operation last, everything should happen in the correct order.

Now, this strategy is questionable, as it is not much better than just wrapping the whole operation into an initializer Float(w + x + y). This solution clearly falls short of our goal, which is at it’s core to make things easier.

So let’s re-evaluate our options, dive deeper into how we are defining our overloaded arithmetic operations, and get a bit darker here, as we venture to do something that is probably quite unsafe in terms of type-safety…

Very Unsafe Arithmetic

From my first attempts at WWDC up to this point I have talked and worked through several ways I could trick the compiler to do my bidding.
Alternating between types as such was suggested to me here, but this still doesn’t work exactly right for explicit return types and is quite exhaustive to implement. So what I came up with in the end is something a bit unsafe.

Let’s begin by uncoupling our convert function to work outside of our protocol as such:

This is the first step on our unsafe path, but let’s continue along these lines and define a summation function that looks quite similar.

Now let’s bring down the compiler’s expectation on the first item in the pair and no longer require that it conform to NumberConvertible.

Now that our left component is allowed to be a non-number type we begin to walk a fine line on type-safety, but it does free the compiler from having to infer a type, allowing us to combine number types and return to any other number type.

And more tests

So now for the most part our goal is achieved. Although it comes at quite a high price, since this is now being accepted by the compiler as well: let o:Int = "NOT A NUMBER" + 5.

Looking more closely, in-depth testing reveals this method still has a flaw, in that if you have exactly two operations, it will only work if the last number to be evaluated is the same as the return type or if the first two numbers are the same type. Appending + 0 to the end fixes the problem.

Double = Int + Float + CGFloat ---FAIL

CGFloat = Int + Float + CGFloat ---OK

Double = Float + Float + CGFloat ---OK

Update 2: After more beta releases and compiler improvements the original implementation now seems to be working equivalently well against this more unsafe version, suffering only from the same two operand potential compiler warning issue.

You can check out the whole project here and my work so far for this last solution on the branch NoPreferenceType