Shouldn't ".withUnsafeBufferPointer" and ".withUnsafeMutableBufferPointer" be parts of protocols?


(Daryle Walker) #1

I was perusing the library for array ideas, and noticed that several types had methods in common, but without a grouping protocol. Shouldn’t that be fixed?

(Oh, if multiple protocols have an associated-type with the same name, is that a problem? Is it a problem if they resolve differently for each protocol attached to a given type? I’m asking because these protocols reuse Sequence’s Element for their own purpose.)

Formal Protocol for Contiguous Storage Visitation
Proposal: SE-NNNN <file:///Users/daryle/Documents/NNNN-filename.md>
Authors: Daryle Walker <https://github.com/CTMacUser>
Review Manager: TBD
Status: Awaiting review
During the review process, add the following fields as needed:

Decision Notes: Rationale <https://lists.swift.org/pipermail/swift-evolution/>, Additional Commentary <https://lists.swift.org/pipermail/swift-evolution/>
Bugs: SR-NNNN <https://bugs.swift.org/browse/SR-NNNN>, SR-MMMM <https://bugs.swift.org/browse/SR-MMMM>
Previous Revision: 1 <https://github.com/apple/swift-evolution/blob/...commit-ID.../proposals/NNNN-filename.md>
Previous Proposal: SE-XXXX <file:///Users/daryle/Documents/XXXX-filename.md>
Introduction
The standard library types Array, ArraySlice, and ContiguousArray have an interface for visiting their elements as a contiguous block of memory (arranging said elements to that configuration first if necessary). These methods are all the same, but not under a common protocol (i.e. seeming to match by "coincidence").

This proposal seeks to correct that with two new protocols that these types will implement.

Swift-evolution thread: Discussion thread topic for that proposal <https://lists.swift.org/pipermail/swift-evolution/>
Motivation
Just adding these protocols for consistency is relatively minor, but they may be used for other types. Particularly, they would be needed if fixed-sized arrays are added to the language.

Proposed solution
The library array types will follow the MutableContiguousBlockprotocol, which inherits from the ContiguousBlock protocol.

extension Array: MutableContiguousBlock {
}

extension ArraySlice: MutableContiguousBlock {
}

extension ContiguousArray: MutableContiguousBlock {
}
For example, a repackaging of the library's example code:

func change<T: MutableContiguousBlock>(object: inout T) -> T.Element where T.Element: Integer {
    let sum = object.withUnsafeBufferPointer { (buffer) -> T.Element in
        var result: T.Element = 0
        for i in stride(from: buffer.startIndex, to: buffer.endIndex, by: 2) {
            result += buffer[i]
        }
        return result
    }
    object.withUnsafeMutableBufferPointer { buffer in
        for j in stride(from: buffer.startIndex, to: buffer.endIndex - 1, by: 2) {
            swap(&buffer[j], &buffer[j + 1])
        }
    }
    return sum
}

var numbers = [1, 2, 3, 4, 5]
print(change(object: &numbers)) // 9
print(numbers) // [2, 1, 4, 3, 5]
Detailed design
/**
    Visitation protocol of the receiver's contiguous storage of immutable elements.
*/
protocol ContiguousBlock {

    /// Inferred alias to the element type to visit
    associatedtype Element

    /**
        Calls a closure with a pointer to the receiver's contiguous storage. If no such storage exists, it is first created.

        Often, the optimizer can eliminate bounds checks within an array algorithm, but when that fails, invoking the same algorithm on the buffer pointer passed into your closure lets you trade safety for speed.

        The following example shows how you can iterate over the contents of the buffer pointer:

            let numbers = [1, 2, 3, 4, 5]
            let sum = numbers.withUnsafeBufferPointer { buffer -> Int in
                var result = 0
                for i in stride(from: buffer.startIndex, to: buffer.endIndex, by: 2) {
                    result += buffer[i]
                }
                return result
            }
            // 'sum' == 9

        - Parameter body: A closure with an `UnsafeBufferPointer` parameter that points to the contiguous storage for the receiver. If `body` has a return value, it is used as the return value for this method. The pointer argument is valid only for the duration of the closure's execution.

        - Returns: The return value of the `body` closure parameter, if any.

        - SeeAlso: Swift.UnsafeBufferPointer
     */
    func withUnsafeBufferPointer<R>(_ body: (UnsafeBufferPointer<Element>) throws -> R) rethrows -> R

}

/**
    Visitation protocol of the receiver's contiguous storage of elements, allowing mutation.
*/
protocol MutableContiguousBlock: ContiguousBlock {

