Removing objects from arrays

I assume that a convenience function like this would be very handy to have in arrays for all the OO frameworks:

extension Array where Element: AnyObject {
    mutating func removeFirst(object: AnyObject) {
        guard let index = firstIndex(where: {$0 === object}) else { return }
        remove(at: index)
    }
}

It assumes reference type equality based on ===.

  1. Why would such an essential function be missing in Swift?
  2. Why is === not the default implementation of class / ref type equality?

My guess would be that in many cases the objects that are identical (===) are a subset of the objects that are equal (==). To avoid unintentional behavior in all these cases there is no default conformance to Equatable that uses ===.

It generally seems that one of Swift's goals is to force you to be explicit where otherwise there would be ambiguity, hence things like force-unwrapping, exhaustive switch statements, strong type checking, and in this case not assuming that identity is equality. In other words, that Swift waits for you to implement equality this way, instead of assuming it, is most likely seen as a feature.

Makes sense. However, now I have difficulty of understanding equality in conjunction with elements of arrays. Why would you want to remove objects from an array based on equality? For structs this makes sense, but for classes I would have assumed that you want to work on arrays (specifically removing objects) based on their identity.

Assume you have two zombie sprites in an array. When all of their instance properties are the same, you might say they are equal. They will be on the same spot on the screen, looking and behaving exactly the same. So when you need to delete one of them, since they are equal, you find the firstIndex and delete the first. However, you were only considering the state within the object, while the reference was handed over to your app where it does make a difference which sprite you delete.

Basically reiterating that for removal of elements of reference types from arrays, identity is more natural than equality.

When all of their instance properties are the same, you might say they
are equal.

That’s really the crux of this issue, namely, how do you define equality. In the object-based frameworks I’ve used objects default to defining equality using their object pointer. You only override that in cases where it makes sense.

Consider this snippet based on traditional Cocoa idioms:

import Foundation

class Zombie: NSObject {

    init(decayLevel: Double) {
        self.decayLevel = decayLevel
    }

    var decayLevel: Double
}

let z1 = Zombie(decayLevel: 0.5)
let z2 = Zombie(decayLevel: 0.5)
print(z1 == z2)     // prints false

I didn’t override isEqual(_:) and thus I got pointer equality. Contrast this to:

let s1 = NSString(string: "Brains!")
let s2 = NSString(string: "Brains!")
print(s1 == s2)     // prints true

where NSString does override isEqual(_:).

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Which underlines what I am saying and leaves the question open: Why is there no remove object method found in Array?

It may be because this just hasn't come up. There's been a lot of discussion about what methods would be considered essential for the stdlib here in the forums that would be good to catch up on.

You can certainly propose to add it. I bet that it would make more sense to have a signature like this, though:

func removeFirst(where: (Element) -> Bool)

We already have this setup for removeAll(where:).

  1. Because it's not essential
  2. Because it's almost never useful.

To elaborate:

I've almost never found object identity to be a useful proxy for equality. It's "good" in so far it's never wrong (there are no false positives, an object can't be differently valued than itself), but it's also almost always incomplete and useless (there are plenty of false negatives, where equal-valued but distinct objects are considered non-equal).

I for one am glad that there isn't a default implementation, because having the compiler remind me to write a proper == is much more useful than it providing a useless default implementation.

I raise 2 questions in return:

  1. Why would object identity be the sensible default equality for objects?
  2. Whose to say the array has a particular object instance only once? Why wouldn't I removeAll(where:) instead?
1 Like

While I don't think this is particularly relevant to Swift, since it has actual value types, one answer to the first question is when the objects in question are instances of a closed class hierarchy with no mutable fields and are created via factory that never produces two instances of the same value.

I will reiterate that this would be an odd thing to have in a Swift program, but I could definitely see it happening in Java or Objective-C.

OK, so reading through the posts, a few things emerged. Firstly, people pointed to the removeAll function and thinking back to ObjC, the removeObject method actually implemented a removeAll semantics:

https://developer.apple.com/documentation/foundation/nsmutablearray/1410689-removeobject

So the stdlib function above should actually look like this:

extension Array where Element: AnyObject {
    mutating func remove(object: AnyObject) {
        removeAll(where: {$0 === object})
    }
}

Could also be named removeAllOccurenciesOf. Comparing the statement lengths, it does not appear a worthwhile addition. However, semantically you are staying in the language of arrays, making Swift more accessible. Also most of frameworks are still based on object-orientation, stuff like UIKit, SpriteKit, GameplayKit, CoreData and I just can't think of equality being anything else than identity there. Or maybe I am missing a point and you are removing your sprite from the scene by looking at its properties instead of just taking the reference...

