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

Composing actions: how to do something like "firstEnabled"? #366

Closed
tikitu opened this issue May 10, 2017 · 4 comments
Closed

Composing actions: how to do something like "firstEnabled"? #366

tikitu opened this issue May 10, 2017 · 4 comments

Comments

@tikitu
Copy link

tikitu commented May 10, 2017

I have two Actions with the same input/output/error types, and I'd like to compose them into a single Action that runs whichever of the two is enabled (with an arbitrary tie-breaker if they both are).

Here's my first, failing, attempt:

let addOrRemove: Action<MyInput, MyOutput, APIRequestError> = Action(enabledIf: add.isEnabled.or(remove.isEnabled)) { input in
    if add.isEnabled.value {
        return add.apply(input)
    } else {
        return remove.apply(input)
    }
}

This fails because the inner add.apply(input) can't see that I checked add.isEnabled, so it wraps an additional ActionError<> layer around the error type. (This might be legit, as I'm not sure how thread-safe this approach would be, or might be a case of us knowing something the type system doesn't.) The corresponding type error is:

cannot convert return expression of type 'SignalProducer<MyOutput, ActionError<APIRequestError>>' to return type 'SignalProducer<MyOutput, APIRequestError>'

What would y'all suggest as a better approach?

I've asked this on Stack Overflow as well, might be a better venue: http://stackoverflow.com/q/43888981/323083

@ikesyo
Copy link
Member

ikesyo commented May 10, 2017

How about this?

let producer: SignalProducer<MyOutput, ActionError<APIRequestError>>
if add.isEnabled.value {
    producer = add.apply(input)
} else {
    producer = remove.apply(input)
}
return producer.flatMapError { error in
    switch error {
    case .disabled: return .empty
    case let .producerFailed(inner): return SignalProducer(error: inner)
    }
}

@tikitu
Copy link
Author

tikitu commented May 10, 2017

Typechecks and my tests pass! Thanks @ikesyo, and I'm learning by studying this too. If you want SO cred I'll happily accept this as an answer over there too.

If I understand correctly, this assumes the .disabled case will never occur. Just for interest I wonder if this is (multithreading-)guaranteed, or just very unlikely?

@tikitu tikitu closed this as completed May 11, 2017
@tikitu
Copy link
Author

tikitu commented May 11, 2017

I think I can answer my own question: if add is enabled when we call apply() but disabled by the time we call start(), we'll hit the .disabled case: in that case I think we'll report success (no values, but .completed) on the outer Action, instead of the disabled we should be getting. It's good enough for me, but I still wonder if there's a way to do this "properly" -- possibly not, as I guess it would require access to private internals of Action.

@tikitu
Copy link
Author

tikitu commented May 11, 2017

And for completeness: I ended up with the following, which I think improves slightly on the correctness of the original solution (by checking if add.isEnabled at start() time as well as apply() time). (It does double apply() work, but that shouldn't trigger side-effects I believe. Avoidable with @autoclosure I expect, if it worries you.) Drive-by readers please note I'm no expert, so write your own test cases.

func firstEnabledAction<Input, Output, Err>(
    _ first: Action<Input, Output, Err>,
    _ second: Action<Input, Output, Err>) -> Action<Input, Output, Err> {
    func whenDisabled(_ producer: SignalProducer<Output, ActionError<Err>>,
                      fallBackTo fallback: SignalProducer<Output, Err>) -> SignalProducer<Output, Err> {
        return producer.flatMapError {
            switch $0 {
            case .disabled:
                return fallback
            case .producerFailed(let inner):
                return SignalProducer(error: inner)
            }
        }
    }
    return Action(enabledIf: first.isEnabled.or(second.isEnabled)) { input in
        return whenDisabled(first.apply(input), 
                            fallBackTo: whenDisabled(second.apply(input), 
                                                     fallBackTo: .empty))
    }
}

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

No branches or pull requests

2 participants