-
[SE-0532] Optional noncopyable improvements and generalizationsSwift 2026. 7. 18. 21:33
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SE-0532 — Optional noncopyable 개선 및 일반화에 대해 정리해보겠습니다 🙋🏻
Intro
Proposal: SE-0532
Author: Alejandro Alonso
Review Manager: John McCall
Status: Accepted
Implementation: swiftlang/swift#88505
Motivation
Swift 6.0에서 표준 라이브러리 일부가 noncopyable 값을 저장할 수 있도록 일반화된 이후(SE-0437), noncopyable optional을 다루는 일은 꽤 번거로웠습니다.
optional 내부 값을 살짝 들여다보고 싶은 것뿐인데, borrow만 하고 싶을 때도 마땅한 방법이 없었거든요.아래 코드를 볼까요?
if let payload = optional { } foo(optional) // error: use after consume!다행히 noncopyable switch를 쓰면 optional의 내용을 borrow하도록 제어 흐름을 만들 수 있습니다.
switch optional { case .some(let wrapped): // wrapped는 borrow된 상태 default: break } foo(optional) // ok하지만
if let이나guard let에 익숙한 사람이라면 이 switch 문이 그리 직관적이진 않죠 🥲값을 consume하지 않고 mutate만 하고 싶은 경우도 마찬가지예요.
단순한 프로퍼티 변경이나 mutating 메서드 호출 정도는
?.체이닝으로 충분하지만, payload를 조건부로inout함수에 넘기고 싶다면 이렇게 써야 합니다.if someStruct.x != nil { foo(&someStruct.x!) }혹은 switch로 consume한 뒤 다시 초기화하는, 더 장황하고 직관적이지 않은 방식을 써야 하죠.
func foo(_: inout NoncopyableString) {} func bar(_ x: inout NoncopyableString?) { switch consume x { case .some(var string): foo(&string) x = consume string default: x = nil } }여기에 더해
map,flatMap,unsafelyUnwrapped같은OptionalAPI들도 noncopyable 래핑 타입에는 지원되지 않아서, 이런 코드를 매번 switch 문으로 직접 풀어써야 했습니다.Proposed Solution
Optional에 새로운 프로퍼티 두 개,ref와mutableRef를 추가합니다.이 프로퍼티들은 내부 payload가 있으면 그 참조를, 없으면
nil을 돌려줍니다.func bar(_ x: borrowing SomeNoncopyable) { ... } if let payload = optional.ref { bar(payload.value) // 'bar'는 'borrowing Wrapped'를 전달받음 } foo(optional) // okfunc baz(_ x: inout SomeNoncopyable) { ... } if var payload = optional.mutableRef { baz(&payload.value) // 'baz'가 payload를 그 자리에서 mutate 가능! } foo(optional) // ok이 두 프로퍼티 덕분에 optional의 소유권을 가져오지 않고도 내부 값에 조건부로 접근하는 관용적인 방법이 생겼습니다.
더불어 아래 세 메서드도 noncopyable / nonescapable 래핑 타입을 지원하도록 일반화됩니다.
mapflatMapunsafelyUnwrapped
let optAtomicInt: Optional<Atomic> = ... let optInt: Optional = optAtomicInt.map { $0.load(ordering: .relaxed) } let optInlineArray: Optional<[0 of Atomic]> = [] let optInt2: Optional = optInlineArray.flatMap { $0.isEmpty ? nil : $0[0].load(ordering: .relaxed) } let optMutex: Optional<Mutex> = ... let mutex: Mutex = optMutex.unsafelyUnwrapped optMutex?.withLock { ... } // error: use of 'optMutex' after consume마지막으로
Optional.put()이라는 편의 API도 추가돼요!아래처럼
!로 강제 언래핑하던 패턴을 생각해볼게요.struct Cache: ~Copyable { var opt: UniqueArray? } var cache = Cache() var items: UniqueArray = fooBar() cache.opt = items let newItem = await retrieveNewItem() cache.opt!.append(newItem)이미 값이 있다는 걸 알고 있는 상황이라
!가 사실 불필요하고, inline되지 않는 함수를 거치는 경우엔 컴파일러가 최적화해서 없애주지도 못해요.put()은 새 값을 넣어주면서 그 payload에 대한 direct mutable reference를 바로 돌려줍니다.struct Cache: ~Copyable { var opt: UniqueArray? } var cache = Cache() let items: UniqueArray = fooBar() var itemsRef = cache.opt.put(items) let newItem = await retrieveNewItem() itemsRef.value.append(newItem)Detailed Design
ref와 mutableRef
extension Optional where Wrapped: ~Copyable & ~Escapable { /// optional 안에 payload가 있으면 그에 대한 borrow된 참조를 반환합니다. public var ref: Ref? { @lifetime(borrow self) get } } extension Optional where Wrapped: ~Copyable { /// optional 안에 payload가 있으면 그에 대한 mutable 참조를 반환합니다. public var mutableRef: MutableRef? { @lifetime(&self) mutating get } }개념적으로 이 프로퍼티들은 optional과 그 payload의 소유권을 옮기는 역할을 해요.
ref를 호출하려면 최소한borrowing Optional<T>, 즉Ref<Optional<T>>가 있어야 하는데, 이ref는 그Ref<Optional<T>>를 받아Optional<Ref<T>>로 만들어줍니다.참조를 optional 내부로 옮겨서, 우리가 mutate하거나 consume하거나 독점적으로 접근할 수 있는 owned 값을 돌려받는 셈이죠.
mutableRef도 마찬가지로 최소inout Optional<T>를 받아 ownedOptional<MutableRef<T>>값으로 바꿔줍니다.put()
extension Optional where Wrapped: ~Copyable { /// optional의 값을 새 값으로 설정하면서, optional 안에 새로 들어간 /// payload에 대한 mutable 참조를 반환합니다. /// /// 이미 값이 있었다면 그 값은 destroy됩니다. @lifetime(&self) public mutating func put(_ new: consuming Wrapped) -> MutableRef }unsafelyUnwrapped 일반화
extension Optional where Wrapped: ~Copyable & ~Escapable { public var unsafelyUnwrapped: Wrapped { consuming get } }map(), flatMap() 일반화
extension Optional where Wrapped: ~Copyable & ~Escapable { @lifetime(copy self) public consuming func map( _ transform: (consuming Wrapped) throws(E) -> Result ) throws(E) -> Result? @lifetime(copy self) public consuming func flatMap( _ transform: (consuming Wrapped) throws(E) -> Result? ) throws(E) -> Result? }사실 SE-0437 당시에는 이 일반화를 급하게 적용 하고 싶지 않았다고해요.
noncopyable 값을 다루는
map/flatMap에는 사실 세 가지 형태가 있을 수 있거든요.optional 전체를 consume해서 owned 값을 클로저에 넘기거나, borrow해서 참조를 넘기거나, mutate해서
inout참조를 넘기거나. 셋 다 유용하지만, 전부map이라는 이름으로 오버로드하기엔 규칙상 구분할 방법이 없었죠.그래서 이번 제안은 이 일반화를 기본적으로 항상 consuming으로 동작하게 만들고, 대신
ref()/mutableRef()로 owned optional 값을 먼저 만들어서 나머지 두 형태를 구현할 수 있게 했습니다.func foo(x: consuming Optional<UniqueArray>) -> Optional { x.map { $0[0] } } func bar(x: borrowing Optional<Atomic>) -> Optional { x.ref.map { // ok! $0.value.load(ordering: .relaxed) &+ 1 } } func baz(x: inout Optional<UniqueArray>) -> Optional { x.mutableRef.map { // map 하면서 array를 그 자리에서 업데이트 $0.value.append(123) return $0.value.count } }Source Compatibility
Optional.ref,Optional.mutableRef,Optional.put()은 새로 추가되는 메서드라 소스 호환성 문제가 없습니다.map/flatMap의 새 오버로드는 기존 함수보다 덜 특수화된 오버로드예요.기존에
Optional.map을 쓰던 코드는 모두Copyablewrapped 값을 다루고 있었을 테니, 더 특수화된 기존Wrapped: Copyable오버로드로 그대로 resolve됩니다.flatMap도 마찬가지고요.그래서 이 일반화된 함수들로 인한 소스 호환성 문제는 없을 것으로 예상합니다.
unsafelyUnwrapped는 순수한 일반화라 소스 호환성 문제를 일으키지 않습니다.ABI Compatibility
Optional에 추가되는 새 메서드들은 표준 라이브러리의 새 API지만 별도 ABI를 필요로 하지 않아요.map,flatMap,unsafelyUnwrapped의 일반화 역시 새로운 ABI를 추가하거나 기존 ABI를 깨뜨리지 않습니다.Implications on Adoption
Optional.ref,Optional.mutableRef,Optional.put()은Ref와MutableRef와 동일한 availability를 가집니다.나머지 일반화 부분들은 항상 사용 가능한 것으로 표시됩니다.
Future Directions
Borrow와 inout 바인딩
앞으로 borrow/inout 바인딩을 지원하기로 한다면, 컴파일러가 optional에 대해
if borrow/if inout패턴을 인식해서 조건부로 scope된 접근을 제공하게 할 수도 있어요.if borrow x = optional { } foo(optional) // ok만약 이 방향이
ref/mutableRef방식보다 낫다고 판단되면,map,flatMap,unsafelyUnwrapped를 일반화하는 방식도 다시 고민해야 해요.더 이상 항상 consuming일 수 없으니까요.
consumingMap,borrowingMap,mutatingMap등을 따로 만드는 방법도 있지만, API가 계속 늘어나는 건 그리 썩 괜찮은 해법은 아니라고 하네요 😅Optional: Equatable / Hashable 일반화
SE-0499가
Equatable,Hashable같은 프로토콜을 noncopyable / nonescapable conformer까지 지원하도록 일반화한 만큼,Optional의 이 conformance들도 suppressed wrapped 타입을 지원하도록 일반화하는 게 당연해 보입니다.다만 특정 conformance가 어느 시점에 일반화됐는지 표현할 방법이 없어서, 오래된 ABI 안정 OS에서 실수로 제네릭
==나hash(into:)를 호출해 payload를 copy해버리는 상황은 막아야 해요.Ref / MutableRef 자동 역참조
제안된 예시들에서
ref/mutableRef로map을 쓸 때.value프로퍼티를 명시적으로 접근해야 했죠.앞으로
Ref와MutableRef에 자동 역참조 기능을 추가해서, 아래처럼 더 편하게 쓸 수 있게 할 수도 있습니다.func bar(x: borrowing Optional<Atomic>) -> Optional { x.ref.map { // ok! // '.value.'가 더 이상 필요 없음 $0.load(ordering: .relaxed) &+ 1 } }Alternatives Considered
Optional 바인딩의 기본 소유권 방식 변경
이 제안의 동기에서는
ref,mutableRef같은 프로퍼티가 borrowing 버전의 제어 흐름을 위해 필요하다고 설명했어요.대신
if let같은 optional 바인딩의 기본 소유권 방식을 borrowing으로 바꾸는 방법도 있었습니다.하지만 이건 optional 바인딩이 consuming이라고 가정하는 기존 코드에 대해 source-breaking 변경이 됩니다.
Optional.borrow()와 Optional.mutate() 메서드
이전 리비전에서는
ref,mutableRef대신borrow(),mutate()라는 메서드 이름을 제안했었어요. 하지만InlineArray나UniqueArray같은 타입의span,mutableSpan프로퍼티와 결을 맞추고 싶어서 최종적으로는 프로퍼티 형태가 더 선호됐습니다.Conclusion
noncopyable optional을 다루는 게 이번 제안으로 한결 편해질 것 같아요 🙌
지금까지는 내부 값을 살짝 들여다보기만 해도 consume 에러를 피하려고 switch 문을 직접 작성해야 했는데,
ref/mutableRef덕분에if let처럼 자연스럽게 접근할 수 있게 됐습니다.put()도 강제 언래핑을 줄여주는 실용적인 추가고요.map,flatMap,unsafelyUnwrapped까지 일반화되면서, noncopyable 타입을 다루는 코드가 Copyable 타입을 다룰 때와 비슷한 느낌으로 작성할 수 있을 것 같습니다 🙌References
swift-evolution/proposals/0532-optional-noncopyable-improvements.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution
github.com
'Swift' 카테고리의 다른 글
[SE-0525] Safe loading API for RawSpan (1) 2026.07.26 What's new in Swift (feat. WWDC 2026) (1) 2026.06.22 [SE-0531] Literal Expressions (1) 2026.06.21 [SE-0530] Async Result Support (0) 2026.06.07 [SE-0528] Continuation — Safe and Performant Async Continuations (0) 2026.05.30