SwiftUI is a different story and takes structs, functions and Swift to its full potential. But we are not there yet.

This comes up all the time. Not checking full equality, but removing elements by some of their properties.

Think of all the usecases of filter, but in a context where you're reassigning the result. a.remove(where: f) is similar (but more performant) than a = a.filter(f)

1 Like

I think you are missing something — you've given no reason why we should think that a default implementation of == should exist at all for classes in Swift. If there's no default, there isn't much reason to add a semantically ambiguous function to stdlib.

It's very much true that in the Obj-C ecosystem NSObject-derived classes have a default implementation of -[isEqual:] that uses pointer equality, and this of course is honored in Swift (via the Swift == operator). The fact that Swift does not provide this for non-Obj-C classes is an explicit design decision, not an accident.

That's why you'd have to justify why the design decision should be changed, if you thought Swift should behave like Obj-C in this case.

OK, I am slowly beginning to understand the whole point. But back to a concrete SpriteKit example: How would you cleanly and tersely delete a node from an array of SKNodes after receiving a reference to it from the atPoint method?

https://developer.apple.com/documentation/spritekit/sknode/accessing_and_modifying_the_node_tree

I don't want to remove nodes from SKNode children, I want to remove them from an array. Something like:

var selectedNodes = [node1, node2, node3]

    override func mouseDown(with event: NSEvent) {
        let position = event.location(in: self)
        
        let node = atPoint(position)
        selectedNodes.remove(node)

Are you sure that there is only a single copy of each node? Then you should use a Set<SKNode>, and you can then call remove. If you're not sure there's only one copy of each, then an array is appropriate, and you probably want to remove all copies using removeAll(where:).

Note that SKNode is a subclass of NSObject, and therefore uses identity as its definition of equivalence.

Thanks! It's just an example, so let's say ordering is important and we have to stay with array. Now, let's create our own entity class not inheriting from NSObject (and not based on GKEntity):

class GameScene: SKScene {
  var listOfZombies = [Zombie]()
  ...
}

class Zombie {
  var sprite: SKSpriteNode
}

func killZombie(atPosition position: CGPoint) {
  // find zombie object at position
  ...
  scene.listOfZombies.removeAll(where: {$0 === zombie})
  // vs.
  scene.listOfZombies.remove(zombie)
  // or
  scene.listOfZombies.removeAllOccurrenciesOf(zombie)

But you guys have pointed me to the correct direction: Apple OO-bases frameworks use NSObject, where equality IS based on identity.

I think that's one origin of my conceptual problem. What I should be actually looking for is not a general array (my bad), but an OrderedSet or an array without duplicates! However, I don't think this solves my problem of elegant removal of objects that do not inherit from NSObject / do not conform to Equatable, Hashable...

OK, now let's quickly summarize what I have learned so far:

  • Swift has done a tremendous job of migrating a ton of things from reference types to value types (strings, arrays, ...).
  • Swift arrays are based on Equatable, since this the best way to work with value types.
  • In Obj-C arrays were also based on equality, the sole reason probably being that even value types were implemented as objects (see NSString example above). This was semantically a bad decision and the reason why Swift is focusing on value types.
  • Supporting Obj-C ecosystem in Swift arrays was easy, just make NSObject conform to Equatable using isEqual.
  • My assumption: It is because of this legacy that objects are identified using Equatable instead of Identifiable, which is what I consider to now be a legacy itself.

However, looking into Swift once all the Obj-C clutter is gone and NSObject, NSString, NSArray legacy can be ignored, I would state the following for the rest for pure classes/objects:

  • Objects should mainly be identified via === (or more precise: use Identifiable instead of Equatable when finding objects in collections)
  • Don’t use classes for value bases types like Strings, Arrays, Structs etc.

Therefore: Extend the Array type to be based on Identifiable (instead of Equatable) for AnyObject elements.

I am including an early blog post that IMHO corroborates this statement:

Use a value type when:

  • Comparing instance data with == makes sense
  • You want copies to have independent state
  • The data will be used in code across multiple threads

Use a reference type (e.g. use a class) when:

  • Comparing instance identity with === makes sense
  • You want to create shared, mutable state

Perhaps I should go a step further and claim that objects SHOULD NOT be identified using Equatable in stdlib (except that NSObject stuff).

1 Like

If you personally think that objects should conform to Equatable using === then just conform your objects to Equatable using ===. It's going to be an uphill battle to get everyone to agree with you and change the default though.

You’re absolutely right. For me this is more about clarifying the underlying logic than about change. It’s just hard to understand that you cannot remove something identifiable from an array, whereby we have already clarified that identifiable is a special case of equatable.

Terms of Service

Privacy Policy

Cookie Policy