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

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.

It also seems strange because I can pass a borrow function parameter to another function which accepts its parameter by take (because the compiler will insert an implicit copy), but I can't actually perform a take manually.

func someFunction<T>(_ x: /* implicit: borrow */ T) {

  takesArg(x) // ok, compiler inserts a copy

  let _ = take x // error: 'x' is not 'take'
}

func takesArg<T>(_ x: take T) {

  let _ = take x // ok...
}

I wonder if that's going to feel confusing to people.

1 Like

Would it be better to make this a warning rather than an error? It has no semantic effect at run time, and (if the compiler takes it literally as “produce an owned value”) a negative performance effect, but it doesn’t otherwise make the program invalid. That would be in line with similar warnings like “cast always succeeds”.

(The take operator does have a compile-time effect, that of ending the lifetime of the original name, but I agree it’s better not to allow that if it doesn’t come with the run-time effect of releasing the value.)

8 Likes

I'll provide a complete summary of the review outcome with the acceptance, but since this is right at the top: the Language Workgroup discussed this and we decided that we'd like more real-world experience with take/consume before considering downgrading this restriction. It's easy to remove restrictions later. It's not easy to add back a restriction later if we find that only producing a warning is harmful or confusing in practice.

1 Like

Thanks everyone for your feedback so far! I've kicked off another re-review of SE-0366 here:

Please provide further feedback on the new review thread. Thank you!

Holly Borla
Review Manager