23

Swift lets you create an Array extension that sums Integer's with:

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

Which can now be used to sum Int[] like:

[1,2,3].sum() //6

But how can we make a generic version that supports summing other Number types like Double[] as well?

[1.1,2.1,3.1].sum() //fails

This question is NOT how to sum numbers, but how to create a generic Array Extension to do it.


Getting Closer

This is the closest I've been able to get if it helps anyone get closer to the solution:

You can create a protocol that can fulfills what we need to do, i.e:

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

Then extend each of the types we want to support that conforms to the above protocol:

extension Int : Addable {
}

extension Double : Addable {
}

And then add an extension with that constraint:

extension Array {
    func sum<T : Addable>(min:T) -> T
    {
        return self.map { $0 as T }.reduce(min) { $0 + $1 }
    }
}

Which can now be used against numbers that we've extended to support the protocol, i.e:

[1,2,3].sum(0) //6
[1.1,2.1,3.1].sum(0.0) //6.3

Unfortunately I haven't been able to get it working without having to supply an argument, i.e:

func sum<T : Addable>(x:T...) -> T?
{
    return self.map { $0 as T }.reduce(T()) { $0 + $1 }
}

The modified method still works with 1 argument:

[1,2,3].sum(0) //6

But is unable to resolve the method when calling it with no arguments, i.e:

[1,2,3].sum() //Could not find member 'sum'

Adding Integer to the method signature also doesn't help method resolution:

func sum<T where T : Integer, T: Addable>() -> T?
{
    return self.map { $0 as T }.reduce(T()) { $0 + $1 }
}

But hopefully this will help others come closer to the solution.


Some Progress

From @GabrielePetronella answer, it looks like we can call the above method if we explicitly specify the type on the call-site like:

