59

Is it possible to extend an generic class for a specialised/constructed generic type? I would like to extend Int Arrays with a method to calculate the sum of its elements.

e.g.

extension Array<Int> {

    func sum() -> Int {
        return reduce(0) { $0 + $1 }
    }

}
Tom Blodget
  • 20,260
  • 3
  • 39
  • 72
Alexander Battisti
  • 2,178
  • 2
  • 19
  • 24

8 Answers8

57

This can be achieved using protocol extensions (See The Swift Programming Language: Protocols for more information).

To sum just Ints you could do:

extension Sequence where Iterator.Element == Int {
    var sum: Int {
        return reduce(0, +)
    }
}

Usage:

let nums = [1, 2, 3, 4]
print(nums.sum) // Prints: "10"

Or, for something more generic (Swift 5) you can use AdditiveArithmetic:

extension Sequence where Iterator.Element: AdditiveArithmetic {
    var sum: Iterator.Element {
        return reduce(.zero, +)
    }
}

For Swift 4 and below, you can use what @Wes Campaigne suggested and create an Addable protocol:

protocol Addable {
    init()
    func + (lhs: Self, rhs: Self) -> Self
}

extension Int   : Addable {}
extension Double: Addable {}
extension String: Addable {}
...

Next, extend Sequence to add sequences of Addable elements:

extension Sequence where Iterator.Element: Addable {
    var sum: Iterator.Element {
        return reduce(Iterator.Element(), +)
    }
}

Usage:

let doubles = [1.0, 2.0, 3.0, 4.0]
print(doubles.sum) // Prints: "10.0"

let strings = ["a", "b", "c"]
print(strings.sum) // Prints: "abc"
Bradley Mackey
  • 6,777
  • 5
  • 31
  • 45
ABakerSmith
  • 22,759
  • 9
  • 68
  • 78
  • 1
    Yes, nice use of Swift 2 protocol extensions. Though it'd be more idiomatic to make `sum` a read-only computed property instead of a zero-parameter function. – rickster Jun 10 '15 at 01:29
  • Good idea, I totally agree. Helps keep things cleaner. – ABakerSmith Jun 10 '15 at 01:33
  • 1
    Just a note. Equal type constraints(`==`) can be applied only to extensions to a protocol, not struct or class. – eonil Jun 21 '16 at 07:51
  • 1
    This answer is pretty straight forward. While keeping this answer, can you also add answer for any Type that conforms to `IntegerType`? I think that way your answer would be much more generic. Perhaps doing it that way make extending *both* `double` & `Int` unnecessary but then you would have to create a separate one for `String` – mfaani Sep 05 '16 at 20:11
  • This awesome answer needs a swift 3 update Basically: Generator is now called Iterator. + must be static in the protocol and reduce has omitted the combine variable name. – Sentry.co Mar 07 '17 at 15:09
  • **Swift 5 update**: there's already an addable protocol called `AdditiveArithmetic` built-in! – Bradley Mackey Oct 18 '22 at 08:51
8

Managed to get something working in an extensible, generic fashion without abusing the type system too badly, however it has some limitations.

protocol Addable {
    func +(lhs: Self, rhs: Self) -> Self
    class var identity: Self { get }
}

extension Int : Addable {
    static var identity: Int { get { return 0 } }
}

extension String : Addable {
    static var identity: String { get { return "" } }
}

extension Array {
    func sum<U : Addable>() -> U? {
        let s: U? = U.identity
        return self.sum(s)
    }

    func sum<U : Addable>(start: U?) -> U? {
        return reduce(start) { lhs, rhs in
            switch (lhs, rhs) {
            case (.Some(let left), let right as U):
                return left + right
            default:
                return nil
            }
        }
    }
}

Specifically: with this solution, type inferencing won't work on the no-parameter sum() method, so you have to either annotate the expected return type or give it a starting value (from which it can infer the type).

Note also that this returns a value of Optional type: if for any reason a sum of the expected type cannot be computed from the array, it returns nil.

To illustrate:

let int_array = Array(1...10)

let x: Int? = int_array.sum() // result: {Some 55}
let x2 = int_array.sum(0) // result: {Some 55}
let x3 = int_array.sum() // Compiler error because it can't infer type


let string_array = ["a", "b", "c"]

let y: String? = string_array.sum() // result: {Some "abc"}
let y2 = string_array.sum("") // result: {Some "abc"}

