Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename SignalProducer.Type.attempt to init(_:). #346

Merged
merged 1 commit into from
May 15, 2017

Conversation

andersio
Copy link
Member

@andersio andersio commented Apr 24, 2017

Follow-up to #304.

The original signatures are ambiguous, but they somehow work as protocol extensions in Swift 3. Moving them to the concrete type reveals the ambiguity.

extension SignalProducer {
	public static func attempt(_ operation: @escaping () -> Result<Value, Error>) -> SignalProducer<Value, Error>
}

extension SignalProducer where Error == AnyError {
	public static func attempt(_ operation: @escaping () throws -> Value) -> SignalProducer<Value, Error>
}

Consider .attempt { () -> Result<Int, NoError> in .success(1) }, the closure can satisfy both methods and produce:

  1. SignalProducer<Int, NoError>; or
  2. SignalProducer<Result<Int, NoError>, AnyError> since non-throwing closures are covariant of their throwing version.

Solution

Renaming SignalProducer.Type.attempt to init(_:) mitigated the ambiguity. How this happens is uncertain, given that Swift has no documentation on the overload selection rules at the moment.

Edit: It is suspected that these as static functions require the compiler to consider two additional generic parameters in the return type. Being init(_:), however, binds the return type to be Self, and reduce the overload selection factors to the arguments.

@mdiep
Copy link
Contributor

mdiep commented Apr 26, 2017

Does this ambiguity cause problems in practice?

It's ambiguous for our test suite because we do this:

let operation: () -> Result<String, NSError> = {
	operationRunTimes += 1

	return .success("OperationValue")
}

SignalProducer.attempt(operation).start()
SignalProducer.attempt(operation).start()

But I think that's understandable.

Ordinarily, you wouldn't use a SignalProducer like that—you'd return it from some function which will add additional type information.

This is okay, for instance:

let operation: () -> Result<String, NSError> = {
	operationRunTimes += 1

	return .success("OperationValue")
}

func makeProducer() -> SignalProducer<String, NSError> {
	return .attempt(operation)
}

makeProducer().start()
makeProducer().start()

@andersio
Copy link
Member Author

andersio commented Apr 27, 2017

The type checker works in this context:

func attempt(_ operation: @escaping () -> Result<String, NSError>) -> SignalProducer<(), NSError> {
	return SignalProducer.attempt(operation)
		.on(started: { started += 1 })
		.map { _ in }
}

But not if the Result has a type of AnyError:

func attempt2(_ operation: @escaping () -> Result<String, AnyError>) -> SignalProducer<(), AnyError> {
	return SignalProducer.attempt(operation) // Ambiguous use of `attempt`.
		.on(started: { started += 1 })
		.map { _ in () }
}

@mdiep
Copy link
Contributor

mdiep commented Apr 27, 2017

But not if the Result has a type of AnyError

I think we could add a 3rd overload to disambiguate that.

extension SignalProducerProtocol where Error == AnyError {
	public static func attempt(_ operation: @escaping () -> Result<Value, Error>) -> SignalProducer<Value, Error>
}

That seems like the same thing we do for flatMap.

@andersio
Copy link
Member Author

andersio commented Apr 27, 2017

The problem is that the type checker can infer Value to be Result<Value, Error>, or Value inside the Result. Both are valid to form the return type, which is now ambiguous. This overload does not solve the ambiguity.

Copy link
Contributor

@sharplet sharplet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to give my 👍 purely from an API design point of view here. An initialiser strikes me as more idiomatic in Swift.

@andersio
Copy link
Member Author

andersio commented May 2, 2017

As a side note (mentioned on Slack), there is a workaround for attempt to work without ambiguity:

public protocol _AnyError {}
extension AnyError: _AnyError {}
extension SignalProducer where Value: _AnyError { ... }

@andersio andersio modified the milestone: 2.0 May 5, 2017
/// - operation: A failable closure.
public init(_ action: @escaping () throws -> Value) {
self.init {
return ReactiveSwift.materialize {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we actually need this materialize alias. I think we can do Result(attempt: try action()).

Copy link
Contributor

@mdiep mdiep left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still prefer .attempt, but I accept that Swift has tied our hands here.

@andersio andersio force-pushed the producer-protocol-cleanup-2 branch from a9f6538 to 3dc5f34 Compare May 15, 2017 13:57
@mdiep mdiep merged commit a02bb4c into master May 15, 2017
@mdiep mdiep deleted the producer-protocol-cleanup-2 branch May 15, 2017 20:19
@ikesyo
Copy link
Member

ikesyo commented Jul 24, 2017

I've just experienced a weirdness due to this and the attemptMap overloads: mdiep/Tentacle#86 (comment).

@ikesyo
Copy link
Member

ikesyo commented Jul 26, 2017

FYI I suffered this again in Carthage: Carthage/Carthage#2077.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants