この記事はこちらの転載です
Opaque Result Typeの解説
omochimetaru
わいわいswiftc #11
5秒なら
これを見て
Proposal SE-0244 Opaque Result Type
これを見て
Swift 5.1 に導入される Opaque Result Type とは何か
解説するなら
Opaque Result Type
パフォーマンスを保ちつつ、
真の型を隠蔽する新機能
動機
真の型を隠蔽
「あるプロトコルを返す」と言いたいことがある
func loadUsers() -> ??? // Sequence of User
具体的な型を返してしまうと将来変更できない
// ver 1.0.0
func loadUsers() -> [User]
// ver 1.1.0
func loadUsers() -> LazySequence<User>
従来の方法
Type ErasureやExistentialで返す
func loadUsers() -> AnySequence<User>
- 型の正しさを損なう
- パフォーマンスを損なう
型の正しさを損なう
let a = AnyCollection<String>(["hello"])
let b = AnyCollection<Character>("hello")
let c = b[a.startIndex]
**実行時(!)**エラー
Collection.Index
に関する型情報が失われている
書き込み
protocol P {}
extension Int : P {}
struct Container {
var value: P {
get { return _value }
set { _value = newValue as! Int }
}
var _value: Int = 1
}
実質的にsuper typeなので、contravarianceポジションで壊れる
パフォーマンスを損なう
ラッパーに包まれてる分遅い
解決方法
Opaque Result Type
some P
「プロトコルPを満たす何らかの型」を表す記法
関数の返り値で使える
extension Int : P {}
func getP() -> some P {
return 3
}
プロパティで使える
var propertyP: some P {
get { ... }
set { ... }
}
associated typeがついてても使える
func numbers() -> some MutableCollection {
return [1, 2, 3]
}
var xs = numbers()
let i0 = xs.startIndex
let i1 = xs.index(after: i0)
xs[i0] = xs[i1]
print(xs) // [2, 2, 3]
ここがミソ
2つのsome Pは違う型
func f1() -> some P
func f2() -> some P
var a = f1()
let b = f2()
a = b // コンパイルエラー
ORTは関数単位のアイデンティティを持つ
下記のような状態を想像すると良い
struct ResultOfF1 : P {}
func f1() -> ResultOfF1
struct ResultOfF2 : P {}
func f2() -> ResultOfF2
var a = f1()
let b = f2()
a = b // コンパイルエラー
ORTの抽象化表現
some P
として意味付けされるため、将来のバージョンで真の型を変更できる
ソース互換性だけではなく、バイナリ互換性があり、再コンパイルせずに変更できる
// ver 1.0.0
func f() -> some P { return 3 }
// ver 1.1.0
func f() -> some P { return "3" }
問題の解決をみていく
- 型情報の保持
- 書き込み
- パフォーマンス
型情報の保持
func numbers() -> some Collection { return [1, 2, 3] }
func message() -> some Collection { return "hello" }
let a = numbers()
let b = message()
let x = a[b.startIndex] // 型エラー
書き込み可能
struct S {
var value: some P {
get { return _value }
set { _value = newValue as! Int }
}
var _value: Int = 3
}
var s = S()
let x = s.value
s.value = x // OK
s.value = Int(1) // Error
s.value = "hello" // Error
パフォーマンス
コンパイラが真の型を知っている時は、最適化により真の型として扱うコードに特殊化されるため、不要なパフォーマンスロスが無い
「最適化できるときは速い」という意味ではExistentialと同じ。何が違うのか?
Existentialはsuper typeであるため、「他の型が入っている可能性がない」証明が必要。
ORTは「知らないある型」なので、その証明は不要。単にその真の型を知れば良いだけ。
結果として同一モジュールなら常に最適化可能。
発展
ORTの導入にあたって議論が拡大していった。
収拾をつけるため新しいマニフェストが作られた。
Improving the UI of generics
https://forums.swift.org/t/improving-the-ui-of-generics/22814
Generic Manifestoに続く指針
ORTはジェネリクスと似ている
// Pである真の型を返す
func makeSomeP() -> some P
// Pである真の型Xを受け取る
func useGeneP<X: P>(_ x: X)
これとは違う
// Pである真の型Xを返す
func makeGeneP<X: P>() -> X
このXは呼び出し側が決める
some Pは呼び出され側が決める
Reverse Generics
呼び出され側が決めるジェネリックパラメータという新概念を考えてみる
func makeRevGeneP() -> <X: P> X
ORTはReverse Genericのsugarとみなせる
// Pである真の型を返す
func makeSomeP() -> some P
// Pである真の型Xを返す
func makeRevGeneP() -> <X: P> X
Reverse Genericsを使うと、表現力が上がる
// 2つのsome Pの関連を書けない
func makePS() -> (some P, some P)
// 2つの「同一の」Pを満たす型
func makePS() -> <X: P> (X, X)
ORT構文の拡張案
func makePS()
-> (some P A, some P B)
where A == B
Opaque Result Type → Reverse Generics
?? → Generics
Opaque Argument Type
もしsomeを引数部分に書けたら、Genericsのsugarとみなせる
// Pである真の型を受け取る
func useSomeP(_ x: some P)
// Pである真の型Xを受け取る
func useGeneP<X: P>(_ x: X)
// Pである真の型を受け取る
func useSomeP(_ x: some P)
// Pである真の型Xを受け取る
func useGeneP<X: P>(_ x: X)
頭の中で一度X
を記憶しないので読みやすい、流行りそう
ORTとExistential
よく似ている
// Pを満たす**とある**型を返す
func makeSomeP() -> some P
// Pを満たす**どんな**型でも代入できる
var b: P = ...
Existentialのany記法
// Pを満たす**とある**型を返す
func makeSomeP() -> some P
// Pを満たす**どんな**型でも代入できる
var b: any P
対称的できれい
Protocol制約とExistentialの類似
初級者殺し
// Pは制約
func useGeneP<X: P>(_ x: X)
// PはExistential
func useAnyP(_ x: P)
違う機能は違う記法のほうがわかりやすい
// Pは制約
func useGeneP<X: P>(_ x: X)
// PはExistential
func useAnyP(_ x: any P)