1

如何在 Scala 中编写自定义整数集?具体来说,我想要一个具有以下属性的类:

  1. 它是不可变的。
  2. 它扩展了Set特征。
  3. 所有收集操作都将根据需要返回此类型的另一个对象。
  4. 它在其构造函数中采用整数参数的变量列表。
  5. 它的字符串表示形式是用大括号括起来的以逗号分隔的元素列表。
  6. 它定义了一个mean返回元素平均值的方法。

例如:

CustomIntSet(1,2,3) & CustomIntSet(2,3,4) // returns CustomIntSet(2, 3)
CustomIntSet(1,2,3).toString // returns {1, 2, 3}
CustomIntSet(2,3).mean // returns 2.5

(1) 和 (2) 确保该对象以正确的 Scala 方式执行操作。(3) 要求正确编写builder代码。(4)确保构造函数可以自定义。(5) 是如何覆盖现有toString实现的示例。(6) 是如何添加新功能的示例。

这应该使用最少的源代码和样板来完成,尽可能利用 Scala 语言中已经存在的功能。

我已经就任务的各个方面提出了几个 问题,但我认为这个问题涵盖了整个问题。到目前为止,我得到的最佳响应SetProxy是使用,这很有帮助,但在上面的 (3) 中失败了。我已经广泛研究了《Scala编程》第二版中的“Scala 集合的体系结构”一章,并查阅了各种在线示例,但仍然一头雾水。

我这样做的目的是写一篇博文,比较 Scala 和 Java 处理这个问题的方式的设计权衡,但在我这样做之前,我必须实际编写 Scala 代码。我不认为这会那么困难,但确实如此,而且我承认失败。


经过几天的折腾,我想出了以下解决方案。

package example

import scala.collection.{SetLike, mutable}
import scala.collection.immutable.HashSet
import scala.collection.generic.CanBuildFrom

case class CustomSet(self: Set[Int] = new HashSet[Int].empty) extends Set[Int] with SetLike[Int, CustomSet] {
  lazy val mean: Float = sum / size

  override def toString() = mkString("{", ",", "}")

  protected[this] override def newBuilder = CustomSet.newBuilder

  override def empty = CustomSet.empty

  def contains(elem: Int) = self.contains(elem)

  def +(elem: Int) = CustomSet(self + elem)

  def -(elem: Int) = CustomSet(self - elem)

  def iterator = self.iterator
}

object CustomSet {
  def apply(values: Int*): CustomSet = new CustomSet ++ values

  def empty = new CustomSet

  def newBuilder: mutable.Builder[Int, CustomSet] = new mutable.SetBuilder[Int, CustomSet](empty)

  implicit def canBuildFrom: CanBuildFrom[CustomSet, Int, CustomSet] = new CanBuildFrom[CustomSet, Int, CustomSet] {
    def apply(from: CustomSet) = newBuilder

    def apply() = newBuilder
  }

  def main(args: Array[String]) {
    val s = CustomSet(2, 3, 5, 7) & CustomSet(5, 7, 11, 13)
    println(s + " has mean " + s.mean)
  }
}

这似乎符合上述所有标准,但它有很多样板。我发现以下 Java 版本更容易理解。

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;

public class CustomSet extends HashSet<Integer> {
    public CustomSet(Integer... elements) {
        Collections.addAll(this, elements);
    }

    public float mean() {
        int s = 0;
        for (int i : this)
            s += i;
        return (float) s / size();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (Iterator<Integer> i = iterator(); i.hasNext(); ) {
            sb.append(i.next());
            if (i.hasNext())
                sb.append(", ");
        }
        return "{" + sb + "}";
    }

    public static void main(String[] args) {
        CustomSet s1 = new CustomSet(2, 3, 5, 7, 11);
        CustomSet s2 = new CustomSet(5, 7, 11, 13, 17);

        s1.retainAll(s2);

        System.out.println("The intersection " + s1 + " has mean " + s1.mean());
    }
}

这很糟糕,因为 Scala 的卖点之一是它比 Java 更简洁明了。

Scala 版本中有很多不透明的代码。SetLike, newBuilder, 和canBuildFrom都是语言样板:它们与用花括号编写集合或取平均值无关。我几乎可以接受它们作为您为 Scala 的不可变集合类库支付的价格(目前接受不可变作为一种不合格的商品),但是仍然留下contains, +, -, 并且iterator它们只是样板传递代码。它们至少和 getter 和 setter 函数一样丑陋。

似乎 Scala 应该提供一种不编写Set接口样板的方法,但我想不通。我尝试使用SetProxy和扩展具体HashSet类而不是抽象类,Set但这两者都给出了令人困惑的编译器错误。

有没有办法在没有contains, +,-iterator定义的情况下编写这段代码,或者上面是我能做的最好的吗?


按照下面axel22的建议,我编写了一个简单的实现,它利用了非常有用的如果不幸命名为 pimp 我的库模式。

package example

class CustomSet(s: Set[Int]) {
  lazy val mean: Float = s.sum / s.size
}

object CustomSet {
  implicit def setToCustomSet(s: Set[Int]) = new CustomSet(s)
}

有了这个,您只需实例化Sets 而不是CustomSets 并根据需要进行隐式转换以获得平均值。

scala> (Set(1,2,3) & Set(2,3,5)).mean
res4: Float = 2.0

这满足了我最初的愿望清单中的大部分内容,但仍然未能通过第 (5) 项。


axel22在下面的评论中所说的话是我问这个问题的核心。

至于继承,不可变(集合)类一般都不容易继承……</p>

这符合我的经验,但从语言设计的角度来看,这里似乎有些不对劲。Scala 是一种面向对象的语言。(当我去年看到 Martin Odersky 发表演讲时,这是他强调的 Haskell 的卖点。)不变性是明确首选的操作模式。Scala 的集合类被吹捧为其对象库的皇冠上的明珠。然而,当你想扩展一个不可变的集合类时,你会遇到所有这些非正式的传说,大意是“不要那样做”或“除非你真的知道自己在做什么,否则不要尝试”。通常类的目的是使它们易于扩展。(毕竟,集合类没有标记final。)我'

4

1 回答 1

2

除了mean您可以使用隐式类和值类添加为扩展方法之外,您列出的所有属性都应该由immutable.BitSet标准库中的类支持。也许您可以在该实现中找到一些提示,特别是出于效率目的。

您编写了很多代码来实现上面的委托,但是您可以使用 Java 中的类继承来实现类似的事情——请注意,编写自定义集的委托版本也需要更多的 Java 样板。

也许宏将来会允许您编写自动生成委托样板的代码——在此之前,有旧的AutoProxy编译器插件。

于 2013-04-21T17:54:05.773 回答