Hi Swift Evolution,

Below is a pitch for the first part of the String revision. This covers a 
number of changes that would allow the basic internals to be overhauled.

Online version here: 
https://github.com/airspeedswift/swift-evolution/blob/3a822c799011ace682712532cfabfe32e9203fbb/proposals/0161-StringRevision1.md
 
<https://github.com/airspeedswift/swift-evolution/blob/3a822c799011ace682712532cfabfe32e9203fbb/proposals/0161-StringRevision1.md>


String Revision: Collection Conformance, C Interop, Transcoding

Proposal: SE-0161 
<file:///Users/ben_cohen/Documents/swift-evolution/proposals/0161-StringRevision1.md>
Authors: Ben Cohen <https://github.com/airspeedswift>, Dave Abrahams 
<http://github.com/dabrahams/>
Review Manager: TBD
Status: Awaiting review
Introduction

This proposal is to implement a subset of the changes from the Swift 4 String 
Manifesto <https://github.com/apple/swift/blob/master/docs/StringManifesto.md>.

Specifically:

Make String conform to BidirectionalCollection
Make String conform to RangeReplaceableCollection
Create a Substring type for String.SubSequence
Create a Unicode protocol to allow for generic operations over both types.
Consolidate on a concise set of C interop methods.
Revise the transcoding infrastructure.
Other existing aspects of String remain unchanged for the purposes of this 
proposal.

Motivation

This proposal follows up on a number of recommendations found in the manifesto:

Collection conformance was dropped from String in Swift 2. After reevaluation, 
the feeling is that the minor semantic discrepancies (mainly with 
RangeReplaceableCollection) are outweighed by the significant benefits of 
restoring these conformances. For more detail on the reasoning, see here 
<https://github.com/apple/swift/blob/master/docs/StringManifesto.md#string-should-be-a-collection-of-characters-again>
While it is not a collection, the Swift 3 string does have slicing operations. 
String is currently serving as its own subsequence, allowing substrings to 
share storage with their “owner”. This can lead to memory leaks when small 
substrings of larger strings are stored long-term (see here 
<https://github.com/apple/swift/blob/master/docs/StringManifesto.md#substrings> 
for more detail on this problem). Introducing a separate type of Substring to 
serve as String.Subsequence is recommended to resolve this issue, in a similar 
fashion to ArraySlice.

As noted in the manifesto, support for interoperation with nul-terminated C 
strings in Swift 3 is scattered and incoherent, with 6 ways to transform a C 
string into a String and four ways to do the inverse. These APIs should be 
replaced with a simpler set of methods on String.

Proposed solution

A new type, Substring, will be introduced. Similar to ArraySlice it will be 
documented as only for short- to medium-term storage:

Important

Long-term storage of Substring instances is discouraged. A substring holds a 
reference to the entire storage of a larger string, not just to the portion it 
presents, even after the original string’s lifetime ends. Long-term storage of 
a substring may therefore prolong the lifetime of elements that are no longer 
otherwise accessible, which can appear to be memory leakage.
Aside from minor differences, such as having a SubSequence of Self and a larger 
size to describe the range of the subsequence, Substring will be near-identical 
from a user perspective.

In order to be able to write extensions accross both String and Substring, a 
new Unicode protocol to which the two types will conform will be introduced. 
For the purposes of this proposal, Unicode will be defined as a protocol to be 
used whenver you would previously extend String. It should be possible to 
substitute extension Unicode { ... } in Swift 4 wherever extension String { ... 
} was written in Swift 3, with one exception: any passing of self into an API 
that takes a concrete String will need to be rewritten as String(self). If Self 
is a String then this should effectively optimize to a no-op, whereas if Self 
is a Substring then this will force a copy, helping to avoid the “memory leak” 
problems described above.

The exact nature of the protocol – such as which methods should be protocol 
requirements vs which can be implemented as protocol extensions, are considered 
implementation details and so not covered in this proposal.

Unicode will conform to BidirectionalCollection. RangeReplaceableCollection 
conformance will be added directly onto the String and Substring types, as it 
is possible future Unicode-conforming types might not be range-replaceable 
(e.g. an immutable type that wraps a const char *).

The C string interop methods will be updated to those described here 
<https://github.com/apple/swift/blob/master/docs/StringManifesto.md#c-string-interop>:
 a single withCString operation and two init(cString:) constructors, one for 
