Combined SE-0366 (third review) and SE-0377 (second review): rename `take`/`taking` to `consume`/`consuming`

Hello, Swift community!

The combined third review of SE-0366: consume operator to end the lifetime of a variable binding and second review of SE-0377: borrow and consume parameter ownership modifiers begins now and run through December 14th, 2022.

Throughout the reviews for SE-0366 and SE-0377, reviewers surfaced the concern that ‘take’ is too general a name to describe ownership transfer; ‘take’ is used colloquially to describe general passing of arguments to parameters, e.g. a function takes an argument of type X. The Language Workgroup agrees and recommends the term 'consume' instead of 'take', because it is a more specific term that has the same semantic meaning in the context of ownership transfer.

As such, the proposal authors have made the following amendments to SE-0366 and SE-0377:

  • The take operator is renamed to consume.
  • The take and taking parameter modifiers are renamed to consume and consuming, respectively.

This round of review is focused on the renaming of take/taking to consume/consuming. Ideas for terms other than 'consume' are also welcome, and please provide an explanation for why you believe a different term is a better choice.

Note that the Language Workgroup will not consider renaming existing keywords such as inout and mutating, because that would result in a wide-spread source breaking change for little benefit.


Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email or DM. When contacting the review manager directly, please keep the proposal link at the top of the message and put "SE-0366" or "SE-0377" in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you for your help in making Swift a better language.

Holly Borla
Review Manager

22 Likes

+1. I like consume much better. Casual heuristic reasoning based on the English meaning of the word is much more likely to lead to semantically correct conclusions about Swift’s actual behavior. The metaphor works.

In particular, this now makes intuitive sense:

_ = consume x
stuff(x)  // ERROR: x has been consumed

[Minor editorial note] It seems that the second half of both these comments is missing?

  // Pass the current value of x off to another function, that
  doStuffUniquely(with: consume x)

  // Reset x to a new value. Since we don't use the old value anymore,
  x = []
5 Likes

To quickly summarize, with this revision and some likely future pitches, here's the table for how we spell the three basic kinds of ownership use (using Rust's terms for them, which I don't entirely love):

                  | arguments + other uses | parameter modifier | method (self) modifier | variable keyword
                  +------------------------+--------------------+------------------------+---------------------------
owned transfer    |        consume         |     consume        |       consuming        |   var / let x = ...   [2]
immutable borrow  |        borrow          |     borrow     [3] |       borrowing [3][4] |   borrow x = ...      [1]
mutable borrow    |        &               |     inout          |       mutating         |   inout x = ...       [1]

[1] not yet formally proposed
[2] copies by default, but can be made a transfer by initializing with consume
[3] the default in most situations
[4] in some sense equivalent to nonmutating

The & / inout spellings for mutable borrows stand out as the most inconsistent here, and you could certainly argue that, if we were starting from scratch, they ought to be spelled mutate / mutate. But I agree with the authors that that is at the very least separable, and it probably isn't something worth pursuing given the long history of those spellings in Swift.

27 Likes

I find myself torn about the use of the imperative form for the parameter modifiers. Parameter modifiers are, like attributes, generally adjectival. We don't really keep to the pretense that a method declaration needs to "read like a sentence", but there is an argument for consistency between features.

For example, in the current proposal, a consumed parameter is written like this in the function declaration:

func collect(all widgets: consume [Widget])

Types usually read as descriptions of the parameter, but we wouldn't say that this is a "consume array of widgets", we'd say that it's a "consumed array of widgets". So that would argue for this:

func collect(all widgets: consumed [Widget])

Of course, that requires us to claim consumed and borrowed as keywords and use different keywords in different places based roughly on the rules of English grammar. That might be natural for programmers who are fluent in English, but it could be an obstacle for other people. On the other hand, we're already doing this elsewhere with consume vs. consuming.

An alternative would be to use the gerund for parameters, just like we do for methods:

func collect(all widgets: consuming [Widget])

This has the great merit of underlining the connection between these two placements, since the method modifier ultimately just behaves like a parameter modifier on self. And there's precedent for using gerunds here from @escaping. But it doesn't quite read right to me given the role of the ownership modifier. A parameter type is usually something that you ought to be able to read as a noun phrase that describes the argument value. "Consuming" doesn't describe the argument: it describes the relationship between the function and the argument.

( I know that some people have also argued against using the same keyword in these two places because we might want to be able to use both modifiers on functions simultaneously, in order to e.g. express the ownership relationship of first-class function values to their captured context (a sometimes important property in other languages, like Rust and C++). I don't think this is a very convincing argument, mostly because it would be a terrible idea to use such similar spellings for these different ideas — it directly invites confusion in both reading and writing code. If we find ourselves needing spellings for these properties on a function type, we probably ought to use names like @calledOnce (the effective behavior of a function that consumes its context) and @calledSequentially (the effective behavior of a function that requires a mutable reference to its context). )

I think this readability problem could potentially be resolved by moving the ownership modifier to the beginning of the clause. So you would write:

func collect(consuming all widgets: [Widget])

Of course, that would then be inconsistent with the placement of inout, which seems unfortunate in its own way. And I don't know what it would mean in function value types.

In favor of the current proposal, there's certainly an argument that we can side-step all this by just arbitrarily accepting the use of the imperative even if it's not totally consistent. Programmers will, of course, just learn whatever rule the language ends up specifying; no spelling is perfect, and inconsistency is a given.

Anyhow, I don't have a firm conclusion right now, but I wanted to put this out as food for thought.

23 Likes

You have "I don't know what it would mean in function value types" as a bit of a throwaway, but that was one of the reasons we moved inout after the colon in the first place. I don't think we should mess with that even if we can make the declaration "read" a little better to native English speakers. (Though I agree that it's a little clunky, so people should continue to bikeshed the word we use.)

