4

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?

4

2 に答える 2

5

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.

于 2011-03-16T23:06:40.637 に答える
4

2番目はケーキパターンとしても知られています。トレイトが混在しているクラス内のコードが、そのトレイトのメソッドとタイプの特定の実装から独立するという利点があります。これにより、具体的な実装が何であるかを知らなくても、トレイトのメンバーを使用できます。

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をビルドして配布できます。

ただし、このパターンを使用する際には注意が必要なことがいくつかあります。

  1. トレイト内で定義されたすべてのクラスには、外部クラスへの参照があります。これは、パフォーマンスが懸念される場合、またはシリアル化を使用している場合(外部クラスがシリアル化できない場合、またはさらに悪い場合はシリアル化されたくない場合)に問題になる可能性があります。
  2. 'モジュール'が非常に大きくなると、非常に大きな特性と非常に大きなソースファイルが存在するか、モジュール特性コードを複数のファイルに分散する必要があります。これは、ボイラープレートにつながる可能性があります。
  3. このパラダイムを使用してアプリケーション全体を作成する必要が生じる可能性があります。あなたがそれを知る前に、すべてのクラスはその要件を混ぜ合わせる必要があります。
  4. ある種の手書きの委任を使用しない限り、具体的な実装はコンパイル時に認識されている必要があります。実行時に使用可能な値に基づいて、実装特性を動的に混在させることはできません。

Parsers図書館の設計者は、上記のいずれも懸念事項とは見なしていなかったと思います。

于 2011-03-16T17:15:05.997 に答える