Scala 中用于具有继承返回类型的集合的最小框架

发布于 2024-09-05 03:35:36 字数 1187 浏览 7 评论 0原文

假设有人想要构建一个新颖的泛型类 Novel[A]。该类将包含许多有用的方法——也许它是一种集合类型——因此您希望对其进行子类化。但您希望这些方法返回子类的类型,而不是原始类型。在 Scala 2.8 中,要使该类的方法返回相关子类而不是原始子类,需要做的最少工作量是多少?例如,

class Novel[A] /* What goes here? */ {
  /* Must you have stuff here? */
  def reverse/* What goes here instead of :Novel[A]? */ = //...
  def revrev/*?*/ = reverse.reverse
}
class ShortStory[A] extends Novel[A] /* What goes here? */ {
  override def reverse: /*?*/ = //...
}
val ss = new ShortStory[String]
val ss2 = ss.revrev  // Type had better be ShortStory[String], not Novel[String]

如果您希望 Novel 具有协变性,那么这个最小量会改变吗?

(2.8 集合除其他外,还执行此操作,但它们还以更奇特(且有用)的方式处理返回类型 - 问题是,如果只想要这种子类型 - 始终返回子类型,那么可以摆脱的框架有多么少功能。)

编辑:假设在上面的代码中,reverse 进行了复制。如果进行就地修改然后返回自己,则可以使用 this.type,但这不起作用,因为副本不是 this

Arjan 链接到另一个问题,该问题提出了以下解决方案:

def reverse: this.type = {
  /*creation of new object*/.asInstanceOf[this.type]
}

这基本上取决于类型系统以获得我们想要的东西。但这并不是真正的解决方案,因为现在我们对类型系统撒了谎,编译器无法帮助我们确保我们确实确实得到一个ShortStory 回到我们认为我们可以做到的时候。 (例如,我们不必在上面的示例中重写 reverse 来让编译器满意,但我们的类型并不是我们想要的。)

Suppose one wants to build a novel generic class, Novel[A]. This class will contain lots of useful methods--perhaps it is a type of collection--and therefore you want to subclass it. But you want the methods to return the type of the subclass, not the original type. In Scala 2.8, what is the minimal amount of work one has to do so that methods of that class will return the relevant subclass, not the original? For example,

class Novel[A] /* What goes here? */ {
  /* Must you have stuff here? */
  def reverse/* What goes here instead of :Novel[A]? */ = //...
  def revrev/*?*/ = reverse.reverse
}
class ShortStory[A] extends Novel[A] /* What goes here? */ {
  override def reverse: /*?*/ = //...
}
val ss = new ShortStory[String]
val ss2 = ss.revrev  // Type had better be ShortStory[String], not Novel[String]

Does this minimal amount change if you want Novel to be covariant?

(The 2.8 collections do this among other things, but they also play with return types in more fancy (and useful) ways--the question is how little framework one can get away with if one only wants this subtypes-always-return-subtypes feature.)

Edit: Assume in the code above that reverse makes a copy. If one does in-place modification and then returns oneself, one can use this.type, but that doesn't work because the copy is not this.

Arjan linked to another question that suggests the following solution:

def reverse: this.type = {
  /*creation of new object*/.asInstanceOf[this.type]
}

which basically lies to the type system in order to get what we want. But this isn't really a solution, because now that we've lied to the type system, the compiler can't help us make sure that we really do get a ShortStory back when we think we do. (For example, we wouldn't have to override reverse in the example above to make the compiler happy, but our types wouldn't be what we wanted.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

星光不落少年眉 2024-09-12 03:35:36

编辑:我刚刚意识到雷克斯在他的例子中有一个具体的类小说,而不是我在下面使用的特征。因此,trait 的实现有点太简单了,无法解决 Rex 的问题。它也可以使用具体的类来完成(见下文),但我可以实现该工作的唯一方法是通过一些转换,这使得这并不是真正的“编译时类型安全”。这所以这不符合解决方案。

也许不是最漂亮的,但使用抽象成员类型的简单示例可以实现如下:


