Generalised @noescape


(Karl) #1

Hi

I had the need recently to mark a value-type as @noescape, but we only allow that for closure-types. I would like to propose that we allow any parameter to any function to be marked @noescape (by default, unlike closures, they would be potentially-escaping).

The standard library has several locations where this could be useful, for example Array’s withUnsafeBufferPointer() method:

let array = [1, 2, 3]
var ptr : UnsafeBufferPointer<Int>
array.withUnsafeBufferPointer { ptr = $0 }
ptr.count // has escaped the withUnsafeBufferPointer context

If we could mark the pointer as @noescape, the assignment to ptr would be a compile-time error.

My own (non-stdlib) case was rather complex: we had a class with a big data-table, and clients could asynchronously request data. They would get a callback inside a synchronised window during which they could access the data, but by that time they may have moved on and not care any more. So we decided the callback should tell the clients which data was available, and rather than pass the data itself, pass an accessor which they can use if they still care. Over time, we added more operations that they could perform within their little slice of exclusive access, and wrapped those accessors up in a struct (with a private initialiser). Unfortunately, it’s now no-longer a closure type, and we can’t annotate that the view object clients receive is only valid for the duration of the callback and should not be captured.

Something like this:

class Database<Value> {

    public func editRecord<T>(at index: RecordIndex, with block: (Record<Value>)->T) -> T {
      
            // Must be synchronised to the DB’s internal queue
            return dbQueue.sync {
              let view = Record(database: self, index: index)
              return block(view)
            }
    }

    fileprivate func _editValue(at index: RecordIndex, newValue: Value) {
      // precondition: on queue ‘dbQueue'
    }

    fileprivate func _deleteRecord(at index: RecordIndex) {
      // precondition: on queue ‘dbQueue'
    }
}

struct Record<Value> {
    private let database: DB
    private let index: RecordIdx

    public var value : Value {
        get { return database._getValue(at: index) }
        nonmutating set { database._deleteValue(at: index) }
    }
    public func delete() { database._deleteValue(at: index) }
}

// Usage example.

let recordIdx = database.queryRecord(...)
database.editRecord(at: recordIdx) {
    $0.value = “Hello!”
    // $0 (‘Record’) could escape this closure and be used to make non-synchronised edits
}

Maybe the new memory-model will solve these problems; I can’t find a single, clear proposal (that I can understand…)

Since it has standard-library consequences, it would be ABI-breaking.

- Karl