Let's say I want to add an HTML type to my Swift project which encapsulates escaping behavior.
struct HTML {
var rawString: String
init(rawString: String) {
self.rawString = rawString
}
init(_ unescapedValue: String) {
self.init(rawString: escapeHTML(unescapedValue))
}
}Great—now when I type HTML(someString), someString is escaped automatically. I can type HTML(rawString: someString) if there are tags and stuff. I can even add concatenation operators and stuff to make this type work a lot like String.
If a parameter takes HTML, I want to be able to use a string literal, so I can write "<img src='wat.png'>" without having to do the whole "HTML(rawString:)" rigamarole. No problem:
extension HTML: StringLiteralConvertible {
init(stringLiteral value: StaticString) {
self.init(rawString: value.stringValue)
}
[similar initializers omitted]
}What I really want to do, though, is use string interpolation to construct my HTML, like "<img src='\(imageName)'>". If imageName is a String, I want it escaped; if it's an HTML, its escaping has already been handled, so I don't want to do it again. That's what StringInterpolationConvertible is for, right?
extension HTML: StringInterpolationConvertible {
init(stringInterpolation strings: HTML...) {
self = strings.reduce(HTML(""), combine: +)
}
init(stringInterpolationSegment expr: HTML) {
self.init(rawString: expr.rawString)
}
init<T>(stringInterpolationSegment expr: T) {
self.init(String(expr))
}
}Nope.
See that init<T>(stringInterpolationSegment:)? That means Swift is free to choose any type. And when Swift can make a string literal be any type, it always makes it a StringLiteralType, which of course is Swift.String by default. So both the literal and interpolated parts of the string become Strings, both go through init<T>(stringInterpolationSegment: T), and both are escaped.
Okay, so let's write an HTMLConvertible protocol and handle it there:
protocol HTMLConvertible {
func asHTML() > HTML
}
extension HTML: HTMLConvertible {
func asHTML() -> HTML { return self }
}
extension String: HTMLConvertible {
func asHTML() -> HTML { return HTML(self) }
}
extension HTML {
init(stringInterpolationSegment expr: HTMLConvertible) {
self.init(expr.asHTML().rawString))
}
}Nope. Still goes through the maximum-generic <T> prefers to pass a String, not an HTML. The same thing happens if you do init<T: HTMLConvertible>, and if you remove String's conformance, it just passes a String to <T> instead.
Alright, let's do a type check:
init<T>(stringInterpolationSegment expr: T) {
if let convertible = expr as? HTMLConvertible {
self.init(rawString: convertible.asHTML().rawString)
}
else {
self.init(String(expr))
}
}Nope. The HTMLConvertible branch is never taken.
What does work, kind of, is typealiasing StringLiteralType to something besides String, like HTML or StaticString, and making sure I have an init(stringInterpolationSegment:) that matches that type. But this has enormous ramifications all through the code, so that doesn't seem like a great idea.
Basically, I'm left looking at this and going "what the ****?" It seems like all, or almost all, of these ought to work, but at every step Swift stymies me. I particularly can't understand why literals are fed through init(stringInterpolationSegment:) at all (if they weren't, the loose <T> typing wouldn't become a factor), why Swift prefers init<T> over init<T: HTMLConvertible> when the latter has a more specific type, why the type check in init<T> doesn't work, and ultimately, what in the world Swift's designers think I ought to be doing here.
As it stands, StringInterpolationConvertible seems completely useless except to Swift.String. But I have a very, very hard time believing that that's what Swift's designers intended. After all, if that's what they wanted, they could've just had the compiler convert interpolations to concatenation operators and been done with it.
Does anyone have working code that does this sort of thing, or ideas for other workarounds? Anybody on the Swift team want to comment?