trait Novel[A] { 
   type T <: Novel[A] 
   def reverse : T 
   def revrev : T#T = reverse.reverse 
}

class ShortStory[A](var story: String) extends Novel[A] {
 type T = ShortStory[A]
 def reverse : T = new ShortStory[A](story reverse)
 def myMethod: Unit = println("a short story method")
}

scala> val ss1 = new ShortStory[String]("the story so far")
ss1: ShortStory[String] = ShortStory@5debf305

scala> val ssRev = ss1 reverse 
ssRev: ss1.T = ShortStory@5ae9581b

scala> ssRev story
res0: String = raf os yrots eht

scala> val ssRevRev = ss1 revrev
ssRevRev: ss1.T#T = ShortStory@2429de03

scala> ssRevRev story
res1: String = the story so far

scala> ssRevRev myMethod
a short story method

它当然是最小的,但我怀疑这是否足以用作一种框架。当然,返回的类型并不像 Scala 集合框架中那样清晰,所以这可能有点太简单了。 对于给定的情况,它似乎可以完成这项工作。 如上所述,这对于给定的情况不起作用,因此这里需要一些其他解决方案。

另一个编辑:也可以使用具体类来完成类似的操作,尽管这也不足以保证类型安全:


class Novel[A](var story: String) {
  type T <: Novel[A] 
  def reverse: T = new Novel[A](story reverse).asInstanceOf[T]  
  def revrev : T#T = reverse.reverse
}
class ShortStory[A](var s: String) extends Novel[A](s) {
 type T = ShortStory[A]
 override def reverse : T = new ShortStory(story reverse)
 def myMethod: Unit = println("a short story method")
}

并且代码将像特征示例中那样工作。但它也遇到了雷克斯在他的编辑中提到的同样的问题。编译时不需要覆盖 ShortStory。但是,如果您不这样做并在 ShortStory 实例上调用反向方法,它将在运行时失败。

Edit: I just realized that Rex had a concrete class Novel in his example, not a trait as I've used below. The trait implementation is a bit too simple to be a solution to Rex's question, therefore. It can be done as well using a concrete class (see below), but the only way I could make that work is by some casting, which makes this not really 'compile time type-safe'. This So this does not qualify as a solution.

Perhaps not the prettiest, but a simple example using abstract member types could be implemented as follows:


trait Novel[A] { 
   type T <: Novel[A] 
   def reverse : T 
   def revrev : T#T = reverse.reverse 
}

class ShortStory[A](var story: String) extends Novel[A] {
 type T = ShortStory[A]
 def reverse : T = new ShortStory[A](story reverse)
 def myMethod: Unit = println("a short story method")
}

scala> val ss1 = new ShortStory[String]("the story so far")
ss1: ShortStory[String] = ShortStory@5debf305

scala> val ssRev = ss1 reverse 
ssRev: ss1.T = ShortStory@5ae9581b

scala> ssRev story
res0: String = raf os yrots eht

scala> val ssRevRev = ss1 revrev
ssRevRev: ss1.T#T = ShortStory@2429de03

scala> ssRevRev story
res1: String = the story so far

scala> ssRevRev myMethod
a short story method

It's certainly minimal, but I doubt whether this would enough to be used as a kind of framework. And of course the types returned not anywhere near as clear as in the Scala collections framework, so perhaps this might be a bit too simple. For the given case, it seems to do the job, however. As remarked above, this does not do the job for the given case, so some other solution is required here.

Yet Another Edit: Something similar can be done using a concrete class as well, though that also not suffices to be type safe:


class Novel[A](var story: String) {
  type T <: Novel[A] 
  def reverse: T = new Novel[A](story reverse).asInstanceOf[T]  
  def revrev : T#T = reverse.reverse
}
class ShortStory[A](var s: String) extends Novel[A](s) {
 type T = ShortStory[A]
 override def reverse : T = new ShortStory(story reverse)
 def myMethod: Unit = println("a short story method")
}

And the code will work as in the trait example. But it suffers from the same problem as Rex mentioned in his edit as well. The override on ShortStory is not necessary to make this compile. However, it will fail at runtime if you don't do this and call the reverse method on a ShortStory instance.

