[英]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
添加到ColumnInfo
来ColumnInfo
,例如[+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). 实际上,
ColumnInfo
在T
似乎是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 我可以看到两种解决方法
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. 我们现在必须改变
list
来cols.list
和columns
到cols.columns
在我们的模板匹配。
We can call our template like: 我们可以这样称呼我们的模板:
// In ListController...
Ok(views.html.list(TableData(users, columns)))
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: 并在实际调用时将
columns
为List[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.