Scala: построение сложной иерархии признаков и классов

Я недавно опубликовал несколько вопросов о 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)
  }
}

Ответ 1

Я уменьшил вашу проблему до черт, и я начинаю понимать, почему вы попадаете в проблемы с приведениями и абстрактными типами.

То, что вам действительно не хватает, это ad-hoc-полиморфизм, который вы получаете через следующее: - Написание метода с общей подписью, основанной на неявном одном и том же родовом для делегирования работы - Сделать неявным доступным только для определенного значения этого общего параметра, который превратится в "неявную не найденную" ошибку времени компиляции при попытке сделать что-то незаконное.

Теперь посмотрим на проблему по порядку. Во-первых, подпись вашего метода неверна по двум причинам:

  • При замене сайта вы хотите создать новый мономер нового родового типа, как и при добавлении в коллекцию объекта, который является суперклассом существующего родового типа: вы получаете новую коллекцию чей параметр типа является суперклассом. В результате вы должны получить этот новый мономер.

  • Вы не уверены, что операция даст результат (в случае, если вы действительно не сможете заменить состояние). В этом случае правильный тип: 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]

}
  • Обратите внимание, что этот шаблон можно использовать без специального импорта, если вы используете умные способы неявного разрешения правил (например, вы помещаете свой insector в объект-компаньон из свойства Intersector, чтобы он автоматически разрешался).

  • Хотя этот шаблон работает отлично, существует ограничение, связанное с тем, что ваше решение работает только для определенного типа 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] a Producer[T2], где T2<:<T1. Но если вам нужно использовать значение, созданное T2 внутри T1, вы можете

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 <: A, но класс Stuff инвариантен по типу T. Вы можете определить T как + T. (SLS 4.5) consumerOfStuff.doWith(stuff2).

Но опять же это происходит из-за неверного истолкования использования дисперсии, поскольку Как координируется и противоречиво используется при проектировании бизнес-приложений? Крис Нуттыкомб ответ объясняет. Если мы реорганизуем следующий

class ConsumerOfStuff[T <: CanBeContained] {

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

}

Вы можете видеть, что все компилируется отлично.

Ответ 2

Не ответ, но что я могу наблюдать из вопроса:

  • Я вижу MonomerClass, но не Monomer

Мои мужества говорят, что вам следует избегать проявлений, когда это возможно, поскольку вы видели, что они могут усложнить ситуацию. Я не думаю, что они вам понадобятся. Например, метод justTheProductMonomers в GraphClass – так как у вас есть полный контроль над иерархией классов, почему бы не добавить методы тестирования для чего-либо, связанного с проверкой времени выполнения, на Monomer напрямую? Например.

trait Monomer {
   def productOption: Option[ProductMonomer]
}

тогда у вас будет

def justTheProductMonomers : Seq[ProductMonomer] = monomers.flatMap( _.productOption )

и т.д.

Проблема заключается в том, что кажется, что у вас может быть общий мономер, удовлетворяющий предикату продукта, тогда как вы каким-то образом захотите подтипа ProductMonomer.

Общая рекомендация, которую я бы дал, - это сначала определить вашу матрицу тестов, которая вам нужна для обработки правил, а затем поместить эти тесты в качестве методов в конкретные черты, если у вас нет плоской иерархии, для которой вы можете выполнить сопоставление шаблонов, что проще, так как неоднозначность окажется сосредоточенной на вашем сайте использования и не распространяется на все типы реализации.

Также не пытайтесь просрочить его с помощью ограничений типа компиляции. Часто это прекрасно, чтобы некоторые ограничения проверялись во время выполнения. Таким образом, по крайней мере, вы можете создать полностью работоспособную систему, а затем можете попытаться определить точки, в которых вы можете конвертировать проверку времени выполнения в проверку времени компиляции, и решить, стоит ли это или нет. Это привлекательно, чтобы решить вещи на уровне типа в Scala, из-за его сложности, но для этого также требуется большинство навыков, чтобы сделать это правильно.

Ответ 3

Существует несколько проблем. Во-первых, весь метод является странным: с одной стороны, вы передаете аргумент monomer, и если найден аргумент thisState, этот метод не имеет ничего общего с приемником &mdash, то почему это метод в 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 of ReactantStateSite. Поэтому параметр типа A <: ReactantStateSite { type MyType = A } — последний бит интересен, и это была новая находка для меня: здесь вы можете указать член типа, чтобы убедиться, что ваш тип возврата из createIntersection на самом деле A.


В вашем методе все еще есть что-то странное, потому что, если я не ошибаюсь, вы в конечном итоге вызываете x.createIntersection(x) (пересекая thisSite с собой, что является не-оператором).

Ответ 4

Одна из недостатков replaceSiteWithIntersection заключается в том, что согласно сигнатуре метода тип thisSite (A) является супер-типом StateSiteType тип ReactantStateSite.

Но затем вы в конце концов отбросите его на StateSiteType with ReactantStateSite. Это не имеет смысла для меня.

Где вы получаете уверенность в том, что A внезапно является StateSiteType?