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

时间:2014-04-16 12:44:32

标签: scala playframework playframework-2.0

我正在尝试从Play框架中的对象列表中构建<table>的通用方法。

我想创建一个代表metadatas列的类ColumnInfo

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

name字段代表......嗯,列的名称,函数value应该在参数中获取一个对象,并返回该列的值。

假设我有一个模型User,扩展了另一个类(或特征,无论如何)Bean

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

然后我创建一个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>

在我的控制器的动作中,我应该有这样的事情:

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

问题是我无法将ColumnInfo[User]放入ColumnInfo[Bean]的列表中!

这是正常的。但是,如果我在T协变中创建ColumnInfo类型,它会告诉我:

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

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

逻辑。但那我该怎么办?我也尝试使用下限,向U添加其他类型ColumnInfo,例如[+T, U >: T],但它只给我带来了其他错误。

非常感谢你的帮助!

1 个答案:

答案 0 :(得分:2)

问题是ColumnInfo[User]不是ColumnInfo[Bean]。例如,如果你有

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

由于myBean没有name方法(即使我们可以强制它进行编译,它会在运行时失败),因此无法工作,所以编译器捕获并将其抛出。

事实上,ColumnInfo似乎在T中是逆变的(任何进入函数的东西都是逆变的,由于示例中演示的原因 - 在某些语言中,它们实际上使用了关键字{{ 1}}对于逆变,要明确这一点。

因此,您可以定义in,如:

ColumnInfo

不幸的是,这限制了模板的重复使用,因为其签名必须是case class ColumnInfo[-T](name: String, value: T => Any)

在理想的世界中,模板将支持类型参数,例如常规的Scala方法,因此您可以拥有类似@(list: List[User], columns: List[ColumnInfo[User]])的签名。但是,Play templates do not currently support type parameters

我可以通过两种方式看到这个

存在类型

我们可以用存在类型来破解它。我们将模板的参数包装成一个不变的case类:

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

并将模板的签名更改为:

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

我们现在必须在模板中将@(cols: TableData[T forSome {type T}]) 更改为list并将cols.list更改为columns以匹配。

我们可以调用我们的模板:

cols.columns

施放

或者,我们可以解决问题。为您的模板添加以下签名:

// In ListController...
Ok(views.html.list(TableData(users, columns)))
实际调用时,

并将@(list: List[Any], columns: List[ColumnInfo[Any]]) 投射到columns

List[ColumnInfo[Any]]

这将编译,因为Scala使用类型擦除。如果// In ListController... Ok(views.html.list(users, columns.asInstanceOf[List[ColumnInfo[Any]]])) 实际上是list,那么这些类型在运行时就是正确的。