[Pitch] `Span` over a single value

Hello all, this is a new proposal to add initializers to form a single-element Span over any value, and to form a single-element MutableSpan over any mutable value. The evolution pull request is at [pitch] `Span` over a single value by glessard · Pull Request #3445 · swiftlang/swift-evolution · GitHub, and most of the content follows.

Motivation

Occasionally, programmers need adaptors between single values and Span-taking API. Currently it is possible to use the span property of CollectionOfOne in order to pass a single value to a Span parameter, but it requires a copy that Span doesn't need, or that non-copyable values cannot support. We can provide initializers for Span, RawSpan, MutableSpan and MutableRawSpan that borrow single values in place.

These initializers will also act as safe versions of withUnsafePointer(to:), withUnsafeMutablePointer(to:), withUnsafeBytes(of:) and withUnsafeMutableBytes(of:)

Proposed solution

Span and MutableSpan gain unlabeled initializers that form a span of count 1
over a single value:

let header = PacketHeader(...)
let c = checksum(Span(header).bytes)   // borrows `header` in place

var timestamp = UInt64.zero
var span = MutableSpan(&timestamp)
parser.read(into: span.mutableBytes)   // writes directly into `timestamp`

Similarly, RawSpan and MutableRawSpan gain initializers that form spans over the bytes of a single value:

let header = PacketHeader(...)
let c = checksum(RawSpan(header))

var timestamp = UInt64.zero
var bytes = MutableRawSpan(&timestamp)
parser.read(into: &bytes)

Detailed design

Span

extension Span where Element: ~Copyable {
  /// Create a span over the single value passed as a parameter.
  ///
  /// - Parameters:
  ///   - value: a value to be borrowed by the span
  @_lifetime(borrow value)
  public init(_ value: borrowing Element)
}

MutableSpan

extension MutableSpan where Element: ~Copyable {
  /// Create a mutable span over the single value passed as a parameter.
  ///
  /// The `MutableSpan` created by this initializer will represent a
  /// mutation of `value`.
  ///
  /// - Parameters:
  ///   - value: a value to be mutated through the span
  @_lifetime(&value)
  public init(_ value: inout Element)
}

RawSpan

extension RawSpan {
  /// Create a span over the bytes of the single value passed as a parameter.
  ///
  /// The `RawSpan` created by this initializer will have a byteCount of
  /// `MemoryLayout<Element>.size` bytes.
  ///
  /// - Parameters:
  ///   - value: a value to be borrowed by the span
  @_lifetime(borrow value)
  public init<Element: ConvertibleToBytes>(_ value: borrowing Element)
}

MutableRawSpan

extension MutableRawSpan {
  /// Create a mutable span over the bytes of the value passed as a parameter.
  ///
  /// The `MutableRawSpan` created by this initializer will represent a
  /// mutation of `value`. It will have a byteCount of
  /// `MemoryLayout<Element>.size` bytes.
  ///
  /// - Parameters:
  ///   - value: a value to be mutated through the span
  @_lifetime(&value)
  public init<Element: ConvertibleToBytes & ConvertibleFromBytes>(
    _ value: inout Element
  )
}
15 Likes

The need makes sense, but single-element spans are conceptually equivalent to Ref/MutableRef. I wonder if Ref(x).span might be a better spelling? I also wonder about the missing opposite direction (obtaining a Ref to a particular element of a Span).

8 Likes

Would we consider adding RawSpan over single ConvertibleToBytes values and MutableRawSpan over single FullyInhabited values in the same proposal? (edit: oh wait we do have it)

