Optional Deep Dive
optional은 swift programming에서 가장 자주 사용하는 문법중 하나이다. 하지만, (이전의 나 역시도) optional이 어떤 형태를 갖추었고 어떻게 구현되어있는지 ‘정확히’ 알지 못하여 이번 기회를 통해 알게 된 것들을 기록하고 서술한다.
Optional is Enum
optional은 단지 enum 이다. 이는 구현부를 보게되면 쉽게 확인해 볼 수 있다.
@frozen
public enum MyOptional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
이를 기반으로 이런식으로 enum형태로 optinal을 사용할 수 있다.
func returnSomeOptional(_ num: Int) -> Optional<Int> {
if num % 2 == 0 {
return .some(num)
} else {
return .none
}
}
이걸 Short 버전으로 줄인 버전이 바로 우리가 흔히쓰는 옵셔널 문법이다.
// shorterWay
func returnSomeOptinal(_ num: Int) -> Int? {
if num % 2 == 0 {
return num
} else {
return nil
}
}
Switch Optional
이어 옵셔널이 enum이기 때문에 다음과 같이 switch문으로도 사용 가능하다.
func switchSomeOptional(_ num: Int?) -> String {
switch num {
case 22?: return "hello 22?"
case (1..<100)?: return "1~100"
case let number?: return "number: \(number)"
case nil: return "nil"
}
}
Nil-coalescing operator
옵셔널의 ?? 연산자를 통해 기존보다(연산자를 사용하기 전보다) 더욱 쉽게 옵셔널을 언래핑 할 수 있다. 이어, 실패할 경우 Default Value를 리턴하도록 한다.
var name = BookStore.store?.book?.name ?? "Default Value"
print(name)
구현을 보게 되면,
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T)
rethrows -> T {
switch optional {
case .some(let value):
return value
case .none:
return try defaultValue()
}
}
사실 설정했던 Default Value가 closure라는 사실을 알 수 있다. 최적화 관점에서, 디폴트 값을 실제로 사용하지 않을시, 클로져는 실행되지 않기 때문에 효율성이 증가한다.
표현식으로는 @autoclosure를 사용하여 (클로져임에도 불구하고) 괄호를 생략하여 사용 가능 했던 것 이였다.
map & flatMap
Swift에서는 collection뿐만아니라 optional도 값 변환을 위한 오퍼레이션인 map과 flatMap을 제공한다.
1. Map
let number: Int? = Int("100")
let mappedNumber = number.map { (Int("\($0 + $0)")) }
print(mappedNumber)
// prints: Optional(Optional(200))
2. flatMap
Map과 거의 같지만, flatMap은 말그대로 값을 한번 flat하게 처리한다.
let number: Int? = Int("100")
let mappedNumber = number.flatMap { (Int("\($0 + $0)")) }
print(mappedNumber)
// prints: Optional(200)
좀더 자세한 차이를 살펴보려면 구현부를 찾아보면 된다.
@inlinable
public func map<U>(
_ transform: (Wrapped) throws -> U
) rethrows -> U? {
switch self {
case .some(let y):
return .some(try transform(y)) // some에 담아서 처리
case .none:
return .none
}
}
@inlinable
public func flatMap<U>(
_ transform: (Wrapped) throws -> U?
) rethrows -> U? {
switch self {
case .some(let y):
return try transform(y)
case .none:
return .none
}
}
map의 경우 .some(try transform(y))와 같이 처리하여, 말 그대로 map을 수행하는 self객체(optional)을 감싸서 반환하게 된다.
하지만 flatMap의 경우, 바로 try transform(y)으로 받은 클로져값을 그대로 리턴해준다.
전체 Custom Optional Implementation
//
// MyOptinal.swift
//
// Created by 이상헌 on 2023/10/02.
//
@frozen
public enum MyOptional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
@_transparent
public init(_ some: Wrapped) { self = .some(some) }
@inlinable
public func map<U>(
_ transform: (Wrapped) throws -> U
) rethrows -> U? {
switch self {
case .some(let y):
return .some(try transform(y))
case .none:
return .none
}
}
@inlinable
public func flatMap<U>(
_ transform: (Wrapped) throws -> U?
) rethrows -> U? {
switch self {
case .some(let y):
return try transform(y)
case .none:
return .none
}
}
// var i: Index? = nil
@_transparent
public init(nilLiteral: ()) {
self = .none
}
@inlinable
public var unsafelyUnwrapped: Wrapped {
@inline(__always)
get {
if let x = self {
return x
}
fatalError("unsafelyUnwrapped of nil optional")
}
}
@inlinable
internal var _unsafelyUnwrappedUnchecked: Wrapped {
@inline(__always)
get {
if let x = self {
return x
}
fatalError("_unsafelyUnwrappedUnchecked of nil optional")
}
}
/// if let numberOfShoes = numberOfShoes.take() {
/// print(numberOfShoes)
/// // Prints "34"
/// }
internal mutating func _take() -> Wrapped? {
switch self {
case .some(let wrapped):
self = nil
return wrapped
case .none:
return nil
}
}
}
extension MyOptional: Equatable where Wrapped: Equatable {
public static func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l == r
case (nil, nil):
return true
default:
return false
}
}
}
// MARK: - Hashable
extension MyOptional: Hashable where Wrapped: Hashable {
@inlinable
public func hash(into hasher: inout Hasher) {
switch self {
case .none:
hasher.combine(0 as UInt8)
case .some(let wrapped):
hasher.combine(1 as UInt8)
hasher.combine(wrapped)
}
}
}
// MARK: - ?? operator
extension MyOptional {
@inlinable
static public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T)
rethrows -> T {
switch optional {
case .some(let value):
return value
case .none:
return try defaultValue()
}
}
}
+)Ref:
https://github.com/apple/swift
https://agostini.tech/2017/09/24/understanding-optionals/
https://developer.apple.com/documentation/swift/optional/flatmap(_:)