【问题标题】:Scala Inheritance; Builder Trouble; Non-Generic IterableLike斯卡拉继承;建设者麻烦;非泛型 IterableLike
【发布时间】:2013-01-26 17:04:02
【问题描述】:

我试图实现一个简单的设计目标,但是 Scala 类型系统的复杂性让我有些头疼。在比较了 Traversable、Iterator、Iterable、Stream、View 等之后,我决定定义一个自定义 trait(为了简洁起见,我们称之为 Stream

  • 是非泛型的(我的流在语义上只对Stream[StreamEntry] 有意义,我想避免像Stream[Int] 这样无意义的类型)
  • Iterable 的用法相似
  • takedrop 等所有成员都应返回Stream,而不是基本的Iterable

这是我迄今为止尝试过的:

方法 1

为了勾勒用例,一个简单的例子(违反了第三个设计目标)是:

case class StreamEntry(data: Double) // just a dummy

trait Stream extends Iterable[StreamEntry] {
  val metaInfo: String
}

// example use case
val s = new Stream {
  val metaInfo = "something"
  val iterator = StreamEntry(1) :: StreamEntry(2) :: StreamEntry(3) :: Nil toIterator
}

val t = s.take(1) // unfortunately, this is no longer a Stream

方法2

这第三个要求要求使用 template trait 而不是 base trait(我希望这是引用 SomeCollection 或 SomeCollectionLike)。这意味着我必须使用IterableLike[StreamEntry, Stream] 重新定义表示集合的返回类型,就像Iterable 扩展IterableLike[A, Iterable[A]] 以返回Iterables。我的想法是和Iterable 做的差不多。这将是:

// this is exactly the way `Iterable` is defined, but non-generic
trait Stream extends Traversable[StreamEntry]
             with GenIterable[StreamEntry]
             with GenericTraversableTemplate[StreamEntry, Stream]
             with IterableLike[StreamEntry, Stream] {
  ...
}

不幸的是,这无法编译,因为Stream 显示为GenericTraversableTemplate 的模板参数,而编译器现在需要Stream 本身的模板参数(正好是一个),这是有道理的。

方法 3、4、...

从这里开始,我迷失在类型系统中。仅删除with GenericTraversableTemplate 会导致newBuilder 的类型不兼容,并且由于GenericTraversableTemplate 中的类型参数与GenInterableTraversable 的冲突而导致非法继承。

也许最接近的解决方案如下:

trait Stream extends TraversableLike[StreamEntry, Stream] 
             with IterableLike[StreamEntry, Stream] {
  val metaInfo: String
  def seq = this
  def newBuilder: scala.collection.mutable.Builder[StreamEntry, Stream] = ???
}

这可以编译,但不幸的是我不知道如何实现 Builder。是否可以为我的非通用特征重用通用生成器?实际上,我虽然可以不使用 Builder,因为我真的不想从其他集合中构建新的 Stream。但是目前我在使用这种方法时遇到了一些奇怪的运行时行为,我无法完全理解。例如:

val s = new Stream {
  val metaInfo = "something"
  val iterator = StreamEntry(1) :: StreamEntry(2) :: StreamEntry(3) :: Nil toIterator
}

// executing the following independently (not in sequence) results in:

s.take(1)    // throws: scala.NotImplementedError: an implementation is missing
             // seems to require a Builder :(
s.toArray    // works
s.toIterator // works
s.toIterable // throws: java.lang.ClassCastException: cannot be cast to scala.collection.Iterable

现在我对 Scala 类型系统的深度感到有些迷茫。我是否仍然在使用最后一种方法的正确轨道上,而 Builder 是否只是这个难题中缺失的部分?

对于非泛型非缓存类型,Builder 实现会是什么样子?实现+= 的一个简单想法是使用一些可变缓冲区,但这首先会非常反对使用迭代器......如果我不知道如何实现to 成员构造一个那种类型的类?我想所有相关的代码一定在图书馆的某个地方,我就是挖不出来。

【问题讨论】:

  • 您应该知道Stream 是一个内置类型,因此不应在您自己的代码中重新定义该名称。它会让那些更熟悉 Scala 标准库的人感到困惑。
  • 我知道并且我正在考虑将其重命名为 MyStream 以发布此帖子,但我同样清楚地提到我将在下面将我的自定义特征称为 Stream。跨度>
  • 你应该看看标准库中StringOps和WrappedString的实现。
  • @Ptharien'sFlame:我确实对此进行了调查,但如果我在这些情况下使用类似于底层构建器的构建器(一个高度可变的 Java StringBuilder),我最终会得到我没有的想要:调用例如'drop'会将整个流的其余部分存储在内存中。我现在的感觉是,这只能通过非基于构建器的集合来解决,这意味着 Iterable 不是要走的路。

标签: scala inheritance iterator builder


【解决方案1】:

哇!你有很多事情要做......

在解决此设计问题时,您应该了解或考虑以下事项...

术语:

  • 我们不引用“模板”,我们称它们为“通用”或“参数化”类型。原因是这些类型不是模板!也就是说,每次使用它们时,它们都没有填充它们的实际类型参数来创建新类(就像 C++ 中的情况一样,它正确地使用了术语“模板”)。相反,只创建一个类 (*),它使用特定类型参数为该泛型类型的每个实例提供服务。

设计和语言因素:

你说:

… 是非泛型的(我的流在语义上只对某些 Stream[StreamEntry] 有意义,我想避免像 Stream[Int] 这样的无意义类型)

要求主张非泛型类。相反,它是“类型绑定”的本质。例如:

class Generic[T <: UpperBound](ctorArgs...) {
}

在这种情况下,类Generic 只能使用UpperBound 的子类型来实例化。 (请注意,每当我们说“子类型”时,我们的意思是自反子类型关系。换句话说,在此定义下,每个类型都是其自身的子类型。

结果:

我想知道您的“流”类是什么、做什么或有什么不满足 Scala 标准库中现有类型的要求?正如您所发现的那样,扩展标准库集合类并不是一件容易的事,尽管它确实是可行的。我认为这样做不是 Scala 编程的基本练习,可能不应该将其作为您首次涉足 Scala 的尝试之一。

(*) 这是一种过度简化,足以满足本说明的目的。

【讨论】:

  • +1 这些优点。关于术语:我只是依赖于 scaladoc 公式中的主要区别“Iterable is a base trait for ...”与“IterableLike is a template trait for ...”,但你是对的“参数化类型”也很多更符合我的想法。您的设计提示也是正确的。另一方面,“保持简单”的解决方案也有它的优势(根本不必考虑类型参数;引入适当的类型界限更像是另一个极端)。令人惊讶的是,非泛型版本实际上更复杂......
  • 老实说:我的设计目标部分是教育性的 :)。在使用 Scala 1.5 年后,真正学习 Scala 可能是一件事......而且可能永远避免扩展标准库无助于实现这一目标:)。
  • 我的流的作用:不多。它主要包含有关流的元信息。原则上,可以单独存储此信息并依赖于例如Iterable[StreamEntry] 进行迭代。但是,如果元数据在应用了'take','drop'等之后直接在流中可用,那会更方便。此外,一些操作可能会改变元信息。如果单独存储,这样的更改是不方便的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-19
  • 1970-01-01
相关资源
最近更新 更多