> On Mar 10, 2017, at 11:17 PM, Jacob Bandes-Storch <[email protected]> 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?
> 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
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution