🔬

Stringリテラル / String Interpolation "\()" 調査隊

2024/04/04に公開

環境

macOS Ventura 13.6.6
Xcode 15.2

Stringリテラル

みんなが息をするように書いているこれ。

let abc = "abc"

これは後ろで

extension String : ExpressibleByStringLiteral {
    //コメント省略
    @inlinable public init(stringLiteral value: String)

が走っているそうである。このイニシャライザのコメントにそう書いてある。

コメント

与えられた文字列でインスタンスを作ります
直接呼んではいけません。Stringリテラルが与えられたらコンパイラが使います。
たとえば
let nextStop = "Clark & Lake"
このように書くと後ろで(このイニシャライザを)コンパイラが使います。

Stringリテラルという言葉が出てきたが、これは "aaa" という表記方式のこと。(またはその表記方法で表しているもの)

関係あるところをもう少し出す。このイニシャライザの源の ExpressibleByStringLiteral プロトコルは次のようになっている。

//抜粋
public protocol ExpressibleByStringLiteral : ExpressibleByExtendedGraphemeClusterLiteral {
    init(stringLiteral value: Self.StringLiteralType)
}

このプロトコルの説明の先頭に

Stringリテラルでイニシャライズ出来る型

と書いてあるので、Stringリテラルを投げてインスタンスを作れることを明示するプロトコルと読める。

以上より、「Stringに ExpressibleByStringLiteral がついているのでStringは String("abc") のように "abc" を投げて初期化できる」と言える。

String Interpolation

みんなが大好きなこれ。

let n = 5
print("\(n)") //出力は5

今度は後ろで

@frozen public struct String {
    //コメント
    @inlinable public init(stringInterpolation: DefaultStringInterpolation)

が走っているそうである。このイニシャライザのコメントにそう書いてある。

コメント

interpolated string literalを使ってインスタンスを作る。
直接呼んではいけません。 あなたがstring interpolationを使ってStringを作る時にコンパイラが使います。
あなたはバックスラッシュと()を使う方式で書いてください。

string interpolationという言葉が出てきたが、これは "aaa\(bbb)" という表記方式のこと。

関係あるところをもう少し出す。このイニシャライザの源の ExpressibleByStringInterpolation プロトコルは次のようになっている。

//抜粋
public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral {
    init(stringInterpolation: Self.StringInterpolation)
}

public protocol StringProtocol : /* 省略 */ 
    ExpressibleByStringInterpolation, /* 省略 */ {
}

extension String : StringProtocol {
}

読み方は
init(stringInterpolation:)ExpressibleByStringInterpolation に書かれている。Stringは (間をひとつ経由して) ExpressibleByStringInterpolation に準拠しているのでStringにも init(stringInterpolation:) が書かれている(上で見た)。

となる。
関連するものをもう少し出すと

public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral {
    associatedtype StringInterpolation : StringInterpolationProtocol
        = DefaultStringInterpolation
        where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType

    init(stringInterpolation: Self.StringInterpolation)
}

public protocol StringProtocol : /* 省略 */ 
    ExpressibleByStringInterpolation, /* 省略 */
    where /* 省略 */ Self.StringInterpolation == DefaultStringInterpolation, /* 省略 */ {
}

extension String : StringProtocol {
    public typealias StringInterpolation = DefaultStringInterpolation
}

となっていて init(stringInterpolation:) に渡す StringInterpolationExpressibleByStringInterpolation の中に書かれている。

ここはあとでやる。

このプロトコルの説明の先頭に

string interpolationを使ってイニシャライズ出来る型

と書いてあるので、string interpolationを投げてインスタンスを作れることを明示するプロトコルと読める。

以上より、「Stringに ExpressibleByStringInterpolation がついているので、Stringは String("\(n)") のように "\(n)" を投げて初期化できる」と言える。

押さえる用語

この先、押さえておきたい用語が3つ。

ExpressibleByStringLiteral
"" 方式でインスタンス作成できるタイプを表す(or に付ける)プロトコル。"こんにちは" を渡して初期化できる。

ExpressibleByStringInterpolation
\() 方式でインスタンス作成できるタイプを表す(or に付ける)プロトコル。"合計は\(sum)円です" を渡して初期化できる。

StringInterpolation
\() 方式の変換機能の型。\() の中身を解釈して展開する機能。
例でいうと、sum が42のときに "\(sum)" を文字4と文字2に展開する機能。
これ自体は associatedtype StringInterpolation となっている。

Interpolationは補間という意味らしいが、この記事に限っては展開という言葉で解釈するのもよいでしょう。Intの42を文字の4と文字の2に展開してますし。

ただ、StringInterpolation と string interpolation が紛らわしい。

