在 Scala 中,从字符串到 case class 的映射类型以及从字符串到以这些 case classes 作为输入参数的函数的映射应该是什么类型?

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?

我尝试建模的场景如下。我有几个 case 类,它们的参数不同,但它们都扩展了特征 Entity

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

我有一组函数接受一个参数,该参数是 Entity 的子类型,如下所示(我们的函数比实体多):

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

现在,我得到一个 Entity 序列化为字符串的实例,在它旁边我得到调用它的函数的名称。反序列化不是问题:假设我有一个函数 read[T](str) 可以将 str 反序列化为 T.

类型的对象

我想写一段通用的 Scala 代码,给定这两个字符串(函数名,序列化实体)可以在反序列化实体后调用正确的函数。

我想,我需要像下面这样的映射,给定一个函数名就会给我函数本身,以及它的参数类型。那么我原则上应该可以很容易地拨打如下电话。

// 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. 这是行不通的,因为我无法获得正确的类型(当然推断类型也行不通)。

  2. 有没有办法把map1map2放在一起(这样就不会弄乱函数和参数类型之间的关系)?


编辑:为了简单起见,我们可以在这里忽略实体的参数,因此忽略实际的序列化实体。这应该有助于编写可编译的代码而无需太多工作。


编辑:用例:我正在编写一个从 RabbitMQ 接收消息的程序。消息正文包含实体,消息密钥暗示如何处理它。

为简单起见,我假设 none 的函数 return 是一个值,因此每个函数的类型都是 something => Unit。此外,我不打算考虑任何异常处理,所以我希望映射始终 return 兼容值。

设置(如问题中所述):

trait Entity

case class E1() extends Entity

case class E2() extends Entity

case class En() extends Entity

def f1(p: E1) = println(25)

def f2(p: En) = println("foo")

def fm(p: E2) = println(p.toString)

我们还有一个函数 read: String => Entity return 是 Entity 的某个子类型,以及一个存储我们函数的存储(实现为 Map):

def read[T](s: String) = s match {
  case "E1" => E1()
  case "E2" => E2()
  case "En" => En()
}

val functionMap = Map[String, Nothing => Unit](
  "f1" -> f1 _,
  "f2" -> f2 _,
  "fm" -> fm _
)

functionMap 的值类型是从 NothingUnit 的函数的原因是,为了从映射中调用任意函数,参数必须是所有参数类型的子类型。但是,E1E2 的唯一子类型是 Nothing。这也是类型推断对您没有帮助的原因:静态地没有满足映射中任意函数的类型。

一个解决方案:

如果您想坚持地图,没有任何解决方案可以为您提供静态安全。但是,如果您知道自己在做什么(或在运行时处理异常),则可以通过执行运行时转换来解决问题。首先,使用 read 方法反序列化 Entity。然后,通过在函数映射中查找 fName 来解析要调用的方法。如果您确定函数和参数匹配,那么您可以强制转换此函数并让编译器满意:

def makeCall(fName: String, ent: String) = {
  val param = read(ent)
  functionMap.get(fName).get.asInstanceOf[param.type => Unit](param)
}

编辑 2:使用与我之前的编辑类似的功能,您可以使用 String => Unit 功能创建一个地图,该功能结合了反序列化和您的功能,因此您不需要不需要两张地图。

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:看起来您可能有多个具有相同输入类型的函数,这使得它不是类型 class.[=33 的好用例=]

你可以使用类似的东西:

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

它将您的字符串反序列化为传递函数的输入类型。
您可以将其用作 makeTheCall(f2, "serializedE4").


即使您可以找到使 makeTheCall 方法正常工作的正确类型,您也不应使用字符串来区分多种类型。如果您在 fname 中输入错误,map1 包含 fnamemap2 不包含怎么办,...?

从你的问题中并不清楚你到底想做什么,但 class 类型似乎很适合你的情况。使用类型 class,您可以为类型创建一个具有特定功能的实例,它可以执行您想要对 f1f2、... 函数执行的操作。

假设您的 f1f2、...函数全部 return 和 Int,我们可以创建一个类型 class,其中包含这样的Entity 类型的函数:

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

让我们创建一些 classes 扩展 Entity :

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

现在我们可以为 FooBar 创建类型 class 的实例:

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
}

我们可以使用我们的类型 class 如下:

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

在你的情况下,这可能看起来像:

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

// makeTheCall[Baz]("serializedBaz")