見出し画像

Swiftでプログラミング- Opaque Types

不透明な戻り型を持つ関数またはメソッドは、その戻り値の型情報を非表示にします。 関数の戻り型として具体的な型を提供する代わりに、戻り値はサポートするプロトコルの内容が記述される場合があります。 戻り値の基になる型はプライベートのままである可能性があるため、型情報を非表示にすることは、モジュールとモジュールを呼び出すコードとの分けることが出来ます。 型がプロトコル型である値を返すのとは異なり、不透明(OPAQUE)型は型IDを保持します。コンパイラは型情報にアクセスできますが、モジュールのクライアントはアクセスできません。

The Problem That Opaque Types Solve

たとえば、ASCIIアートシェイプを描画するモジュールを作成しているとします。 ASCIIアートシェイプの基本的な特徴は、そのシェイプの文字列表現を返すdraw()関数です。これは、Shapeプロトコルの要件として使用できます。

   protocol Shape {
       func draw() -> String
   }
   struct Triangle: Shape {
       var size: Int
       func draw() -> String {
           var result: [String] = []
           for length in 1...size {
               result.append(String(repeating: "*", count: length))
           }
           return result.joined(separator: "\n")
       }
   }
   let smallTriangle = Triangle(size: 3)
   print(smallTriangle.draw())
   // *
   // **
   // ***

以下のコードに示すように、ジェネリックスを使用して、シェイプを垂直方向に反転するなどの操作を実装できます。 ただし、このアプローチには重要な制限があります。反転した結果は、それを作成するために使用された正確なジェネリック型を公開します。

    struct FlippedShape<T: Shape>: Shape {
       var shape: T
       func draw() -> String {
           let lines = shape.draw().split(separator: "\n")
           return lines.reversed().joined(separator: "\n")
       }
   }
   let flippedTriangle = FlippedShape(shape: smallTriangle)
   print(flippedTriangle.draw())
   // ***
   // **
   // *

以下のコードが示すように、2つの形状を垂直に結合するJoinedShape <T:Shape、U:Shape>構造を定義するこのアプローチでは、JoinedShape <FlippedShape <Triangle>、Triangle>のようなタイプが反転した三角形を別の三角形と結合します。 

    struct JoinedShape<T: Shape, U: Shape>: Shape {
       var top: T
       var bottom: U
       func draw() -> String {
           return top.draw() + "\n" + bottom.draw()
       }
   }
   let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
   print(joinedTriangles.draw())
   // *
   // **
   // ***
   // ***
   // **
   // *

シェイプの作成に関する詳細情報を公開すると、完全なリターン型を指定する必要があるため、ASCIIアートモジュールのパブリックインターフェイスの一部ではない型をリークする可能性があります。 モジュール内のコードはさまざまな方法で同じ形状を構築できます。その形状を使用するモジュール外の他のコードは、変換のリストに関する実装の詳細を考慮する必要はありません。 JoinedShapeやFlippedShapeなどのWrapper型は、モジュールのユーザーには関係なく、表示されるべきではありません。 モジュールのパブリックインターフェイスは、シェイプの結合や反転などの操作で構成されており、これらの操作は別のシェイプ値を返します。

Returning an Opaque Type

不透明(OPAQUE)型は、ジェネリック型の逆であると考えることができます。 ジェネリック型を使用すると、関数を呼び出すコードで、その関数のパラメータの型を選択し、関数の実装から抽象化された方法で値を返すことができます。 たとえば、次のコードの関数は、呼び出し元に依存する型を返します。

func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }

max(_:_ :)を呼び出すコードは、xとyの値を選択し、それらの値の型によってTの具体的な型が決まります。呼び出しコードは、Comparableプロトコルに準拠する任意の型を使用できます。 関数内のコードは一般的な方法で記述されているため、呼び出し元が提供するタイプを処理できます。 max(_:_ :)の実装は、すべてのComparable型が共有する機能のみを使用します。

不透明な戻り型を持つ関数では、これらの役割が逆になります。 不透明(OPAQUE)型を使用すると、関数を呼び出すコードから抽象化された方法で、関数の実装が返す値の型を選択できます。 たとえば、次の例の関数は、その形状の基になるタイプを公開せずに台形を返します。

    struct Square: Shape {
       var size: Int
       func draw() -> String {
           let line = String(repeating: "*", count: size)
           let result = Array<String>(repeating: line, count: size)
           return result.joined(separator: "\n")
       }
   }
   func makeTrapezoid() -> some Shape {
       let top = Triangle(size: 2)
       let middle = Square(size: 2)
       let bottom = FlippedShape(shape: top)
       let trapezoid = JoinedShape(
           top: top,
           bottom: JoinedShape(top: middle, bottom: bottom)
       )
       return trapezoid
   }
   let trapezoid = makeTrapezoid()
   print(trapezoid.draw())
   // *
   // **
   // **
   // **
   // **
   // *

