[Swift] Opaque Result Typeを使ってジェネリック型の型パラメータを隠蔽しよう!
追記
より良い方法が出来ました。
状況
ライブラリを作ろうとするとしばしば遭遇するのが次のような状況です。
public protocol MyPublicProtocol: Equatable {
init()
var number: Int { get }
}
// 何らかの値
public struct MyValue: MyPublicProtocol {
public init() {}
public let number: Int = 46
}
public struct MyPublicStruct<T: MyPublicProtocol> {
private var value: T
func isEqual(to value: T) -> Bool {
return value == self.value
}
}
MyPublicStruct
は利用者にも使って欲しい構造体です。ですからこれがpublic
であることに問題はありません。
問題はMyValue
の方です。これは本当は利用者が「知らなくていい」もので、使って欲しくないのです。
ところが、これを利用者が使うためには以下のようにして呼び出すことが不可欠です。不本意ながらMyValue
をpublic
にする必要が生じます。
// 別のモジュールで初期化する処理
let instance = MyPublicStruct<MyValue>()
どういう状況?
例えばJapaneseKeyboardInputManager<T: InputStyleData>
が「日本語のキーボード入力を管理する型」だったとし、InputStyleData
に準拠するのは
- かな入力の場合のデータ
KanaInputStyleData
- ローマ字入力の場合のデータ
RomanInputStyleData
であるような状況を考えます。KanaInputStyleData
もRomanInputStyleData
も実装の話なので外部には公開したくありません。けれどそれを管理する「日本語のキーボード入力を管理する型」は公開したいのです。
やりたいこと
どうにかしてMyValue
を隠蔽したいですね。隠蔽といえばOpaque Result Typeです。あれを使えばどうにかなりそうです。
一番良いのは次のようなtypealias
が作れることです。しかしこんな構文はありませんし、今のところこんな構文が予定されているという話も聞いたことがありません。
// 利用者にはMyPublicStructForUserがMyPublicStruct<some MyPublicProtocol>に見えて欲しい
typealias MyPublicStructForUser = MyPublicStruct<MyValue> as MyPublicStruct<some MyPublicProtocol>
これができなくても、次のように書ければまだマシです。この方法ならば外部ではMyPublicStructFactory.instance()
を通して初期化することにしておいて、MyValue
は隠蔽することができるからです。
public enum MyPublicStructFactory {
// 'some' types are only implemented for the declared type of properties and subscripts and the return type of functions
static func instance() -> MyPublicStruct<some MyPublicProtocol> {
return MyPublicStruct<MyValue>(value: .init())
}
}
しかしこれも不可能です。エラーメッセージの通り、返値で型パラメータとしてOpaque Result Typeを使うことはまだできないのです。
これはOpaque Result Typeの現在の実装上の制約で、将来的には可能になるかもしれません。上のtypealias
よりは希望が持てそうです。しかしそれまで待たないといけないのでしょうか。
解決
その必要はありません。少しだけ実装の方法を変えれば、これは可能になります。
public protocol MyPublicStructProtocol {
associatedtype T: MyPublicProtocol
func isEqual(to value: T) -> Bool
}
private struct MyPublicStruct<T: MyPublicProtocol>: MyPublicStructProtocol {
private var value: T
func isEqual(to value: T) -> Bool {
return value == self.value
}
}
private struct MyValue: MyPublicProtocol {
let number: Int = 46
}
public enum MyPublicStructFactory {
static func instance() -> some MyPublicStructProtocol {
return MyPublicStruct<MyValue>(value: .init())
}
}
最終的に利用者が手にするのはsome MyPublicStructProtocol
という型です。これでは何もしようがないように見えますが、MyPublicStructProtocol
はpublic
にすべきものを全て約束しているため、この利用には何の支障もありません。また、外部に公開されるのはOpaqueな型にすぎないため、MyPublicStruct
もMyValue
も公開する必要がありませんし、value
についてはそもそもprotocol
の要件に入れていないため、外部からはアクセスできません。
実際に利用者サイドではどのようにこれが動くのでしょうか。一見これが不思議なことになりますが、別に何も困ることはありません。
let instance = MyPublicStructFactory.instance()
let isEqual = instance.isEqual(to: .init())
let isEqualFunction = instance.isEqual
instance
はsome MyPublicStructProtocol
型の値です。isEqual
は問題なく呼べています。
面白いのは、isEqual
を呼ぶには.init()
をドット記法で呼ぶしかないということです。T
は秘匿されているため、利用者はそれがどのような型なのか知ることはできません。それでもMyPublicProtocol
がイニシャライザの存在は約束しているため、ドット記法を通してのみ初期化ができるのです。
isEqualFunction
を試すとその意味がわかります。これは((some MyPublicStructProtocol).T) -> Bool
という型を持つ関数になっています。
以上で確認した通り、無事に型パラメータを隠蔽したジェネリック型を手にすることができました。ただ、当初の目的ではMyPublicStruct
は公開していてよかったのに、この方法の場合はそれすら隠蔽されてしまい、「実装を過剰に隠蔽している」状況になっているのが若干悔しいところです。
もっといい方法があればぜひ教えてください!
Discussion