Search code examples
scalashapeless

Using Shapeless records to combine arbitrary number of state-changing functions


I'm trying to port combineReducers from Redux to Scala. The idea is that each function controls it's small part of the state and combineReducers creates a function that controls the whole state. I can't figure out required signature for the function that should work something like this:

sealed trait Event

case class Create() extends Event
case class Save() extends Event

object Reducers {
  def combineReducers[E, S1, S2](fTag: String, f: (E, S1) => S1, gTag: String, g: (E, S2) => S2) = ???

  type IntState = Int
  type StringState = String

  def intReducer(e: Event, state: IntState): IntState = state + 1

  def stringReducer(e: Event, state: StringState): StringState = state + e.toString


  val reducer = combineReducers("count", intReducer, "names", stringReducer)

  val initialState = ("count" ->> 0 ) :: ("names" ->> "") :: HNil

  val newState = reducer(Create(), initialState) // returns ("count" ->> 1) :: ("names" ->> "Create()") :: HNil
  reducer(Save(), newState) // returns ("count" ->> 2) :: ("names" ->> "Create()Save()") :: HNil

}

Solution

  • Try

    import shapeless.labelled.{FieldType, field}
    import shapeless.{::, HNil}
    import shapeless.syntax.singleton._
    
    sealed trait Event
    
    case class Create() extends Event
    case class Save() extends Event
    
    object Reducers {
      type Reducer[E, S] = (E, S) => S
    
      def combineReducers[K1 <: String, E, S1, K2 <: String, S2](
                                                                  fTag: K1,
                                                                  f: Reducer[E, S1],
                                                                  gTag: K2,
                                                                  g: Reducer[E, S2]
                                                                ): Reducer[E, FieldType[K1, S1] :: FieldType[K2, S2] :: HNil] =
        { case (e, v1 :: v2 :: HNil) => field[K1](f(e, v1)) :: field[K2](g(e, v2)) :: HNil }
    
      type IntState = Int
      type StringState = String
    
      def intReducer(e: Event, state: IntState): IntState = state + 1
    
      def stringReducer(e: Event, state: StringState): StringState = state + e.toString
    
      val reducer: Reducer[Event, FieldType[Witness.`"count"`.T, IntState] :: FieldType[Witness.`"names"`.T, StringState] :: HNil] =
        combineReducers("count".narrow, intReducer, "names".narrow, stringReducer)
    
      val initialState = ("count" ->> 0 ) :: ("names" ->> "") :: HNil
    
      val newState = reducer(Create(), initialState) // returns ("count" ->> 1) :: ("names" ->> "Create()") :: HNil
      val newState1 = reducer(Save(), newState) // returns ("count" ->> 2) :: ("names" ->> "Create()Save()") :: HNil
    }
    

    We should annotate reducer with explicit type because compiler cant't infer E in combineReducers. If you change signature to

    def combineReducers[K1 <: String, S1, K2 <: String, S2](
                                                             fTag: K1,
                                                             f: Reducer[Event, S1],
                                                             gTag: K2,
                                                             g: Reducer[Event, S2]
                                                           ): Reducer[Event, FieldType[K1, S1] :: FieldType[K2, S2] :: HNil] = ...
    

    then you can write just

    val reducer = combineReducers("count".narrow, intReducer, "names".narrow, stringReducer)
    

    For combining arbitrary number of reducers you can create type class

    trait CombineReducers[Keys <: HList, Reducers <: HList] {
      type Record <: HList
      def apply(keys: Keys, reducers: Reducers): Reducer[Event, Record]
    }
    object CombineReducers {
      type Aux[Keys <: HList, Reducers <: HList, Record0 <: HList] = CombineReducers[Keys, Reducers] { type Record = Record0 }
      def instance[Keys <: HList, Reducers <: HList, Record0 <: HList](f: (Keys, Reducers) => Reducer[Event, Record0]): Aux[Keys, Reducers, Record0] = new CombineReducers[Keys, Reducers] {
        type Record = Record0
        override def apply(keys: Keys, reducers: Reducers): Reducer[Event, Record0] = f(keys, reducers)
      }
    
      implicit val hnilCombineReducers: Aux[HNil, HNil, HNil] = instance { case (HNil, HNil) => { case (_, HNil) => HNil }}
      implicit def hconsCombineReducers[K <: String, Ks <: HList, S, Rs <: HList](implicit
        cr: CombineReducers[Ks, Rs]): Aux[K :: Ks, Reducer[Event, S] :: Rs, FieldType[K, S] :: cr.Record] = instance {
        case (k :: ks, r :: rs) => {
          case (e, s :: ss) => field[K](r(e, s)) :: cr(ks, rs)(e, ss)
        }
      }
    }
    

    and use it

    def combineReducers[Keys <: HList, Reducers <: HList](keys: Keys, reducers: Reducers)(implicit
      cr: CombineReducers[Keys, Reducers]): Reducer[Event, cr.Record] = cr(keys, reducers)
    
    val reducer =
      combineReducers("count".narrow :: "names".narrow :: HNil, (intReducer: (Event, IntState) => IntState) :: (stringReducer : (Event, StringState) => StringState) :: HNil)