この例のmakeTrapezoid()関数は、その戻り型を何らかのShapeとして宣言します。その結果、この関数は、特定の具象型を指定せずに、Shapeプロトコルに準拠する特定の型の値を返します。 makeTrapezoid()をこのように記述すると、パブリックインターフェイスの一部からシェイプが作成される特定の型を作成せずに、パブリックインターフェイスの基本的な側面(返される値はシェイプ)を表現できます。この実装では2つの三角形と1つの正方形を使用しますが、関数を書き直して、戻り型を変更せずに、他のさまざまな方法で台形を描画することができます。

この例は、不透明な戻り型がジェネリック型の逆のようである方法を強調しています。 makeTrapezoid()内のコードは、呼び出し元のコードがジェネリック関数に対して行うように、その型がShapeプロトコルに準拠している限り、必要な任意の型を返すことができます。関数を呼び出すコードは、ジェネリック関数の実装など、一般的な方法で記述して、makeTrapezoid()によって返される任意のShape値で機能するようにする必要があります。

不透明なリターン型をジェネリックと組み合わせることもできます。次のコードの関数は両方とも、Shapeプロトコルに準拠するあるタイプの値を返します。

    func flip<T: Shape>(_ shape: T) -> some Shape {
       return FlippedShape(shape: shape)
   }
   func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
       JoinedShape(top: top, bottom: bottom)
   }
   let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
   print(opaqueJoinedTriangles.draw())
   // *
   // **
   // ***
   // ***
   // **
   // *

この例のopaqueJoinedTrianglesの値は、この章で前述した「不透明(OPAQUE)型が解決する問題」セクションのジェネリックスの例のjoinedTrianglesと同じです。 ただし、その例の値とは異なり、flip(_ :)とjoin(_:_ :)は、汎用シェイプ操作が返す基になる型を不透明な戻り型にラップします。これにより、これらの型が表示されなくなります。 依存する型がジェネリックであり、関数の型パラメーターがFlippedShapeとJoinedShapeに必要な型情報を渡すため、両方の関数はジェネリックです。

不透明な戻り型を持つ関数が複数の場所から戻る場合、可能なすべての戻り値は同じ型である必要があります。 ジェネリック関数の場合、その戻り型は関数のジェネリック型パラメーターを使用できますが、それでも単一の型である必要があります。 たとえば、正方形の特殊なケースを含む、無効なバージョンのシェイプフリッピング関数を次に示します。

    func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
       if shape is Square {
           return shape // Error: return types don't match
       }
       return FlippedShape(shape: shape) // Error: return types don't match
   }

この関数をSquareで呼び出すと、Squareが返されます。 それ以外の場合は、FlippedShapeを返します。 これは、1つのタイプのみの値を返すという要件に違反し、invalidFlip(_ :)を無効なコードにします。 invalidFlip(_ :)を修正する1つの方法は、正方形の特殊なケースをFlippedShapeの実装に移動することです。これにより、この関数は常にFlippedShape値を返すことができます。

    struct FlippedShape<T: Shape>: Shape {
       var shape: T
       func draw() -> String {
           if shape is Square {
               return shape.draw()
           }
           let lines = shape.draw().split(separator: "\n")
           return lines.reversed().joined(separator: "\n")
       }
   }

常に単一の型を返すという要件は、不透明な戻り型でジェネリックを使用することを妨げるものではありません。 次に、型パラメーターを、返す値の基になる型に組み込む関数の例を示します。

    func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
       return Array<T>(repeating: shape, count: count)
   }

この場合、戻り値の基になる型はTによって異なります。渡される形状が何であれ、repeat(shape:count :)はその形状の配列を作成して返します。 それにもかかわらず、戻り値は常に同じ基になる型[T]を持っているため、不透明な戻り型を持つ関数は単一の型のみの値を返さなければならないという要件に従います。

Differences Between Opaque Types and Protocol Types

不透明(OPAQUE)型を返すことは、関数の戻り型としてプロトコル型を使用することと非常に似ていますが、これら2種類の戻り型は、型の同一性を保持するかどうかが異なります。 不透明(OPAQUE)型は、特定の1つの型を指しますが、関数の呼び出し元はどの型を確認できません。 プロトコルタイプは、プロトコルに準拠する任意のタイプを参照できます。 一般的に、プロトコル型は、格納する値の基になる型についてより柔軟性があり、不透明(OPAQUE)型は、それらの基になる型についてより強力な保証を行うことができます。

たとえば、不透明な戻り型の代わりにプロトコル型を戻り型として使用するバージョンのflip(_ :)を次に示します。

    func protoFlip<T: Shape>(_ shape: T) -> Shape {
       return FlippedShape(shape: shape)
   }