(Is there any point also adding initializers for OutputSpan starting at 1 initialized value? I have ideas for where I would like to use RawSpan, but for OutputSpan I'm kind of just spitballing)

Prior art in Rust: from_ref in std::slice - Rust

I'm all for adding this functionality. I'm not 100% sure an unlabeled init(_:) is how I'd choose to spell it, particularly for Span. I know we've been using span properties to access spans from collections, but you could imagine an initializer to convert from UnsafeBufferPointer or similar. Or to put it abstractly, I usually think of Foo(bar) as "convert bar to Foo" but this isn't doing that.

5 Likes

Getting Refs from span elements is also of interest. I'd like to keep this proposal from growing into an omnibus.

Ref(x).span is a possible future thing, but simply don't think it's the natural spelling for a span over x.

That sounds like a MutableSpan. This being said, it might be interesting to provide a way to get an OutputSpan from a MutableSpan, where the count and capacity are equal at the start, and they must still be equal at the end. In between, do whatever you like. Same for MutableRawSpan, obviously.

I agree with these points of feedback. I do think that, if we help enough folks internalize the idea that Ref is a span-of-one, they will naturally think along the lines of what @KeithBauerANZ points out.

There will no doubt always be folks who want something even more direct, but if accommodating that is the goal, I almost wonder if we ought to go all the way and make some combination of [bar], &bar, or &[bar] "just work." We are at a weird juncture where we have special affordances for UMBP and UBP that are more ergonomic than what we have for Span and Ref: that being the overarching problem, I'm skeptical that stretching the use of unlabeled initializers is the answer we want to rest on.

Note that the initializers for Ref and MutableRef are spelled with the same shape: Ref<T>.init(_: T). It seems like having to go through Ref to get a Span over a single value would feel like ceremony.

The direct initializer also allows us to have an initializer for RawSpan, whereas going through Ref would likely become Ref(something).span.bytes.

3 Likes

I did forget that Ref's init(_:) was already approved, sigh. I wish I'd thought of this at the time. …but I do think it's worse for Span, someone is going to write Span(array) and be confused at the resulting type mismatch.

6 Likes

This is a useful API we should offer. It's worth noting somewhere that, since Span and RawSpan require that the referenced value have an address somewhere, there will be situations where the resulting Span has a shorter lifetime than the borrow it came from, when we need to move the referenced value into a temporary location. (This is in contrast to Ref which has representational shenanigans to avoid this limitation.)

func foo(x: Int) -> Ref<Int> {
  return Ref(x) // OK
}

func bar(x: Int) -> Span<Int> {
  return Span(x) // error, span depends on a temporary allocation
}

I weakly agree with Jordan that it "feels" better for this initializer to give its parameter a label.

5 Likes

To me, Ref.init(_:) feels morally equivalent to Optional.init(_:), which we've always had and never needed, since it could always be written Optional.some(_:).

By contrast, we've never had the same for Array, which (if we counterfactually did) to me would be the moral equivalent of Span.


Incidentally, that Array("Hello") gives you an array of 5 elements is a nice reminder of why we need to be careful here. For values that themselves are span-providing, foo.span would be totally different from Span(foo) as pitched here. A label is required at minimum but even so may not be entirely a complete solution.

Ref.init (by contrast) has no such issues.

2 Likes

How is it expected that this will present itself to the user in practice?

For example:

struct SmallEnough {
  var words: [4 of Int]
}

struct NotSmallEnough {
  var words: [5 of Int]
}

func wrap(value: NotSmallEnough) -> Span<NotSmallEnough> {
  Span(value) // accepted because value is passed by address?
}

func wrap(value: SmallEnough) -> Span<SmallishValue> {
  Span(value) // error because the span is in a temp address?
}

Or would the compiler reject both for consistency?

Also how is this thought about in the context of resiliency? Will it be ok if the "accept code opportunistically when passed by address" path is taken that the same code inside vs outside a resiliency domain may compile or not compile differently? (I'm not sure if there's already precedent for that in the language)

One more thought/question: Is it possible for the compiler to select the "right" passing convention based on if the value is the target of a lifetime dependency, similar to how it does today for types containing an InlineArray?

1 Like

These require an annotation to the parameter (@_addressable). At the moment the diagnostics aren't providing a good message, but that's fixable. Later on, either (a) lifetime types will provide enough signal to the compiler to do the right thing, or (b) we'll need to make the addressability attributes an official part of the language.

1 Like

The pitch starts with the use case that someone is calling a Span taking API and has a single value. I think an initializer on Span is the easiest place to find it, rather than requiring a mental redirection through an unrelated Ref type.

Personally, I'm neutral on the argument label for the initializer, but the API guidelines do say "In initializers that perform value preserving type conversions, omit the first argument label" - there is no loss of precision or similar conversion here.

6 Likes

To be clear, my argument is that while it is "value-preserving", it is not a "type conversion". But I can see how that's handwavy and perhaps not convincing.

At first I thought “how is that confusing, it’s just like Array(sequence)”, and only later realized that this is a very different operation to the regular converting inits.

Span is strongly related to the notion of working with multiple elements. I think this init needs a label to explain that a single-element span is created. I expect this init will be used in places where CollectionOfOne is use today (that is, not commonly) and thus the extra explicitness is reasonable.

Maybe Span(single: value)?

4 Likes

To be honest, I think the outlier here is the unlabeled Array.init(_: some Sequence<Element>) initializer, which carries out a full copy (an O(n) operation.) We can't change it, but I don't think that operation's name adheres to the API guidelines.

We have established the pattern that in order to get a Span from an existing container's storage, you should reach for a property of the instance: Array.span, UniqueBox.span, Data.span, etc. This proposal is a different shape from that pattern, and in my opinion it deserves a short name.

The .span properties make sense to have that spelling, because the receiver is always something container-like. There's no ambiguity about what the span is coming from.

If this new pattern deserves a similarly short name, that presupposes that getting a span from a single out-of-collection value is commonplace enough to warrant getting that short name. But the examples in the proposal all involve getting/mutating the raw bytes of a value, which seems like a sufficiently advanced operation to warrant having it spelled out, I think. I can see that it's something that might be more frequently used in low-level/embedded environments, but I can't help but wonder if the initializer spelling is optimizing for writability rather than readability.

1 Like

Wanted to chime in an say getting a span of one in Rust is very straightforward and doesn't lead to potentially confusing scenarios like is Span(array) a Span<Array<Int>> or Span<Int>.

let x = 123;
let y = &[x];

You put x in an InlineArray (using Swift's type names) and grab a borrow to that (technically it has the type &[isize; 1] but you can very easily coerce it to a slice type if you used it as such). It's unfortunate that [x].span in Swift would go through Array and would probably require heroics to eliminate all of that overhead.

This is not quite the same thing; the proposed API works with a borrowed non-copyable type, and the simple "wrap in InlineArray" approach does not.