UTF8 and one for arbitrary encodings. The primary change is to remove 
“non-repairing” variants of construction from nul-terminated C strings. In both 
of the construction APIs, any invalid encoding sequence detected will have its 
longest valid prefix replaced by U+FFFD, the Unicode replacement character, per 
the Unicode specification. This covers the common case. The replacement is done 
physically in the underlying storage and the validity of the result is recorded 
in the String’s encoding such that future accesses need not be slowed down by 
possible error repair separately. Construction that is aborted when encoding 
errors are detected can be accomplished using APIs on the encoding.

The current transcoding support will be updated to improve usability and 
performance. The primary changes will be:

to allow transcoding directly from one encoding to another without having to 
triangulate through an intermediate scalar value
to add the ability to transcode an input collection in reverse, allowing the 
different views on String to be made bi-directional
to have decoding take a collection rather than an iterator, and return an index 
of its progress into the source, allowing that method to be static
The standard library currently lacks a Latin1 codec, so a enum Latin1: 
UnicodeEncoding type will be added.

Detailed design

The following additions will be made to the standard library:

protocol Unicode: BidirectionalCollection {
  // Implementation detail as described above
}

extension String: Unicode, RangeReplaceableCollection {
  typealias SubSequence = Substring
}

struct Substring: Unicode, RangeReplaceableCollection {
  typealias SubSequence = Substring
  // near-identical API surface area to String
}
The subscript operations on String will be amended to return Substring:

struct String {
  subscript(bounds: Range<String.Index>) -> Substring { get }
  subscript(bounds: ClosedRange<String.Index>) -> Substring { get }
}
Note that properties or methods that due to their nature create new String 
storage (such as lowercased()) will not change.

C string interop will be consolidated on the following methods:

extension String {
  /// Constructs a `String` having the same contents as `nulTerminatedUTF8`.
  ///
  /// - Parameter nulTerminatedUTF8: a sequence of contiguous UTF-8 encoded 
  ///   bytes ending just before the first zero byte (NUL character).
  init(cString nulTerminatedUTF8: UnsafePointer<CChar>)
  
  /// Constructs a `String` having the same contents as 
`nulTerminatedCodeUnits`.
  ///
  /// - Parameter nulTerminatedCodeUnits: a sequence of contiguous code units in
  ///   the given `encoding`, ending just before the first zero code unit.
  /// - Parameter encoding: describes the encoding in which the code units
  ///   should be interpreted.
  init<Encoding: UnicodeEncoding>(
    cString nulTerminatedCodeUnits: UnsafePointer<Encoding.CodeUnit>,
    encoding: Encoding)
    
  /// Invokes the given closure on the contents of the string, represented as a
  /// pointer to a null-terminated sequence of UTF-8 code units.
  func withCString<Result>(
    _ body: (UnsafePointer<CChar>) throws -> Result) rethrows -> Result
}
Additionally, the current ability to pass a Swift String into C methods that 
take a C string will remain as-is.

A new protocol, UnicodeEncoding, will be added to replace the current 
UnicodeCodec protocol:

public enum UnicodeParseResult<T, Index> {
/// Indicates valid input was recognized.
///
/// `resumptionPoint` is the end of the parsed region
case valid(T, resumptionPoint: Index)  // FIXME: should these be reordered?
/// Indicates invalid input was recognized.
///
/// `resumptionPoint` is the next position at which to continue parsing after
/// the invalid input is repaired.
case error(resumptionPoint: Index)

/// Indicates that there was no more input to consume.
case emptyInput

  /// If any input was consumed, the point from which to continue parsing.
  var resumptionPoint: Index? {
    switch self {
    case .valid(_,let r): return r
    case .error(let r): return r
    case .emptyInput: return nil
    }
  }
}

/// An encoding for text with UnicodeScalar as a common currency type
public protocol UnicodeEncoding {
  /// The maximum number of code units in an encoded unicode scalar value
  static var maxLengthOfEncodedScalar: Int { get }
  
  /// A type that can represent a single UnicodeScalar as it is encoded in this
  /// encoding.
  associatedtype EncodedScalar : EncodedScalarProtocol

  /// Produces a scalar of this encoding if possible; returns `nil` otherwise.
  static func encode<Scalar: EncodedScalarProtocol>(
    _:Scalar) -> Self.EncodedScalar?
  
  /// Parse a single unicode scalar forward from `input`.
  ///
  /// - Parameter knownCount: a number of code units known to exist in `input`.
  ///   **Note:** passing a known compile-time constant is strongly advised,
  ///   even if it's zero.
  static func parseScalarForward<C: Collection>(
    _ input: C, knownCount: Int /* = 0, via extension */
  ) -> ParseResult<EncodedScalar, C.Index>
  where C.Iterator.Element == EncodedScalar.Iterator.Element

