web-dev-qa-db-ja.com

スーパークラスを使用したケースクラスコピー 'メソッド'

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

_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 Baseclass 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の応答を考慮して問題をより現実的にします。

26
nairbv

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

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

次の署名を持つメソッドのアイデア:

_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のメカニズムに依存することはできません。 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) a 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でこの契約を表現する方法はありませんが、役立つ高度なテクニックがあります。

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

2番目の観察は、クラス階層のプロパティの数が同じであることが保証されていないため、copyメソッドsignatureは同じであるとは限りません。

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

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

すべての要素がIntであれば、これはそれほど難しいことではありません。同じタイプのすべての要素を持つタプルはListであり、Listの最初の要素を置き換える方法を知っています。 TupleXListに変換し、最初の要素を置き換えてから、ListTupleXに戻す必要があります。はい、Xが想定する可能性のあるすべての値に対してすべてのコンバーターを作成する必要があります。迷惑ですが難しくはありません。

ただし、この場合、すべての要素がIntであるとは限りません。 Tupleを扱いたいのですが、最初の要素が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はタプルを書く複雑な方法にすぎませんか? しかし、これはあなたにいくつかの頭痛を与えるだろうと私はあなたに言います

24
Edmondo1984

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

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

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)
13
ron

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

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

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

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

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の提案は、合理的すぎるように思われます。

13

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

2
satyagraha

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

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

大文字と小文字を区別する特性がない理由を尋ねるのは興味深いことです。コピーを呼び出す方法については、引数なしで常に呼び出すことができることを除いて、あまり説明されていません。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]
  }
2
som-snytt

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

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)

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

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

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

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

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

1
Chris