    /**
        Calls the given closure with a pointer to the receiver's mutable contiguous storage. If no such storage exists, it is first created.

        Often, the optimizer can eliminate bounds checks within an array algorithm, but when that fails, invoking the same algorithm on the buffer pointer passed into your closure lets you trade safety for speed.

        The following example shows modifying the contents of the `UnsafeMutableBufferPointer` argument to `body` alters the contents of the receiver:

            var numbers = [1, 2, 3, 4, 5]
            numbers.withUnsafeMutableBufferPointer { buffer in
                for i in stride(from: buffer.startIndex, to: buffer.endIndex - 1, by: 2) {
                    swap(&buffer[i], &buffer[i + 1])
                }
            }
            print(numbers)
            // Prints "[2, 1, 4, 3, 5]"

        - Warning: Do not rely on anything about `self` (the receiver of this method) during the execution of the `body` closure: It may not appear to have its correct value. Instead, use only the `UnsafeMutableBufferPointer` argument to `body`.

        - Parameter body: A closure with an `UnsafeMutableBufferPointer` parameter that points to the contiguous storage for the receiver. If `body` has a return value, it is used as the return value for this method. The pointer argument is valid only for the duration of the closure's execution.

        - Returns: The return value of the `body` closure parameter, if any.

        - SeeAlso: withUnsafeBufferPointer, Swift.UnsafeMutableBufferPointer
     */
    mutating func withUnsafeMutableBufferPointer<R>(_ body: (inout UnsafeMutableBufferPointer<Element>) throws -> R) rethrows -> R

}
I don't know how the 3 library types implement this methods, but we know it can be done. If added as an implicit interface to built-in fixed-sized arrays, the implementers can use similar techniques or compiler magic. I don't know the difficulty to adapt these interfaces with third-party code.

Source compatibility
The changes are strictly additive, and at the library level, so there should be no impact to existing code.

Effect on ABI stability
These changes should not affect ABI stability.

Effect on API resilience
As just stated, the addition of two protocol names to the API shouldn't affect the ABI.

Alternatives considered
The main alternative is to do nothing. If fixed-sized arrays are later added, they could have the same methods too, but there would still be no common link.

Another alternative would make MutableContiguousBlock not inherit from ContiguousBlock, meaning the former would need to define an Element associated type. But don't see a need for a protocol with a read-write method without its read-only counterpart available.

···


Daryle Walker
Mac, Internet, and Video Game Junkie
darylew AT mac DOT com


(Zachary Waldowski) #2

I like the sound of it so far, but my first major thought is that isn't
it modeling a "has-a" relationship instead of an "is-a"? The buffer
methods indicate that the data type *can* be represented as a buffer for
the duration of the method call, but may not necessarily be before or
after. Such a semantic distinction would also allow Data and
DispatchData, as well as other theoretical data structures like a Deque
(I think?) to participate.

Cheers!

  Zachary Waldowski

  zach@waldowski.me

I was perusing the library for array ideas, and noticed that several
types had methods in common, but without a grouping protocol.
Shouldn’t that be fixed?

(Oh, if multiple protocols have an associated-type with the same name,
is that a problem? Is it a problem if they resolve differently for
each protocol attached to a given type? I’m asking because these
protocols reuse Sequence’s Element for their own purpose.)

Formal Protocol for Contiguous Storage Visitation

* Proposal: SE-NNNN
* Authors: Daryle Walker[1]
* Review Manager: TBD
* Status: *Awaiting review*
*During the review process, add the following fields as needed:*

* Decision Notes: Rationale[2], Additional Commentary[3]
* Bugs: SR-NNNN[4], SR-MMMM[5]
* Previous Revision: 1[6]
* Previous Proposal: SE-XXXX
Introduction

The standard library types Array, ArraySlice, and ContiguousArray have
an interface for visiting their elements as a contiguous block of
memory (arranging said elements to that configuration first if
necessary). These methods are all the same, but not under a common
protocol (i.e. seeming to match by "coincidence").
This proposal seeks to correct that with two new protocols that these
types will implement.
Swift-evolution thread: Discussion thread topic for that proposal[7]
Motivation

Just adding these protocols for consistency is relatively minor, but
they may be used for other types. Particularly, they would be needed
if fixed-sized arrays are added to the language.
Proposed solution

