Implementing generic mathematical function on Collection Types

Hi there,

forgive me if this question or a similar one has already been asked or if it's somewhat of a beginner question. I did some googling but it's a bit hard to find the correct search terms. What I'm trying to achieve is to add some generic mathematical functions like average(), median(), mode(), etc. to Collection types. I made some progress but it seems that I need to have two functions for each of these operations with the exact same implementation: one with a Element: BinaryInteger constraint and one with a Element:BinaryFloatingPoint constraint. For example for the average function, this would look like the following:

    func sum() -> Element
    {
        return self.reduce(0, +)
    }
    
    func average() -> Double where Element: BinaryInteger
    {
        return Double(self.sum()) / Double(self.count)
    }
    
    func average() -> Double where Element: BinaryFloatingPoint
    {
        return Double(self.sum()) / Double(self.count)
    }

Is there a way to reduce the code duplication?

kind regards,

Tim

Well, you can write your sum function against AdditiveArithmetic, but you’ll still need two separate versions of average because Double does not have an initializer that takes an AdditiveArithmetic instance.

Conversely, you could make a new protocol to encapsulate the idea of “convertible to Double”.

• • •

However, the reality of calculating an average (or even a sum) is quite a bit more intricate than you may realize.

When you sum a collection of signed integers, the partial sums could spuriously overflow even though the final result does not. For example, [100 as Int8, 100, -100, -100] clearly “should” sum to zero, but the first two elements add to 200, which overflows the Int8 type.

And when you sum a collection of floating-point numbers, significant bits could be lost due to order-of-magnitude differences. For example, [16_777_216 as Float, 1, -16_777_216] clearly “should” sum to one, but a naive calculation results in zero.

I have written more about these issues in this thread, including an implementation for integers that solves the first problem, and an implementation for floats that mitigates the second.

• • •

Furthermore, on the subject of average specifically, there exist so-called “online” algorithms for floating-point types, which don’t require calculating the full sum. A similar algorithm exists for the variance / standard deviation, and both can be done with compensated (Kahan) summation to reduce the accumulation of errors.

A while back I wrote a very simple implementation to calculate basic statistics. This version does not perform error correction, but such a change is not too difficult.

3 Likes

You should also be aware of the Numeric protocol, which implements addition, subtraction, and multiplication, although, unfortunately, there is no API for converting any Numeric type to Double.

I have indeed been looking at the Numeric protocol but indeed could not find any way to convert a Numeric into a Double. I was also browsing through the Swift Numerics package to see if that offered a solution, but as far as I have seen it doesn't.

Thanks for your extensive reply! I indeed didn't think about the possibility of overflows. I will have a look at the links you provided to see if I can use your suggestions to improve my code :smiley: