[Swift] 23. Opaque Types

도윤·2021년 10월 12일
0

Swift

목록 보기
21/21

Opaque 리턴 타입의 함수 또는 메서드는 리턴타입에 대한 정보를 숨긴다.
반환 값은 함수의 반환 유형으로 구체적인 유형을 제공하는 대신 지원하는 프로토콜로 설명된다. 반환 값의 기본 유형은 비공개로 유지될 수 있기 때문에 형식 정보를 숨기는 것은 모듈과 모듈을 호출하는 코드 사이의 경계에서 유용하다. 유형이 프로토콜 유형인 값을 반환하는 것과 달리 불투명 유형은 유형 ID를 한다. 컴파일러는 유형 정보에 액세스할 수 있지만 모듈의 클라이언트는 그렇지 않다.


The Problem That Opaque Types solve

예를 들어 ASCII Art Shape를 그리는 모듈을 작성한다고 생각해보자. ASCII Art Shape의 기본 특성는 shape 프로토콜의 요구사항으로 사용할 수 있는 해당 shape의 문자열 표현을 반환하는 draw()함수이다.

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

size를 지정하여 그림을 그려주는 코드이다

아래 코드와 같이 제네릭을 사용하여 도형을 수직으로 뒤집는 등의 연산을 구현할 수 있다. 그러나 이 접근 방식에는 다음과 같은 중요한 제한이 있다. 플립된 결과는 해당 결과를 만드는 데 사용된 정확한 제네릭 타입을 표시한다.

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

위의 코드는 두개의 모양을 만들고 이를 합친것을 반환하는 코드이다.
위의 코드로 새로 생성된 타입은 JoinedShape<T: Shape, U: Shape> 타입이다.

--

Returning an Opaque Type

Opaque 타입은 제네릭 타입의 반대라고 생각할 수 있다. 제네릭 타입을 사용하면 함수를 호출하는 코드에서 해당 함수의 매개변수에 대한 타입을 선택하고 사용한다.

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

위의 함수는 호출자에 따라 반환값이 달라진다. max(: :)를 호출하는 코드는 x,y의 값을 선택하고 이 값에 따라 T라는 타입 매개변수의 구체적인 타입이 결정된다. 이때 T는 Comparable 프로토콜을 준수하는 모든 타입을 사용할 수 있다.

하지만 Opaque타입을 사용하면 달라진다. 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())
// *
// **
// **
// **
//**
//*

위의 코드처럼 함수는 Shape의 타입을 노출하지 않고 사다리꼴을 반환한다. makeTrapezoid() 함수에서 반환 타입을 some 키워드를 붙인 some Shape으로 선언한 것을 볼 수 있다. 즉 함수는 구체적인 타입을 지정하지 않았고 Shape 프로토콜을 준수하는 특정 타입의 값만 반환하면 된다. 이러한 방식으로 정의된 makeTrapezoid()는 반환되는 값의 타입을 정의하지 않고도 값을 반환할 수 있다.

즉 제네릭 타입과 반대되는 것이다. Opaque타입을 사용한 makeTrapezoid()함수는 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())
// *
// **
// ***
// ***
// **
// *

Opaque타입과 제너릭을 결합하여 사용할 수 있다. 위의 코드의 함수는 Shape프로토콜을 준수하는 타입의 값을 반환한다.

위의 코드에서 OpaqueJoinedTriangles의 값은 JoinTriangles와 동일하다. 하지만 joinedTriangles와 달리 opaqueJoinedTriangles에서 사용하는 flip(:),join(:)은 반환하는 타입을 Opaque 타입으로 래핑하여 타입이 표시되지 않도록 한다. 두 함수의 타입 매개변수가 FlippedShape,JoinedShape에 필요한 타입 정보를 전달하기 때문에 두 함수는 모두 제네릭이다.

만약 opaque 반환 타입을 가진 함수가 여러위치에서 반환되는 경우 모든 리턴 타입은 똑같아야 한다.

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이면 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")
    }
}

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