The library array types will follow the
MutableContiguousBlockprotocol, which inherits from the
ContiguousBlock protocol.
extension Array: MutableContiguousBlock { } extension ArraySlice:
MutableContiguousBlock { } extension ContiguousArray:
MutableContiguousBlock { }
For example, a repackaging of the library's example code:

func change<T: MutableContiguousBlock>(object: inout T) -> T.Element
where T.Element: Integer { let sum = object.withUnsafeBufferPointer {
(buffer) -> T.Element in var result: T.Element = 0 for i in
stride(from: buffer.startIndex, to: buffer.endIndex, by: 2) { result
+= buffer[i] } return result } object.withUnsafeMutableBufferPointer {
buffer in for j in stride(from: buffer.startIndex, to: buffer.endIndex
- 1, by: 2) { swap(&buffer[j], &buffer[j + 1]) } } return sum } var
numbers = [1, 2, 3, 4, 5] print(change(object: &numbers)) // 9
print(numbers) // [2, 1, 4, 3, 5]
Detailed design

/** Visitation protocol of the receiver's contiguous storage of
immutable elements. */ protocol ContiguousBlock { /// Inferred alias
to the element type to visit associatedtype Element /** Calls a
closure with a pointer to the receiver's contiguous storage. If no
such storage exists, it is first created. Often, the optimizer can
eliminate bounds checks within an array algorithm, but when that
fails, invoking the same algorithm on the buffer pointer passed into
your closure lets you trade safety for speed. The following example
shows how you can iterate over the contents of the buffer pointer:
let numbers = [1, 2, 3, 4, 5] let sum =
numbers.withUnsafeBufferPointer { buffer -> Int in var result = 0 for
i in stride(from: buffer.startIndex, to: buffer.endIndex, by: 2) {
result += buffer[i] } return result } // 'sum' == 9 - Parameter body:
A closure with an `UnsafeBufferPointer` parameter that points to the
contiguous storage for the receiver. If `body` has a return value, it
is used as the return value for this method. The pointer argument is
valid only for the duration of the closure's execution. - Returns:
The return value of the `body` closure parameter, if any. - SeeAlso:
Swift.UnsafeBufferPointer */ func withUnsafeBufferPointer<R>(_ body:
(UnsafeBufferPointer<Element>) throws -> R) rethrows -> R } /**
Visitation protocol of the receiver's contiguous storage of elements,
allowing mutation. */ protocol MutableContiguousBlock: ContiguousBlock
{ /** Calls the given closure with a pointer to the receiver's
mutable contiguous storage. If no such storage exists, it is first
created. Often, the optimizer can eliminate bounds checks within an
array algorithm, but when that fails, invoking the same algorithm on
the buffer pointer passed into your closure lets you trade safety for
speed. The following example shows modifying the contents of the
`UnsafeMutableBufferPointer` argument to `body` alters the contents of
the receiver: var numbers = [1, 2, 3, 4, 5]
numbers.withUnsafeMutableBufferPointer { buffer in for i in
stride(from: buffer.startIndex, to: buffer.endIndex - 1, by: 2) {
swap(&buffer[i], &buffer[i + 1]) } } print(numbers) // Prints "[2, 1,
4, 3, 5]" - Warning: Do not rely on anything about `self` (the
receiver of this method) during the execution of the `body` closure:
It may not appear to have its correct value. Instead, use only the
`UnsafeMutableBufferPointer` argument to `body`. - Parameter body: A
closure with an `UnsafeMutableBufferPointer` parameter that points to
the contiguous storage for the receiver. If `body` has a return value,
it is used as the return value for this method. The pointer argument
is valid only for the duration of the closure's execution. - Returns:
The return value of the `body` closure parameter, if any. - SeeAlso:
withUnsafeBufferPointer, Swift.UnsafeMutableBufferPointer */ mutating
func withUnsafeMutableBufferPointer<R>(_ body: (inout
UnsafeMutableBufferPointer<Element>) throws -> R) rethrows -> R }
I don't know how the 3 library types implement this methods, but we
know it can be done. If added as an implicit interface to built-in fixed-
sized arrays, the implementers can use similar techniques or compiler
magic. I don't know the difficulty to adapt these interfaces with third-
party code.
Source compatibility

The changes are strictly additive, and at the library level, so there
should be no impact to existing code.
Effect on ABI stability

These changes should not affect ABI stability.

Effect on API resilience

As just stated, the addition of two protocol names to the API
shouldn't affect the ABI.
Alternatives considered

