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

发布于 2024-10-22 01:05:23 字数 409 浏览 4 评论 0原文

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

  1. 使用其成员已导入的包。例如,这就是访问 scala.collection.mutable.ListBuffer 的方式。这种技术在 Java、Python 等中很常见。

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

我猜 (2) 的优点之一是它组织了方法和类型,但根据 Scala 2.8 的包对象,使用 (1) 也可以完成同样的事情。为什么要采用这两种策略?每种应该什么时候使用?

I've noticed that the Scala standard library uses two different strategies for organizing classes, traits, and singleton objects.

  1. Using packages whose members are them imported. This is, for example, how you get access to scala.collection.mutable.ListBuffer. This technique is familiar coming from Java, Python, etc.

  2. Using type members of traits. This is, for example, how you get access to the Parser type. You first need to mix in scala.util.parsing.combinator.Parsers. This technique is not familiar coming from Java, Python, etc, and isn't much used in third-party libraries.

I guess one advantage of (2) is that it organizes both methods and types, but in light of Scala 2.8's package objects the same can be done using (1). Why have both these strategies? When should each be used?

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

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

发布评论

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

评论(2

我乃一代侩神 2024-10-29 01:05:23

这里值得注意的命名法是路径相关类型。这就是你所说的选项 2,我只谈它。除非您碰巧用它解决了问题,否则您应该始终采用选项 1。

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

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

定义了 Input 类型像这样:

type Input = Reader[Elem]

Elem 是抽象的。例如,考虑一下 RegexParsersTokenParsers。前者将Elem定义为Char,后者将其定义为Token。这意味着每个的 Parser 是不同的。更重要的是,由于 ParserParsers 的子类,Scala 编译器将确保在编译时您不会传递 RegexParsersParserTokenParsers 或反之亦然。事实上,您甚至无法将 RegexParsers 的一个实例的 Parser 传递给它的另一个实例。

The nomenclature of note here is path-dependent types. That's the option number 2 you talk of, and I'll speak only of it. Unless you happen to have a problem solved by it, you should always take option number 1.

What you miss is that the Parser class makes reference to things defined in the Parsers class. In fact, the Parser class itself depends on what input has been defined on Parsers:

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

The type Input is defined like this:

type Input = Reader[Elem]

And Elem is abstract. Consider, for instance, RegexParsers and TokenParsers. The former defines Elem as Char, while the latter defines it as Token. That means the Parser for the each is different. More importantly, because Parser is a subclass of Parsers, the Scala compiler will make sure at compile time you aren't passing the RegexParsers's Parser to TokenParsers or vice versa. As a matter of fact, you won't even be able to pass the Parser of one instance of RegexParsers to another instance of it.

远昼 2024-10-29 01:05:23

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

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. 特征内定义的所有类都将引用外部类。这可能是一个涉及性能的问题,或者当您使用序列化时(当外部类不可序列化时,或更糟糕的是,如果可以序列化,但您不希望它被序列化)。
  2. 如果您的“模块”变得非常大,您将要么拥有非常大的特征和非常大的源文件,要么必须将模块特征代码分布在多个文件中。这可能会导致一些样板文件。
  3. 它可能迫使您必须使用此范例来编写整个应用程序。在您知道之前,每个类都必须混合其要求。
  4. 具体实现必须在编译时知道,除非您使用某种手写委托。您不能根据运行时可用的值动态地混合实现特征。

我猜库设计者并没有将上述任何问题视为解析器所关心的问题。

The second is also known as the Cake pattern.
It has the benefit that the code inside the class that has a trait mixed in becomes independent of the particular implementation of the methods and types in that trait. It allows to use the members of the trait without knowing what's their concrete implementation.

trait Logging {
  def log(msg: String)
}

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

Above, the Logging trait is the requirement for the App (requirements can also be expressed with self-types). Then, at some point in your application you can decide what the implementation will be and mix the implementation trait into the concrete class.

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

object MyApp extends App with ConsoleLogging

This has an advantage over imports, in the sense that the requirements of your piece of code aren't bound to the implementation defined by the import statement. Furthermore, it allows you to build and distribute an API which can be used in a different build somewhere else provided that its requirements are met by mixing in a concrete implementation.

However, there are a few things to be careful with when using this pattern.

  1. All of the classes defined inside the trait will have a reference to the outer class. This can be an issue where performance is concerned, or when you're using serialization (when the outer class is not serializable, or worse, if it is, but you don't want it to be serialized).
  2. If your 'module' gets really large, you will either have a very big trait and a very big source file, or will have to distribute the module trait code across several files. This can lead to some boilerplate.
  3. It can force you to have to write your entire application using this paradigm. Before you know it, every class will have to have its requirements mixed in.
  4. The concrete implementation must be known at compile time, unless you use some sort of hand-written delegation. You cannot mix in an implementation trait dynamically based on a value available at runtime.

I guess the library designers didn't regard any of the above as an issue where Parsers are concerned.

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