为什么Scala API有两种组织类型的策略?

时间:2011-03-16 16:40:07

标签: scala import packages traits

我注意到Scala标准库使用两种不同的策略来组织类,特征和单例对象。

  1. 使用其成员为导入的包。例如,这是您访问scala.collection.mutable.ListBuffer的方式。这种技术很熟悉来自Java,Python等。

  2. 使用特征的类型成员。例如,这是您访问Parser类型的方式。您首先需要混合scala.util.parsing.combinator.Parsers。这种技术不熟悉来自Java,Python等,并且在第三方库中使用的并不多。

  3. 我猜(2)的一个优点是它组织了方法和类型,但根据Scala 2.8的包对象,可以使用(1)完成相同的操作。为什么要有这两种策略?应该何时使用?

2 个答案:

答案 0 :(得分:5)

此处注释的术语是 path-dependent types 。那是你所说的2号选项,我只会说它。除非您碰巧遇到问题,否则您应该始终选择1号选项。

您错过的是Parser类引用了Parsers类中定义的内容。事实上,Parser类本身取决于input上定义的Parsers

abstract class Parser[+T] extends (Input => ParseResult[T])

类型Input的定义如下:

type Input = Reader[Elem]

Elem是抽象的。例如,考虑RegexParsersTokenParsers。前者将Elem定义为Char,而后者将其定义为Token。这意味着每个Parser都不同。更重要的是,因为ParserParsers的子类,Scala编译器将确保在编译时您没有将RegexParsers的{​​{1}}传递给{{1} }} 或相反亦然。事实上,您甚至无法将Parser的{​​{1}}个实例的TokenParsers传递给它的另一个实例。

答案 1 :(得分:4)

第二个也被称为Cake pattern。 它的好处是,具有混合特征的类中的代码变得独立于该特征中的方法和类型的特定实现。它允许使用特征的成员而不知道它们的具体实现是什么。

trait Logging {
  def log(msg: String)
}

trait App extends Logging {
  log("My app started.")
}

上面,Logging特征是App的要求(要求也可以用自我类型表示)。然后,在应用程序的某个时刻,您可以决定实现的内容,并将实现特性混合到具体的类中。

trait ConsoleLogging extends Logging {
  def log(msg: String) = println(msg)
}

object MyApp extends App with ConsoleLogging

这比导入更有优势,因为您的代码片段的要求不受import语句定义的实现的约束。此外,它允许您构建和分发一个API,该API可以在其他地方的其他构建中使用,前提是通过混合具体实现来满足其要求。

但是,使用此模式时需要注意一些事项。

  1. 在trait中定义的所有类都将引用外部类。这可能是性能问题,或者当您使用序列化时(当外部类不可序列化时,或者更糟糕的是,如果它是,但您不希望它被序列化)。
  2. 如果你的'模块'变得非常大,你将拥有一个非常大的特征和一个非常大的源文件,或者必须在几个文件中分发模块特征代码。这可能导致一些样板。
  3. 它可以强制您使用此范例编写整个应用程序。在您知道之前,每个班级都必须混合其中。
  4. 除非您使用某种手写委派,否则必须在编译时知道具体实现。您不能根据运行时可用的值动态混合实现特征。
  5. 我想图书馆设计师并不认为上述任何问题是Parsers所关注的问题。