diff --git a/CHANGELOG.md b/CHANGELOG.md index 17acf1441..f833d1436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # master *Please add new entries at the top.* +1. When composing `Signal` and `SignalProducer` of inhabitable types, e.g. `Never` or `NoError`, ReactiveSwift now warns about operators that are illogical to use, and traps at runtime when such operators attempt to instantiate an instance. (#429, kudos to @andersio) + 1. N-ary `SignalProducer` operators are now generic and accept any type that can be expressed as `SignalProducer`. (#410, kudos to @andersio) Types may conform to `SignalProducerConvertible` to be an eligible operand. diff --git a/ReactiveSwift.xcodeproj/project.pbxproj b/ReactiveSwift.xcodeproj/project.pbxproj index 3221f14e0..ea408588b 100644 --- a/ReactiveSwift.xcodeproj/project.pbxproj +++ b/ReactiveSwift.xcodeproj/project.pbxproj @@ -68,6 +68,10 @@ 9A1D067D1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */; }; 9A1D067E1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */; }; 9A1D067F1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */; }; + 9A5D93731EE5733300438925 /* InhabitableTypeGuards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5D93721EE5733300438925 /* InhabitableTypeGuards.swift */; }; + 9A5D93741EE5733300438925 /* InhabitableTypeGuards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5D93721EE5733300438925 /* InhabitableTypeGuards.swift */; }; + 9A5D93751EE5733300438925 /* InhabitableTypeGuards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5D93721EE5733300438925 /* InhabitableTypeGuards.swift */; }; + 9A5D93761EE5733300438925 /* InhabitableTypeGuards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5D93721EE5733300438925 /* InhabitableTypeGuards.swift */; }; 9A681A9E1E5A241B00B097CF /* DeprecationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A681A9D1E5A241B00B097CF /* DeprecationSpec.swift */; }; 9A681A9F1E5A241B00B097CF /* DeprecationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A681A9D1E5A241B00B097CF /* DeprecationSpec.swift */; }; 9A681AA01E5A241B00B097CF /* DeprecationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A681A9D1E5A241B00B097CF /* DeprecationSpec.swift */; }; @@ -245,6 +249,7 @@ 9A090C131DA0309E00EE97CA /* Reactive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reactive.swift; sourceTree = ""; }; 9A1A4F981E16961C006F3039 /* ValidatingPropertySpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidatingPropertySpec.swift; sourceTree = ""; }; 9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnidirectionalBindingSpec.swift; sourceTree = ""; }; + 9A5D93721EE5733300438925 /* InhabitableTypeGuards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InhabitableTypeGuards.swift; sourceTree = ""; }; 9A681A9D1E5A241B00B097CF /* DeprecationSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeprecationSpec.swift; sourceTree = ""; }; 9A9100DE1E0E6E620093E346 /* ValidatingProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidatingProperty.swift; sourceTree = ""; }; 9ABCB1841D2A5B5A00BCA243 /* Deprecations+Removals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Deprecations+Removals.swift"; sourceTree = ""; }; @@ -412,6 +417,7 @@ D08C54B11A69A2AC00AD8286 /* Signal.swift */, D08C54B21A69A2AC00AD8286 /* SignalProducer.swift */, BE9CF3941D751B6B003AE479 /* UnidirectionalBinding.swift */, + 9A5D93721EE5733300438925 /* InhabitableTypeGuards.swift */, ); name = Signals; sourceTree = ""; @@ -884,6 +890,7 @@ 9A090C171DA0309E00EE97CA /* Reactive.swift in Sources */, 57A4D1BB1BA13D7A00F7D4B1 /* Signal.swift in Sources */, 57A4D1BC1BA13D7A00F7D4B1 /* SignalProducer.swift in Sources */, + 9A5D93761EE5733300438925 /* InhabitableTypeGuards.swift in Sources */, 57A4D1BD1BA13D7A00F7D4B1 /* Atomic.swift in Sources */, 57A4D1BE1BA13D7A00F7D4B1 /* Bag.swift in Sources */, 57A4D1C01BA13D7A00F7D4B1 /* FoundationExtensions.swift in Sources */, @@ -938,6 +945,7 @@ 9A090C161DA0309E00EE97CA /* Reactive.swift in Sources */, A9B315C31B3940810001CB9C /* Signal.swift in Sources */, A9B315C41B3940810001CB9C /* SignalProducer.swift in Sources */, + 9A5D93751EE5733300438925 /* InhabitableTypeGuards.swift in Sources */, A9B315C51B3940810001CB9C /* Atomic.swift in Sources */, A9B315C61B3940810001CB9C /* Bag.swift in Sources */, A9B315C81B3940810001CB9C /* FoundationExtensions.swift in Sources */, @@ -965,6 +973,7 @@ D08C54B31A69A2AE00AD8286 /* Signal.swift in Sources */, D85C652A1C0D84C7005A77AD /* Flatten.swift in Sources */, D0C312CF19EF2A5800984962 /* Bag.swift in Sources */, + 9A5D93731EE5733300438925 /* InhabitableTypeGuards.swift in Sources */, 4A0E10FF1D2A92720065D310 /* Lifetime.swift in Sources */, D0C312E719EF2A5800984962 /* Scheduler.swift in Sources */, D0C312CD19EF2A5800984962 /* Atomic.swift in Sources */, @@ -1019,6 +1028,7 @@ 9A090C151DA0309E00EE97CA /* Reactive.swift in Sources */, D85C652B1C0E70E3005A77AD /* Flatten.swift in Sources */, 4A0E11001D2A92720065D310 /* Lifetime.swift in Sources */, + 9A5D93741EE5733300438925 /* InhabitableTypeGuards.swift in Sources */, D08C54BB1A69C54400AD8286 /* Property.swift in Sources */, D03B4A3E19F4C39A009E02AC /* FoundationExtensions.swift in Sources */, D08C54B71A69A3DB00AD8286 /* Event.swift in Sources */, diff --git a/Sources/InhabitableTypeGuards.swift b/Sources/InhabitableTypeGuards.swift new file mode 100644 index 000000000..5c5d44a4d --- /dev/null +++ b/Sources/InhabitableTypeGuards.swift @@ -0,0 +1,132 @@ +import Result + +// Observation +extension SignalProducer where Value == Never { + @discardableResult + @available(*, deprecated, message:"`Result.success` is never delivered - `Value` is inhabitable (Instantiation at runtime would trap)") + public func startWithResult(_ action: @escaping (Result) -> Void) -> Disposable { observingInhabitableTypeError() } + + @discardableResult + @available(*, deprecated, message:"Observer is never called - `Value` is inhabitable (Instantiation at runtime would trap)") + public func startWithValues(_ action: @escaping (Value) -> Void) -> Disposable { observingInhabitableTypeError() } +} + +extension SignalProducer where Value == Never, Error == NoError { + @discardableResult + @available(*, deprecated, message:"Observer is never called - `Value` and `Error` are inhabitable (Instantiation at runtime would trap)") + public func startWithResult(_ action: @escaping (Result) -> Void) -> Disposable { observingInhabitableTypeError() } +} + +extension SignalProducer where Error == NoError { + @discardableResult + @available(*, deprecated, message:"`Error` is inhabitable so the observer is never called (Instantiation at runtime would trap)") + public func startWithFailed(_ action: @escaping (Error) -> Void) -> Disposable { observingInhabitableTypeError() } +} + +extension Signal where Value == Never { + @discardableResult + @available(*, deprecated, message:"`Result.success` is never delivered - `Value` is inhabitable (Instantiation at runtime would trap)") + public func observeResult(_ action: @escaping (Result) -> Void) -> Disposable? { observingInhabitableTypeError() } + + @discardableResult + @available(*, deprecated, message:"Observer is never called - `Value` is inhabitable (Instantiation at runtime would trap)") + public func observeValues(_ action: @escaping (Value) -> Void) -> Disposable? { observingInhabitableTypeError() } +} + +extension Signal where Value == Never, Error == NoError { + @discardableResult + @available(*, deprecated, message:"Observer is never called - `Value` and `Error` are inhabitable (Instantiation at runtime would trap)") + public func observeResult(_ action: @escaping (Result) -> Void) -> Disposable? { observingInhabitableTypeError() } +} + +extension Signal where Error == NoError { + @discardableResult + @available(*, deprecated, message:"Observer is never invoked - `Error` is inhabitable (Instantiation at runtime would trap)") + public func observeFailed(_ action: @escaping (Error) -> Void) -> Disposable? { observingInhabitableTypeError() } +} + +// flatMap +extension SignalProducer where Value == Never { + @discardableResult + @available(*, deprecated, message:"Use `promoteValue` instead - `Value` is inhabitable (Instantiation at runtime would trap)") + public func flatMap(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> Inner) -> SignalProducer where Inner.Error == Error { observingInhabitableTypeError() } + + @discardableResult + @available(*, deprecated, message:"Use `promoteValue` instead - `Value` is inhabitable (Instantiation at runtime would trap)") + public func flatMap(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> Inner) -> SignalProducer where Inner.Error == NoError { observingInhabitableTypeError() } +} + +extension SignalProducer where Value == Never, Error == NoError { + @discardableResult + @available(*, deprecated, message:"Use `promoteValue` instead - `Value` and `Error` are inhabitable (Instantiation at runtime would trap)") + public func flatMap(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> Inner) -> SignalProducer { observingInhabitableTypeError() } + + @discardableResult + @available(*, deprecated, message:"Use `promoteValue` instead - `Value` and `Error` are inhabitable (Instantiation at runtime would trap)") + public func flatMap(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> Inner) -> SignalProducer where Inner.Error == Error { observingInhabitableTypeError() } +} + +extension SignalProducer where Error == NoError { + @discardableResult + @available(*, deprecated, message:"Use `promoteError` instead - `Error` is inhabitable (Instantiation at runtime would trap)") + public func flatMapError(_ transform: @escaping (Error) -> SignalProducer) -> SignalProducer { observingInhabitableTypeError() } +} + +extension Signal where Value == Never { + @discardableResult + @available(*, deprecated, message:"Use `promoteValue` instead - `Value` is inhabitable (Instantiation at runtime would trap)") + public func flatMap(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> Inner) -> Signal where Inner.Error == Error { observingInhabitableTypeError() } + + @discardableResult + @available(*, deprecated, message:"Use `promoteValue` instead - `Value` is inhabitable (Instantiation at runtime would trap)") + public func flatMap(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> Inner) -> Signal where Inner.Error == NoError { observingInhabitableTypeError() } + +} + +extension Signal where Value == Never, Error == NoError { + @discardableResult + @available(*, deprecated, message:"Use `promoteValue` instead - `Value` and `Error` are inhabitable (Instantiation at runtime would trap)") + public func flatMap(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> Inner) -> Signal { observingInhabitableTypeError() } + + @discardableResult + @available(*, deprecated, message:"Use `promoteValue` instead - `Value` and `Error` are inhabitable (Instantiation at runtime would trap)") + public func flatMap(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> Inner) -> Signal where Inner.Error == Error { observingInhabitableTypeError() } +} + +extension Signal where Error == NoError { + @discardableResult + @available(*, deprecated, message:"Use `promoteError` instead - `Error` is inhabitable (Instantiation at runtime would trap)") + public func flatMapError(_ transform: @escaping (Error) -> SignalProducer) -> Signal { observingInhabitableTypeError() } +} + +@inline(never) +private func observingInhabitableTypeError() -> Never { + fatalError("Detected an attempt to instantiate a `Signal` or `SignalProducer` that observes an inhabitable type, e.g. `Never` or `NoError`. This is considered a logical error, and appropriate operators should be used instead. Please refer to the warnings raised by the compiler.") +} + +/* +func test() { + SignalProducer.never.startWithResult { _ in } + SignalProducer.never.startWithResult { _ in } + SignalProducer.never.startWithFailed { _ in } + SignalProducer.never.startWithFailed { _ in } + Signal.never.observeResult { _ in } + Signal.never.observeResult { _ in } + Signal.never.observeFailed { _ in } + Signal.never.observeFailed { _ in } + + SignalProducer.never.flatMap(.latest) { _ in SignalProducer.empty } + SignalProducer.never.flatMap(.latest) { _ in SignalProducer.empty } + SignalProducer.never.flatMap(.latest) { _ in SignalProducer.empty } + SignalProducer.never.flatMap(.latest) { _ in SignalProducer.empty } + SignalProducer.never.flatMapError { _ in SignalProducer.empty } + SignalProducer.never.flatMapError { _ in SignalProducer.empty } + + Signal.never.flatMap(.latest) { _ in SignalProducer.empty } + Signal.never.flatMap(.latest) { _ in SignalProducer.empty } + Signal.never.flatMap(.latest) { _ in SignalProducer.empty } + Signal.never.flatMap(.latest) { _ in SignalProducer.empty } + Signal.never.flatMapError { _ in SignalProducer.empty } + Signal.never.flatMapError { _ in SignalProducer.empty } +} +*/ diff --git a/Sources/Signal.swift b/Sources/Signal.swift index 50d5128ba..32663e450 100644 --- a/Sources/Signal.swift +++ b/Sources/Signal.swift @@ -2473,6 +2473,47 @@ extension Signal where Error == NoError { } } +extension Signal where Value == Never { + /// Promote a signal that does not generate values, as indicated by `Never`, to be + /// a signal of the given type of value. + /// + /// - note: The promotion does not result in any value being generated. + /// + /// - parameters: + /// - _ The type of value to promote to. + /// + /// - returns: A signal that forwards all terminal events from `self`. + public func promoteValue(_: U.Type = U.self) -> Signal { + return Signal { observer in + return self.observe { event in + switch event { + case .value: + fatalError("Never is impossible to construct") + case let .failed(error): + observer.send(error: error) + case .completed: + observer.sendCompleted() + case .interrupted: + observer.sendInterrupted() + } + } + } + } + + /// Promote a signal that does not generate values, as indicated by `Never`, to be + /// a signal of the given type of value. + /// + /// - note: The promotion does not result in any value being generated. + /// + /// - parameters: + /// - _ The type of value to promote to. + /// + /// - returns: A signal that forwards all terminal events from `self`. + public func promoteValue(_: Value.Type = Value.self) -> Signal { + return self + } +} + extension Signal where Value == Bool { /// Create a signal that computes a logical NOT in the latest values of `self`. /// diff --git a/Sources/SignalProducer.swift b/Sources/SignalProducer.swift index e3694b5a5..c69b50f98 100644 --- a/Sources/SignalProducer.swift +++ b/Sources/SignalProducer.swift @@ -1392,6 +1392,34 @@ extension SignalProducer where Error == AnyError { } } +extension SignalProducer where Value == Never { + /// Promote a signal that does not generate values, as indicated by `Never`, to be + /// a signal of the given type of value. + /// + /// - note: The promotion does not result in any value being generated. + /// + /// - parameters: + /// - _ The type of value to promote to. + /// + /// - returns: A signal that forwards all terminal events from `self`. + public func promoteValue(_: U.Type = U.self) -> SignalProducer { + return lift { $0.promoteValue(U.self) } + } + + /// Promote a signal that does not generate values, as indicated by `Never`, to be + /// a signal of the given type of value. + /// + /// - note: The promotion does not result in any value being generated. + /// + /// - parameters: + /// - _ The type of value to promote to. + /// + /// - returns: A signal that forwards all terminal events from `self`. + public func promoteValue(_: Value.Type = Value.self) -> SignalProducer { + return self + } +} + extension SignalProducer where Value: Equatable { /// Forward only values from `self` that are not equal to its immediately preceding /// value. diff --git a/Tests/ReactiveSwiftTests/SignalSpec.swift b/Tests/ReactiveSwiftTests/SignalSpec.swift index 90949bc77..f87e07afa 100755 --- a/Tests/ReactiveSwiftTests/SignalSpec.swift +++ b/Tests/ReactiveSwiftTests/SignalSpec.swift @@ -3105,6 +3105,16 @@ class SignalSpec: QuickSpec { expect(combined is Signal<(Int, Double, Float, UInt), TestError>) == true } } + + describe("promoteValue") { + it("should infer the value type from the context") { + let completable = Signal.never + let producer: Signal = Signal.never + .flatMap(.latest) { _ in completable.promoteValue() } + + expect((producer as Any) is Signal) == true + } + } } }