このバージョンのprotoFlip(_ :)は、flip(_ :)と同じ本体を持ち、常に同じ型の値を返します。 フリップ(_ :)とは異なり、protoFlip(_ :)が返す値は、常に同じ型である必要はありません。Shapeプロトコルに準拠している必要があります。 言い換えると、protoFlip(_ :)は、flip(_ :)が作成するよりも、呼び出し元とのAPIコントラクトがはるかに緩くなります。 複数のタイプの値を返す柔軟性を持たせます。

    func protoFlip<T: Shape>(_ shape: T) -> Shape {
       if shape is Square {
           return shape
       }
       return FlippedShape(shape: shape)
   }

改訂したコードは、渡される形状に応じて、SquareのインスタンスまたはFlippedShapeのインスタンスを返します。この関数によって返される2つの反転された形状は、完全に異なる型である可能性があります。 この関数の他の有効なバージョンは、同じ形状の複数のインスタンスを反転するときに、異なる型の値を返す可能性があります。 protoFlip(_ :)からのより具体的でない戻り型情報は、型情報に依存する多くの操作が戻り値で利用できないことを意味します。 たとえば、この関数によって返された結果を比較する==演算子を作成することはできません。

   let protoFlippedTriangle = protoFlip(smallTriangle)
   let sameThing = protoFlip(smallTriangle)
   protoFlippedTriangle == sameThing  // Error

例の最後の行のエラーは、いくつかの理由で発生します。差し迫った問題は、Shapeにプロトコル要件の一部として==演算子が含まれていないことです。 1つ追加しようとすると、次に発生する問題は、==演算子がその左側と右側の引数の型を知る必要があるということです。この種の演算子は通常、Self型の引数を取り、プロトコルを採用する具体的な型に一致しますが、プロトコルにSelf要件を追加しても、プロトコルを型として使用するときに発生する型消去は許可されません。

関数の戻り型としてプロトコル型を使用すると、プロトコルに準拠する任意の型を柔軟に返すことができます。ただし、その柔軟性の代償として、戻り値に対して一部の操作を実行できないことがあります。この例は、==演算子が使用できない方法を示しています。これは、プロトコル型を使用しても保持されない特定の型情報に依存します。

このアプローチのもう1つの問題は、形状変換がネストしないことです。三角形を反転した結果はShape型の値であり、protoFlip(_ :)関数は、Shapeプロトコルに準拠するある型の引数を取ります。ただし、プロトコルタイプの値は、そのプロトコルに準拠していません。 protoFlip(_ :)によって返される値がShapeに準拠していません。これは、反転した形状がprotoFlip(_ :)の有効な引数ではないため、複数の変換を適用するprotoFlip(protoFlip(smallTriange))のようなコードが無効であることを意味します。

対照的に、不透明(OPAQUE)型は、基になる型のIDを保持します。 Swiftは関連する型を推測できるため、プロトコル型を戻り値として使用できない場所で不透明な戻り値を使用できます。たとえば、GenericsのContainerプロトコルのバージョンは次のとおりです。

    protocol Container {
       associatedtype Item
       var count: Int { get }
       subscript(i: Int) -> Item { get }
   }
   extension Array: Container { }

そのプロトコルにはassociatedtypeがあるため、関数の戻り型としてコンテナを使用することはできません。 また、ジェネリック型が何である必要があるかを推測するのに十分な情報が関数本体の外部にないため、ジェネリック戻り型の制約として使用することはできません。

    // Error: Protocol with associated types can't be used as a return type.
   func makeProtocolContainer<T>(item: T) -> Container {
       return [item]
   }
   // Error: Not enough information to infer C.
   func makeProtocolContainer<T, C: Container>(item: T) -> C {
       return [item]
   }

不透明(OPAQUE)型を使用して、戻り型として一部のコンテナは目的のAPIコントラクトを表します。関数はコンテナを返しますが、コンテナの型の指定を拒否します。

   func makeOpaqueContainer<T>(item: T) -> some Container {
       return [item]
   }
   let opaqueContainer = makeOpaqueContainer(item: 12)
   let twelve = opaqueContainer[0]
   print(type(of: twelve))
   // Prints "Int"

12の型はIntであると推測されます。これは、型推論が不透明(OPAQUE)型で機能することを示しています。 makeOpaqueContainer(item :)の実装では、不透明なコンテナの基になるタイプは[T]です。 この場合、TはIntであるため、戻り値は整数の配列であり、Itemに関連付けられたタイプはIntであると推測されます。 Containerの添え字はItemを返します。これは、12のタイプもIntであると推測されることを意味します。

参考


この記事が気に入ったらサポートをしてみませんか?