Assign if different

We don't have "assign if different" in Swift other than writing it explicitly:

if a != b {
    a = b
}

Which is not ideal and getting worse in case of complex expressions:

let b = expression(x, y, z)
if foo[123].bar[baz] != b {
    foo[123].bar[baz] = b
}

Assigning the same value may be benign but in general case it can cause side effects (like didSet) and might be unwanted if the value is unchanged.

I wonder if there's a precedent of "assign if different" operator in other languages?
Is there some tradition for it established in third party libraries?
Would there be an interest to have it built into Swift or is it infrequently used case to bother optimising?

Bike shedding (hope there's something nicer):

a =!= b
a.assignIfDifferent(b)
2 Likes

Can you give some real worked examples of where you've wanted this operation?

Some examples:

  @IBInspectable public var firstColor: UIColor = .white {
    didSet { updateGradient() }
  }
  
  @IBInspectable public var middleColor: UIColor? {
    didSet { updateGradient() }
  }
  
  @IBInspectable public var lastColor: UIColor = .black {
    didSet { updateGradient() }
  }

// didSet here leads to unneeded side effects if the same color is assigned

Similar examples:

public var textAndTrailingImageSpacing: CGFloat = 8 {
  didSet { textAndTrailingImageSpacingConstraint?.constant = textAndTrailingImageSpacing }
}

open override var isEnabled: Bool {
  didSet {
    if oldValue != isEnabled { updateAppearance() }
  }
}

public var cornerRadius: CGFloat = 0 {
  didSet { updateCorners(cornerRadius: cornerRadius) }
}

public var minSize = CGSize(width: 16, height: 16) {
  didSet { invalidateIntrinsicContentSize() }
}

private var items: [String] = [] {
  didSet { collectionView.reloadData()  }
}

private var currentSelectedItemIndex: Int = 0 {
  didSet {
    if let points = model[uncheckedIndex: currentSelectedItemIndex]?.pointsForReview {
      subtitleLabel.setText("Text", animated: true) // unwanted animation
    }
  }
}

private var currentIndex: Int = 0 {
  // heavy operation
  didSet { imageView.image = images[uncheckedIndex: currentIndex].grayscale()  }
}

public var color: Color {
  didSet { hasUpdate = true }
}

Non UI examples:

var phoneString: String? {
  didSet {
    phone = validated(ctnString: phoneString)
  }
}

private(set) var state: State = .isUploading {
  didSet { didChangeStateObserver(state) }
}

private var pages: [TutorialPage] = [] {
  didSet { pages.forEach { $0.pageListener = self } }
}

private(set) var count: Int = 0 {
  didSet {
    Log("Count didSet with value: \(count)")
      .category(.database).level(.info)
    if count != oldValue {
      onObjectsDidChange()
    }
    guard count > limit else { return }
    do {
      try cleanUp()
    } catch {
      Log("Unable to remove first element")
        .category(.database).level(.error)
    }
  }
}

var userUUID: String? {
  didSet {
    configuration?.previousUserUUID = userUUID
    onDidChange?()
  }
}

var token: String? {
  didSet {
    tokenSaveDate = Date()
    onDidChange?()
  }
}

weak var delegate: MyDelegate? {
  didSet {
    guard let error = initialError else { return }
    delegate?.logError(with: self, error: error)
  }
}
1 Like

What about instead having willChange/didChange observers? Applicable only when property type confirms to Equatable.

1 Like

My first inclination is that for an example like this, I would much rather see the logic to avoid the update in didSet, rather than where the value is being set:

  didSet {
    if val != oldValue { updateGradient() }
  }
9 Likes

Yes, I totally agree here. The examples I've provided are a bit misleading, didSet is not very good example. What I want to say is that:

  1. sometimes applying the same value can cause expensive or heavy side effects
  2. in perfomance critical code reassigning the same value to dictionary leads to hash calculation which may be unwanted
1 Like

It's not always practical to do it on the "inside" (e.g. willSet / didSet in parent classes, all places need to be modified), or possible (e.g. setting a @Published var property). Plus the above example still stands:

let card = modifiedCard(location: recognizer.location(ofTouch: touchIndex, in: view))
if boards[boardIndex].cards[cardIndex] != card {
    boards[boardIndex].cards[cardIndex] = card
}

vs some new:

boards[boardIndex].cards[cardIndex] =!= modifiedCard(location: recognizer.location(ofTouch: touchIndex, in: view))
1 Like

Not about != itself, but I had similar idea for < and >.

if a < b {
    a = b
}

Using max, I can write this. But it still contains two a expression. That's sometimes too verbose in real situation.

a = max(a, b)

I want something like

a max= b
1 Like

Not being able to have more than two arguments with operators makes me think a function is best. Tuples as function arguments is too terrible.

public func assignOutput<Value>(
  of makeValue: (Value, Value) -> Value,
  with newValue: Value,
  to value: inout Value
) {
  value = makeValue(value, newValue)
}

var a = 1
assignOutput(of: max, with: 2, to: &a)
a // 2
assign(2, to: &a, if: <)
public func assign<Value>(
  _ newValue: Value,
  to value: inout Value,
  if predicate: (Value, Value) -> Bool
) {
  if predicate(value, newValue) {
    value = newValue
  }
}

infix operator =!=: AssignmentPrecedence

public func =!= <Value: Equatable>(value: inout Value, newValue: Value) {
  assign(newValue, to: &value, if: !=)
}
5 Likes

Interesting use-case. How about this?

func assign<T: Equatable>(_ a: inout T, _ b: T, `if` condition: (T, T) -> Bool) {
    if condition(a, b) {
        a = b
    }
}

var a = 4
let b = 2
assign(&a, b, if: <)

ditto for !=

Heh, just realised @anon9791410 already suggested this.

Binding an inout to storage counts as a mutation of it even if you don’t actually assign into it. That means didSet will run.

Among other things, that means this operator would not be doable in the standard library; it would need special language support.

14 Likes

Not supporting your pitch, though you can add as 3rd point: properties' observers are usually on the edge of "side effect" smell

1 Like

What does that mean?

How would this new operator avoid the hash calculation? Wouldn't it still need the hash in order to perform the lookup to see if there were a difference? If there are heavy side-effects, I'd rather see that closer to the side-effects rather than the assigner having to know those internal details.

Most of the time it is not expected (what is the definition of side-effect) to do something, when you get/set property's value. So doing something in willSet, didSet in most of the situations produces side-effected and inconsequential code. So if another programmer would like to modify your code he should learn first what you do in these observers (probably he will learn it seeing unexpected behaviour of the final program).

P.S. It makes it inconsequential because modifying properties in function bodies you should scroll up and see the block, that will be executed after getting/assigning value

One case where I've wanted to do something like this was when using Core Data. Assigning any value to a modeled property, even if it's the same value, results in the object being marked "dirty", and thus means that it will be included in the next save transaction. That results in more I/O traffic and higher memory usage, so we try to avoid re-assigning an equal value when possible.

I've written this helper, and it works well for me:

extension NSManagedObject {
    public func updateIfNotEqual<T: Equatable>(
        _ path: ReferenceWritableKeyPath<Self, T>, 
        _ newValue: T) 
    {
        if self[keyPath: path] != newValue {
            self[keyPath: path] = newValue
        }
    }
}

// Used like this:
person.updateIfNotEqual(\.name, updatedName)
6 Likes