10

私は最近、Scalaの特性表現型メンバー型マニフェスト、および暗黙の証拠を扱う SO に関するいくつかの質問を投稿しました。これらの疑問の背後には、生物学的タンパク質ネットワークのモデリング ソフトウェアを構築するという私のプロジェクトがあります。非常に役立つ回答が寄せられ、自分自身で得ることができなかったほど身近になりましたが、プロジェクトの解決策にはまだ到達していません。いくつかの回答は、私の設計に欠陥があることを示唆しています。Foo-フレーム付きの質問は実際には機能しません。ここでは、私の問題のより複雑な (ただし大幅に簡略化された) バージョンを投稿しています。この問題と解決策が、Scala でトレイトとクラスの複雑な階層を構築しようとしている人々にとって広く役立つことを願っています。

私のプロジェクトの最上位クラスは、生物反応規則です。ルールは、1 つまたは 2 つの反応物が反応によってどのように変換されるかを記述します。各反応物は、モノマーと呼ばれるノードと、モノマーの名前付きサイト間を接続するエッジを持つグラフです。各サイトには、可能な状態もあります。編集:エッジの概念は、質問にあまり貢献せずに例を複雑にするため、コード例から削除されました。規則は次のように言うかもしれません: それぞれサイト a1 と b1 を介してモノマー B に結合したモノマー A でできた 1 つの反応物があります。結合は、サイト a1 と b1 を非結合のままにするルールによって切断されます。モノマー A で同時に、サイト a1 の状態が U から P に変更されます。これを次のように記述します。

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

(Scala でこのような文字列を解析するのはとても簡単で、頭が混乱しました。)-1は、bond #1 がこれらのサイト間にあることを示しています。番号は、任意のラベルにすぎません。

これまでに得たものと、各コンポーネントを追加した理由を以下に示します。コンパイルされますが、無償で使用する必要がありasInstanceOfます。asInstanceOfタイプが一致するように s を削除するにはどうすればよいですか?

基本クラスでルールを表します。

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は型パラメーターがあります。これにより、特定のグラフで許可されるモノマーとエッジの種類に制約を加えることができます。たとえば、aProducedMonomerの に s を含めることはできません。また、特定のタイプのすべてのsを実行できるようにしたいと考えています。型エイリアスを使用して制約を管理します。ReactantRulecollectMonomerReactantMonomer

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適切な状態を表す です。(オブジェクトの状態を保持するために文字列を使用することに注意してください。実際には、実際のコードでは名前クラスです。) これらのトレイトの重要な目的の 1 つは、すべてのサブクラスが必要とする機能を提供することです。まあ、それがすべての特性の目的ではありません。私の特性は、多くのメソッドが特性のすべてのサブクラスに共通するオブジェクトのプロパティに小さな変更を加えてからコピーを返すという点で特別です。戻り値の型がオブジェクトの基になる型と一致することが望ましいでしょう。これを行うための不十分な方法は、すべてのトレイト メソッドを抽象化し、必要なメソッドをすべてのサブクラスにコピーすることです。これを行うための適切な 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 は、私が見ることができる 2 つの主な理由により、型またはメンバー パラメーターのインスタンスを渡すことに特に不満を持っています。コピーを返すメソッドが実際に入れられたものとまったく同じ型のオブジェクトを返すことを Scala に納得させるのは困難です。

私は間違いなく、誰にとっても明確ではないいくつかのことを残しました. 追加する必要がある詳細、または削除する必要がある余分な詳細がある場合は、迅速に解決できるように努めます。

編集

@0__ は、replaceSiteWithIntersectionを なしでコンパイルしたメソッドに置き換えましたasInstanceOf。残念ながら、型エラーなしでメソッドを呼び出す方法が見つかりません。彼のコードは基本的に、この新しいクラスの最初のメソッドですMonomerClass。それを呼び出す 2 番目のメソッドを追加しました。

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

4 に答える 4

6

私はあなたの問題を特徴に落とし込みました.なぜあなたがキャストと抽象型で問題を抱えているのかを理解し始めています.

実際に欠けているのはアドホックなポリモーフィズムです。これは、次の方法で取得できます。 - 同じジェネリックの暗黙に依存してジェネリック署名を使用してメソッドを記述し、作業を委譲する - そのジェネリックの特定の値に対してのみ暗黙を使用できるようにするこれは、何か違法なことをしようとすると、「暗黙的に見つかりません」というコンパイル時エラーになります。

