简体   繁体   English

Scala-泛型,协变类型和函数作为参数

[英]Scala - Generics, covariant type and function as parameter

I am trying to make a generic way of building <table> from a List of objects in Play Framework. 我正在尝试从Play框架中的对象列表中构建一种<table>的通用方法。

I wanted to create a class ColumnInfo representing columns metadatas : 我想创建一个表示列元数据的ColumnInfo类:

case class ColumnInfo[T](name: String, value: T => Any)  

The name field represents ... well, the name of the column, and the function value should take an object in parameter, and return a value for that column. name字段表示……好吧,列的名称,函数value应在参数中包含一个对象,并为该列返回一个值。

Let's say I have a model User , extending an other class (or trait, whatever) Bean : 假设我有一个User模型,扩展了另一个类(或特性,无论如何) Bean

case class User(name: String, age: Int) extends Bean  

I then create a Play Framework template name list.scala.html that takes a List[Bean] and a List[Column[Bean]] as parameters, and displays the corresponding <table> : 然后,我创建一个Play Framework模板名称list.scala.html ,它使用List[Bean]List[Column[Bean]]作为参数,并显示相应的<table>

@(list: List[Bean], columns: List[ColumnInfo[Bean]])

<table>
    <thead>
        <tr>
        @for(c <- columns) {
            <th>@c.name</th>
        }
        </tr>
    </thead>
    <tbody>
    @for(obj <- list) {
        <tr>
        @for(c <- columns) {
            <td>@c.value(obj)</td>
        }
        </tr>
    }
    </tbody>
</table>

In my controller's Action, I should have something like this : 在控制器的Action中,我应该具有以下内容:

object ListController extends Controller {

  def list = Action {
    val users = List(
        User("foo", 20),
        User("bar", 30)
    )

    val columns = List(
        ColumnInfo[User]("Name", _.name),
        ColumnInfo[User]("Age", _.age)
    )

    Ok(views.html.list(users, columns)
  }
}

The problem is that I can't put a ColumnInfo[User] in a List of ColumnInfo[Bean] ! 问题是我无法将ColumnInfo[User]放在ColumnInfo[Bean]的列表中!

That's normal. 那很正常 But if I make the type T in ColumnInfo covariant, it tells me that : 但是,如果我使ColumnInfo的类型T为协变量,它会告诉我:

case class ColumnInfo[+T](name: String, value: T => Any)

covariant type T occurs in contravariant position in type => (T) => Any of value value

Logic. 逻辑。 But what can I do then ? 但是我该怎么办? I also tried with lower bounds, by adding an other type U to ColumnInfo , like [+T, U >: T] , but it only brought me other errors. 我还尝试通过将其他类型U添加到ColumnInfoColumnInfo ,例如[+T, U >: T] ,但这只给我带来了其他错误。

Thanks a lot for your help ! 非常感谢你的帮助 !

The problem is that a ColumnInfo[User] is not a ColumnInfo[Bean] . 问题在于ColumnInfo[User]不是ColumnInfo[Bean] For example, if you have 例如,如果您有

val myInfo = ColumnInfo[User]("MyCol", user => user.name)
val myBean = new Bean
myInfo.value(myBean)

There's no way this can possibly work, since myBean has no name method (even if we could force it to compile, it would fail at run-time), so the compiler catches this and throws it out. 这是不可能的,因为myBean没有name方法(即使我们可以强制对其进行编译,但它在运行时会失败),因此编译器将其捕获并抛出。

In fact, ColumnInfo appears to be contravariant in T (anything that goes into a function is contravariant, for the reasons demonstrated in the example - in some languages, they actually use the keyword in for contravariance, to make this clear). 实际上, ColumnInfoT似乎是ColumnInfo变的(出于示例中的原因,函数中的任何内容都是ColumnInfo变的-在某些语言中,他们实际上使用关键字in是协变的,这一点很清楚)。

You could therefore define ColumnInfo like: 因此,您可以将ColumnInfo定义ColumnInfo

case class ColumnInfo[-T](name: String, value: T => Any)

Unfortunately, this limits re-use of your template, as its signature has to be @(list: List[User], columns: List[ColumnInfo[User]]) 不幸的是,这限制了模板的重复使用,因为其签名必须为@(list: List[User], columns: List[ColumnInfo[User]])

In an ideal world, templates would support type parameters, like regular Scala methods, so you could have a signature like @[T](list: List[T], columns: List[ColumnInfo[T]]) . 在理想情况下,模板将支持类型参数,例如常规的Scala方法,因此您可以使用@[T](list: List[T], columns: List[ColumnInfo[T]]) However, Play templates do not currently support type parameters . 但是, 播放模板当前不支持type参数

I can see two ways around this 我可以看到两种解决方法

Existential Types 存在类型

We can hack around it with existential types. 我们可以通过存在性类型来解决它。 We'll wrap up our arguments to the template into an invariant case class: 我们将模板的参数包装到不变的case类中:

case class TableData[T](list: List[T], columns: List[ColumnInfo[T]])

and change the signature of the template to: 并将模板的签名更改为:

@(cols: TableData[T forSome {type T}])

We now have to change list to cols.list and columns to cols.columns in our template to match up. 我们现在必须改变listcols.listcolumnscols.columns在我们的模板匹配。

We can call our template like: 我们可以这样称呼我们的模板:

// In ListController...
Ok(views.html.list(TableData(users, columns)))

Casts 演员表

Alternatively, we can cast around the problem. 或者,我们可以解决这个问题。 Give your template a signature of: 给您的模板签名:

@(list: List[Any], columns: List[ColumnInfo[Any]])

and cast columns to List[ColumnInfo[Any]] when you actually call it: 并在实际调用时将columnsList[ColumnInfo[Any]]

// In ListController...
Ok(views.html.list(users, columns.asInstanceOf[List[ColumnInfo[Any]]]))

This will compile, as Scala uses type erasure. 这将进行编译,因为Scala使用类型擦除。 And provided list is actually a List[User] , the types will be correct at run-time. 并且提供的list实际上是List[User] ,类型在运行时将是正确的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM