[Accepted] SE-0437: Generalizing standard library primitives for non-copyable types

Hello, Swift community.

The review of SE-0437: Generalizing standard library primitives for non-copyable types ran from May 22nd through June 4th, 2024. There were several questions about the proposal, which were answered in the thread. Otherwise, feedback was light but uniformly positive. The Language Steering Group agrees with the community that this proposal lays out the right basic shape for these primitive operations.

One reviewer suggested that, since the proposal touches withExtendedLifetime, it should take the opportunity to improve it for common use patterns. The Language Steering Group feels this ought to be done as a separate proposal since it has broader applicability and is not particularly tied to supporting non-copyability, and because the naming of the new APIs is not completely obvious.

SE-0437 is therefore accepted.

I'd like to thank the community for participating in this review. Review feedback is an important contribution to the project, even when it's just a question or a positive note.

John McCall
Review Manager

17 Likes

I realize this is late to ask, but I remember that the Optional.take method was heavily discussed in the review of an earlier proposal that was just focused on Optional. Were the concerns brought up back then considered in the acceptance of this proposal?

Could you give me a reference? I may have overlooked them from earlier pitches. We didn't spent much time in the LSG talking about Optional.take specifically, no.

It was in this thread.

Hello, folks. Karoy has asked to make a small revision to this proposal. The patch is small and largely editorial in nature, such as the changes bringing the document in line with the accepted syntax for conformances from SE-0427. The only substantive change is the addition of func extracting(_ bounds: UnboundedRange) -> Self to Unsafe*BufferPointer, which is a trivial addition for consistency with other range-taking operations; this permits the use of ... as the argument to extracting. The LSG has elected to accept this revision without a formal extension of the review. However, if anyone has any significant concerns about this revision, you are welcome to bring them up here.

John McCall
Review Manager

5 Likes

Yes. Was there a concern in particular you feel wasn't addressed? I see concerns about take() being a special case that isn't otherwise expressible in the language, which I believe is not true as of SE-0427. I also see a question about whether take() is actually necessary vs. using consume, which seems to have been answered in that thread.

I believe the primary concerns were that:

  • it will be unnecessary/redundant due to local mutating bindings and the swap/replace/exchange function,
  • it introduces a precedent of types having implicit default values, and,
  • its name does not make it obvious what it does.

As for my personal opinion, other constructs involving optionals tend to either be specific to optionals, or explicitly spell out the nil case.[1] For example, to check if an optional has a value, we use value != nil, not a cast to Bool or a hasValue() method. And when we want to reset an optional back to nil, we use value = nil.

This has the benefit of making it clear at the call site that the value is, in fact, optional, which is important because we generally use the same names for optional values as non-optional values (for example, books and not optionalBooks). books.take() is ambiguous depending on whether or not books is optional. If we add take methods to other types in the future, then books.take() could be, say, equivalent to exchange(&books, with: []) if books is of a non-optional array type.

I think it would be better if we stuck with exchange(&value, with: nil). Just like how we don't have a value.reset() method that's equivalent to value = nil, because = is the way to set values; and we don't have a value.hasValue() method, because ==/!= is the way to compare values; we could recommend that people use exchange(&value, with: nil), because exchange will be the way to set a value while getting the old one back.


  1. The only exceptions I can think of are the map and flatMap methods, but those are consistent with other monadic types. take has no precedent in Swift currently. ↩ī¸Ž

Since the LSG did not specifically talk much about Optional.take in our discussions, I will put this on our agenda to verify that we're happy with the design. In this case, I suspect we are, but it's good to check, and there may still be time to revise it if we decide we want to. In general, though, these threads are not left open to serve as an eternal extension of the review; there were plenty of opportunities for you to express your opinion about Optional.take during the normal pitch and review process.

I will note that Optional has always, by design, had a special relationship to nil as representing an absent value. Indeed, you say that take "introduces a precedent of types having implicit default values", apparently forgetting that Optional already does, uniquely, have an implicit default value of nil when e.g. declaring a local var of type T?. I don't personally expect that we'll ever add take to other types, precisely because of this unique role of Optional as the type that abstractly represents the presence or absence of a value.

9 Likes

The advent of the special syntactic form consume has rendered all three of swap, exchange and Optional.take entirely redundant, as these functions are all "trivial" helpers consisting of a few consume invocations.

func swap<T: ~Copyable>(_ a: inout T, _ b: inout T) {
  let temp = consume a
  a = consume b
  b = consume temp
}

func exchange<T: ~Copyable>(
  _ item: inout T,
  with newValue: consuming T
) -> T {
  let oldValue = consume item
  item = consume newValue
  return oldValue
}

extension Optional where Wrapped: ~Copyable {
  mutating func take() -> Self {
    let result = consume self
    self = nil
    return result
  }
}

These functions do not contribute anything that we wouldn't otherwise be able to express. From a strict, soulless technical standpoint, these are all completely redundant. This does not make these functions outliers, though -- the majority of preexisting functionality in the Standard Library is also redundant in this sense. For example, your favorite notational examples if foo == nil and foo = nil are also entirely redundant, as one could "simply" choose to spell them if case .none = foo and foo = .none, respectively.

However, this does not at all mean that these features are "unnecessary". Quite the opposite! These APIs give proper names to frequently occurring patterns: they establish a vocabulary for Swift developers, freeing them from the need to repeatedly puzzle out the underlying simple intention.

The Standard Library going out of its way to implement these redundant notations (and SE-0437 taking pains to make sure they carry over to this new age of noncopyable types) is not at all a waste -- it gives the language its expressivity and character. It is crucial that we provide a great selection of utility operations: these helpers are lubricants for the gears of coding.

Optional.take() gives a name to a frequently needed operation. take is not just a good name for this function -- it is a great one! Not only does it capture exactly what this operation does, but it does so in a wonderfully succinct manner that flows great in code. As an additional bonus, it matches the terminology used by our friendly competitor / role model Rust.

As John explained, take does not introduce the idea of a type with an "implicit default value" -- Optional has had an implicit default nil value since the start.

The page has now turned on this proposal. The review period is over; the changes have landed, and work has now progressed to building new things on top of them. If we discover a major issue with what was proposed, we may of course need to revisit it. This is, however, not the right time to start bikeshedding accepted API additions -- there is a huge amount of work that SE-0437 has left undone, and for the language to progress, we need to look forward, not back.

20 Likes