33

私はこのようなことをしたい:

sealed abstract class Base(val myparam:String)

case class Foo(override val myparam:String) extends Base(myparam)
case class Bar(override val myparam:String) extends Base(myparam)

def getIt( a:Base ) = a.copy(myparam="changed")

getItのコンテキストでは、すべてのBaseに「copy」メソッドがあることをコンパイラーに伝えていませんが、copyも実際にはメソッドではないため、特性や抽象メソッドはないと思います。 Baseに入れて、これを正しく機能させることができます。または、ありますか?

Baseをとして定義しようとすると、次のようabstract class Base{ def copy(myparam:String):Base }になりcase class Foo(myparam:String) extends Baseます。class Foo needs to be abstract, since method copy in class Base of type (myparam: String)Base is not defined

Baseすべてのクラスが実装でケースクラスになることをコンパイラに伝える他の方法はありますか?「ケースクラスのプロパティを持っている」という意味のいくつかの特性?

Baseをケースクラスにすることはできますが、ケースクラスからの継承は非推奨であるというコンパイラの警告が表示されますか?

私もできることを知っています:

def getIt(f:Base)={ 
  (f.getClass.getConstructors.head).newInstance("yeah").asInstanceOf[Base]
}

しかし...それは非常に醜いようです。

考え?私のアプローチ全体は単に「間違っている」のでしょうか?

UPDATE属性を含むように基本クラスを変更し、ケースクラスで「override」キーワードを使用するようにしました。これは実際の問題をよりよく反映し、Edmondo1984の応答を考慮して問題をより現実的にします。

4

7 に答える 7

28

これは、質問が変更される前の古い答えです。

強く型付けされたプログラミング言語は、あなたがやろうとしていることを妨げます。理由を見てみましょう。

次のシグネチャを持つメソッドのアイデア:

def getIt( a:Base ) : Unit

メソッドの本体は、Baseクラスまたはインターフェイスを介して表示されるプロパティ、つまり、Baseクラス/インターフェイスまたはその親でのみ定義されたプロパティとメソッドにアクセスできるようになります。コードの実行中に、メソッドに渡される特定のインスタンスごとgetItに異なるサブクラスが存在する可能性がありますが、のコンパイルタイプaは常に次のようになります。Base

このように推論することができます:

わかりました。クラスBaseがあり、2つのケースクラスで継承し、同じ名前のプロパティを追加してから、Baseのインスタンスのプロパティにアクセスしようとします。

簡単な例は、これが安全でない理由を示しています。

sealed abstract class Base
case class Foo(myparam:String) extends Base
case class Bar(myparam:String) extends Base
case class Evil(myEvilParam:String) extends Base

def getIt( a:Base ) = a.copy(myparam="changed")

次の場合、コンパイラがコンパイル時にエラーをスローしなかった場合、コードは実行時に存在しないプロパティにアクセスしようとすることを意味します。これは厳密に型指定されたプログラミング言語では不可能です。これにより、コードに含まれる可能性のあるバグの数が大幅に減少することを知っているため、コンパイラによるコードの検証を大幅に強化するために、記述できるコードに対する制限を交換しました。


これが新しい答えです。結論に達するまでに必要なポイントが少ないため、少し長いです

残念ながら、提案した内容を実装するために、ケースクラスのコピーのメカニズムに依存することはできません。copyメソッドが機能する方法は、大文字と小文字を区別しないクラスで自分で実装できるコピーコンストラクターです。ケースクラスを作成し、REPLで分解してみましょう。

scala>  case class MyClass(name:String, surname:String, myJob:String)
defined class MyClass

scala>  :javap MyClass
Compiled from "<console>"
public class MyClass extends java.lang.Object implements scala.ScalaObject,scala.Product,scala.Serializable{
    public scala.collection.Iterator productIterator();
    public scala.collection.Iterator productElements();
    public java.lang.String name();
    public java.lang.String surname();
    public java.lang.String myJob();
    public MyClass copy(java.lang.String, java.lang.String, java.lang.String);
    public java.lang.String copy$default$3();
    public java.lang.String copy$default$2();
    public java.lang.String copy$default$1();
    public int hashCode();
    public java.lang.String toString();
    public boolean equals(java.lang.Object);
    public java.lang.String productPrefix();
    public int productArity();
    public java.lang.Object productElement(int);
    public boolean canEqual(java.lang.Object);
    public MyClass(java.lang.String, java.lang.String, java.lang.String);
}