let y3: Int? = string_array.sum() // result: nil  (can't cast String to Int)
let y4 = string_array.sum(0) // result: nil  (can't cast String to Int)


let double_array = [1.3, 4.2, 2.1]

let z = double_array.sum(0.0) // Compiler error because we haven't extended Double to be Addable
Wes Campaigne
  • 4,060
  • 3
  • 22
  • 17
  • But see http://stackoverflow.com/a/24028208/341994 - extend Array and, if needed, genericize the method(s) – matt Jun 23 '14 at 17:39
  • It seems like the `func +` in the protocol is not being implemented by any implementing classes? – Unheilig Apr 30 '15 at 12:38
  • Instead of having generic methods, I think `extension Array where Element: Addable` is the way to go. – Raphael Jan 10 '17 at 12:46
8

Swift 5.x:

extension Array where Element == Int {

    var sum: Int {
        reduce(0, +)
    }
}
2

Swift 5+

There is a built in protocol, AdditiveArithmetic, that defines the generic + operator, so we can write an extension that works for all Sequences (Array, ArraySlice, Set) of addable types (Int, Double, Float, custom types) automatically. This is the example given on Apple's documentation page:

extension Sequence where Element: AdditiveArithmetic {
    func sum() -> Element {
        return reduce(.zero, +)
    }
}

This will now work on any Sequence or Collection with any kind of 'Addable' elements, including Int, Double, Float etc.

Bradley Mackey
  • 6,777
  • 5
  • 31
  • 45
1

Looks like you can't. The closest we can get is the function

func sum(a:Array<Int>) -> Int {
    return a.reduce(0) {$0 + $1}
}

Swift will allow you to add extension on the Array class but not specifically to a specialized version of the class.

error: <REPL>:108:1: error: non-nominal type 'Array<Int>' cannot be extended

You can extend the Array class.

extension Array {

    func sum() -> Int {
        return reduce(0) { $0 + $1 }
    }
}

The problem is now with the + operator

error: <REPL>:102:16: error: could not find an overload for '+' that accepts the supplied arguments
        return reduce(0) { $0 + $1 }

This is somewhat expected since we cannot be sure that the + operator will be will be overloaded for all the possible types that could be used in an array.

So we could try to constraint the operation only on certain classes. Something like

class Dummy {
}

extension Array {
    func someFunc<T:Dummy>() -> Int {
       return 0
    }
}

var l = [Dummy()]
var r = l.someFunc() // Expect 0

Conceptually this should work (currently it seems that there is a bug, Xcode crashes when evaluating a playground using this code). In the eventually that it works, we cannot use this trick since the type Int is not a class.

extension Array {
    func sum<T:Int>() -> T {
        return reduce(0) { $0 + $1 }
    }
}

error: <REPL>:101:14: error: inheritance from non-protocol, non-class type 'Int'
    func sum<T:Int>() -> T {

I also looked at extending the Array class with a protocol but again Int not being a class makes it impossible. If the numeric types were classes, it would be nice if we could have a protocol to define that a class can be added just like Comparable or Equatable but my understanding is that protocol cannot define generic function which would be needed to create a Addable protocol.

Edit:

As stated by other answers, you can make it work for Int by explicitly checking and casting to Int in the closure. I guess I missed it will investigating. But it would still be nice if we could have a generic way of working with numeric types.

Rod
  • 52,748
  • 3
  • 38
  • 55
1

It is possible to return a real sum-value after you have tested for the int-type in sum(). Doing so I would solve the problem as follows:

import Cocoa

extension Array {
    func sum() -> Int {
        if !(self[0] is Int) { return 0; }
        var sum = 0;
        for value in self { sum += value as Int }
        return sum;
    }
}

let array = [1,2,3,4,5]
array.sum() // =15

let otherArray = ["StringValue"]
otherArray.sum() // =0
Dennis Zoma
  • 2,621
  • 2
  • 17
  • 27
1

Alexander,

Here's how you can do it:

extension Array {
    func sum() -> Int {
        return reduce(0) { ($0 as Int) + ($1 as Int) }
    }
}

Works like a charm, tested in the playground. However, you might get into trouble if you call this function on different types of arrays.

Jean Le Moignan
  • 22,158
  • 3
  • 31
  • 37
-4

you can do it as well

extension Array {
    func sum () -> Int? {
        guard self.count > 0  && self.first is Int  else {
            return nil
        }
        var s = 0
        forEach {
            s += $0 as! Int
        }
        return s
    }
}
Tux
  • 5
  • 2