iTranslated by AI
[Swift] Hide Generic Type Parameters Using Opaque Result Types
Update
A better method has been developed.
Situation
When trying to create a library, you often encounter a situation like the following:
public protocol MyPublicProtocol: Equatable {
init()
var number: Int { get }
}
// Some value
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 is a struct that you want users to use. Therefore, there is no problem with it being public.
The problem lies with MyValue. This is actually something that users "don't need to know about," and you don't want them to use it directly.
However, in order for users to use it, it is essential to call it as follows. Unwillingly, it becomes necessary to make MyValue public.
// Process to initialize in another module
let instance = MyPublicStruct<MyValue>()
What kind of situation is this?
For example, consider a situation where JapaneseKeyboardInputManager<T: InputStyleData> is a "type that manages Japanese keyboard input," and the types conforming to InputStyleData are:
- Data for Kana input:
KanaInputStyleData - Data for Romaji input:
RomanInputStyleData
Both KanaInputStyleData and RomanInputStyleData are implementation details, so you don't want to expose them externally. However, you do want to expose the "type that manages Japanese keyboard input" which manages them.
What I want to do
I want to hide MyValue somehow. Speaking of hiding, Opaque Result Types come to mind. It seems like we can manage something using that.
The best scenario would be if we could create a typealias like the one below. However, such syntax does not exist, and I haven't heard of any plans for such syntax yet.
// I want users to see MyPublicStructForUser as MyPublicStruct<some MyPublicProtocol>
typealias MyPublicStructForUser = MyPublicStruct<MyValue> as MyPublicStruct<some MyPublicProtocol>
Even if that's not possible, it would be better if we could write it as follows. With this method, we could decide to initialize through MyPublicStructFactory.instance() externally, allowing MyValue to be hidden.
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())
}
}
However, this is also impossible. As the error message states, it is not yet possible to use Opaque Result Types as type parameters in return values.
This is a constraint in the current implementation of Opaque Result Types, and it might become possible in the future. It seems more promising than the typealias mentioned above. But do we have to wait until then?
Solution
There is no need for that. By changing the implementation method slightly, this becomes possible.
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())
}
}
Ultimately, what the user gets is the type some MyPublicStructProtocol. While it might seem like there is nothing they can do with it, since MyPublicStructProtocol guarantees everything that should be public, there are no obstacles to using it. Also, because only an opaque type is exposed to the outside, there is no need to expose MyPublicStruct or MyValue. Furthermore, since value was never included in the protocol requirements in the first place, it cannot be accessed from the outside.
How does this actually work on the user side? At first glance, this might seem mysterious, but it doesn't cause any problems.
let instance = MyPublicStructFactory.instance()
let isEqual = instance.isEqual(to: .init())
let isEqualFunction = instance.isEqual
instance is a value of type some MyPublicStructProtocol. isEqual can be called without any issues.
The interesting part is that to call isEqual, you have no choice but to call .init() using dot notation. Since T is hidden, the user cannot know what specific type it is. Nevertheless, because MyPublicProtocol guarantees the existence of an initializer, initialization can be performed exclusively through dot notation.
You can see the meaning of this if you try isEqualFunction. This becomes a function with the type ((some MyPublicStructProtocol).T) -> Bool.
As confirmed above, we have successfully obtained a generic type with its type parameters hidden. However, while in the original goal it was fine for MyPublicStruct to be public, in this method even that ends up being hidden. It is slightly regrettable that this results in a situation where the implementation is being "excessively hidden."
If there is a better way, please let me know!
Discussion