Chaining PartialFunctions execute side effects 2 times in FSM

According to the andThen method definition in PartialFunction trait in scala 12.13.6:


  /**  Composes this partial function with a transformation function that
   *   gets applied to results of this partial function.
   *
   *   If the runtime type of the function is a `PartialFunction` then the
   *   other `andThen` method is used (note its cautions).
   *
   *   @param  k  the transformation function
   *   @tparam C  the result type of the transformation function.
   *   @return a partial function with the domain of this partial function,
   *           possibly narrowed by the specified function, which maps
   *           arguments `x` to `k(this(x))`.
   */
  override def andThen[C](k: B => C): PartialFunction[A, C] = k match {
    case pf: PartialFunction[B, C] => andThen(pf)
    case _                         => new AndThen[A, B, C](this, k)
  }

  /**
   * Composes this partial function with another partial function that
   * gets applied to results of this partial function.
   *
   * Note that calling [[isDefinedAt]] on the resulting partial function may apply the first
   * partial function and execute its side effect. It is highly recommended to call [[applyOrElse]]
   * instead of [[isDefinedAt]] / [[apply]] for efficiency.
   *
   * @param  k  the transformation function
   * @tparam C  the result type of the transformation function.
   * @return a partial function with the domain of this partial function narrowed by
   *         other partial function, which maps arguments `x` to `k(this(x))`.
   */
  def andThen[C](k: PartialFunction[B, C]): PartialFunction[A, C] =
    new Combined[A, B, C](this, k)

That seems something new as in scala 2.12.14 we have one overload method for andThen:

  /**  Composes this partial function with a transformation function that
   *   gets applied to results of this partial function.
   *   @param  k  the transformation function
   *   @tparam C  the result type of the transformation function.
   *   @return a partial function with the same domain as this partial function, which maps
   *           arguments `x` to `k(this(x))`.
   */
  override def andThen[C](k: B => C): PartialFunction[A, C] =
    new AndThen[A, B, C] (this, k)

And in FSM.scala we have this method:

private[akka] def processEvent(event: Event, @unused source: AnyRef): Unit = {
    val stateFunc = stateFunctions(currentState.stateName)
    val nextState = if (stateFunc.isDefinedAt(event)) {
      stateFunc(event)
    } else {
      // handleEventDefault ensures that this is always defined
      handleEvent(event)
    }
    applyState(nextState)
  }

That checks if stateFunc is defined at event then run it in stateFunc(event) line

Also this is the definition of FSM transform method:

 final class TransformHelper(func: StateFunction) {
    def using(andThen: PartialFunction[State, State]): StateFunction =
      func.andThen(andThen.orElse { case x => x })
  }

  final def transform(func: StateFunction): TransformHelper = new TransformHelper(func)

that chains 2 partialFunctions.

So if we have a FSM actor with this definition:

def sideEffect(): Unit
def updateDB() : Unit

 when(customStateName)(transform {
    case Event(CustomEvent, CustomStateData) =>
      sideEffect()
      stay() 
  } using {
    case FSM.State(_, NewSateData, _, _, _)  =>
       updateDB()
      goto(nextState)
  }))

So if this FSM actor receives CustomEvent:

  • sideEffect would be called 2 times
  • updateDB would be called 1 times

Is it right?

We have the same logic in our application and seems was working well with scala 2.12.14 (both sideEffect and updateDB called 1 time), but after upgrading to scala 2.13.6 we see this new behaviour

1 Like

For the record now tracked by issue: FSM `transform...using` pattern double executes `transform` piece · Issue #30855 · akka/akka · GitHub