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

Refactor to Allow Explicit Inputs. #32

Closed
wants to merge 5 commits into from
Closed

Conversation

zzcgumn
Copy link
Contributor

@zzcgumn zzcgumn commented Oct 30, 2018

Current production code tend to subscribe to external inputs in a way that is slightly awkward. In this example from the demo app it is quite difficult to reason about when the startLoadingNextPage event will fire.

   Feedback(predicate: { !$0.paging }) { _ in
                  nearBottomSignal.map { Event.startLoadingNextPage }
   }

I propose that we resolve this by adding explicit inputs which listen to an external input. When an input is received they evaluate a function (State, InputEvent) -> Event? which models if and how the system should react to an InputEvent given its current State.

This code would need a bit of tidying up before being merged, but I would appreciate feedback on whether explicit inputs is considered to be a good design and if the proposed implementation can be improved.

❤️
@zzcgumn

@ilyapuchka
Copy link
Contributor

ilyapuchka commented Nov 3, 2018

@zzcgumn I had a similar issue once when I was trying to make a state to react on some external signal. But thinking of it now can we achieve the same by creating a regular Feedback from this signal and add it into feedback loop as we do for UI actions?
Here instead of Event.ui we use a new Event.external type of event, or we can just have a switch right in the feedback loop to return some concrete events if we don't want to wrap them like we wrap UI actions.

class ViewModel {
    init(externalSignal: Signal<ExternalSignalValue, NoError>) {
        ...
        state = Property(
            initial: .initial,
            reduce: ViewModel.reduce,
            feedbacks: [
                ViewModel
                    .userActions(actions: actions),
                ...,
                ViewModel
                    .externalSignal(externalSignal),
        )
    }
}

extension ViewModel {
    static func externalSignal(_ signal: Signal<ExternalSignalValue, NoError>) -> Feedback<State, Event> {
        return Feedback { _ in
            return signal.map(Event.external)
        }
    }
}

enum Event {
  case ui
  ...
  case external(ExternalSignalValue)
}

@andersio
Copy link
Contributor

andersio commented Nov 17, 2018

While I agree on the need of convenience for turning external reactive sources into Events, performing conditional evaluation against the state outside the reducer has considerable implication on consistency if not carefully crafted. I would prefer not to relax the "everything in reducer" mental model we have.

@RuiAAPeres
Copy link
Contributor

RuiAAPeres commented Nov 17, 2018

Hi gentlemen. 👋 There are two parts to this proposal and we should tackle them separately and consequently in the right order and place.

