I am trying to make a generic way of building <table>
from a List of objects in Play Framework.
I wanted to create a class ColumnInfo
representing columns metadatas :
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.
Let's say I have a model User
, extending an other class (or trait, whatever) 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>
:
@(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 :
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]
!
That's normal. But if I make the type T
in ColumnInfo
covariant, it tells me that :
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.
Thanks a lot for your help !
The problem is that a ColumnInfo[User]
is not a 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.
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).
You could therefore define ColumnInfo
like:
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]])
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]])
. However, Play templates do not currently support type parameters .
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 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.
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:
// In ListController...
Ok(views.html.list(users, columns.asInstanceOf[List[ColumnInfo[Any]]]))
This will compile, as Scala uses type erasure. And provided list
is actually a List[User]
, the types will be correct at run-time.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.