追記
Swift Forumにおける議論の結果、この記事とは異なる決定に落ち着きました。詳細は以下をご覧ください。
背景
Swift5.1でOpaque Result Typeという種類の型が追加されました。これは「制約を満たすなんらかの特定の型」を関数が返せるようにする仕組みです。その動作は「リバースジェネリクス」という概念で説明されます。
Swiftのジェネリクスの方向性についての議論がなされたForumのスレッド「ジェネリクスのUIの改善(Improving the UI of generics)」では、以下の構文が提案されています。
// この2つは同じ動作
func concatenate(a: some Collection, b: some Collection) -> some Collection
func concatenate<T: Collection, U: Collection>(a: T, b: U) -> some Collection
また、Opaque Result Typeの導入のプロポーザルであるSE-0244でもこの点が言及されています。
This
some Protocol
sugar can be generalized to generic arguments and structural positions in return types in the future
端的にいうと、ここではsome
というキーワードが次のように働きます。
- 引数の位置ではジェネリックな型
- 戻り値の位置ではOpaque Result Type
この構文は「ジェネリクスの表記の軽量化」という目標を達成します。既存の構文は「威圧感が強い(intimidating)」うえに、慣習的に1文字の名前が利用されるため理解もしづらい傾向にあります。新しい構文は制約を流暢に表現するため威圧感が弱く、無理に名前をつけていないため(匿名型であるため)理解にも支障がありません。
また、some
の導入時点から意図されてきたany
というキーワードとの対応も同時に目指されています。any P
とはプロトコルP
を用いたExistential Type、すなわち現在P
と表記される型の新しい表記で、P
に準拠する任意(any)の型の値を代入できることを示唆する表記です。any P
への移行は正式な提案には至っていませんが、過去複数のプロポーザルで言及されるなど、半ば既定路線となっています。このany P
は引数位置でも用いることが出来るので、some P
も引数位置で用いられると対照的で綺麗です。
この記事では、引数の位置で使われるsome
が本当にジェネリック引数であるのかどうか検討します。
根拠付け
戻り値の位置ではOpaque Result Typeを示すキーワードであるsome
が引数の位置ではジェネリックな型を表すのは何故でしょうか。非常に直感的に言えば、Opaque Result Typeとジェネリック引数の振る舞いがかなり類似しているからです。
let collection: some Collection = "Hello"
print(collection.hasPrefix("H")) // valueの型がStringなのは分からないのでエラー
print(collection.prefix(3)) // Collectionに準拠した型はprefixを実装しているので呼び出せる
func hoge<T: Collection>(_ collection: T) {
print(collection.hasPrefix("H")) // valueの型がStringなのは分からないのでエラー
print(collection.prefix(3)) // Collectionに準拠した型はprefixを実装しているので呼び出せる
}
このため、ジェネリクスを用いたコードをsome
を使って以下のように書くこともできそうです。
func hoge(_ collection: some Collection) {
print(collection.hasPrefix("H"))
print(collection.prefix(3))
}
ここではsome
が引数の位置にあります。この引数の位置に置かれたsome P
を指してOpaque Argument Typeと呼ぶ場合があります。
「Opaque」という言葉は、このような状態を形容しています。つまり、ある型が実際に何なのかが不透明である、ということです。そのように考えると、冒頭で紹介したジェネリクスの役割をも担うsome
をもっと単純に表現することが出来ます。つまり「Opaque」であることを標識するものがsome
だということです1。
Rustとの関連
私はRustがほとんど分からないので誤りを含む可能性があります。
これはRustのimpl
の挙動に倣った提案であると考えられます。
Swiftにおけるプロトコルにあたる機能がRustのトレイトです。しかしSwiftと異なり、トレイトをそのまま型として扱うことはできません。Box<dyn Trait>
として提供されるのがSwiftのProtocol
にあたり、impl Trait
によって提供されるのがSwiftのOpaque Result Typeとほぼ同様の働きを持つ機能です2。つまり、impl Trait
はTrait
を実装した何らかの決まった型を表していて、その実際の型は分からず、コンパイル時には静的に解決されます。
Rustでは引数の位置にimpl Trait
が現れた場合どうなるのでしょうか。ピッチ段階にあったOpaque Result Typeのスレッドでこれについての言及があります。
- We're not even going to mention the use of
opaque
in argument position, because it's a distraction for the purposes of this proposal; see Rust RFC 1951.
ここで言及されているRFC 1951という文章で、ちょうど上記のような内容が主張されています。つまり、引数の位置のimpl Trait
は、ジェネリクスと同様に振る舞う、ということです3。
なお、ちょっとややこしいのですが、Rustではimpl Trait
がExistential Typeと呼ばれ、SwiftではProtocol
がExistential Typeと呼ばれています。この記事で以降Existential Typeという場合はSwiftの意味です。
問題
発想
さて、現在は許されませんが、そのうち次のような表現が可能になるでしょう。型の構成要素として用いられるsome
です。
let someArray: [some Numeric] = [0, 1, 2] // someArray: [Int]
let someClosure: (some Numeric) -> () = { (value: Int) in print(value) } // 実際は (Int) -> ()
どちらも特に不思議なことはありません。someArray
はElement
がOpaque Result Typeであるような配列で、someClosure
はNumeric
に準拠した何らかの型の値を引数に取り、その値をprint
するクロージャです。
脳内でsomeClosure
を実行してみましょう。きっと引数の型がOpaqueなので、こんな感じで動くはずです。
let number: Int = 42
someClosure(number) // 引数の型がIntとはわからないのでエラー
someClosure(.zero) // (some Numeric).zeroは存在するのでエラーにならない
とはいえやはり実際に動作しているところを見たいものです。実例を持ってきましょう。
[some Numeric]
と書けないからといって、そういう型を作ることが出来ないわけではありません。
let value: some Numeric = 42
let someArray = [value] // someArray: [some Numeric]
同じことを(some Numeric) -> ()
でもやってみましょう。Numeric
だと上手くいかないので、今回はBinaryInteger
というプロトコルでやってみます。このプロトコルはisMultiple(of: Self)
というメソッドの実装を要求します。
let value: some BinaryInteger = 42
let someClosure = value.isMultiple // someClosure: (some BinaryInteger) -> Bool
このクロージャは、脳内で実行したのと全く同じように、引数がOpaqueになっているように動きます。
let number: Int = 42
someClosure(number) // 引数の型がIntとはわからないのでエラー
someClosure(.zero) // (some BinaryInteger).zeroは存在するのでエラーにならない
困惑
以上で、some
を引数の位置に含むクロージャが構成できることを確認し、その挙動は「引数の型をOpaqueにする」と形容できることがわかりました。
ところが、関数ではどうでしょうか。最初に確認したとおり、関数ではsome
を引数の位置におくとジェネリックな型を表すのでした。つまり、こういうことが起きます。
// 引数の型がOpaqueなクロージャ
let someClosure: (some Numeric) -> () = { (value: Int) in print(value) }
// 引数の型がジェネリックな関数
func someFunction(_ value: some Numeric) { print(value) }
let number: Int = 42
someClosure(number) // エラー
someFunction(number) // 問題なく動作
関数でもクロージャでも、表記が同一であれば型も同じだというのが自然な推測です。しかしここでは、クロージャであるか、関数であるかによって、引数の型が真逆に変わってしまうのです。
うまく理屈をつければこの動作を正当化することはできるでしょう4。ただ、無理に理屈をつけないと説明できない言語仕様は妥当なのでしょうか。
別案
(some Numeric) -> ()
が表すのは、実はジェネリックなクロージャだったかもしれません。こうすることで関数の表現と一致します。
let someClosure: (some Numeric) -> () = { (value: some Numeric) in print(value) }
someClosure(42 as Int) // 42
someClosure(42.0 as Double) // 42.0
残念ながら、こうしてもやはり不自然な事態が起こります。
以下は全て有効な宣言です。動作に若干の違いはありますが、全て同じように「Opaque Result Type」として動作することが期待されます。
let x: some HogeProtocol = Hoge()
var x: some HogeProtocol { return Hoge() }
func x() -> some HogeProtocol { return Hoge() }
もちろん以下も同様です。
// これはジェネリックなクロージャ
let x: (some Numeric) -> () = /* ... */
// なのでこれも
var x: (some Numeric) -> () { /* ... */ }
// これも、ジェネリックなクロージャを返すべき
func x() -> (some Numeric) -> () { /* ... */ }
しかし、3つ目はよく考えると奇妙です。下のようにsome Numeric
の位置をずらす事を考えると、1つ目はジェネリックな関数、2つ目はジェネリックなクロージャ(Rank2型として)を返す関数、3つ目はOpaque Result Typeを含む関数型になります。
func x(some Numeric) -> () -> () { /* ... */ }
func x() -> (some Numeric) -> () { /* ... */ }
func x() -> () -> (some Numeric) { /* ... */ }
複雑に考えれば、この振る舞いを理解することは出来ます5。ただ、それではそもそもの目標であった「軽量で読みやすい構文」から程遠いものになってしまいます。
まとめ
以上をまとめると、引数の位置のsome
をジェネリック引数と考えた場合、次のような問題が生じることになります。
-
(some P) -> ()
型のクロージャをジェネリックでないクロージャと考えた場合 - 関数宣言と動作が一致せず、非直感的な振る舞いをすることになる
-
(some P) -> ()
型のクロージャをジェネリックなクロージャと考えた場合 - 振る舞いが非常に複雑になり、理解に支障を来す
どちらにしても、some P
に関する直感的な理解を諦め、複雑な理由付けや場合分けを用いて納得しなければなりません。コードを書く際にも読む際にも重くのしかかる負担となり、「ジェネリクスのUIの改善」となるどころか、UIが悪化してしまうことになります。
解決策
リバースジェネリクス
リバースジェネリクスという概念がOpaque Result Typeを理解するために提案されています。Opaque Result Typeはリバースジェネリックな戻り値です。
リバースジェネリクスを「実装者が型を決めるジェネリクス」と表現することがありますが、より簡潔には「外側へのジェネリクス」と表現することが出来ます。通常のジェネリクスが、外部によって決定される型を内部で利用するという意味で「内側へのジェネリクス」であるのに対し、リバースジェネリクスは内部で決定される型を外部で利用するという意味です。こう表現することで「リバース=裏返し」という言葉がよりうまく当てはまります。
こう考えると、以下のようなコードが多少わかりやすくなります。「実装者」と考えると少し混乱しそうです。
// ここから上の行は外側
let value: some Numeric = 42 // この行が内側
// ここから下の行は外側
print(value)
some
の役割
「内側で見るジェネリックな型」と「外側で見るリバースジェネリックな型」は同一物に見えます。どちらも実際の型は抽象化されているからです。別の言い方にすると「内側で見るジェネリックな型と外側で見るリバースジェネリックな型は共にOpaqueだ」と言えます。「Opaque」と「リバースジェネリック」が同じ意味ではないことに注意してください。ジェネリックな引数も内側から見てOpaqueですが、決してリバースジェネリックではありません。
some
をジェネリクスに使おう、という提案は「内側で見るジェネリックな型」と「外側で見るリバースジェネリックな型」が同一物に見えることに注目し、これらをsome
で統一しようとするものです。
ところが既に確認したとおり、このようなsome
の利用には無理があります。UIの改善とは程遠い複雑性を孕み、解釈もかなり難しくなります。そこでsome
をジェネリクスに使うのを諦め、次のようにすることで無矛盾な統一が得られます。Opaqueか否かに関係なく、リバースジェネリクスにあたるものをsome
で表すのです。
ジェネリクス | リバースジェネリクス | |
---|---|---|
内側から見る | Opaque |
some (Visible) |
外側から見る | Visible |
some (Opaque) |
この場合どうなるのでしょうか。some P
を引数に取る関数を考えるとき、この引数はジェネリックではなく、リバースジェネリックな型です。some Numeric
は外部に向かって抽象化された型なので、内部では決まった型として扱えます。この発想を即席の構文で表現すると、下のようになります。
// 引数の型がリバースジェネリックな関数
func someFunction(_ value: Int as some Numeric) {
print(value)
}
そして、この関数の挙動はクロージャと全く同じになるはずです。つまり以下が成り立ちます。非常に直感的ではないでしょうか。
// この2つは同じ動作
let someClosure: (some Numeric) -> () = { (value: Int) in print(value) }
func someFunction(_ value: Int as some Numeric) { print(value) }
このとき、some
の役割はリバースジェネリクスの標識であり、外部への抽象化の宣言です。そして、Opaqueであることの標識ではありません。
ジェネリクスのショートハンド
しかしこれでは冒頭で挙げたsome P
をジェネリクスに導入することのモチベーションであった「ジェネリクスの軽量化」や「any
との対応」といった目標が達成できなくなってしまいます。そこで発想を転換することでこれまで以上に綺麗な対応関係を持った構文体系を得ることが出来ます。ジェネリクスの標識にany
を用いるのです。
ジェネリクス | リバースジェネリクス | |
---|---|---|
内側から見る | any |
some |
外側から見る | any |
some |
つまり、こうです。
// この2つは同じ動作
func concatenate(a: any Collection, b: any Collection) -> some Collection
func concatenate<T: Collection, U: Collection>(a: T, b: U) -> some Collection
この構文の最大の利点は、ジェネリクスとリバースジェネリクスの表記に直接対応していることです。特に、今度は戻り値のジェネリクスにもショートハンドができたことになります。
// ジェネリックな型パラメータは常にanyになる
// リバースジェネリックな型パラメータは常にsomeになる
func x<A: P, B: P, ^C: P = Q , ^D: P = Q>(a: A, c: C) -> (b: B, d: D) { /* ... */ }
func x(a: any P, c: Q as some P) -> (b: any P, d: Q as some P) { /* ... */ }
こうすることで「軽量化」「直感的な構文」「any
との対応」「矛盾の解決」といった私たちの欲しかったものが一挙に手に入ります。
結論
- 引数の位置の
some
はジェネリック引数ではありません。 -
some
はOpaqueではなくリバースジェネリクスの標識に用いた方がいいでしょう。 -
any
はExistential Typeではなくジェネリクスの標識に用いた方がいいでしょう。
some
をOpaqueの標識と考えてジェネリック引数に用いれば、大きな混乱を招くことになるでしょう。そこでsome
をリバースジェネリクスの標識と考えることで理解しづらい挙動を取り除くことが出来ます。さらにany
をジェネリクスの標識として導入することによって綺麗な対応関係、軽量な構文、さらには混乱のない体系という欲しかったものを得られるのです。
補足
これまでの言及
実際のところ、リバースジェネリックな引数という発想は以前にも出てはいます。
例えばChris Lattner氏によるOpaque Type Aliasという提案6ではリバースジェネリックな引数が書ける可能性が提示されていました。
You can take opaque types as arguments as well, because the compiler knows the identity of types on the caller side:
public func extractField(a : OpaqueReturn1) -> Int {
// I'm defined in the same module as OpaqueReturn1, so I know it is a string.
return a.count
}
これに関連して、Joe Groff氏は以下のように述べています。(時系列は上の提案より前です)。
Opaque typealiases are completely independent of "opaque argument types". There is perhaps an alternative factoring of these features, where we have opaque typealiases, and then the "some" sugar is introduced uniformly for arguments (to introduce anonymous generic arguments) and returns (to introduce an anonymous opaque return typealias).
面白いことに、長い議論の中でリバースジェネリックな引数についての言及はこれくらいです。需要のなさを物語っているとも思います。
RFC 1951での言及
記事を書いている最中に気付いて大変驚いたのですが、Opaque Result Typeのピッチやプロポーザルで言及されていたRFC 1951では、私の主張と同様に、some
がリバースジェネリクス、any
がジェネリクスに充てられています。
In any case, one longstanding proposal for
impl Trait
is to split it into two distinct features:some Trait
andany Trait
. Then you'd have:
// These two are equivalent
fn foo(t: T)
fn foo(t: any MyTrait)
// These two are equivalent
fn foo() -> impl Iterator
fn foo() -> some Iterator
// These two are equivalent
fn foo() -> T
fn foo() -> any Default
`impl Trait`を引数位置に用いてジェネリクスとして使おうと主張するRFC 1951の議論では、`some`と`any`が一貫してリバースジェネリクスとジェネリクスの意味で用いられています。Swiftは**ジェネリクスの`any`とリバースジェネリクスの`some`の両方の特徴を持った概念として説明されていた`impl Trait`を`some Protocol`として導入し、それをジェネリック引数として使おうとしています**。なぜ?
ちなみに、その後のセクションでは以下のようにも書かれています。
> it's possible to make sense of `some Trait` and `any Trait` in arbitrary positions in a function signature. But experience with the language strongly suggests that `some Trait` semantics is virtually never wanted in argument position, and `any Trait` semantics is rarely used in return position.
「リバースジェネリックな引数」「ジェネリックな戻り値」のショートハンドはほとんど需要がない、というこの指摘は実際その通りだと思います。だからこそRustは`some`と`any`ではなく`impl`を導入するだけで済ませたのです。しかし`impl Protocol`でも`opaque Protocol`でもなく`some Protocol`を導入したSwiftでは、もはやこの道しかないのではないでしょうか。
> this RFC also proposes to *disallow* use of `impl Trait` within `Fn` trait sugar or higher-ranked bounds, i.e. to disallow examples like the following:
>
```Rust
fn foo(f: impl Fn(impl SomeTrait) -> impl OtherTrait)
fn bar() -> (impl Fn(impl SomeTrait) -> impl OtherTrait)
While we will eventually want to allow such uses, it's likely that we'll want to introduce nested universal quantifications (i.e., higher-ranked bounds) in at least some cases; we don't yet have the ability to do so. We can revisit this question later on, once higher-ranked bounds have gained full expressiveness.
面白いことに、Fn
(クロージャを示すトレイト)内部でのimpl Trait
の利用は禁止され、これを高ランクの型に当てるかもしれないということになっています。Swiftで言えばany ClosureProtocol<.Argument == any P, .Result == Int>
のような書き方ができるという話なので、確かに高ランクの型を意味してもおかしくないかもしれません。
Swiftでもクロージャ内でのsome
の利用を禁止できるかもしれません。そうすることで上で私が問題として挙げた不自然な振る舞いは考えずに済むでしょう。ただ、今度はクロージャ内でのsome
の利用が禁止されていること自体が理解しづらい例外になってしまいます。
Protocol
のためのany
Existential Typeにつけるためのキーワードがなくなってしまうのが気になるかもしれません。これについてはexist
などのキーワードがつけばいいのではないでしょうか7。
不安点として型消去に用いられるAnyHogehoge
やトップ型のAny
が挙げられます。some
との対応を目指すのであれば多少被ってしまったとしてもany
をジェネリクスに使った方が良さそうだと思います。
定数に用いるany
some
が定数の型にも使えることから、any
も定数の型で使えた方が良さそうです。これに当たる概念は既にGenerics Manifestoで言及されています。
Generic constants
let
constants could be allowed to have generic parameters, such that they produce differently-typed values depending on how they are used. For example, this is particularly useful for named literal values, e.g.,
let π<T : ExpressibleByFloatLiteral>: T = 3.141592653589793238462643383279502884197169399
any
はちょうどこの宣言と同じ意味になるはずです。つまり以下のように動くでしょう。
// この2つは同じ動作
let π<T : ExpressibleByFloatLiteral>: T = 3.141592653589793238462643383279502884197169399
let π: any ExpressibleByFloatLiteral> = 3.141592653589793238462643383279502884197169399
これが実現されれば、some
とany
の構文上の対称関係がより明確になるはずです。
-
厳密には「値の主な利用者にとってOpaque」と
some
の役割を説明することができます。引数を主に利用するのは関数の内部、戻り値を主に利用するのは関数の外部だからです。 ↩ -
そもそも歴史的にはRustの
impl Trait
を参考に導入されたのがSwiftのOpaque Result Typeです。 ↩ -
ただし、導入には反発も強かったようです。Add RFC undo-universal-impl-trait. by phaazon · Pull Request #2444 · rust-lang/rfcs ↩
-
例えば「戻り値の位置の
some
はOpaque Result Typeで捉え、純粋に引数の位置にあるsome
のみジェネリクスで捉える」と考えれば辻褄が合います。ただ、かなり複雑です。 ↩ -
例えば「純粋に戻り値の位置の
some
のみOpaque Result Typeで捉え、引数位置にあるsome
はその場でジェネリクスと捉える」と考えれば良さそうです。ただ、高ランク型の実装が必要になりますし、直感的とはとても言えない考え方です。 ↩ -
Opaque Result Typeの議論の中で、Alternativeとして提案されたものです。 ↩
-
具体的なキーワードの提案ではありません。 ↩