Search code examples
scalaakkafsm

Passing extractors dynamically for pattern matching


I want to be able to dynamically choose which extractors to use in my case class pattern matching.

I want something like:

def handleProcessingResult(extract: SomeType) : PartialFunction[Event, State] = {
    case Event(someEvent: SomeEvent, extract(handlers)) =>
        ...

    case Event(otherEvent: OtherEvent, extract(handlers)) =>
        ...
}

The idea is that I can have the above partial function, and can then use it anywhere where I know how to write an unapply to match and extract handlers from some pattern matched type.

If you are wondering why I want these partial functions, it is so that I can compose partial functions of common behaviour together to form the handlers for my states in an Akka FSM. This is not required to understand the question, but for example:

when(ProcessingState) {
    handleProcessingResult(extractHandlersFromProcessing) orElse {
        case Event(Created(path), Processing(handlers)) =>
            ...
    }
}

when(SuspendedState) {
   handleProcessingResult(extractHandlersFromSuspended) orElse {
       case Event(Created(path), Suspended(waiting, Processing(handlers))) =>
           ...
}

It seems like this should be possible but I can't figure out how!

I have tried the following two simplifications:

object TestingUnapply {

  sealed trait Thing
  case class ThingA(a: String) extends Thing
  case class ThingB(b: String, thingA: ThingA) extends Thing

  val x = ThingA("hello")
  val y = ThingB("goodbye", ThingA("maybe"))

  process(x, new { def unapply(thing: ThingA) = ThingA.unapply(thing)})
  process(y, new { def unapply(thing: ThingB) = ThingB.unapply(thing).map(_._2.a)})


  def process(thing: Thing, extract: { def unapply[T <: Thing](thing: T): Option[String]}) = thing match {
    case extract(a) => s"The value of a is: $a"
  }
}

The idea being that I should be able to pass any sub-type of Thing and a suitable extractor to process. However, it doesn't compile due to:

[error] /tmp/proj1/TestUnapply.scala:10: type mismatch;
[error]  found   : AnyRef{def unapply(thing: TestingUnapply.ThingA): Option[String]}
[error]  required: AnyRef{def unapply[T <: TestingUnapply.Thing](thing: T): Option[String]}
[error]   process(x, new { def unapply(thing: ThingA) = ThingA.unapply(thing)})
[error]              ^
[error] /tmp/proj1/TestUnapply.scala:11: type mismatch;
[error]  found   : AnyRef{def unapply(thing: TestingUnapply.ThingB): Option[String]}
[error]  required: AnyRef{def unapply[T <: TestingUnapply.Thing](thing: T): Option[String]}
[error]   process(y, new { def unapply(thing: ThingB) = ThingB.unapply(thing).map(_._2.a)})
[error]              ^

Subsequently, moving the declaration of type parameter T onto process, gives us:

import scala.reflect.ClassTag

object TestingUnapply {

  sealed trait Thing
  case class ThingA(a: String) extends Thing
  case class ThingB(b: String, thingA: ThingA) extends Thing

  val x = ThingA("hello")
  val y = ThingB("goodbye", ThingA("maybe"))

  process(x, new { def unapply(thing: ThingA) = ThingA.unapply(thing)})
  process(y, new { def unapply(thing: ThingB) = ThingB.unapply(thing).map(_._2.a)})

  def process[T <: Thing: ClassTag](thing: Thing, extract: { def unapply(thing: T): Option[String]}) = thing match {
    case extract(a) => s"The value of a is: $a"
  }
}

Now gives us a different compilation error of:

[error] /tmp/TestUnapply.scala:18: Parameter type in structural refinement may not refer to an abstract type defined outside that refinement
[error]   def process[T <: Thing: ClassTag](thing: Thing, extract: { def unapply(thing: T): Option[String]}) = thing match {

I am most likely doing something daft. Can someone help me please?


Solution

  • Try to make an workaround based on your first simplification, hope it helps.

     object DynamicPattern extends App {
    
        sealed trait Thing
        case class ThingA(a: String) extends Thing
        case class ThingB(b: String, thingA: ThingA) extends Thing
    
       // change structural type to an abstract class
       abstract class UniversalExtractor[T <: Thing] {
         def unapply(thing: T): Option[String]
       }
    
       // extract is an instance of UniversalExtractor with unapply method
       // naturally it's an extractor
       def process[T <: Thing](thing: T, extract: UniversalExtractor[T]) = 
         thing match {
           case extract(a) => s"The value of a is: $a"
         }
    
       val x = ThingA("hello")
       val y = ThingB("goodbye", ThingA("maybe"))
    
       val result1 = process(
         x,
         new UniversalExtractor[ThingA] {
            def unapply(thing: ThingA) = ThingA.unapply(thing)
         }
       )
    
       val result2 = process(y,
         new UniversalExtractor[ThingB] {
            def unapply(thing: ThingB) =   ThingB.unapply(thing).map(_._2.a)
         }
       )
    
       // result1 The value of a is: hello
       println(" result1 " + result1)
       // result2 The value of a is: maybe
       println(" result2 " + result2)
    }
    

    Update

    A probably "nasty" method without using an abstract class or trait hinted by the type conformance problem I explained later.

      // when invoking process method, we actually know which subtype of 
      // Thing is pattern-matched, plus the type conformance problem, 
      // so here comes the ```unapply[T <: Thing](thing: T)``` 
      // and ```asInstanceOf(which is usually not that appealing)```. 
      val thingAExtractor = new { 
        def unapply[T <: Thing](thing: T): Option[String] = 
          ThingA.unapply(thing.asInstanceOf[ThingA])
      }
    
      val thingBExtractor = new {
        def unapply[T <: Thing](thing: T): Option[String] =
          ThingB.unapply(thing.asInstanceOf[ThingB]).map(_._2.a)
      }
    
      // hello
      println(process(x, thingAExtractor))
      // maybe
      println(process(y, thingBExtractor))
    

    The reason it doesn't work in the two simplifications(actually the nasty method just pops up in my mind when I try to figure out the reason, so just write it here in case it helps).

    For the first simplication: it's about the type conformance problem.

      type ExtractType = { def unapply[T <: Thing](thing: T): Option[String] }
      val anonExtractor = new { def unapply(thing: ThingA) = ThingA.unapply(thing) }
    
      import scala.reflect.runtime.{ universe => ru }
      import scala.reflect.runtime.universe.{ TypeTag, typeTag }
      def getTypeTag[T: TypeTag](o: T) = typeTag[T].tpe
      def getTypeTag[T: TypeTag] = ru.typeOf[T]
    
      // false | so in turn type check fails at compilation time
      println(getTypeTag(anonExtractor) <:< getTypeTag[ExtractType])
    

    ScalaDoc Reflection's Runtime Classes in Java vs. Runtime Types in Scala part demonstrates the type conformance in similar case. In short, Scala compiler creates synthetic classes that are used at runtime in place of user-defined classes to be translated to equivalent Java Bytecode in cases mentioned in that part.

    For the second simplication: this post parameter-type-in-structural-refinement-may-not-refer-to-an-abstract-type-define-outside has given some detailed explanation.