🙈

[Swift] 「publicなプロトコルへの準拠」だけを外部に公開する

2021/05/06に公開

以前書いた記事でジェネリック型の型パラメータの隠蔽を行おうとして、結果ジェネリック型ごと隠蔽し、内部実装を過剰に隠蔽してしまいました。
https://zenn.dev/en3_hcl/articles/452655a7b743c0

今回の方法はその改良版です。

ヒントにしたのはSwiftUIです。SwiftUIのViewというのは以下のようなプロトコルです。

protocol View {
    associatedtype Body: View
    var body: Body { get }
}

Bodyを知らなかった人も多いかもしれません。実は以下のようにbodyを宣言するとき、Bodysome Viewとして自動で推論されるのです。

var body: some View {
    /* ... */
}

このため、外部にはMyView.Bodyのような型はsome Viewとして公開されます。

このことを利用すると、型をOpaqueにできます。今回は簡単な例で、以下の状況を考えましょう。

public protocol MyPublicUsable {
    static func test()
}

private struct MyPrivateStruct: MyPublicUsable {
    static func test() {}
}

上と同じ方法を使ってMyPrivateStructMyPublicUsableに準拠したPublicStructとして公開します。

// 型をOpaqueにするためのprotocol(上記で言うViewに対応)
public protocol OpaqueTypeBuilderProtocol {
    associatedtype PublicStruct: MyPublicUsable
    func publicStruct() -> PublicStruct
}

// 型をOpaqueにするための具体的な型(上記で言うMyViewなどに対応)
public enum _OpaqueTypeBuilder: OpaqueTypeBuilderProtocol {
    public func publicStruct() -> some MyPublicUsable {
        return MyPrivateStruct()
    }
}

// associatedtypeを取り出して公開する
public typealias PublicStruct = _OpaqueTypeBuilder.PublicStruct

これで、実際のところはMyPrivateStructである型をPublicStructとして公開することができました。この型がMyPublicUsableであることは分かっているため、MyPublicUsableに準拠した「何らかの型」として扱うことは可能です。しかしMyPublicUsableに関係のないプロパティやメソッドは引き続き公開されずに済みます。

このため、もちろんジェネリック型の型パラメータとしても使えます。

public struct MyWrapper<Value: MyPublicUsable> {}

// 別のモジュールで以下のように使える。
let value: MyWrapper<PublicStruct> = .init()

なお、_OpaqueTypeBuilderpublicStructという関数を作りましたが、この関数を実際に呼ぶことはありません。そのため、パフォーマンス上のデメリットもありません。

複数の型をOpaqueにしたい場合は次のようにOpaqueTypeBuilderProtocolの制約を増やしていくことができます。

public protocol OpaqueTypeBuilderProtocol {
    associatedtype PublicStruct1: MyPublicUsable
    func publicStruct1() -> PublicStruct1
    
    associatedtype PublicStruct2: MyPublicUsable
    func publicStruct2() -> PublicStruct2
    
    associatedtype PublicStruct3: MyPublicUsable
    func publicStruct3() -> PublicStruct3
}

public enum _OpaqueTypeBuilder: OpaqueTypeBuilderProtocol {
    public func publicStruct1() -> some MyPublicUsable {
        return MyPrivateStruct1()
    }
    public func publicStruct2() -> some MyPublicUsable {
        return MyPrivateStruct2()
    }
    public func publicStruct3() -> some MyPublicUsable {
        return MyPrivateStruct3()
    }
}

public typealias PublicStruct1 = _OpaqueTypeBuilder.PublicStruct1
public typealias PublicStruct2 = _OpaqueTypeBuilder.PublicStruct2
public typealias PublicStruct3 = _OpaqueTypeBuilder.PublicStruct3

こうすることで、任意の型の「プロトコルへの準拠」という側面だけを外部に公開することができるようになりました。実装を過剰に公開することも過剰に隠蔽することもないので、なかなか良い方法だと思います。

Discussion