Я недавно опубликовал несколько вопросов о SO, которые касаются Scala признаков, типов объявлений, , показывает и скрытые данные. За этими вопросами стоит мой проект по созданию программного обеспечения для моделирования биологических белковых сетей. Несмотря на чрезвычайно полезные ответы, которые приблизили меня ближе, чем когда-либо, я еще не пришел к решению для своего проекта. Несколько ответов показали, что мой дизайн ошибочен, поэтому решения на Foo
-фрагментарные вопросы не работают на практике. Здесь я отправляю более сложную (но все же значительно упрощенную) версию моей проблемы. Я надеюсь, что проблема и решение будут в целом полезными для людей, пытающихся создать сложные иерархии признаков и классов в Scala.
Высшим классом в моем проекте является правило биологической реакции. Правило описывает, как один или два реагента трансформируются реакцией. Каждый реагент представляет собой график, который имеет узлы, называемые мономерами и краями, которые соединяются между названными сайтами на мономерах. Каждый сайт также имеет состояние, в котором он может находиться. Изменить: концепция ребер была удалена из кода примера, потому что они усложняют пример, не внося большой вклад в вопрос. Правило может сказать что-то например: имеется один реагент из мономера A, связанный с мономером B через сайты a1 и b1, соответственно; связь ломается по правилу, оставляя сайты 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
a Rule
. Я также хотел бы иметь возможность collect
всех Monomer
определенного типа, например ReactantMonomer
s. Я использую псевдонимы типов для управления ограничениями.
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
также имеет параметры типа, так что я могу устанавливать ограничения на сайты; например, a 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)
}
}