The main alternative is to do nothing. If fixed-sized arrays are later
added, they could have the same methods too, but there would still be
no common link.
Another alternative would make MutableContiguousBlock not inherit from
ContiguousBlock, meaning the former would need to define an Element
associated type. But don't see a need for a protocol with a read-write
method without its read-only counterpart available.

Daryle Walker

Mac, Internet, and Video Game Junkie

darylew AT mac DOT com

_________________________________________________

swift-evolution mailing list

swift-evolution@swift.org

https://lists.swift.org/mailman/listinfo/swift-evolution

Links:

  1. https://github.com/CTMacUser
  2. https://lists.swift.org/pipermail/swift-evolution/
  3. https://lists.swift.org/pipermail/swift-evolution/
  4. https://bugs.swift.org/browse/SR-NNNN
  5. https://bugs.swift.org/browse/SR-MMMM
  6. https://github.com/apple/swift-evolution/blob/...commit-ID.../proposals/NNNN-filename.md
  7. https://lists.swift.org/pipermail/swift-evolution/

···

On Fri, Jan 27, 2017, at 06:40 PM, Daryle Walker via swift-evolution wrote:


(Karl) #3

There is a bug tracking this:

https://bugs.swift.org/browse/SR-3631


(Dave Abrahams) #4

I like the sound of it so far, but my first major thought is that isn't
it modeling a "has-a" relationship instead of an "is-a"? The buffer
methods indicate that the data type *can* be represented as a buffer for
the duration of the method call, but may not necessarily be before or
after. Such a semantic distinction would also allow Data and
DispatchData, as well as other theoretical data structures like a Deque
(I think?) to participate.

The way to handle Deque is to add this requirement to Collection when
we get the language features to express it:

  protocol Collection {

    associatedtype Segments : Collection
    where Segments.Element : Collection,
      Segments.Element.Element == Element
     = EmptyCollection<EmptyCollection<Element>>
    
    var segments: Segments? {get}
    ...
  }

  extension Collection
  where Segments == EmptyCollection<EmptyCollection<Element>> {
    var segments: Segments? { return nil }
  }

Data structures like Deque would vend a non-nil `var segments`, and
algorithms that can benefit from segmentation would be rewritten to take
advantage of it. The contiguous storage referenced by a Deque's
segments would be available by virtue of their conformance to
ContiguouslyStored.

···

on Fri Jan 27 2017, Zach Waldowski <swift-evolution@swift.org> wrote:

Cheers!

  Zachary Waldowski

  zach@waldowski.me

On Fri, Jan 27, 2017, at 06:40 PM, Daryle Walker via swift-evolution wrote:

I was perusing the library for array ideas, and noticed that several
types had methods in common, but without a grouping protocol.
Shouldn’t that be fixed?

(Oh, if multiple protocols have an associated-type with the same name,
is that a problem? Is it a problem if they resolve differently for
each protocol attached to a given type? I’m asking because these
protocols reuse Sequence’s Element for their own purpose.)

Formal Protocol for Contiguous Storage Visitation

* Proposal: SE-NNNN
* Authors: Daryle Walker[1]
* Review Manager: TBD
* Status: *Awaiting review*
*During the review process, add the following fields as needed:*

* Decision Notes: Rationale[2], Additional Commentary[3]
* Bugs: SR-NNNN[4], SR-MMMM[5]
* Previous Revision: 1[6]
* Previous Proposal: SE-XXXX
Introduction

The standard library types Array, ArraySlice, and ContiguousArray have
an interface for visiting their elements as a contiguous block of
memory (arranging said elements to that configuration first if
necessary). These methods are all the same, but not under a common
protocol (i.e. seeming to match by "coincidence").
This proposal seeks to correct that with two new protocols that these
types will implement.
Swift-evolution thread: Discussion thread topic for that proposal[7]
Motivation

Just adding these protocols for consistency is relatively minor, but
they may be used for other types. Particularly, they would be needed
if fixed-sized arrays are added to the language.
Proposed solution

The library array types will follow the
MutableContiguousBlockprotocol, which inherits from the
ContiguousBlock protocol.
extension Array: MutableContiguousBlock { } extension ArraySlice:
MutableContiguousBlock { } extension ContiguousArray:
MutableContiguousBlock { }
For example, a repackaging of the library's example code:

func change<T: MutableContiguousBlock>(object: inout T) -> T.Element
where T.Element: Integer { let sum = object.withUnsafeBufferPointer {
(buffer) -> T.Element in var result: T.Element = 0 for i in
stride(from: buffer.startIndex, to: buffer.endIndex, by: 2) { result
+= buffer[i] } return result } object.withUnsafeMutableBufferPointer {
buffer in for j in stride(from: buffer.startIndex, to: buffer.endIndex
- 1, by: 2) { swap(&buffer[j], &buffer[j + 1]) } } return sum } var
numbers = [1, 2, 3, 4, 5] print(change(object: &numbers)) // 9
print(numbers) // [2, 1, 4, 3, 5]
Detailed design

/** Visitation protocol of the receiver's contiguous storage of
immutable elements. */ protocol ContiguousBlock { /// Inferred alias
to the element type to visit associatedtype Element /** Calls a
closure with a pointer to the receiver's contiguous storage. If no
such storage exists, it is first created. Often, the optimizer can
eliminate bounds checks within an array algorithm, but when that
fails, invoking the same algorithm on the buffer pointer passed into
your closure lets you trade safety for speed. The following example
shows how you can iterate over the contents of the buffer pointer:
let numbers = [1, 2, 3, 4, 5] let sum =
numbers.withUnsafeBufferPointer { buffer -> Int in var result = 0 for
i in stride(from: buffer.startIndex, to: buffer.endIndex, by: 2) {
result += buffer[i] } return result } // 'sum' == 9 - Parameter body:
A closure with an `UnsafeBufferPointer` parameter that points to the
contiguous storage for the receiver. If `body` has a return value, it
is used as the return value for this method. The pointer argument is
valid only for the duration of the closure's execution. - Returns:
The return value of the `body` closure parameter, if any. - SeeAlso:
Swift.UnsafeBufferPointer */ func withUnsafeBufferPointer<R>(_ body:
(UnsafeBufferPointer<Element>) throws -> R) rethrows -> R } /**
Visitation protocol of the receiver's contiguous storage of elements,
allowing mutation. */ protocol MutableContiguousBlock: ContiguousBlock
{ /** Calls the given closure with a pointer to the receiver's
mutable contiguous storage. If no such storage exists, it is first
created. Often, the optimizer can eliminate bounds checks within an
array algorithm, but when that fails, invoking the same algorithm on
the buffer pointer passed into your closure lets you trade safety for
speed. The following example shows modifying the contents of the
`UnsafeMutableBufferPointer` argument to `body` alters the contents of
the receiver: var numbers = [1, 2, 3, 4, 5]
numbers.withUnsafeMutableBufferPointer { buffer in for i in
stride(from: buffer.startIndex, to: buffer.endIndex - 1, by: 2) {
swap(&buffer[i], &buffer[i + 1]) } } print(numbers) // Prints "[2, 1,
4, 3, 5]" - Warning: Do not rely on anything about `self` (the
receiver of this method) during the execution of the `body` closure:
It may not appear to have its correct value. Instead, use only the
`UnsafeMutableBufferPointer` argument to `body`. - Parameter body: A
closure with an `UnsafeMutableBufferPointer` parameter that points to
the contiguous storage for the receiver. If `body` has a return value,
it is used as the return value for this method. The pointer argument
is valid only for the duration of the closure's execution. - Returns:
The return value of the `body` closure parameter, if any. - SeeAlso:
withUnsafeBufferPointer, Swift.UnsafeMutableBufferPointer */ mutating
func withUnsafeMutableBufferPointer<R>(_ body: (inout
UnsafeMutableBufferPointer<Element>) throws -> R) rethrows -> R }
I don't know how the 3 library types implement this methods, but we
know it can be done. If added as an implicit interface to built-in fixed-
sized arrays, the implementers can use similar techniques or compiler
magic. I don't know the difficulty to adapt these interfaces with third-
party code.
Source compatibility

The changes are strictly additive, and at the library level, so there
should be no impact to existing code.
Effect on ABI stability

These changes should not affect ABI stability.

Effect on API resilience

As just stated, the addition of two protocol names to the API
shouldn't affect the ABI.
Alternatives considered

The main alternative is to do nothing. If fixed-sized arrays are later
added, they could have the same methods too, but there would still be
no common link.
Another alternative would make MutableContiguousBlock not inherit from
ContiguousBlock, meaning the former would need to define an Element
associated type. But don't see a need for a protocol with a read-write
method without its read-only counterpart available.

Daryle Walker

Mac, Internet, and Video Game Junkie

darylew AT mac DOT com

_________________________________________________

swift-evolution mailing list

swift-evolution@swift.org

https://lists.swift.org/mailman/listinfo/swift-evolution