无人问我粥可暖 2024-09-12 03:35:36

我还没有完全考虑到这一点,但它进行了类型检查:

object invariant {
  trait Novel[A] {
    type Repr[X] <: Novel[X]

    def reverse: Repr[A]

    def revrev: Repr[A]#Repr[A]
       = reverse.reverse
  }
  class ShortStory[A] extends Novel[A] {
    type Repr[X] = ShortStory[X]

    def reverse = this
  }

  val ss = new ShortStory[String]
  val ss2: ShortStory[String] = ss.revrev
}

object covariant {
  trait Novel[+A] {
    type Repr[X] <: Novel[_ <: X]

    def reverse: Repr[_ <: A]

    def revrev: Repr[_ <: A]#Repr[_ <: A] = reverse.reverse
  }

  class ShortStory[+A] extends Novel[A] {
    type Repr[X] = ShortStory[X]

    def reverse = this
  }

  val ss = new ShortStory[String]
  val ss2: ShortStory[String] = ss.revrev
}

编辑

协变版本可以更好:

object covariant2 {
  trait Novel[+A] {
    type Repr[+X] <: Novel[X]

    def reverse: Repr[A]

    def revrev: Repr[A]#Repr[A] = reverse.reverse
  }

  class ShortStory[+A] extends Novel[A] {
    type Repr[+X] = ShortStory[X]

    def reverse = this
  }

  val ss = new ShortStory[String]
  val ss2: ShortStory[String] = ss.revrev
}

I haven't thought this through fully, but it type checks:

object invariant {
  trait Novel[A] {
    type Repr[X] <: Novel[X]

    def reverse: Repr[A]

    def revrev: Repr[A]#Repr[A]
       = reverse.reverse
  }
  class ShortStory[A] extends Novel[A] {
    type Repr[X] = ShortStory[X]

    def reverse = this
  }

  val ss = new ShortStory[String]
  val ss2: ShortStory[String] = ss.revrev
}

object covariant {
  trait Novel[+A] {
    type Repr[X] <: Novel[_ <: X]

    def reverse: Repr[_ <: A]

    def revrev: Repr[_ <: A]#Repr[_ <: A] = reverse.reverse
  }

  class ShortStory[+A] extends Novel[A] {
    type Repr[X] = ShortStory[X]

    def reverse = this
  }

  val ss = new ShortStory[String]
  val ss2: ShortStory[String] = ss.revrev
}

EDIT

The co-variant version can be much nicer:

object covariant2 {
  trait Novel[+A] {
    type Repr[+X] <: Novel[X]

    def reverse: Repr[A]

    def revrev: Repr[A]#Repr[A] = reverse.reverse
  }

  class ShortStory[+A] extends Novel[A] {
    type Repr[+X] = ShortStory[X]

    def reverse = this
  }

  val ss = new ShortStory[String]
  val ss2: ShortStory[String] = ss.revrev
}
绝不服输 2024-09-12 03:35:36

经过对 Scala 邮件列表的讨论——非常感谢那里的人让我走上了正确的道路!——我认为这是最接近最小框架的。我把它留在这里供参考,我使用了一个不同的示例,因为它更好地突出了正在发生的事情:

abstract class Peano[A,MyType <: Peano[A,MyType]](a: A, f: A=>A) {
  self: MyType =>
  def newPeano(a: A, f: A=>A): MyType
  def succ: MyType = newPeano(f(a),f)
  def count(n: Int): MyType = {
    if (n<1) this
    else if (n==1) succ
    else count(n-1).succ
  }
  def value = a
}

abstract class Peano2[A,MyType <: Peano2[A,MyType]](a: A, f: A=>A, g: A=>A) extends Peano[A,MyType](a,f) {
  self: MyType =>
  def newPeano2(a: A, f: A=>A, g: A=>A): MyType
  def newPeano(a: A, f: A=>A): MyType = newPeano2(a,f,g)
  def pred: MyType = newPeano2(g(a),f,g)
  def uncount(n: Int): MyType = {
    if (n < 1) this
    else if (n==1) pred
    else uncount(n-1).pred
  }
}

