11

I am trying to write a simple Array extension that provides a 'distinct' method. Here is what I have so far:

extension Array {
  func distinct() -> T[] {
    var rtn = T[]()

    for x in self {
      var containsItem = contains(rtn, x)
      if !containsItem {
        rtn.append(x)
      }
    }
    return rtn
  }
 }

The problem is that the 'contains' statement fails as follows:

Could not find an overload for 'contains' that accepts the supplied arguments

I am pretty sure the type constraints are correct. Any ideas?

ColinE
  • 68,894
  • 15
  • 164
  • 232

4 Answers4

13

Swift 1.x

The elements in an array don't have to be Equatable, i.e. they don't have be comparable with ==.

That means you can't write that function for all possible Arrays. And Swift doesn't allow you to extend just a subset of Arrays.

That means you should write it as a separate function (and that's probably why contains isn't a method, either).

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

func distinct<T: Equatable>(array: [T]) -> [T] {
    var rtn = [T]()

    for x in array {
        var containsItem = contains(rtn, x)
        if !containsItem {
            rtn.append(x)
        }
    }
    return rtn
}

distinct(array) // ["a", "b", "c"]

Update for Swift 2/Xcode 7 (Beta)

Swift 2 supports restricting extensions to a subset of protocol implementations, so the following is now allowed:

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

extension SequenceType where Generator.Element: Comparable {
    func distinct() -> [Generator.Element] {
        var rtn: [Generator.Element] = []

        for x in self {
            if !rtn.contains(x) {
                rtn.append(x)
            }
        }
        return rtn
    }
}

array.distinct() // ["a", "b", "c"]

Note how apple added SequenceType.contains using the same syntax.

nschum
  • 15,322
  • 5
  • 58
  • 56
  • 1
    Could you raise an exception or return nil somehow if the type of the array does not conform to `Equatable`? That would allow to extend Array, and gracefully handle unsupported types. – Alex Wayne Jun 06 '14 at 22:09
  • @AlexWayne you can have it fail silently (I would not say it is graceful) by doing an optional casting to the new local templated type that is equatable. You can see an example on my answer [here](http://stackoverflow.com/questions/24938948/array-extension-to-remove-object-by-value#24939100) – drewag Jul 24 '14 at 16:52
5

Finally found out how to do it:

extension Array {
    func contains<T : Equatable>(obj: T) -> Bool {
        return self.filter({$0 as? T == obj}).count > 0
    }

    func distinct<T : Equatable>(_: T) -> T[] {
        var rtn = T[]()

        for x in self {
            if !rtn.contains(x as T) {
                rtn += x as T
            }
        }

        return rtn
    }
}

And usage/testing:

let a = [ 0, 1, 2, 3, 4, 5, 6, 1, 2, 3 ]

a.contains(0)
a.contains(99)

a.distinct(0)

Unfortunately, I can't figure out a way to do it without having to specify an argument which is subsequently ignored. The only reason it's there is to invoke the correct form of distinct. The major advantage of this approach for distinct seems to be that it's not dumping a common term like distinct into the global namespace. For the contains case it does seem more natural.

David Berry
  • 40,941
  • 12
  • 84
  • 95
  • Yes. Unfortunately, by design, this allows you to call something illogical like `a.distinct("a")`, while for the function the compiler can give you a nice error. That would always be the case, even if there was a way to get rid of the parameter, because the two `T`s aren't enforced to match. – nschum Jun 07 '14 at 07:54
  • @nschum In my tests I wasn't able to pass the wrong type in. – David Berry Jun 07 '14 at 14:56
  • Indeed? For me `[0, 1].distinct("a")` causes an EXC_BAD_ACCESS at runtime. – nschum Jun 07 '14 at 19:11
  • @nschum Hmm... thought I tried it at least, but now I seem to be seeing the same thing. Dunno :) – David Berry Jun 07 '14 at 20:53
  • Well what's the point of type safety if it crashes at runtime :) – Marius Soutier Jun 30 '14 at 09:54
  • Swift is a bit strange. While you do "dump" into the *global* (more correctly, *module-wide*) namespace, you won't cause *global* conflicts since there's (generic and function) argument overloading. So `func contains(set: MathematicalSetOfSomeKind, element: MathematicalObjectOfSomeKind)` won't cause you any problems with any of the other (more traditional) `contains`—unless the compiler/source kit service fails at its task. – Constantino Tsarouhas Jul 31 '14 at 12:18
3

As of Swift 2, this can be achieved with a protocol extension method, e.g. on all types conforming to SequenceType where the sequence elements conform to Equatable:

extension SequenceType where Generator.Element : Equatable {

    func distinct() -> [Generator.Element] {
        var rtn : [Generator.Element] = []

        for elem in self {
            if !rtn.contains(elem) {
                rtn.append(elem)
            }
        }

        return rtn
    }
}

Example:

let items = [1, 2, 3, 2, 3, 4]
let unique = items.distinct()
print(unique) // [1, 2, 3, 4]

If the elements are further restricted to be Hashable then you can take advantage of the Set type:

extension SequenceType where Generator.Element : Hashable {

    func distinct() -> [Generator.Element] {
        return Array(Set(self))
    }
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
2

Another solution is to use the find(Array:[T], obj:T) function. It will return an optional Int, so what you could do is

if let foundResult = find(arr, obj) as Int
{
     //obj is contained in arr
} else
{
     //obj is not contained in arr.
}
Ethan
  • 1,567
  • 11
  • 16