  • StringInterpolation は型
  • string interpolation は \() という表記方法

StringInterpolation

さっき後でやると言った StringInterpolation について軽く。
これは string interpolation の各変数(個々の \() や素の文)の展開を定義するものである。
これは ExpressibleByStringInterpolation の中で出てくる。

public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral {
    associatedtype StringInterpolation : StringInterpolationProtocol
        = DefaultStringInterpolation
        where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType
}

登場人物

  • StringInterpolation
  • StringInterpolationProtocol
  • DefaultStringInterpolation

StringInterpolation

(associatedtypeで宣言された)型の名前

StringInterpolationProtocol

変換機能が備え付ける機能を定義したもの

DefaultStringInterpolation

基本的な変換機能を実装したものの型。StringInterpolationに何も指定しなければこれが使われる。

ややこしい

ExpressibleByStringInterpolationStringInterpolation は、ややこしい書かれ方をしているのでシンプルなものから始めてこのややこしいものにたどり着くことをやる。

その1

まずは一番シンプル。

protocol ExpressibleByStringLiteral {
    associatedtype StringLiteralType
}

protocol StringInterpolationProtocol {
    associatedtype StringLiteralType
}

protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
    associatedtype StringInterpolation : StringInterpolationProtocol
}

struct DummyString : ExpressibleByStringInterpolation {
    typealias StringLiteralType = 型A
    
    struct StringInterpolation : StringInterpolationProtocol {
        typealias StringLiteralType = 型B
    }
}

その2

イコールの関係を付ける。

protocol ExpressibleByStringLiteral //上と同じ
protocol StringInterpolationProtocol //上と同じ

protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
    associatedtype StringInterpolation : StringInterpolationProtocol
        where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType
}

struct DummyString : ExpressibleByStringInterpolation {
    typealias StringLiteralType = 型A
    
    struct StringInterpolation : StringInterpolationProtocol {
        typealias StringLiteralType = 型B
    }
}

型A == 型B でないとダメ。
イコールが成り立たなくてコンパイルエラーが出ない組み方はちょっと出来なかった。

その3

DefaultStringInterpolation を付ける。
実装側で StringInterpolation を省くことができる。

protocol ExpressibleByStringLiteral //上と同じ
protocol StringInterpolationProtocol //上と同じ

protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
    associatedtype StringInterpolation : StringInterpolationProtocol
        = DefaultStringInterpolation
        where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType
}

struct DefaultStringInterpolation : StringInterpolationProtocol {
    typealias StringLiteralType = String
}

struct DummyString2 : ExpressibleByStringInterpolation {
    typealias StringLiteralType = 型B
}

DummyString2StringInterpolation の実装を省いている。このとき DefaultStringInterpolation が採用される。
DefaultStringInterpolation では型が String で書かれているので 型B == String でないとダメ。

StringInterpolation とは何をするものか

appendInterpolation を定義して、 \() で渡されたものを処理する。

引数を持ったものも定義できる。
appendInterpolation(foo: x) と定義すると \(foo: x) に対応できる。

Stringリテラル / String Interpolation のまとめ

let s: String = "abc"

let s1 = "abc" // `init(stringLiteral:)` につながる
let s2 = "abc\(s)" // `init(stringInterpolation:)` につながる

コンパイラの仕事

let s: String = "abc"

let s1 = String("abc") // `init(stringLiteral:)`
let s2 = String("abc\(s)") // `init(stringInterpolation:)`
let s3: String = "abc" // `init(stringLiteral:)`
let s4: String = "abc\(s)" // `init(stringInterpolation:)`
let s5 = "abc" // `init(stringLiteral:)`
let s6 = "abc\(s)" // `init(stringInterpolation:)`

s3 s4のようにStringが想定される型ところにStringリテラル/string interpolationが投げられて、想定される型が対応していればそのイニシャライザにつながる。
で、s5やs6はデフォルトの想定がStringになっていてうまくいく。

この
「ある型が想定されるところにStringリテラルや string interpolation が投げられて、その型が対応している場合は対応するイニシャライザにつながる」
という考え方でうまく説明できるところが多くある。

Discussion