Scalaでは、copyメソッドは3つのパラメーターを取り、最終的には現在のインスタンスから指定していないものを使用できます(Scala言語は、その機能の中でメソッド呼び出しのパラメーターのデフォルト値を提供します)

分析を進めて、更新されたコードをもう一度取得してみましょう。

sealed abstract class Base(val myparam:String)

case class Foo(override val myparam:String) extends Base(myparam)
case class Bar(override val myparam:String) extends Base(myparam)

def getIt( a:Base ) = a.copy(myparam="changed")

このコンパイルを行うには、次のコントラクトを尊重するgetIt(a:MyType) の署名で使用する必要があります。MyType

パラメータmyparamと、デフォルト値を持つ他のパラメータを持つもの

これらすべての方法が適しています。

  def copy(myParam:String) = null
  def copy(myParam:String, myParam2:String="hello") = null
  def copy(myParam:String,myParam2:Option[Option[Option[Double]]]=None) = null

この契約をScalaで表現する方法はありませんが、役立つ高度なテクニックがあります。

case classes私たちができる最初の観察は、ScalaとScalaの間には厳密な関係があるということですtuples。実際、ケースクラスは、追加の動作と名前付きプロパティを備えたタプルです。

2番目の観察結果は、クラス階層のプロパティの数が同じであることが保証されていないため、コピーメソッドのシグネチャが同じであるとは保証されていないということです。

AnyTuple[Int]実際には、最初の値がInt型である任意のサイズを記述していると仮定するとTuple、次のようなことを実行しようとしています。

def copyTupleChangingFirstElement(myParam:AnyTuple[Int], newValue:Int) = myParam.copy(_1=newValue)

すべての要素がである場合、これは難しくありませんInt。同じタイプのすべての要素を持つタプルはでありList、の最初の要素を置き換える方法を知っていListます。TupleXanyをに変換しList、最初の要素を置き換えて、Listbackをに変換する必要がありTupleXます。Xはい、想定するすべての値に対してすべてのコンバーターを作成する必要があります。迷惑ですが、難しくはありません。

ただし、この場合、すべての要素がであるとは限りませんIntTuple最初の要素がIntの場合、要素のタイプが異なる場所をすべて同じであるかのように扱いたいと思います。これはと呼ばれます

「アリティを超えて抽象化」

つまり、サイズに関係なく、一般的な方法で異なるサイズのタプルを処理します。そのためには、それらを異種タイプをサポートする特別なリストに変換する必要があります。HList


結論

メーリングリストの複数の投稿からわかるように、ケースクラスの継承は非常に正当な理由で非推奨になっています:http ://www.scala-lang.org/node/3289

問題に対処するための2つの戦略があります。

  1. 変更する必要のあるフィールドの数が限られている場合は、@Ronによって提案されたようなコピーメソッドを持つアプローチを使用してください。型情報を失わずにやりたいのなら、基本クラスの生成に行きます

    sealed abstract class Base[T](val param:String){
      def copy(param:String):T
    }
    
    class Foo(param:String) extends Base[Foo](param){
      def copy(param: String) = new Foo(param)
    }
    
    def getIt[T](a:Base[T]) : T = a.copy("hello")
    
    scala>  new Foo("Pippo")
    res0: Foo = Foo@4ab8fba5
    
    scala>  getIt(res0)
    res1: Foo = Foo@5b927504
    
    scala>  res1.param
    res2: String = hello
    
  2. 本当にアリティを抽象化したい場合、解決策は、MilesSabinによって開発されたShapelessというライブラリを使用することです。議論の後に尋ねられた質問がここにあります:HListsはタプルを書く複雑な方法にすぎませんか?しかし、これはあなたにいくつかの頭痛を与えるだろうとあなたに言います

