Scala:构建复杂的特征和类层次结构 [英] Scala: Building a complex hierarchy of traits and classes

查看:33
本文介绍了Scala:构建复杂的特征和类层次结构的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have posted several questions on SO recently dealing with Scala traits, representation types, member types, manifests, and implicit evidence. Behind these questions is my project to build modeling software for biological protein networks. Despite the immensely helpful answers, which have gotten me closer than I ever could get on my own, I have still not arrived at an solution for my project. A couple of answers have suggested that my design is flawed, which is why the solutions to the Foo-framed questions don't work in practice. Here I am posting a more complicated (but still greatly simplified) version of my problem. My hope is that the problem and solution will be broadly useful for people trying to build complex hierarchies of traits and classes in Scala.

我项目中最高级别的课程是生物反应规则.规则描述了一种或两种反应物如何被反应转化.每个反应物都是一个图,其中包含称为单体的节点和连接单体上命名位点之间的边.每个站点也有一个它可以处于的状态.边的概念已从示例代码中删除,因为它们使示例复杂化而对问题没有太大影响.规则可能会说明一些问题像这样:有一种反应物由单体 A 分别通过位点 a1 和 b1 与单体 B 结合;绑定被规则打破,使站点 a1 和 b1 未绑定;同时在单体 A 上,位点 a1 的状态从 U 变为 P.我将其写为:

The highest-level class in my project is the biological reaction rule. A rule describes how one or two reactants are transformed by a reaction. Each reactant is a graph that has nodes called monomers and edges that connect between named sites on the monomers. Each site also has a state that it can be in. The concept of the edges have been removed from the example code because they complicate the example without contributing much to the question. A rule might say something like this: there is one reactant made of monomer A bound to monomer B through sites a1 and b1, respectively; the bond is broken by the rule leaving sites a1 and b1 unbound; simultaneously on monomer A, the state of site a1 is changed from U to P. I would write this as:

A(a1~U-1).B(b1-1) -> A(a1~P) + B(b1)

(在 Scala 中解析这样的字符串非常容易,这让我头晕目眩.)-1 表示键 #1 位于这些站点之间——这个数字只是一个任意的标签.

(Parsing strings like this in Scala was so easy, it made my head spin.) The -1 indicates that bond #1 is between those sites--the number is just a arbitrary label.

这是我到目前为止所拥有的以及我添加每个组件的原因.它可以编译,但只能免费使用 asInstanceOf.如何摆脱 asInstanceOf 以便类型匹配?

Here is what I have so far along with the reasoning for why I added each component. It compiles, but only with gratuitous use of asInstanceOf. How do I get rid of the asInstanceOfs so that the types match?

我用一个基本类表示规则:

I represent rules with a basic class:

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 具有类型参数,因为这样我就可以限制特定图形中允许的单体和边的类型;例如,RuleReactant 中不能有任何ProducedMonomer.我还希望能够收集特定类型的所有Monomer,比如ReactantMonomer.我使用类型别名来管理约束.

The class for graphs GraphClass has type parameters because so that I can put constraints on what kinds of monomers and edges are allowed in a particular graph; for example, there cannot be any ProducedMonomers in the Reactant of a Rule. I would also like to be able to collect all the Monomers of a particular type, say ReactantMonomers. I use type aliases to manage the constraints.

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.此外,我需要收集特定类型的所有单体,例如,收集产品中规则中的所有单体,因此我添加了一个Manifest每个类型参数.

The class for monomers MonomerClass has type parameters, as well, so that I can put constraints on the sites; for example, a ConsumedMonomer cannot have a StaticStateSite. Furthermore, I need to collect all the monomers of a particular type to, say, collect all the monomers in a rule that are in the product, so I add a Manifest to each type parameter.

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 的类.(最好使用字符串来保存对象状态;它们实际上是我真实代码中的名称类.)这些特征的一个重要目的是提供所有子类都需要的功能.好吧,这不就是所有特征的目的吗?我的 trait 很特别,因为许多方法对对象的属性进行了小的更改,该属性对 trait 的所有子类都是通用的,然后返回一个副本.如果返回类型与对象的基础类型匹配,则更好.这样做的蹩脚方法是将所有 trait 方法抽象化,并将所需的方法复制到所有子类中.我不确定执行此操作的正确 Scala 方法.一些来源建议使用一个成员类型 MyType 来存储底层类型(如下所示).其他来源建议使用表示类型参数.

My current implementation for StateSite does not have type parameters; it is a standard hierarchy of traits, terminating in classes that have a name and some Strings that represent the appropriate state. (Be nice about using strings to hold object states; they are actually name classes in my real code.) One important purpose of these traits is provide functionality that all the subclasses need. Well, isn't that the purpose of all traits. My traits are special in that many of the methods make small changes to a property of the object that is common to all subclasses of the trait and then return a copy. It would be preferable if the return type matched the underlying type of the object. The lame way to do this is to make all the trait methods abstract, and copy the desired methods into all the subclasses. I am unsure of the proper Scala way to do this. Some sources suggest a member type MyType that stores the underlying type (shown here). Other sources suggest a representation type parameter.

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

