0

Consider this hypothetical Swift function:

func putFirst(_ string: String) {
    var str = string
    let c = str.popFirst()
    print(c)
}

I would expect, based on previous questions such as this one, that str is a variable and thus mutable. However, str.popFirst() throws a compile error,

Cannot use mutating member on immutable value: 'str' is immutable

Is this a subtlety I'm not aware of? Is it new behavior as of Swift 4? How do I work around it?

Violet
  • 183
  • 2
  • 7

1 Answers1

1

It's a useless error message; the problem is that there is only one popFirst() method defined on Collection. Here it is:

extension Collection where SubSequence == Self {
  /// Removes and returns the first element of the collection.
  ///
  /// - Returns: The first element of the collection if the collection is
  ///   not empty; otherwise, `nil`.
  ///
  /// - Complexity: O(1)
  @_inlineable
  public mutating func popFirst() -> Element? {
    // TODO: swift-3-indexing-model - review the following
    guard !isEmpty else { return nil }
    let element = first!
    self = self[index(after: startIndex)..<endIndex]
    return element
  }
}

You'll notice that it's constrained such that the Collection's SubSequence is itself. This is not true for String (as well as many other collection types such as Array and Set); but it is true for the slices of those types.

There's no unconstrained overload of popFirst() on RangeReplaceableCollection (which String conforms to). The reasoning for this, as given by Dave Abrahams in this bug report, is that popFirst() should be O(1); and an implementation on RangeReplaceableCollection cannot guarantee that (indeed for String, it is linear time).

Another good reason against this, as mentioned by Leo Dabus, is this is that popFirst() doesn't invalidate a collection's indices. An implementation on RRC wouldn't be able to guarantee this.

So therefore because of the wildly different semantics, it's quite reasonable not to expect an overload of popFirst() on RRC. You could always define a differently named convenience method on RRC for this though:

extension RangeReplaceableCollection {

  /// Removes and returns the first element of the collection.
  ///
  /// Calling this method may invalidate all saved indices of this
  /// collection. Do not rely on a previously stored index value after
  /// altering a collection with any operation that can change its length.
  ///
  /// - Returns: The first element of the collection if the collection is
  ///   not empty; otherwise, `nil`.
  ///
  /// - Complexity: O(n)
  public mutating func attemptRemoveFirst() -> Element? {
    return isEmpty ? nil : removeFirst()
  }
}

You would then say:

func putFirst(_ string: String) {
  var str = string
  let c = str.attemptRemoveFirst()
  print(c)
}
Hamish
  • 78,605
  • 19
  • 187
  • 280