Links:

  1. https://github.com/CTMacUser
  2. https://lists.swift.org/pipermail/swift-evolution/
  3. https://lists.swift.org/pipermail/swift-evolution/
  4. https://bugs.swift.org/browse/SR-NNNN
  5. https://bugs.swift.org/browse/SR-MMMM
  6. https://github.com/apple/swift-evolution/blob/...commit-ID.../proposals/NNNN-filename.md
  7. https://lists.swift.org/pipermail/swift-evolution/
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
-Dave


(Daryle Walker) #5

Saw it. Joined Swift-JIRA. Posted my comments.

···

On Jan 27, 2017, at 7:24 PM, Karl Wagner <razielim@gmail.com> wrote:

There is a bug tracking this:

https://bugs.swift.org/browse/SR-3631


Daryle Walker
Mac, Internet, and Video Game Junkie
darylew AT mac DOT com


(Daryle Walker) #6

I copied those parts of the documentation from Array. Unlike ArraySlice and ContiguousArray, due to NSArray compatibility, continuous storage for Array isn’t prepared until needed. I didn’t mean to imply that the relationship isn’t IS-A. Although now I don’t see any reason to ban ephemeral storage, the conforming type should act as if its element state is consistent between calls. (A conforming type that generates a new random block before each call, even ignoring previous mutations, isn’t very useful.)

···

On Jan 27, 2017, at 7:36 PM, Zach Waldowski via swift-evolution <swift-evolution@swift.org> wrote:

I like the sound of it so far, but my first major thought is that isn't it modeling a "has-a" relationship instead of an "is-a"? The buffer methods indicate that the data type *can* be represented as a buffer for the duration of the method call, but may not necessarily be before or after. Such a semantic distinction would also allow Data and DispatchData, as well as other theoretical data structures like a Deque (I think?) to participate.


Daryle Walker
Mac, Internet, and Video Game Junkie
darylew AT mac DOT com

Cheers!
  Zachary Waldowski
  zach@waldowski.me <mailto:zach@waldowski.me>

On Fri, Jan 27, 2017, at 06:40 PM, Daryle Walker via swift-evolution wrote:

I was perusing the library for array ideas, and noticed that several types had methods in common, but without a grouping protocol. Shouldn’t that be fixed?

(Oh, if multiple protocols have an associated-type with the same name, is that a problem? Is it a problem if they resolve differently for each protocol attached to a given type? I’m asking because these protocols reuse Sequence’s Element for their own purpose.)

Formal Protocol for Contiguous Storage Visitation
Proposal: SE-NNNN <>
Authors: Daryle Walker <https://github.com/CTMacUser>
Review Manager: TBD
Status: Awaiting review
During the review process, add the following fields as needed:

Decision Notes: Rationale <https://lists.swift.org/pipermail/swift-evolution/>, Additional Commentary <https://lists.swift.org/pipermail/swift-evolution/>
Bugs: SR-NNNN <https://bugs.swift.org/browse/SR-NNNN>, SR-MMMM <https://bugs.swift.org/browse/SR-MMMM>
Previous Revision: 1 <https://github.com/apple/swift-evolution/blob/...commit-ID.../proposals/NNNN-filename.md>
Previous Proposal: SE-XXXX <>
Introduction
The standard library types Array, ArraySlice, and ContiguousArray have an interface for visiting their elements as a contiguous block of memory (arranging said elements to that configuration first if necessary). These methods are all the same, but not under a common protocol (i.e. seeming to match by "coincidence").

This proposal seeks to correct that with two new protocols that these types will implement.

Swift-evolution thread: Discussion thread topic for that proposal <https://lists.swift.org/pipermail/swift-evolution/>
Motivation
Just adding these protocols for consistency is relatively minor, but they may be used for other types. Particularly, they would be needed if fixed-sized arrays are added to the language.

Proposed solution
The library array types will follow the MutableContiguousBlockprotocol, which inherits from the ContiguousBlock protocol.

extension Array: MutableContiguousBlock {
}

extension ArraySlice: MutableContiguousBlock {
}

extension ContiguousArray: MutableContiguousBlock {
}
For example, a repackaging of the library's example code:

func change<T: MutableContiguousBlock>(object: inout T) -> T.Element where T.Element: Integer {
    let sum = object.withUnsafeBufferPointer { (buffer) -> T.Element in
        var result: T.Element = 0
        for i in stride(from: buffer.startIndex, to: buffer.endIndex, by: 2) {
            result += buffer[i]
        }
        return result
    }
    object.withUnsafeMutableBufferPointer { buffer in
        for j in stride(from: buffer.startIndex, to: buffer.endIndex - 1, by: 2) {
            swap(&buffer[j], &buffer[j + 1])
        }
    }
    return sum
}

