Swift Digest
SE-0315 | Swift Evolution

Type placeholders (formerly, “Placeholder types”)

Proposal
SE-0315
Authors
Frederick Kellison-Linn
Review Manager
Joe Groff
Status
Implemented (Swift 5.6)

01 何が問題だったのか

Swiftの型推論は強力ですが、オーバーロードの解決などで型情報が足りないとコンパイラから明示を求められることがあります。このとき、Swiftには「変数の型注釈」「as による型強制」「ジェネリックパラメータの明示」などいくつかの方法がありますが、いずれも型全体を書かなければなりません。型のうちコンパイラが本当に必要としているのは一部だけでも、残りを含めて丸ごと書くしかないというのが問題でした。

たとえば、Double.init は複数のオーバーロードを持つため、そのまま関数として扱おうとすると型が定まりません。

let losslessStringConverter = Double.init
// error: ambiguous use of 'init'

String を受け取るオーバーロードを選ばせるには、戻り値を含めて型を全部書く必要がありました。

let losslessStringConverter = Double.init as (String) -> Double?

本当に曖昧さを解消するのに必要なのは引数側が String だという情報だけで、戻り値が Double? であることは Double.init の側から決まるはずです。

ジェネリック型でも同じ問題が起きます。

enum Either<Left, Right> {
  case left(Left)
  case right(Right)

  init(left: Left) { self = .left(left) }
  init(right: Right) { self = .right(right) }
}

func makePublisher() -> Some<Complex<Nested<Publisher<Chain<Int>>>>> { ... }

let publisherOrValue = Either(left: makePublisher())
// error: generic parameter 'Right' could not be inferred

LeftmakePublisher() の戻り値から推論できますが、Right は決め手がないので書く必要があります。ところが現状では、

let publisherOrValue = Either<Some<Complex<Nested<Publisher<Chain<Int>>>>>, Int>(left: makePublisher())

のように、推論できるはずの Left の長大な型まで書かされてしまい、読み書きのコストが跳ね上がります。Combine の演算子をつなげた長いチェーンの結果などでは、そもそも正しい型をドキュメントやコンパイラエラーから読み取る必要すら出てきます。

02 どのように解決されるのか

型を書ける位置に、その部分だけ推論に任せたいことを示す type placeholder(アンダースコア _)を書けるようにします。型のうち必要な骨格だけをプログラマが与え、残りは従来どおり型推論に委ねるという書き方が可能になります。

先ほどの Double.init の例は、戻り値を _? にして次のように書けます。

let losslessStringConverter = Double.init as (String) -> _?

ジェネリック型の例も、推論できる側を _ にして書き分けられます。

let publisherOrValue = Either<_, Int>(left: makePublisher())

書ける場所

文法上、type placeholder は型が書ける場所ならどこにでも書けます。

Array<_>       // 要素型が placeholder の配列
[Int: _]       // 値型が placeholder の辞書
(_) -> Int     // 引数型が placeholder の関数型
(_, Double)    // placeholder と Double のタプル
_?             // placeholder をラップするオプショナル

型チェッカは placeholder 以外の部分をこれまでどおり解釈し、placeholder の箇所は「ユーザーが明示した無名の型変数」として扱って、周囲の文脈から解決しようとします。たとえば次のコードでは、

import Combine

func makeValue() -> String { "" }
func makeValue() -> Int { 0 }

let publisher: AnyPublisher<Int, _> =
    Just(makeValue()).setFailureType(to: Error.self).eraseToAnyPublisher()

AnyPublisherFailuresetFailureType(to: Error.self) から自明に Error と決まるので placeholder にしておき、一方で Output == Int という情報を与えることで makeValue() のオーバーロードが Int を返す方に解決されます。

型制約の扱い

placeholder がプロトコルへの適合を要求される文脈にあっても問題ありません。

let dict: [_: String] = [0: "zero", 1: "one", 2: "two"]

ここでキーの型 placeholder には Hashable への適合が要求されますが、型チェッカは placeholder が必要な制約を満たすと仮定したうえで推論を進め、最終的に決まった型について制約を検証します。

ジェネリック引数の省略との関係

もともとSwiftでは、文脈からジェネリック引数を推論できる場合に型名だけを書くことが許されています。

import Combine
let publisher = Just(0) // Just<Int> が推論される

この提案のもとでは、素の型名を書くことは、ジェネリック引数をすべて placeholder で埋めるのと等価に扱われます。上の例は次と同じ意味になります。

let publisher = Just<_>(0)

トップレベルでは使えない

型全体が placeholder だけになる書き方は許されません。

let percent: _ = 100.0
// error: placeholders are not allowed as top-level types

関数シグネチャでは使えない

関数の引数型と戻り値型は、これまでどおり完全に書く必要があります。プロトコル要件やデフォルト引数式から推論できそうに見えても、シグネチャの中に placeholder を書くことはできません。

func doSomething(_ count: _? = 0) { ... } // error

これは、次のようにジェネリック型の名前だけを書くのが認められていないのと同じ扱いです。

func doSomething(_ count: Optional = 0) { ... } // error

関数の本体の中であれば、もちろん placeholder を使えます。たとえば次の Bar の例では、シグネチャに placeholder を含む frobnicate2 / frobnicate4 / frobnicate5 / frobnicate7 / frobnicate8 はエラー、本体の式の中でのみ placeholder を使う frobnicate3 / frobnicate6 はOKです。

struct Bar<T, U>
where T: ExpressibleByIntegerLiteral, U: ExpressibleByIntegerLiteral {
    var t: T
    var u: U
}

extension Bar {
    func frobnicate3() -> Bar {
        return Bar<_, _>(t: 42, u: 42) // OK
    }
    func frobnicate6() -> Bar {
        return Bar<_, U>(t: 42, u: 42) // OK
    }
}

動的キャストでの扱い

is / as? / as! のような動的キャストでは、キャスト元の式とキャスト先の型の間に型推論上の結び付きがありません。文法上 placeholder を禁止はしませんが、推論の手がかりがないため、たとえば 0 as? [_] のような書き方はほとんどの場合型チェックに失敗します。case let y as [_] のような is / as パターンも同様です。

API / ABI への影響

type placeholder はAPIとして外に露出しません。コンパイル済みインターフェースでは、@inlinable な関数本体やデフォルト引数式を除き、placeholder はその場で推論された型に置き換えられます。ライブラリ側で placeholder を使うかどうかは実装の詳細ですが、placeholder を挟むことによって推論結果の型が変わってしまうとAPI/ABIの破壊につながり得るため、導入・除去時にはそこだけ注意が必要です。

Future Directions

将来的な拡張としては、ジェネリックのベース型自体を placeholder にして _<Int, _> のように書く案や、S._ のようにネストされた型名の位置で placeholder を使う案、さらには @convention(c) _ のように属性だけを与えて型は推論させる案、トップレベル placeholder(let x: _ = 0.0)などが議論されています。いずれも今回のスコープからは外され、実使用のフィードバックを踏まえて今後検討される見通しです(現時点で実現が約束されているものではありません)。