📖

SwiftのLiteralとLiteral Protocol

に公開

Literal

Swiftには様々なLiteralがあります。

nil // NilLiteral
10 // IntegerLiteral
true // BooleanLiteral
"hello" // StringLiteral
[] // ArrayLiteral

ここで大事なのが、Swiftにおいて「Literal」と「Literalから生成される値の型」は別の概念だということです。

https://qiita.com/koher/items/cbe232782b9943715c9b

SwiftにおいてLiteralはどんな型の値でも生成することができます。そのため、Literalの記述だけを見て「XX型だ」と言い切ることはできません。
例えばSwiftUI.TextStringLiteralを用いて初期化するコードを考えてみます。

Text("Hello")

ここで、"Hello"から結果的に生成される値の型はStringではありません。LocalzedStringKeyです。

選択されているinit

init(
    _ key: LocalizedStringKey,
    tableName: String? = nil,
    bundle: Bundle? = nil,
    comment: StaticString? = nil
)

このLocalizedStringKeyのようにLiteralから初期化可能な型を作るためには、型をLiteral Protocolに準拠させる必要があります。
例えばLocalizedStringKeyExpressibleByStringInterpolationに準拠しています。

Literal Protocol

例として、以下のようにIntegerLiteralからStringを初期化できるようにしてみます。

let string: String = 10

この時StringExpressibleByIntegerLiteralに準拠させる必要があります。
まず、ExpressibleByIntegerLiteralの定義を見てみます。

public protocol ExpressibleByIntegerLiteral {
  /// The standard library integer and floating-point types are all valid types
  /// for `IntegerLiteralType`.
  associatedtype IntegerLiteralType: _ExpressibleByBuiltinIntegerLiteral
  init(integerLiteral value: IntegerLiteralType)
}

IntegerLiteralTypeというassociatedTypeがあります。これはLiteralから変換する際の中間表現に使う型です。さらにこの中間表現には_ExpressibleByBuiltinIntegerLiteralの制約があります。
究極的(直接的)にLiteralから型に変換するにはコンパイラ上で実装が必要なので、コンパイラ上に実装があり、Literalから直接変換できる型(=_ExpressibleByBuiltinIntegerLiteral)の中間表現を一旦噛ます必要があるわけです。
(厳密にはコンパイラ上で実装があってLiteralから直接変換できるのはBuiltin.IntX型とかで、IntとかはそのBuiltin.IntXを内部表現として持つので、Literalから直接変換できる=_ExpressibleByBuiltinIntegerLiteral、という構造。)

ExpressibleByIntegerLiteralのケースだと(コメントにあるように)stdlibの整数型と浮動小数点型が_ExpressibleByBuiltinIntegerLiteralに準拠しています。

実際に実装してみると以下のようになります。

extension String: @retroactive ExpressibleByIntegerLiteral {
    // LiteralをIntで一旦受け取り、Stringに変換する
    // Int32でもUIntでもFloatでもOK
    public init(integerLiteral value: Int) {
        self.init(describing: value)
    }
}

// `10`(IntegerLiteral)が
// `_ExpressibleByBuiltinIntegerLiteral`によってIntに変換された後
// `ExpressibleByIntegerLiteral`によってStringに変換される
let number: String = 10
print(number) // 10

以上がLiteral Protocolの概要です。Literal/Literal Protocolは複数ありますが、どれも中間表現が必要な点と、準拠の流れは基本的には同じです。

Literal Protocol一覧

Swift 6.1時点でのLiteral Protocol一覧を簡単にまとめます。(RegexLiteralはLiteral Protocolがないので除外)
参考元は CompilerProtocols.swift

ExpressibleByNilLiteral

nil

定義

public protocol ExpressibleByNilLiteral: ~Copyable, ~Escapable {
  @lifetime(immortal)
  init(nilLiteral: ())
}

中間表現は固定で() (Void)

利用例

enum JSON {
    case null
}
extension JSON: CustomStringConvertible {
    var description: String {
        switch self {
        case .null: "null"
        }
    }
}
extension JSON: ExpressibleByNilLiteral {
    init(nilLiteral: ()) {
        self = .null
    }
}

let value: JSON = nil
print(value) // null

ExpressibleByIntegerLiteral

10   // 10進数
0b10 // 2進数
0o07 // 8進数
0x0A // 16進数

定義

