概要
Swiftのprotocolには型の性質を記述する機能と、存在型として使用する機能があります。その存在型はそれ自身のprotocolに準拠しません。その理由は、スタティックメンバに関して問題が生じるからです。この記事ではそれについて解説します。
解説
protocolの2つの機能
Swiftでprotocolを定義したとき、それには大きく2つの用途があります。1つは、ある型に準拠させたり、ジェネリクスの型パラメータの制約の記述に使用する、型の性質を記述する機能です。この記事では、この機能をprotocolの機能と呼ぶことにします。もう1つは、それを型として使用する機能です。このように型として使用されたときの型のことを、存在型と呼びます。下記に例を示します。
まずプロトコルを定義します。
protocol P {
func m()
}
型に準拠させます。
struct S: P {
func m() { print("S.m") }
}
ジェネリック関数の型パラメータに対する制約として使用します。
func f<X: P>(_ x: X) {}
ジェネリック型の型パラメータに対する制約として使用します。
struct SBox<T: P> {
var value: T
}
以上が、protocolの機能です。
存在型として使用します。
func g(_ x: P) {}
これが、存在型としての機能です。
存在型がそれ自身に準拠しない
先程定義した関数fとgはどちらもSのオブジェクトを渡すことができるので、よく似ています。
let s = S()
f(s)
g(s)
これらfの引数である型<X: P> X
と、gの引数である型P
は、相互に呼び出してみると違いがあります。
下記のように、fからgを呼び出すことができます。
func f<X: P>(_ x: X) {
g(x)
}
逆に、gからfを呼び出すことはできず、コンパイルエラーになります。
func g(_ x: P) {
// Cannot invoke 'f' with an argument list of type '(P)'
f(x)
}
つまり、<X: P> X
はP
として使用できるが、P
は<X: P> X
として使用できないということです。この後者の現象から、存在型PがプロトコルPに準拠していないことがわかります。これを指して、存在型がそのプロトコル自身に準拠しない、と言えます。
以下ではその理由の詳細を説明します。
staticメンバが提供できない
protocolの制約は下記の要素について記述できます。
- メソッド(func), プロパティ(var)
- スタティックメソッド(static func), スタティックプロパティ(static var)
- イニシャライザ(init)
- associated type
存在型がそれ自身に準拠しようとする際に、このうち、スタティックな要素の存在が問題になります。
下記のようにプロトコルがスタティックメソッドを持っていたとします。
protocol P {
func m()
static func sm()
}
<X: P> X
型に対しては、これを呼び出すことが可能です。
func f<X: P>(_ x: X) {
X.sm()
}
しかし、P
型に対してこれを呼び出すことはできません。
func g(_ x: P) {
// Static member 'sm' cannot be used on protocol metatype 'P.Protocol'
P.sm()
}
<X: P> X
の場合は、実行時にはX
にはP
に準拠した何らかの型が与えられているので、それを呼び出すことができます。しかし、P
の場合は、そのような対応がありません。P
型はP
の制約だけから生まれているので、この実装の与えようがないのです。
インスタンスメソッドについては、Pの存在型の値の中には、実際にPに準拠している型の値が入っているので、呼び出せます。スタティックについてはそのような中身の実体がないわけです。
イニシャライザも同様です。イニシャライザは型に対して呼び出す処理である点で、スタティックメソッドと同じことになるからです。
associated typeが決定できない
associated typeも問題になります。そもそも、associated typeがあると、プロトコルを存在型として使用できなくなります。
protocol P {
associatedtype Value
var value: Value { get set }
}
// Protocol 'P' can only be used as a generic constraint because it has Self or associated type requirements
func g(_ x: P) {}
もし仮にこの問題がなく、P
が存在型として使えたとしても、すぐに破綻してうまくいきません。P
プロトコルに準拠するためにはassociated typeのValue
が何かの型として定まっている必要があるからです。それが定まっていなければ、value
プロパティの型も定まらず、プロトコルとして機能しません。
func g(_ x: P) {
// aの型が決まらない
let a = x.value
}
getterだけを見ればAny
で受けとれそうに見えますが、setterがうまくいきません。存在型の中には実際の型の値が入っているので、例えばその型ではValue
がInt
だったりするわけです。Int
を書き込むsetterをAnyで受けるわけにはいきません。
Error型の自己準拠
Swift5からErrorプロトコルはそれ自身に準拠するようになりました。Errorプロトコルは、ここまで指摘したようなスタティックメンバは持っていませんし、associated typeも持っていません。というか、中身が空のマーキングのみを目的としたprotocolです。そのため、この特別待遇が可能になっています。
public protocol Error {}
@objc protocolの自己準拠
@objc protocolは、スタティックメンバを持っていない場合に限り、自分自身に準拠します。
@objc protocol P {
func m()
}
func f<X: P>(_ x: X) {}
func g(_ x: P) {
f(x)
}
スタティックメンバを持っていると、それは失われます。
@objc protocol P {
func m()
static func sm()
}
func f<X: P>(_ x: X) {}
func g(_ x: P) {
// Cannot invoke 'f' with an argument list of type '(P)'
f(x)
}
将来的な展望
ここでは現状を踏まえた将来的な展望を考えてみます。
自己準拠の一般化
Errorプロトコルの特別対応を、一般化するとわかりやすい気がします。つまり、スタティックメンバがなければ、存在型が自己準拠するようにします。この自動的な準拠の仕組み自体は@objcプロトコルではすでに搭載されているので、その点でも一貫性が改善します。
associated typeの指定
将来的にGeneralized existentialが実装されれば、associated typeの型を指定することで存在型を使用できるようになりそうです。
let strings: Collection<Element == String> = ["a", "b"]
これの採用後であれば、associated typeがあることは、もはや存在型の自己準拠を阻害しません。
明示的な存在型構文
公式フォーラムにおけるOpaque Result Typeという言語機能についての議論の中で、any
というキーワードを指定することで存在型を記述するというアイデアが提示されています。
もしこれが採用されると、プロトコルを存在型として使用する場合は下記のように記述することになります。
protocol P {
func m()
}
func g(_ x: any P) {}
上述のGeneralized existentialは下記のようになるでしょう。
let strings: any Collection<Element == String> = ["a", "b"]
私はこのように言語仕様が変更されると今よりもわかりやすくなるのではないかと思います。
この記事の冒頭の説明で、私自身もプロトコルには2つの機能がある、という書き方をしましたが、これは構文の見た目に引っ張られた結果として歪んでしまった捉え方だと思います。<X: P> X
でもP
でも同じようにただP
と書くために、同じ何かに見えてしまっているのです。そうではなく、そもそも全く異なる2つの機能なのです。もし存在型としての用法に関してはany P
と記述するようにした場合、これは「Pの機能」ではなく、「anyという言語機能にPを与えている」と直感的に理解できると思います。