Opaque Type

YSYD·2021년 3월 17일
0

Study

목록 보기
2/4

Opaque Type이란?

  • opaque를 사용하는 함수나 메소드는 return 값의 type 정보를 숨긴다.
  • 함수의 return type으로 concrete type 을 제공하는 대신 프로토콜 관점에서 describe된다.
  • Type정보를 숨기는 것은 모듈안쪽을 호출하는 코드와 모듈간의 경계를 구분하는 데에 유용하다. return값의 type이 private으로 유지될 수 있기 때문이다.(클린아키텍처가 생각나는 문구다.)
  • 프로토콜 타입을 반환하는 것과 달리 opaque types는 type identity를 유지한다.
  • 컴파일러는 type정보에 접근할수 있고 모듈의 클라이언트는 모르는 형태

Opaque Types으로 해결하고자 했던것은 무엇일까?

ASCII art module을 만든다고 가정해보자.
Opaque Types가 없던시절, 삼각형을 그리는 Triangle struct를 만들어보면 아래와 같다.

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())
// *
// **
// ***

여기서 generics을 이용해서 shape을 뒤집어주는 FlippedShape Struct를 작성하면 아래와 같다.

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())
// ***
// **
// *

이방식에서의 문제 : FilppedShape를 생성하는데 사용되는 generic type을 노출하게된다. (모듈 외부의 클라이언트가 이 타입을 알게된다.)

두개의 삼각형을 위아래로 합치는 JoinedShape Struct를 만들어보면 아래와 같다(위랑 마찬가지로 생성에 필요한 generic type을 노출하게된다.)

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())
// *
// **
// ***
// ***
// **
// *
  • Shape 생성에 대한 정보를 유출하게되면 module의 public interface에 없는 전체 return type을 명시해야한다.
  • JoinedShape 이나 FlippedShape 같은 Wrapper type들은 모듈 사용자에게는 중요하지 않고 보이지도 않아야한다.
  • module의 public interface는 Shape을 joining, flipping 하는 operation들로 구성되어있고 변환된 Shape값을 return해야한다.

Returning an Opaque Type

Generic Type과의 비교

[Generic Type]

  • Opaque Type은 Generic Type의 반대로 생각할 수 있다.
  • Generic Type을 먼저 설명해보면, 호출하는 쪽에서 parameter들의 타입을 지정해 주고 구현부에서 추상적인 타입을 return할 수있다
  • 아래 코드는 caller 에 의해 결정된 타입을 return 하는 function이다.
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
  • max ( : :)를 호출하는 코드에서 x, y의 값을 선택하고 이 값의 type에 따라 T의 concrete type이 결정된다.
  • x, y는 Comparable 프로토콜을 준수하는 모든 type을 사용할 수 있다.
  • max ( : :) 구현부에서는 Comparable type이 공유하는 기능 만 사용할 수 있다.

[Opaque Type]

  • Opaque Type은 반대로 함수 구현부에서 값의 구체적인 타입을 제공해주고, 호출하는 쪽에서는 추상적으로 return type을 결정한다.
  • 아래 코드는 shape의 type을 노출하지않고 사다리꼴을 반환한다.
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 {
//만약 제네리으로 구현했다면 top, middle을 인자로 받아야했을 것이다. 그렇게되면 모듈 사용자는 Triangle, Saqure라는 Type을 (알 필요가없는데도)알아야한다. 
    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() function은 특정 concrete type이 아닌some Shape 타입(Shape protocol을 따르는 타입)을 return하도록 선언했다.
  • 이렇게하면 public interface를 특정 모양의 type이 아닌 Shape type을 return하는 형태로 표현할수 있다.
  • 또한 구현부에서 사다리꼴을 그리기 위해 두개의triangle이랑 square를 사용하고있는데 다른 방법으로 만들더라도 return type을 변경하지 않아도 된다.

opaque return type이랑 generic을 같이 쓸 수도있다.

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())
// *
// **
// ***
// ***
// **
// *
  • generic 함수에서 return type은 function의 제네릭 타입 파라미터를 쓸수있지만 single type으로 한정적이다.
  • 이처럼 한정적이기 때문에 발생하는 예시로 아래 squares인 경우에만 invalid처리를하는 shape-flipping function을 들수있다.
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
}

  • 이 함수에서 shape이 Square인 경우는 그대로 shap을 반환하고 그렇지 않은 경우 FlippedShape을 써서 뒤집어준후 반환한다.
  • 이는 한가지 타입의 값만 return한다는 원칙에 어긋나서 사진처럼 컴파일에러가난다.
  • 해결할 방법은 Square의 예외케이스를 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")
    }
}

