Can a mutating function be used while not actually escaping?

struct LazySplitIterator<Base: IteratorProtocol, Element> {
    var base: Base
    var remainingSplits: Int?
    let omitEmptySubsequences: Bool
    let isSeparator: (Base.Element) -> Bool
    let transform: (AnySequence<Base.Element>) -> Element
    private(set) var previousSeparator: Base.Element? = nil

    //...
}

extension LazySplitIterator: IteratorProtocol {
   mutating func next() -> Element? {
        //...
        return transform(AnySequence(AnyIterator {
            if let firstRawElement = first {
                first = nil
                return firstRawElement
            } else if let nextRawElement = self.base.next(), !self.isSeparator(nextRawElement) {
                // I haven't figured out this part yet.
            }
            //...
            return nil
        }))
        //...
    }
}

The problem is at the last else line. The error is "Closure cannot implicitly capture a mutating self parameter". I remember something about a function that converts between escaping and non-escaping functions. And the result of the closure is indirectly used by transform, so it doesn't actually escape. But I can't figure out how to properly invoke withoutActuallyEscaping(_: do:). Worse, one attempt flagged a warning that the behavior may be undefined and the use of a mutating function will be removed in a later version of Swift. So is there any way to do this?

As a reminder (mainly to myself), the invocation has to take in self as mutating and allow a call to Base.next.

I can get it to work using a helper function:

extension LazySplitIterator: IteratorProtocol {
    private func helper(_ x: () -> Base.Element?) -> Element {
        return withoutActuallyEscaping(x) {
            transform(AnySequence(AnyIterator($0)))
        }
    }

    mutating func next() -> Element? {
        //...
        return helper {
            if let firstRawElement = first {
                first = nil
                return firstRawElement
            } else if let nextRawElement = self.base.next(), !self.isSeparator(nextRawElement) {
                // I haven't figured out this part yet.
            }
            //...
            return nil
        }
        //...
    }
}

But I couldn't get it to work with the withoutActuallyEscaping appearing inline in next. Can you file a bug for that?

1 Like

What exactly did you try and what errors did you get?

I first tried a simplified sample iterator along with your helper method:

/// An iterator that vends the transformation of every 10 elements.
struct Map10ElementsIterator<Base: IteratorProtocol, Element>: IteratorProtocol {
    /// The wrapped iterator
    var base: Base
    /// The closure to map each subsequence to an output element
    let transform: (AnySequence<Base.Element>) -> Element

    mutating func next() -> Element? {
        func helper(_ body: () -> Base.Element?) -> Element {
            return withoutActuallyEscaping(body) {
                transform(AnySequence(AnyIterator($0)))
            }
        }

        guard let first = base.next() else { return nil }

        var remaining = 10
        return helper {
            guard remaining > 0 else { return nil }
            defer { remaining -= 1 }
            guard remaining < 10 else { return first }

            return self.base.next()
        }
    }
}

let hundred = Array(0..<100)
var iterator100 = Map10ElementsIterator(base: hundred.makeIterator(), transform: { Array($0) })
iterator100.base.next()  // 0
iterator100.next()  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
iterator100.base.next()  // 11
iterator100.next()  // [12, 13, 14, 15, 16, 17, 18, 19. 20. 21]
iterator100.base.next()  // 22

let five = Array(0..<5)
var iterator5 = Map10ElementsIterator(base: five.makeIterator(), transform: Array.init)
iterator5.base.next()  // 0
iterator5.next()  // [1, 2, 3, 4]
iterator5.base.next()  // nil
iterator5.next()  // nil

I'm on Xcode 10.1 with its default Swift.

Then I tried without a helper function:

mutating func next() -> Element? {
    guard let first = base.next() else { return nil }

    var remaining = 10
    return withoutActuallyEscaping({
        guard remaining > 0 else { return nil }
        defer { remaining -= 1 }
        guard remaining < 10 else { return first }

        return self.base.next()  // Error here
    }) { transform(AnySequence(AnyIterator($0))) }
}

And got an error underlined on the "s" in "self". The error was:

Closure cannot implicitly capture a mutating self parameter

The examples for withoutActuallyEscaping(_: do:) in its documentation used a passed-in closure for the non-escaping parameter. So I rearranged the code so the increment is in a distinct function:

mutating func next() -> Element? {
    guard let first = base.next() else { return nil }

    var remaining = 10

    func generate() -> Base.Element? {
        guard remaining > 0 else { return nil }
        defer { remaining -= 1 }
        guard remaining < 10 else { return first }

        return base.next()
    }

    return withoutActuallyEscaping(generate, do: { transform(AnySequence(AnyIterator($0))) })
}

And this passed. I was about to report on needing a named separately-defined argument for the first parameter of withoutActuallyEscaping(_: do:), but I tried out something on my actual iterator:

extension LazySplitIterator: IteratorProtocol {
    public mutating func next() -> Element? {
        switch remainingSplits {
        case .some(0):
            // Dump the remaining elements into the mapping function, whether they are separators or not.
            guard let first = base.next() else { return nil }

            let firstCollection = CollectionOfOne(first)
            var firstIterator = firstCollection.makeIterator()
            return withoutActuallyEscaping({ firstIterator.next() ?? self.base.next() }, do: { transform(AnySequence(AnyIterator($0))) })
        case .some(let limit):
            remainingSplits = .some(limit - 1)
            fallthrough
        case .none:
            // Read until the next separator, possibly skipping initial separators.
            return nil
        }
    }
}

The compiler accepted the call at the end of the .some(0) case. Is it the length of a defined-inline closure that caused the problem, not the fact the closure is defined inline at all?

If we're treating single-expression closures differently from non-single-expression closures here, that's also concerning. The local function case really should not work, since it has the same problems as the regular closure.

@Joe_Groff, any insights?

I agree that it's a bug if the first argument of withoutActuallyEscaping is being subject to the constraints of an escaping closure in any case, but the use in the local function is fine. Remember local functions do not form closures just by virtue of declaration; it's the use of an unapplied local function that creates the closure, and is subject to the constraints of the closure at the use site, so a local function can freely use its outer scope, and can be passed as a nonescaping function value, but can only be passed as an escaping function value if it doesn't capture inouts or nonescaping arguments of its outer scope.

1 Like

What's the difference between functions and closures, as far as the Swift compiler's model is concerned?

The closure is what you get when you form a value of function type, either by assigning to a variable or passing as an argument. A function declaration by itself doesn't have any representation as a value until you force it to. If you use a function immediately as part of a function call, including a closure literal, then no value is really formed at all, and the function is called directly with its captured environment passed directly as arguments.

1 Like

SR-9743: "withoutActuallyEscaping(_: do:) doesn't work when the first argument is defined inline and is more than one expression"

I don't know if it's fully readable. The text entry fields can't actually accept code! You can fake it, but pasting Swift code is broken because the curly braces are significant to the entry fields! If someone can fix it, I took all the code from my first reply above.

Terms of Service

Privacy Policy

Cookie Policy