于 2012-09-19T10:11:18.090 に答える
14

2つのケースクラスが時間の経過とともに分岐して異なるフィールドを持つ場合、共有copyアプローチは機能しなくなります。

抽象を定義することをお勧めしますdef withMyParam(newParam: X): Base。さらに良いことに、抽象型を導入して、戻り時にcaseクラス型を保持することができます。

scala> trait T {
     |   type Sub <: T
     |   def myParam: String
     |   def withMyParam(newParam: String): Sub
     | }
defined trait T

scala> case class Foo(myParam: String) extends T {
     |   type Sub = Foo
     |   override def withMyParam(newParam: String) = this.copy(myParam = newParam)
     | }
defined class Foo

scala>

scala> case class Bar(myParam: String) extends T {
     |   type Sub = Bar
     |   override def withMyParam(newParam: String) = this.copy(myParam = newParam)
     | }
defined class Bar

scala> Bar("hello").withMyParam("dolly")
res0: Bar = Bar(dolly)
于 2012-09-11T13:13:30.267 に答える
14

TL; DR:コンパイラに派生ケースクラスでの実装を自動生成させながら、Baseでcopyメソッドを宣言することができました。これには少しトリックが必要です(実際には、型階層を再設計するだけです)が、少なくとも、派生したケースクラスのいずれにもボイラープレートコードを記述しなくても、実際に機能させることができることを示しています。

まず、ronとEdmondo1984ですでに述べたように、ケースクラスのフィールドが異なると問題が発生します。

ただし、厳密にあなたの例に固執し、すべてのケースクラスが同じフィールドを持っていると仮定します(githubリンクを見ると、これは実際のコードにも当てはまるようです)。

すべてのケースクラスが同じフィールドを持っているとすると、自動生成されcopyたメソッドは同じシグネチャを持つことになります。これは良いスタートです。その場合、あなたが行ったように、に共通の定義を追加するのが合理的だと思わBaseれます 。問題は、基本クラスにすでにメソッドが存在するためabstract class Base{ def copy(myparam: String):Base } 、scalaがメソッドを生成しないことです。copy

Baseが正しい方法であることを静的に確認する別の方法があることがわかりましたcopy。それは、構造型と自己型の注釈を使用することです。

type Copyable = { def copy(myParam: String): Base }
sealed abstract class Base(val myParam: String) { this : Copyable => }

そして、以前の試みとは異なり、これはscalaがcopyメソッドを自動生成することを妨げることはありません。最後の問題が1つあります。自己型アノテーションは、のサブクラスにメソッドBaseがあることを確認しcopyますが、それを公に利用可能にするわけではありませんBase

val foo: Base = Foo("hello")
foo.copy()
scala> error: value copy is not a member of Base

これを回避するために、BaseからCopyableへの暗黙の変換を追加できます。ベースはコピー可能であることが保証されているため、単純なキャストで十分です。

implicit def toCopyable( base: Base ): Base with Copyable = base.asInstanceOf[Base with Copyable]

まとめると、これにより次のことがわかります。

object Base {
  type Copyable = { def copy(myParam: String): Base }
  implicit def toCopyable( base: Base ): Base with Copyable = base.asInstanceOf[Base with Copyable]
}
sealed abstract class Base(val myParam: String) { this : Base. Copyable => }

case class Foo(override val myParam: String) extends Base( myParam )
case class Bar(override val myParam: String) extends Base( myParam )

def getIt( a:Base ) = a.copy(myParam="changed")

ボーナス効果:異なるシグネチャでケースクラスを定義しようとすると、コンパイルエラーが発生します:

case class Baz(override val myParam: String, truc: Int) extends Base( myParam ) 
scala> error: illegal inheritance; self-type Baz does not conform to Base's selftype Base with Base.Copyable

最後に、1つの警告:上記のトリックに頼る必要がないように、おそらくデザインを修正する必要があります。etypeあなたの場合、追加のフィールドを持つ単一のケースクラスを使用するというronの提案は、合理的すぎるように思われます。