  /// Parse a single unicode scalar in reverse from `input`.
  ///
  /// - Parameter knownCount: a number of code units known to exist in `input`.
  ///   **Note:** passing a known compile-time constant is strongly advised,
  ///   even if it's zero.
  static func parseScalarReverse<C: BidirectionalCollection>(
    _ input: C, knownCount: Int /* = 0 , via extension */
  ) -> ParseResult<EncodedScalar, C.Index>
  where C.Iterator.Element == EncodedScalar.Iterator.Element
}

/// Parsing multiple unicode scalar values
extension UnicodeEncoding {
  @discardableResult
  public static func parseForward<C: Collection>(
    _ input: C,
    repairingIllFormedSequences makeRepairs: Bool = true,
    into output: (EncodedScalar) throws->Void
  ) rethrows -> (remainder: C.SubSequence, errorCount: Int)
  
  @discardableResult    
  public static func parseReverse<C: BidirectionalCollection>(
    _ input: C,
    repairingIllFormedSequences makeRepairs: Bool = true,
    into output: (EncodedScalar) throws->Void
  ) rethrows -> (remainder: C.SubSequence, errorCount: Int)
  where C.SubSequence : BidirectionalCollection,
        C.SubSequence.SubSequence == C.SubSequence,
        C.SubSequence.Iterator.Element == EncodedScalar.Iterator.Element
}
UnicodeCodec will be updated to refine UnicodeEncoding, and all existing codecs 
will conform to it.

Note, depending on whether this change lands before or after some of the 
generics features, generic where clauses may need to be added temporarily.

Source compatibility

Adding collection conformance to String should not materially impact source 
stability as it is purely additive: Swift 3’s String interface currently 
fulfills all of the requirements for a bidirectional range replaceable 
collection.

Altering String’s slicing operations to return a different type is source 
breaking. The following mitigating steps are proposed:

Add a deprecated subscript operator that will run in Swift 3 compatibility mode 
and which will return a String not a Substring.

Add deprecated versions of all current slicing methods to similarly return a 
String.

i.e.:

extension String {
  @available(swift, obsoleted: 4)
  subscript(bounds: Range<Index>) -> String {
    return String(characters[bounds])
  }

  @available(swift, obsoleted: 4)
  subscript(bounds: ClosedRange<Index>) -> String {
    return String(characters[bounds])
  }
}
In a review of 77 popular Swift projects found on GitHub, these changes 
resolved any build issues in the 12 projects that assumed an explicit String 
type returned from slicing operations.

Due to the change in internal implementation, this means that these operations 
will be O(n) rather than O(1). This is not expected to be a major concern, 
based on experiences from a similar change made to Java, but projects will be 
able to work around performance issues without upgrading to Swift 4 by 
explicitly typing slices as Substring, which will call the Swift 4 variant, and 
which will be available but not invoked by default in Swift 3 mode.

The C string interoperability methods outside the ones described in the 
detailed design will remain in Swift 3 mode, be deprecated in Swift 4 mode, and 
be removed in a subsequent release. UnicodeCodec will be similarly deprecated.

Effect on ABI stability

As a fundamental currency type for Swift, it is essential that the String type 
(and its associated subsequence) is in a good long-term state before being 
locked down when Swift declares ABI stability. Shrinking the size of String to 
be 64 bits is an important part of this.

Effect on API resilience

Decisions about the API resilience of the String type are still to be 
determined, but are not adversely affected by this proposal.

Alternatives considered

For a more in-depth discussion of some of the trade-offs in string design, see 
the manifesto and associated evolution thread 
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170116/thread.html#30497>.

This proposal does not yet introduce an implicit conversion from Substring to 
String. The decision on whether to add this will be deferred pending feedback 
on the initial implementation. The intention is to make a preview toolchain 
available for feedback, including on whether this implicit conversion is 
necessary, prior to the release of Swift 4.

Several of the types related to String, such as the encodings, would ideally 
reside inside a namespace rather than live at the top level of the standard 
library. The best namespace for this is probably Unicode, but this is also the 
name of the protocol. At some point if we gain the ability to nest enums and 
types inside protocols, they should be moved there. Putting them inside String 
or some other enum namespace is probably not worthwhile in the mean-time.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to