Question

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 !

Était-ce utile?

La solution

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

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 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)))

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:

// 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.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top