-
Notifications
You must be signed in to change notification settings - Fork 13
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
Proposal to improve naming and reduce concepts #228
Comments
I guess I'll need to read the book to fully embrace this concept 😅 These renames look reasonable to me for the most parts. I'm a bit concerned about |
So far (I'm only halfway), the book covers basic functional concepts which I'm pretty familiar with (likely you are as well), but it does teach them in a well-structured way. Once I finish the book, I can let you know if it's worth reading in full. Regarding the
From a more general point of view, |
Hopefully I am not too late for this discussion, but here are my 2 cents having recently attempted to integrate coroutines into Formula (#233) I really liked how I definitely support the introduction of I believe I see different types of actions:
This paradigm seems to collide with the Couldn't all side effect only fun (currentState: S) : suspend (S) -> S {
val sideEffect: (suspend (S) -> S) = {accumulatedState: S -> /*call api;*/ accumulatedState.copy() }
return sideEffect
} A closer look at
Let's look at a simple example: class CounterFormula : Formula<Unit, Int, CounterRenderModel> {
override fun initialState(input: Unit): Int = 0
override fun evaluate(
input: Unit,
state: Int,
context: FormulaContext<Int>
): Evaluation<CounterRenderModel> {
return Evaluation(
output = CounterRenderModel(
count = "Count: $state",
onDecrement = context.callback {
transition(state - 1)
},
onIncrement = context.callback {
transition(state + 1)
}
)
)
}
} would become: class CounterFormula : Formula<Unit, State, CounterRenderModel> {
override fun initialState(input: Unit): State = State(count: 0)
override fun model(
state: State
): CounterRenderModel {
return CounterRenderModel(
count = "Count: $state.count",
onDecrement = {
dispatch(DECREMENT)
},
onIncrement = {
dispatch(INCREMENT)
}
)
}
} Where object INCREMENT : SyncAction<State> {
override fun stateGenerator() = {accumulatedState: State -> accumulatedState.copy(count: accumulatedState.count + 1)}
}
object DECREMENT : SyncAction<State> {
override fun stateGenerator() = {accumulatedState: State -> accumulatedState.copy(count: accumulatedState.count - 1)}
} Creating different types of It seems that the input parameter should only be used for initial state creation and is not directly needed by the model.
How can all of this be implemented?I took this code from here and changed it a bit to better fit what I described above. I have not updated it in a while hence the use of old classes/mechanisms (i.e. It is a pure kotlin implementation of what I outlined above, but the same can be accomplished with Rx. @FlowPreview
@ExperimentalCoroutinesApi
class StateProvider<S>(
private val initialState: S,
private val syncDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
private val asyncActions by lazy { ConflatedBroadcastChannel<AsyncAction<S>>() }
private val syncActions by lazy { ConflatedBroadcastChannel<SyncAction<S>>() }
private val streamActions by lazy { ConflatedBroadcastChannel<StreamAction<S>>() }
private val state by lazy { ConflatedBroadcastChannel(initialState) }
private lateinit var errorAction: (cause: Throwable) -> (suspend (S) -> S)
private val operation: suspend ((S, suspend (S) -> S) -> S) = { acc, value ->
val newState = value(acc)
check((acc !== newState)) { "State should be immutable. Make sure you call copy()" }
newState
}
fun error(action: (cause: Throwable) -> (suspend (S) -> S)) {
errorAction = action
}
private val asyncState by lazy {
asyncActions
.asFlow()
.map {
it.fire(state.value)
}
.catch { t ->
emit(errorAction(t))
}
.flowOn(ioDispatcher)
}
private val streamState by lazy {
streamActions
.asFlow()
.flatMapConcat {
it.fire(state.value) //naïve implementation
}
.catch { t ->
emit(errorAction(t))
}
.flowOn(ioDispatcher)
}
private val syncState by lazy {
syncActions
.asFlow()
.map { it.fire() }
.catch { t ->
emit(errorAction(t))
}
.flowOn(syncDispatcher)
}
val combinedState by lazy {
flowOf(syncState, asyncState, streamState)
.flattenMerge()
.scan(initialState, operation)
.map {
state.send(it);
it
}.flowOn(syncDispatcher)
}
suspend fun dispatch(action: Action<S>) {
when (action) {
is AsyncAction -> asyncActions.send(action)
is SyncAction -> syncActions.send(action)
is StreamAction -> streamActions.send(action)
}
}
}
interface Action<S>
interface SyncAction<S> : Action<S> {
fun fire(): suspend (S) -> S
}
interface AsyncAction<S> : Action<S> {
fun fire(currentState: S): suspend (S) -> S
}
interface StreamAction<S> : Action<S> {
suspend fun fire(currentState: S): Flow<suspend (S) -> S>
} Then By no means I am trying to force this implementation or none of the terms above, I just found it really interesting that maybe there is some alignment in how we think about managing state. |
I'm still in the phase where I don't have strong opinions on this yet. That said, do these re-names make it easier to grok what is happening or abstract things to a level where someone needs to learn another concept to understand the framework. To me an action is easy to confuse with a calculation from hearing the words. I could see someone confusing the two. I'm not sure what a more descriptive alternative would be. Calculations feel like logic to me. But I'm struggling for a better word than just plain actions. |
I definitely agree, but at some point there will be a learning curve where new/foreign concepts will need to be assimilated. Using I am not so much interested in the name difference as I am with how moving away from
|
Thanks for a really thoughtful comment. I generally think that some of this is out of scope for this issue as it would require huge potentially non-incremental changes to the Formula core API.
The API of
This brings a good point that In
This is a big change and I'm not immediately sure this paradigm would cover all use-cases. Will need to think more about it.
This is a huge change that currently doesn't handle
It's unlikely we can get rid of the
Input is used frequently during formula composition. |
Currently, there is a plethora of frameworks and libraries for state management on Android. While it's very different from some, conceptually,
It also borrows some thinking from the non-Android world such as
It's highly unlikely that we can move away from these mechanisms. |
Thanks a lot for the response. I definitely understand that there are a lot of things I do not fully understand about Formula and my suggestions may not apply.
I meant to say here that I like how the word
I originally named the method
I definitely avoided child formulas, did not spend enough time to understand them. I though that a disposable action was: fun (currentState: S) : suspend (S) -> S {
val sideEffect: (suspend (S) -> S) = {accumulatedState: S -> /*call api;*/ accumulatedState.copy() }
return sideEffect
} Again, I might not be understanding all the concepts fully. I understand that a lot of my comments would require huge changes and I do not expect Formula to change to adopt them. The purpose of the comment was to trigger some conversation, and it did. In any case, thanks a lot for creating Formula! |
What
To improve naming and create more cohesiveness, I'm thinking to consolidate around 3 concepts: data, calculations, and actions.
The concepts are borrowed from Grokking Simplicity
Data
Inert objects such as Input, State, Output, RenderModel, State, Evaluation.
Calculations
Place where decision-making lives. It takes inputs and produces an output (no actions/side-effects happen within this). An example would be
initialState
,evaluate
, or event callbacks.Action
Action is a function that contains side effects such as
Proposal
As the first step, I'm thinking to rename some types to fit the mental model above.
Effects
Action
Stream
DisposableAction
RxStream
RxDisposableAction
Update
BoundedAction
Evaluation.updates
Evaluation.actions
Transition.OnlyEffects
Transition.OnlyAction
The text was updated successfully, but these errors were encountered: