Hi everyone, I have been playing around with Orbit...
# orbit-mvi
b
Hi everyone, I have been playing around with Orbit-MVI lately and I really like its simplicity. However, after using Mobius for a long time I'm not too sure what the best practices are. So here's my problem. I usually represent my network calls as suspend function returning and Either from the Arrow library. In Mobius, when consuming an Either I would just call
fold()
and dispatch and effect on the left case (error), and a new state on the right case (success), within the same
fold()
Using Orbit-MVI I either have to fold the
apiResponse
twice in the reduce block and the suspend block from the
intent
(option1), or do the
fold()
outside the reduce block but then the fold is happening in the impure world which isn't good practice in my opinion. Which option do you think is best, and is there any alternative to achieve this?
k
Why can't you do it in a single fold in option1? Can't post a side effect in a reducer?
b
postSideEffect
is a suspend function, so no it's not possible. Posting a side effect is an impure thing. Mobius deals with that by having the equivalent of the
reduce
block (the Update function) returning a
Next<State,Effect>
which is just a
State?, Set<Effect>
Another issue I came across is, since the
reduce
block returns Unit, there's no way to know what happened inside it. Sometimes depending on what happened in
reduce
I would want to do a
return@intent
and just stop the computation here
k
If a side effect is impure, then it makes sense that it can't be run in a reducer...?
b
Posting the side effect is impure, but declaring that you want one to be posted isn't, like in Mobius' Next object, which only contains instructions of what needs to happen. Just like the reduce function, updating the state is impure but the reduce function itself simply returns a new state so it is pure. The code inside Orbit-MVI that uses the returned value to update the state is impure, but it happens outside the reduce block. If the reduce function was to return a new state + a list of effects, instead of just a state, it wouldn't make it less pure, but it would allow for better use of pure functions
👍 1
m
@Benoît Orbit it not designed primarily around functional programming principles but rather heavily inspired by the coroutine approach. So here’s how I would do this: • Go with option 2 - fold outside of the
reduce
to keep it simple. • In Option 2 - can you inline the reduce into
success
? What does having it outside give you? We don’t really need to return anything from the fold - looks to me like it’s kind of done for the sake of being “functional” (please take no offense, just commenting on the code as I see it 🙂 ) • after
reduce
runs you can simply get
state
to get the post-reduction state - reduce operations suspend until completed. Do you think it would be better for the
reduce
op to return the result? Not opposed to the idea if it makes things more streamlined. I’m not very familiar with Arrow so let me know if I’m suggesting weird things.
Speaking of alternatives, Kotlin comes with
Result
which is functionally similar to
Either
interested to hear your thoughts on it
b
Hi Mikolaj, thank you for bringing your help. The fact that
reduce
is blocking and executed on a single thread ensures that if 2 events come in at the same time, they will both be taken into account. Acting on the state outside the
reduce
block allows for some edge cases where the state would be modified by 2 different threads at the same time, essentially discarding one of the results. So in order to avoid this type of edge cases, option 1 is actually better, but it forces us to consume the apiResponse twice. And yes you are right,
Either
is basically a
Result
with more capabilities
m
Acting on the state outside the 
reduce
 block allows for some edge cases where the state would be modified by 2 different threads at the same time, essentially discarding one of the results.
