2

Swift 4.2

I have multiple functions that replace an object or struct in an array if it exists, and if it does not exist, it adds it.

func updateFruit(_ fruit: Fruit)
{
    if let idx = fruitArray.firstIndex(where: { $0.id == fruit.id })
    {
        fruitArray[idx] = fruit
    }
    else
    {
        fruitArray.append(fruit)
    }
}

Obviously I could make this into extension on Array:

extension Array
{
    mutating func replaceOrAppend(_ item: Element, whereFirstIndex predicate: (Element) -> Bool)
    {
        if let idx = self.firstIndex(where: predicate)
        {
            self[idx] = item
        }
        else
        {
            append(item)
        }
    }
}

However, is there a simpler, easier way of expressing this? Preferably using a closure or build-in function.

NOTE: current implementation does not allow using a set.

8
  • 3
    Your code is simple and easy :)
    – Martin R
    Commented Mar 20, 2019 at 15:21
  • 1
    FYI - Since you are actually looking for more of a code review for working code, you may wish to look at the Code Review site. If you do end up posting your question there, remove this one.
    – rmaddy
    Commented Mar 20, 2019 at 15:28
  • 2
    You could make your life "easier" making your Fruit conform to Equatable. Doing so you don't need a predicate. If you would like to use a Set just make it conform to Hashable as well.
    – Leo Dabus
    Commented Mar 20, 2019 at 15:32
  • 1
    @LeoDabus I like that solution. Commented Mar 20, 2019 at 16:30
  • 1
    When you say you have "multiple functions," what do the other functions look like? I suspect you're making this code generic along the wrong axis. Do they all have something like $0.id == newthing.id, or do they have other predicates? Are all the predicates $0.<prop> == newthing.<prop> (even if not just id)? Or are these predicates much more complex? Generic code must always start from how it is called or you'll go down the wrong road.
    – Rob Napier
    Commented Mar 20, 2019 at 20:05

3 Answers 3

4

Given your use case, in which you're always checking $0.<prop> == newthing.<prop>, you can lift this a little more by adding:

mutating func replaceOrAppend<Value>(_ item: Element, 
                                     firstMatchingKeyPath keyPath: KeyPath<Element, Value>)
    where Value: Equatable
{
    let itemValue = item[keyPath: keyPath]
    replaceOrAppend(item, whereFirstIndex: { $0[keyPath: keyPath] == itemValue })
}

You can then use it like:

struct Student {
    let id: Int
    let name: String
}

let alice0 = Student(id: 0, name: "alice")
let alice1 = Student(id: 1, name: "alice")
let bob = Student(id: 0, name: "bob")

var array = [alice0]

array.replaceOrAppend(alice1, firstMatchingKeyPath: \.name) // [alice1]
array.replaceOrAppend(bob, firstMatchingKeyPath: \.name)    // [alice1, bob]

And of course if you do this a lot, you can keep lifting and lifting.

protocol Identifiable {
    var id: Int { get }
}

extension Student: Identifiable {}

extension Array where Element: Identifiable {
    mutating func replaceOrAppendFirstMatchingID(_ item: Element)
    {
        replaceOrAppend(item, firstMatchingKeyPath: \.id)
    }
}

array.replaceOrAppendFirstMatchingID(alice0) // [alice1, alice0]
2

I can suggest to create protocol Replacable with replaceValue that will represent identifier which we can use to enumerate thru objects.

protocol Replacable {
    var replaceValue: Int { get }
}

now we can create extension to Array, but now we can drop predicate from example code like this

extension Array where Element: Replacable {
    mutating func replaceOrAppend(_ item: Element) {
        if let idx = self.firstIndex(where: { $0.replaceValue == item.replaceValue }) {
            self[idx] = item
        }
        else {
            append(item)
        }
    }
}

Since Set is not ordered collection, we can simply remove object if set contains it and insert new value

extension Set where Element: Replacable {
    mutating func replaceOrAppend(_ item: Element) {
        if let existItem = self.first(where: { $0.replaceValue == item.replaceValue }) {
            self.remove(existItem)
        }
        self.insert(item)
    }
}
4
  • 1
    Although the solution for an array might work, I prefer the one where we conform to equatable, as it is simpler Commented Mar 20, 2019 at 16:31
  • You will need to override == method of Equatable by checking only if object ids is equal. What would be not correct for all situation. For example, you have two object with same ids but with different names. Should they be equal? That's why I prefer too have special designed protocol Commented Mar 20, 2019 at 18:42
  • I agree that using Equatable would be incorrect here, but this protocol is extremely single-purpose (particularly for such a generic name as "Replaceable"). The original code in the question is much better IMO. If an extension were desired, where Element == Fruit would be the appropriate restriction until you could find enough other use cases in the same program to extract a meaningful protocol.
    – Rob Napier
    Commented Mar 20, 2019 at 19:26
  • @TarasChernyshenko after reconsidering Equatable, I agree it's not the best solution. Commented Mar 25, 2019 at 17:45
1

Assuming your Types are Equatable, this is a generic extension:

extension RangeReplaceableCollection where Element: Equatable {

    mutating func addOrReplace(_ element: Element) {
        if let index = self.firstIndex(of: element) {
            self.replaceSubrange(index...index, with: [element])
        }
        else {
            self.append(element)
        }
    }
}

Though, keep in mind my (and your) function will only replace one of matching items.

Full Working playground test:

Playgrounds Test

13
  • Your example implementation of TestType.== is not a valid conformance to Equatable. Equatable requires "To maintain substitutability, the == operator should take into account all visible aspects of an Equatable type." Your == only considers first.
    – Rob Napier
    Commented Mar 20, 2019 at 19:23
  • @RobNapier It's a test implementation to validate the functionality of addOrReplace...it's not meant to be "correct"
    – GetSwifty
    Commented Mar 20, 2019 at 19:38
  • But if it were more correct, it wouldn't be useful. If element is fully substitutable for the element found in the collection, why would you replace it? They're indistinguishable from each other.
    – Rob Napier
    Commented Mar 20, 2019 at 19:44
  • 1
    If that were significant, it would violate Equatable. "Equality implies substitutability—any two instances that compare equally can be used interchangeably in any code that depends on their values." The problem you're describing only comes up with mutable reference types. This is why mutable reference types are extremely difficult to make Equatable using anything but ===.
    – Rob Napier
    Commented Mar 20, 2019 at 20:02
  • 2
    @RobNapier If you want to be dogmatic about unenforced rules, go for it. Tool-shedding about encapsulation aside, I'll just say APIs like this are public so we can use them dynamically in a way that makes sense when esoteric ideals meet real world usage.
    – GetSwifty
    Commented Mar 20, 2019 at 20:59

Not the answer you're looking for? Browse other questions tagged or ask your own question.