Never
“Never”는 과거 또는 미래에 일어나지 않을 이벤트에 대한 일을 다룹니다. 이것은 논리적으로 불가능해서 존재하지 않는 일이 사방으로 영원히 뻗어나가고 있는 것과 같습니다.
이런 주석을 마주친다면 특히 걱정이 됩니다.
// this will never happen
모든 컴파일러 교과서들은 이런 주석이 아무 영향을 끼치지 않을 거라고 하지만 머피의 법칙은 다르게 말할 것입니다.
스위프트는 이러한 혼돈 속에서 우리르 지키기 위해 아무것도 없는 상태 와 크래쉬 를 사용합니다.
Never
은 Joe Groff가 작성한 SE-0102: “@noreturn 속성을 삭제하고 빈 Never 타입을 소개합니다”의 내용처럼 @noreturn
속성을 대체하기 위해 제안되었습니다.
스위프트 3 이전에는 fatal
, abort()
그리고 exit(_:)
와 같은 실행을 중단하는 함수들은 @noreturn
속성과 함께 적히고 컴파일러에게 반환 값이 없다는 사실을 알려주었습니다.
// Swift < 3.0
@noreturn func fatal Error(_ message: () -> String = String(),
file: Static String = #file,
line: UInt = #line)
변경된 이후에는 fatal
가 Never
타입을 반환하도록 정의되었습니다.
// Swift >= 3.0
func fatal Error(_ message: @autoclosure () -> String = String(),
file: Static String = #file,
line: UInt = #line) -> Never
주석의 기능을 대체할 타입이어야 하니까 상당히 복잡해야할까요? 아닙니다! 실제론 반대입니다. Never
는 전체 스위프트 표준 라이브러리 중에서 가장 간단한 종류가 틀림없습니다.
enum Never {}
아무것도 없는 타입 (Uninhabited Types)
Never
은 아무것도 없는(uninhabited) 타입이고 실제로도 아무것도 없습니다. 또 다른 방식으로 말씀드리면 아무것도 없는 타입은 구성될 수 없습니다.
아무것도 없는 타입을 스위프트에서 예를 든다면 아무 케이스도 존재하지 않는 이너머레이션이 가장 보통일 것입니다. 구조체나 클래스와 다르게 이너머레이션은 이니셜라이져를 받지 않습니다. 프로토콜과는 다르게 이너머레이션은 속성, 메소드, 제네릭 제약 그리고 중첩 타입을 가질 수 있습니다. 이러한 이유로, 아무것도 없는 이너머레이션 타입은 스위프트에서 네임스페이스 기능이나 타입에 대한 이유를 설명할 때 사용합니다.
하지만 Never
는 조금 다릅니다. 멋진 종이나 호루라기를 가지고 있지 않지만 그 존재 자체로 특별합니다. (또는 존재하지 않아서 특별합니다)
아무것도 없는 타입을 반환하도록 선언된 함수를 생각해봅시다. 아무것도 없는 타입은 아무 값도 가지고 있지 않기 때문에 함수는 정상적으로 반환할 수 없습니다. 대신에 함수는 실행을 중단하거나 무기한으로 실행하는 것 둘 중에 하나를 반드시 하게 됩니다.
제네릭 타입에서 불가능한 상태 제거하기
지금까지 나온 이론적인 측면은 아주 흥미롭습니다. 그런데 Never
를 실제로는 어디서 사용할까요?
SE-0215: Equatable과 Hashable애서 Never 사용하기를 받아들이기 전이라면 더욱 더 사용할 일이 없을 것입니다.
위의 제안에서 Matt Diephouse는 이 모호한 타입을 Equatable
과 다른 프로토콜에서 사용하게 되는 동기를 다음과 같이 설명합니다.
Never
는 일어나지 않을 일을 코드로 표현하는데에 아주 유용합니다. 많은 사람들이fatal
와 같은 함수의 반환 타입으로는 친밀하겠지만Error 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 always Succeeds(_ completion: (Result<String, Never>) -> Void) {
completion(.success("yes!"))
}
Never
를 결과의 Error
타입으로 지정하면 실패가 선택지에 없다는 것을 의미하게 됩니다. 이게 정말 멋진 이유는 스위프트가 여러분에게 .failure
이 필요하지 않다는 것을 알 수 있을 정도로 충분히 똑똑해서 switch
문이 다음과 같이 생략될 수 있기 때문입니다.
always Succeeds { (result) in
switch result {
case .success(let string):
print(string)
}
}
이를 극한까지 이끌어내면 Never
를 Comparable
로 구현할 수도 있습니다.
extension Never: Comparable {
public static func < (lhs: Never, rhs: Never) -> Bool {
switch (lhs, rhs) {}
}
}
Never
는 아무것도 없는 타입이기 때문에 가능한 값이 존재하지 않습니다. 그래서 lhs
와 rhs
를 비교할 때 스위프트는 빼먹은 케이스가 없다는 것을 이해합니다. 모든 케이스가 Bool
을 반환하기 때문에 메소드는 문제 없이 컴파일 됩니다.
깔끔하네요!
Bottom 타입으로서의 Never
당연한 결과로 Never
에 대한 원래의 스위프트 발전 제안은 더 강화된 타입의 이론적 유용성을 암시합니다.
아무것도 없는 타입은 다른 어떤 타입의 서브타입이 될 수 있습니다. 표현식을 계산하는 것이 아무 값도 생산하지 않는다면 그 표현값이 어떤 타입인지는 중요하지 않습니다. 이것이 컴파일러에 의해 지원된다면 잠재적으로 유용한 무언가를 사용할 수도 있게 됩니다.
죽느냐 언래핑(Unwrap)이냐
강제 언래핑 연산자 (!
) 는 스위프트의 가장 논란이 많은 부분 중 하나입니다. 좋게 말하자면 필요악이라고 할 수 있습니다. 나쁘게 말하면 질척함을 제안하는 나쁜 코드입니다. 그리고 추가적인 정보 없이는 둘 사이의 차이를 말하기 어렵습니다.
예를 들면 array
가 빈 상태가 아니라는 것을 가정한 코드가 있습니다.
let array: [Int]
let first Iem = array.first!
강제 언래핑을 피하기 위해선 guard
문을 사용해서 조건적인 할당을 할 수 있습니다.
let array: [Int]
guard let first Item = array.first else {
fatal Error("배열은 빈 상태일 수 없습니다")
}
만약 Never
가 bottom 타입으로 구현된다면 미래에는 Nil 병합 연산자 표현식의 오른쪽으로 사용될수도 있을 것입니다.
// 미래의 스위프트...? 🔮
let first Item = array.first ?? fatal Error("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
앞에서와 비슷하게 throw
가 Never
를 반환하게 바뀐다면 throw
를 ??
의 오른쪽에 사용할 수 있을 것입니다.
// 미래의 스위프트...? 🔮
let first Item = array.first ?? throw Error.empty
타입화된 Throws
길을 더 내려가보겠습니다.
Looking even further down the road:
함수 정의에서 throws
키워드가 타입 제약에 대한 지원을 추가하면 Never
타입은 함수가 throw 하지 않는 것을 인지하는데에 사용될 수 있습니다. (앞에서 나왔던 Result
예제와 비슷합니다)
// 미래의 스위프트...? 🔮
func never Throws() throws<Never> {
// ...
}
never Throws() // 성공하는 것이 보장돼있기 때문에 `try`가 불필요할 것입니다. (아마도요?)
절대 일어나지 않을 일에 대해 작업하는 것은 그 일이 일어날 것이라고 증명하는 우주에 초대되는 것과 같은 느낌입니다. 모달이나 독단적인 논리는 체면을 세우기 위한 타협(“그때는 맞았기 떄문에 그래서 나느 믿었다!”)을 허락해주는 반면에 일시적인 논리는 더 높은 표준에 대한 명제를 보유하고 있는 것처럼 보입니다.
다행인 것은 스위프트는 에상 밖의 유형인 Never
덕분에 더 높은 표준에 부합합니다.