1

I'm trying to create an extension of Array that returns a new array of unique items based on the items with the closure applied.

For example: If I had an array of Apple where apple has properties name and origin, to get one Apple of each origin I would call apple.uniqued(on: { $0.origin })

Here's the code I have so far:

extension Array where Element: Equatable {
    func uniqued(on extract: (Element) -> Equatable) { // A
        let elementsAndValues = map { (item: $0, extracted: extract($0)) } // 1

        var uniqueValues: [Element] = []
        var uniqueExtracts: [Equatable] = [] // A

        elementsAndValues.forEach { (item, extracted) in
            if !uniqueExtracts.contains(extracted) { // 3, B
                uniqueValues += [item]
                uniqueExtracts += [extracted]
            }
        }

        return uniqueValues
    }
}

This should work as follows:

  1. Create an array of tuples with both the original elements (item) and the elements with the closure applied (extracted)
  2. If uniqueExtracts doesn't contain the item, add it and add the item to the uniqueItems array.

The errors I'm getting are:

A) "Protocol 'SomeProtocol' can only be used as a generic constraint because it has Self or associated type requirements" (twice)

B) "Missing argument label 'where:' in call"

I'm using the latest version of Xcode. Any advice would be a lot of help. Many thanks.

Josh Paradroid
  • 1,172
  • 18
  • 45

2 Answers2

5

You have multiple problems that mix together to create the errors you’re seeing. What you should do is use a generic.

extension Array
{
    func uniqued<T:Equatable>(on extract:(Array.Element) -> T) -> [Array.Element]
    {
        let elementsAndValues = self.map{ (item: $0, extracted: extract($0)) }

        var uniqueValues:[Element] = []
        var uniqueExtracts:[T] = []

        for (item, extracted) in elementsAndValues
        {
            if !uniqueExtracts.contains(extracted)
            {
                uniqueValues.append(item)
                uniqueExtracts.append(extracted)
            }
        }

        return uniqueValues
    }
}

The <T:Equatable> declares a generic type parameter T that conforms to Equatable. Then the function signature can expect a closure that returns some generic type T that we know conforms to Equatable from the type constraint in the angle brackets. You also have to change every occurrence of Equatable to the generic parameter T, since Equatable isn’t a real type; see my answer here. If you do that the code should compile.

You have a few other things you should probably change:

  • Instead of using elementsAndValues.forEach(:), use a for <pattern> in list {} loop.

  • Although this is contentious, you should probably use the Array().append(:) method instead of += concatenation when adding one element to an array. In the case of +=, as opposed to +, this is purely to convey intent.

  • You did not declare a return type for your function, so the compiler assumes it returns Void, and so the return uniqueValues statement will cause a compiler error. Add a -> [Array.Element] to the function to fix this.

  • where Element:Equatable as a constraint on Array itself is superfluous. You are using a key function to determine equability, so whether the elements themselves are equatable is irrelevant.

  • You may want to use a Set or some other hashed data structure instead of a uniqueExtracts array. Testing for membership in an array is an O(n) operation.

Community
  • 1
  • 1
taylor swift
  • 2,039
  • 1
  • 15
  • 29
  • 1
    Great answer. I like how you addressed each of OP's short comings, point by point. – Alexander Apr 26 '17 at 17:20
  • Thank you for your great answer — clear and detailed. I'm curious about your first (bullet) point though. Is that a requirement or suggestion? – Josh Paradroid Apr 26 '17 at 18:49
  • @Alexander They didn't address each of my short comings. Trust me, I have many, many more short comings. – Josh Paradroid Apr 26 '17 at 18:51
  • 2
    @JoshParadroid it’s a suggestion as your code will compile, but “Swifty” code only uses `forEach(:)` when reusing closures, for example, if you have multiple arrays you wanted to iterate through while mutating outside variables. Otherwise use a `for` loop. – taylor swift Apr 26 '17 at 18:52
  • 1
    @JoshParadroid `forEach(_:)` loops are far too overused IMO (people just tend to like them because they're shiny and use closures). They're good at the end of long functional chains – but in most cases you should prefer `for ... in` loops as they support pattern matching & `where` clauses, can be easily broken out of, and are just as concise as `forEach(_:)` loops in most cases. – Hamish Apr 26 '17 at 19:01
  • @taylorswift Just another question of style. Are the "self." and "Array." recommended? I feel like they're both maybe a bit too verbose. Any suggestions? Thanks again. – Josh Paradroid Apr 27 '17 at 09:33
  • 1
    @JoshParadroid my advice is to always include them for clarity. It can be hard to tell what scope an instance or type variable really lives in. better to let absence of `self.` to mean “local variable”. I also recommend writing the type colon without spaces (`func f(a:Int)`) but the parameter colon with a space to the right (`f(a: 13)`) to differentiate the two meanings – taylor swift Apr 27 '17 at 09:39
1

I would do this with a group(by:) function, which would group each element in the sequence by a given key (e.g. the origin), yielding a Dictionary mapping keys to groups (arrays of elements in the group). From there, I would just map over the dictionary and just get the first element in each group.

public extension Sequence {
    public typealias Element = Iterator.Element
    public typealias Group = [Element]

    public func group<Key: Hashable>(by deriveKey: (Element) -> Key) -> [Key: Group] {
        var groups =  [Key: Group]()

        for element in self {
            let key = deriveKey(element)

            if var existingArray = groups[key] { // Group already exists for this key
                groups[key] = nil //performance optimisation to prevent CoW
                existingArray.append(element)
                groups[key] = existingArray
            }
            else {
                groups[key] = [element] // Create new group
            }
        }

        return groups
    }
}

struct Apple {
    let name: String
    let origin: String
}

let apples = [
    Apple(name: "Foo", origin: "Origin 1"),
    Apple(name: "Bar", origin: "Origin 1"),
    Apple(name: "Baz", origin: "Origin 2")
]

let firstAppleInEachOrigin = apples.group(by: {$0.origin}).flatMap{ _, group in group.first }


firstAppleInEachOrigin.forEach{ print($0) }
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • This is a good solution but I'll be accepting @taylor-swift's answer as I think it's more focussed on the problem that I was having, rather than an alternative solution. – Josh Paradroid Apr 26 '17 at 18:40
  • 1
    @Josh yeah I was aiming to solve this in a way that uses more generalized, reusable components. This way the grouping logic isnt restricted to just getting the first element of each origin. Now you have this extension under your belt to use in cases where you want all elements of each origin, or the last, or whatever – Alexander Apr 26 '17 at 21:18