Implementing `ExpressibleByStringLiteral`: can use either type `StringLiteralType`,`StaticString` or `String` to implement the require init

enum Foo: String {
    case bar = "blah"
}

// either this
extension Foo: ExpressibleByStringLiteral {
    init(stringLiteral value: StringLiteralType) {
        guard let foo = Self(rawValue: value) else {
            fatalError("enum \(Self.self): \(value) is not valid")
        }
        self = foo
    }
}

// or this
extension Foo: ExpressibleByStringLiteral {
    init(stringLiteral value: StaticString) {
        guard let foo = Self(rawValue: "\(value)") else {
            fatalError("enum \(Self.self): \(value) is not valid")
        }
        self = foo
    }
}
  1. Why StringLiteralType can be used? It's associatedtype StringLiteralType. So how does the compiler infer what is StringLiteralType?

  2. Is there any advantage to use StaticString if you must then convert it to String?

  3. Self(rawValue: value) works. But not self.init(rawValue: value). Why?

Does the compiler give special treatment to ExpressibleByStringLiteral? Maybe this is why?

No

The latter is a failable initializer, meaning it could return nil. But init(stringLiteral) is not failable.

I'm calling it in the guard let clause to handle in case of it returning nil. Isn't self.init(rawValue: value) and Self(rawValue: value) the exact same thing? What I don't understand is:

this works:

 guard let foo = Self(rawValue: value) else { ... }

This do not compile:

 guard let foo = self.init(rawValue: value) else { ... }

So maybe I am wrong and these two are not the same: it's delegating.

But the most puzzling thing is why I can use StringLiteralType in the require init for ExpressibleByStringLiteral:

init(stringLiteral value: StringLiteralType) { ... }

Usually you have to use a suitable actual type like String for the associatedtype StringLiteralType of this protocol. Is it not? Why can StringLiteralType be used here?

Pay attention to this error:

Initializer delegation ('self.init') cannot be nested in another statement

self.init does not return an instance of Self (or Self?), so you can't use it in another expression. This doesn't work either:

let x = self.init(rawValue: "")

I don't know what a "suitable actual type" is. Just read the documentation and it tells you what the allowed types for StringLiteralType are:

/// A type that can be initialized with a string literal.
///
/// The `String` and `StaticString` types conform to the
/// `ExpressibleByStringLiteral` protocol. You can initialize a variable or
/// constant of either of these types using a string literal of any length.
///
///     let picnicGuest = "Deserving porcupine"
///
/// Conforming to ExpressibleByStringLiteral
/// ========================================
///
/// To add `ExpressibleByStringLiteral` conformance to your custom type,
/// implement the required initializer.
public protocol ExpressibleByStringLiteral : ExpressibleByExtendedGraphemeClusterLiteral {

    /// A type that represents a string literal.
    ///
    /// Valid types for `StringLiteralType` are `String` and `StaticString`.
    associatedtype StringLiteralType : _ExpressibleByBuiltinStringLiteral

    /// Creates an instance initialized to the given string value.
    ///
    /// - Parameter value: The value of the new instance.
    init(stringLiteral value: Self.StringLiteralType)
}
1 Like

I see what's going on:

associatedtype StringLiteralType : _ExpressibleByBuiltinStringLiteral

I was only looking at the doc page and it shows:

associatedtype StringLiteralType

So I was wondering how did the compiler infer what StringLiteralType is? But looks like _ExpressibleByBuiltinStringLiteral is the magic that let it be any string'y type.

What is this type of associatedtype declaration? "Fully constrained"/"Fully specified"?

You're not looking at the right StringLiteralType. There's a default literal typealias:

By conforming to ExpressibleByStringLiteral with an initializer that takes an argument of type StringLiteralType, what you're doing is allowing Foo.StringLiteralType to be inferred as StringLiteralType (aka String, unless you shadow the typealias with another value).

Oh no. Now I'm confuse:

What is this:

associatedtype StringLiteralType : _ExpressibleByBuiltinStringLiteral

??

If StringLiteralType is simply String, then why not this:

init(stringLiteral value: Self.StringLiteralType)

simply:

init(stringLiteral value: String)

?

It seem either StaticString and String work in this init

That's the associated type StringLiteralType, which needs to be defined for every type that conforms to ExpressibleByStringLiteral. You can choose any built-in type that's expressible by a string literal to fulfill that requirement, which includes String and StaticString. You can also choose to fulfill that requirement with the typealias StringLiteralType, which by default is just String.

What is _ExpressibleByBuiltinStringLiteral? It seems it means StaticString | String, not something user can express. I only know Type1 & Type2.

That is an internal protocol to which StaticString and String both conform.

2 Likes

When you write init(stringLiteral value: StaticString) { ... }, Swift knows that the type of the value parameter—StaticString in this case—must be the type you've chosen for the StringLiteralType.

I'm talking about when writing the init this way:

init(stringLiteral value: StringLiteralType) { ... }

It confuse me that it works because StringLiteralType is the associatedtype of this protocol, so what's its type here? But @xwu point out above: the standard library defines a typealias typealias StringLiteralType = String. So it's actually equivalent to:

init(stringLiteral value: String) { ... }