では、問題を順番に見ていきましょう。1 つ目は、次の 2 つの理由でメソッドの署名が間違っていることです。

  • 既存のジェネリック型のスーパークラスであるオブジェクトをコレクションに追加するときと同じように、新しいジェネリック型の新しいモノマーを作成するサイトを置き換える場合: 型パラメーターがスーパークラスである新しいコレクションを取得します。 . 結果として、この新しいモノマーが得られるはずです。

  • 操作が結果をもたらすかどうか確信が持てない (状態を実際に置き換えることができない場合)。そのような場合、正しいタイプは Option[T] です

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

型エラーで digger を見ると、実際の型エラーは次のメソッドから来ていることがわかります。

 thisSite.createIntersection

理由は簡単です。署名は ReactantSite を受け入れるため、残りの型と一貫性がありませんが、stateSite ( Seq[StateSiteType] 型) の 1 つをパラメーターとして渡して呼び出す必要がありますが、保証はありません。それ

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]aと aがあるとしましょうProducer[T]

whereに提供Consumer[T1]する場合は、共分散が必要です。ただし、T1 内で T2 によって生成された値を使用する必要がある場合は、次のことができます。Producer[T2]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)

}

このようなものは明らかにコンパイルされません:

エラー: タイプが一致しません。found : Stuff[B] required: Stuff[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

}

すべてが正常にコンパイルされていることがわかります。

于 2012-07-25T14:01:36.187 に答える
2

答えではありませんが、質問を見たときに観察できることは次のとおりです。

  • 見えますMonomerClassが見えませんMonomer

私の内臓は、マニフェストは物事を複雑にする可能性があることを見てきたので、可能な場合はマニフェストを避けるべきだと言っています。私はあなたがそれらを必要とするとは思わない。たとえば、のjustTheProductMonomersメソッドGraphClass–クラス階層を完全に制御できるので、ランタイムチェックを含むもののテストメソッドをMonomer直接追加してみませんか?例えば

trait Monomer {
   def productOption: Option[ProductMonomer]
}

その後、あなたは持っているでしょう

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

などなど。

ここでの問題は、サブタイプが必要なのに、製品の述語を満たすジェネリックモノマーを使用できるように見えることですProductMonomer

私が与える一般的なアドバイスは、最初にルールを処理するために必要なテストのマトリックスを定義し、次にパターンマッチングを実行できるフラットな階層がない限り、それらのテストをメソッドとして特定の特性に配置することです。曖昧性解消は使用サイトに集中して表示され、すべての実装タイプに広がるわけではないため、より簡単です。

また、コンパイル時の型の制約でそれを遅らせようとしないでください。多くの場合、実行時にいくつかの制約をチェックすることはまったく問題ありません。そうすれば、少なくとも完全に機能するシステムを構築できます。次に、ランタイムチェックをコンパイル時チェックに変換できるポイントを見つけて、その努力に見合う価値があるかどうかを判断できます。Scalaは洗練されているため、タイプレベルで問題を解決することは魅力的ですが、それを正しく行うには最も多くのスキルが必要です。

于 2012-07-24T17:51:32.983 に答える
1

複数の問題があります。まず、メソッド全体が奇妙です: 一方では、monomer引数を渡します。引数が見つかった場合、メソッドthisStateレシーバーとは何の関係もありません。 " 関数 — 一方、 が見つからない場合は戻ります。あなたはもともと も持っていたので、議論全体が時代遅れであり、あなたは実際に を操作したかったのだと思います。 MonomerClassthisthisSiteimplicit evidence: MyType <:< ReactantMonomermonomernew_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である証拠に制限されていReactantStateSiteます。したがって、型パラメーターA <: ReactantStateSite { type MyType = A }— 最後のビットは興味深いものであり、これは私にとって新しい発見でした: ここで型メンバーを指定して、 from からの戻り値の型createIntersectionが実際にであることを確認できますA


私が間違っていなければ、あなたは電話をかけることになるので、あなたの方法にはまだ奇妙なことがありますx.createIntersection(x)(それ自体と交差thisSiteします。これはノーオペレーションです)。

于 2012-07-24T23:11:51.490 に答える
0

欠点の1つreplaceSiteWithIntersectionは、メソッドシグネチャによると、thisSiteA)のタイプがのスーパータイプStateSiteTypeサブタイプであるということReactantStateSiteです。

しかし、最終的にはにキャストしStateSiteType with ReactantStateSiteます。それは私には意味がありません。

A突然それがどこにあるのかという保証はどこで得られますStateSiteTypeか?

于 2012-07-24T19:22:39.913 に答える