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 都有一个“复制”方法,但复制也不是真正的方法,所以我认为没有特征或抽象方法我可以放入 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]
}

但是……这看起来很丑陋。

想法?我的整个方法只是“错误”吗?

更新我更改了基类以包含该属性,并使案例类使用“覆盖”关键字。考虑到 Edmondo1984 的响应,这更好地反映了实际问题,并使问题更加现实。

4

7 回答 7

28

This is old answer, before the question was changed.

Strongly typed programming languages prevent what you are trying to do. Let's see why.

The idea of a method with the following signature:

def getIt( a:Base ) : Unit

Is that the body of the method will be able to access a properties visible through Base class or interface, i.e. the properties and methods defined only on the Base class/interface or its parents. During code execution, each specific instance passed to the getIt method might have a different subclass but the compile type of a will always be Base

One can reason in this way:

Ok I have a class Base, I inherit it in two case classes and I add a property with the same name, and then I try to access the property on the instance of Base.

A simple example shows why this is unsafe:

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")

In the following case, if the compiler didn't throw an error at compile time, it means the code would try to access a property that does not exist at runtime. This is not possible in strictly typed programming languages: you have traded restrictions on the code you can write for a much stronger verification of your code by the compiler, knowing that this reduces dramatically the number of bugs your code can contain


This is the new answer. It is a little long because few points are needed before getting to the conclusion

Unluckily, you can't rely on the mechanism of case classes copy to implement what you propose. The way the copy method works is simply a copy constructor which you can implement yourself in a non-case class. Let's create a case class and disassemble it in the 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);
}

In Scala, the copy method takes three parameter and can eventually use the one from the current instance for the one you haven't specified ( the Scala language provides among its features default values for parameters in method calls)

Let's go down in our analysis and take again the code as updated:

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")

Now in order to make this compile, we would need to use in the signature of getIt(a:MyType) a MyType that respect the following contract:

Anything that has a parameter myparam and maybe other parameters which have default value

All these methods would be suitable:

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

There is no way to express this contract in Scala, however there are advanced techniques that can be helpful.

The first observation that we can do is that there is a strict relation between case classes and tuples in Scala. In fact case classes are somehow tuples with additional behaviour and named properties.

The second observation is that, since the number of properties of your classes hierarchy is not guaranteed to be the same, the copy method signature is not guaranteed to be the same.

In practice, supposing AnyTuple[Int] describes any Tuple of any size where the first value is of type Int, we are looking to do something like that:

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

This would not be to difficult if all the elements were Int. A tuple with all element of the same type is a List, and we know how to replace the first element of a List. We would need to convert any TupleX to List, replace the first element, and convert the List back to TupleX. Yes we will need to write all the converters for all the values that X might assume. Annoying but not difficult.

In our case though, not all the elements are Int. We want to treat Tuple where the elements are of different type as if they were all the same if the first element is an Int. This is called

"Abstracting over arity"

i.e. treating tuples of different size in a generic way, independently of their size. To do it, we need to convert them into a special list which supports heterogenous types, named HList


Conclusion

Case classes inheritance is deprecated for very good reason, as you can find out from multiple posts in the mailing list: http://www.scala-lang.org/node/3289

You have two strategies to deal with your problem:

  1. If you have a limited number of fields you require to change, use an approach such as the one suggested by @Ron, which is having a copy method. If you want to do it without losing type information, I would go for generifying the base class

    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. If you really want to abstract over arity, a solution is to use a library developed by Miles Sabin called Shapeless. There is a question here which has been asked after a discussion : Are HLists nothing more than a convoluted way of writing tuples? but I tell you this is going to give you some headache

于 2012-09-19T10:11:18.090 回答
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方法。最后一个问题:self-type 注释确保子类Base有一个copy方法,但它没有使它公开可用Base

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

为了解决这个问题,我们可以添加一个从 Base 到 Copyable 的隐式转换。一个简单的演员就可以了,因为 Base 保证是可复制的:

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

最后,一个警告:您可能应该只修改您的设计以避免不得不诉诸上述技巧。在您的情况下,ron 建议使用带有附加etype字段的单个案例类似乎非常合理。

于 2012-09-20T21:19:17.433 回答
14

如果这两个案例类别会随着时间的推移而分道扬镳,以至于它们具有不同的字段,那么共享copy方法将不再有效。

最好定义一个 abstract 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)
于 2012-09-11T13:13:30.267 回答
4

我认为这就是扩展方法的用途。为复制方法本身选择实施策略。

我喜欢这里的问题是在一个地方解决的。

有趣的是,为什么没有 caseness 的特征:它不会过多说明如何调用 copy,只是它总是可以在没有 args 的情况下调用,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 回答