public protocol ExpressibleByIntegerLiteral {
  /// The standard library integer and floating-point types are all valid types
  /// for `IntegerLiteralType`.
  associatedtype IntegerLiteralType: _ExpressibleByBuiltinIntegerLiteral

  init(integerLiteral value: IntegerLiteralType)
}

中間表現は_ExpressibleByBuiltinIntegerLiteral: stdlibの整数型と浮動小数点型

利用例

enum JSON {
    case null
    case int(Int)
}
extension JSON: CustomStringConvertible {
    var description: String {
        switch self {
        ...
        case .int(let int): int.description
        }
    }
}
extension JSON: ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        self = .int(value)
    }
}

let value: JSON = 10
print(value) // 10

ExpressibleByFloatLiteral

1.0 // 10進数
1e4 // 10進数指数
0x1p4 // 16進数表記+2進数指数

定義

public protocol ExpressibleByFloatLiteral {
  /// Valid types for `FloatLiteralType` are `Float`, `Double`, and `Float80`
  /// where available.
  associatedtype FloatLiteralType: _ExpressibleByBuiltinFloatLiteral

  init(floatLiteral value: FloatLiteralType)
}

中間表現は_ExpressibleByBuiltinFloatLiteral: stdlibの浮動小数点型Float, Double, Float80

利用例

enum JSON {
    case null
    case int(Int)
    case double(Double)
}
extension JSON: CustomStringConvertible {
    var description: String {
        switch self {
        ...
        case .double(let double): double.description
        }
    }
}
extension JSON: ExpressibleByFloatLiteral {
    init(floatLiteral value: Double) {
        self = .double(value)
    }
}

let value: JSON = 10.0
print(value) // 10.0

ExpressibleByBooleanLiteral

false
true

定義

public protocol ExpressibleByBooleanLiteral {
  /// A type that represents a Boolean literal, such as `Bool`.
  associatedtype BooleanLiteralType: _ExpressibleByBuiltinBooleanLiteral

  init(booleanLiteral value: BooleanLiteralType)
}

中間表現はExpressibleByBooleanLiteral: Boolだけ

利用例

enum JSON {
    case null
    case int(Int)
    case double(Double)
    case bool(Bool)
}
extension JSON: CustomStringConvertible {
    var description: String {
        switch self {
        ...
        case .bool(let bool): bool.description
        }
    }
}
extension JSON: ExpressibleByBooleanLiteral {
    init(booleanLiteral value: Bool) {
        self = .bool(value)
    }
}

let value: JSON = false
print(value)

ExpressibleByUnicodeScalarLiteral

UnicodeScalarLiteralは1つのUnicode Scalar(コードポイント)のみで構成されるStringLiteralのこと。

"a" // U+0061 ✅ Unicode Scalar一つ
"ab" // U+0061 U+0062 🔴 二文字, Unicode Scalar二つなのでUnicodeScalarLiteralではない
"🔴" // U+1F534 ✅ Unicode Scalar一つで表現される絵文字
"🖐️" // U+1F590 U+FE0F  🔴 Unicode Scalar二つで表現される絵文字なのでUnicodeScalarLiteralではない
"\(1)" // 🔴 String InterpolationはUnicodeScalarLiteralではない

UnicodeとSwiftのUnicode表現についてはTSPL-Unicode-Representations-of-Stringsら辺を参考

定義

public protocol ExpressibleByUnicodeScalarLiteral {
  /// Valid types for `UnicodeScalarLiteralType` are `Unicode.Scalar`,
  /// `Character`, `String`, and `StaticString`.
  associatedtype UnicodeScalarLiteralType: _ExpressibleByBuiltinUnicodeScalarLiteral

  init(unicodeScalarLiteral value: UnicodeScalarLiteralType)
}

中間表現は_ExpressibleByBuiltinUnicodeScalarLiteral: Unicode.Scalar, Character, String, StaticString

利用例

実用的な利用例が思いつかなかったので適当な実装

struct MyUnicodeScalar: ExpressibleByUnicodeScalarLiteral {
    var value: UnicodeScalar

    init(unicodeScalarLiteral value: UnicodeScalar) {
        self.value = value
    }
}

let a: MyUnicodeScalar = "a"

ExpressibleByExtendedGraphemeClusterLiteral

ExtendedGraphemeClusterLiteralは1つのCharacterで構成されるStringLiteral。
つまり実質ExpressibleByCharacterLiteral。(Swift.Characterはextended grapheme clusterの表現であるため。)

