Never

“Never”는 과거 또는 미래에 일어나지 않을 이벤트에 대한 일을 다룹니다. 이것은 논리적으로 불가능해서 존재하지 않는 일이 사방으로 영원히 뻗어나가고 있는 것과 같습니다.

이런 주석을 마주친다면 특히 걱정이 됩니다.

// this will never happen

모든 컴파일러 교과서들은 이런 주석이 아무 영향을 끼치지 않을 거라고 하지만 머피의 법칙은 다르게 말할 것입니다.

스위프트는 이러한 혼돈 속에서 우리르 지키기 위해 아무것도 없는 상태크래쉬 를 사용합니다.


NeverJoe Groff가 작성한 SE-0102: “@noreturn 속성을 삭제하고 빈 Never 타입을 소개합니다”의 내용처럼 @noreturn 속성을 대체하기 위해 제안되었습니다.

스위프트 3 이전에는 fatalError(_:file:line:), abort() 그리고 exit(_:)와 같은 실행을 중단하는 함수들은 @noreturn 속성과 함께 적히고 컴파일러에게 반환 값이 없다는 사실을 알려주었습니다.

// Swift < 3.0
@noreturn func fatalError(_ message: () -> String = String(),
                               file: StaticString = #file,
                               line: UInt = #line)

변경된 이후에는 fatalErrorNever 타입을 반환하도록 정의되었습니다.

// Swift >= 3.0
func fatalError(_ message: @autoclosure () -> String = String(),
                     file: StaticString = #file,
                     line: UInt = #line) -> Never

주석의 기능을 대체할 타입이어야 하니까 상당히 복잡해야할까요? 아닙니다! 실제론 반대입니다. Never는 전체 스위프트 표준 라이브러리 중에서 가장 간단한 종류가 틀림없습니다.

enum Never {}

아무것도 없는 타입 (Uninhabited Types)

Never아무것도 없는(uninhabited) 타입이고 실제로도 아무것도 없습니다. 또 다른 방식으로 말씀드리면 아무것도 없는 타입은 구성될 수 없습니다.

아무것도 없는 타입을 스위프트에서 예를 든다면 아무 케이스도 존재하지 않는 이너머레이션이 가장 보통일 것입니다. 구조체나 클래스와 다르게 이너머레이션은 이니셜라이져를 받지 않습니다. 프로토콜과는 다르게 이너머레이션은 속성, 메소드, 제네릭 제약 그리고 중첩 타입을 가질 수 있습니다. 이러한 이유로, 아무것도 없는 이너머레이션 타입은 스위프트에서 네임스페이스 기능이나 타입에 대한 이유를 설명할 때 사용합니다.

하지만 Never는 조금 다릅니다. 멋진 종이나 호루라기를 가지고 있지 않지만 그 존재 자체로 특별합니다. (또는 존재하지 않아서 특별합니다)

아무것도 없는 타입을 반환하도록 선언된 함수를 생각해봅시다. 아무것도 없는 타입은 아무 값도 가지고 있지 않기 때문에 함수는 정상적으로 반환할 수 없습니다. 대신에 함수는 실행을 중단하거나 무기한으로 실행하는 것 둘 중에 하나를 반드시 하게 됩니다.

제네릭 타입에서 불가능한 상태 제거하기

지금까지 나온 이론적인 측면은 아주 흥미롭습니다. 그런데 Never를 실제로는 어디서 사용할까요?

SE-0215: Equatable과 Hashable애서 Never 사용하기를 받아들이기 전이라면 더욱 더 사용할 일이 없을 것입니다.

위의 제안에서 Matt Diephouse는 이 모호한 타입을 Equatable과 다른 프로토콜에서 사용하게 되는 동기를 다음과 같이 설명합니다.

Never는 일어나지 않을 일을 코드로 표현하는데에 아주 유용합니다. 많은 사람들이 fatalError와 같은 함수의 반환 타입으로는 친밀하겠지만 Never는 또한 제네릭 클래스를 작업할 때도 유용합니다. 예를 들면 Result 타입은 Never를 항상 에러를 나타내는 Value로 사용하거나 절대 에러가 나지 않는 에러로 Never를 사용할 수 있습니다.

스위프트는 표쥰 Result 타입을 가지고 있진 않지만 대부분의 Result 타입이 다음과 같이 생겼습니다.

enum Result<Value, Error: Swift.Error> {
    case success(Value)
    case failure(Error)
}

Result 타입은 비동기로 실행되는 함수에서 생산되는 값과 에러를 캡슐화하는데에 쓰입니다. (동기 함수는 에러 상황에 throws를 통해 소통할 수 있는 것처럼요)

예를 들어 비동기 HTTP 리퀘스트를 만드는 함수는 Result 타입을 리스폰스나 에러를 저장하는데에 사용할 수 있습니다.

func fetch(_ request: Request, completion: (Result<Response, Error>) -> Void) {
    // ...
}

이 함수를 호출할 때 여러분은 .success.failure 를 분리해서 다루기 위해서 result 에 switch문의 사용하게 될 것입니다.

fetch(request) { result in
    switch result {
    case .success(let value):
        print("Success: \(value)")
    case .failure(let error):
        print("Failure: \(error)")
    }
}

이제 컴플리션 핸들러에서 항상 성공적인 결과만 반환하도록 보증된 함수를 생각해보겠습니다.

func alwaysSucceeds(_ completion: (Result<String, Never>) -> Void) {
    completion(.success("yes!"))
}

Never를 결과의 Error 타입으로 지정하면 실패가 선택지에 없다는 것을 의미하게 됩니다. 이게 정말 멋진 이유는 스위프트가 여러분에게 .failure이 필요하지 않다는 것을 알 수 있을 정도로 충분히 똑똑해서 switch 문이 다음과 같이 생략될 수 있기 때문입니다.

alwaysSucceeds { (result) in
    switch result {
    case .success(let string):
        print(string)
    }
}

이를 극한까지 이끌어내면 NeverComparable 로 구현할 수도 있습니다.

extension Never: Comparable {
  public static func < (lhs: Never, rhs: Never) -> Bool {
    switch (lhs, rhs) {}
  }
}

Never 는 아무것도 없는 타입이기 때문에 가능한 값이 존재하지 않습니다. 그래서 lhsrhs 를 비교할 때 스위프트는 빼먹은 케이스가 없다는 것을 이해합니다. 모든 케이스가 Bool을 반환하기 때문에 메소드는 문제 없이 컴파일 됩니다.

깔끔하네요!


Bottom 타입으로서의 Never

당연한 결과로 Never 에 대한 원래의 스위프트 발전 제안은 더 강화된 타입의 이론적 유용성을 암시합니다.

아무것도 없는 타입은 다른 어떤 타입의 서브타입이 될 수 있습니다. 표현식을 계산하는 것이 아무 값도 생산하지 않는다면 그 표현값이 어떤 타입인지는 중요하지 않습니다. 이것이 컴파일러에 의해 지원된다면 잠재적으로 유용한 무언가를 사용할 수도 있게 됩니다.

죽느냐 언래핑(Unwrap)이냐

강제 언래핑 연산자 (!) 는 스위프트의 가장 논란이 많은 부분 중 하나입니다. 좋게 말하자면 필요악이라고 할 수 있습니다. 나쁘게 말하면 질척함을 제안하는 나쁜 코드입니다. 그리고 추가적인 정보 없이는 둘 사이의 차이를 말하기 어렵습니다.

예를 들면 array 가 빈 상태가 아니라는 것을 가정한 코드가 있습니다.

let array: [Int]
let firstIem = array.first!

강제 언래핑을 피하기 위해선 guard 문을 사용해서 조건적인 할당을 할 수 있습니다.

let array: [Int]
guard let firstItem = array.first else {
    fatalError("배열은 빈 상태일 수 없습니다")
}

만약 Never가 bottom 타입으로 구현된다면 미래에는 Nil 병합 연산자 표현식의 오른쪽으로 사용될수도 있을 것입니다.

// 미래의 스위프트...? 🔮
let firstItem = array.first ?? fatalError("array cannot be empty")

이 패턴을 지금 당장 적용하고 싶으시면 수동으로 ?? 연산자를 다음과 같이 덮어씌울 수 있습니다.

func ?? <T>(lhs: T?, rhs: @autoclosure () -> Never) -> T {
    switch lhs {
    case let value?:
        return value
    case nil:
        rhs()
    }
}

SE-0217: !! (죽느냐 언랩핑하느냐) 연산자를 스위프트 표준 라이브러리에 소개합니다에서 Joe Groff는 “[??를 Never]로 덮어씌우는 것이 타입 확인 퍼포먼스에 수용할 수 없는 임팩트를 초래한다는 사실을 알았습니다”라고 합니다. 그러므로 여러분의 코드에 넣지 않는 것을 추천드립니다.

표현적인 Throw

앞에서와 비슷하게 throwNever를 반환하게 바뀐다면 throw??의 오른쪽에 사용할 수 있을 것입니다.

// 미래의 스위프트...? 🔮
let firstItem = array.first ?? throw Error.empty

타입화된 Throws

길을 더 내려가보겠습니다. Looking even further down the road: 함수 정의에서 throws 키워드가 타입 제약에 대한 지원을 추가하면 Never 타입은 함수가 throw 하지 않는 것을 인지하는데에 사용될 수 있습니다. (앞에서 나왔던 Result 예제와 비슷합니다)

// 미래의 스위프트...? 🔮
func neverThrows() throws<Never> {
    // ...
}

neverThrows() // 성공하는 것이 보장돼있기 때문에 `try`가 불필요할 것입니다. (아마도요?)

절대 일어나지 않을 일에 대해 작업하는 것은 그 일이 일어날 것이라고 증명하는 우주에 초대되는 것과 같은 느낌입니다. 모달이나 독단적인 논리는 체면을 세우기 위한 타협(“그때는 맞았기 떄문에 그래서 나느 믿었다!”)을 허락해주는 반면에 일시적인 논리는 더 높은 표준에 대한 명제를 보유하고 있는 것처럼 보입니다.

다행인 것은 스위프트는 에상 밖의 유형인 Never 덕분에 더 높은 표준에 부합합니다.

다음 글

머신 러닝은 애플 플랫폼에서 오랜 시간동안 자연어 처리의 심장이었습니다. 하지만 외부 개발자들이 직접 접근할 수 있게 된 것은 최근의 일입니다.