My biggest problems are with methods framed like MonomerClass.replaceSiteWithIntersection. A lot of methods do some complicated search for particular members of the class, then pass those members to other functions where complicated changes are made to them and return a copy, which then replaces the original in a copy of the higher-level object. How should I parameterize methods (or the classes) so that the calls are type safe? Right now I can get the code to compile only with lots of asInstanceOfs everywhere. Scala is particularly unhappy with passing instances of a type or member parameter around because of two main reasons that I can see: (1) the covariant type parameter ends up as input to any method that takes them as input, and (2) it is difficult to convince Scala that a method that returns a copy indeed returns an object with exactly the same type as was put in.

我无疑留下了一些大家都不清楚的东西.如果有什么需要补充的细节,或者多余的细节需要删除,我会尽量快速清理.

I have undoubtedly left some things that will not be clear to everyone. If there are any details I need to add, or excess details I need to delete, I will try to be quick to clear things up.

编辑

@0__ 用不使用 asInstanceOf 编译的方法替换了 replaceSiteWithIntersection.不幸的是,我找不到在没有类型错误的情况下调用该方法的方法.他的代码本质上是 MonomerClass 这个新类中的第一个方法;我添加了调用它的第二种方法.

@0__ replaced the replaceSiteWithIntersection with a method that compiled without asInstanceOf. Unfortunately, I can't find a way to call the method without a type error. His code is essentially the first method in this new class for MonomerClass; I added the second method that calls it.

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

推荐答案

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

I have reduced your problem to traits, and I am starting to understand why you are getting into troubles with casts and abstract types.

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

What you are actually missing is ad-hoc polymorphism, which you obtain through the following: - Writing a method with generic signature relying on an implicit of the same generic to delegate the work to - Making the implicit available only for specific value of that generic parameter, which will turn into a "implicit not found" compile time error when you try to do something illegal.

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

Let's now look to the problem in order. The first is that the signature of your method is wrong for two reasons:

  • 当替换一个站点时,你想创建一个新的泛型类型的新单体,就像你将一个对象添加到集合中时所做的那样,该对象是现有泛型类型的超类:你得到一个新的集合其类型参数是超类.因此,您应该产生这个新的 Monomer.

  • When replacing a site you want to create a new monomer of the new generic type, much as you do when you add to a collection an object which is a superclass of the existing generic type: you get a new collection whose type parameter is the superclass. You should yield this new Monomer as a result.

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

You are not sure that the operation will yield a result (in case you can't really replace a state). In such a case the right type it's Option[T]

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

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

If we now look digger in the type errors, we can see that the real type error comes from this method:

 thisSite.createIntersection

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

The reason is simple: it's signature is not coherent with the rest of your types, because it accepts a ReactantSite but you want to call it passing as parameter one of your stateSites (which is of type Seq[StateSiteType] ) but you have no guarantee that

StateSiteType<:<ReactantSite

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

Now let's see how evidences can help you:

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 trait 的伴生对象中,以便它会自动解析),则无需特殊导入即可使用此模式).

  1. Please note that this pattern can be used with no special imports if you use in a clever way implicit resolving rules (for example you put your insector in the companion object of Intersector trait, so that it will be automatically resolved).

虽然此模式完美运行,但您的解决方案仅适用于特定 StateSiteType 的事实存在限制.Scala 集合解决了添加另一个隐式调用 CanBuildFrom 的类似问题.在我们的例子中,我们称之为 CanReact

While this pattern works perfectly, there is a limitation connected to the fact that your solution works only for a specific StateSiteType. Scala collections solve a similar problem adding another implicit, which is call CanBuildFrom. In our case we will call it CanReact

您必须使 MonomerClass 保持不变,但这可能是个问题(但是,为什么需要协方差?)

You will have to make your MonomerClass invariant, which might be a problem though (why do you need covariance, however?)

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 的新隐式实例.

With such an implementation, whenever you want to make the possibility to replace a site with another site of a different type, all you need is to make available new implicit instances of CanReact with different types.

我将以(我希望)清楚地解释为什么您不需要协方差作为结束.

I will conclude with a (I hope) clear explanation of why you should not need covariance.

假设您有一个 Consumer[T] 和一个 Producer[T].

Let's say you have a Consumer[T] and a Producer[T].

当你想向 Consumer[T1] 提供一个 Producer[T2] 时你需要协方差,其中 T2<:.但是如果需要在T1内部使用T2产生的值,可以

You need covariance when you want to provide to the Consumer[T1] a Producer[T2] where T2<:<T1 . But if you need to use the value produced by T2 inside T1, you can

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)

}

这东西显然不能编译:

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

error: type mismatch; found : Stuff[B] required: Stuff[A] Note: B <: A, but class Stuff is invariant in type T. You may wish to define T as +T instead. (SLS 4.5) consumerOfStuff.doWith(stuff2).

但同样,这是由于对方差用法的误解,如 如何在设计业务应用程序时使用协方差和逆方差? Kris Nuttycombe 回答解释.如果我们像下面这样重构

But again, this come from a misinterpretation of usage of variance, as How are co- and contra-variance used in designing business applications? Kris Nuttycombe answer explain. If we refactor like the following

class ConsumerOfStuff[T <: CanBeContained] {

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

}

你可以看到一切都编译正常.

You could see everything compiling fine.

这篇关于Scala:构建复杂的特征和类层次结构的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