Search code examples
scalatypesdeserializationcovariancecontravariance

In Scala, what should be the type of a map from string to case class, and a map from string to functions taking those case classes as input parameter?


The scenario I am trying to model is as follows. I have a couple of case classes that differ in their parameters, but they all extend the trait Entity.

// case classes 
trait Entity
case class E1(..params..) extends Entity
case class E2(..params..) extends Entity
...
case class En(..params..) extends Entity

I have a set of functions that take one parameter which is a subtype of Entity, like the following (we have more functions than entities):

// functions using case classes as parameters
def f1(val p:E1) = ???
def f2(val p:E4) = ???
...
def fm(val p:E2) = ??? 

Now, I get an instance of an Entity serialized into a String, and next to it I get the name of the function to call on it. To deserialize is not a problem: let's say I have a function read[T](str) that can deserialize str into an object of type T.

I want to write a generic piece of Scala code, that given these two Strings (function name, serialized entity) can call the right function after deserializing the entity.

I thought, I would need maps like below that given a function name will give me the function itself, and the type of its parameter. Then I should, in principle, easily be able to make a call as below.

// the mappings from String to entity and corresponding function
val map1 = Map (
    "f1" -> f1
    "f2" -> f2
    ...
    "fn" -> fn
  ) 
}
val map2 = Map (
    "f1" -> E1
    "f2" -> E4
    ...
    "fn" -> E2
  ) 
}

def makeTheCall (fname: String, ent: String) = map1.get(fname)(read[map2.get(fname)](ent))
  1. This does not work because I cannot get the types right (and definitely the inferred types do not work either).

  2. Is there a way to put map1 and map2 together (so that there is less chance of messing up the relations between the functions and parameter types)?


EDIT: For the sake of simplicity, we can here ignore the parameters to the Entities and therefore the actual serialized entity. This should help write a compilable code without too much work.


EDIT: Use-case: I am writing a program that receives messages from RabbitMQ. The body of the message contains the entity, and the message key implies what to do with it.


Solution

  • Edit 2: Using a similar function as my previous edit, you could create a map with String => Unit functions which combine the deserialization and your function, so you don't need two maps.

    def deserializeAnd[E <: Entity](f: E => Unit): String => Unit = 
      (s: String) => f(read[E](s))
    
    val behaviour = Map(
      "key1" -> deserializeAnd(println(_: Foo)),
      "key2" -> deserializeAnd(println(_: Bar)),
      "key3" -> deserializeAnd((foo: Foo) => println(foo.copy(a=0))
    )
    
    def processMessage(key: String, serialized: String): Option[Unit] = 
      behaviour.get(key).map(f => f(serialized))
    
    // throws an exception if 'behaviour' doesn't contain the key
    def processMessage2(key: String, serialized: String): Unit =
      behaviour(key)(serialized)
    

    Edit: It seems you have potentially multiple functions with the same input type, which makes it not a good use case for a type class.

    You could use something like :

    def makeTheCall[E <: Entity, Out](f: E => Out, s: String): Out = f(read[E](s))
    

    It deserializes your string to the input type of the passed function.
    Which you could use as makeTheCall(f2, "serializedE4").


    Even if you could find the right types to get your makeTheCall method working, you shouldn't use strings to differentiate between multiple types. What if you make a typo in fname, map1 contains fname and map2 doesn't, ... ?

    It is not really clear from your question what you want to do exactly, but it seems a type class would be a good fit for your case. With a type class you can create an instance with specific functionality for a type, which could do something like you want to do with your f1, f2, ... functions.

    Imagine that your f1, f2, ... functions all return an Int, we could create a type class which contains such a function for Entity types :

    trait EntityOperation[E <: Entity] {
      def func(e: E): Int 
    }
    

    Lets create some case classes which extend Entity :

    trait Entity
    case class Foo(a: Int, b: Int) extends Entity
    case class Bar(c: String, d: String) extends Entity
    

    Now We can create an instance of our type class for Foo and Bar :

    implicit val FooEntityOp = new EntityOperation[Foo] {
      def func(foo: Foo) : Int = foo.a + foo.b
    }
    
    implicit val BarEntityOp = new EntityOperation[Bar] {
      def func(bar: Bar) : Int = bar.c.length + bar.d.length
    }
    

    We could use our type class as follows :

    def callF[E <: Entity](e: E)(implicit op: EntityOperation[E]) = op.func(e)
    
    callF(Foo(1, 2))          // Int = 3
    callF(Bar("xx", "yyyy"))  // Int = 6
    

    In your case this could look like :

    def makeTheCall[E <: Entity](s: String)(implicit op: EntityOperation[E]) = 
      op.func(read[E](s))
    
    // makeTheCall[Baz]("serializedBaz")