Swift Digest
SE-0315 | Swift Evolution

型プレースホルダ(旧称”Placeholder types”)

Type placeholders (formerly, “Placeholder types”)

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

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

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の破壊につながり得るため、導入・除去時にはそこだけ注意が必要です。

03 今後の見通し

type placeholder の応用範囲を広げる方向として、いくつかの拡張が今後の検討対象として挙げられています。いずれも今回のスコープからは外され、将来の構想として残されているもので、実現を約束するものではありません。

ジェネリックのベース型やネストされた型での placeholder

現状の placeholder はジェネリック引数の位置などに書きますが、ジェネリック型のベース部分そのものを placeholder にする書き方が考えられます。たとえば eraseToAnyPublisher() の戻り値からベース型まで決まる文脈なら、次のように書けるかもしれません。

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

同様に、ある型の中にネストされた型を表す位置でも placeholder を使えるようにする案があります。

struct S {
  struct Inner {}

  func overloaded() -> Inner { ... }
  func overloaded() -> Int { ... }
}

func test(val: S) {
  let result: S._ = val.overloaded() // Inner を返す方を呼び出す
}

これらが本当にコードを読みやすくするかは議論の余地があるとされ、ユースケースの蓄積を待って改めて検討するという位置付けです。

属性付きの placeholder

型に属性だけを付けて、残りは推論に任せる書き方も挙がっています。

let x: @convention(c) _ = { 0 }

ただし現在のSwiftでは型属性が型名の構文形と密に結び付いており、typealias を介した同等の書き方すら認められていないなど、設計上の整理が必要です。ユースケースが比較的限られることもあり、今後の検討課題として残されています。

トップレベルでの placeholder

提案の初期案には、placeholder を型全体として使えるようにする方向もありました。

let x: _ = 0.0 // x の型は Double と推論される

型注釈や as キャストで使う場合、これは「この型は推論される」と少しだけ明示的に書くだけにとどまり、利点は限られます。一方で、メタタイプ値を渡す位置で使えると setFailureType(to: _.self) のように軽く書けるなどの用途が考えられます。

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

ただしこれを許すと、KeyedDecodingContainer.decode(_:forKey:) のようにライブラリ側が型情報の明示を期待しているAPIで、利用者が decode(_.self, forKey:) のように型名を省略できてしまうという副作用も生じます。トップレベル placeholder は実利用のフィードバックを踏まえて改めて検討する、という扱いになっています。