`borrow` and `take` parameter ownership modifiers

Thinking about it a bit, if we say that a take parameter is bound to a var internally, then it shouldn't be immediately source-breaking to the implementation, since it wouldn't be able to modify the unannotated argument previously. That would make it so that removing take or changing it to borrow might break the implementation source if it modifies the taken parameter, necessitating an explicit local copy, but that change is at least localized to the implementation already being modified. So it may be a reasonable default to have it so that take parameters are internally mutable.

10 Likes

I guess the trick is going to be how to reconcile inout variable semantics with the new keyword so that mutability is somewhat similar.

SHIP IT.

5 Likes

Another reason we might not want to use the exact same keywords is user confusion. The difference between putting take on a parameter and take on an argument is a little tricky to explain, and using precisely the same word for both probably won’t help.

Ideally I think we might want to say something like takeable and borrowable on the parameters and then take and borrow on the arguments; using different but closely related words communicates which keywords match which, while the -able suffix roughly conveys the relationship between the parameter and the argument (expressing the capability vs. actually using it).

func foo(_: takeable Foo) { ... }

foo(take myFoo)

However, the specific words take and borrow read a little awkwardly with the -able suffix, so I’m not married to those exact choices.

5 Likes

Hm, I like that using the same word makes the connection explicit. The -able suffix, aside from opening an English spelling argument every time (is it "takable" or "takeable"), also doesn't seem appropriate once move-only values factor into the equation. When you take implicit copying away, the parameter not just can take the argument, it must.

5 Likes

I actually like using the same word just fine, but thinking this through, wouldn't the logical pairing be give/take rather than take/takeable?

2 Likes

I like own instead of take, so wouldn't this be own / borrow and ownable / borrowable or owned / borrowed? Personally I don't think the correctness of the grammar is the issue here, so a single set of modifiers would be fine to me.

4 Likes

And along these lines, if we don't want to use the same word, the other pair would naturally be "lend/borrow," which works out nicely.

7 Likes

replace inout with &mut
replace borrow with &

or replace inout with &
(& is already used to get ref to object and pass it as inout param into func call)
and replace borrow with &const

for e.g. instead of this
func test(_ arg1: inout Int, _ arg2: borrow Int)
will be this
func test(_ arg1: & Int, _ arg2: &const Int)

This leads to breaking change, so make the change in swift 6

This sure is confusing. On that discussion @beccadax wrote that move and __owned were not synonymous. Are you saying now that they are? This pitch wants to rename __owned to take and you want move to also be take? I’m so confused.

1 Like

Parameter Modifier:
mutable borrow (today inout)
immutable borrow ( today __owned)
mutable take (todo future)
immutable take (today __consuming)

Operator:
mutable borrow (&x ) // inout today
immutable borrow ( borrow x )
mutable take (todo future)
immutable take (take x) // previously proposed as move x

How does move fit in this picture?

take operator was previously proposed as move x

The mutable/immutable distinction that exists between inout and __owned/borrow has no equivalent for __consuming/take, and presenting it as some sort of matrix is not helpful. If you take ownership of a value, any mutations are local and unobservable, so whether or not they happen shouldn’t be part of the function’s type.

The question of whether take variables should be implicitly var, or reintroducing var-parameter syntax, is a minor quality-of-life issue that isn’t semantically important the way inout is.

5 Likes

Yes, you are right that pass-by-move (take) should not focused too much on immutable vs mutable axis since once something is move/take into a function then that value is consumed but the way I think about it is if we pass-by-move to a let parameter then that is immutable. If we pass-by-move to a var (implicit) parameter then that is mutable.

If I have full ownership of a value then I should be able to mutate it without rebinding it so I guess I am proposing implicit var for move/take.

They aren't synonymous per se, but you can look at them as opposite ends of the same ownership transfer process. Declaring a function with a take parameter like func foo(x: take T) makes it so that the function takes ownership of (possibly a copy of) the argument. Calling the function with foo(x: take x) ensures that the argument is taken without copying.

2 Likes

This implies an error if the function declares parameter x as borrow, right? The other possibility is that it ends the lifetime of variable x immediately after the call stops borrowing it, which I think is what move x would have done in the earlier proposal. I think the distinction is important. Call-side take is anchored at the call site and must align with the function declaration, whereas move can occur anywhere regardless of whether it makes sense in relation to the function call.

Any thought about operators? Can I do (take x)*(take y)?

Or auto-closures? Would assert(false, take x) make any sense to avoid making a copy of x? It could probably work the way move was defined, but not with take as an argument modifier. Am I correct?

Yes, that design is the most straightforward and consistent with the source compatibility goals. For copyable types, that will end up passing a copy of the variable to the callee. Some have objected to this behavior though because it probably wasn't the author's intention.

In the simplest interpretation, take just means that this expression takes ownership of the named variable. It works just as well with operators or as a stand-alone statement, which is an important use case. That's why "give" doesn't make sense. You're not naming a recipient.

1 Like

In the case of decorating a parameter at the call site, isn’t the caller giving to the callee that’s taking it?

//in the case of give/take
func foo(take x: Int) { … }

var bar = 0

foo(x: give bar)

You are giving bar to foo. That’s the recipient, right?

I feel like the give/take dichotomy (and the lend/borrow one) make a lot more sense. (Though again it makes inout and &x look really out of place.)

1 Like

How come you can conform to a protocol but use different take/borrow ownership than what the protocol says? You can't do that with the existing inout as far as I know.

1 Like