Really confusing behavior of type inference with `consuming`

i had a function that looked like this:

{
    public consuming
    func response() -> HTTP.ServerResponse
    {
        let page:Page = { .init($0.batch, on: $0.date) } (self)
        return .ok(page.resource())
    }
}

why the funny closure expression? because Page.init(_:on:) sorts the buffer:

extension Page
{
    init(_ articles:consuming [Article], on date:Date)
    {
        articles.sort
        {
            $0.title < $1.title
        }

        self.init(articles: articles, date: date)
    }
}

i was skeptical that this would actually do what i expected it to do, so i tried to test my assumption by calling a property on self after constructing a Page from it.

{
    public consuming
    func response() -> HTTP.ServerResponse
    {
        let page:Page = { .init($0.batch, on: $0.date) } (self)
        let date:Date = self.date
        return .ok(page.resource())
    }
}

to my disappointment, this did compile, which indicated to me that the buffer was still being copied. instead, i needed to expand the closure to

        let page:Page =
        { 
            (self:consuming Self) in 
            .init(self.batch, on: self.date) 
        } (self)

to prevent the copy. (i do not know why this compiles, as it is consuming self twice, but somehow it does.)

i was going to post this in Using Swift, but the issue is not that i do not understand why the compiler copies the array. by default, the closure infers $0 to be borrowing Self, and this has a terrible interaction with the compiler-generated shim for init(_ articles:consuming [Article], on date:Date). you can also fool yourself into thinking you fixed the problem by adding consume self to the argument expression, but that will still copy due to the compiler-generated shim on the closure itself.

so the compiler really is following its own rules, they are just really confusing and inconvenient rules.

although i often feel like the ownership keywords are always doing the opposite of what i would like them to do, it is probably too late to change how they behave. so i think the problem now is that we lack a lightweight syntax to indicate that $0 is consuming, because we should not need to reformat the entire closure to use named arguments with explicit signatures to solve a COW issue.

5 Likes

Don't parameters of init default to consuming?

Did you try passing consume self to the closure parameter? Because otherwise I think it's going to default to passing copy Self unlesss your type is ~Copyable.

That's not quite the right way to think about it. consuming a value transfers ownership of that value. So in this case, calling x.response() transfers ownership of x to the self of response() which then transfers ownership of self to the closure parameter. This is all fine and dandy, but it does mean you should no longer have access to self after calling the closure.

they default to __owned, consuming just avoids the need to rebind the parameter to a local var inside the initializer.

i don’t think that affects the convention of the closure itself, i assumed that

let page:Page = { .init($0.batch, on: $0.date) } (consume self)

is the same thing as

let page:Page =
{ 
    (self:__shared Self) in 
    .init(self.batch, on: self.date) 
          ^~~~~~~~~~ 
//  will copy-on-write since `self` is alive for the duration of the 
//  closure’s execution
} (consume self)

i was talking about the two consecutive accesses to stored properties in the .init(self.batch, on: self.date) expression. accessing self.batch should have consumed self, making it impossible to access self.date to the right of it.

it ended up being because self.batch was actually choosing a borrowing overload introduced by a protocol conformance elsewhere in the code. i didn’t want that to happen, so i wound up rewriting the function to something like:

{
    public consuming
    func response() -> HTTP.ServerResponse
    {
        //  If we access `self.batch` directly, it dispatches through the protocol witness to
        //  avoid consuming `self`, so we need to use the closure to make `self` `borrowing`
        //  which will cause the compiler to choose the stored property accessor.
        let batch:[Article] = { $0.batch } (self)
        //  This consumes `self` because it is accessing a stored property that witnesses no
        //  protocol requirements.
        let date:Date = self.date
        let page:Page = .init(
            groups: .init(organizing: batch),
            date: date)

        return .ok(page.resource(format: format))
    }
}
1 Like

I was under the impression that consuming was the official version of __owned, and that being able to directly mutate the parameter was the only real difference between the unstable feature and the stable one. I had expected all the implicilty __owned parameters to act as consuming in compilers that support ownership since the lack of mutablity is a purely artificial constraint and not affected by ABI. It's really weird that they don't.

I wasn't expecting batch to be a consuming property. I had assumed it merely borrowed self, since AFAIK that's how properties work by default, even stored ones.

I'm no longer sure whether it effects the calling convention of the closure, but the consume operator absolutely ends the lifetime of the variable it's applied to, so the outer self will definitely not be alive during the closure. In fact, I don't think the compiler is required to end the lifetime of an implicitly copyable type passed as a consuming parameter unless you explicitly consume it. I'm pretty sure it's allowed to insert a copy first.

Using this function would probably work better than having to type out the proper closure every time:

func using<T, R>(_ resource: consuming T, operation: (inout T) throws -> R) rethrows -> R {
    try operation(&resource)
}

...

// This definitely doesn't copy self.
let page: Page = using(consume self) { .init($0.batch, on: $0.date) }

Although that's largely irrelevant if batch is a consuming property.

1 Like

__owned is solely about ARC convention, an __owned parameter still lives for the entire duration of the function’s execution, so if you mutate it inside the function it will always copy since the parameter itself is immutable and cannot be eagerly moved into something mutable.

2 Likes