opaque return type에 제네릭을 사용해서 하나의 type을 리턴할수도 있다.
아래는 return타입에 제네릭 타입을 포함하는 function이다.

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

이 경우 리턴값의 underlying type은 T에 의존한다
어떤 shape 이 전달되던 repeat(shape:count:) 은 shape의 array를 생성해서 리턴한다.
항상 같은 [T] 라는 타입을 리턴하기때문에 opaque return type을 쓰면서도 하나의 타입을 리턴하고있다.

Opaque Types 과 Protocol Types 비교

  • opqque Type을 리턴하는건 protocol type 을 사용하는거랑 비슷하지만 type identity를 유지한다는 데서 차이가있다.
  • opaque type은 하나의 특정 타입을 refer 한다(비록 caller의 함수가 타입을 보지 못하지만)
  • 프로토콜 타입은 그 프로토콜을 따르는 어떤 type도 refer할수있다. - 일반적으로 protocol type이 저장하려는 값의 underlying type에 대해 더 유연함을 제공한다.
  • 아래는 protocol type을 써서 flip(_:) 을 자성 한 예이다.
func protoFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}
  • protoFlip(:) 의 body는 filp(:)과 같은 body를 가지고 있지만 항상 같은 타입을 return 한다.
  • flip(:) 과 달리 protoFlip(:)의 값은 항상 같은 타입을 return하도록 요구하지 않으며 그저 Sape protocol을 따르기만 하면 된다.
  • caller가 flip(:)을 만들때 protoFlip(:) 가 좀더 느슨한 API재약을 가지고있다.
  • 여러 타입의 값을 리턴할수 있는 유연함을 갖게된다.
func protoFlip<T: Shape>(_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }

    return FlippedShape(shape: shape)
}
  • 위 코드는 어떤 shape이 전달되었느지에 따라 FlippedShape의 인스턴스나 Square 인스턴스를 리턴한다.
  • 완전 다른 두가지 타입의 flipped shape이 리턴된다.
  • 같은 shape의 여러 인스턴스를 flipping할 때 다른 타입의 값을 리턴할수 있다.
  • protoFlip(_:)의 return타입 정보는 type정보에 의존하는 많은 operation들을 사용할 수 없다.
  • 예를들어 == operation은 사용불가능하다
let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing // Error
  • Shape은 프로토콜 reqirement로 == operator를 포함하지 않기 때문에 마지막 라인에서 에러가나다.
  • 추가하려면 == operator가 양쪽의의 타입정보를 알아야하기 때문에 문제가 생긴다.
  • 프로토콜을 따르는 concrete type에 맞는 Self을 사용할수 있긴하지만 Self requirement를 프로토콜에 추가하는 것은 실제 사용될 때진짜 그 타입일지 보장하지 못한다.
  • 리턴타입으로 protocol type을 사용하면 protocol을 따르는 어떤 타입이건 리턴할 수있다는 유연함이 생긴다.
  • 그렇지만 몇가지 operation을 리턴된 값에 사용하지 못한다는 단점이 있다.
  • opaque type은 underlying type의 identity를 알 수 있다.
  • Swift는 opaque return value를 추론가능하다. (protocol type은 불가능)
  • 아래는 Container protocol의 예이다.
protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container { }
  • 아래는 Container protocol type을 리턴하게 한 예제이다.
  • 타입을 추론할 수 업기 때문에 에러가난다.
// 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]
}
  • 아래의 예제에서 twelve type은 Int로 추론되어진다.
  • 이 경우 type T는 Int가 되어 Int형의 배열리 return 된다.
func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Prints "Int"

https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html

profile
YSYD

0개의 댓글