Skip to content

Commit 6a958fd

Browse files
committed
Update the proposal, implementation and tests with formatting, additional details and modifications for being able to push it to a review
1 parent 598ca12 commit 6a958fd

File tree

3 files changed

+159
-208
lines changed

3 files changed

+159
-208
lines changed

Evolution/NNNN-map-error.md

Lines changed: 22 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,49 @@
11
# Map Error
22

33
* Proposal: [SAA-NNNN](NNNN-map-error.md)
4-
* Authors: [Clive Liu](https://github.com/clive819)
4+
* Authors: [Clive Liu](https://github.com/clive819) [Philippe Hausler](https://github.com/phausler)
55
* Review Manager: TBD
66
* Status: **Awaiting review**
77

8-
*During the review process, add the following fields as needed:*
8+
## Introduction
99

10-
* Implementation: [apple/swift-async-algorithms#324](https://github.com/apple/swift-async-algorithms/pull/324)
11-
* Decision Notes:
12-
* Bugs:
10+
Asynchronous sequences often particpate in carefully crafted systems of algorithms spliced together. Some of those algorithms require that both the element type and the failure type are the same as each-other. In order to line those types up for the element we have the map algorithm today, however there are other times at which when using more specific APIs that transforming the Failure type is needed.
1311

14-
## Introduction
12+
## Motivation
1513

1614
The `mapError` function empowers developers to elegantly transform errors within asynchronous sequences, enhancing code readability and maintainability.
1715

18-
```swift
19-
extension AsyncSequence {
16+
Building failure-type-safe versions of zip or other algorithms will need to require that the associated Failure types are the same. Having an effecient and easy to use transformation routine to adjust the failure-types is then key to delivering or interfacing with those failure-type-safe algorithms.
2017

21-
public func mapError<MappedFailure: Error>(_ transform: @Sendable @escaping (Self.Failure) -> MappedFailure) -> some AsyncSequence<Self.Element, MappedFailure> {
22-
AsyncMapErrorSequence(base: self, transform: transform)
23-
}
24-
}
25-
```
18+
## Proposed solution
19+
20+
A new extension and type will be added to transform the failure-types of AsyncSequences.
2621

2722
## Detailed design
2823

29-
The function iterates through the elements of an `AsyncSequence` within a do-catch block. If an error is caught, it calls the `transform` closure to convert the error into a new type and then throws it.
24+
The method will be applied to all AsyncSequences via an extension with the function name of `mapError`. This is spiritually related to the `mapError` method on `Result` and similar in functionality to other frameworks' methods of the similar naming. This will not return an opaque result since the type needs to be refined for `Sendable`; in that the `AsyncMapERrorSequence` is only `Sendable` when the base `AsyncSequence` is `Sendable`.
3025

3126
```swift
32-
struct AsyncMapErrorSequence<Base: AsyncSequence, MappedFailure: Error>: AsyncSequence {
33-
34-
...
35-
36-
func makeAsyncIterator() -> Iterator {
37-
Iterator(
38-
base: base.makeAsyncIterator(),
39-
transform: transform
40-
)
41-
}
27+
extension AsyncSequence {
28+
public func mapError<MappedFailure: Error>(_ transform: @Sendable @escaping (Failure) async -> MappedFailure) -> AsyncMapErrorSequence<Element, MappedFailure>
4229
}
4330

44-
extension AsyncMapErrorSequence {
45-
46-
struct Iterator: AsyncIteratorProtocol {
31+
public struct AsyncMapErrorSequence<Base: AsyncSequence, TransformedFailure: Error>: AsyncSequence { }
4732

48-
typealias Element = Base.Element
33+
extension AsyncMapErrorSequence: Sendable where Base: Sendable { }
4934

50-
private var base: Base.AsyncIterator
51-
52-
private let transform: @Sendable (Failure) -> MappedFailure
35+
@available(*, unavailable)
36+
extension AsyncMapErrorSequence.Iterator: Sendable {}
37+
```
5338

54-
init(
55-
base: Base.AsyncIterator,
56-
transform: @Sendable @escaping (Failure) -> MappedFailure
57-
) {
58-
self.base = base
59-
self.transform = transform
60-
}
39+
## Effect on API resilience
6140

62-
mutating func next() async throws(MappedFailure) -> Element? {
63-
do {
64-
return try await base.next(isolation: nil)
65-
} catch {
66-
throw transform(error)
67-
}
68-
}
41+
This cannot be back-deployed to 1.0 since it has a base requirement for the associated `Failure` and requires typed throws.
6942

70-
mutating func next(isolation actor: isolated (any Actor)?) async throws(MappedFailure) -> Element? {
71-
do {
72-
return try await base.next(isolation: actor)
73-
} catch {
74-
throw transform(error)
75-
}
76-
}
77-
}
78-
}
43+
## Naming
7944

80-
extension AsyncMapErrorSequence: Sendable where Base: Sendable, Base.Element: Sendable {}
81-
```
45+
The naming follows to current method naming of the Combine [mapError](https://developer.apple.com/documentation/combine/publisher/maperror(_:)) method and similarly the name of the method on `Result`
8246

83-
## Naming
47+
## Alternatives considered
8448

85-
The naming follows to current method naming of the Combine [mapError](https://developer.apple.com/documentation/combine/publisher/maperror(_:)) method.
49+
It was initially considered that the return type would be opaque, however the only way to refine that as Sendable would be to have a disfavored overload; this ended up creating more ambiguity than it seemed worth.

Sources/AsyncAlgorithms/AsyncMapErrorSequence.swift

Lines changed: 59 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,95 +11,82 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
#if compiler(>=6.0)
14+
@available(AsyncAlgorithms 1.1, *)
1415
extension AsyncSequence {
1516

16-
/// Converts any failure into a new error.
17-
///
18-
/// - Parameter transform: A closure that takes the failure as a parameter and returns a new error.
19-
/// - Returns: An asynchronous sequence that maps the error thrown into the one produced by the transform closure.
20-
///
21-
/// Use the ``mapError(_:)`` operator when you need to replace one error type with another.
22-
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
23-
public func mapError<MappedError: Error>(_ transform: @Sendable @escaping (Self.Failure) -> MappedError) -> some AsyncSequence<Self.Element, MappedError> {
24-
AsyncMapErrorSequence(base: self, transform: transform)
25-
}
26-
27-
/// Converts any failure into a new error.
28-
///
29-
/// - Parameter transform: A closure that takes the failure as a parameter and returns a new error.
30-
/// - Returns: An asynchronous sequence that maps the error thrown into the one produced by the transform closure.
31-
///
32-
/// Use the ``mapError(_:)`` operator when you need to replace one error type with another.
33-
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
34-
public func mapError<MappedError: Error>(_ transform: @Sendable @escaping (Self.Failure) -> MappedError) -> (some AsyncSequence<Self.Element, MappedError> & Sendable) where Self: Sendable, Self.Element: Sendable {
35-
AsyncMapErrorSequence(base: self, transform: transform)
36-
}
17+
/// Converts any failure into a new error.
18+
///
19+
/// - Parameter transform: A closure that takes the failure as a parameter and returns a new error.
20+
/// - Returns: An asynchronous sequence that maps the error thrown into the one produced by the transform closure.
21+
///
22+
/// Use the ``mapError(_:)`` operator when you need to replace one error type with another.
23+
@available(AsyncAlgorithms 1.1, *)
24+
public func mapError<MappedError: Error>(_ transform: @Sendable @escaping (Failure) async -> MappedError) -> AsyncMapErrorSequence<Self, MappedError> {
25+
AsyncMapErrorSequence(base: self, transform: transform)
26+
}
3727
}
3828

3929
/// An asynchronous sequence that converts any failure into a new error.
40-
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
41-
fileprivate struct AsyncMapErrorSequence<Base: AsyncSequence, MappedError: Error>: AsyncSequence {
30+
@available(AsyncAlgorithms 1.1, *)
31+
public struct AsyncMapErrorSequence<Base: AsyncSequence, MappedError: Error> {
32+
public typealias Element = Base.Element
33+
public typealias Failure = MappedError
34+
35+
private let base: Base
36+
private let transform: @Sendable (Base.Failure) async -> MappedError
37+
38+
init(
39+
base: Base,
40+
transform: @Sendable @escaping (Base.Failure) async -> MappedError
41+
) {
42+
self.base = base
43+
self.transform = transform
44+
}
45+
}
4246

43-
typealias AsyncIterator = Iterator
44-
typealias Element = Base.Element
45-
typealias Failure = Base.Failure
47+
@available(AsyncAlgorithms 1.1, *)
48+
extension AsyncMapErrorSequence: AsyncSequence {
4649

47-
private let base: Base
48-
private let transform: @Sendable (Failure) -> MappedError
50+
/// The iterator that produces elements of the map sequence.
51+
public struct Iterator: AsyncIteratorProtocol {
52+
public typealias Element = Base.Element
53+
54+
private var base: Base.AsyncIterator
55+
56+
private let transform: @Sendable (Base.Failure) async -> MappedError
4957

5058
init(
51-
base: Base,
52-
transform: @Sendable @escaping (Failure) -> MappedError
59+
base: Base.AsyncIterator,
60+
transform: @Sendable @escaping (Base.Failure) async -> MappedError
5361
) {
54-
self.base = base
55-
self.transform = transform
62+
self.base = base
63+
self.transform = transform
5664
}
5765

58-
func makeAsyncIterator() -> Iterator {
59-
Iterator(
60-
base: base.makeAsyncIterator(),
61-
transform: transform
62-
)
66+
public mutating func next() async throws(MappedError) -> Element? {
67+
try await self.next(isolation: nil)
6368
}
64-
}
65-
66-
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
67-
extension AsyncMapErrorSequence {
68-
69-
/// The iterator that produces elements of the map sequence.
70-
fileprivate struct Iterator: AsyncIteratorProtocol {
71-
72-
typealias Element = Base.Element
73-
74-
private var base: Base.AsyncIterator
75-
76-
private let transform: @Sendable (Failure) -> MappedError
77-
78-
init(
79-
base: Base.AsyncIterator,
80-
transform: @Sendable @escaping (Failure) -> MappedError
81-
) {
82-
self.base = base
83-
self.transform = transform
84-
}
85-
86-
mutating func next() async throws(MappedError) -> Element? {
87-
try await self.next(isolation: nil)
88-
}
8969

90-
mutating func next(isolation actor: isolated (any Actor)?) async throws(MappedError) -> Element? {
91-
do {
92-
return try await base.next(isolation: actor)
93-
} catch {
94-
throw transform(error)
95-
}
96-
}
70+
public mutating func next(isolation actor: isolated (any Actor)?) async throws(MappedError) -> Element? {
71+
do {
72+
return try await base.next(isolation: actor)
73+
} catch {
74+
throw await transform(error)
75+
}
9776
}
77+
}
78+
79+
public func makeAsyncIterator() -> Iterator {
80+
Iterator(
81+
base: base.makeAsyncIterator(),
82+
transform: transform
83+
)
84+
}
9885
}
9986

100-
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
101-
extension AsyncMapErrorSequence: Sendable where Base: Sendable, Base.Element: Sendable {}
87+
@available(AsyncAlgorithms 1.1, *)
88+
extension AsyncMapErrorSequence: Sendable where Base: Sendable { }
10289

10390
@available(*, unavailable)
104-
extension AsyncMapErrorSequence.Iterator: Sendable {}
91+
extension AsyncMapErrorSequence.Iterator: Sendable { }
10592
#endif

0 commit comments

Comments
 (0)