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

Most general form of feedback loops #7

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

eliekarouz
Copy link
Contributor

@eliekarouz eliekarouz commented May 5, 2019

This pull request includes the most general form of feedback loops that was implemented in RxFeedback.swift. TODO:

  • Unit Tests
    This is still a draft as I still need to add the unit tests for the new feedback loops. I am not very familiar with unit testing with TestScheduler. I will be doing some research to understand how it works.

  • Equatable
    In the Swift version of this library, we are forcing the Query to be equatable. There was an overload that takes areEqual: (Query, Query) -> Bool. This was removed from RxFeedback.swift . @kzaher I think this was needed for things like arrays because prior to Swift 4.2 Array<Equatable> was not Equatable. but I think we still need this overload in Kotlin because we need to call special methods for deep comparison of arrays.
    If you agree, i will add an overload to the most general form that takes a (Query, Query) -> Boolean parameter.
    Do you think we need this in Swift?

  • Optional<T>
    Regarding the Optional type. I am not sure but I don't think this is needed. Maybe we can provide a new API without Optional and deprecate the current ones that use Optional. We can in a future version decide to remove them.
    However, we can keep the Optional type in the library as it can be used to wrap Kotlin nullables when we need to build streams with nullable values.

- Implemented initial version of AsyncSynchronized to synchronize state mutation.
- To do `areEqual` parameter is not needed anymore in Swift but we might still want in Kotlin.
This should be discussed further with Zaher and Juraj.
- Changed the other `react` methods to use the feedback loops as a general
@markotron
Copy link
Contributor

markotron commented May 8, 2019 via email

@kzaher
Copy link
Member

kzaher commented May 8, 2019

@eliekarouz yes, that was because of compiler limitations :(.

@kzaher
Copy link
Member

kzaher commented May 8, 2019

@markotron I've refactored the feedback loops in Swift version so that they are all just simplifications of one general form. Seemed elegant to me.

The idea is to have a set of identifiable values controlling the lifetime of observable state machines. When a new value is added to a set, new observable state machine in effects is subscribed to. When it is removed the subscription is disposed/unsubscribed -> effectively killing the state machine inside the observable.

If the value for some key changes that is sent to the state parameter inside (intialQuery: Query, state: Observable<Query>) -> Observable<Event>.

We've renamed Query to Request. People were complaining about the name Query. I think it might make sense to rename to Kotlin version also.

@markotron
Copy link
Contributor

markotron commented May 8, 2019 via email

@kzaher
Copy link
Member

kzaher commented May 9, 2019

observable state machine -> Observable
identifiable values (requests) -> Some value that has an identifier. The value is used to check out should new value be sent. Identifier is used to know where to send it.

@eliekarouz
Copy link
Contributor Author

@markotron

Could you please
add some comments explaining what’s happening? For example, class comments
for the RequestLifetimeTracking and its nested classes.

Sure. Will work on it this weekend...

Copy link
Contributor

@markotron markotron left a comment

Choose a reason for hiding this comment

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

Thanks for the interesting PR! It was fun reviewing it :)

Also, does the AsyncSynchronized concept have a name? Is it used somewere else as well? I'd like to read more about it.