于 2012-09-20T21:19:17.433 に答える
4

これが拡張メソッドの目的だと思います。コピーメソッド自体の実装戦略を選択してください。

私はここで問題が一箇所で解決されるのが好きです。

ケースネスの特性がない理由を尋ねるのは興味深いことです。コピーを呼び出す方法については、引数なしで常に呼び出すことができることを除いて、あまり説明しませんcopy()

sealed trait Base { def p1: String }

case class Foo(val p1: String) extends Base
case class Bar(val p1: String, p2: String) extends Base
case class Rab(val p2: String, p1: String) extends Base
case class Baz(val p1: String)(val p3: String = p1.reverse) extends Base

object CopyCase extends App {

  implicit class Copy(val b: Base) extends AnyVal {
    def copy(p1: String): Base = b match {
      case foo: Foo => foo.copy(p1 = p1)
      case bar: Bar => bar.copy(p1 = p1)
      case rab: Rab => rab.copy(p1 = p1)
      case baz: Baz => baz.copy(p1 = p1)(p1.reverse)
    }
    //def copy(p1: String): Base = reflect invoke
    //def copy(p1: String): Base = macro xcopy
  }

  val f = Foo("param1")
  val g = f.copy(p1="param2") // normal
  val h: Base = Bar("A", "B")
  val j = h.copy("basic")     // enhanced
  println(List(f,g,h,j) mkString ", ")

  val bs = List(Foo("param1"), Bar("A","B"), Rab("A","B"), Baz("param3")())
  val vs = bs map (b => b copy (p1 = b.p1 * 2))
  println(vs)
}

楽しみのためだけに、反射コピー:

  // finger exercise in the api
  def copy(p1: String): Base = {
    import scala.reflect.runtime.{ currentMirror => cm }
    import scala.reflect.runtime.universe._
    val im = cm.reflect(b)
    val ts = im.symbol.typeSignature
    val copySym = ts.member(newTermName("copy")).asMethod
    def element(p: Symbol): Any = (im reflectMethod ts.member(p.name).asMethod)()
    val args = for (ps <- copySym.params; p <- ps) yield {
      if (p.name.toString == "p1") p1 else element(p)
    }
    (im reflectMethod copySym)(args: _*).asInstanceOf[Base]
  }
于 2012-09-25T06:51:35.457 に答える
2

これは私にとってはうまくいきます:

sealed abstract class Base { def copy(myparam: String): Base }

case class Foo(myparam:String) extends Base {
  override def copy(x: String = myparam) = Foo(x)
}

def copyBase(x: Base) = x.copy("changed")

copyBase(Foo("abc")) //Foo(changed)
于 2012-09-11T18:56:38.610 に答える
2

http://www.cakesolutions.net/teamblogs/copying-sealed-trait-instances-a-journey-through-generic-programming-and-shapelessに、 shapelessを使用してこれを行う方法の非常に包括的な説明があります。リンクが壊れた場合、このアプローチでは、shapelessのcopySyntaxユーティリティを使用します。これは、詳細を見つけるのに十分なはずです。

于 2016-04-08T18:21:20.683 に答える
1

その古い問題、古い解決策、

https://code.google.com/p/scala-scales/wiki/VirtualConstructorPreSIP

ケースクラスのコピーメソッドが存在する前に作成されました。

したがって、この問題に関しては、各ケースクラスはとにかくリーフノードでなければならないので、コピーとMyType / thisTypeに加えてnewThis関数を定義すると、各ケースクラスがタイプを修正します。tree / newThis関数を広げてデフォルトのパラメータを使用する場合は、名前を変更する必要があります。

余談ですが、これを実装する前にコンパイラプラグインの魔法が改善されるのを待っていましたが、タイプマクロが魔法のジュースかもしれません。リストでKevinのAutoProxyを検索して、私のコードがどこにも行かなかった理由の詳細な説明を確認してください

于 2012-09-22T21:58:45.627 に答える