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

2024-04-02

我最近在处理 Scala 时发布了几个关于 SO 的问题traits https://stackoverflow.com/questions/11618915, 表示类型 https://stackoverflow.com/questions/11277656, 会员类型 https://stackoverflow.com/questions/11236122, 体现 https://stackoverflow.com/questions/11284538, and 隐性证据 https://stackoverflow.com/questions/11588014。这些问题背后是我为生物蛋白质网络构建建模软件的项目。尽管这些答案非常有帮助,让我比我自己能得到的答案更加接近,但我仍然没有为我的项目找到解决方案。几个答案表明我的设计有缺陷,这就是为什么解决方案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是在Reactant of a Rule。我也希望能够collect一切Monomer特定类型的 s,例如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]

单体类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 相信返回副本的方法确实返回与放入的类型完全相同的对象。

毫无疑问,我留下了一些大家都不清楚的事情。如果我需要添加任何细节,或者需要删除多余的细节,我会尽力快速澄清。

Edit

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

我已经将你的问题简化为特征,并且我开始理解为什么你会遇到强制转换和抽象类型的麻烦。

您实际上缺少的是临时多态性,您可以通过以下方式获得它: - 编写具有通用签名的方法,依赖于相同通用的隐式将工作委托给 - 使隐式仅可用于该通用参数的特定值,当您尝试执行非法操作时,这将变成“隐式未找到”编译时错误。

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

  • 替换站点时,您想要创建新泛型类型的新单体,就像向集合添加一个现有泛型类型的超类对象时所做的那样:您将获得一个其类型参数是超类的新集合。结果您应该得到这个新的单体。

  • 您不确定该操作是否会产生结果(以防您无法真正替换状态)。在这种情况下,正确的类型是 Option[T]

    def replaceSiteWithIntersection[A >: StateSiteType <: ReactantStateSite]
    (thisSite: A, monomer: ReactantMonomer): Option[MonomerClass[A]] 
    

如果我们现在查看类型错误中的 digger,我们可以看到真正的类型错误来自此方法:

 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]

}
  1. 请注意,如果您以巧妙的方式使用隐式解析规则(例如,您将昆虫放在 Intersector 特征的伴生对象中,以便它会自动解析),则无需特殊导入即可使用此模式。

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


    }


  }

通过这样的实现,每当您想要将一个站点替换为不同类型的另一个站点时,您所需要的只是提供具有不同类型的 CanReact 的新隐式实例。

最后,我将(我希望)清楚地解释为什么您不需要协变。

假设你有一个Consumer[T] and a Producer[T].

当你想提供给Consumer[T1] a Producer[T2] where 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

但同样,这来自对方差使用的误解,因为在设计业务应用程序时如何使用协变和逆变? https://stackoverflow.com/questions/5277526/real-world-examples-of-co-and-contravariance-in-scala克里斯·纳蒂科姆(Kris Nuttycombe)回答解释。如果我们像下面这样重构

class ConsumerOfStuff[T <: CanBeContained] {

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

}

你可以看到一切都编译良好。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Scala:构建复杂的特征和类层次结构 的相关文章

随机推荐