Giter VIP home page Giter VIP logo

Comments (5)

sockeqwe avatar sockeqwe commented on August 31, 2024 1

TL;DR: Assuming that we have the same understanding of the definition of Hiearchical State Machines and Parallel State Machines then yes, "composable" refers to supporting Hierarchical & Parallel state machines (read: can be hierarchical or parallel or both, totally up to the developer).


1. My definition of Hierchical State Machine:

Let's say we want to show a list of Items but with Pagination support. Then you could have 1 "root" state machine with states loading first page state, error loading first page state, and show items state, and then you could have a "sub state machine that manages while being in show items state the loading of the next page with substates loading next page state, error loading next page state and show items with latest loaded page state.

This would translate to something like that

class RooStateMachine<State, Action>(nextPageStateMachine : NextPageStateMachine) : FlowReduxStateMachine(initialState = LoadingState) {
   inState<LoadingFirstPageState> {
       onEnter { 
           try {
              val items = loadFirstPageItems()
              OverrideState( ShowItemsState(items) )
       } catch (t : Throwable) {
          OverrideState( ErrorFirstPageState(t) )
       }
   }
   
   inState<ErrorFirstPageState> {
       on<RetryLoadingAction> {
          OverrideState( LoadingFirstPageState() )
       }
   }
   
   // Now instead of having an inState<ShowItemsState> block we  rather hierarchically compose it by splitting this functionality in a "subStateMachine"
   
   subStateMachine(nextPageStateMachine) // register "subStateMachine". It will get all the "SubActions" and emits back "substate" to RooStateMachine
}


// data ShowItemState(val items : List<Item>, nextPageLoadingState : NextPageLoadingState)  with enum class NextPageLoadingState { IDLE, LOADING, ERROR }

class NextPageStateMachine<ShowItemsState, SubActions> : FlowReduxStateMachine(initialState =ShowItemState( ){
  inState <ShowItemsState>(condition = { state : ShowItemsState -> state.nextPageLoadingState == NextPageLoadingState.IDLE}) {
       on<LoadNextPageAction> {
          MutateState { copy( nextPageLoadingState =NextPageLoadingState. LOADING )}
       }
  }
  
  inState <ShowItemsState>(condition = { state : ShowItemsState -> state.nextPageLoadingState == LOADING}) {
      try {
         val nextItems = loadNextPageItems()
         MutateState { copy(items = items + nextItems, nextPageLoadingState = IDLE) }
      catch (t : Throwable) {
         MutateState { copy( nextPageLoadingState = IDLE) }
      }
  }
  
 inState <ShowItemsState>(condition = { state : ShowItemsState -> state.nextPageLoadingState == NextPageLoadingState.ERROR}) {
       delay(3000) // wait 3 seconds, this translates show an error message in UI for 3 seconds
       MutateState {  copy (nextPageLoadingState = IDLE) } 
  }
}

2. For parallel state Machines, imho this is just some special case of hierarchical state machines just with indipendent branches.

Let's say we would like to have some sort of screen that has 2 features in one screen: 1. upload Files and 2. download Files
Then you can split this 2 features into parallel state machines like that:

data class ScreenState (val uploadState : UploadState, val downloadState : DownloadState) 

class RootStateMachine <ScreenState, Actions>(uploadStateMachine: UploadStateMachine, downloadStateMachine : DowloadStateMachine) : FlowReduxStateMachine {
  
  spec { 
      subStateMachine(uploadStateMachine, stateMapper = { screenState, uploadState ->  screenState.copy( uploadState = uploadState ) })
      
      subStateMachine(downloadStateMachine, stateMapper = { screenState, downloadState ->  screenState.copy( downloadState = downloadState ) })


    // You could have some more inState{ ... } blocks here below if you want to ...
  }
}

I was actually wondering what a better name for subStateMachine() could be to make it more explicit. do you think we should stick to the academic terminology of Hierachical and Parallel State Machines?

from flowredux.

abhimuktheeswarar avatar abhimuktheeswarar commented on August 31, 2024 1

We are on the same page on the definition of Hierarchical & Parallel state machines. I'm referring to XState.
subStateMachine - may not be appropirate for defining parallel state machines. Perhaps we could just use the same or generic name like stateMachine() since the state machine is going to be composbale.
You can refer to W3C SCXML specification for state machines.

from flowredux.

sockeqwe avatar sockeqwe commented on August 31, 2024 1

Proposal Number 1: make composition of substatemachines part of inState {...}

Idea

the idea is that you "register" a "sub-statemachine" with:

class ParentStateMachine(childStateMachine : FlowReduxStateMachine) : FlowReduxStateMachine {
  ... 
  inState<FooState> {
  
    stateMachine(
           childStateMachineFactory: (FooState) -> FlowReduxStateMachine<ChildState, ChildAction>(),  // Create Child StateMachine with Initial State
          actionMapper : (ParentStateMachineAction) -> ChildStateMachineAction, 
          stateMapper: (FooState, ChildState) -> ChangeState<ParentState>
     )
  
  }
}

Terminology

Please note that the wording ParentStateMachine and ChildStateMachine is a bit misleading. ChildStateMachine does not need to have Actions and State that are in the same inherence hierarchy (sealed class hierarchy) as the State and Action of the ParentStateMachine. There are actionMapper and stateMapper to "bridge" or "convert" actions and states back and forth. So no common base class or something like that for

Need for this feature? Example use case

Composing StateMachines is a powerful concept in my opinion and mainly addresses 2 issues:

  1. A StateMachine (like any other class, think of it as a "GodStateMachine" in analogy to "GodActivity" 😄 ) could become too big and hard to test and maintain if it just contains too much code. Therefore, splitting functionality into smaller state machines and compose them together is a valid strategy.
  2. Reusability: With composition (favor composition over inheritance) we can build reusable smaller state machines that then can be used to reuse functionality.

Example

A concrete example is combining

  • a traditional screen that loads items and either displays an error or the loaded items on screen (called LceStates in example bellow)

lce-states

  • with pagination

PaginationStates

Of course you can do all in one state machine but making it reusable and composable by splitting it in 2 state machines it could look as following:

sealed interface LceState

object LoadingState : LceState
object ErrorState : LceState
data class ContentState( 
  val currentPage : Int,
  val items : List<Item>,
  val paginationState : PaginationState
) : LceState


enum class PaginationState { 
  LOADING_NEXT_PAGE,
  ERROR_NEXT_PAGE,
  IDLE
}

sealed interface Action
object RetryLoadingAction : Action
object LoadNextPageAction : Action
class LceStateMachine(
    httpClient : HttpClient
) : FlowReduxStateMachine<LceState, Action>(LoadingState){
  init {
      spec {
         inState<LoadingState> {
             onEnter {
                   try {
                       val items =  httpClient.loadFirstPage()
                       OverrideState( ContentState(1, items, PaginationState.IDLE) )
                    } catch (t : Throwable) {
                       OverrideState( ErrorState )
                   }
             }
         }

        inState<ErrorState> { 
             on<RetryLoadingAction> { _ , _ -> OverrideState( LoadingState ) }
        }
        
        inState<ContentState> {
           stateMachine(
           childStateMachineFactory = { contentState -> PaginationStateMachine(initialState = contentState, httpClient = httpClient) }, 
           actionMapper = { it },  
           stateMapper = {  contentState, newPaginationState ->
             OverrideState( newPaginationState )
           } )
        }
      }
  }

}
class PaginationStateMachine<ContentState, Action>(initialState : ContentState, httpClient : HttpClient) : FlowReduxStateMachine(initialState) {
  init {
    spec {
      inStateWithCondition({ it.paginationState == LOADING_NEXT_PAGE) {
          onEnter { stateSnapshot : ContentState ->
              try {
                  val nextPage = stateSnapshot.currentPage + 1
                  val nextItems = httpClient.loadNextPage(nextPage)
                  MutateState { copy( currentPage = nextPage, items = this.items + nextItems, paginationState = IDLE) }
              } catch (t : Throwable) {
                   MutateState { copy( paginationState = ERROR_NEXT_PAGE ) }
              }
          }
      }


      inStateWithCondition({ it.paginationState == ERROR_NEXT_PAGE) {
             onEnter {
                  delay(3000) // display error message in UI for 3 seconds
                  MutateState { copy( paginationState = IDLE )  }
             }
             
             on<LoadNextPageAction> { _ , _ -> MutateState {  copy( paginationState = LOADING_NEXT_PAGE ) }
     }
     
     
      inStateWithCondition({ it.paginationState == LOADING_NEXT_PAGE) {
             on<LoadNextPageAction> { _ , _ -> MutateState {  copy( paginationState = LOADING_NEXT_PAGE ) }
     }
          
    }
  }

}

Design Cosiderations

Additionally, I thought about following the inState<State> semantics that we already have. Thus stateMachine(...) is part of the inState { ... } block and act accordingly by only collecting the state of ChildStateMachine if the surrounding inState block condition is met. Same applies for dispatching actions to ChildStateMachine (will only be done if condition of the surrounding inState block is met).

Some more thoughts and design problems that I have seen while working on this proof of concept:

  • stateMapper will only be invoked if the ChildStatemachine emits a new state. That means stateMapper will not be invoked when the parent's StateMachine changes state. So this is not some sort of combineLatest() operator. That to me seems alright because the overall idea is in my head is that one property of the ParentState is "computed" or "produced" by the state of the child state. Example:

     data class ParentState(
         val property1 : String
         val property2 : String
         val property3 : String
    )
    
    data class ChildState(val i : Int)

    then the mapper function could look something like this:

    fun mapStateOfChild(parentState : ParentState, childState : ChildState) : ParentState { // simplification: in real world the return type is ChangeState<S>
        // this mapperFunction is only called when childState has changed, but not every time parentState changes
        return parentState.copy(property2 = "computed state from childstatemachine with value = ${childState.i}"
    }
  • Since stateMachine(childStateMachineFactory , ...) is surrounded by an inState {...} block I believe we need a factory that would instantiate a fresh instance of ChildStateMachine whenever we enter the inState block and release the old ChildStateMachine instance whenever we leave the inState block. The issue I see is that the initial state of child state machine could come from the parent (not in all cases it is the case that childstatemachine's initial state depends on parent state machine, but see Pagination example).

    • But how would that work with dependency injection?
    • What about state flow? I guess here is a good argument to make FlowReduxStateMachines "cold" again and remove StateFlow.

Any thoughts, feedback or comments are very much appreciated

from flowredux.

sockeqwe avatar sockeqwe commented on August 31, 2024 1

🎉 done.

#198

from flowredux.

abhimuktheeswarar avatar abhimuktheeswarar commented on August 31, 2024

Does "composable" refer to defining Hierarchical & Parallel state machines?

from flowredux.

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.