[swift-evolution] ExpressibleByStringInterpolation vs. String re-evaluation vs. Regex

Brent Royal-Gordon brent at architechies.com
Tue Aug 2 20:10:56 CDT 2016

> On Jul 30, 2016, at 10:35 PM, Jacob Bandes-Storch via swift-evolution <swift-evolution at swift.org> wrote:
> In the past, there has been some interest in refining the behavior of ExpressibleByStringInterpolation (née StringInterpolationConvertible), for example:
> - Ability to restrict the types that can be used as interpolation segments
> - Ability to distinguish the string-literal segments from interpolation segments whose type is String
> Some prior discussions: 
> - "StringInterpolationConvertible and	StringLiteralConvertible inheritance" https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160516/017654.html
> - Sub-discussion in "Allow multiple conformances to the same protocol" https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160606/020746.html
> - "StringInterpolationConvertible: can't distinguish between literal components and String arguments"  https://bugs.swift.org/browse/SR-1260 / rdar://problem/19800456&18681780
> - "Proposal: Deprecate optionals in string interpolation" https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160516/018000.html
> About Swift 4, Chris wrote:
>  - String re-evaluation: String is one of the most important fundamental types in the language.  The standard library leads have numerous ideas of how to improve the programming model for it, without jeopardizing the goals of providing a unicode-correct-by-default model.  Our goal is to be better at string processing than Perl!
> I'd be interested in any more detail the team can provide on this. I'd like to talk about string interpolation improvements, but it wouldn't be wise to do so without keeping an eye towards possible regex/pattern-binding syntax, and the String refinements that the stdlib team has in mind, if there's a chance they would affect interpolation.
> Discuss!

I'm not one of the core team, so all I can really provide is a use case.

Given a LocalizedString type like:

    /// Conforming types can be included in a LocalizedString.
    protocol LocalizedStringConvertible {
        /// The format to use for this instance. This format string will be included in the key when
	/// this type is interpolated into a LocalizedString.
        var localizedStringFormat: String { get }
        /// The arguments to use when formatting to represent this instance.
        var localizedStringArguments: [CVarArg] { get }

    extension NSString: LocalizedStringConvertible {…}
    extension String: LocalizedStringConvertible {…}
    extension LocalizedString: LocalizedStringConvertible {…}
    extension Int: LocalizedStringConvertible {…}
    // etc.

    struct LocalizedString {
        /// Initializes a LocalizedString by applying the `arguments` to the format string with the 
        /// indicated `key` using `String.init(format:arguments:)`.
        /// If the `key` does not exist in the localized string file, the `key` itself will be used as 
        /// the format string.
        init(key: String, formattedWith arguments: [CVarArg]) {…}
    extension String {
        init(_ localizedString: LocalizedString) {
            self.init(describing: localizedString)

    extension LocalizedString {
        /// Initializes a LocalizedString with no arguments which uses the indicated `key`. `%` 
        /// characters in the `key` will be converted to `%%`.
        /// If the `key` does not exist in the localized string file, the `key` itself will be used as 
        /// the string.
        init(key: String) {…}
        /// Initializes a LocalizedString to represent the indicated `value`.
        init(_ value: LocalizedStringConvertible) {…}
        /// Initializes a LocalizedString to represent the empty string.
        init() {…}

    extension LocalizedString: CustomStringConvertible {…}
    extension LocalizedString: ExpressibleByStringLiteral {
        init(stringLiteral value: String) {
            self.init(key: value)

The current ExpressibleByStringInterpolation protocol has a number of defects.

	1. We want to only permit LocalizedStringConvertible types, or at least *use* the LocalizedStringConvertible conformance; neither of these appears to be possible. (`is` and `as?` casts always fail, overloads don't seem to be called, etc.)

	2. The literal parts of the string are interpreted using `String`'s `ExpressibleByStringLiteral` conformance; we really want them to use `LocalizedString`'s instead. 

	3. We don't want the literal parts of the string to pass through `init(stringInterpolationSegment:)`, because we want to treat interpolation and literal segments differnetly.

In other words, we want to be able to write something like this:

	extension LocalizedString: ExpressibleByStringInterpolation {
		typealias StringInterpolatableType = LocalizedStringConvertible
		init(stringInterpolation segments: LocalizedString) {
			for segment in segments {
				formatKey += segment.formatKey
				arguments += segment.arguments
		init(stringInterpolationSegment expr: LocalizedStringConvertible) {

And change the code generated by the compiler from (given the statement `"foo \(bar) baz" as LocalizedString`) this:

		LocalizedString(stringInterpolationSegment: String(stringLiteral: "foo ")),
		LocalizedString(stringInterpolationSegment: bar),
		LocalizedString(stringInterpolationSegment: String(stringLiteral: " baz"))

To this:

		LocalizedString(stringLiteral: "foo "),
		LocalizedString(stringInterpolationSegment: bar),
		LocalizedString(stringLiteral: " baz")

This would obviously require a few changes to the ExpressibleAsStringInterpolation protocol:

	// You cannot accept interpolations unless you can also be a plain literal.
	// Necessary for literal segments.
	protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
		// An associated type for the type of a permitted interpolation
		associatedtype StringInterpolatableType = Any
		// No changes here
		init(stringInterpolation segments: Self...)
		// No longer generic; instead uses StringInterpolatableType existentials.
		// Also a semantic change: this is only called for the actual interpolations.
		// init(stringLiteral:) is called for literal segments.
		init(stringInterpolationSegment expr: StringInterpolatableType)
		// Given the change in roles, we might want to consider renaming the initializers:
		// init(stringInterpolation:) => init(combinedStringLiteral:) or init(stringInterpolationSegments:)
		// init(stringInterpolationSegment:) => init(stringInterpolation:)

Or perhaps we would hoist the combining initializer up into ExpressibleAsStringLiteral, and generate an `init(combinedStringLiteral:)` call every time string literals are used.

	protocol ExpressibleByStringLiteral {
		associatedtype StringLiteralType: _ExpressibleByBuiltinStringLiteral = String
		init(stringLiteralSegments segments: Self...)
		init(stringLiteral value: StringLiteralType)

	protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
		associatedtype StringInterpolatableType = Any
		init(stringInterpolation expr: StringInterpolatableType)

	// "foo" as LocalizedString
		LocalizedString(stringLiteral: "foo")
	// "foo \(bar) baz" as LocalizedString
		LocalizedString(stringLiteral: "foo "),
		LocalizedString(stringInterpolation: bar),
		LocalizedString(stringLiteral: " baz")

Now, it's quite possible--perhaps even likely--that there are really good reasons for the current design. But I've been thinking about this for two years and I don't know what they are yet; nor can I find much relevant design documentation. I, too, would love to find out why the current design was selected.

Brent Royal-Gordon

More information about the swift-evolution mailing list