  1. Allowing these changes, proposed by @zzcgumn, to be merged. This by itself is IMO what this proposal should be about. As Anders said, this is something that was discussed before and eventually closed, so caution is warranted. Since I am out of my depth in this discussion, I will defer the decision to you guys and @sergdort.😅
  2. The second part is actually adopting this approach in our codebase and it, by itself, needs a proposal in ios-playbook. Ultimately the whole team needs to understand the consequences of this approach and agree that this is the right way forward for us and our codebase. ❤️

I want to make this very clear: even if this PR is approved and merged by no means it follows that we will adopt in our codebase without proper discussion and agreement. 😄

Copy link

@inamiy inamiy left a comment

Choose a reason for hiding this comment

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

I'm +1 for allowing explicit external inputs (which will solve initial racing issue discussed in #38 ) , but without using different reducers provided by FeedbackInputSignal.

Ideally:

// Use plain `Signal` for external input rather than `FeedbackInputSignal`.
let inputs = Signal.merge(
    nearBottomSignal.map { .startLoadingNextPage },
    // NOTE: { !$0.paging } filtering should be done in existing `reduce`
    
    retrySignal.map { .retry },
)

let system: SignalProducer<State, NoError> = .system(input: inputs,
                                                     initial: State.initial,
                                                     scheduler: UIScheduler(),
                                                     reduce: State.reduce,
                                                     feedbacks: feedbacks)

The reason behind is, separating reducers (State -> Event -> State and State -> Event -> Event?) means we will switch from current Moore state machine model to Mealy (EDIT: #32 (comment)) , which will heavily impact existing state transition diagram (Moore tends to have more clumsy states than Mealy).

If we want to switch to Mealy model, I highly recommend to check out my ReactiveAutomaton and keep this library at Moore-level as is.

@ilyapuchka
Copy link
Contributor

@inamiy what do you mean by separating reducers? If I understand correctly the way input signal would work is that it would map input values to some events which then will still go through the common reducer.

@sergdort
Copy link
Contributor

In the theory of computation, a Moore machine is a finite-state machine whose output values are determined only by its current state. This is in contrast to a Mealy machine, whose (Mealy) output values are determined both by its current state and by the values of its inputs.

According to this I would say we have Mealy state machine

@inamiy
Copy link

inamiy commented Mar 26, 2019

@ilyapuchka

what do you mean by separating reducers? If I understand correctly the way input signal would work is that it would map input values to some events which then will still go through the common reducer.

let inputReducer: (State, ExternalEvent) -> Event? is considered as another (event) reducer along with common (state) reducer.
These are 2 separate reducers sequentially combine-able into one.

Anyway, the definition of reducer is not a problem here, but inputReducer also taking State as an argument makes not equivalent to inputSignal.map { ... } and makes state transition more complex as already mentioned in #32 (comment) , which I referred it as Mealy machine.

@sergdort

According to this I would say we have Mealy state machine

Feedback will be an output type of state-machine in this library, which is essentially let events: (Scheduler, Signal<State, NoError>) -> Signal<Event, NoError>, which can be simplified as State -> IO<Event>.
Since this doesn't have State -> Event -> IO<Event>, I would say this library is Moore machine, and it is a challenging process to accept this PR to switch to Mealy.

@inamiy
Copy link

inamiy commented Mar 27, 2019

I think I was a bit wrong about relating let inputReducer: (State, ExternalEvent) -> Event? with Mealy machine since it is not actually a side-effect output, so please forget about different machine model for now.

My whole point is, let inputReducer: (State, ExternalEvent) -> Event? is too complex (relying on state) so we can simplify into let inputReducer: (ExternalEvent) -> Event?, which can be replaced with ReactiveSwift's filter + map (so we don't need protocol FeedbackInputSignal).

And all the state-filtering logic needs to go into "common reducer" as described in #32 (review) .

@sergdort
Copy link
Contributor

sergdort commented Apr 1, 2019

@inamiy Does this signature State -> Event -> IO<Event> mean that there is a set of event's that does not go via reducer or is it State -> ExternalEvent -> IO<Event>?

@sergdort
Copy link
Contributor

sergdort commented Apr 1, 2019

Isn't let inputReducer: (ExternalEvent) -> Event the same that we are doing ATM with send(action: Action) where Action is just a subset of the even's?

Also, correct me if I'm wrong, I have a feeling that you wan't something to be able to trigger effects, e.g
Signal<ExternalEvent> -> Signal<Event> am I correct?

@RuiAAPeres
Copy link
Contributor

RuiAAPeres commented Apr 10, 2019

@zzcgumn can we reach an outcome for this? Otherwise, I am afraid this needs to be closed.

@zzcgumn
Copy link
Contributor Author

zzcgumn commented Apr 10, 2019

@RuiAAPeres I will have a careful look at what @inamiy'a suggestion tomorrow morning. At a first glance what he suggests makes sense.

@RuiAAPeres
Copy link
Contributor

@zzcgumn awesome! Do please remember that merging this work/approach doesn't mean we will adopt it.

@inamiy
Copy link

inamiy commented Apr 10, 2019

@sergdort #32 (comment)

(Sorry for late response, seems like my prev reply hasn't been successfully sent 😢)

Isn't let inputReducer: (ExternalEvent) -> Event the same that we are doing ATM with send(action: Action) where Action is just a subset of the even's?

Current inputReducer also takes State argument, which is a big difference.

Also, correct me if I'm wrong, I have a feeling that you wan't something to be able to trigger effects, e.g
Signal<ExternalEvent> -> Signal<Event> am I correct?

Yes, so I introduced an example in #41 😉

@sergdort
Copy link
Contributor

sergdort commented Apr 10, 2019

I would prefer not to relax the "everything in reducer" mental model we have.

I'm with @andersio the issue I see here is that #41 introduce a set of events that does nothing to the state essentially reducer ignores them, and they serve as a triggers for side effects.

The signature of Signal<(Event, State)> -> Signal<Event> is effectively a signature of the Middleware in Redux. And in my opinion is a one of the down sides of it, which does not allow to model closed feedback systems (The simplest example of it could be a traffic light), where side effects got triggered only by the state changes.

@inamiy
Copy link

inamiy commented Apr 11, 2019

FYI: To support opinions above, cf. Labeled transition systems https://ncatlab.org/nlab/show/transition+system

Definition. A labelled transition system consists of a transition system T=(S,i,E,Tran) together with a set L of labels, and a function l:E→L. We denote it by (T,L,l).

Let E = UserAction (InputEvent) and L = Event.
Then, l: UserAction -> Event is a simple function that won't contain State as argument.

@zzcgumn
Copy link
Contributor Author

zzcgumn commented Apr 11, 2019

To have a Signal<InputEvent> -> Signal<Event> we need a function InputEvent -> Event. This is the 𝜆 in the transition system article. Why is it an advantage to introduce this function compared to our current practise and nominate a subset of Event as inputs? I can think of three realistic ways to construct the function InputEvent -> Event.

  1. As a pure function. In this case we have made it an explicit requirement to nominate a subset of Event as inputs. In my opinion this is better than relying on naming conventions.
  2. As a feedback system (InputState, InputEvent) which emits Event pretty much in the same way as our view models emit routes.
  3. As a feedback system (State, InputEvent) which again emits Event but shares the state with the main feedback system.

Intuitively, I would expect 1 to be dominating use case. I think we can have situations when 2 makes it easier to build highly dynamic user interfaces.

Scenario 3, we should probably avoid. It is essentially my original proposal. What I was worried about was that it might not be possible to calculate InputEvent -> Event without inspecting the current state. I do, however, believe that will 2 always offer a better solution.

@inamiy Does the above agree with how you are reasoning?

P.S. Am I completely wrong to think of a transition system as a Markov chain with deterministic transitions?

@ilyapuchka
Copy link
Contributor

we need a function InputEvent -> Event

Isn't that effectively Event.ui(InputEvent)? Even though it's modelled as enum case (or in other words a subset of Event), technically it's still a function.

@inamiy
Copy link

inamiy commented Apr 11, 2019

we need a function InputEvent -> Event

Isn't that effectively Event.ui(InputEvent)

@ilyapuchka is right.
Event.ui : InputEvent -> Event is already provided as a pure function, so we can go with Scenario 1 for sure.
For Scenario 2 and 3 (which I heard InputState is almost same as State), adding extra argument requires another reason to proof its logic, and we want to avoid putting complex logic no more than reducer and feedback.

So instead, I would like to propose a new feedback system that depends on both State and Event to achieve this PR's goal of creating (State, Event) -> /* event related something */ .

It will be based on #32 (comment) and #41 , and I will post to ios-playbook later :)

@RuiAAPeres
Copy link
Contributor

Please re-open when you can work on it. 👍

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

Successfully merging this pull request may close these issues.

None yet

6 participants