queryLifetime.latestQuery.onNext(query)
} else continue
} else {
val subscription = CompositeDisposable() // SingleAssignmentDisposable
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of using a CompositeDisposable you could have rearranged this so that you add a new QueryID to the lifetimeByIdentifier map after subscribing to the effect, right? In this case, you don't need a ComponsiteDisposable that will actually contain just one Disposable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah you are right... I was not very convinced of what I was writing there...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, does the AsyncSynchronized concept have a name? Is it used somewere else as well? I'd like to read more about it.

Thanks for the feedback... Maybe SerialQueue? I don't think there is a standard definition for for this concept because the mutations can run on any thread vs serial execution queues which is a know concept on iOS. With serial execution queues all mutations are dispatched to the same thread. Not sure if @kzaher has any link to share with us...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@markotron , I think it's called AyncLock

* as well as update already activated effects with the new Query that was returned.
*/
private class QueryLifetimeTracking<Query, QueryID, Event>(
val effects: (intialQuery: Query, state: Observable<Query>) -> Observable<Event>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Now the effects accepts the state: Observable<Query> as a parameter, but in all our current helpers we're just ignoring this parameter:

effects = { initial: Query, _ ->
            effects(initial)
}

What is the use case of this state parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea of the general form is to have some standard way to implement nested systems while still updating the inner systems with the parent substate/state changes.

The react and reactSet can be seen as a projection (subset) of the general form. It would be better to provide the general form but also refactor existing react methods to use the general form in order to reduce the code base...

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I understand why we have a general form. What I meant is that the general form is more general then all our current use cases (actually, my use cases :)). I thought that the implementation in swift was created with a specific use case in mind and wanted to know what that use case is. @kzaher did you use this general form for something that cannot be described without a general form?


private val queue = mutableListOf<Mutate<State>>()

fun async(mutate: (State) -> Unit) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be named async? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@markotron ,
mmm... Do you have any other suggestion? It's called a async because it the mutation to the state can be performed after the async completes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that’s what confused me. It can be performed after the async completes. But in some cases it want. Actually it will last even longer (unbounded) if other threads keep queuing tasks. When you say that something is async, I would assume that it will just queue something and complete.

So I would name this something like queueOrExecuteAll (or something similar).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually it will last even longer (unbounded) if other threads keep queuing tasks. When you say that something is async, I would assume that it will just queue something and complete.

Good point! I will rename it to queueOrExecuteAll. It communicates the intent better.

@markotron
Copy link
Contributor

Btw. I suggest we drop the optional and introduce a breaking change. So that new versions actually have a form without the optional. In my app I use the arrow.Oprional and it’s annoying to have 2 optionals.

Also, I don’t follow the second bullet (equatable comment) in your PR message. In swift you don’t have the (Query, Query) -> Bool method?

One last question. Is Query the same as Request now? If yes, should we rename that?

@eliekarouz
Copy link
Contributor Author

eliekarouz commented May 22, 2019

Btw. I suggest we drop the optional and introduce a breaking change. So that new versions actually have a form without the optional. In my app I use the arrow.Oprional and it’s annoying to have 2 optionals.

I agree and in case anyone doesn't have time to do the migration he can still create a helper method.

Also, I don’t follow the second bullet (equatable comment) in your PR message. In swift you don’t have the (Query, Query) -> Bool method?

It was removed in RxFeedback.swift.

One last question. Is Query the same as Request now? If yes, should we rename that?

I can do this in another pull request. In RxFeedback.swift, they renamed Event to Mutation do you want me to do the same here too?

@markotron
Copy link
Contributor

In RxFeedback.swift, they renamed Event to Mutation do you want me to do the same here too?

So Event --> Mutation and Query --> Request?

How do you define a state machine? You have a enum called State and an enum called Mutation (instead of Event)? What is the standard naming for mutations? Let's say that you have a login state machine and the user enters credentials, would you say that this emitted a CredentialsEntered mutation or CredentialsEntered event?

@kzaher
Copy link
Member

kzaher commented May 24, 2019

@markotron
We've renamed Mutation -> Event again. That was the feedback we got.
Query -> Request was an improvement.

@markotron
Copy link
Contributor

Nice! I was hesitant a bit about that name :) Good!

@markotron
Copy link
Contributor

@eliekarouz — what are the reminding things here? Tests?

}
}

// queries
val State.loadNextPage: Optional<String>
val State.loadNextPage: String?
Copy link
Member

@JurajBegovac JurajBegovac May 28, 2019

Choose a reason for hiding this comment

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

Now, on line 154 we pass null inside a chain because of this change:

// line 154
it.map { it.loadNextPage }.drive { showQuery(it) }

We had optional here because in rxJava you'll get NPE immediately if you pass null through the chain.
https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#nulls

This works here accidentaly because we're using Driver that has try-catch block inside it's map function so it swallows NPE but nulls are not supposed to be used in rxjava2.

That was also the reason why we used Optional for nextPageUrl and lastError (ok we currently don't map a state to these fields so that could remain nullable)

Copy link
Contributor

Choose a reason for hiding this comment

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

@eliekarouz — @JurajBegovac is correct. Although you refactored feedback loops in a way that they don't require Optionals, this guy still needs it.

Instead of removing an optional here, you should convert it on line 162.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@JurajBegovac , @markotron
Yeah you are right. I missed it in the map operator. Actually I didn't test the example yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@JurajBegovac , @markotron , We can transform them to empty strings so we don't have to add the back the Optional. I know it's a bit dirty... A cleaner solution would be to add back the Optional but make it internal to the library or even public if you want.

if (error is Optional.Some) {
Toast.makeText(this, error.data.displayMessage, Toast.LENGTH_SHORT).show()
private fun GithubPaginatedSearchActivity.showOrHideError(error: GitHubServiceError?) {
error?.let {
Copy link
Member

Choose a reason for hiding this comment

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

As I said in comment before - when we reach this place error would never be null because chain will throw NPE before (it cannot pass null) and Driver will swallow it (it looks like it works but it only works because of Driver)

@eliekarouz
Copy link
Contributor Author

@markotron

@eliekarouz — what are the reminding things here? Tests?

Mainly tests but not sure if I will be able to do anything this weekend.

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