SE-0366 (second review): `take` operator to end the lifetime of a variable binding

Hello, Swift community!

The second review of SE-0366: take operator to end the lifetime of a variable binding begins now and runs through November 8th, 2022.

The changes to the first reviewed proposal include:

  • move is renamed to take.
  • Dropping a value without using it now requires an explicit _ = take x assignment again.
  • "Movable bindings" are referred to as "bindings with static lifetime", since this term is useful and relevant to other language features.
  • Additional "alternatives considered" raised during review and pitch discussions were added.
  • Expansion of "related directions" section contextualizes the take operator among other planned features for selective copy control.
  • Now that ownership modifiers for parameters are being pitched, this proposal ties into that one. Based on feedback during the first review, we have gone back to only allowing parameters to be used with the take operator if the parameter declaration is take or inout.

This re-review is running in parallel with SE-0377: borrow and take parameter ownership modifiers, which proposes the corresponding take and borrow parameter modifiers that will be used with the take operator at the callsite.

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" 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,

Holly Borla
Review Manager

12 Likes

The example code, I hink, is incomplete.

The example following this line:
“ Likewise, if the maintainer tries to access the original value parameter inside of doStuffUniquely after being taken to initialize newValue , they will get an error” shows no such error example.

The snippet following that has twice this line of code: ’useX(other)’
I think something else was intended here too?

The proposal doesn't explicitly address struct stored properties. The proposal specifies the following possible references for take:

  • a local let constant in the immediately-enclosing function,
  • a local var variable in the immediately-enclosing function,
  • one of the immediately-enclosing function's parameters, or
  • the self parameter in a mutating or __consuming method.

This leaves unspecified whether the ability to take self in a mutating method implies the ability to take a stored property of self. "Future Directions" implies by omission that it doesn't, by way of the forbidding of piecewise take, but it would be good to call this out explicitly in either direction.

Otherwise, aside from my continued dislike for _ = take x as a spelling for drop x, I think this proposal looks very reasonable, and I'd support its adoption.

2 Likes

Is the potential confusion here just that referencing a property with implicit self looks like a reference to a local variable which would otherwise be take-able? Otherwise it seems quite clear to me that a stored property of the enclosing type doesn't fall into any of the four listed categories.

The pitch seems to say that it's ok to do this:

xtension Optional {
  mutating func take() -> Wrapped {
    switch take self {
    case .some(let x):
      self = .none
      return x
    case .none:
      fatalError("trying to take from an empty Optional")
    }
  }
}

It seems therefore to be a bit unclear why I can't do this:

struct Foo {
    var x: Int
    var y: Int

    mutating func reset() -> Int {
        let x = take self.x
        self.x = 0
        return x
    }
}

Especially because I can do:

mutating func reset() -> Int {
    let old = take self
    self = Foo(x: 0, y: old.y)
    return old.x
    }

Awkwardly, there's a missing space for take old.y in the other spelling too, though in this case I'm not mutating y so the inability to take it is largely irrelevant.

1 Like

The first one is using take self to allow the switch to take the current value of self, in order to pull the payload out of it. The analogous thing in your second example would be to write (take self).x, to give up ownership of self's current value and allow the extracted value of x to be pulled out of the relenquished value (though that would in turn require you to reassign all of self afterward). As the "future directions" section notes, we could conceivably do elementwise tracking of stored properties in structs and tuples in the future, but don't propose to do that currently.

Gotcha. I think it might be useful for the pitch to call that constraint out explicitly, as right now I think understanding it requires reading between the lines a bit.

5 Likes

+1 This plus the borrow and take parameters proposal should take care of most of the cases where unnecessary CoW is a problem. I'm glad we went back to explicit discards.

Can't wait till we get to move-only types and an official version of _read/_modify.

Does this operation provide a solution to some compiler warnings about Sendable?

For example a non-sendable class might clearly have a problem being returned by reference from an Actor… but if it's the only copy, because we take it, maybe we can anyway?

Please make this a warning rather than an error, so that an upstream library deciding that borrow is better than take after all doesn’t break downstream clients.

(also, inout requires &; is this proposing to allow using inout with take to throw away the result?)

Not by itself, no.

You’re correct that, in principle, non-Sendable class instances can be safely transferred between concurrency domains in some cases. The conditions on that are that (1) the object needs to not be referenced elsewhere in the original domain and (2) the object needs to not itself reference anything else that’s still referenced by the original domain. Moving a single class reference doesn’t mean there aren’t other references to the object, and it doesn’t mean the object doesn’t share values with its current domain, so it doesn’t do much on its own; you need a higher-level concept of isolation.

I think you may be confusing parameters with arguments. I believe Holly is saying that a function cannot take out of one of its parameter if that parameter isn’t explicitly marked take.

2 Likes

Oops, indeed I did. Sorry for the noise!

Why was this restriction added? I might want to use take to end the lifetime of a variable binding from a function parameter, even if the calling convention isn't necessarily take WRT that parameter.

Just to ensure that it isn't being used later in the function, and to allow me to incrementally reason about how ownership should be transferred.

1 Like

I can't speak for the authors here, but while I think it makes sense to have a general lifetime-ending operator, it shouldn't be take, which (between this and the other proposal) signifies a transfer of value ownership.

Yeah, we're trying to strike a balance here for copyable types between allowing for calling conventions to evolve without unnecessary source breakage/conceptual leakage, and being able to provide firmer performance guarantees. When a function receives an argument by borrow, the callee can't practically shorten its lifetime, so allowing take could be false comfort for a developer expecting to get ownership forwarding from it. However, being able to use the take operator experimentally to see whether there's a benefit to making the function pass-by-take as @Karl describes sounds like a useful evolution path, so I'd be fine with having it be a warning only.

1 Like

Couldn’t it still be useful to use take to implement swap-like semantics?

struct Cell {
  init() { }
}
func dequeue(_ cell: borrow Cell) -> Int {
  let count = push(take cell)
  cell = .init()
  return count
}

cell would need to be passed inout to do that, in which case, it would work.

Ugh, right. I offer this as evidence that inout let would be a more self-explanatory spelling than borrow. :wink:

What you're describing is literally just inout. The function is mutating the variable that's passed in. The fact that it's mutating it by this specific take-and-reinitialize pattern doesn't really change anything in terms of the semantics in the caller vs. using the current value and then assigning a new value in normally.