September 12, 2022·6 min read·engineeringswiftgenericsregextooling

Swift 5.7 preserves useful type relationships with less code

Swift 5.7 adds opaque parameters, constrained existential types and typed regex literals. The changes reduce declaration overhead while keeping different forms of abstraction distinct.

A function that accepts one conforming value should not always need a named generic parameter. A collection of different implementations should not always need to erase the type of data those implementations exchange. Swift 5.7 addresses both cases, giving programmers shorter declarations while retaining relationships the type checker can use.

The September 12 release announcement includes a substantial set of language and library changes, with regular expressions among them. The useful connection is between what a declaration reveals and what it deliberately hides. Opaque parameters preserve a concrete type without naming it. Constrained existentials hide an implementation while retaining a specified associated type. Regex literals give captures a type that callers can inspect without reconstructing their shape at runtime.

I appreciate Swift's habit of making these choices explicit. Shorter source helps when it removes repeated declarations without hiding a consequential change in meaning. This release supplies several cases where less annotation still leaves the compiler with the relationship that matters.

Some parameters are unnamed generics

Doug Gregor's SE-0341 introduces opaque parameter declarations. A parameter written with some P becomes an unnamed generic parameter constrained to protocol P. The caller still supplies a concrete type, and the function remains generic over that type.

protocol Labelled {
    var label: String { get }
}

func printLabel(_ value: some Labelled) {
    print(value.label)
}

This function could have declared a named type parameter with a conformance constraint. The new form is useful because the type parameter has no other role in the declaration. Naming it adds information the body and signature do not need to refer to again.

The syntax should not be interpreted as existential storage. An opaque parameter preserves the generic relationship to the caller's selected type. A parameter written with any Labelled expresses a different abstraction, where the value is presented through an existential. Similar protocol requirements do not make those declarations identical.

SE-0341 also specifies that each occurrence of some introduces a distinct implicit generic parameter. Two parameters written with some Labelled do not promise that their concrete types are equal. If equality between their types matters, the declaration still needs a way to express that relationship through a shared named generic parameter.

That boundary is important for API design. The shorter form is appropriate when a parameter is used once as a constraint. Named parameters remain useful when the signature needs to connect inputs or express a result in terms of one of them. The feature simplifies a common case without replacing the more general notation.

Erase an implementation while retaining its element type

SE-0353 addresses a different problem. An existential can hide a concrete collection implementation, but a caller may still need to know the collection's element type. Without that information, even a simple operation returning an element can lose useful static type information.

The new constraint syntax applies same-type requirements to a protocol's primary associated types. A declaration such as any Collection<String> states that the stored collection's element type is String, while leaving its concrete collection type unspecified. The angle brackets constrain the associated type. They do not turn the protocol into a conventional generic struct.

func firstLabel(_ labels: any Collection<String>) -> String? {
    labels.first
}

The proposal contrasts an unconstrained collection's first element, erased to an upper bound, with a constrained collection whose first element remains String?. Keeping that associated type known lets application code use the result directly. The erased detail is the collection implementation, not every relationship exposed by its protocol.

The motivating example uses heterogeneous producers and consumers. Their implementations can differ, but their event type must agree if a consumer is to accept a producer's output safely. Constrained existentials allow that shared event type to remain explicit while removing the need to make every producer the same concrete type.

This can reduce the need for handwritten type-erasing wrappers. A wrapper may otherwise be necessary merely to express that one associated type remains fixed. The language now has notation for that combination of hidden implementation and preserved data type, though wrappers can still serve other API or compatibility purposes.

Constraints do not create universal conversions

SE-0353 explicitly treats constrained existentials as invariant in their associated type constraints. A collection of one element type does not automatically become an existential collection of a broader element type. The proposal considers the temptation to implement such a conversion by producing an array and explains why silently fixing that representation would be surprising.

This matters because an abstraction's apparent generality can suggest operations its contract does not provide. Knowing that values of one element type can convert individually does not establish that the original collection can change its associated type. The collection's operations and concrete representation still belong to its original conformance.

The constraint also does not expose the hidden concrete collection type. It preserves the stated relationship and leaves the remaining identity erased. A function that requires two collections to have the same concrete implementation needs a stronger relationship than merely sharing Element == String.

I find that separation useful when deciding what belongs in a public signature. Some APIs care about the exact implementation type because their operations connect several values of it. Others only care about the data being produced or consumed. The declaration can now express that second case without erasing the data type along with the implementation.

Regex literals make capture structure available to the compiler

SE-0354 brings a related kind of type information to patterns known in source. Runtime pattern construction is still useful for user-supplied searches. A literal lets the compiler inspect the pattern early, diagnose invalid syntax and infer capture types.

The extended delimiter form avoids relying on the bare slash syntax's language-mode requirements. A simple literal can name a capture and produce a corresponding labelled result.

let pattern = #/(?<code>[A-Z]+)/#
if let match = "READY".wholeMatch(of: pattern) {
    print(match.code)
}

The complete match is present in the output, and captured groups add components to its type. A named capture supplies a label, allowing match.code in this example. That relationship comes from the pattern's structure, instead of relying on a caller's memory that a particular numbered capture has a particular role.

Optional captures retain another useful distinction. The proposal wraps a capture in an optional when the group may be absent in a successful match, such as when the group occurs inside a zero-or-more repetition or one alternative. A capture containing an expression that matches empty text is different. The capture can exist with an empty substring, so its type need not be optional for that reason alone.

That distinction follows Swift's broader use of optionals to describe absence. The result's type can communicate that a successful overall match did not necessarily produce every capture. The application then handles missing capture values as an explicit case.

Extended delimiters also change how literal text is delimited without making regex escapes ordinary characters. SE-0354 distinguishes them from raw string literals in this respect. A backslash sequence inside an extended regex still has its regex meaning. Additional number signs can surround the delimiter when the pattern itself contains delimiter-like text. This lets a pattern containing forward slashes remain readable without changing the matching language used inside it.

Compiler implementation remains part of the release

The release announcement also reports a rewritten generics implementation in the type checker. It identifies fixes involving complex same-type requirements and improved scaling for certain protocol and associated-type configurations. The stated exponential-to-linear improvement concerns those configurations, not a promise that every package compiles in linear time.

That qualification matters when relating language expressiveness to compiler cost. Supporting more useful type relationships requires an implementation that handles them correctly. The release's compiler work and new generic syntax address different parts of that requirement, so adopting shorter notation alone should not be credited with an unmeasured build improvement.

Editor support receives related attention. SourceKit-LSP recomputes compiler arguments after changes to package and compilation configuration files, and a single server can manage multiple SwiftPM projects in one VS Code workspace. These changes help semantic services keep using the correct project information as a workspace changes.

For a function declaration, the practical choice remains specific. Use an unnamed opaque parameter when its one conformance requirement is sufficient. Preserve a named generic parameter when other parts of the signature must refer to the same concrete type. Constrain an existential's associated type when implementations should vary while the exchanged data remains fixed. Swift 5.7 gives each of those requirements a direct spelling.