Skip to content

Commit c9cb48b

Browse files
authored
Merge pull request #141 from ReactiveCocoa/as-throwing-operators
Add throwing versions of attempt(_:) and attemptMap(_:)
2 parents 4e0785d + 06a2d53 commit c9cb48b

File tree

7 files changed

+411
-74
lines changed

7 files changed

+411
-74
lines changed

Documentation/BasicOperators.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ types will be referred to by name.
3838
1. [Concatenating](#concatenating)
3939
1. [Switching to the latest](#switching-to-the-latest)
4040

41-
**[Handling failures](#handling-failures)**
41+
**[Working with errors](#working-with-errors)**
4242

4343
1. [Catching failures](#catching-failures)
44+
1. [Failable transformations](#failable-transformations)
4445
1. [Retrying](#retrying)
4546
1. [Mapping errors](#mapping-errors)
4647
1. [Promote](#promote)
@@ -370,9 +371,9 @@ lettersObserver.send(value: "c") // nothing printed
370371
numbersObserver.send(value: "3") // prints "3"
371372
```
372373

373-
## Handling failures
374+
## Working with errors
374375

375-
These operators are used to handle failures that might occur on an event stream.
376+
These operators are used to handle failures that might occur on an event stream, or perform operations that might fail on an event stream.
376377

377378
### Catching failures
378379

@@ -394,6 +395,32 @@ observer.send(value: "Second") // prints "Second"
394395
observer.send(error: error) // prints "Default"
395396
```
396397

398+
### Failable transformations
399+
400+
`SignalProducer.attempt(_:)` allows you to turn a failable operation into an event stream.
401+
The `attempt(_:)` and `attemptMap(_:)` operators allow you to perform failable operations or transformations on an event stream.
402+
403+
```swift
404+
let dictionaryPath = URL(fileURLWithPath: "/usr/share/dict/words")
405+
406+
// Create a `SignalProducer` that lazily attempts the closure
407+
// whenever it is started
408+
let data = SignalProducer.attempt { try Data(contentsOf: dictionaryPath) }
409+
410+
// Lazily apply a failable transformation
411+
let json = data.attemptMap { try JSONSerialization.jsonObject(with: $0) }
412+
413+
json.startWithResult { result in
414+
switch result {
415+
case let .success(words):
416+
print("Dictionary as JSON:")
417+
print(words)
418+
case let .failure(error):
419+
print("Couldn't parse dictionary as JSON: \(error)")
420+
}
421+
}
422+
```
423+
397424
### Retrying
398425

399426
The `retry` operator will restart the original `SignalProducer` on failure up to `count` times.

ReactiveSwift.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
4A0E11041D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
2020
4A0E11051D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
2121
4A0E11061D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
22+
4AC73ECB1DF273570004EC4F /* ResultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */; };
23+
4AC73ECC1DF273570004EC4F /* ResultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */; };
24+
4AC73ECD1DF273570004EC4F /* ResultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */; };
25+
4AC73ECE1DF273570004EC4F /* ResultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */; };
2226
579504331BB8A34200A5E482 /* BagSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C312EF19EF2A7700984962 /* BagSpec.swift */; };
2327
579504341BB8A34300A5E482 /* BagSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C312EF19EF2A7700984962 /* BagSpec.swift */; };
2428
57A4D1B11BA13D7A00F7D4B1 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = D871D69E1B3B29A40070F16C /* Optional.swift */; };
@@ -225,6 +229,7 @@
225229
4A0AB6711DC28EFF00AA1E81 /* ReactiveExtensionsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveExtensionsSpec.swift; sourceTree = "<group>"; };
226230
4A0E10FE1D2A92720065D310 /* Lifetime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lifetime.swift; sourceTree = "<group>"; };
227231
4A0E11031D2A95200065D310 /* LifetimeSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifetimeSpec.swift; sourceTree = "<group>"; };
232+
4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultExtensions.swift; sourceTree = "<group>"; };
228233
57A4D2411BA13D7A00F7D4B1 /* ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
229234
57A4D2441BA13F9700F7D4B1 /* tvOS-Application.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Application.xcconfig"; sourceTree = "<group>"; };
230235
57A4D2451BA13F9700F7D4B1 /* tvOS-Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Base.xcconfig"; sourceTree = "<group>"; };
@@ -405,6 +410,7 @@
405410
D03B4A3A19F4C26D009E02AC /* Internal Utilities */ = {
406411
isa = PBXGroup;
407412
children = (
413+
4AC73ECA1DF273570004EC4F /* ResultExtensions.swift */,
408414
D00004081A46864E000E7D41 /* TupleExtensions.swift */,
409415
);
410416
name = "Internal Utilities";
@@ -869,6 +875,7 @@
869875
57A4D1B41BA13D7A00F7D4B1 /* Disposable.swift in Sources */,
870876
57A4D1B61BA13D7A00F7D4B1 /* Event.swift in Sources */,
871877
57A4D1B81BA13D7A00F7D4B1 /* Scheduler.swift in Sources */,
878+
4AC73ECE1DF273570004EC4F /* ResultExtensions.swift in Sources */,
872879
57A4D1B91BA13D7A00F7D4B1 /* Action.swift in Sources */,
873880
57A4D1BA1BA13D7A00F7D4B1 /* Property.swift in Sources */,
874881
9A090C171DA0309E00EE97CA /* Reactive.swift in Sources */,
@@ -920,6 +927,7 @@
920927
A9B315BC1B3940810001CB9C /* Disposable.swift in Sources */,
921928
A9B315BE1B3940810001CB9C /* Event.swift in Sources */,
922929
A9B315C01B3940810001CB9C /* Scheduler.swift in Sources */,
930+
4AC73ECD1DF273570004EC4F /* ResultExtensions.swift in Sources */,
923931
A9B315C11B3940810001CB9C /* Action.swift in Sources */,
924932
A9B315C21B3940810001CB9C /* Property.swift in Sources */,
925933
9A090C161DA0309E00EE97CA /* Reactive.swift in Sources */,
@@ -946,6 +954,7 @@
946954
D871D69F1B3B29A40070F16C /* Optional.swift in Sources */,
947955
D08C54B61A69A3DB00AD8286 /* Event.swift in Sources */,
948956
D0C312D319EF2A5800984962 /* Disposable.swift in Sources */,
957+
4AC73ECB1DF273570004EC4F /* ResultExtensions.swift in Sources */,
949958
EBCC7DBC1BBF010C00A2AE92 /* Observer.swift in Sources */,
950959
D03B4A3D19F4C39A009E02AC /* FoundationExtensions.swift in Sources */,
951960
9A090C141DA0309E00EE97CA /* Reactive.swift in Sources */,
@@ -997,6 +1006,7 @@
9971006
D8E84A671B3B32FB00C3E831 /* Optional.swift in Sources */,
9981007
D0C312D419EF2A5800984962 /* Disposable.swift in Sources */,
9991008
D08C54B91A69A9D100AD8286 /* SignalProducer.swift in Sources */,
1009+
4AC73ECC1DF273570004EC4F /* ResultExtensions.swift in Sources */,
10001010
9ABCB1861D2A5B5A00BCA243 /* Deprecations+Removals.swift in Sources */,
10011011
EBCC7DBD1BBF01E100A2AE92 /* Observer.swift in Sources */,
10021012
9A090C151DA0309E00EE97CA /* Reactive.swift in Sources */,

Sources/ResultExtensions.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Result
2+
3+
/// Private alias of the free `materialize()` from `Result`.
4+
///
5+
/// This exists because within a `Signal` or `SignalProducer` operator,
6+
/// `materialize()` refers to the operator with that name.
7+
/// Namespacing as `Result.materialize()` doesn't work either,
8+
/// because it tries to resolve a static member on the _type_
9+
/// `Result`, rather than the free function in the _module_
10+
/// of the same name.
11+
internal func materialize<T>(_ f: () throws -> T) -> Result<T, AnyError> {
12+
return materialize(try f())
13+
}

Sources/Signal.swift

Lines changed: 116 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1651,51 +1651,6 @@ extension SignalProtocol {
16511651
}
16521652
}
16531653

1654-
/// Apply `operation` to values from `self` with `success`ful results
1655-
/// forwarded on the returned signal and `failure`s sent as failed events.
1656-
///
1657-
/// - parameters:
1658-
/// - operation: A closure that accepts a value and returns a `Result`.
1659-
///
1660-
/// - returns: A signal that receives `success`ful `Result` as `value` event
1661-
/// and `failure` as failed event.
1662-
public func attempt(_ operation: @escaping (Value) -> Result<(), Error>) -> Signal<Value, Error> {
1663-
return attemptMap { value in
1664-
return operation(value).map {
1665-
return value
1666-
}
1667-
}
1668-
}
1669-
1670-
/// Apply `operation` to values from `self` with `success`ful results mapped
1671-
/// on the returned signal and `failure`s sent as failed events.
1672-
///
1673-
/// - parameters:
1674-
/// - operation: A closure that accepts a value and returns a result of
1675-
/// a mapped value as `success`.
1676-
///
1677-
/// - returns: A signal that sends mapped values from `self` if returned
1678-
/// `Result` is `success`ful, `failed` events otherwise.
1679-
public func attemptMap<U>(_ operation: @escaping (Value) -> Result<U, Error>) -> Signal<U, Error> {
1680-
return Signal { observer in
1681-
self.observe { event in
1682-
switch event {
1683-
case let .value(value):
1684-
operation(value).analysis(
1685-
ifSuccess: observer.send(value:),
1686-
ifFailure: observer.send(error:)
1687-
)
1688-
case let .failed(error):
1689-
observer.send(error: error)
1690-
case .completed:
1691-
observer.sendCompleted()
1692-
case .interrupted:
1693-
observer.sendInterrupted()
1694-
}
1695-
}
1696-
}
1697-
}
1698-
16991654
/// Throttle values sent by the receiver, so that at least `interval`
17001655
/// seconds pass between each, then forwards them on the given scheduler.
17011656
///
@@ -2230,3 +2185,119 @@ extension SignalProtocol where Error == NoError {
22302185
.timeout(after: interval, raising: error, on: scheduler)
22312186
}
22322187
}
2188+
2189+
extension SignalProtocol {
2190+
/// Apply `operation` to values from `self` with `success`ful results
2191+
/// forwarded on the returned signal and `failure`s sent as failed events.
2192+
///
2193+
/// - parameters:
2194+
/// - operation: A closure that accepts a value and returns a `Result`.
2195+
///
2196+
/// - returns: A signal that receives `success`ful `Result` as `value` event
2197+
/// and `failure` as failed event.
2198+
public func attempt(_ operation: @escaping (Value) -> Result<(), Error>) -> Signal<Value, Error> {
2199+
return attemptMap { value in
2200+
return operation(value).map {
2201+
return value
2202+
}
2203+
}
2204+
}
2205+
2206+
/// Apply `operation` to values from `self` with `success`ful results mapped
2207+
/// on the returned signal and `failure`s sent as failed events.
2208+
///
2209+
/// - parameters:
2210+
/// - operation: A closure that accepts a value and returns a result of
2211+
/// a mapped value as `success`.
2212+
///
2213+
/// - returns: A signal that sends mapped values from `self` if returned
2214+
/// `Result` is `success`ful, `failed` events otherwise.
2215+
public func attemptMap<U>(_ operation: @escaping (Value) -> Result<U, Error>) -> Signal<U, Error> {
2216+
return Signal { observer in
2217+
self.observe { event in
2218+
switch event {
2219+
case let .value(value):
2220+
operation(value).analysis(
2221+
ifSuccess: observer.send(value:),
2222+
ifFailure: observer.send(error:)
2223+
)
2224+
case let .failed(error):
2225+
observer.send(error: error)
2226+
case .completed:
2227+
observer.sendCompleted()
2228+
case .interrupted:
2229+
observer.sendInterrupted()
2230+
}
2231+
}
2232+
}
2233+
}
2234+
}
2235+
2236+
extension SignalProtocol where Error == NoError {
2237+
/// Apply a failable `operation` to values from `self` with successful
2238+
/// results forwarded on the returned signal and thrown errors sent as
2239+
/// failed events.
2240+
///
2241+
/// - parameters:
2242+
/// - operation: A failable closure that accepts a value.
2243+
///
2244+
/// - returns: A signal that forwards successes as `value` events and thrown
2245+
/// errors as `failed` events.
2246+
public func attempt(_ operation: @escaping (Value) throws -> Void) -> Signal<Value, AnyError> {
2247+
return self
2248+
.promoteErrors(AnyError.self)
2249+
.attempt(operation)
2250+
}
2251+
2252+
/// Apply a failable `operation` to values from `self` with successful
2253+
/// results mapped on the returned signal and thrown errors sent as
2254+
/// failed events.
2255+
///
2256+
/// - parameters:
2257+
/// - operation: A failable closure that accepts a value and attempts to
2258+
/// transform it.
2259+
///
2260+
/// - returns: A signal that sends successfully mapped values from `self`, or
2261+
/// thrown errors as `failed` events.
2262+
public func attemptMap<U>(_ operation: @escaping (Value) throws -> U) -> Signal<U, AnyError> {
2263+
return self
2264+
.promoteErrors(AnyError.self)
2265+
.attemptMap(operation)
2266+
}
2267+
}
2268+
2269+
extension SignalProtocol where Error == AnyError {
2270+
/// Apply a failable `operation` to values from `self` with successful
2271+
/// results forwarded on the returned signal and thrown errors sent as
2272+
/// failed events.
2273+
///
2274+
/// - parameters:
2275+
/// - operation: A failable closure that accepts a value.
2276+
///
2277+
/// - returns: A signal that forwards successes as `value` events and thrown
2278+
/// errors as `failed` events.
2279+
public func attempt(_ operation: @escaping (Value) throws -> Void) -> Signal<Value, AnyError> {
2280+
return attemptMap { value in
2281+
try operation(value)
2282+
return value
2283+
}
2284+
}
2285+
2286+
/// Apply a failable `operation` to values from `self` with successful
2287+
/// results mapped on the returned signal and thrown errors sent as
2288+
/// failed events.
2289+
///
2290+
/// - parameters:
2291+
/// - operation: A failable closure that accepts a value and attempts to
2292+
/// transform it.
2293+
///
2294+
/// - returns: A signal that sends successfully mapped values from `self`, or
2295+
/// thrown errors as `failed` events.
2296+
public func attemptMap<U>(_ operation: @escaping (Value) throws -> U) -> Signal<U, AnyError> {
2297+
return attemptMap { value in
2298+
ReactiveSwift.materialize {
2299+
try operation(value)
2300+
}
2301+
}
2302+
}
2303+
}

0 commit comments

Comments
 (0)