https://developer.apple.com/documentation/swift/character

"a" // U+0061 ✅ Unicode Scalar一つで一文字
"ab" // U+0061 U+0062 🔴 二文字なのでExtendedGraphemeClusterLiteralではない
"🔴" // U+1F534 ✅ Unicode Scalar一つで表現される絵文字
"🖐️" // U+1F590 U+FE0F  ✅  Unicode Scalar二つだが一文字
"\(1)" // 🔴 String InterpolationはExtendedGraphemeClusterLiteralではない

なおExpressibleByExtendedGraphemeClusterLiteralExpressibleByUnicodeScalarLiteralの子protocolである。

定義

public protocol ExpressibleByExtendedGraphemeClusterLiteral
  : ExpressibleByUnicodeScalarLiteral {
  /// Valid types for `ExtendedGraphemeClusterLiteralType` are `Character`,
  /// `String`, and `StaticString`.
  associatedtype ExtendedGraphemeClusterLiteralType
    : _ExpressibleByBuiltinExtendedGraphemeClusterLiteral
  
  init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType)
}

中間表現は_ExpressibleByBuiltinExtendedGraphemeClusterLiteral: Character, String, StaticString

利用例

実用的な利用例が思いつかなかったので適当な実装

struct MyChar {
    var value: Character
}
extension MyChar: ExpressibleByExtendedGraphemeClusterLiteral {
    init(extendedGraphemeClusterLiteral value: Character) {
        self.value = value
    }
}

let a: MyChar = "a"
let c: MyChar = "🔴"
let d: MyChar = "👨‍👨‍👧‍👧"

ExpressibleByStringLiteral

StringLiteral。複数文字も表現可能だが、文字列展開はまだできない。

"a" // U+0061 ✅
"ab" // U+0061 U+0062 ✅ 二文字以上でもOK
"🔴" // U+1F534 ✅ 
"🖐️" // U+1F590 U+FE0F  ✅
"\(1)" // 🔴 String InterpolationはStringLiteralではない
// multiline
"""
    let a = 10
"""

// raw string
#"\(10)"# // \(10)

// raw string multiline
#"""
    let a = "\(10)"