var numbers = [1, 2, 3, 4, 5]
print(change(object: &numbers)) // 9
print(numbers) // [2, 1, 4, 3, 5]
Detailed design
/**
    Visitation protocol of the receiver's contiguous storage of immutable elements.
*/
protocol ContiguousBlock {

    /// Inferred alias to the element type to visit
    associatedtype Element

    /**
        Calls a closure with a pointer to the receiver's contiguous storage. If no such storage exists, it is first created.

        Often, the optimizer can eliminate bounds checks within an array algorithm, but when that fails, invoking the same algorithm on the buffer pointer passed into your closure lets you trade safety for speed.

        The following example shows how you can iterate over the contents of the buffer pointer:

            let numbers = [1, 2, 3, 4, 5]
            let sum = numbers.withUnsafeBufferPointer { buffer -> Int in
                var result = 0
                for i in stride(from: buffer.startIndex, to: buffer.endIndex, by: 2) {
                    result += buffer[i]
                }
                return result
            }
            // 'sum' == 9

        - Parameter body: A closure with an `UnsafeBufferPointer` parameter that points to the contiguous storage for the receiver. If `body` has a return value, it is used as the return value for this method. The pointer argument is valid only for the duration of the closure's execution.

        - Returns: The return value of the `body` closure parameter, if any.

        - SeeAlso: Swift.UnsafeBufferPointer
     */
    func withUnsafeBufferPointer<R>(_ body: (UnsafeBufferPointer<Element>) throws -> R) rethrows -> R

}

/**
    Visitation protocol of the receiver's contiguous storage of elements, allowing mutation.
*/
protocol MutableContiguousBlock: ContiguousBlock {

    /**
        Calls the given closure with a pointer to the receiver's mutable contiguous storage. If no such storage exists, it is first created.

        Often, the optimizer can eliminate bounds checks within an array algorithm, but when that fails, invoking the same algorithm on the buffer pointer passed into your closure lets you trade safety for speed.

        The following example shows modifying the contents of the `UnsafeMutableBufferPointer` argument to `body` alters the contents of the receiver:

            var numbers = [1, 2, 3, 4, 5]
            numbers.withUnsafeMutableBufferPointer { buffer in
                for i in stride(from: buffer.startIndex, to: buffer.endIndex - 1, by: 2) {
                    swap(&buffer[i], &buffer[i + 1])
                }
            }
            print(numbers)
            // Prints "[2, 1, 4, 3, 5]"

        - Warning: Do not rely on anything about `self` (the receiver of this method) during the execution of the `body` closure: It may not appear to have its correct value. Instead, use only the `UnsafeMutableBufferPointer` argument to `body`.

        - Parameter body: A closure with an `UnsafeMutableBufferPointer` parameter that points to the contiguous storage for the receiver. If `body` has a return value, it is used as the return value for this method. The pointer argument is valid only for the duration of the closure's execution.

        - Returns: The return value of the `body` closure parameter, if any.

        - SeeAlso: withUnsafeBufferPointer, Swift.UnsafeMutableBufferPointer
     */
    mutating func withUnsafeMutableBufferPointer<R>(_ body: (inout UnsafeMutableBufferPointer<Element>) throws -> R) rethrows -> R

}
I don't know how the 3 library types implement this methods, but we know it can be done. If added as an implicit interface to built-in fixed-sized arrays, the implementers can use similar techniques or compiler magic. I don't know the difficulty to adapt these interfaces with third-party code.

Source compatibility
The changes are strictly additive, and at the library level, so there should be no impact to existing code.

Effect on ABI stability
These changes should not affect ABI stability.

Effect on API resilience
As just stated, the addition of two protocol names to the API shouldn't affect the ABI.

Alternatives considered
The main alternative is to do nothing. If fixed-sized arrays are later added, they could have the same methods too, but there would still be no common link.

Another alternative would make MutableContiguousBlock not inherit from ContiguousBlock, meaning the former would need to define an Element associated type. But don't see a need for a protocol with a read-write method without its read-only counterpart available.


Daryle Walker
Mac, Internet, and Video Game Junkie
darylew AT mac DOT com

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #7