The result of one
reduce
would only get discarded if the second one acts on the same part of the state, no?
I mean this is a geniune possibility but highly dependent on how you design your state 🙂 I consider it a rare enough edge case to deal with it as and when necessary, rather than evrywhere
that said - I like the idea of
reduce
returning so I’m happy to make that change if it would make life easier for you
b
In this particular example, indeed option 2 could suffice but assuming there was a
List
in my state, if I want to modify 1 item of the list it would have to happen in a reduce block, as modifying a
List
on an immutable object means copying it. Think of the following scenario: Some thread reads the state outside the reduce block, modifies item at index 1 Some thread reads the state outside the reduce block, modifies item at index 0 Then thread 1 triggers the
reduce
function with its new state, everything is fine. Then thread 2 triggers the
reduce
function, erasing what thread 1 just did
I think that, in general, it would be bad practice to read the state outside a reduce block, but even more bad practice to write the state in a variable outside the reduce block and just do a
reduce { myNewState }
ignoring the current state
m
yeah - genuinely possible. However you should not modify items outside of the
reduce
block
b
indeed, so option2 isn't a valid option
which leaves me with option 1 which isn't super nice to look at 😅
m
option 2 is valid if you inline
reduce
☝️ 1
which is what I’d personally do 🙂
Copy code
apiResponse.fold(
    ...,
    { success -> 
         reduce { state.copy(...) } 
    }
)
b
So you mean something like
Copy code
success->
reduce { state.copy(...) }
👍 2
m
but! Like I said I can make
reduce
return - if this is something that would make things easier for you
if you still want to go ahead with a more functional approach
b
okay 1 sec I need to think 😄😅
😉 1
I can't think of anything wrong with your proposal in terms of execution behaviour, it just sounds like it could be more functional, by having the reduce block returning something. This way you'd just return the effect in the reduce block and post it in the intent block just below. I get your point that Orbit-MVI wasn't designed to be functional, but it just happens to be functional and that's a very good quality. The whole state is stored in 1 single data object, stored outside of your Container which is just the behaviour.
Well the state is stored inside the container but it's transparent when writing logic with it, my Containers are just pure interfaces with no state
m
you mean container hosts
b
yes sorry
still new to this 😅
m
no worries
there’s a third option too: extending the DSL
all of our DSL is built on extension functions so there’s no reason you can’t create one more suited to your purposee
b
Which is what I did actually, it's not implemented in a super clean way I was just playing with the thing, but I've already managed to make the reduce block return :
Copy code
data class ReduceResult<out S : Any, out F : Any>(val state: S?, val effects: Set<F>)
I wasn't super comfortable with taking a library and modifying the core of how it's intended to be used without talking with the community first
m
it’s designed to be extensible 🙂 So feel free to experiment!
b
Is there a reason why
containerContext
is
internal
in
SimpleSyntax
? It is preventing me from writing my own implementation of
reduce{}
m
might be an omission on our part
b
Are there any plans of exposing it?
m
sure, it’s meant to be exposed so people can extend the library
I’ll try to release a new version this/next week.
Sorry for the delay - pretty rammed right now.
a
well, we hadn't expected anyone to extend it so in that sense exposing it didn't make sense where we were marking as much as we can as internal
m
I think the issue is we intended it for it to be extensible - but haven’t tested extending it from the outside world
a
are you not concerned there's something still internal statey about it though? we originally created it so we could implement the complex and simple syntax and kept it when we dropped the complex syntax
m
I mean there’s not much you can do with it other than posting a side effect, calling a reducer and getting the state
so it’s not like you can break anything.
by opening this up at least people will be able to create their own syntax extensions
WDYT @appmattus
a
we have to be cautious, look at something like simplesyntax reduce, we don't allow suspends in the block because, well frankly, thats dangerous and highlights someone potentially doing too much work in the reducer that can block that thread the reduce in containercontext is suspending, so inherently can be abused
b
@appmattus you're right that this would allow one to use a suspend function in the reduce block, but this could be achieve anyways by re-implementing the
intent()
function, with a
transformer
that would be expecting something else than SimpleSyntax So since one could already abuse it, I don't think it matters much
a
with regards to part of the original question, i have some concerns over a reduce function returning a state anyway. from an internal implementation point of view the reduce blocks are designed to run atomically, and really it may mean multiple reduce blocks are queued up at the same time. these reduce calls can run in any order... in essence a return value from reduce is somewhat meaningless as its a snapshot in time that may be out of date. state should only ever be copied inside a reduce call. really what this means is the best you can achieve is something like:
Copy code
fun option3() = intent {
    networkCall().fold(
        { error -> postSideEffect(ShowToast("Error")) },
        { success -> reduce { state.copy(stateString = "Success") } }
    )
}
ultimately there's nothing pure about any of this code,
reduce
is a side effect from a functional point of view as your affecting a different system, as is
postSideEffect
and of course the
networkCall()
take note, that
state
inside the
intent
block is impure and can change across the execution of that block
b
Consuming the Either and outputting the intended behaviour can be considered pure though. That's what I like about Mobius' approach, their equivalent of the
reduce
function ouputs a new state + a set of effects, this way dispatching effects happens in the pure world. Another issue with these 3 options is that they can only be written inside an
intent()
which tightly couple them to the Orbit-MVI framework. With Mobius, the logic can be written outside any MobiusLoop
Yes I saw that,
state
can change in
intent
that's why most of the logic should happen in the reduce block
a
only modifying state should happen in the reduce block otherwise you risk blocking other reductions.... in that sense there shouldn't be any other logic so to speak
(or at least nothing heavy)
b
Modifying the state but also reading it, I've talked about an example above with lists. Ideally you want to put as much logic as possible in the reduce block (as long as it's fast to run indeed)
Copy code
Think of the following scenario:
Some thread reads the state outside the reduce block, modifies item at index 1
Some thread reads the state outside the reduce block, modifies item at index 0
Then thread 1 triggers the reduce function with its new state, everything is fine.
Then thread 2 triggers the reduce function, erasing what thread 1 just did
This shows that even reading the state is best in the reduce block, only side effects should happen outside
And so because most of the logic should run in the reduce block, it would be nice to be able to have insights of what happened in the block
a
yeah it's just tricky if you need to read the state to drive a network call... there's no perfect solution
b
If you need the state to drive a network call, you do it outside the reduce block and that's fine, it's impure stuff. But consuming the network responses itself is pure so if should be able to happen in a pure function like
handleResult( currentState, networkResponse ) : Ouput<State,Set<Effect>>
This function could then be called inside a reduce block but it shouldn't need the reduce block to run
k
Could effects be part of state? I.e. run another reducer that fires effects that are in state and then removes them? Then the reducers would stay pure, I think?
b
You always want effects and state to be separated, otherwise you run into the risk of keeping a side effect across several reductions. That's why Mobius uses a
Next<State,Effect>
approach The state and the effects are separated, but a reduction can trigger the output of an effect. But in the Next itself, states and effects are 2 different
val
👍 1
@appmattus @Mikolaj Leszczynski are you guys planning on opening the
ContainerContext
in the
SimpleSyntax
class (which btw could be an interface) or do you want me to fork the repo? I don't mind doing so, I completely understand that Orbit-MVI wasn't necessarily thought of with functional programming in mind and I thank you for helping me
a
we @Mikolaj Leszczynski will make it public in the
SimpleSyntax
b
Awesome 😄 Something else I've noticed, in
ContainerContext
,
subscribedCounter
is internal, any reason behind this? Anything that could break if it was exposed?
a
i guess technically no reason for it... more getting espresso in a state if increment/decrement are called poorly
b
Would that be okay to expose this one as well? It would allow for custom
repeatOnSubscription
implementations, not that I need it, but from what you said you want Orbit-MVI to be extensible I can put up a PR with these 2 changes if you want
a
yeah, thanks, that'll speed us up a bit
b
Cool I'll do that shortly
👍 1
a
realise this is jumping around a bit... been in meetings most the day so haven't read everything fully...
Copy code
Think of the following scenario:
Some thread reads the state outside the reduce block, modifies item at index 1
Some thread reads the state outside the reduce block, modifies item at index 0
Then thread 1 triggers the reduce function with its new state, everything is fine.
Then thread 2 triggers the reduce function, erasing what thread 1 just did
this type of scenario is why state changing should only happen in reduce and be a copy of it's captured state... thread 1 should only modify index 1 in its reduce function and thread 2 should only modify index 0 in its reduce function. you only modify what should change and you don't rely on the state outside of reduce. of course if both thread 1 and thread 2 may alter the same index or field then you cannot stop this type of issue.
b
No problem, thank you for taking the time to discuss this with me anyways Indeed, this is the reason why state should be copied inside the
reduce
, this is also the reason why I want to be able to do more in
reduce
than simply updating the state. In my experience, the more code you can put in a pure context, the better. One common scenario is, if you have a list of business objects and you want to update one of them, you'd check if it exists in the
reduce
block, if it doesn't then you probably want to stop the full
intent
. For cases like this, you want to know what happened during the reduction.
a
you may find uniflow-kt suits better as they capture the state at the beginning of their block which then blocks until it is complete - downside means two actions coming in at the same time will queue up until the first is complete
b
Thanks for the suggestion, but I'm using Kotlin Multiplatform and this one seems to be Android only
Also I don't like having to rely on event classes, I prefer to use event functions. Mobius uses events as classes and it makes things very verbose when trying to re-use them across multiple features
a
ah yeah forgot you mentioned KMP earlier
m
Thanks for the PR @Benoît - I’ve prepared one to make sure using the context requires opt-in here: https://github.com/orbit-mvi/orbit-mvi/pull/86
b
Brilliant! When do you think the new version will be uploaded to maven?
m
I’ll try to get it released by EOW
👏 2
@Benoît we're close to a release but need to update the docs still. I'm in progress of doing this but have limited spare time so just bear with us 😉 will definitely tag in the next few days if everything goes smoothly.
b
No problem, thanks for the update
412 Views