단일 타입을 반환해야 한다고 해서 Opaque 반환 타입에 제네릭을 사용하지 못하는 것은 아니다. 위의 코드는 반환 값이 타입 매개변수에 영향을 받도록 만든 함수이다. 이 경우 반환 값의 타입은 타입 매개 변수인 T에 따라 다르다. 전달되는 모양이 무엇이든 repeat(shape:count:)는 해당 모양의 배열을 만들고 반환한다. 그렇지만 반환 값은 항상 동일한 타입인 [T]를 가지므로 Opaque 타입을 반환하는 함수는 단일 타입을 반환해야 한다는 요구사항을 따르게 된다.

Difference Between Opaque Tpyes and Protocol Types

opaque타입을 반환하는 것은 함수 형태를 반환하는 protocol타입과 비슷해보이지만 이 두 종류의 반환 형태는 어떤 type identity를 유지하고 있는지에 따라 다르다. Opaque형태는 어떤 형태를 참조하고 있지만 함수 호출자는 어떤 타입인지 알 수 없다. 프로토콜 타입은 프로토콜을 따르는 모든 타입을 참조할 수 있다. 프로토콜 타입을 더 많은 융통성을 제공하고 강한 보증을 해준다.

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

protoFilp은 flip과 동일한 코드를 썼고 동일한 형태를 반환하지만 항상 같은 타입을 요구하진 않아서 더 다양한 값을 반환하는 유연함을 제공한다.

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

    return FlippedShape(shape: shape)
}

위는 직전에 쓰인 코드를 변경한 것인데, 전달된 Shape의 형태에 따라 Sqaure을 반환하거나 FlippedShape을 반환한다.

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

위 에러는 Shape 프로토콜은 == 연산자를 포함하지 않는다. 프로토콜을 반환 형태로 하는 것은 많은 유연함을 제공하지만 몇몇 연산자는 가능하지 않는다. 프로토콜에서 따르지 않는 연산자에 대해서는 불가능하다.

다른 문제는 모양 변형이 중첩되지 않는 것이다. flipping의 반환형은 shape이고 protoFilp의 반환형은 Shape 프로토콜을 따르는 어떠한 타입도 가능하다. 하지만 프로토콜 타입의 값은 프로토콜을 따르지 않는다. 즉 protoFilp의 반환형은 Shape 프로토콜을 따르지 않는다. flipped된 Shape은 protoFilp의 argument로 적절하지 않아서 여러번의 변형은 불가능하다.

반대로 opaque타입은 타입의 identity를 유지한다. Swift는 관련 타입을 추론할 수 있어서 protocol 타입을 반환을 할 수 없는 곳에서 opaque타입을 사용할 수 있다.

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

// 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]
}

protocol이 associatedtype을 가지고 있어서 프로토콜을 반환형태로 사용할 수 없다.또한 제네릭 반환 타입의 제약조건으로 사용할 수 없다. 이는 함수 외부에서 제네릭 타입이 필요한 것을 추론하기에 충분한 정보가 없기 때문이다.

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

Opaque 타입인 some Container를 반환 타입으로 사용하면 이를 수행할 수 있다. 함수는 Container를 반환하지만 Container 타입 지정을 거부한다.

실제 사용한 코드를 보면 makeOpaqueContainer(item:)의 구현에서 Opaque Container의 기본 타입은 [T]이다. 위의 코드에서는 T가 Int이므로 반환 값은 Int Array이고 Item은 Int로 추론된다. Container의 서브 스크립트는 Item을 반환하는데 이 때도 Int로 추론되는 것을 볼 수 있다.


이해가 잘 되지 않아서 거의 한달동안 쓰다가 지우다가 몇번을 반복했는데 아직도 이해가 잘 안돼서 나머지 부분을 정리한 후에 복습하여 더 깔끔하게 정리하겠습니다.

0개의 댓글