> On Mar 10, 2017, at 8:49 AM, Joe Groff <[email protected]> wrote:
>
> I think there's a more powerful alternative design you should also consider.
> If the protocol looked like this:
>
> protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
> associatedtype LiteralSegment: ExpressibleByStringLiteral
> associatedtype InterpolatedSegment
> init(forStringInterpolation: Void)
>
> mutating func append(literalSegment: LiteralSegment)
> mutating func append(interpolatedSegment: InterpolatedSegment)
> }
>
> Then an interpolation expression like this in `Thingy` type context:
>
> "foo \(bar) bas \(zim: 1, zang: 2)\n"
>
> could desugar to something like:
>
> {
> var x = Thingy(forStringInterpolation: ())
> // Literal segments get appended using append(literalSegment: "literal")
> x.append(literalSegment: "foo ")
> // \(...) segments are arguments to a InterpolatedSegment constructor
> x.append(interpolatedSegment: Thingy.InterpolatedSegment(bar))
> x.append(literalSegment: " bas ")
> x.append(interpolatedSegment: Thingy.InterpolatedSegment(zim: 1, zang: 2))
>
> return x
> }()
>
> This design should be more efficient, since there's no temporary array of
> segments that needs to be formed for a variadic argument, you don't need to
> homogenize everything to Self type up front, and the string can be built up
> in-place. It also provides means to address problems 3 and 4, since the
> InterpolatedSegment associated type can control what types it's initializable
> from, and can provide initializers with additional arguments for formatting
> or other purposes.
On the other hand, you end up with an `init(forStringInterpolation: ())`
initializer which is explicitly intended to return an incompletely initialized
instance. I don't enjoy imagining this. For instance, you might find yourself
having to change certain properties from `let` to `var` so that the `append`
methods can operate.
If we *do* go this direction, though, I might suggest a slightly different
design which uses fewer calls and makes the finalization explicit:
protocol ExpressibleByStringLiteral {
associatedtype StringLiteralSegment: ExpressibleByStringLiteral
init(startingStringLiteral: ())
func endStringLiteral(with segment: StringLiteralSegment)
}
protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
associatedtype StringInterpolationSegment
func continueStringLiteral(with literal: StringLiteralSegment,
followedBy interpolation: StringInterpolationSegment)
}
Your `"foo \(bar) bas \(zim: 1, zang: 2)\n"` example would then become:
{
var x = Thingy(startingStringLiteral: ())
x.continueStringLiteral(with: "Foo ", followedBy: .init(bar))
x.continueStringLiteral(with: " bas ", followedBy: .init(zim:
1, zang: 2))
x.endStringLiteral(with: "\n")
return x
}
While a plain old string literal would have a more complicated pattern than
they do currently, but one which would have completely compatible semantics
with an interpolated string:
{
var x = Thingy(startingStringLiteral: ())
x.endStringLiteral(with: "Hello, world!")
return x
}
* * *
Another possible design would separate the intermediate type from the final
one. For instance, suppose we had:
protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
associatedtype StringInterpolationBuffer = Self
associatedtype StringInterpolationType
static func makeStringLiteralBuffer(startingWith
firstLiteralSegment: StringLiteralType) -> StringLiteralBuffer
static func appendInterpolationSegment(_ expr:
StringInterpolationType, to stringLiteralBuffer: inout StringLiteralBuffer)
static func appendLiteralSegment(_ string: StringLiteralType,
to stringLiteralBuffer: inout StringLiteralBuffer)
init(stringInterpolation buffer: StringInterpolationBuffer)
}
// Automatically provide a parent protocol conformance
extension ExpressibleByStringInterpolation {
init(stringLiteral: StringLiteralType) {
let buffer = Self.makeStringLiteralBuffer(startingWith:
stringLiteral)
self.init(stringInterpolation: buffer)
}
}
Then your example would be:
{
var buffer = Thingy.makeStringLiteralBuffer(startingWith: "foo
")
Thingy.appendInterpolationSegment(Thingy.StringInterpolationSegment(bar), to:
&buffer)
Thingy.appendLiteralSegment(" bas ", to: &buffer)
Thingy.appendInterpolationSegment(Thingy.StringInterpolationSegment(zim: 1,
zang: 2), to: &buffer)
Thingy.appendLiteralSegment("\n", to: &buffer)
return Thingy(stringInterpolation: x)
}()
For arbitrary string types, `StringInterpolationBuffer` would probably be
`Self`, but if you had something which could only create an instance of itself
once the entire literal was gathered together, it could use `String` or `Array`
or whatever else it wanted.
* * *
One more design possibility. Would it make sense to handle all the segments in
a single initializer call, instead of having one call for each segment, plus a
big call at the end? Suppose we did this:
protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
associatedtype StringInterpolationType
init(stringInterpolation segments:
StringInterpolationSegment...)
}
@fixed_layout enum StringInterpolationSegment<StringType:
ExpressibleByStringInterpolation> {
case literal(StringType.StringLiteralType)
case interpolation(StringType.StringInterpolationType)
}
extension ExpressibleByStringInterpolation {
typealias StringInterpolationSegment =
Swift.StringInterpolationSegment<Self>
init(stringLiteral: StringLiteralType) {
self.init(stringInterpolation: .literal(stringLiteral))
}
}
Code pattern would look like this:
Thingy(stringInterpolation:
.literal("Foo "),
.interpolation(.init(bar)),
.literal(" bas "),
.interpolation(.init(zim: 1, zang: 2)),
.literal("\n")
)
I suppose it just depends on whether the array or the extra calls are more
costly. (Well, it also depends on whether we want to be expanding single
expressions into big, complex, multi-statement messes like we discussed before.)
(Actually, I realized after writing this that you mentioned a similar design
downthread. Oops.)
* * *
As for validation, which is mentioned downthread: I think we will really want
plain old string literal validation to happen at compile time. Doing that in a
general way means macros, so that's just not in the cards yet.
However, once we *do* have that, I think we can actually handle
runtime-failable interpolated literals pretty easily. For this example, I'll
assume we adopt the `StringInterpolationSegment`-enum-based option, but any of
them could be adapted in the same way:
protocol ExpressibleByFailableStringInterpolation:
ExpressibleByStringLiteral {
associatedtype StringInterpolationType
init(stringInterpolation: StringInterpolationSegment...)
}
extension ExpressibleByFailableStringInterpolation {
typealias StringInterpolationSegment =
Swift.StringInterpolationSegment<Self?>
init(stringLiteral: StringLiteralType) {
self.init(stringInterpolation segments:
.literal(stringLiteral))
}
}
extension Optional: ExpressibleByStringInterpolation where Wrapped:
ExpressibleByFailableStringInterpolation {
typealias StringLiteralType = Wrapped.StringLiteralType
typealias StringInterpolationType =
Wrapped.StringInterpolationType
init(stringInterpolation segments:
StringInterpolationSegment...) {
self = Wrapped(stringInterpolation: segments)
}
}
If we think we'd rather support throwing inits instead of failable inits, that
could be supported directly by `ExpressibleByStringInterpolation` if we get
throwing types and support `Never` as a "doesn't throw" type.
* * *
Related question: Can the construction of the variadic parameter array be
optimized? For instance, the arity of any given call site is known at compile
time; can the array buffer be allocated on the stack and somehow marked so that
attempting to retain it (while copying the `Array` instance) will copy it into
the heap? (Are we doing that already?) I suspect that would make variadic calls
a lot cheaper, perhaps enough so that we just don't need to worry about this
problem at all.
--
Brent Royal-Gordon
Architechies
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution