GithubHelp home page GithubHelp logo

Comments (11)

andersio avatar andersio commented on April 28, 2024

This makes Actions operate on immutable snapshots of the input, and aligns better with the spirits of FP.

The downside is, as you might have implied, that it is less ideal as a bridge to the imperative world.

Edit:
Moreover, this might limit the applicability of Action, I'd say. Say if we have an action that is expected to be one-to-one in input and output, are we supposed to guarantee the order of apply and the input source among multiple clients?

e.g.

// Thread 1
coordinator.nextOpenToken.value = tokenA
coordinator.open.apply().startWithFailed { error in
    // prompt user about the error.
}

// Thread 2
coordinator.nextOpenToken.value = tokenB
coordinator.open.apply() .startWithFailed { error in
    // prompt user about the error.
}

from reactiveswift.

mdiep avatar mdiep commented on April 28, 2024

This definitely limits the applicablility of Action. The question is whether the eliminated applications are useful and good, or whether this clarifies the role of actions.

from reactiveswift.

andersio avatar andersio commented on April 28, 2024

One question though - would apply capture the input at its call time, or would apply return a producer that operates on the current value when it starts?

In the later case, apply might not necessary return a producer, but start the execution immediately, since we no longer need to inject the input as what apply meant to do.

(It seems to be starting immediately as apply was said to be bindable to trigger signals)

I don't know how to describe it precisely. Just trying my best.

What's eliminated are those having multiple input sources competing for execution.

The entire bloc of a form UI can be treated as the sole and only input source of the view model. So Action in the proposal for view models in such cases is perfectly fine.

But for the example I put above*, there are multiple concurrent clients that want to execute the action with the value they proposed. With this proposal, there is no definitive way to:

  1. ensure atomicity of input-then-execution, as clients could overwrite each other's input; and
  2. differentiate their results from each other, via the producer returned by the old apply.

The proposal axes these semantics, in order to eliminate the race condition between the execution closure and the enabling state. But the said race condition is probably relevant to just UI programming, as I feel the use of it in the model layer (or anything outside the VM-View bindings) would most likely be a plain stored boolean.

Nonetheless, the proposed API does feel better for UI programming IMO. There is no better alternative either, assuming thread containment is always off the table.

* That is an action, hosted in the app coordinator, to handle opening of a new document (window). That's said I could instead turn it back being imperative, and move the action (which now calls the imperative version when executed) to the view model to fit this new semantic of Action.

from reactiveswift.

sharplet avatar sharplet commented on April 28, 2024

I don't know how widely-used Action is. To me, it feels harder to use than it should be (in the sense that it's super useful and things that are super useful should also be easy to use).

Here's what I get out of it:

  • Serialisation: I know that when the action's producer is executing, it is the only one executing. I can trust that side effects won't be executed more than I expect.
  • Command-Query Separation: Signal producers execute their side effects for every observer, and so it can become quite difficult to reason about what side effects will occur, and when. Actions give me a way to separate the work to be done from observing the progress or effect of that work.

On the other hand, there's a really great symmetry between these two type signatures:

let execute: (Input) -> SignalProducer<Output, Error>
let task: Action<Input, Output, Error>

task = Action(execute)

I wonder if there's two different concepts here? And maybe one could be composed from the other?

I'm still trying to think this through...

from reactiveswift.

dmcrodrigues avatar dmcrodrigues commented on April 28, 2024

Action is a concept super useful and powerful to me especially, when used at UI layer.

But this does require (1) removing the input type from Action and (2) removing the input parameter from apply(). (2) could be especially problematic, depending on how people are currently using Action. But in the standard bind-this-action-to-a-button case, this model seems like it would be simpler.

@mdiep I like how your proposal aims for immutability which is definitely a good thing. Unfortunately, personally speaking about my use of actions, I have a few cases where the input is dynamic, e.g. according to the value of a UISwitch, and others where my enabledIf property is not related at all with my input.