6 Likes

consume/consuming is arguably better than take/taking for readability and understanding.
+1

I also have an affinity towards consumed/consuming over consume/consuming. For any position that isn’t a method modifier my personal inclination is to think about the keyword in terms of its relationship with the type, in which case I think consumed works best. This isn’t a strongly held opinion though by any means.

If this is a concern could it be helped by fixits correcting to the right form?

5 Likes

Yes, it would be very easy to correct the keyword with a fix-it.

1 Like

I only quickly scanned the proposal to find out what will happen to functions named consume as I do write some from time to time and there's one thing that stood out to me in the examples under the answer to my question.

consume x + y // Parses as (consume x) + y

As a reader I would expect this to be consume (x + y) not (consume x) + y.


Is it actually bad to burn the name and introduce a warning for functions called consume? We can simply say it's a warning in Swift 5.x.y, but an error in Swift 6 and require back ticks.

- func consume()
+ func `consume`()

Why do we want to special case these keyword(s)?

3 Likes

This has been already asked and answered:

1 Like

"Consuming" doesn't describe the argument: it describes the relationship between the function and the argument.

I'm not sure this holds true even now though? For example, inout doesn't describe the argument, it describes how the argument interacts with the function.


Also, the use of "consume" and "borrow" for the parameter modifier to my eyes reads very strangly when in the context of point free methods, e.g. this function:

struct Foo {
    consuming func thing(_ bar: consume Bar) -> Baz { ... }
}

has the signature: (consume Foo) -> (consume Bar) -> Baz which doesn't mirror the definition (consuming vs consume). These maybe aren't the strongest arguments, just my 2 cents.

1 Like

Does the not-yet-proposed inout x mean the following?

struct S { var x: Int = 0 }
struct T { var s = S() }

var t = T()
inout nestedProp = t.s.x
nestedProp = 1
t.s.x  // now 1

Yes, that would be the meaning.

Personally, I expect to use these modifiers a lot. Maybe not quite as much as std::move, but they will certainly be important tools when working on real-time graphics in Swift. So I’d be happy with shorter forms in which punctuation played a more prominent role.

Punctuation could also address @John_McCall’s points:

Defining & to mean “operate on this binding” (much like how $ means “operate through this property wrapper”) could make these feel more closely related.

My suggestion is to drop the imperatives entirely and embrace the dichotomy between expressions and declarations:

protocol Renderable {
  func appendDisplayList(to frame: inout Frame) // familiar
}

struct Renderer {
  func render(_ frame: inout(let) Frame) // f.k.a. `borrow Frame`
  func releaseResources(for frame: in Frame) // f.k.a. `consume Frame`
}

class World {
  var entities: [any Renderable]
  func draw(in renderer: Renderer) {
    let frame = Frame()
    for entity in entities {
      entity.appendDisplayList(to: &frame)
    }
    renderer.render(&frame)
    renderer.releaseResources(for: &frame)
    // could also do _ = &frame
}

Do we really need syntax to distinguish between lending and forfeiting at the call site? I don’t think it can be ambiguous as to which operation applies. Anyone familiar with inout parameters can immediately apprehend that &x operates on the binding named x, not the value held within it, and why render(&Frame()) doesn’t work.

2 Likes

Thanks, John. Given that, inout local vars would be really useful. I look forward to the proposal.

To your consum(e|er|ing|ed) naming question: looking at your table, my intuition is that terminology should be aligned along an lvalue/rvalue-flavored distinction betwen “expressions that provide values” (your first column) and “declarations of things that receive values” (the other three columns).

That raises the question of what lhs/rhs combinations like these are allowed and meaningful:

// Are these both allowed? Equivalent?
var x = borrow y
borrow x = y

// Which combinations like these are allowed?
borrow x = consume y
inout x = borrow y
// etc

// Why shouldn’t this be allowed?
consuming x = y

Terminology (including conjugation) should help elucidate all that.

1 Like

I'll second in here; I had the same thought independently. Not sure how I feel about inout(let) though.

1 Like

Another alternative would be to bring back var in parameters:

func appendDisplayList(to frame: inout Frame) // short for `inout var Frame`
func render(_ frame: inout let Frame) // override the implied `var`
func releaseResources(for frame: in var Frame) // no ABI effect; `frame` is mutable within function body
1 Like

Xiaodi wisely points out that this comment I posted in another review is at least as relevant here:

2 Likes

I’ll also follow up from there:

Maybe “lend(ing)” then makes sense at the usage site.

It would make the mental model clearer to me, than trying to use the same/similar word in two places - it gives clarity at the point of reading.

1 Like

This is what I was trying to get across, but your (actual) explanation is much much better. Using a verb makes it clearer (to me) what the function is doing with the instance. The alternative in my head is func ex(arg0: consumes Foo, arg1: borrows Bar) but I think the symmetry of ing is better.

3 Likes

Hello,
This all seems quite good and happy with the direction is going. I just had one question, I hope is alright asking it here, about the behaviour that is not clear to me after reading the proposal.

Do we have to use consume in a parameter on the calling side even if the declaration of the function already has the argument defined as consume? From a first read in the proposal I thought the answer was no, but then I'm not sure. Specially since inout needs & in the other side.

func doStuffUniquely(with value: consume [Int]) {
}

doStuffUniquely(with: consume x) // <-- is this consume needed?