我最近在SO上发布了几个问题,涉及Scala traits,representation types,member types,manifests和implicit evidence。这些问题背后是我为生物蛋白质网络构建建模软件的项目。尽管有非常有用的答案,这让我比我自己更接近,但我仍然没有找到我的项目的解决方案。有几个答案表明我的设计存在缺陷,这就是为什么Foo
框架问题的解决方案在实践中不起作用的原因。在这里,我发布了一个更复杂(但仍然大大简化)的问题版本。我希望问题和解决方案对于试图在Scala中构建复杂的特征和类层次结构的人来说将是广泛有用的。
我项目中的最高级别是生物反应规则。规则描述了如何通过反应转化一种或两种反应物。每个反应物是具有称为单体和边缘的节点的图,所述节点连接在单体上的命名位点之间。每个站点也都有一个可以处于的状态。编辑:边缘的概念已从示例代码中删除,因为它们使示例复杂化而没有对问题做出太多贡献。规则可能会说些什么像这样:由单体A制成的一种反应物分别通过位点a1和b1与单体B结合;债券被规则打破,使得网站a1和b1不受约束;同时在单体A上,位置a1的状态从U变为P.我将其写为:
A(a1~U-1).B(b1-1) -> A(a1~P) + B(b1)
(在Scala中解析这样的字符串非常容易,这让我头晕目眩。)-1
表示债券#1在这些网站之间 - 这个数字只是一个任意标签。
这是我到目前为止的原因以及我添加每个组件的原因。它编译,但只是无偿使用asInstanceOf
。如何摆脱asInstanceOf
以使类型匹配?
我用基本类代表规则:
case class Rule(
reactants: Seq[ReactantGraph], // The starting monomers and edges
producedMonomers: Seq[ProducedMonomer] // Only new monomers go here
) {
// Example method that shows different monomers being combined and down-cast
def combineIntoOneGraph: Graph = {
val all_monomers = reactants.flatMap(_.monomers) ++ producedMonomers
GraphClass(all_monomers)
}
}
图GraphClass
的类具有类型参数,因为我可以对特定图形中允许的单体和边缘类型进行约束;例如,ProducedMonomer
的{{1}}中不能有Reactant
个Rule
。我还希望能够collect
特定类型的所有Monomer
,例如ReactantMonomer
。我使用类型别名来管理约束。
case class GraphClass[
+MonomerType <: Monomer
](
monomers: Seq[MonomerType]
) {
// Methods that demonstrate the need for a manifest on MonomerClass
def justTheProductMonomers: Seq[ProductMonomer] = {
monomers.collect{
case x if isProductMonomer(x) => x.asInstanceOf[ProductMonomer]
}
}
def isProductMonomer(monomer: Monomer): Boolean = (
monomer.manifest <:< manifest[ProductStateSite]
)
}
// The most generic Graph
type Graph = GraphClass[Monomer]
// Anything allowed in a reactant
type ReactantGraph = GraphClass[ReactantMonomer]
// Anything allowed in a product, which I sometimes extract from a Rule
type ProductGraph = GraphClass[ProductMonomer]
单体MonomerClass
的类也有类型参数,因此我可以在网站上设置约束;例如,ConsumedMonomer
不能拥有StaticStateSite
。此外,我需要collect
特定类型的所有单体,例如,收集产品中规则中的所有单体,因此我为每个类型参数添加Manifest
。
case class MonomerClass[
+StateSiteType <: StateSite : Manifest
](
stateSites: Seq[StateSiteType]
) {
type MyType = MonomerClass[StateSiteType]
def manifest = implicitly[Manifest[_ <: StateSiteType]]
// Method that demonstrates the need for implicit evidence
// This is where it gets bad
def replaceSiteWithIntersection[A >: StateSiteType <: ReactantStateSite](
thisSite: A, // This is a member of this.stateSites
monomer: ReactantMonomer
)(
// Only the sites on ReactantMonomers have the Observed property
implicit evidence: MyType <:< ReactantMonomer
): MyType = {
val new_this = evidence(this) // implicit evidence usually needs some help
monomer.stateSites.find(_.name == thisSite.name) match {
case Some(otherSite) =>
val newSites = stateSites map {
case `thisSite` => (
thisSite.asInstanceOf[StateSiteType with ReactantStateSite]
.createIntersection(otherSite).asInstanceOf[StateSiteType]
)
case other => other
}
copy(stateSites = newSites)
case None => this
}
}
}
type Monomer = MonomerClass[StateSite]
type ReactantMonomer = MonomerClass[ReactantStateSite]
type ProductMonomer = MonomerClass[ProductStateSite]
type ConsumedMonomer = MonomerClass[ConsumedStateSite]
type ProducedMonomer = MonomerClass[ProducedStateSite]
type StaticMonomer = MonomerClass[StaticStateSite]
我StateSite
的当前实现没有类型参数;它是一个标准的特征层次结构,终止于具有名称的类和一些代表适当状态的String
。 (熟悉使用字符串来保存对象状态;它们实际上是我真实代码中的名称类。)这些特性的一个重要目的是提供所有子类所需的功能。那么,并不是所有特质的目的。我的特征是特殊的,因为许多方法对特征的所有子类共有的对象属性进行小的更改,然后返回一个副本。如果返回类型与对象的基础类型匹配将是优选的。执行此操作的蹩脚方法是使所有特征方法都抽象化,并将所需方法复制到所有子类中。我不确定正确的Scala方法。一些来源建议存储基础类型的成员类型MyType
(如此处所示)。其他来源建议使用表示类型参数。
trait StateSite {
type MyType <: StateSite
def name: String
}
trait ReactantStateSite extends StateSite {
type MyType <: ReactantStateSite
def observed: Seq[String]
def stateCopy(observed: Seq[String]): MyType
def createIntersection(otherSite: ReactantStateSite): MyType = {
val newStates = observed.intersect(otherSite.observed)
stateCopy(newStates)
}
}
trait ProductStateSite extends StateSite
trait ConservedStateSite extends ReactantStateSite with ProductStateSite
case class ConsumedStateSite(name: String, consumed: Seq[String])
extends ReactantStateSite {
type MyType = ConsumedStateSite
def observed = consumed
def stateCopy(observed: Seq[String]) = copy(consumed = observed)
}
case class ProducedStateSite(name: String, Produced: String)
extends ProductStateSite
case class ChangedStateSite(
name: String,
consumed: Seq[String],
Produced: String
)
extends ConservedStateSite {
type MyType = ChangedStateSite
def observed = consumed
def stateCopy(observed: Seq[String]) = copy(consumed = observed)
}
case class StaticStateSite(name: String, static: Seq[String])
extends ConservedStateSite {
type MyType = StaticStateSite
def observed = static
def stateCopy(observed: Seq[String]) = copy(static = observed)
}
我最大的问题是像MonomerClass.replaceSiteWithIntersection
这样的方法。许多方法对类的特定成员进行一些复杂的搜索,然后将这些成员传递给其中对其进行复杂更改的其他函数并返回一个副本,然后将副本替换为更高级别对象的副本中的原始副本。我应该如何参数化方法(或类)以使调用类型安全?现在我可以让代码只在很多asInstanceOf
处编译。由于我可以看到两个主要原因,Scala特别不满意传递类型或成员参数的实例:(1)协变类型参数最终作为任何将它们作为输入的方法的输入,以及(2)它是难以让Scala相信一个返回副本的方法确实会返回一个与放入的类型完全相同的对象。
毫无疑问,我遗漏了一些人人都不清楚的事情。如果我需要添加任何细节,或者我需要删除多余的细节,我会尽快清理。
修改
@ 0__用没有replaceSiteWithIntersection
编译的方法替换了asInstanceOf
。不幸的是,我无法在没有类型错误的情况下找到调用方法的方法。他的代码基本上是MonomerClass
这个新类中的第一个方法;我添加了第二个调用它的方法。
case class MonomerClass[+StateSiteType <: StateSite/* : Manifest*/](
stateSites: Seq[StateSiteType]) {
type MyType = MonomerClass[StateSiteType]
//def manifest = implicitly[Manifest[_ <: StateSiteType]]
def replaceSiteWithIntersection[A <: ReactantStateSite { type MyType = A }]
(thisSite: A, otherMonomer: ReactantMonomer)
(implicit ev: this.type <:< MonomerClass[A])
: MonomerClass[A] = {
val new_this = ev(this)
otherMonomer.stateSites.find(_.name == thisSite.name) match {
case Some(otherSite) =>
val newSites = new_this.stateSites map {
case `thisSite` => thisSite.createIntersection(otherSite)
case other => other
}
copy(stateSites = newSites)
case None => new_this // This throws an exception in the real program
}
}
// Example method that calls the previous method
def replaceSomeSiteOnThisOtherMonomer(otherMonomer: ReactantMonomer)
(implicit ev: MyType <:< ReactantMonomer): MyType = {
// Find a state that is a current member of this.stateSites
// Obviously, a more sophisticated means of selection is actually used
val thisSite = ev(this).stateSites(0)
// I can't get this to compile even with asInstanceOf
replaceSiteWithIntersection(thisSite, otherMonomer)
}
}
答案 0 :(得分:6)
我已经将你的问题减少到了特征,我开始明白为什么你会遇到有关强制转换和抽象类型的麻烦。
您实际缺少的是ad-hoc多态,您可以通过以下方式获得: - 使用通用签名编写一个方法,依赖于相同泛型的隐式来委托工作 - 使隐式仅对该泛型参数的特定值可用,当您尝试执行非法操作时,这将变成“隐式未找到”编译时错误。
现在让我们按顺序查看问题。首先是你的方法的签名是错误的,原因有两个:
替换要创建新泛型类型的新单体的网站时,就像在向集合添加对象时所做的那样,该对象是现有泛型类型的超类:您将获得一个新集合其类型参数是超类。你应该得到这个新的单体。
您不确定该操作是否会产生结果(如果您无法真正替换状态)。在这种情况下,正确的类型是Option [T]
def replaceSiteWithIntersection[A >: StateSiteType <: ReactantStateSite]
(thisSite: A, monomer: ReactantMonomer): Option[MonomerClass[A]]
如果我们现在看一下类型错误的挖掘者,我们可以看到真正的类型错误来自这个方法:
thisSite.createIntersection
原因很简单:它的签名与其他类型不一致,因为它接受一个ReactantSite,但是你想把它作为参数之一传递给你的stateSites(类型为Seq [StateSiteType])但是你无法保证
StateSiteType<:<ReactantSite
现在让我们看看证据如何帮助你:
trait Intersector[T] {
def apply(observed: Seq[String]): T
}
trait StateSite {
def name: String
}
trait ReactantStateSite extends StateSite {
def observed: Seq[String]
def createIntersection[A](otherSite: ReactantStateSite)(implicit intersector: Intersector[A]): A = {
val newStates = observed.intersect(otherSite.observed)
intersector(newStates)
}
}
import Monomers._
trait MonomerClass[+StateSiteType <: StateSite] {
val stateSites: Seq[StateSiteType]
def replaceSiteWithIntersection[A >: StateSiteType <: ReactantStateSite](thisSite: A, otherMonomer: ReactantMonomer)(implicit intersector:Intersector[A], ev: StateSiteType <:< ReactantStateSite): Option[MonomerClass[A]] = {
def replaceOrKeep(condition: (StateSiteType) => Boolean)(f: (StateSiteType) => A)(implicit ev: StateSiteType<:<A): Seq[A] = {
stateSites.map {
site => if (condition(site)) f(site) else site
}
}
val reactantSiteToIntersect:Option[ReactantStateSite] = otherMonomer.stateSites.find(_.name == thisSite.name)
reactantSiteToIntersect.map {
siteToReplace =>
val newSites = replaceOrKeep {_ == thisSite } { item => thisSite.createIntersection( ev(item) ) }
MonomerClass(newSites)
}
}
}
object MonomerClass {
def apply[A <: StateSite](sites:Seq[A]):MonomerClass[A] = new MonomerClass[A] {
val stateSites = sites
}
}
object Monomers{
type Monomer = MonomerClass[StateSite]
type ReactantMonomer = MonomerClass[ReactantStateSite]
type ProductMonomer = MonomerClass[ProductStateSite]
type ProducedMonomer = MonomerClass[ProducedStateSite]
}
请注意,如果以巧妙的方式使用隐式解析规则,则可以使用此模式而不使用特殊导入(例如,将您的insector放在Intersector trait的伴随对象中,以便它将自动解析)。
虽然此模式运行良好,但您的解决方案仅适用于特定的StateSiteType这一事实存在限制。 Scala集合解决了添加另一个隐式的类似问题,即调用CanBuildFrom。在我们的例子中,我们将其称为CanReact
你必须使你的MonomerClass不变,这可能是一个问题(但为什么你需要协方差?)
trait CanReact[A, B] {
implicit val intersector: Intersector[B]
def react(a: A, b: B): B
def reactFunction(b:B) : A=>B = react(_:A,b)
}
object CanReact {
implicit def CanReactWithReactantSite[A<:ReactantStateSite](implicit inters: Intersector[A]): CanReact[ReactantStateSite,A] = {
new CanReact[ReactantStateSite,A] {
val intersector = inters
def react(a: ReactantStateSite, b: A) = a.createIntersection(b)
}
}
}
trait MonomerClass[StateSiteType <: StateSite] {
val stateSites: Seq[StateSiteType]
def replaceSiteWithIntersection[A >: StateSiteType <: ReactantStateSite](thisSite: A, otherMonomer: ReactantMonomer)(implicit canReact:CanReact[StateSiteType,A]): Option[MonomerClass[A]] = {
def replaceOrKeep(condition: (StateSiteType) => Boolean)(f: (StateSiteType) => A)(implicit ev: StateSiteType<:<A): Seq[A] = {
stateSites.map {
site => if (condition(site)) f(site) else site
}
}
val reactantSiteToIntersect:Option[ReactantStateSite] = otherMonomer.stateSites.find(_.name == thisSite.name)
reactantSiteToIntersect.map {
siteToReplace =>
val newSites = replaceOrKeep {_ == thisSite } { canReact.reactFunction(thisSite)}
MonomerClass(newSites)
}
}
}
通过这样的实现,每当您想要用另一个不同类型的站点替换站点时,您只需要使用不同类型的CanReact的新隐式实例。
我将以(我希望)明确解释为什么你不需要协方差来结束。
假设您有Consumer[T]
和Producer[T]
。
如果您希望提供Consumer[T1]
Producer[T2]
T2<:<T1
,则需要协方差。但是如果你需要在T1中使用T2产生的值,你可以
class ConsumerOfStuff[T <: CanBeContained] {
def doWith(stuff: Stuff[T]) = stuff.t.writeSomething
}
trait CanBeContained {
def writeSomething: Unit
}
class A extends CanBeContained {
def writeSomething = println("hello")
}
class B extends A {
override def writeSomething = println("goodbye")
}
class Stuff[T <: CanBeContained](val t: T)
object VarianceTest {
val stuff1 = new Stuff(new A)
val stuff2 = new Stuff(new B)
val consumerOfStuff = new ConsumerOfStuff[A]
consumerOfStuff.doWith(stuff2)
}
这些东西显然没有编译:
错误:类型不匹配;发现:需要东西[B]:东西[A]注:B&lt;: A,但类Stuff在类型T中是不变的。您可能希望将T定义为 + T而不是。 (SLS 4.5)consumerOfStuff.doWith(stuff2)。
但同样,这来自对方差使用的误解,正如How are co- and contra-variance used in designing business applications? Kris Nuttycombe回答的解释。如果我们重构如下
class ConsumerOfStuff[T <: CanBeContained] {
def doWith[A<:T](stuff: Stuff[A]) = stuff.t.writeSomething
}
你可以看到一切编译得很好。
答案 1 :(得分:2)
不是答案,但我可以通过查看问题来观察:
MonomerClass
但未Monomer
我的胆量说你应该尽可能避免清单,因为你已经看到他们可以让事情变得复杂。我认为你不需要它们。例如justTheProductMonomers
中的GraphClass
方法 - 由于您可以完全控制类层次结构,为什么不直接将{em> runtime 检查的任何测试方法添加到Monomer
? E.g。
trait Monomer {
def productOption: Option[ProductMonomer]
}
然后你会
def justTheProductMonomers : Seq[ProductMonomer] = monomers.flatMap( _.productOption )
等等。
这里的问题是,你似乎可以拥有满足产品谓词的通用单体,而你却想要子类型ProductMonomer
。
我要给出的一般建议是首先定义处理规则所需的测试矩阵,然后将这些测试作为方法放入特定的特征中,除非你有一个扁平的层次结构您可以为此进行模式匹配,这更容易,因为消歧将集中在您的使用网站上,而不会分散在所有实施类型中。
另外,不要尝试使用编译时类型约束来过期它。通常,在运行时检查一些约束是完全正常的。这样至少可以构建一个完全正常工作的系统,然后你可以尝试找到可以将运行时检查转换为编译时检查的点,并确定是否值得。在Scala中解决类型级别的事情是有吸引力的,因为它的复杂性,但它也需要最多的技能才能正确完成。
答案 2 :(得分:1)
有很多问题。首先,整个方法很奇怪:一方面你传入monomer
参数,如果找到参数thisState
,该方法与接收器无关 - 那为什么这是一个 MonomerClass
中的方法而不是“自由浮动”功能 - 另一方面,如果this
是,则回归thisSite
找不到。由于您最初也有implicit evidence: MyType <:< ReactantMonomer
,我的猜测是整个monomer
参数已过时,而您实际上想要对new_this
进行操作。
有点清理,暂时忘记了清单,你可以
case class MonomerClass[+StateSiteType <: StateSite, +EdgeSiteType <: EdgeSite](
stateSites: Seq[StateSiteType], edgeSites: Seq[EdgeSiteType]) {
def replaceSiteWithIntersection[A <: ReactantStateSite { type MyType = A }]
(thisSite: A)(implicit ev: this.type <:< MonomerClass[A, ReactantEdgeSite])
: MonomerClass[A, ReactantEdgeSite] = {
val monomer = ev(this)
monomer.stateSites.find(_.name == thisSite.name) match {
case Some(otherSite) =>
val newSites = monomer.stateSites map {
case `thisSite` => thisSite.createIntersection(otherSite)
case other => other
}
monomer.copy(stateSites = newSites)
case None => monomer
}
}
}
这是一个有趣的问题,它花了我一些迭代来摆脱(错!)演员。现在它实际上非常易读:此方法仅限于StateSiteType
实际上是A
的子类型ReactantStateSite
的证据。因此,类型参数A <: ReactantStateSite { type MyType = A }
- 最后一点很有趣,这是我自己的新发现:您可以在此处指定类型成员,以确保createIntersection
的返回类型实际为{{ 1}}。
你的方法还有一些奇怪的地方,因为如果我没弄错的话,你最终会调用A
(与x.createIntersection(x)
相交,这是一个无操作的方式。)
答案 3 :(得分:0)
关于replaceSiteWithIntersection
存在缺陷的一点是,根据方法签名,thisSite
(A
)的类型是{strong>超类型 { {1}}和StateSiteType
的子类型。
然后你最终把它投到ReactantStateSite
。这对我没有意义。
从StateSiteType with ReactantStateSite
A
突然出现StateSiteType