"""#

なおExpressibleByStringLiteralExpressibleByExtendedGraphemeClusterLiteralの子protocolである。

定義

public protocol ExpressibleByStringLiteral
  : ExpressibleByExtendedGraphemeClusterLiteral {
  
  /// Valid types for `StringLiteralType` are `String` and `StaticString`.
  associatedtype StringLiteralType: _ExpressibleByBuiltinStringLiteral
  
  init(stringLiteral value: StringLiteralType)
}

中間表現は_ExpressibleByBuiltinStringLiteral: StringとStaticString。

利用例

enum JSON {
    case null
    case int(Int)
    case double(Double)
    case bool(Bool)
    case string(String)
}
extension JSON: CustomStringConvertible {
    var description: String {
        switch self {
        ...
        case .string(let string): "\"" + string + "\""
        }
    }
}

extension JSON: ExpressibleByStringLiteral {
    init(stringLiteral value: String) {
        self = .string(value)
    }
}
let json = "hoge"
print(json) // "hoge"

ExpressibleByStringInterpolation

文字列展開を含むStringLiteral。

"a" // U+0061 ✅
"ab" // U+0061 U+0062 ✅
"🔴" // U+1F534 ✅ 
"🖐️" // U+1F590 U+FE0F  ✅
"\(1)" // ✅ String InterpolationもOK

なおExpressibleByStringInterpolationExpressibleByStringLiteralの子protocolである。

定義

中間表現はStringInterpolationProtocol
この中間表現の目的は他のLiteral Protocolとは違う。\(10)のような文字列展開は関数呼び出しに静的変換されており、StringInterpolationProtocolはその呼び出し先関数を定義するレイヤーである。

参考: SE-0228 Fix ExpressibleByStringInterpolation

デフォルトの実装としてDefaultStringInterpolationが用意されている。

public protocol ExpressibleByStringInterpolation
  : ExpressibleByStringLiteral {
  /// The `StringLiteralType` of an interpolation type must match the
  /// `StringLiteralType` of the conforming type.
  associatedtype StringInterpolation: StringInterpolationProtocol
    = DefaultStringInterpolation
    where StringInterpolation.StringLiteralType == StringLiteralType

  init(stringInterpolation: StringInterpolation)
}

public protocol StringInterpolationProtocol {
  associatedtype StringLiteralType: _ExpressibleByBuiltinStringLiteral

  init(literalCapacity: Int, interpolationCount: Int)

  mutating func appendLiteral(_ literal: StringLiteralType)

  // Informal requirement: Any desired appendInterpolation overloads, e.g.:
  // 
  //   mutating func appendInterpolation<T>(_: T)
  //   mutating func appendInterpolation(_: Int, radix: Int)
  //   mutating func appendInterpolation<T: Encodable>(json: T) throws
}

利用例

extension JSON: ExpressibleByStringInterpolation {
    init(stringInterpolation: DefaultStringInterpolation) {
        self = .string(stringInterpolation.description)
    }
}

let json: JSON = "\(1)"
print(json) // "1"

また、DefaultStringInterpolationにextensionでappendInterpolationを追加するか、StringInterpolationProtocol自体を自分で実装することで任意の文字列展開の挙動を実現できます。コレは記事冒頭で言及したSwiftUI.Textにも利用されているテクニックです。

https://qiita.com/ensan_hcl/items/d9b0fdadec8ca77a556c

struct UnicodeScalarLog: ExpressibleByStringInterpolation, CustomStringConvertible {
    var description: String

    init(stringLiteral value: String) {
        description = value
    }

    init(stringInterpolation: StringInterpolation) {
        self.init(stringLiteral: stringInterpolation.storage)
    }
}
extension UnicodeScalarLog {
    struct StringInterpolation: StringInterpolationProtocol {
        var storage: String = ""

        init(literalCapacity: Int, interpolationCount: Int) {
            let capacityPerInterpolation = 2
            let initialCapacity = literalCapacity + interpolationCount * capacityPerInterpolation
            storage.reserveCapacity(initialCapacity)
        }

        mutating func appendLiteral(_ literal: StringLiteralType) {
            storage += literal
        }

        mutating func appendInterpolation(unicodeScalarOf value: String) {
            let unicodeScalars = value.unicodeScalars.map {
                "U+\($0.value)"
            }.joined(separator: " ")
            appendLiteral(unicodeScalars)
        }
    }
}
func printUnicodeScalar(_ log: UnicodeScalarLog) {
    print(log)
}

let hand = "🖐️"
// Default String Interpolation
print("🖐️ is \(hand)") // 🖐️ is 🖐️

// UnicodeScalarLog.StringInterpolation
printUnicodeScalar("🖐️ is \(unicodeScalarOf: hand)") // 🖐️ is U+128400 U+65039

ExpressibleByArrayLiteral

[1, 2, 3]

定義

public protocol ExpressibleByArrayLiteral {
  associatedtype ArrayLiteralElement
  init(arrayLiteral elements: ArrayLiteralElement...)
}

中間表現は固定で中間表現は固定で可変超引数。ArrayLiteralElementは要素を指定するためのassociated type。

利用例

enum JSON {
    case null
    case int(Int)
    case double(Double)
    case bool(Bool)
    case string(String)
    case array([JSON])
}
extension JSON: CustomStringConvertible {
    var description: String {
        switch self {
        ...
        case .array(let array):
            "[" + array.map(\.description).joined(separator: ", ") + "]"
        }
    }
}
extension JSON: ExpressibleByArrayLiteral {
    init(arrayLiteral elements: JSON...) {
        self = .array(elements)
    }
}

let json = [1, "10", nil]
print(json) // [1, "10", null]

ExpressibleByDictionaryLiteral

[1: 10, 2: 20]

定義

public protocol ExpressibleByDictionaryLiteral {
  associatedtype Key
  associatedtype Value
  init(dictionaryLiteral elements: (Key, Value)...)
}

中間表現は固定でタプルの可変超引数。

利用例

enum JSON {
    case null
    case int(Int)
    case double(Double)
    case bool(Bool)
    case string(String)
    case array([JSON])
    case object([String: JSON])
}
extension JSON: CustomStringConvertible {
    var description: String {
        switch self {
        ...
        case .object(let object):
            "{" + object.map { "\"" + $0 + "\": " + $1.description }.joined(separator: ",") + "}"
        }
    }
}
extension JSON: ExpressibleByDictionaryLiteral {
    init(dictionaryLiteral elements: (String, JSON)...) {
        self = .object(Dictionary(uniqueKeysWithValues: elements))
    }
}

let value: JSON = [
    "id": "123456",
    "name": "taro"
]
print(value) // {"id": "123456","name": "taro"}

余談ですが今までの利用例の実装でJSON型が完成しました

フルコード
enum JSON {
    case null
    case int(Int)
    case double(Double)
    case bool(Bool)
    case string(String)
    case array([JSON])
    case object([String: JSON])
}
extension JSON: CustomStringConvertible {
    var description: String {
        switch self {
        case .null: "null"
        case .int(let int): int.description
        case .double(let double): double.description
        case .bool(let bool): bool.description
        case .string(let string): "\"" + string + "\""
        case .array(let array):
            "[" + array.map(\.description).joined(separator: ", ") + "]"
        case .object(let object):
            "{" + object.map { "\"" + $0 + "\": " + $1.description }.joined(separator: ",") + "}"
        }
    }
}
extension JSON: ExpressibleByNilLiteral {
    init(nilLiteral: ()) {
        self = .null
    }
}
extension JSON: ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        self = .int(value)
    }
}
extension JSON: ExpressibleByFloatLiteral {
    init(floatLiteral value: Double) {
        self = .double(value)
    }
}
extension JSON: ExpressibleByBooleanLiteral {
    init(booleanLiteral value: Bool) {
        self = .bool(value)
    }
}
extension JSON: ExpressibleByArrayLiteral {
    init(arrayLiteral elements: JSON...) {
        self = .array(elements)
    }
}
extension JSON: ExpressibleByDictionaryLiteral {
    init(dictionaryLiteral elements: (String, JSON)...) {
        self = .object(Dictionary(uniqueKeysWithValues: elements))
    }
}
extension JSON: ExpressibleByStringLiteral {
    init(stringLiteral value: String) {
        self = .string(value)
    }
}
extension JSON: ExpressibleByStringInterpolation {
    init(stringInterpolation: DefaultStringInterpolation) {
        self = .string(stringInterpolation.description)
    }
}

let value: JSON = [[
    "id": 10,
    "name": "hoge",
    "price": 1000,
    "user": nil
], [
    "id": 11,
    "name": "fuga",
    "price": 1100,
    "user": [
        "id": "123456",
        "name": "taro"
    ]
]]
print(value) // [{"user": null,"id": 10,"name": "hoge","price": 1000}, {"name": "fuga","price": 1100,"id": 11,"user": {"id": "123456","name": "taro"}}]

_ExpressibleByColorLiteral

Playground literalと呼ばれるXcode側にUIで対応のある特殊なliteral。

ソース上でみるとただの関数呼び出しのように見えるが、Xcode上だと特殊なUIで表示される。

#colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)

これ

定義

public protocol _ExpressibleByColorLiteral {
  init(_colorLiteralRed red: Float, green: Float, blue: Float, alpha: Float)
}

利用例

extension SIMD4<Float>: @retroactive _ExpressibleByColorLiteral {
    public init(_colorLiteralRed red: Float, green: Float, blue: Float, alpha: Float) {
        self.init(red, green, blue, alpha)
    }
}

let simd4: SIMD4<Float> = #colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1)
print(simd4) // SIMD4<Float>(0.12156863, 0.011764706, 0.42352942, 1.0)

_ExpressibleByImageLiteral

Playground literalの一つ。

#imageLiteral(resourceName: "hogehoge.png")

定義

public protocol _ExpressibleByImageLiteral {
  init(imageLiteralResourceName path: String)
}

利用例

struct MyImage {
    var path: String
}
extension MyImage: _ExpressibleByImageLiteral {
    init(imageLiteralResourceName path: String) {
        self.path = path
    }
}

let image: MyImage = #imageLiteral(resourceName: "hogehoge.png")
print(image.path)

_ExpressibleByFileReferenceLiteral

Playground literalの一つ。

#fileLiteral(resourceName: "resource.txt")

定義

public protocol _ExpressibleByFileReferenceLiteral {
  init(fileReferenceLiteralResourceName path: String)
}

利用例

struct MyFile {
    var path: String
}
extension MyFile: _ExpressibleByFileReferenceLiteral {
    init(fileReferenceLiteralResourceName path: String) {
        self.path = path
    }
}
let file: MyFile = #fileLiteral(resourceName: "resource.txt")

参考

https://github.com/swiftlang/swift/blob/main/docs/Literals.md

https://qiita.com/freddi_/items/044bdf6defbbe434a8e2

Discussion