Scala:构建特征和类的复杂层次结构

drh*_*gen 10 scala

我最近在SO上发布了几个关于Scala 特征,表示类型,成员类型,清单隐式证据的问题.这些问题背后是我为生物蛋白质网络构建建模软件的项目.尽管有非常有用的答案,这让我比我自己更接近,但我仍然没有找到我的项目的解决方案.有几个答案表明我的设计存在缺陷,这就是为什么解决Foo问题的方法在实践中不起作用的原因.在这里,我发布了一个更复杂(但仍然大大简化)的问题版本.我希望问题和解决方案对于尝试在Scala中构建复杂的特征和类层次结构的人来说将是广泛有用的.

我项目中的最高级别是生物反应规则.规则描述了如何通过反应转化一种或两种反应物.每个反应物是具有称为单体和边缘的节点的图,所述节点连接在单体上的命名位点之间.每个站点也都有一个可以处于的状态.编辑:边缘的概念已从示例代码中删除,因为它们使示例复杂化而不会对问题做出太多贡献.规则可能是这样的:有一种由单体A制成的反应物分别通过位点a1和b1与单体B结合; 债券被规则打破,使得网站a1和b1不受约束; 同时在单体A上,站点a1的状态从U变为P.我将其写为:

A(a1~U-1).B(b1-1) -> A(a1~P) + B(b1)
Run Code Online (Sandbox Code Playgroud)

(在Scala中解析这样的字符串非常简单,它使我的头旋转.)-1表示债券#1在这些网站之间 - 数字只是一个任意标签.

这是我到目前为止的原因以及为什么我添加每个组件的原因.它编译,但只有无偿使用asInstanceOf.如何摆脱asInstanceOfs以使类型匹配?

我用基本类代表规则:

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)
  }
}
Run Code Online (Sandbox Code Playgroud)

图的类GraphClass具有类型参数,因为我可以对特定图中允许的单体和边的类型进行约束; 例如,不能存在任何ProducedMonomerS IN的Reactant的一个Rule.我也希望能够使用特定类型的collect所有Monomers,比如ReactantMonomers.我使用类型别名来管理约束.

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]
Run Code Online (Sandbox Code Playgroud)

单体类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]
Run Code Online (Sandbox Code Playgroud)

我当前的实现StateSite没有类型参数; 它是一个标准的特征层次结构,终止于具有名称的类和一些String代表适当状态的s.(熟悉使用字符串来保存对象状态;它们实际上是我真实代码中的名称类.)这些特性的一个重要目的是提供所有子类所需的功能.好吧,这不是所有特质的目的.我的特征是特殊的,因为许多方法对特征的所有子类共有的对象属性进行小的更改,然后返回一个副本.如果返回类型与对象的基础类型匹配将是优选的.执行此操作的蹩脚方法是使所有特征方法都抽象化,并将所需方法复制到所有子类中.我不确定正确的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)
}
Run Code Online (Sandbox Code Playgroud)

我最大的问题是方法框架如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)
  }
}
Run Code Online (Sandbox Code Playgroud)

Edm*_*984 6

我已经将你的问题减少到了特征,我开始明白为什么你会遇到有关强制转换和抽象类型的麻烦.

您实际缺少的是ad-hoc多态,您可以通过以下方式获得: - 使用通用签名编写一个方法,依赖于相同泛型的隐式来委托工作 - 使隐式只对该泛型的特定值可用参数,当您尝试执行非法操作时,将变为"隐式未找到"编译时错误.

现在让我们按顺序查看问题.首先,您的方法的签名是错误的,原因有两个:

  • 当替换要创建新泛型类型的新单元的站点时,就像在向集合添加一个对象(现有泛型类型的超类)时一样:您将获得一个新类型,其类型参数是超类.结果你应该产生这个新的单体.

  • 您不确定该操作是否会产生结果(如果您无法真正替换状态).在这种情况下正确的类型它的选项[T]

    def replaceSiteWithIntersection[A >: StateSiteType <: ReactantStateSite]
    (thisSite: A, monomer: ReactantMonomer): Option[MonomerClass[A]] 
    
    Run Code Online (Sandbox Code Playgroud)

如果我们现在看一下类型错误的挖掘者,我们可以看到真正的类型错误来自这个方法:

 thisSite.createIntersection
Run Code Online (Sandbox Code Playgroud)

原因很简单:它的签名与其他类型不一致,因为它接受一个ReactantSite,但你想将它作为参数之一传递给你的stateSites(类型为Seq [StateSiteType]),但你无法保证那

StateSiteType<:<ReactantSite
Run Code Online (Sandbox Code Playgroud)

现在让我们看看证据如何帮助您:

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]

}
Run Code Online (Sandbox Code Playgroud)
  1. 请注意,如果以巧妙的方式使用隐式解析规则,则可以使用此模式而不使用特殊导入(例如,将您的insector放在Intersector trait的伴随对象中,以便它将自动解析).

  2. 虽然此模式运行良好,但是您的解决方案仅适用于特定的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)
             }


    }


  }
Run Code Online (Sandbox Code Playgroud)

有了这样的实现,每当您想要用另一个不同类型的站点替换站点时,您所需要的只是使用不同类型的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)

}
Run Code Online (Sandbox Code Playgroud)

这个东西显然没有编译:

错误:类型不匹配; 发现:需要东西[B]:东西[A]注意:B <:A,但是类型T中的类Stuff是不变的.你可能希望将T定义为+ T. (SLS 4.5)consumerOfStuff.doWith(stuff2).

但同样,这来自对方差使用的误解,如在设计业务应用程序时如何使用共同和反方差?Kris Nuttycombe回答解释.如果我们重构如下

class ConsumerOfStuff[T <: CanBeContained] {

  def doWith[A<:T](stuff: Stuff[A]) = stuff.t.writeSomething

}
Run Code Online (Sandbox Code Playgroud)

你可以看到一切编译得很好.


0__*_*0__ 2

不是答案,而是我通过查看问题可以观察到的内容:

  • 我看到了MonomerClass但没有Monomer

我的直觉告诉你,你应该尽可能避免使用清单,因为你已经看到它们会让事情变得复杂。我认为你不需要它们。例如justTheProductMonomersGraphClass既然您可以完全控制类层次结构,为什么不直接为涉及运行时检查的任何内容添加测试方法呢Monomer?例如

trait Monomer {
   def productOption: Option[ProductMonomer]
}
Run Code Online (Sandbox Code Playgroud)

那么你就会有

def justTheProductMonomers : Seq[ProductMonomer] = monomers.flatMap( _.productOption )
Run Code Online (Sandbox Code Playgroud)

等等。

这里的问题是,似乎您可以拥有一个满足产品谓词的通用单体,而您以某种方式想要 sub-type ProductMonomer

我给出的一般建议是首先定义处理规则所需的测试矩阵,然后将这些测试作为方法放入特定的特征中,除非您有一个可以进行模式匹配的扁平层次结构,即更容易,因为消歧会集中在您的使用站点,而不是分布在所有实施类型中。

也不要尝试通过编译时类型限制来逾期。通常,在运行时检查一些约束是完全可以的。这样至少你可以构建一个完全工作的系统,然后你可以尝试找出可以将运行时检查转换为编译时检查的点,并决定这种努力是否值得。在 Scala 中解决类型级别的问题很有吸引力,因为它很复杂,但它也需要最多的技能才能正确完成。