[swift-evolution] [Draft] Fix ExpressibleByStringInterpolation

Jacob Bandes-Storch jtbandes at gmail.com
Sat Mar 11 13:53:15 CST 2017

On Sat, Mar 11, 2017 at 5:34 AM, Brent Royal-Gordon <brent at architechies.com>

> > On Mar 10, 2017, at 11:17 PM, Jacob Bandes-Storch <jtbandes at gmail.com>
> wrote:
> >
> > I'm confused by this example — was ExpressibleByFailableStringInterpolation's
> init() supposed to be failable here?
> Ugh, yes, I'm sorry. That should have been:
>         protocol ExpressibleByFailableStringInterpolation:
> ExpressibleByStringLiteral {
>                 associatedtype StringInterpolationType
>                 init?(stringInterpolation: StringInterpolationSegment...)
>         }
>         ...
>         extension Optional: ExpressibleByStringInterpolation where
> Wrapped: ExpressibleByFailableStringInterpolation {
>                 typealias StringLiteralType = Wrapped.StringLiteralType
>                 typealias StringInterpolationType =
> Wrapped.StringInterpolationType
>                 init(stringInterpolation segments:
> StringInterpolationSegment...) {
>                         self = Wrapped(stringInterpolation: segments)
>                 }
>         }
> > Just to throw out another idea, what about keeping the entirety of the
> string in one contiguous block and providing String.Indexes to the
> initializer?
> >
> > protocol ExpressibleByStringInterpolation {
> >     associatedtype Interpolation
> >     init(_ string: String, with interpolations: (String.Index,
> Interpolation)...)
> > }
> I've thought about that too. It's a little bit limiting—you have no choice
> except to use `String` as your input type. Also, the obvious way to use
> these parameters:
>         init(stringLiteral string: String, with interpolations:
> (String.Index, Interpolation)...) {
>                 var copy = string
>                 for (i, expr) in interpolations {
>                         let exprString = doSomething(with: expr)
>                         copy.insert(exprString, at: i)
>                 }
>                 self.string = copy
>         }
> Will probably be slow, since you're inserting into the middle instead of
> appending to the end. Obviously a clever programmer can avoid doing that,
> but why create the attractive nuisance in the first place?

It's also easy to get wrong ;-)  Docs for insert(_:at:) say "Calling this
method invalidates any existing indices for use with this string." And even
if they weren't invalidated, but simple numerical indices as into an Array,
you'd need to use them offsetBy however much content you'd inserted so far.

> > On the other hand, unless I've missed something, it seems like most of
> the suggestions so far are assuming that for any
> ExpressibleByStringInterpolation type, the interpolated values' types
> will be homogeneous. In the hypothetical printf-replacement case, you'd
> really want the value types to depend on the format specifiers, so that a
> Float couldn't be passed to %d without explicitly converting to an integer
> type.
> >
> > Although I suppose that could simply be achieved with a typealias
> Interpolation = enum { case f(Float), d(Int), ... }
> Yup. Given Swift's current feature set, the only way to design this is to
> have a single type which all interpolations funnel through. That type could
> be `Any`, of course, but that doesn't really help anybody.
> If we had variadic generics, you could of course have a variadic
> initializer with heterogeneous types. And I've often given thought to a
> "multiple associated types" feature where a protocol could be conformed to
> multiple times by specifying more than one concrete type for specific
> associated types. But these are both exotic features. In their absence, an
> enum (or something like that) is probably the best choice.
> * * *
> I'm going to try to explore some of these other designs, but they all seem
> to assume the new formatting system I sketched out in "future directions",
> so I implemented that first:
>         https://github.com/brentdax/swift/compare/new-interpolation.
> ..brentdax:new-interpolation-formatting
> The switch to `\(describing: foo)` has more impact than I expected; just
> the code that's built by `utils/build-script`—not including tests—has over
> a hundred lines with changes like this:
> -    expectationFailure("\(lhs) < \(rhs)", trace: ${trace})
> +    expectationFailure("\(describing: lhs) < \(describing: rhs)", trace:
> ${trace})
> On the other hand, I like what it does to other formatting (I've only
> applied this kind of change in a few places):
> -    return "CollectionOfOne(\(String(reflecting: _element)))"
> +    return "CollectionOfOne(\(reflecting: _element))"
> And it *does* make you think about whether you want to use `describing:`
> or `reflecting:`:
> -    expectEqual(expected, actual, "where the argument is: \(a)")
> +    expectEqual(expected, actual, "where the argument is: \(describing:
> a)")
> And, thanks to LosslessStringConvertible, it also does a pretty good job
> of calling out the difference between interpolations that will probably
> look good to a user and ones that will look a little funny:
> -      return "watchOS(\(major).\(minor).[\(bugFixRange)], reason:
> \(reason))"
> +      return "watchOS(\(major).\(minor).[\(describing: bugFixRange)],
> reason: \(reason))"
> All in all, it's a bit of a mixed bag:
> -          return "<\(type(of: x)): 0x\(String(asNumericValue(x), radix:
> 16, uppercase: false))>"
> +          return "<\(describing: type(of: x)): 0x\(asNumericValue(x),
> radix: 16, uppercase: false)>"
> We could probably improve this situation with a few targeted
> `String.init(_:)`s for things like type names, `Error` instances, and
> `FloatingPoint` types. (Actually, I think that `FloatingPoint` should
> probably conform to `LosslessStringConvertible`, but that's a different
> story.) Possibly `Array`s of `LosslessStringConvertible` types as well.
> But ultimately, this might just be too source-breaking. If it is, we'll
> need to think about changing the design.
> The simplest fix is to add a leading parameter label if there isn't
> one—that is, `\(foo)` becomes `.init(formatting: foo)`—but then you lose
> the ability to use full-width initializers which are already present and
> work well outside of interpolation. Perhaps we could hack overload checking
> so that, if a particular flag is set on a call, it will consider both
> methods with *and* without the first parameter label? But that's kind of
> bizarre, and probably above my pay grade to implement.
> In any case, I really think this is in the right general direction, and
> with it done, I can start exploring some of the alternatives we've
> discussed here. I'm hoping to build several and run the string
> interpolation benchmark against them—we'll see how that goes.
> --
> Brent Royal-Gordon
> Architechies
I still have the feeling that using full-on argument labels is too wordy
for string interpolation. The grammar is a bit awkward: "a String
describing x" makes sense when reading String(describing: x), but
"\(describing: x)" has no subject. And, at least for number formatting,
it's nowhere near the concision of printf-style format specifiers. Although
\() into T.init() does seem like a very obvious way of allowing more than
just a single argument per interpolation — maybe what I long for is not a
better core syntax, but a great DSL built on top of it. I'm struggling to
come up with anything that would be particularly ergonomic.

enum FloatSpec { case pad(Character), width(Int), ... }
init<F: FloatingPoint>(_ value: F, _ specs: FloatSpec...)
"\(value, .width(10), .pad(4))"

However, your proposal as written is a great step!
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170311/7ef32ba2/attachment.html>

More information about the swift-evolution mailing list