这里的关键是添加 MyType 类型参数,它是类型的占位符我们最终将要上的课。每次继承时,我们都必须将其重新定义为类型参数,并且添加一个构造函数方法来创建该类型的新对象。如果构造函数发生变化,我们必须创建一个新的构造函数方法。

现在,当您想要创建一个实际使用的类时,您只需通过调用 new 来填充构造函数方法(并告诉该类它是它自己的类型):

class Peano2Impl[A](a: A, f: A=>A, g: A=>A) extends Peano2[A,Peano2Impl[A]](a,f,g) {
  def newPeano2(a: A, f: A=>A, g: A=>A) = new Peano2Impl[A](a,f,g)
}

您就可以开始运行了:

val p = new Peano2Impl(0L , (x:Long)=>x+1 , (y:Long)=>x-1)

scala> p.succ.value
res0: Long = 1

scala> p.pred.value
res1: Long = -1

scala> p.count(15).uncount(7).value
res2: Long = 8

然后 回顾一下,最小的样板文件——如果你想包含递归方法,这会破坏其他答案的风格——是任何从类外部返回新副本的方法(使用 new 或工厂或其他)保持抽象(在这里,我将所有内容都归结为一个复制构造函数的方法),并且您必须添加 MyType 类型注释,如图所示。然后,在最后一步,必须实例化这些新复制方法。

此策略也适用于 A 中的协方差,但此特定示例不起作用,因为 fg 不是协变的。

After discussions on the Scala mailing list--many thanks to the people there for setting me on the right track!--I think that this is the closest that one can come to a minimal framework. I leave it here for reference, and I'm using a different example because it highlights what is going on better:

abstract class Peano[A,MyType <: Peano[A,MyType]](a: A, f: A=>A) {
  self: MyType =>
  def newPeano(a: A, f: A=>A): MyType
  def succ: MyType = newPeano(f(a),f)
  def count(n: Int): MyType = {
    if (n<1) this
    else if (n==1) succ
    else count(n-1).succ
  }
  def value = a
}

abstract class Peano2[A,MyType <: Peano2[A,MyType]](a: A, f: A=>A, g: A=>A) extends Peano[A,MyType](a,f) {
  self: MyType =>
  def newPeano2(a: A, f: A=>A, g: A=>A): MyType
  def newPeano(a: A, f: A=>A): MyType = newPeano2(a,f,g)
  def pred: MyType = newPeano2(g(a),f,g)
  def uncount(n: Int): MyType = {
    if (n < 1) this
    else if (n==1) pred
    else uncount(n-1).pred
  }
}

The key here is the addition of the MyType type parameter that is a placeholder for the type of the class that we'll really end up with. Each time we inherit, we have to redefine it as a type parameter, and we have add a constructor method that will create a new object of this type. If the constructor changes, we have to create a new constructor method.

Now when you want to create a class to actually use, you only have to fill in the constructor method with a call to new (and tell the class that it's of its own type):

class Peano2Impl[A](a: A, f: A=>A, g: A=>A) extends Peano2[A,Peano2Impl[A]](a,f,g) {
  def newPeano2(a: A, f: A=>A, g: A=>A) = new Peano2Impl[A](a,f,g)
}

and you're off and running:

val p = new Peano2Impl(0L , (x:Long)=>x+1 , (y:Long)=>x-1)

scala> p.succ.value
res0: Long = 1

scala> p.pred.value
res1: Long = -1

scala> p.count(15).uncount(7).value
res2: Long = 8

So, to review, the minimal boilerplate--if you want to include recursive methods, which breaks the other style of answer--is for any methods that return a new copy from outside the class (using new or a factory or whatever) to be left abstract (here, I've boiled everything down to one method that duplicates the constructor), and you have to add the MyType type annotation as shown. Then, at the final step, these new-copy methods have to be instantiated.

This strategy works fine for covariance in A also, except that this particular example doesn't work since f and g are not covariant.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文