Couldn't that be be expressed more accurately with `Never` if it were a subtype-of-all-types?

   protocol Collection {
  
     associatedtype Segments : Collection
     where Segments.Element : Collection,
       Segments.Element.Element == Element
      = Never
  
     var segments: Segments? {get}
     ...
   }
  
   extension Collection
   where Segments == Never {
     var segments: Segments? { return nil }
   }

Or you could say that there is always at least *one* segment:

   protocol Collection {
  
     associatedtype Segments : Collection
     where Segments.Element : Collection,
       Segments.Element.Element == Element
      = CollectionOfOne<Self>
  
     var segments: Segments {get}
     ...
   }
  
   extension Collection
   where Segments == CollectionOfOne<Self> {
     var segments: Segments { return CollectionOfOne(self) }
   }

···

On Jan 28, 2017, at 11:48 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

The way to handle Deque is to add this requirement to Collection when
we get the language features to express it:

protocol Collection {

   associatedtype Segments : Collection
   where Segments.Element : Collection,
     Segments.Element.Element == Element
    = EmptyCollection<EmptyCollection<Element>>

   var segments: Segments? {get}
   ...
}

extension Collection
where Segments == EmptyCollection<EmptyCollection<Element>> {
   var segments: Segments? { return nil }
}

--
Brent Royal-Gordon
Architechies


(Dave Abrahams) #8

The way to handle Deque is to add this requirement to Collection when
we get the language features to express it:

protocol Collection {

   associatedtype Segments : Collection
   where Segments.Element : Collection,
     Segments.Element.Element == Element
    = EmptyCollection<EmptyCollection<Element>>

   var segments: Segments? {get}
   ...
}

extension Collection
where Segments == EmptyCollection<EmptyCollection<Element>> {
   var segments: Segments? { return nil }
}

Couldn't that be be expressed more accurately with `Never` if it were a subtype-of-all-types?

   protocol Collection {

     associatedtype Segments : Collection
     where Segments.Element : Collection,
       Segments.Element.Element == Element
      = Never

     var segments: Segments? {get}
     ...
   }

   extension Collection
   where Segments == Never {
     var segments: Segments? { return nil }
   }

Maybe, if we had the language feature, we could do that... but we don't.

Or you could say that there is always at least *one* segment:

   protocol Collection {

     associatedtype Segments : Collection
     where Segments.Element : Collection,
       Segments.Element.Element == Element
      = CollectionOfOne<Self>

     var segments: Segments {get}
     ...
   }

   extension Collection
   where Segments == CollectionOfOne<Self> {
     var segments: Segments { return CollectionOfOne(self) }
   }

That's not actually helpful, because segmented algorithms need to be
able to use the presence of segments to stop recursing :slight_smile:

···

on Sat Jan 28 2017, Brent Royal-Gordon <brent-AT-architechies.com> wrote:

On Jan 28, 2017, at 11:48 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

--
-Dave


(Dave Abrahams) #9

Er, the absence of segments I mean.

···

on Sun Jan 29 2017, Dave Abrahams <swift-evolution@swift.org> wrote:

on Sat Jan 28 2017, Brent Royal-Gordon <brent-AT-architechies.com> wrote:

On Jan 28, 2017, at 11:48 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> > wrote:

The way to handle Deque is to add this requirement to Collection when
we get the language features to express it:

protocol Collection {

   associatedtype Segments : Collection
   where Segments.Element : Collection,
     Segments.Element.Element == Element
    = EmptyCollection<EmptyCollection<Element>>

   var segments: Segments? {get}
   ...
}

extension Collection
where Segments == EmptyCollection<EmptyCollection<Element>> {
   var segments: Segments? { return nil }
}

Couldn't that be be expressed more accurately with `Never` if it were a subtype-of-all-types?

   protocol Collection {

     associatedtype Segments : Collection
     where Segments.Element : Collection,
       Segments.Element.Element == Element
      = Never

     var segments: Segments? {get}
     ...
   }

   extension Collection
   where Segments == Never {
     var segments: Segments? { return nil }
   }

Maybe, if we had the language feature, we could do that... but we don't.

Or you could say that there is always at least *one* segment:

   protocol Collection {

     associatedtype Segments : Collection
     where Segments.Element : Collection,
       Segments.Element.Element == Element
      = CollectionOfOne<Self>

     var segments: Segments {get}
     ...
   }

   extension Collection
   where Segments == CollectionOfOne<Self> {
     var segments: Segments { return CollectionOfOne(self) }
   }

That's not actually helpful, because segmented algorithms need to be
able to use the presence of segments to stop recursing :slight_smile:

--
-Dave