But doing a more philosophical analysis, can we really avoid this kind of bad usage? I mean, this definetly makes it more harder but it's still possible to ignore the input and access a shared state while defining the executing block (which can be the body of a SignalProducer).

But I'm still reflecting about this, it's definitely not an easy one.

from reactiveswift.

mdiep avatar mdiep commented on April 28, 2024

Unfortunately, personally speaking about my use of actions, I have a few cases where the input is dynamic, e.g. according to the value of a UISwitch

You could still achieve that.

let switch = UISwitch()
let property = Property<Bool>(value: false, then: switch.reactive.isOnValues)
let action = Action(input) { ... }

from reactiveswift.

erichoracek avatar erichoracek commented on April 28, 2024

We use the RAC2 equivalent of Action (RACCommand) extensively in our ReactiveObjC codebase, so I think I can speak to our usage of RACCommand's enabled property and how we tend to use it in practice.

In cases where an Action's enabled-ness depends exclusively on its inputs, a pattern like the proposed makes total sense. We have definitely been frustrated by the fact that the state that changes the enabled-ness of an Action is not read at the same time that the apply() closure of the action is invoked (and thus can introduce inconsistencies).

However, this use is not the only way that we use Action. In our codebase, approximately 2 of every 3 Actions use input values. As a specific example from our codebase, say that we have an Action that presents a modal when apply() is invoked. The action has the following signature:

Action<Bool, ModalViewModel>

Where the input Bool value is whether or not the presentation is animated, and the apply SignalProducer sends values of type ModalViewModel for the view model backing the presentation. Additionally, this Action is enabled when the presentation can occur, and disabled for the entirety of the presentation (until the modal is dismissed).

In this case, if the input type was removed, there would be no way for a consumer (such as a button) to indicate whether or not the presentation would be animated. Additionally, the enabled-ness of this Action does not depend on its inputs, which (as far as I can tell) is incompatible with the proposed changes to action.

Following from the above, perhaps this behavior could be built in a way where it is available as an option when creating Actions, but is not the only way to work with them (somewhat like #22). Throwing out the ability for Action to have an InputType would (in my mind) be a much greater loss than the current frustration of having to deal with enabled-ness of Actions not necessarily being in sync with invocations of apply().

from reactiveswift.

mdiep avatar mdiep commented on April 28, 2024

Where the input Bool value is whether or not the presentation is animated

Could you share a bit more about where the Bool value comes from?

from reactiveswift.

erichoracek avatar erichoracek commented on April 28, 2024

Sure—the Bool value can come from a number of places:

  • The UIViewController that consumes the view model can decide to perform an animated presentation, and executes the Action with true (in the case of a cell or button being tapped)
  • Within the view model itself, if a presentation is triggered as a side-effect of another action.
  • A consumer of the view model that wants to perform a presentation non-animatedly (in the case of a deep link that synchronously creates a stack of presented modals at launch-time)

from reactiveswift.

sharplet avatar sharplet commented on April 28, 2024

So it seems clear that there's some value to both ideas: a) Action having an Input type, and b) Action being able to observe some external state in a thread-safe way.

Earlier on in #22 I had an idea to make (b) happen, but never got it to work. This conversation inspired me to take another crack at it, and I think I've got it: fea3dc6.

@mdiep's original example can now work, with only a slight change to preserve the Input parameter:

class LoginViewModel {
    let username = Property<String>()
    let password = Property<String>()
    let action: Action<(), Void, LoginError> // input type is still part of the type

    init() {
        let form = Property.combineLatest(username, password)

        // this is still a little messy, but I think we can
        // layer some nicer convenience initialisers on top of this API
        action = Action(_internalState: input, enabledIf: { !$0.isEmpty && !$1.isEmpty }) { (username, password), _ in
          
        }
    }
}


// elsewhere
loginViewModel.action.apply()

from reactiveswift.

mdiep avatar mdiep commented on April 28, 2024

That approach looks fine to me!

from reactiveswift.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.