let i:Int = [1,2,3].sum()
let d:Double = [1.1,2.2,3.3].sum()
mythz
  • 141,670
  • 29
  • 246
  • 390
  • Why are you mapping with "as Int" like that? In fact, since `reduce` sums the array elements, why are you mapping at all? – matt Jun 06 '14 at 19:45
  • @matt what else can I do that works for Int's and Double's? It's an example, that I'd like to apply to `min()` and `max()` as well. And no `func sum() -> T { return self.reduce() }` does not work. – mythz Jun 06 '14 at 20:06
  • 1
    The fact we can't extend Array is the real killer here. Also that you can't say . If Int and Double had a common superclass you'd be home free, but they don't. That's a serious design flaw, I think. – matt Jun 06 '14 at 21:01
  • @matt yeah I've been struggling with this for a while, keep thinking there's a hidden generic extensibility point in extensions that can enable it. `extension Array {...}` would've been my intuitive intuitive first guess as well. – mythz Jun 06 '14 at 21:05
  • 1
    I went for a run and thought some more, and came to a similar conclusion. You'll notice that the built-in `reduce` declaration depends on the type of the initial seed (the first parameter) to determine the output type. – matt Jun 06 '14 at 23:42
  • What is the capitalized Self here and how did you know about it? I can't find it documented in the Guide. I'm guessing it's a kind of type alias for "the type of the adopter". – matt Jun 07 '14 at 18:23
  • @matt Yeah that's my impression of `Self` as well, i.e. used in protocols to refer to its type. It's a keyword in the lang ref, visible in the API headers when you click on protocols like `Comparable` or `BitwiseOperations` and mentioned briefly at ~33:00 in the [Advanced Swift](https://developer.apple.com/videos/wwdc/2014/) WWDC presentation. – mythz Jun 07 '14 at 19:13

8 Answers8

8

I think I found a reasonable way of doing it, borrowing some ideas from scalaz and starting from your proposed implementation. Basically what we want is to have typeclasses that represents monoids.

In other words, we need:

  • an associative function
  • an identity value (i.e. a zero)

Here's a proposed solution, which works around the swift type system limitations

First of all, our friendly Addable typeclass

protocol Addable {
    class func add(lhs: Self, _ rhs: Self) -> Self
    class func zero() -> Self
}

Now let's make Int implement it.

extension Int: Addable {
    static func add(lhs: Int, _ rhs: Int) -> Int {
        return lhs + rhs
    }

    static func zero() -> Int {
        return 0
    }
}

So far so good. Now we have all the pieces we need to build a generic `sum function:

extension Array {
    func sum<T : Addable>() -> T {
        return self.map { $0 as T }.reduce(T.zero()) { T.add($0, $1) }
    }
}

Let's test it

let result: Int = [1,2,3].sum() // 6, yay!

Due to limitations of the type system, you need to explicitly cast the result type, since the compiler is not able to figure by itself that Addable resolves to Int.

So you cannot just do:

let result = [1,2,3].sum()

I think it's a bearable drawback of this approach.

Of course, this is completely generic and it can be used on any class, for any kind of monoid. The reason why I'm not using the default + operator, but I'm instead defining an add function, is that this allows any type to implement the Addable typeclass. If you use +, then a type which has no + operator defined, then you need to implement such operator in the global scope, which I kind of dislike.

Anyway, here's how it would work if you need for instance to make both Int and String 'multipliable', given that * is defined for Int but not for `String.

protocol Multipliable {
    func *(lhs: Self, rhs: Self) -> Self
    class func m_zero() -> Self
}

func *(lhs: String, rhs: String) -> String {
    return rhs + lhs
}
extension String: Multipliable {
    static func m_zero() -> String {
        return ""
    }
}
extension Int: Multipliable {
    static func m_zero() -> Int {
        return 1
    }
}

extension Array {
    func mult<T: Multipliable>() -> T {
        return self.map { $0 as T }.reduce(T.m_zero()) { $0 * $1 }
    }
}

let y: String = ["hello", " ", "world"].mult()

Now array of String can use the method mult to perform a reverse concatenation (just a silly example), and the implementation uses the * operator, newly defined for String, whereas Int keeps using its usual * operator and we only need to define a zero for the monoid.

For code cleanness, I much prefer having the whole typeclass implementation to live in the extension scope, but I guess it's a matter of taste.

Gabriele Petronella
  • 106,943
  • 21
  • 217
  • 235
  • Yep, this looks pretty similar to my approach except for `init()` you've got `T.zero()` and instead of `+` you have an explicit `add()`. But this is more work since `Int` and `Double` need to explicitly implement the protocol definitions, instead of re-using existing functions. – mythz Jun 07 '14 at 00:37
  • @mythz, in my first version I used `+` as well, but I didn't like it since it's not generic enough: you cannot have operators in extensions, so if you want to make a generic type, which has no `+` defined, `Addable` you can't. Instead the `add` function can be implemented by any type, making this approach as generic as possible. – Gabriele Petronella Jun 07 '14 at 00:40
  • You can, you just need to declare operator definitions in global scope. The [Advanced Swift](https://developer.apple.com/videos/wwdc/2014/) video talks about this a bit. – mythz Jun 07 '14 at 00:50
  • I know, but I think getting out of the bounds of the typeclass implementation pollutes the design a little. So far it's pretty clean, so I'm prone to write a couple of lines more, rather than *hacking* the design. That being said, I think this is a minor detail. – Gabriele Petronella Jun 07 '14 at 00:54
  • 1
    @mythz, I added an example which uses operators. Both approaches are valid, I believe. Thanks for the interesting question and for the proposed solution :) – Gabriele Petronella Jun 07 '14 at 01:07
  • Adding `init(_: Int)` to the protocol works for all `IntegerType` classes. There's no need to modify built-in classes to make Swift look like Java. –  Jun 11 '15 at 05:01
8

As of Swift 2 it's possible to do this using protocol extensions. (See The Swift Programming Language: Protocols for more information).

First of all, the Addable protocol:

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

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

Next, extend SequenceType to add sequences of Addable elements:

extension SequenceType where Generator.Element: Addable {
    var sum: Generator.Element {
        return reduce(0, combine: +)
    }
}

Usage:

let ints = [0, 1, 2, 3]
print(ints.sum) // Prints: "6"

let doubles = [0.0, 1.0, 2.0, 3.0]
print(doubles.sum) // Prints: "6.0"
ABakerSmith
  • 22,759
  • 9
  • 68
  • 78
4

In Swift 2, you can solve it like this:

Define the monoid for addition as protocol

protocol Addable {
    init()
    func +(lhs: Self, rhs: Self) -> Self
    static var zero: Self { get }
}
extension Addable {
    static var zero: Self { return Self() }
}

In addition to other solutions, this explicitly defines the zero element using the standard initializer.

Then declare Int and Double as Addable:

extension Int: Addable {}
extension Double: Addable {}

Now you can define a sum() method for all Arrays storing Addable elements:

extension Array where Element: Addable {
    func sum() -> Element {
        return self.reduce(Element.zero, combine: +)
    }
}
Tali
  • 861
  • 8
  • 17
2

Here's a silly implementation:

extension Array {
    func sum(arr:Array<Int>) -> Int {
        return arr.reduce(0, {(e1:Int, e2:Int) -> Int in return e1 + e2})
    }
    func sum(arr:Array<Double>) -> Double {
        return arr.reduce(0, {(e1:Double, e2:Double) -> Double in return e1 + e2})
    }
}

It's silly because you have to say arr.sum(arr). In other words, it isn't encapsulated; it's a "free" function sum that just happens to be hiding inside Array. Thus I failed to solve the problem you're really trying to solve.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • thx, appreciate the alternative example. Still trying out different things myself. – mythz Jun 06 '14 at 20:56
  • 2
    It would have been more honest at least to make those `static` functions, but I couldn't get that to work. I'm tempted to call that a bug. – matt Jun 06 '14 at 21:00
  • You can make them static by using the `static` keyword in front of the `func` definition. Similar can be done with classes using the `class` keyword instead of `static`. – Erik Jun 07 '14 at 06:03
  • @matt I did, try `static func sum(array:Array) { }` inside `extension Array`. It worked for me in the playground and I was able to `println()` within it and see the result. – Erik Jun 07 '14 at 15:59
1
  3> [1,2,3].reduce(0, +)
$R2: Int = 6

  4> [1.1,2.1,3.1].reduce(0, +)
$R3: Double = 6.3000000000000007

Map, Filter, Reduce and more

adjusting
  • 51
  • 2
0

From my understanding of the swift grammar, a type identifier cannot be used with generic parameters, only a generic argument. Hence, the extension declaration can only be used with a concrete type.

Lorentz Vedeler
  • 5,101
  • 2
  • 29
  • 40
0

It's doable based on prior answers in Swift 1.x with minimal effort:

import Foundation

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

extension Int : Addable {}
extension Int8 : Addable {}
extension Int16 : Addable {}
extension Int32 : Addable {}
extension Int64 : Addable {}

extension UInt : Addable {}
extension UInt8 : Addable {}
extension UInt16 : Addable {}
extension UInt32 : Addable {}
extension UInt64 : Addable {}

extension Double : Addable {}
extension Float : Addable {}
extension Float80 : Addable {}

// NSNumber is a messy, fat class for ObjC to box non-NSObject values
// Bit is weird

extension Array {
    func sum<T : Addable>(min: T = T(0)) -> T {
        return map { $0 as! T }.reduce(min) { $0 + $1 }
    }
}

And here: https://gist.github.com/46c1d4d1e9425f730b08

Swift 2, as used elsewhere, plans major improvements, including exception handling, promises and better generic metaprogramming.

0

Help for anyone else struggling to apply the extension to all Numeric values without it looking messy:

extension Numeric where Self: Comparable {

    /// Limits a numerical value.
    ///
    /// - Parameter range: The range the value is limited to be in.
    /// - Returns: The numerical value clipped to the range.
    func limit(to range: ClosedRange<Self>) -> Self {
        if self < range.lowerBound {
            return range.lowerBound
        } else if self > range.upperBound {
            return range.upperBound
        } else {
            return self
        }
    }
}
George
  • 25,988
  • 10
  • 79
  • 133